This commit is contained in:
cottongin
2025-11-01 21:00:16 -04:00
parent bd9513b86c
commit dd398c9a8c
31 changed files with 5211 additions and 4 deletions

161
cmd/compare-auth/main.go Normal file
View File

@@ -0,0 +1,161 @@
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
bkosmi "github.com/42wim/matterbridge/bridge/kosmi"
"github.com/sirupsen/logrus"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: compare-auth <email> <password>")
fmt.Println("")
fmt.Println("This script will:")
fmt.Println("1. Get an anonymous token")
fmt.Println("2. Get an authenticated token via browser")
fmt.Println("3. Decode and compare both JWT tokens")
fmt.Println("4. Show what's different")
os.Exit(1)
}
email := os.Args[1]
password := os.Args[2]
// Set up logging
log := logrus.New()
log.SetLevel(logrus.InfoLevel)
entry := logrus.NewEntry(log)
fmt.Println(strings.Repeat("=", 80))
fmt.Println("COMPARING ANONYMOUS vs AUTHENTICATED TOKENS")
fmt.Println(strings.Repeat("=", 80))
fmt.Println()
// Get anonymous token
fmt.Println("📝 Step 1: Getting anonymous token...")
client := bkosmi.NewGraphQLWSClient("https://app.kosmi.io/room/@test", "@test", entry)
anonToken, err := client.GetAnonymousTokenForTest()
if err != nil {
fmt.Printf("❌ Failed to get anonymous token: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Anonymous token obtained (length: %d)\n", len(anonToken))
fmt.Println()
// Get authenticated token
fmt.Println("📝 Step 2: Getting authenticated token via browser...")
browserAuth := bkosmi.NewBrowserAuthManager(email, password, entry)
authToken, err := browserAuth.GetToken()
if err != nil {
fmt.Printf("❌ Failed to get authenticated token: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Authenticated token obtained (length: %d)\n", len(authToken))
fmt.Println()
// Decode both tokens
fmt.Println("📝 Step 3: Decoding JWT tokens...")
fmt.Println()
anonClaims := decodeJWT(anonToken)
authClaims := decodeJWT(authToken)
fmt.Println("ANONYMOUS TOKEN:")
fmt.Println(strings.Repeat("=", 80))
printClaims(anonClaims)
fmt.Println()
fmt.Println("AUTHENTICATED TOKEN:")
fmt.Println(strings.Repeat("=", 80))
printClaims(authClaims)
fmt.Println()
// Compare
fmt.Println("DIFFERENCES:")
fmt.Println(strings.Repeat("=", 80))
compareClaims(anonClaims, authClaims)
fmt.Println()
fmt.Println("📝 Step 4: Testing connection with both tokens...")
fmt.Println()
// We can't easily test the actual connection here, but we've shown the token differences
fmt.Println("✅ Analysis complete!")
fmt.Println()
fmt.Println("RECOMMENDATION:")
fmt.Println("If the authenticated token has a different 'sub' (user ID), that user")
fmt.Println("might not have the same permissions or profile as the anonymous user.")
}
func decodeJWT(token string) map[string]interface{} {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return map[string]interface{}{"error": "invalid JWT format"}
}
// Decode payload
payload := parts[1]
// Add padding if needed
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
// Replace URL-safe characters
payload = strings.ReplaceAll(payload, "-", "+")
payload = strings.ReplaceAll(payload, "_", "/")
decoded, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return map[string]interface{}{"error": fmt.Sprintf("decode error: %v", err)}
}
var claims map[string]interface{}
if err := json.Unmarshal(decoded, &claims); err != nil {
return map[string]interface{}{"error": fmt.Sprintf("parse error: %v", err)}
}
return claims
}
func printClaims(claims map[string]interface{}) {
keys := []string{"sub", "aud", "iss", "typ", "iat", "exp", "nbf", "jti"}
for _, key := range keys {
if val, ok := claims[key]; ok {
fmt.Printf(" %-10s: %v\n", key, val)
}
}
}
func compareClaims(anon, auth map[string]interface{}) {
allKeys := make(map[string]bool)
for k := range anon {
allKeys[k] = true
}
for k := range auth {
allKeys[k] = true
}
different := false
for key := range allKeys {
anonVal := anon[key]
authVal := auth[key]
if fmt.Sprintf("%v", anonVal) != fmt.Sprintf("%v", authVal) {
different = true
fmt.Printf(" %-10s: ANON=%v AUTH=%v\n", key, anonVal, authVal)
}
}
if !different {
fmt.Println(" (No differences found - tokens are essentially the same)")
}
}

92
cmd/decode-token/main.go Normal file
View File

