Files
ccs/src/cliproxy/auth/oauth-trace/trace-events.ts
T
Tam Nhu Tran 641d492cd6 feat(cliproxy): add OAuth callback traceability across profiles
Introduces structured per-phase tracing for OAuth flows so users see
branch-specific error messages instead of a generic "token not found"
fallback.

- New oauth-trace/ module: recorder, redactor, three sinks
  (in-memory ring buffer, verbose stdout, opt-in JSONL file at
  ~/.ccs/logs/oauth-YYYYMMDD.log mode 0o600 via CCS_OAUTH_LOG_FILE=1)
- Branch-specific diagnostics for: URL-not-displayed,
  callback-not-observed, binary-error, token-exchange-error,
  session-expired, token-file-missing; UNKNOWN fallback preserves
  prior UX
- Trace recorder threaded through oauth-process.ts spawn/stdout/stderr/
  exit lifecycle; SIGINT/SIGTERM cleanup flushes recorder and records
  Cancelled
- Redactor strips code, state, access_token, refresh_token, id_token,
  client_secret, code_verifier, device_code, assertion, subject_token,
  plus Authorization: Bearer headers; covers URL fragments,
  URL-encoded keys, and arrays
- 68 new tests including adversarial regression cases for redactor
  bypass attempts

Phases for paste-callback path and cross-profile failure-matrix
deferred to follow-up #1207.

Closes #1206
2026-05-10 15:12:40 -04:00

64 lines
2.0 KiB
TypeScript

/**
* OAuth trace event taxonomy.
*
* Single source of truth for phase IDs that flow through the OAuth pipeline.
* Additive only — never remove or renumber.
*/
export enum OAuthTracePhase {
// Pre-flight
PreflightStart = 'preflight.start',
PreflightOk = 'preflight.ok',
PreflightPortBlocked = 'preflight.port_blocked',
// Binary process lifecycle (CLIProxyAPI Go binary)
BinarySpawn = 'binary.spawn',
BinaryStdout = 'binary.stdout',
BinaryStderr = 'binary.stderr',
BinaryExit = 'binary.exit',
// Browser / URL flow (Authorization Code)
AuthUrlDisplayed = 'auth.url_displayed',
BrowserOpened = 'browser.opened',
CallbackObservedHeuristic = 'callback.observed_heuristic',
// Paste-callback (CCS-owned end-to-end)
PasteCallbackPrompted = 'paste.prompted',
PasteCallbackReceived = 'paste.received',
PasteCallbackInvalid = 'paste.invalid',
PasteCallbackSubmitted = 'paste.submitted',
// Token exchange + persistence
TokenExchangePending = 'token.exchange_pending',
TokenFileAppeared = 'token.file_appeared',
TokenFileMissing = 'token.file_missing',
// Provider-specific gates (Phase 4/5 extend)
ProjectSelectionPrompted = 'project.selection_prompted',
ProjectSelectionResolved = 'project.selection_resolved',
AgyResponsibilityPrompted = 'agy.responsibility_prompted',
AgyResponsibilityResolved = 'agy.responsibility_resolved',
// Terminal states
Timeout = 'timeout',
Cancelled = 'cancelled',
Error = 'error',
}
/** A single OAuth trace event. `data` MUST be redacted before construction. */
export interface OAuthTraceEvent {
sessionId: string;
provider: string;
phase: OAuthTracePhase;
ts: number; // Date.now()
elapsedMs: number; // since recorder.start()
data?: Record<string, unknown>;
error?: { code?: string; message: string };
}
/** Sink interface — accept events that have already been redacted. */
export interface OAuthTraceSink {
write(event: OAuthTraceEvent): void;
flush?(): Promise<void>;
}