// JWT decoding helpers. // Portions derived from windsurf-tools (MIT 2025 shaoyu521). See ./LICENSE. package windsurf import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "strings" ) // readRandom abstracts crypto/rand.Read for testability. func readRandom(b []byte) (int, error) { return rand.Read(b) } // JWTClaims holds the fields we care about from the Windsurf session JWT. type JWTClaims struct { SessionID string `json:"session_id,omitempty"` UserID string `json:"user_id,omitempty"` TeamID string `json:"team_id,omitempty"` AuthUID string `json:"auth_uid,omitempty"` Exp int64 `json:"exp,omitempty"` } // StripDevinPrefix returns the raw JWT (without the "devin-session-token$" prefix). func StripDevinPrefix(token string) string { if i := strings.Index(token, "$"); i >= 0 && strings.HasPrefix(token, "devin-session-token$") { return token[i+1:] } return token } // DecodeJWTClaims parses the payload portion of a JWT (after stripping the // optional "devin-session-token$" prefix). It does NOT verify the signature. func DecodeJWTClaims(token string) (*JWTClaims, error) { jwt := StripDevinPrefix(token) parts := strings.Split(jwt, ".") if len(parts) != 3 { return nil, fmt.Errorf("jwt: expected 3 segments, got %d", len(parts)) } payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { payload, err = base64.URLEncoding.DecodeString(parts[1]) if err != nil { return nil, fmt.Errorf("jwt payload base64: %w", err) } } var claims JWTClaims if err := json.Unmarshal(payload, &claims); err != nil { return nil, fmt.Errorf("jwt payload json: %w", err) } return &claims, nil }