@@ -0,0 +1,92 @@
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: decode-token <jwt-token>")
os.Exit(1)
}
token := os.Args[1]
parts := strings.Split(token, ".")
if len(parts) != 3 {
fmt.Println("Invalid JWT token format")
os.Exit(1)
}
// Decode header
fmt.Println("=== JWT HEADER ===")
headerBytes, err := base64.RawStdEncoding.DecodeString(parts[0])
if err != nil {
fmt.Printf("Failed to decode header: %v\n", err)
os.Exit(1)
}
var header map[string]interface{}
if err := json.Unmarshal(headerBytes, &header); err != nil {
fmt.Printf("Failed to parse header: %v\n", err)
os.Exit(1)
}
headerJSON, _ := json.MarshalIndent(header, "", " ")
fmt.Println(string(headerJSON))
// Decode payload
fmt.Println("\n=== JWT PAYLOAD (CLAIMS) ===")
payloadBytes, err := base64.RawStdEncoding.DecodeString(parts[1])
if err != nil {
fmt.Printf("Failed to decode payload: %v\n", err)
os.Exit(1)
}
var payload map[string]interface{}
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
fmt.Printf("Failed to parse payload: %v\n", err)
os.Exit(1)
}
payloadJSON, _ := json.MarshalIndent(payload, "", " ")
fmt.Println(string(payloadJSON))
fmt.Println("\n=== KEY FIELDS ===")
if sub, ok := payload["sub"].(string); ok {
fmt.Printf("User ID (sub): %s\n", sub)
}
if typ, ok := payload["typ"].(string); ok {
fmt.Printf("Token Type (typ): %s\n", typ)
}
if aud, ok := payload["aud"].(string); ok {
fmt.Printf("Audience (aud): %s\n", aud)
}
if exp, ok := payload["exp"].(float64); ok {
fmt.Printf("Expires (exp): %v\n", exp)
}
if iat, ok := payload["iat"].(float64); ok {
fmt.Printf("Issued At (iat): %v\n", iat)
}
// Check for user profile fields
fmt.Println("\n=== USER PROFILE FIELDS ===")
hasProfile := false
for key, value := range payload {
if strings.Contains(strings.ToLower(key), "name") ||
strings.Contains(strings.ToLower(key), "user") ||
strings.Contains(strings.ToLower(key), "display") ||
strings.Contains(strings.ToLower(key), "email") {
fmt.Printf("%s: %v\n", key, value)
hasProfile = true
}
}
if !hasProfile {
fmt.Println("(No user profile fields found in token)")
}
}

View File

@@ -0,0 +1,74 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
)
type HAR struct {
Log struct {
Entries []struct {
WebSocketMessages []struct {
Type string `json:"type"`
Data string `json:"data"`
} `json:"_webSocketMessages"`
} `json:"entries"`
} `json:"log"`
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: extract-queries <har-file>")
os.Exit(1)
}
data, err := os.ReadFile(os.Args[1])
if err != nil {
fmt.Printf("Failed to read file: %v\n", err)
os.Exit(1)
}
var har HAR
if err := json.Unmarshal(data, &har); err != nil {
fmt.Printf("Failed to parse HAR: %v\n", err)
os.Exit(1)
}
fmt.Println("=== WebSocket Messages ===\n")
for _, entry := range har.Log.Entries {
for i, msg := range entry.WebSocketMessages {
if msg.Type == "send" {
var payload map[string]interface{}
if err := json.Unmarshal([]byte(msg.Data), &payload); err != nil {
continue
}
msgType, _ := payload["type"].(string)
fmt.Printf("[%d] Type: %s\n", i, msgType)
if msgType == "subscribe" {
if p, ok := payload["payload"].(map[string]interface{}); ok {
if opName, ok := p["operationName"].(string); ok {
fmt.Printf(" Operation: %s\n", opName)
}
if query, ok := p["query"].(string); ok {
// Pretty print the query
query = strings.ReplaceAll(query, "\\n", "\n")
if len(query) > 500 {
fmt.Printf(" Query:\n%s\n [...truncated...]\n\n", query[:500])
} else {
fmt.Printf(" Query:\n%s\n\n", query)
}
}
}
} else {
fmt.Println()
}
}
}
}
}

244
cmd/monitor-auth/README.md Normal file
View File

@@ -0,0 +1,244 @@
# Kosmi Auth & Reconnection Monitor
A comprehensive WebSocket monitoring tool for reverse engineering Kosmi's authentication and reconnection behavior.
## Features
- 📡 Captures all WebSocket traffic (send/receive)
- 🔐 Monitors authentication flows (login, token acquisition)
- 🔄 Tests reconnection behavior
- 💾 Captures localStorage, sessionStorage, and cookies
- 📝 Logs to both console and file
- 🌐 Monitors HTTP requests/responses
## Building
```bash
cd /Users/erikfredericks/dev-ai/HSO/irc-kosmi-relay
go build -o bin/monitor-auth ./cmd/monitor-auth
```
## Usage
### 1. Monitor Anonymous Connection (Default)
Captures the anonymous token acquisition and WebSocket connection:
```bash
./bin/monitor-auth -room "https://app.kosmi.io/room/@hyperspaceout"
```
**What it captures:**
- Anonymous token request/response
- WebSocket connection handshake
- Message subscription
- Room join
- Incoming messages
### 2. Monitor Login Flow
Captures the full authentication flow when logging in:
```bash
./bin/monitor-auth -login
```
**What to do:**
1. Script opens browser to Kosmi
2. Manually log in with your credentials
3. Navigate to a room
4. Script captures all auth traffic
**What it captures:**
- Login form submission
- Token response
- Token storage (localStorage/cookies)
- Authenticated WebSocket connection
- User information
### 3. Test Reconnection Behavior
Simulates network disconnection and observes reconnection:
```bash
./bin/monitor-auth -reconnect
```
**What it does:**
1. Connects to Kosmi
2. Simulates network offline for 10 seconds
3. Restores network
4. Observes reconnection behavior
**What it captures:**
- Disconnection events
- Reconnection attempts
- Token refresh (if any)
- Re-subscription
## Output
All captured data is written to:
- **Console**: Real-time output with emojis for easy reading
- **File**: `auth-monitor.log` in current directory
### Log Format
```
HH:MM:SS.mmm [TYPE] Message
```
Examples:
```
11:45:23.123 🌐 [HTTP REQUEST] POST https://engine.kosmi.io/
11:45:23.456 📨 [HTTP RESPONSE] 200 https://engine.kosmi.io/
11:45:23.789 🔌 [WS MONITOR] WebSocket created: wss://engine.kosmi.io/gql-ws
11:45:24.012 📤 [WS MONITOR] SEND #1: {"type":"connection_init",...}
11:45:24.234 📥 [WS MONITOR] RECEIVE #1: {"type":"connection_ack"}
```
## Analyzing Captured Data
### Finding Authentication API
Look for POST requests to `https://engine.kosmi.io/`:
```bash
grep "POST.*engine.kosmi.io" auth-monitor.log
grep "Response Body" auth-monitor.log | grep -A 10 "login"
```
### Finding Token Storage
Look for localStorage/sessionStorage writes:
```bash
grep "localStorage" auth-monitor.log
grep "token" auth-monitor.log
```
### Finding Reconnection Logic
Look for WebSocket CLOSED/OPENED events:
```bash
grep "WebSocket CLOSED" auth-monitor.log
grep "WebSocket OPENED" auth-monitor.log
```
## Common Patterns to Look For
### 1. Login Mutation
```graphql
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
refreshToken
expiresIn
user {
id
displayName
username
}
}
}
```
### 2. Token Refresh Mutation
```graphql
mutation RefreshToken($refreshToken: String!) {
refreshToken(refreshToken: $refreshToken) {
token
refreshToken
expiresIn
}
}
```
### 3. Typing Indicators
```graphql
mutation SetTyping($roomId: String!, $isTyping: Boolean!) {
setTyping(roomId: $roomId, isTyping: $isTyping) {
ok
}
}
subscription OnUserTyping($roomId: String!) {
userTyping(roomId: $roomId) {
user {
id
displayName
}
isTyping
}
}
```
## Troubleshooting
### Script won't stop with Ctrl+C
**Fixed in latest version**. Rebuild if you have an old version:
```bash
go build -o bin/monitor-auth ./cmd/monitor-auth
```
If still stuck, you can force quit:
```bash
# In another terminal
pkill -f monitor-auth
```
### Browser doesn't open
Make sure Playwright is installed:
```bash
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install
```
### No WebSocket traffic captured
The monitoring script injects BEFORE page load. If you see "WebSocket hook active" in the console, it's working. If not, try:
1. Refresh the page
2. Check browser console for errors
3. Ensure you're on a Kosmi room page
### Log file is empty
Check that you have write permissions in the current directory:
```bash
touch auth-monitor.log
ls -l auth-monitor.log
```
## Tips
1. **Use with real credentials**: The monitoring script is safe - it runs locally and doesn't send data anywhere. Use real credentials to capture actual auth flows.
2. **Compare anonymous vs authenticated**: Run twice - once without `-login` and once with - to see the differences.
3. **Watch the browser**: Keep an eye on the browser window to see what triggers each WebSocket message.
4. **Search the log file**: Use `grep`, `jq`, or text editor to analyze the captured data.
5. **Test edge cases**: Try invalid credentials, expired tokens, network failures, etc.
## Next Steps
After capturing auth data:
1. Review `auth-monitor.log`
2. Identify actual GraphQL mutation formats
3. Update `bridge/kosmi/auth.go` if needed
4. Test with real credentials in production
5. Verify token refresh works correctly
## See Also
- `TYPING_INDICATORS.md` - Guide for implementing typing indicators
- `IMPLEMENTATION_SUMMARY.md` - Overall project documentation
- `chat-summaries/2025-11-01_*_reconnection-and-auth-implementation.md` - Implementation details

545
cmd/monitor-auth/main.go Normal file
View File

@@ -0,0 +1,545 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"time"
"github.com/playwright-community/playwright-go"
)
const (
defaultRoomURL = "https://app.kosmi.io/room/@hyperspaceout"
logFile = "auth-monitor.log"
)
func main() {
roomURL := flag.String("room", defaultRoomURL, "Kosmi room URL")
loginMode := flag.Bool("login", false, "Monitor login flow (navigate to login page first)")
testReconnect := flag.Bool("reconnect", false, "Test reconnection behavior (will pause network)")
flag.Parse()
log.Println("🔍 Starting Kosmi Auth & Reconnection Monitor")
log.Printf("📡 Room URL: %s", *roomURL)
log.Printf("🔐 Login mode: %v", *loginMode)
log.Printf("🔄 Reconnect test: %v", *testReconnect)
log.Printf("📝 Log file: %s", logFile)
log.Println()
// Create log file
f, err := os.Create(logFile)
if err != nil {
log.Fatalf("Failed to create log file: %v", err)
}
defer f.Close()
// Helper to log to both console and file
logBoth := func(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
log.Println(msg)
fmt.Fprintf(f, "%s %s\n", time.Now().Format("15:04:05.000"), msg)
}
// Set up interrupt handler
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
// Launch Playwright
pw, err := playwright.Run()
if err != nil {
log.Fatalf("Failed to start Playwright: %v", err)
}
// Cleanup function
cleanup := func() {
logBoth("\n👋 Shutting down...")
if pw != nil {
pw.Stop()
}
os.Exit(0)
}
// Handle interrupt
go func() {
<-interrupt
cleanup()
}()
// Launch browser (visible so we can interact for login)
browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(false),
})
if err != nil {
log.Fatalf("Failed to launch browser: %v", err)
}
// Create context
context, err := browser.NewContext(playwright.BrowserNewContextOptions{
UserAgent: playwright.String("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"),
})
if err != nil {
log.Fatalf("Failed to create context: %v", err)
}
// Monitor all network requests
context.On("request", func(request playwright.Request) {
url := request.URL()
method := request.Method()
// Log all Kosmi-related requests
if containsKosmi(url) {
logBoth("🌐 [HTTP REQUEST] %s %s", method, url)
// Log POST data (likely contains login credentials or GraphQL mutations)
if method == "POST" {
postData, err := request.PostData()
if err == nil && postData != "" {
logBoth(" 📤 POST Data: %s", postData)
}
}
// Log headers for auth-related requests
headers := request.Headers()
if authHeader, ok := headers["authorization"]; ok {
logBoth(" 🔑 Authorization: %s", authHeader)
}
if cookie, ok := headers["cookie"]; ok {
logBoth(" 🍪 Cookie: %s", truncate(cookie, 100))
}
}
})
// Monitor all network responses
context.On("response", func(response playwright.Response) {
url := response.URL()
status := response.Status()
if containsKosmi(url) {
logBoth("📨 [HTTP RESPONSE] %d %s", status, url)
// Try to get response body for auth endpoints
if status >= 200 && status < 300 {
body, err := response.Body()
if err == nil && len(body) > 0 && len(body) < 50000 {
// Try to parse as JSON
var jsonData interface{}
if json.Unmarshal(body, &jsonData) == nil {
prettyJSON, _ := json.MarshalIndent(jsonData, " ", " ")
logBoth(" 📦 Response Body: %s", string(prettyJSON))
}
}
}
// Log Set-Cookie headers
headers := response.Headers()
if setCookie, ok := headers["set-cookie"]; ok {
logBoth(" 🍪 Set-Cookie: %s", setCookie)
}
}
})
// Create page
page, err := context.NewPage()
if err != nil {
log.Fatalf("Failed to create page: %v", err)
}
// Inject comprehensive monitoring script BEFORE navigation
logBoth("📝 Injecting WebSocket, storage, and reconnection monitoring script...")
monitorScript := getMonitoringScript()
logBoth(" Script length: %d characters", len(monitorScript))
if err := page.AddInitScript(playwright.Script{
Content: playwright.String(monitorScript),
}); err != nil {
log.Fatalf("Failed to inject script: %v", err)
}
logBoth(" ✅ Script injected successfully")
// Listen to console messages
page.On("console", func(msg playwright.ConsoleMessage) {
text := msg.Text()
msgType := msg.Type()
// Format the output nicely
prefix := "💬"
switch msgType {
case "log":
prefix = "📋"
case "error":
prefix = "❌"
case "warning":
prefix = "⚠️"
case "info":
prefix = ""
}
logBoth("%s [BROWSER %s] %s", prefix, msgType, text)
})
// Navigate based on mode
if *loginMode {
logBoth("🔐 Login mode: Navigate to login page first")
logBoth(" Please log in manually in the browser")
logBoth(" We'll capture all auth traffic")
// Navigate to main Kosmi page (will redirect to login if not authenticated)
logBoth("🌐 Navigating to https://app.kosmi.io...")
timeout := 30000.0 // 30 seconds
_, err := page.Goto("https://app.kosmi.io", playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
Timeout: &timeout,
})
if err != nil {
logBoth("⚠️ Navigation error: %v", err)
logBoth(" Continuing anyway - page might still be usable")
} else {
logBoth("✅ Page loaded successfully")
}
logBoth("")
logBoth("📋 Instructions:")
logBoth(" 1. Log in with your credentials in the browser")
logBoth(" 2. Navigate to the room: %s", *roomURL)
logBoth(" 3. Browser console messages should appear here")
logBoth(" 4. Press Ctrl+C when done")
logBoth("")
logBoth("💡 Expected console messages:")
logBoth(" - '[Monitor] Installing...'")
logBoth(" - '[WS MONITOR] WebSocket created...'")
logBoth(" - '[FETCH] Request to...'")
logBoth("")
} else {
// Navigate directly to room (anonymous flow)
logBoth("🌐 Navigating to %s...", *roomURL)
timeout := 30000.0 // 30 seconds
_, err := page.Goto(*roomURL, playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
Timeout: &timeout,
})
if err != nil {
logBoth("⚠️ Navigation error: %v", err)
logBoth(" Continuing anyway - page might still be usable")
} else {
logBoth("✅ Page loaded successfully")
}
}
// Wait for WebSocket to connect
time.Sleep(5 * time.Second)
// Capture storage data
logBoth("\n📦 Capturing storage data...")
captureStorage(page, logBoth)
if *testReconnect {
logBoth("\n🔄 Testing reconnection behavior...")
logBoth(" This will simulate network disconnection")
logBoth(" Watch how the browser handles reconnection")
// Simulate network offline
logBoth(" 📡 Setting network to OFFLINE...")
if err := context.SetOffline(true); err != nil {
logBoth(" ❌ Failed to set offline: %v", err)
} else {
logBoth(" ✅ Network is now OFFLINE")
logBoth(" ⏳ Waiting 10 seconds...")
time.Sleep(10 * time.Second)
// Restore network
logBoth(" 📡 Setting network to ONLINE...")
if err := context.SetOffline(false); err != nil {
logBoth(" ❌ Failed to set online: %v", err)
} else {
logBoth(" ✅ Network is now ONLINE")
logBoth(" ⏳ Observing reconnection behavior...")
time.Sleep(10 * time.Second)
}
}
}
// Capture final storage state
logBoth("\n📦 Capturing final storage state...")
captureStorage(page, logBoth)
// Keep monitoring
logBoth("\n⏳ Monitoring all traffic... Press Ctrl+C to stop")
if *loginMode {
logBoth("💡 TIP: Try logging in, navigating to rooms, and logging out")
logBoth(" We'll capture all authentication flows")
}
// Wait forever (interrupt handler will exit)
select {}
}
// captureStorage captures localStorage, sessionStorage, and cookies
func captureStorage(page playwright.Page, logBoth func(string, ...interface{})) {
// Capture localStorage
localStorageResult, err := page.Evaluate(`
(function() {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
data[key] = localStorage.getItem(key);
}
return data;
})();
`)
if err == nil {
if localStorage, ok := localStorageResult.(map[string]interface{}); ok {
logBoth(" 📦 localStorage:")
for key, value := range localStorage {
logBoth(" %s: %s", key, truncate(fmt.Sprintf("%v", value), 100))
}
}
}
// Capture sessionStorage
sessionStorageResult, err := page.Evaluate(`
(function() {
const data = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
data[key] = sessionStorage.getItem(key);
}
return data;
})();
`)
if err == nil {
if sessionStorage, ok := sessionStorageResult.(map[string]interface{}); ok {
logBoth(" 📦 sessionStorage:")
for key, value := range sessionStorage {
logBoth(" %s: %s", key, truncate(fmt.Sprintf("%v", value), 100))
}
}
}
// Capture cookies
cookiesResult, err := page.Evaluate(`
(function() {
return document.cookie.split(';').map(c => {
const parts = c.trim().split('=');
return {
name: parts[0],
value: parts.slice(1).join('=')
};
}).filter(c => c.name && c.value);
})();
`)
if err == nil {
if cookiesArray, ok := cookiesResult.([]interface{}); ok {
logBoth(" 🍪 Cookies:")
for _, cookieItem := range cookiesArray {
if cookie, ok := cookieItem.(map[string]interface{}); ok {
name := cookie["name"]
value := cookie["value"]
logBoth(" %s: %s", name, truncate(fmt.Sprintf("%v", value), 100))
}
}
}
}
// Capture WebSocket status
wsStatusResult, err := page.Evaluate(`
(function() {
return window.__KOSMI_WS_STATUS__ || { error: "No WebSocket found" };
})();
`)
if err == nil {
if wsStatus, ok := wsStatusResult.(map[string]interface{}); ok {
logBoth(" 🔌 WebSocket Status:")
for key, value := range wsStatus {
logBoth(" %s: %v", key, value)
}
}
}
}
// getMonitoringScript returns the comprehensive monitoring script
func getMonitoringScript() string {
return `
(function() {
if (window.__KOSMI_MONITOR_INSTALLED__) return;
console.log('[Monitor] Installing comprehensive monitoring hooks...');
// Store original WebSocket constructor
const OriginalWebSocket = window.WebSocket;
let wsInstance = null;
let messageCount = 0;
let reconnectAttempts = 0;
// Hook WebSocket constructor
window.WebSocket = function(url, protocols) {
console.log('🔌 [WS MONITOR] WebSocket created:', url, 'protocols:', protocols);
const socket = new OriginalWebSocket(url, protocols);
if (url.includes('engine.kosmi.io') || url.includes('gql-ws')) {
wsInstance = socket;
// Track connection lifecycle
socket.addEventListener('open', (event) => {
console.log('✅ [WS MONITOR] WebSocket OPENED');
reconnectAttempts = 0;
updateWSStatus('OPEN', url);
});
socket.addEventListener('close', (event) => {
console.log('🔴 [WS MONITOR] WebSocket CLOSED:', event.code, event.reason, 'wasClean:', event.wasClean);
reconnectAttempts++;
updateWSStatus('CLOSED', url, { code: event.code, reason: event.reason, reconnectAttempts });
});
socket.addEventListener('error', (event) => {
console.error('❌ [WS MONITOR] WebSocket ERROR:', event);
updateWSStatus('ERROR', url);
});
// Intercept outgoing messages
const originalSend = socket.send;
socket.send = function(data) {
messageCount++;
console.log('📤 [WS MONITOR] SEND #' + messageCount + ':', data);
try {
const parsed = JSON.parse(data);
console.log(' Type:', parsed.type, 'ID:', parsed.id);
if (parsed.payload) {
console.log(' Payload:', JSON.stringify(parsed.payload, null, 2));
// Check for auth-related messages
if (parsed.type === 'connection_init' && parsed.payload.token) {
console.log(' 🔑 [AUTH] Connection init with token:', parsed.payload.token.substring(0, 50) + '...');
}
}
} catch (e) {
// Not JSON
}
return originalSend.call(this, data);
};
// Intercept incoming messages
socket.addEventListener('message', (event) => {
messageCount++;
console.log('📥 [WS MONITOR] RECEIVE #' + messageCount + ':', event.data);
try {
const parsed = JSON.parse(event.data);
console.log(' Type:', parsed.type, 'ID:', parsed.id);
if (parsed.payload) {
console.log(' Payload:', JSON.stringify(parsed.payload, null, 2));
}
} catch (e) {
// Not JSON
}
});
}
return socket;
};
// Preserve WebSocket properties
window.WebSocket.prototype = OriginalWebSocket.prototype;
window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;
window.WebSocket.OPEN = OriginalWebSocket.OPEN;
window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;
window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;
// Monitor localStorage changes
const originalSetItem = Storage.prototype.setItem;
Storage.prototype.setItem = function(key, value) {
console.log('💾 [STORAGE] localStorage.setItem:', key, '=', value.substring(0, 100));
if (key.toLowerCase().includes('token') || key.toLowerCase().includes('auth')) {
console.log(' 🔑 [AUTH] Auth-related storage detected!');
}
return originalSetItem.call(this, key, value);
};
// Monitor sessionStorage changes
const originalSessionSetItem = sessionStorage.setItem;
sessionStorage.setItem = function(key, value) {
console.log('💾 [STORAGE] sessionStorage.setItem:', key, '=', value.substring(0, 100));
if (key.toLowerCase().includes('token') || key.toLowerCase().includes('auth')) {
console.log(' 🔑 [AUTH] Auth-related storage detected!');
}
return originalSessionSetItem.call(this, key, value);
};
// Monitor fetch requests (for GraphQL mutations)
const originalFetch = window.fetch;
window.fetch = function(url, options) {
if (typeof url === 'string' && url.includes('kosmi.io')) {
console.log('🌐 [FETCH] Request to:', url);
if (options && options.body) {
console.log(' 📤 Body:', options.body);
try {
const parsed = JSON.parse(options.body);
if (parsed.query) {
console.log(' 📝 GraphQL Query:', parsed.query.substring(0, 200));
if (parsed.query.includes('login') || parsed.query.includes('auth')) {
console.log(' 🔑 [AUTH] Auth-related GraphQL detected!');
}
}
} catch (e) {
// Not JSON
}
}
}
return originalFetch.apply(this, arguments).then(response => {
if (typeof url === 'string' && url.includes('kosmi.io')) {
console.log('📨 [FETCH] Response from:', url, 'status:', response.status);
}
return response;
});
};
// Helper to update WebSocket status
function updateWSStatus(state, url, extra = {}) {
window.__KOSMI_WS_STATUS__ = {
state,
url,
timestamp: new Date().toISOString(),
messageCount,
...extra
};
}
// Monitor network connectivity
window.addEventListener('online', () => {
console.log('🌐 [NETWORK] Browser is ONLINE');
});
window.addEventListener('offline', () => {
console.log('🌐 [NETWORK] Browser is OFFLINE');
});
window.__KOSMI_MONITOR_INSTALLED__ = true;
console.log('[Monitor] ✅ All monitoring hooks installed');
})();
`
}
// Helper functions
func containsKosmi(url string) bool {
return contains(url, "kosmi.io") || contains(url, "engine.kosmi.io") || contains(url, "app.kosmi.io")
}
func contains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}

145
cmd/parse-har/main.go Normal file
View File

@@ -0,0 +1,145 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
)
type HAR struct {
Log struct {
Entries []struct {
Request struct {
Method string `json:"method"`
URL string `json:"url"`
} `json:"request"`
WebSocketMessages []struct {
Type string `json:"type"`
Data string `json:"data"`
Time float64 `json:"time"`
} `json:"_webSocketMessages"`
} `json:"entries"`
} `json:"log"`
}
type WSMessage struct {
ID string `json:"id"`
Type string `json:"type"`
Payload map[string]interface{} `json:"payload"`
}
func main() {
if len(os.Args) < 2 {
log.Fatal("Usage: parse-har <har-file>")
}
data, err := ioutil.ReadFile(os.Args[1])
if err != nil {
log.Fatal(err)
}
var har HAR
if err := json.Unmarshal(data, &har); err != nil {
log.Fatal(err)
}
// First pass: collect all responses
responses := make(map[string]bool) // ID -> has data
for _, entry := range har.Log.Entries {
for _, wsMsg := range entry.WebSocketMessages {
if wsMsg.Type != "receive" {
continue
}
var msg WSMessage
if err := json.Unmarshal([]byte(wsMsg.Data), &msg); err != nil {
continue
}
if msg.Type == "next" {
// Check if payload has data
if data, ok := msg.Payload["data"].(map[string]interface{}); ok && len(data) > 0 {
responses[msg.ID] = true
}
}
}
}
fmt.Println("=== WebSocket Operations (in order) ===\n")
msgCount := 0
for _, entry := range har.Log.Entries {
for _, wsMsg := range entry.WebSocketMessages {
if wsMsg.Type != "send" {
continue
}
var msg WSMessage
if err := json.Unmarshal([]byte(wsMsg.Data), &msg); err != nil {
continue
}
if msg.Type == "connection_init" {
fmt.Printf("[%d] connection_init\n", msgCount)
msgCount++
continue
}
if msg.Type == "subscribe" {
opName := ""
query := ""
variables := map[string]interface{}{}
extensions := map[string]interface{}{}
if payload, ok := msg.Payload["operationName"].(string); ok {
opName = payload
}
if q, ok := msg.Payload["query"].(string); ok {
query = q
}
if v, ok := msg.Payload["variables"].(map[string]interface{}); ok {
variables = v
}
if e, ok := msg.Payload["extensions"].(map[string]interface{}); ok {
extensions = e
}
hasResponse := ""
if responses[msg.ID] {
hasResponse = " ✅ GOT DATA"
} else {
hasResponse = " ❌ NO DATA"
}
fmt.Printf("[%d] %s (ID: %s)%s\n", msgCount, opName, msg.ID, hasResponse)
// Show query type
if strings.Contains(query, "mutation") {
fmt.Printf(" Type: MUTATION\n")
} else if strings.Contains(query, "subscription") {
fmt.Printf(" Type: SUBSCRIPTION\n")
} else if strings.Contains(query, "query") {
fmt.Printf(" Type: QUERY\n")
}
// Show variables
if len(variables) > 0 {
fmt.Printf(" Variables: %v\n", variables)
}
// Show extensions
if len(extensions) > 0 {
fmt.Printf(" Extensions: %v\n", extensions)
}
fmt.Println()
msgCount++
}
}
}
fmt.Printf("\nTotal operations: %d\n", msgCount)
}

View File

@@ -0,0 +1,74 @@
package main
import (
"fmt"
"os"
bkosmi "github.com/42wim/matterbridge/bridge/kosmi"
"github.com/sirupsen/logrus"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: test-browser-auth <email> <password>")
fmt.Println("")
fmt.Println("This tests the browser-based authentication by:")
fmt.Println("1. Launching headless Chrome")
fmt.Println("2. Logging in to Kosmi")
fmt.Println("3. Extracting the JWT token")
fmt.Println("4. Parsing token expiry")
os.Exit(1)
}
email := os.Args[1]
password := os.Args[2]
// Set up logging
log := logrus.New()
log.SetLevel(logrus.DebugLevel)
entry := logrus.NewEntry(log)
fmt.Println("🚀 Testing browser-based authentication...")
fmt.Println()
// Create browser auth manager
browserAuth := bkosmi.NewBrowserAuthManager(email, password, entry)
// Get token
token, err := browserAuth.GetToken()
if err != nil {
fmt.Printf("❌ Authentication failed: %v\n", err)
os.Exit(1)
}
fmt.Println()
fmt.Println("✅ Authentication successful!")
fmt.Println()
fmt.Printf("Token (first 50 chars): %s...\n", token[:min(50, len(token))])
fmt.Printf("Token length: %d characters\n", len(token))
fmt.Println()
// Check if authenticated
if browserAuth.IsAuthenticated() {
fmt.Println("✅ Token is valid")
} else {
fmt.Println("❌ Token is invalid or expired")
}
// Get user ID
userID := browserAuth.GetUserID()
if userID != "" {
fmt.Printf("User ID: %s\n", userID)
}
fmt.Println()
fmt.Println("🎉 Test completed successfully!")
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -0,0 +1,309 @@
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/chromedp/chromedp"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: test-browser-login <email> <password>")
os.Exit(1)
}
email := os.Args[1]
password := os.Args[2]
// Set up Chrome options - VISIBLE browser for debugging
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false), // NOT headless - we want to see it
chromedp.Flag("disable-gpu", false),
)
// Create allocator context
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
// Create context with timeout
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
// Set a reasonable timeout for the entire login process
ctx, cancel = context.WithTimeout(ctx, 120*time.Second)
defer cancel()
var token string
// Run the browser automation tasks
err := chromedp.Run(ctx,
// Navigate to Kosmi
chromedp.Navigate("https://app.kosmi.io"),
// Wait for page to load completely
chromedp.WaitReady("body"),
chromedp.Sleep(2*time.Second),
// Click Login button
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Looking for Log in button...")
var buttonTexts []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim())
`, &buttonTexts).Do(ctx)
fmt.Printf("Found buttons: %v\n", buttonTexts)
var found bool
if err := chromedp.Evaluate(`
(() => {
const buttons = Array.from(document.querySelectorAll('button'));
// Try both "Login" and "Log in"
const btn = buttons.find(el => {
const text = el.textContent.trim();
return text === 'Login' || text === 'Log in';
});
if (btn) {
btn.click();
return true;
}
return false;
})()
`, &found).Do(ctx); err != nil {
return err
}
if !found {
return fmt.Errorf("Login button not found")
}
fmt.Println("✓ Clicked Login button")
return nil
}),
// Wait for login modal
chromedp.Sleep(3*time.Second),
// Click "Login with Email" button
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Looking for Login with Email button...")
var buttonTexts []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim())
`, &buttonTexts).Do(ctx)
fmt.Printf("Found buttons: %v\n", buttonTexts)
var found bool
if err := chromedp.Evaluate(`
(() => {
const btn = Array.from(document.querySelectorAll('button')).find(el => el.textContent.includes('Email'));
if (btn) {
btn.click();
return true;
}
return false;
})()
`, &found).Do(ctx); err != nil {
return err
}
if !found {
return fmt.Errorf("Login with Email button not found")
}
fmt.Println("✓ Clicked Login with Email button")
return nil
}),
// Wait for email form
chromedp.Sleep(3*time.Second),
chromedp.WaitVisible(`input[type="password"]`, chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Password input found, filling in email...")
var inputTypes []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('input')).map(el => el.type + (el.placeholder ? ' ('+el.placeholder+')' : ''))
`, &inputTypes).Do(ctx)
fmt.Printf("Available inputs: %v\n", inputTypes)
return nil
}),
// Click on the email input to focus it
chromedp.Click(`input[placeholder*="Email"], input[placeholder*="Username"]`, chromedp.ByQuery),
chromedp.Sleep(200*time.Millisecond),
// Type email character by character
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Printf("Typing email: %s\n", email)
return chromedp.SendKeys(`input[placeholder*="Email"], input[placeholder*="Username"]`, email, chromedp.ByQuery).Do(ctx)
}),
chromedp.Sleep(500*time.Millisecond),
// Click on the password input to focus it
chromedp.Click(`input[type="password"]`, chromedp.ByQuery),
chromedp.Sleep(200*time.Millisecond),
// Type password character by character
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Printf("Typing password (length: %d)...\n", len(password))
return chromedp.SendKeys(`input[type="password"]`, password, chromedp.ByQuery).Do(ctx)
}),
// Verify password was filled correctly
chromedp.ActionFunc(func(ctx context.Context) error {
var actualLength int
chromedp.Evaluate(`
(() => {
const passwordInput = document.querySelector('input[type="password"]');
return passwordInput ? passwordInput.value.length : 0;
})()
`, &actualLength).Do(ctx)
fmt.Printf("✓ Password filled (actual length in browser: %d, expected: %d)\n", actualLength, len(password))
if actualLength != len(password) {
return fmt.Errorf("password length mismatch: got %d, expected %d", actualLength, len(password))
}
return nil
}),
chromedp.Sleep(500*time.Millisecond),
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Looking for submit button...")
var buttonTexts []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim() + (el.disabled ? ' (disabled)' : ''))
`, &buttonTexts).Do(ctx)
fmt.Printf("Submit buttons available: %v\n", buttonTexts)
return nil
}),
// Wait a moment for form validation
chromedp.Sleep(1*time.Second),
// Click the login submit button (be very specific)
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Attempting to click submit button...")
var result string
if err := chromedp.Evaluate(`
(() => {
const buttons = Array.from(document.querySelectorAll('button'));
console.log('All buttons:', buttons.map(b => ({
text: b.textContent.trim(),
disabled: b.disabled,
visible: b.offsetParent !== null
})));
// Find the submit button in the login form
// It should be visible, enabled, and contain "Login" but not be the main nav button
const submitBtn = buttons.find(el => {
const text = el.textContent.trim();
const isLoginBtn = text === 'Login' || text.startsWith('Login');
const isEnabled = !el.disabled;
const isVisible = el.offsetParent !== null;
const isInForm = el.closest('form') !== null || el.closest('[role="dialog"]') !== null;
return isLoginBtn && isEnabled && isVisible && isInForm;
});
if (submitBtn) {
console.log('Found submit button:', submitBtn.textContent.trim());
submitBtn.click();
return 'CLICKED: ' + submitBtn.textContent.trim();
}
return 'NOT_FOUND';
})()
`, &result).Do(ctx); err != nil {
return err
}
fmt.Printf("Submit button result: %s\n", result)
if result == "NOT_FOUND" {
return fmt.Errorf("Login submit button not found or not clickable")
}
fmt.Println("✓ Clicked submit button")
return nil
}),
// Wait for login to complete
chromedp.Sleep(5*time.Second),
// Check for errors
chromedp.ActionFunc(func(ctx context.Context) error {
var errorText string
chromedp.Evaluate(`
(() => {
const errorEl = document.querySelector('[role="alert"], .error, .alert-error');
return errorEl ? errorEl.textContent : '';
})()
`, &errorText).Do(ctx)
if errorText != "" {
fmt.Printf("❌ Login error: %s\n", errorText)
return fmt.Errorf("login failed: %s", errorText)
}
fmt.Println("✓ No error messages")
return nil
}),
// Extract token from localStorage
chromedp.Evaluate(`localStorage.getItem('token')`, &token),
// Check token details
chromedp.ActionFunc(func(ctx context.Context) error {
var userInfo string
chromedp.Evaluate(`
(() => {
try {
const token = localStorage.getItem('token');
if (!token) return 'NO_TOKEN';
const parts = token.split('.');
if (parts.length !== 3) return 'INVALID_TOKEN';
const payload = JSON.parse(atob(parts[1]));
return JSON.stringify(payload, null, 2);
} catch (e) {
return 'ERROR: ' + e.message;
}
})()
`, &userInfo).Do(ctx)
fmt.Printf("\n=== Token Payload ===\n%s\n", userInfo)
return nil
}),
// Keep browser open for inspection
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("\n✓ Login complete! Browser will stay open for 30 seconds for inspection...")
return nil
}),
chromedp.Sleep(30*time.Second),
)
if err != nil {
fmt.Printf("\n❌ Error: %v\n", err)
os.Exit(1)
}
if token == "" {
fmt.Println("\n❌ No token found in localStorage")
os.Exit(1)
}
fmt.Printf("\n✅ Token obtained (length: %d)\n", len(token))
fmt.Printf("First 50 chars: %s...\n", token[:min(50, len(token))])
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -0,0 +1,73 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
// GraphQL introspection query to find all mutations
query := map[string]interface{}{
"query": `{
__schema {
mutationType {
fields {
name
description
args {
name
type {
name
kind
}
}
}
}
}
}`,
}
jsonBody, err := json.Marshal(query)
if err != nil {
fmt.Printf("Failed to marshal: %v\n", err)
return
}
req, err := http.NewRequest("POST", "https://engine.kosmi.io/", bytes.NewReader(jsonBody))
if err != nil {
fmt.Printf("Failed to create request: %v\n", err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", "https://app.kosmi.io/")
req.Header.Set("User-Agent", "Mozilla/5.0")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Failed to read response: %v\n", err)
return
}
// Pretty print JSON response
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
fmt.Println("Response (raw):")
fmt.Println(string(body))
} else {
prettyJSON, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(prettyJSON))
}
}

89
cmd/test-login/main.go Normal file
View File

@@ -0,0 +1,89 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: test-login <email> <password>")
os.Exit(1)
}
email := os.Args[1]
password := os.Args[2]
// Try the guessed mutation format
mutation := map[string]interface{}{
"query": `mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
refreshToken
expiresIn
user {
id
displayName
username
}
}
}`,
"variables": map[string]interface{}{
"email": email,
"password": password,
},
}
jsonBody, err := json.MarshalIndent(mutation, "", " ")
if err != nil {
fmt.Printf("Failed to marshal: %v\n", err)
os.Exit(1)
}
fmt.Println("📤 Sending request:")
fmt.Println(string(jsonBody))
fmt.Println()
req, err := http.NewRequest("POST", "https://engine.kosmi.io/", bytes.NewReader(jsonBody))
if err != nil {
fmt.Printf("Failed to create request: %v\n", err)
os.Exit(1)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", "https://app.kosmi.io/")
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Request failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
fmt.Printf("📥 Response Status: %d\n", resp.StatusCode)
fmt.Println()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Failed to read response: %v\n", err)
os.Exit(1)
}
// Pretty print JSON response
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
fmt.Println("Response (raw):")
fmt.Println(string(body))
} else {
prettyJSON, _ := json.MarshalIndent(result, "", " ")
fmt.Println("Response (JSON):")
fmt.Println(string(prettyJSON))
}
}

View File

@@ -0,0 +1,94 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: test-profile-query <jwt-token>")
os.Exit(1)
}
token := os.Args[1]
// Try different queries to get user profile
queries := []struct{
name string
query string
}{
{
name: "me query",
query: `query { me { id displayName username email avatarUrl } }`,
},
{
name: "currentUser query",
query: `query { currentUser { id displayName username email avatarUrl } }`,
},
{
name: "user query",
query: `query { user { id displayName username email avatarUrl } }`,
},
{
name: "viewer query",
query: `query { viewer { id displayName username email avatarUrl } }`,
},
}
for _, q := range queries {
fmt.Printf("\n=== Testing: %s ===\n", q.name)
testQuery(token, q.query)
}
}
func testQuery(token, query string) {
payload := map[string]interface{}{
"query": query,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
fmt.Printf("Failed to marshal query: %v\n", err)
return
}
req, err := http.NewRequest("POST", "https://engine.kosmi.io/", bytes.NewBuffer(payloadBytes))
if err != nil {
fmt.Printf("Failed to create request: %v\n", err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Failed to read response: %v\n", err)
return
}
fmt.Printf("Status: %d\n", resp.StatusCode)
// Pretty print JSON
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
fmt.Printf("Response: %s\n", string(body))
} else {
prettyJSON, _ := json.MarshalIndent(result, "", " ")
fmt.Printf("Response:\n%s\n", string(prettyJSON))
}
}