@mastra/code-sdk 1.15.0-alpha.10 → 1.15.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/acp/agent.js +2 -1
  2. package/dist/acp/agent.js.map +1 -1
  3. package/dist/acp/event-mapper.js +0 -2
  4. package/dist/acp/event-mapper.js.map +1 -1
  5. package/dist/acp.js +1 -2
  6. package/dist/agent-connections/registry.js.map +1 -1
  7. package/dist/agent-connections/signal-provider.js.map +1 -1
  8. package/dist/agent-connections/state-processor.js.map +1 -1
  9. package/dist/agent-connections/tools.js +26 -24
  10. package/dist/agent-connections/tools.js.map +1 -1
  11. package/dist/agents/instructions.js +3 -2
  12. package/dist/agents/instructions.js.map +1 -1
  13. package/dist/agents/mastracode-gateway.js +35 -27
  14. package/dist/agents/mastracode-gateway.js.map +1 -1
  15. package/dist/agents/memory.d.ts.map +1 -1
  16. package/dist/agents/memory.js +16 -2
  17. package/dist/agents/memory.js.map +1 -1
  18. package/dist/agents/prompts/agent-instructions.js +1 -1
  19. package/dist/agents/prompts/agent-instructions.js.map +1 -1
  20. package/dist/agents/prompts/index.js +3 -2
  21. package/dist/agents/prompts/index.js.map +1 -1
  22. package/dist/agents/prompts/plan.js +2 -1
  23. package/dist/agents/prompts/plan.js.map +1 -1
  24. package/dist/agents/sandbox-filesystem.d.ts +28 -0
  25. package/dist/agents/sandbox-filesystem.d.ts.map +1 -1
  26. package/dist/agents/sandbox-filesystem.js +81 -2
  27. package/dist/agents/sandbox-filesystem.js.map +1 -1
  28. package/dist/agents/tools.d.ts.map +1 -1
  29. package/dist/agents/tools.js +18 -12
  30. package/dist/agents/tools.js.map +1 -1
  31. package/dist/agents/workspace.js +2 -1
  32. package/dist/agents/workspace.js.map +1 -1
  33. package/dist/auth/account-rotation-processor.js +2 -1
  34. package/dist/auth/account-rotation-processor.js.map +1 -1
  35. package/dist/auth/providers/anthropic.js +2 -2
  36. package/dist/auth/providers/anthropic.js.map +1 -1
  37. package/dist/auth/providers/github-copilot.js +1 -1
  38. package/dist/auth/providers/github-copilot.js.map +1 -1
  39. package/dist/auth/providers/openai-codex.js +3 -2
  40. package/dist/auth/providers/openai-codex.js.map +1 -1
  41. package/dist/auth/providers/xai.js +1 -1
  42. package/dist/auth/providers/xai.js.map +1 -1
  43. package/dist/clipboard/index.js +5 -4
  44. package/dist/clipboard/index.js.map +1 -1
  45. package/dist/evals/context-builder.js +1 -3
  46. package/dist/evals/context-builder.js.map +1 -1
  47. package/dist/evals/scorers/outcome.js +2 -1
  48. package/dist/evals/scorers/outcome.js.map +1 -1
  49. package/dist/factory/remote-controller.js.map +1 -1
  50. package/dist/factory/remote-session.js +1 -3
  51. package/dist/factory/remote-session.js.map +1 -1
  52. package/dist/headless/cli.js +2 -1
  53. package/dist/headless/cli.js.map +1 -1
  54. package/dist/headless/run-mc.js +9 -9
  55. package/dist/headless/run-mc.js.map +1 -1
  56. package/dist/hooks/executor.js +3 -1
  57. package/dist/hooks/executor.js.map +1 -1
  58. package/dist/hooks/manager.js +40 -26
  59. package/dist/hooks/manager.js.map +1 -1
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +30 -18
  62. package/dist/index.js.map +1 -1
  63. package/dist/knowledge-inspector.js +2 -2
  64. package/dist/knowledge-inspector.js.map +1 -1
  65. package/dist/lsp/server.js +4 -2
  66. package/dist/lsp/server.js.map +1 -1
  67. package/dist/mcp/manager.d.ts.map +1 -1
  68. package/dist/mcp/manager.js +14 -2
  69. package/dist/mcp/manager.js.map +1 -1
  70. package/dist/onboarding/settings.js +5 -2
  71. package/dist/onboarding/settings.js.map +1 -1
  72. package/dist/platform/index.js +5 -3
  73. package/dist/platform/index.js.map +1 -1
  74. package/dist/plugins/loader.js +3 -1
  75. package/dist/plugins/loader.js.map +1 -1
  76. package/dist/plugins/manager.js +2 -1
  77. package/dist/plugins/manager.js.map +1 -1
  78. package/dist/plugins/package-link.js.map +1 -1
  79. package/dist/plugins/scaffold.js +4 -2
  80. package/dist/plugins/scaffold.js.map +1 -1
  81. package/dist/plugins/signal-lane.js.map +1 -1
  82. package/dist/process-memory-diagnostics.js +2 -1
  83. package/dist/process-memory-diagnostics.js.map +1 -1
  84. package/dist/providers/amazon-bedrock-gateway.js +2 -1
  85. package/dist/providers/amazon-bedrock-gateway.js.map +1 -1
  86. package/dist/providers/amazon-bedrock.js +2 -2
  87. package/dist/providers/amazon-bedrock.js.map +1 -1
  88. package/dist/providers/claude-max.js +13 -9
  89. package/dist/providers/claude-max.js.map +1 -1
  90. package/dist/providers/github-copilot-gateway.js.map +1 -1
  91. package/dist/providers/github-copilot.js +13 -12
  92. package/dist/providers/github-copilot.js.map +1 -1
  93. package/dist/providers/openai-codex.js +14 -11
  94. package/dist/providers/openai-codex.js.map +1 -1
  95. package/dist/providers/provider-thinking.js.map +1 -1
  96. package/dist/schedules/run-script.js +1 -1
  97. package/dist/schedules/run-script.js.map +1 -1
  98. package/dist/schedules/scheduler.js.map +1 -1
  99. package/dist/tools/request-sandbox-access.js +7 -5
  100. package/dist/tools/request-sandbox-access.js.map +1 -1
  101. package/dist/tools/utils.js +3 -1
  102. package/dist/tools/utils.js.map +1 -1
  103. package/dist/tools/web-search.d.ts +13 -11
  104. package/dist/tools/web-search.d.ts.map +1 -1
  105. package/dist/tools/web-search.js +21 -15
  106. package/dist/tools/web-search.js.map +1 -1
  107. package/dist/utils/debug-log.js +2 -2
  108. package/dist/utils/debug-log.js.map +1 -1
  109. package/dist/utils/gateway-sync.js +1 -1
  110. package/dist/utils/gateway-sync.js.map +1 -1
  111. package/dist/utils/local-observability.d.ts +1 -1
  112. package/dist/utils/local-observability.d.ts.map +1 -1
  113. package/dist/utils/local-observability.js +1 -1
  114. package/dist/utils/local-observability.js.map +1 -1
  115. package/dist/utils/prune-cli.js +10 -8
  116. package/dist/utils/prune-cli.js.map +1 -1
  117. package/dist/utils/signals-pubsub.js.map +1 -1
  118. package/dist/utils/slash-command-processor.js +1 -1
  119. package/dist/utils/slash-command-processor.js.map +1 -1
  120. package/dist/utils/stdin-pipe.js +1 -1
  121. package/dist/utils/stdin-pipe.js.map +1 -1
  122. package/dist/utils/storage-factory.d.ts.map +1 -1
  123. package/dist/utils/storage-factory.js +1 -1
  124. package/dist/utils/storage-factory.js.map +1 -1
  125. package/dist/utils/storage-maintenance.js +1 -1
  126. package/dist/utils/storage-maintenance.js.map +1 -1
  127. package/dist/utils/update-check.js +2 -1
  128. package/dist/utils/update-check.js.map +1 -1
  129. package/dist/workflows/register-primitives.js +1 -1
  130. package/dist/workflows/register-primitives.js.map +1 -1
  131. package/package.json +11 -11
@@ -1 +1 @@
1
- {"version":3,"file":"github-copilot.js","names":[],"sources":["../../../src/auth/providers/github-copilot.ts"],"sourcesContent":["/**\n * GitHub Copilot OAuth flow\n *\n * Inspired by:\n * - pi-mono: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/github-copilot.ts\n * - opencode: https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/plugin/github-copilot/copilot.ts\n *\n * Storage layout in AuthStorage:\n * - `refresh` holds the long-lived GitHub OAuth access token (used to mint Copilot tokens).\n * - `access` holds the short-lived Copilot bearer token (~30 minutes).\n * - `expires` is the Copilot bearer expiry (ms epoch, with a 5-minute safety buffer).\n * - `enterpriseUrl` is the GitHub Enterprise hostname, or undefined for github.com.\n */\n\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from '../types.js';\n\nexport type GitHubCopilotCredentials = OAuthCredentials & {\n enterpriseUrl?: string;\n};\n\nconst decode = (s: string): string => atob(s);\n// Copilot's public client ID, encoded to mirror the upstream references.\nconst CLIENT_ID = decode('SXYxLmI1MDdhMDhjODdlY2ZlOTg=');\n\nexport const COPILOT_USER_AGENT = 'GitHubCopilotChat/0.35.0';\n\n/** Headers that Copilot endpoints expect from a VS Code-like client. */\nexport const COPILOT_HEADERS = {\n 'User-Agent': COPILOT_USER_AGENT,\n 'Editor-Version': 'vscode/1.107.0',\n 'Editor-Plugin-Version': 'copilot-chat/0.35.0',\n 'Copilot-Integration-Id': 'vscode-chat',\n} as const;\n\nconst INITIAL_POLL_INTERVAL_MULTIPLIER = 1.2;\nconst SLOW_DOWN_POLL_INTERVAL_MULTIPLIER = 1.4;\nconst OAUTH_REQUEST_TIMEOUT_MS = 15_000;\n\nfunction requestSignal(signal?: AbortSignal): AbortSignal {\n const timeout = AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);\n return signal ? AbortSignal.any([timeout, signal]) : timeout;\n}\n\ntype DeviceCodeResponse = {\n device_code: string;\n user_code: string;\n verification_uri: string;\n interval: number;\n expires_in: number;\n};\n\ntype DeviceTokenSuccessResponse = {\n access_token: string;\n token_type?: string;\n scope?: string;\n};\n\ntype DeviceTokenErrorResponse = {\n error: string;\n error_description?: string;\n interval?: number;\n};\n\n/**\n * Normalize a user-entered Enterprise URL/domain into a hostname (e.g. `company.ghe.com`).\n * Returns null for an empty input or an unparseable URL.\n */\nexport function normalizeDomain(input: string): string | null {\n const trimmed = input.trim();\n if (!trimmed) return null;\n try {\n const url = trimmed.includes('://') ? new URL(trimmed) : new URL(`https://${trimmed}`);\n return url.hostname || null;\n } catch {\n return null;\n }\n}\n\nfunction getUrls(domain: string): {\n deviceCodeUrl: string;\n accessTokenUrl: string;\n copilotTokenUrl: string;\n} {\n return {\n deviceCodeUrl: `https://${domain}/login/device/code`,\n accessTokenUrl: `https://${domain}/login/oauth/access_token`,\n copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,\n };\n}\n\n/**\n * Parse the `proxy-ep` segment from a Copilot bearer token and convert to its API base URL.\n * Token shape: `tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...`\n */\nfunction getBaseUrlFromToken(token: string): string | null {\n const match = token.match(/proxy-ep=([^;]+)/);\n if (!match) return null;\n const proxyHost = match[1]!;\n const apiHost = proxyHost.replace(/^proxy\\./, 'api.');\n return `https://${apiHost}`;\n}\n\n/**\n * Resolve the Copilot API base URL.\n * Prefers the `proxy-ep` parsed from the bearer token, then falls back to enterprise/individual defaults.\n */\nexport function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {\n if (token) {\n const fromToken = getBaseUrlFromToken(token);\n if (fromToken) return fromToken;\n }\n if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n return 'https://api.individual.githubcopilot.com';\n}\n\nasync function fetchJson(url: string, init: RequestInit, signal?: AbortSignal): Promise<unknown> {\n const response = await fetch(url, { ...init, signal: requestSignal(signal) });\n if (!response.ok) {\n throw new Error(`${response.status} ${response.statusText}`.trim());\n }\n return response.json();\n}\n\nasync function startDeviceFlow(domain: string, signal?: AbortSignal): Promise<DeviceCodeResponse> {\n const urls = getUrls(domain);\n const data = await fetchJson(\n urls.deviceCodeUrl,\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': COPILOT_USER_AGENT,\n },\n body: new URLSearchParams({\n client_id: CLIENT_ID,\n scope: 'read:user',\n }),\n },\n signal,\n );\n\n if (!data || typeof data !== 'object') {\n throw new Error('Invalid device code response');\n }\n\n const obj = data as Record<string, unknown>;\n const deviceCode = obj.device_code;\n const userCode = obj.user_code;\n const verificationUri = obj.verification_uri;\n const interval = obj.interval;\n const expiresIn = obj.expires_in;\n\n if (\n typeof deviceCode !== 'string' ||\n typeof userCode !== 'string' ||\n typeof verificationUri !== 'string' ||\n typeof interval !== 'number' ||\n typeof expiresIn !== 'number'\n ) {\n throw new Error('Invalid device code response fields');\n }\n\n return {\n device_code: deviceCode,\n user_code: userCode,\n verification_uri: verificationUri,\n interval,\n expires_in: expiresIn,\n };\n}\n\n/** Sleep that can be interrupted by an AbortSignal. */\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new Error('Login cancelled'));\n return;\n }\n\n let timeout: ReturnType<typeof setTimeout>;\n const onAbort = () => {\n clearTimeout(timeout);\n reject(new Error('Login cancelled'));\n };\n\n timeout = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/**\n * Serializable pending state for a Copilot device-code login. Safe to persist\n * (e.g. a `pending jsonb` column) so polling can span HTTP requests/replicas.\n */\nexport interface CopilotDeviceLoginPending {\n /** GitHub host performing the flow (`github.com` or an Enterprise hostname). */\n domain: string;\n /** Enterprise hostname, or undefined for github.com. Recorded on the final credentials. */\n enterpriseDomain?: string;\n deviceCode: string;\n userCode: string;\n /** Verification URL for the user to open. */\n url: string;\n instructions: string;\n /** Current base poll interval in ms (grows on slow_down). */\n intervalMs: number;\n /** Safety margin applied to intervalMs when computing the next wait. */\n intervalMultiplier: number;\n /** Number of slow_down responses seen (used for the clock-drift hint). */\n slowDownCount: number;\n /** ms epoch after which the device authorization expires. */\n deadlineAt: number;\n}\n\nexport type CopilotDevicePollResult =\n | { status: 'complete'; credentials: GitHubCopilotCredentials }\n | { status: 'pending'; pending: CopilotDeviceLoginPending; nextPollMs: number }\n | { status: 'failed'; error: string };\n\n/** Delay to wait before the next upstream poll for a pending Copilot device login. */\nexport function copilotNextPollDelayMs(pending: CopilotDeviceLoginPending): number {\n return Math.ceil(pending.intervalMs * pending.intervalMultiplier);\n}\n\n/**\n * Start a Copilot device-code login: request a user code and return the\n * serializable pending state for subsequent polls. Wait `copilotNextPollDelayMs`\n * before the first poll.\n */\nexport async function startGitHubCopilotDeviceLogin(\n enterpriseDomain?: string,\n options?: { signal?: AbortSignal },\n): Promise<CopilotDeviceLoginPending> {\n const domain = enterpriseDomain || 'github.com';\n const device = await startDeviceFlow(domain, options?.signal);\n\n return {\n domain,\n ...(enterpriseDomain ? { enterpriseDomain } : {}),\n deviceCode: device.device_code,\n userCode: device.user_code,\n url: device.verification_uri,\n instructions: `Enter code: ${device.user_code}`,\n intervalMs: Math.max(1000, Math.floor(device.interval * 1000)),\n intervalMultiplier: INITIAL_POLL_INTERVAL_MULTIPLIER,\n slowDownCount: 0,\n deadlineAt: Date.now() + device.expires_in * 1000,\n };\n}\n\n/**\n * Perform exactly one upstream poll for a pending Copilot device login.\n * On success, exchanges the GitHub OAuth token for a Copilot bearer token and\n * returns full credentials. Never throws for flow-level conditions; network\n * errors from fetch still propagate.\n */\nexport async function pollGitHubCopilotDeviceLogin(\n pending: CopilotDeviceLoginPending,\n options?: { signal?: AbortSignal; onProgress?: (message: string) => void },\n): Promise<CopilotDevicePollResult> {\n if (Date.now() >= pending.deadlineAt) {\n if (pending.slowDownCount > 0) {\n return {\n status: 'failed',\n error:\n 'Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.',\n };\n }\n return { status: 'failed', error: 'Device flow timed out' };\n }\n\n const urls = getUrls(pending.domain);\n const raw = await fetchJson(\n urls.accessTokenUrl,\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': COPILOT_USER_AGENT,\n },\n body: new URLSearchParams({\n client_id: CLIENT_ID,\n device_code: pending.deviceCode,\n grant_type: 'urn:ietf:params:oauth:grant-type:device_code',\n }),\n },\n options?.signal,\n );\n\n if (raw && typeof raw === 'object' && typeof (raw as DeviceTokenSuccessResponse).access_token === 'string') {\n const githubAccessToken = (raw as DeviceTokenSuccessResponse).access_token;\n options?.onProgress?.('Fetching Copilot token...');\n const credentials = await refreshGitHubCopilotToken(githubAccessToken, pending.enterpriseDomain, options?.signal);\n return { status: 'complete', credentials };\n }\n\n if (raw && typeof raw === 'object' && typeof (raw as DeviceTokenErrorResponse).error === 'string') {\n const { error, error_description: description, interval } = raw as DeviceTokenErrorResponse;\n\n if (error === 'slow_down') {\n const next: CopilotDeviceLoginPending = {\n ...pending,\n slowDownCount: pending.slowDownCount + 1,\n intervalMs:\n typeof interval === 'number' && interval > 0 ? interval * 1000 : Math.max(1000, pending.intervalMs + 5000),\n intervalMultiplier: SLOW_DOWN_POLL_INTERVAL_MULTIPLIER,\n };\n return { status: 'pending', pending: next, nextPollMs: copilotNextPollDelayMs(next) };\n }\n\n if (error !== 'authorization_pending') {\n const descriptionSuffix = description ? `: ${description}` : '';\n return { status: 'failed', error: `Device flow failed: ${error}${descriptionSuffix}` };\n }\n }\n\n // authorization_pending or an unrecognized-but-OK response: keep polling.\n return { status: 'pending', pending, nextPollMs: copilotNextPollDelayMs(pending) };\n}\n\n/**\n * Refresh the short-lived Copilot bearer token using the long-lived GitHub OAuth token.\n *\n * `refreshToken` here is the GitHub OAuth access token; the Copilot endpoint returns a\n * fresh bearer token (`token`) and absolute expiry (`expires_at`, seconds since epoch).\n */\nexport async function refreshGitHubCopilotToken(\n refreshToken: string,\n enterpriseDomain?: string,\n signal?: AbortSignal,\n): Promise<GitHubCopilotCredentials> {\n const domain = enterpriseDomain || 'github.com';\n const urls = getUrls(domain);\n\n const raw = await fetchJson(\n urls.copilotTokenUrl,\n {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${refreshToken}`,\n ...COPILOT_HEADERS,\n },\n },\n signal,\n );\n\n if (!raw || typeof raw !== 'object') {\n throw new Error('Invalid Copilot token response');\n }\n\n const obj = raw as Record<string, unknown>;\n const token = obj.token;\n const expiresAt = obj.expires_at;\n\n if (typeof token !== 'string' || typeof expiresAt !== 'number') {\n throw new Error('Invalid Copilot token response fields');\n }\n\n const credentials: GitHubCopilotCredentials = {\n refresh: refreshToken,\n access: token,\n // expires_at is seconds; subtract 5 minutes so we refresh before actual expiry.\n expires: expiresAt * 1000 - 5 * 60 * 1000,\n };\n if (enterpriseDomain) {\n credentials.enterpriseUrl = enterpriseDomain;\n }\n return credentials;\n}\n\n/**\n * Login with GitHub Copilot OAuth (device-code flow).\n *\n * Prompts for an optional GitHub Enterprise URL/domain, performs the device-code flow,\n * then exchanges the GitHub OAuth token for a Copilot bearer token.\n */\nexport async function loginGitHubCopilot(options: {\n onAuth: (url: string, instructions?: string) => void;\n onPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;\n onProgress?: (message: string) => void;\n signal?: AbortSignal;\n}): Promise<GitHubCopilotCredentials> {\n const input = await options.onPrompt({\n message: 'GitHub Enterprise URL/domain (blank for github.com)',\n placeholder: 'company.ghe.com',\n allowEmpty: true,\n });\n\n if (options.signal?.aborted) {\n throw new Error('Login cancelled');\n }\n\n const trimmed = input.trim();\n const enterpriseDomain = normalizeDomain(input);\n if (trimmed && !enterpriseDomain) {\n throw new Error('Invalid GitHub Enterprise URL/domain');\n }\n let pending = await startGitHubCopilotDeviceLogin(enterpriseDomain ?? undefined, { signal: options.signal });\n options.onAuth(pending.url, pending.instructions);\n\n while (true) {\n if (options.signal?.aborted) {\n throw new Error('Login cancelled');\n }\n\n // Wait before polling (safety margin over the server interval), clamped to the deadline.\n const remainingMs = Math.max(pending.deadlineAt - Date.now(), 0);\n await abortableSleep(Math.min(copilotNextPollDelayMs(pending), remainingMs), options.signal);\n\n const result = await pollGitHubCopilotDeviceLogin(pending, {\n signal: options.signal,\n onProgress: options.onProgress,\n });\n if (result.status === 'complete') {\n return result.credentials;\n }\n if (result.status === 'failed') {\n throw new Error(result.error);\n }\n pending = result.pending;\n }\n}\n\n/**\n * Filtered, normalized entry from the Copilot `/models` API.\n *\n * The full API payload includes a lot of capability metadata; we only keep the bits\n * we need to expose models in `listAvailableModels()` and route requests sensibly.\n */\nexport type CopilotModelEntry = {\n /** Stable model id (e.g. `claude-sonnet-4.5`, `gpt-4.1`). */\n id: string;\n /** Human-readable display name (e.g. `Claude Sonnet 4.5`). */\n name: string;\n /** Vendor field from the API (e.g. `Anthropic`, `OpenAI`). */\n vendor: string;\n /**\n * Endpoints the model exposes (e.g. `/chat/completions`, `/responses`, `/v1/messages`).\n * Empty when the API didn't return `supported_endpoints` for this entry.\n */\n supportedEndpoints: string[];\n /** True when `supported_endpoints` includes `/v1/messages` (Anthropic-shaped Copilot model). */\n isAnthropicShaped: boolean;\n /** True when `capabilities.supports.vision` is true. */\n supportsVision: boolean;\n /** True when the model supports tool calling. */\n supportsToolCalls: boolean;\n};\n\n/**\n * Fetch the Copilot model list available to the current subscription.\n *\n * Hits `${baseURL}/models` with the Copilot bearer token, filters to\n * `model_picker_enabled === true && policy.state !== 'disabled'`, and returns\n * a normalized list. Mirrors opencode's filtering rules.\n *\n * Inspired by:\n * - opencode: https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/plugin/github-copilot/models.ts\n */\nexport async function fetchCopilotModels(opts: {\n baseUrl: string;\n bearerToken: string;\n signal?: AbortSignal;\n}): Promise<CopilotModelEntry[]> {\n const url = `${opts.baseUrl.replace(/\\/$/, '')}/models`;\n const response = await fetch(url, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${opts.bearerToken}`,\n ...COPILOT_HEADERS,\n },\n signal: requestSignal(opts.signal),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch Copilot models: ${response.status} ${response.statusText}`.trim());\n }\n\n const json = await response.json().catch(() => null);\n if (!json || typeof json !== 'object' || !Array.isArray((json as { data?: unknown }).data)) {\n throw new Error('Invalid Copilot models response: missing `data` array');\n }\n\n const data = (json as { data: unknown[] }).data;\n const result: CopilotModelEntry[] = [];\n\n for (const item of data) {\n if (!item || typeof item !== 'object') continue;\n const obj = item as Record<string, unknown>;\n\n if (obj.model_picker_enabled !== true) continue;\n\n const policy = obj.policy as Record<string, unknown> | undefined;\n if (policy && policy.state === 'disabled') continue;\n\n const id = obj.id;\n if (typeof id !== 'string' || !id) continue;\n\n const name = typeof obj.name === 'string' ? obj.name : id;\n const vendor = typeof obj.vendor === 'string' ? obj.vendor : '';\n\n const capabilities = obj.capabilities as Record<string, unknown> | undefined;\n const supports = capabilities?.supports as Record<string, unknown> | undefined;\n const supportsVision = supports?.vision === true;\n const supportsToolCalls = supports?.tool_calls === true;\n\n const rawEndpoints = obj.supported_endpoints;\n const supportedEndpoints = Array.isArray(rawEndpoints)\n ? rawEndpoints.filter((e): e is string => typeof e === 'string')\n : [];\n const isAnthropicShaped = supportedEndpoints.includes('/v1/messages');\n\n result.push({\n id,\n name,\n vendor,\n supportedEndpoints,\n isAnthropicShaped,\n supportsVision,\n supportsToolCalls,\n });\n }\n\n return result;\n}\n\nexport const githubCopilotOAuthProvider: OAuthProviderInterface = {\n id: 'github-copilot',\n name: 'GitHub Copilot',\n\n async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n return loginGitHubCopilot({\n onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n onPrompt: callbacks.onPrompt,\n onProgress: callbacks.onProgress,\n signal: callbacks.signal,\n });\n },\n\n async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n const creds = credentials as GitHubCopilotCredentials;\n return refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);\n },\n\n getApiKey(credentials: OAuthCredentials): string {\n return credentials.access;\n },\n\n /**\n * The stored refresh token is the long-lived GitHub OAuth token — use it to\n * resolve the account's GitHub login for the account manager. Enterprise\n * tokens don't work against api.github.com, which just falls through to the\n * label prompt/default.\n */\n async getAccountLabel(credentials: OAuthCredentials): Promise<string | undefined> {\n // Enterprise credentials authenticate against the enterprise host, not\n // GitHub.com — sending that token to api.github.com would disclose it.\n // Enterprise accounts fall through to the label prompt/default instead.\n if ((credentials as GitHubCopilotCredentials).enterpriseUrl) return undefined;\n try {\n const response = await fetch('https://api.github.com/user', {\n headers: {\n Authorization: `Bearer ${credentials.refresh}`,\n Accept: 'application/vnd.github+json',\n },\n signal: requestSignal(),\n });\n if (!response.ok) return undefined;\n const user = (await response.json()) as { login?: unknown };\n return typeof user.login === 'string' && user.login.length > 0 ? user.login : undefined;\n } catch {\n return undefined;\n }\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"github-copilot.js","names":[],"sources":["../../../src/auth/providers/github-copilot.ts"],"sourcesContent":["/**\n * GitHub Copilot OAuth flow\n *\n * Inspired by:\n * - pi-mono: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/github-copilot.ts\n * - opencode: https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/plugin/github-copilot/copilot.ts\n *\n * Storage layout in AuthStorage:\n * - `refresh` holds the long-lived GitHub OAuth access token (used to mint Copilot tokens).\n * - `access` holds the short-lived Copilot bearer token (~30 minutes).\n * - `expires` is the Copilot bearer expiry (ms epoch, with a 5-minute safety buffer).\n * - `enterpriseUrl` is the GitHub Enterprise hostname, or undefined for github.com.\n */\n\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from '../types.js';\n\nexport type GitHubCopilotCredentials = OAuthCredentials & {\n enterpriseUrl?: string;\n};\n\nconst decode = (s: string): string => atob(s);\n// Copilot's public client ID, encoded to mirror the upstream references.\nconst CLIENT_ID = decode('SXYxLmI1MDdhMDhjODdlY2ZlOTg=');\n\nexport const COPILOT_USER_AGENT = 'GitHubCopilotChat/0.35.0';\n\n/** Headers that Copilot endpoints expect from a VS Code-like client. */\nexport const COPILOT_HEADERS = {\n 'User-Agent': COPILOT_USER_AGENT,\n 'Editor-Version': 'vscode/1.107.0',\n 'Editor-Plugin-Version': 'copilot-chat/0.35.0',\n 'Copilot-Integration-Id': 'vscode-chat',\n} as const;\n\nconst INITIAL_POLL_INTERVAL_MULTIPLIER = 1.2;\nconst SLOW_DOWN_POLL_INTERVAL_MULTIPLIER = 1.4;\nconst OAUTH_REQUEST_TIMEOUT_MS = 15_000;\n\nfunction requestSignal(signal?: AbortSignal): AbortSignal {\n const timeout = AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);\n return signal ? AbortSignal.any([timeout, signal]) : timeout;\n}\n\ntype DeviceCodeResponse = {\n device_code: string;\n user_code: string;\n verification_uri: string;\n interval: number;\n expires_in: number;\n};\n\ntype DeviceTokenSuccessResponse = {\n access_token: string;\n token_type?: string;\n scope?: string;\n};\n\ntype DeviceTokenErrorResponse = {\n error: string;\n error_description?: string;\n interval?: number;\n};\n\n/**\n * Normalize a user-entered Enterprise URL/domain into a hostname (e.g. `company.ghe.com`).\n * Returns null for an empty input or an unparseable URL.\n */\nexport function normalizeDomain(input: string): string | null {\n const trimmed = input.trim();\n if (!trimmed) return null;\n try {\n const url = trimmed.includes('://') ? new URL(trimmed) : new URL(`https://${trimmed}`);\n return url.hostname || null;\n } catch {\n return null;\n }\n}\n\nfunction getUrls(domain: string): {\n deviceCodeUrl: string;\n accessTokenUrl: string;\n copilotTokenUrl: string;\n} {\n return {\n deviceCodeUrl: `https://${domain}/login/device/code`,\n accessTokenUrl: `https://${domain}/login/oauth/access_token`,\n copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,\n };\n}\n\n/**\n * Parse the `proxy-ep` segment from a Copilot bearer token and convert to its API base URL.\n * Token shape: `tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...`\n */\nfunction getBaseUrlFromToken(token: string): string | null {\n const match = token.match(/proxy-ep=([^;]+)/);\n if (!match) return null;\n const proxyHost = match[1]!;\n const apiHost = proxyHost.replace(/^proxy\\./, 'api.');\n return `https://${apiHost}`;\n}\n\n/**\n * Resolve the Copilot API base URL.\n * Prefers the `proxy-ep` parsed from the bearer token, then falls back to enterprise/individual defaults.\n */\nexport function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {\n if (token) {\n const fromToken = getBaseUrlFromToken(token);\n if (fromToken) return fromToken;\n }\n if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n return 'https://api.individual.githubcopilot.com';\n}\n\nasync function fetchJson(url: string, init: RequestInit, signal?: AbortSignal): Promise<unknown> {\n const response = await fetch(url, { ...init, signal: requestSignal(signal) });\n if (!response.ok) {\n throw new Error(`${response.status} ${response.statusText}`.trim());\n }\n return response.json();\n}\n\nasync function startDeviceFlow(domain: string, signal?: AbortSignal): Promise<DeviceCodeResponse> {\n const urls = getUrls(domain);\n const data = await fetchJson(\n urls.deviceCodeUrl,\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': COPILOT_USER_AGENT,\n },\n body: new URLSearchParams({\n client_id: CLIENT_ID,\n scope: 'read:user',\n }),\n },\n signal,\n );\n\n if (!data || typeof data !== 'object') {\n throw new Error('Invalid device code response');\n }\n\n const obj = data as Record<string, unknown>;\n const deviceCode = obj.device_code;\n const userCode = obj.user_code;\n const verificationUri = obj.verification_uri;\n const interval = obj.interval;\n const expiresIn = obj.expires_in;\n\n if (\n typeof deviceCode !== 'string' ||\n typeof userCode !== 'string' ||\n typeof verificationUri !== 'string' ||\n typeof interval !== 'number' ||\n typeof expiresIn !== 'number'\n ) {\n throw new Error('Invalid device code response fields');\n }\n\n return {\n device_code: deviceCode,\n user_code: userCode,\n verification_uri: verificationUri,\n interval,\n expires_in: expiresIn,\n };\n}\n\n/** Sleep that can be interrupted by an AbortSignal. */\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new Error('Login cancelled'));\n return;\n }\n\n let timeout: ReturnType<typeof setTimeout>;\n const onAbort = () => {\n clearTimeout(timeout);\n reject(new Error('Login cancelled'));\n };\n\n timeout = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/**\n * Serializable pending state for a Copilot device-code login. Safe to persist\n * (e.g. a `pending jsonb` column) so polling can span HTTP requests/replicas.\n */\nexport interface CopilotDeviceLoginPending {\n /** GitHub host performing the flow (`github.com` or an Enterprise hostname). */\n domain: string;\n /** Enterprise hostname, or undefined for github.com. Recorded on the final credentials. */\n enterpriseDomain?: string;\n deviceCode: string;\n userCode: string;\n /** Verification URL for the user to open. */\n url: string;\n instructions: string;\n /** Current base poll interval in ms (grows on slow_down). */\n intervalMs: number;\n /** Safety margin applied to intervalMs when computing the next wait. */\n intervalMultiplier: number;\n /** Number of slow_down responses seen (used for the clock-drift hint). */\n slowDownCount: number;\n /** ms epoch after which the device authorization expires. */\n deadlineAt: number;\n}\n\nexport type CopilotDevicePollResult =\n | { status: 'complete'; credentials: GitHubCopilotCredentials }\n | { status: 'pending'; pending: CopilotDeviceLoginPending; nextPollMs: number }\n | { status: 'failed'; error: string };\n\n/** Delay to wait before the next upstream poll for a pending Copilot device login. */\nexport function copilotNextPollDelayMs(pending: CopilotDeviceLoginPending): number {\n return Math.ceil(pending.intervalMs * pending.intervalMultiplier);\n}\n\n/**\n * Start a Copilot device-code login: request a user code and return the\n * serializable pending state for subsequent polls. Wait `copilotNextPollDelayMs`\n * before the first poll.\n */\nexport async function startGitHubCopilotDeviceLogin(\n enterpriseDomain?: string,\n options?: { signal?: AbortSignal },\n): Promise<CopilotDeviceLoginPending> {\n const domain = enterpriseDomain || 'github.com';\n const device = await startDeviceFlow(domain, options?.signal);\n\n return {\n domain,\n ...(enterpriseDomain ? { enterpriseDomain } : {}),\n deviceCode: device.device_code,\n userCode: device.user_code,\n url: device.verification_uri,\n instructions: `Enter code: ${device.user_code}`,\n intervalMs: Math.max(1000, Math.floor(device.interval * 1000)),\n intervalMultiplier: INITIAL_POLL_INTERVAL_MULTIPLIER,\n slowDownCount: 0,\n deadlineAt: Date.now() + device.expires_in * 1000,\n };\n}\n\n/**\n * Perform exactly one upstream poll for a pending Copilot device login.\n * On success, exchanges the GitHub OAuth token for a Copilot bearer token and\n * returns full credentials. Never throws for flow-level conditions; network\n * errors from fetch still propagate.\n */\nexport async function pollGitHubCopilotDeviceLogin(\n pending: CopilotDeviceLoginPending,\n options?: { signal?: AbortSignal; onProgress?: (message: string) => void },\n): Promise<CopilotDevicePollResult> {\n if (Date.now() >= pending.deadlineAt) {\n if (pending.slowDownCount > 0) {\n return {\n status: 'failed',\n error:\n 'Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.',\n };\n }\n return { status: 'failed', error: 'Device flow timed out' };\n }\n\n const urls = getUrls(pending.domain);\n const raw = await fetchJson(\n urls.accessTokenUrl,\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': COPILOT_USER_AGENT,\n },\n body: new URLSearchParams({\n client_id: CLIENT_ID,\n device_code: pending.deviceCode,\n grant_type: 'urn:ietf:params:oauth:grant-type:device_code',\n }),\n },\n options?.signal,\n );\n\n if (raw && typeof raw === 'object' && typeof (raw as DeviceTokenSuccessResponse).access_token === 'string') {\n const githubAccessToken = (raw as DeviceTokenSuccessResponse).access_token;\n options?.onProgress?.('Fetching Copilot token...');\n const credentials = await refreshGitHubCopilotToken(githubAccessToken, pending.enterpriseDomain, options?.signal);\n return { status: 'complete', credentials };\n }\n\n if (raw && typeof raw === 'object' && typeof (raw as DeviceTokenErrorResponse).error === 'string') {\n const { error, error_description: description, interval } = raw as DeviceTokenErrorResponse;\n\n if (error === 'slow_down') {\n const next: CopilotDeviceLoginPending = {\n ...pending,\n slowDownCount: pending.slowDownCount + 1,\n intervalMs:\n typeof interval === 'number' && interval > 0 ? interval * 1000 : Math.max(1000, pending.intervalMs + 5000),\n intervalMultiplier: SLOW_DOWN_POLL_INTERVAL_MULTIPLIER,\n };\n return { status: 'pending', pending: next, nextPollMs: copilotNextPollDelayMs(next) };\n }\n\n if (error !== 'authorization_pending') {\n const descriptionSuffix = description ? `: ${description}` : '';\n return { status: 'failed', error: `Device flow failed: ${error}${descriptionSuffix}` };\n }\n }\n\n // authorization_pending or an unrecognized-but-OK response: keep polling.\n return { status: 'pending', pending, nextPollMs: copilotNextPollDelayMs(pending) };\n}\n\n/**\n * Refresh the short-lived Copilot bearer token using the long-lived GitHub OAuth token.\n *\n * `refreshToken` here is the GitHub OAuth access token; the Copilot endpoint returns a\n * fresh bearer token (`token`) and absolute expiry (`expires_at`, seconds since epoch).\n */\nexport async function refreshGitHubCopilotToken(\n refreshToken: string,\n enterpriseDomain?: string,\n signal?: AbortSignal,\n): Promise<GitHubCopilotCredentials> {\n const domain = enterpriseDomain || 'github.com';\n const urls = getUrls(domain);\n\n const raw = await fetchJson(\n urls.copilotTokenUrl,\n {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${refreshToken}`,\n ...COPILOT_HEADERS,\n },\n },\n signal,\n );\n\n if (!raw || typeof raw !== 'object') {\n throw new Error('Invalid Copilot token response');\n }\n\n const obj = raw as Record<string, unknown>;\n const token = obj.token;\n const expiresAt = obj.expires_at;\n\n if (typeof token !== 'string' || typeof expiresAt !== 'number') {\n throw new Error('Invalid Copilot token response fields');\n }\n\n const credentials: GitHubCopilotCredentials = {\n refresh: refreshToken,\n access: token,\n // expires_at is seconds; subtract 5 minutes so we refresh before actual expiry.\n expires: expiresAt * 1000 - 5 * 60 * 1000,\n };\n if (enterpriseDomain) {\n credentials.enterpriseUrl = enterpriseDomain;\n }\n return credentials;\n}\n\n/**\n * Login with GitHub Copilot OAuth (device-code flow).\n *\n * Prompts for an optional GitHub Enterprise URL/domain, performs the device-code flow,\n * then exchanges the GitHub OAuth token for a Copilot bearer token.\n */\nexport async function loginGitHubCopilot(options: {\n onAuth: (url: string, instructions?: string) => void;\n onPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;\n onProgress?: (message: string) => void;\n signal?: AbortSignal;\n}): Promise<GitHubCopilotCredentials> {\n const input = await options.onPrompt({\n message: 'GitHub Enterprise URL/domain (blank for github.com)',\n placeholder: 'company.ghe.com',\n allowEmpty: true,\n });\n\n if (options.signal?.aborted) {\n throw new Error('Login cancelled');\n }\n\n const trimmed = input.trim();\n const enterpriseDomain = normalizeDomain(input);\n if (trimmed && !enterpriseDomain) {\n throw new Error('Invalid GitHub Enterprise URL/domain');\n }\n let pending = await startGitHubCopilotDeviceLogin(enterpriseDomain ?? undefined, { signal: options.signal });\n options.onAuth(pending.url, pending.instructions);\n\n while (true) {\n if (options.signal?.aborted) {\n throw new Error('Login cancelled');\n }\n\n // Wait before polling (safety margin over the server interval), clamped to the deadline.\n const remainingMs = Math.max(pending.deadlineAt - Date.now(), 0);\n await abortableSleep(Math.min(copilotNextPollDelayMs(pending), remainingMs), options.signal);\n\n const result = await pollGitHubCopilotDeviceLogin(pending, {\n signal: options.signal,\n onProgress: options.onProgress,\n });\n if (result.status === 'complete') {\n return result.credentials;\n }\n if (result.status === 'failed') {\n throw new Error(result.error);\n }\n pending = result.pending;\n }\n}\n\n/**\n * Filtered, normalized entry from the Copilot `/models` API.\n *\n * The full API payload includes a lot of capability metadata; we only keep the bits\n * we need to expose models in `listAvailableModels()` and route requests sensibly.\n */\nexport type CopilotModelEntry = {\n /** Stable model id (e.g. `claude-sonnet-4.5`, `gpt-4.1`). */\n id: string;\n /** Human-readable display name (e.g. `Claude Sonnet 4.5`). */\n name: string;\n /** Vendor field from the API (e.g. `Anthropic`, `OpenAI`). */\n vendor: string;\n /**\n * Endpoints the model exposes (e.g. `/chat/completions`, `/responses`, `/v1/messages`).\n * Empty when the API didn't return `supported_endpoints` for this entry.\n */\n supportedEndpoints: string[];\n /** True when `supported_endpoints` includes `/v1/messages` (Anthropic-shaped Copilot model). */\n isAnthropicShaped: boolean;\n /** True when `capabilities.supports.vision` is true. */\n supportsVision: boolean;\n /** True when the model supports tool calling. */\n supportsToolCalls: boolean;\n};\n\n/**\n * Fetch the Copilot model list available to the current subscription.\n *\n * Hits `${baseURL}/models` with the Copilot bearer token, filters to\n * `model_picker_enabled === true && policy.state !== 'disabled'`, and returns\n * a normalized list. Mirrors opencode's filtering rules.\n *\n * Inspired by:\n * - opencode: https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/plugin/github-copilot/models.ts\n */\nexport async function fetchCopilotModels(opts: {\n baseUrl: string;\n bearerToken: string;\n signal?: AbortSignal;\n}): Promise<CopilotModelEntry[]> {\n const url = `${opts.baseUrl.replace(/\\/$/, '')}/models`;\n const response = await fetch(url, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${opts.bearerToken}`,\n ...COPILOT_HEADERS,\n },\n signal: requestSignal(opts.signal),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch Copilot models: ${response.status} ${response.statusText}`.trim());\n }\n\n const json = await response.json().catch(() => null);\n if (!json || typeof json !== 'object' || !Array.isArray((json as { data?: unknown }).data)) {\n throw new Error('Invalid Copilot models response: missing `data` array');\n }\n\n const data = (json as { data: unknown[] }).data;\n const result: CopilotModelEntry[] = [];\n\n for (const item of data) {\n if (!item || typeof item !== 'object') continue;\n const obj = item as Record<string, unknown>;\n\n if (obj.model_picker_enabled !== true) continue;\n\n const policy = obj.policy as Record<string, unknown> | undefined;\n if (policy && policy.state === 'disabled') continue;\n\n const id = obj.id;\n if (typeof id !== 'string' || !id) continue;\n\n const name = typeof obj.name === 'string' ? obj.name : id;\n const vendor = typeof obj.vendor === 'string' ? obj.vendor : '';\n\n const capabilities = obj.capabilities as Record<string, unknown> | undefined;\n const supports = capabilities?.supports as Record<string, unknown> | undefined;\n const supportsVision = supports?.vision === true;\n const supportsToolCalls = supports?.tool_calls === true;\n\n const rawEndpoints = obj.supported_endpoints;\n const supportedEndpoints = Array.isArray(rawEndpoints)\n ? rawEndpoints.filter((e): e is string => typeof e === 'string')\n : [];\n const isAnthropicShaped = supportedEndpoints.includes('/v1/messages');\n\n result.push({\n id,\n name,\n vendor,\n supportedEndpoints,\n isAnthropicShaped,\n supportsVision,\n supportsToolCalls,\n });\n }\n\n return result;\n}\n\nexport const githubCopilotOAuthProvider: OAuthProviderInterface = {\n id: 'github-copilot',\n name: 'GitHub Copilot',\n\n async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n return loginGitHubCopilot({\n onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n onPrompt: callbacks.onPrompt,\n onProgress: callbacks.onProgress,\n signal: callbacks.signal,\n });\n },\n\n async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n const creds = credentials as GitHubCopilotCredentials;\n return refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);\n },\n\n getApiKey(credentials: OAuthCredentials): string {\n return credentials.access;\n },\n\n /**\n * The stored refresh token is the long-lived GitHub OAuth token — use it to\n * resolve the account's GitHub login for the account manager. Enterprise\n * tokens don't work against api.github.com, which just falls through to the\n * label prompt/default.\n */\n async getAccountLabel(credentials: OAuthCredentials): Promise<string | undefined> {\n // Enterprise credentials authenticate against the enterprise host, not\n // GitHub.com — sending that token to api.github.com would disclose it.\n // Enterprise accounts fall through to the label prompt/default instead.\n if ((credentials as GitHubCopilotCredentials).enterpriseUrl) return undefined;\n try {\n const response = await fetch('https://api.github.com/user', {\n headers: {\n Authorization: `Bearer ${credentials.refresh}`,\n Accept: 'application/vnd.github+json',\n },\n signal: requestSignal(),\n });\n if (!response.ok) return undefined;\n const user = (await response.json()) as { login?: unknown };\n return typeof user.login === 'string' && user.login.length > 0 ? user.login : undefined;\n } catch {\n return undefined;\n }\n },\n};\n"],"mappings":"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"}
@@ -36,7 +36,7 @@ const DEVICE_REDIRECT_URI = `${ISSUER}/deviceauth/callback`;
36
36
  const DEFAULT_CALLBACK_PORT = 1455;
37
37
  const FALLBACK_CALLBACK_PORT = 1457;
38
38
  const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;
39
- const DEVICE_AUTH_TIMEOUT_MS = 900 * 1e3;
39
+ const DEVICE_AUTH_TIMEOUT_MS = 9e5;
40
40
  const OAUTH_REQUEST_TIMEOUT_MS = 15e3;
41
41
  const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
42
42
  const JWT_CLAIM_PATH = "https://api.openai.com/auth";
@@ -449,7 +449,8 @@ async function loginOpenAICodex(options) {
449
449
  if (result?.code) code = result.code;
450
450
  }
451
451
  if (!code) {
452
- const parsed = parseAuthorizationInput(await options.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }));
452
+ const input = await options.onPrompt({ message: "Paste the authorization code (or full redirect URL):" });
453
+ const parsed = parseAuthorizationInput(input);
453
454
  if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
454
455
  code = parsed.code;
455
456
  }
@@ -1 +1 @@
1
- {"version":3,"file":"openai-codex.js","names":[],"sources":["../../../src/auth/providers/openai-codex.ts"],"sourcesContent":["/**\n * OpenAI Codex (ChatGPT OAuth) flow\n *\n * Inspired by pi-mono's OAuth implementation:\n * https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/openai-codex.ts\n *\n * NOTE: This module uses Node.js http for the OAuth callback.\n * It is only intended for CLI use, not browser environments.\n */\n\n// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui)\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nlet _httpPromise: Promise<typeof import('node:http')> | null = null;\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nlet _http: typeof import('node:http') | null = null;\ntype HttpServer = {\n off: (event: 'error' | 'listening', listener: (...args: any[]) => void) => HttpServer;\n once: (event: 'error' | 'listening', listener: (...args: any[]) => void) => HttpServer;\n listen: (port: number, hostname: string) => HttpServer;\n close: () => void;\n};\nif (typeof process !== 'undefined' && (process.versions?.node || process.versions?.bun)) {\n _httpPromise = import('node:http').then(m => {\n _http = m;\n return m;\n });\n}\n\nimport { parseAuthorizationInput } from '../authorization-input.js';\nimport { generatePKCE } from '../pkce.js';\nimport type { AuthMode, OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from '../types.js';\n\nexport const OPENAI_CODEX_AUTH_MODES: ReadonlyArray<AuthMode> = [\n {\n id: 'browser',\n name: 'Browser (local callback)',\n description: 'Opens the browser and waits for the OAuth callback on localhost.',\n },\n {\n id: 'device',\n name: 'Device code (headless)',\n description: 'Shows a code to enter at openai.com — for SSH, remote, or no-browser environments.',\n },\n];\n\nconst CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';\nconst ISSUER = 'https://auth.openai.com';\nconst AUTHORIZE_URL = `${ISSUER}/oauth/authorize`;\nconst TOKEN_URL = `${ISSUER}/oauth/token`;\nconst DEVICE_USER_CODE_URL = `${ISSUER}/api/accounts/deviceauth/usercode`;\nconst DEVICE_TOKEN_URL = `${ISSUER}/api/accounts/deviceauth/token`;\nconst DEVICE_AUTHORIZE_URL = `${ISSUER}/codex/device`;\nconst DEVICE_REDIRECT_URI = `${ISSUER}/deviceauth/callback`;\nconst DEFAULT_CALLBACK_PORT = 1455;\nconst FALLBACK_CALLBACK_PORT = 1457;\nconst DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;\nconst DEVICE_AUTH_TIMEOUT_MS = 15 * 60 * 1000;\nconst OAUTH_REQUEST_TIMEOUT_MS = 15_000;\nconst SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke';\nconst JWT_CLAIM_PATH = 'https://api.openai.com/auth';\n\nconst SUCCESS_HTML = `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Authentication successful</title>\n</head>\n<body>\n <p>Authentication successful. Return to your terminal to continue.</p>\n</body>\n</html>`;\n\ntype TokenSuccess = {\n type: 'success';\n access: string;\n refresh: string;\n expires: number;\n idToken?: string;\n};\ntype TokenFailure = { type: 'failed' };\ntype TokenResult = TokenSuccess | TokenFailure;\n\ntype JwtPayload = {\n chatgpt_account_id?: string;\n email?: string;\n [JWT_CLAIM_PATH]?: {\n chatgpt_account_id?: string;\n email?: string;\n };\n [key: string]: unknown;\n};\n\nfunction requestSignal(signal?: AbortSignal): AbortSignal {\n const timeout = AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);\n return signal ? AbortSignal.any([timeout, signal]) : timeout;\n}\n\nasync function createState(): Promise<string> {\n if (typeof process === 'undefined' || !(process.versions?.node || process.versions?.bun)) {\n throw new Error('OpenAI Codex OAuth is only available in Node.js environments');\n }\n return Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(16))).toString('hex');\n}\n\nfunction decodeJwt(token: string): JwtPayload | null {\n try {\n const parts = token.split('.');\n if (parts.length !== 3) return null;\n const payload = parts[1] ?? '';\n const padded = payload\n .replace(/-/g, '+')\n .replace(/_/g, '/')\n .padEnd(Math.ceil(payload.length / 4) * 4, '=');\n const decoded = atob(padded);\n return JSON.parse(decoded) as JwtPayload;\n } catch {\n return null;\n }\n}\n\nfunction extractAccountIdFromClaims(payload: JwtPayload | null | undefined): string | null {\n if (!payload) return null;\n const accountId = payload.chatgpt_account_id ?? payload[JWT_CLAIM_PATH]?.chatgpt_account_id;\n return typeof accountId === 'string' && accountId.length > 0 ? accountId : null;\n}\n\nfunction getAccountId(tokens: { idToken?: string; access: string }, fallback?: string): string | undefined {\n const fromIdToken = tokens.idToken ? extractAccountIdFromClaims(decodeJwt(tokens.idToken)) : null;\n if (fromIdToken) return fromIdToken;\n\n const fromAccessToken = extractAccountIdFromClaims(decodeJwt(tokens.access));\n if (fromAccessToken) return fromAccessToken;\n\n return fallback;\n}\n\nfunction requireAccountId(tokens: { idToken?: string; access: string }, fallback?: string): string {\n const accountId = getAccountId(tokens, fallback);\n if (!accountId) {\n throw new Error('Failed to extract ChatGPT account id from OpenAI Codex token');\n }\n return accountId;\n}\n\nfunction extractEmailFromClaims(payload: JwtPayload): string | null {\n const email = payload.email ?? payload[JWT_CLAIM_PATH]?.email;\n return typeof email === 'string' && email.length > 0 ? email : null;\n}\n\nfunction getEmailFromTokens(tokens: { idToken?: string; access: string }): string | undefined {\n const fromIdToken = tokens.idToken ? extractEmailFromClaims(decodeJwt(tokens.idToken) ?? {}) : null;\n if (fromIdToken) return fromIdToken;\n const fromAccessToken = extractEmailFromClaims(decodeJwt(tokens.access) ?? {});\n return fromAccessToken ?? undefined;\n}\n\ntype TokenResponseJson = {\n id_token?: string;\n access_token?: string;\n refresh_token?: string;\n expires_in?: number;\n};\n\nfunction tokenResponseToResult(json: TokenResponseJson, logPrefix: string): TokenResult {\n if (!json.access_token || !json.refresh_token) {\n console.error(\n `[openai-codex] ${logPrefix} response missing required fields (access_token=${Boolean(json.access_token)}, refresh_token=${Boolean(json.refresh_token)})`,\n );\n return { type: 'failed' };\n }\n\n return {\n type: 'success',\n access: json.access_token,\n refresh: json.refresh_token,\n expires: Date.now() + (json.expires_in ?? DEFAULT_TOKEN_EXPIRES_IN_SECONDS) * 1000,\n idToken: json.id_token,\n };\n}\n\nasync function exchangeAuthorizationCode(\n code: string,\n verifier: string,\n redirectUri: string,\n signal?: AbortSignal,\n): Promise<TokenResult> {\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n signal: requestSignal(signal),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: CLIENT_ID,\n code,\n code_verifier: verifier,\n redirect_uri: redirectUri,\n }),\n });\n\n if (!response.ok) {\n console.error('[openai-codex] code->token failed:', response.status);\n return { type: 'failed' };\n }\n\n return tokenResponseToResult((await response.json()) as TokenResponseJson, 'token');\n}\n\nasync function refreshAccessToken(refreshToken: string): Promise<TokenResult> {\n try {\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n signal: requestSignal(),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: CLIENT_ID,\n }),\n });\n\n if (!response.ok) {\n console.error('[openai-codex] Token refresh failed:', response.status);\n return { type: 'failed' };\n }\n\n return tokenResponseToResult((await response.json()) as TokenResponseJson, 'Token refresh');\n } catch {\n console.error('[openai-codex] Token refresh request failed');\n return { type: 'failed' };\n }\n}\n\nasync function createAuthorizationFlow(\n redirectUri: string,\n state: string,\n originator: string = 'mastracode',\n): Promise<{ verifier: string; url: string }> {\n const { verifier, challenge } = await generatePKCE();\n\n const url = new URL(AUTHORIZE_URL);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('client_id', CLIENT_ID);\n url.searchParams.set('redirect_uri', redirectUri);\n url.searchParams.set('scope', SCOPE);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n url.searchParams.set('id_token_add_organizations', 'true');\n url.searchParams.set('codex_cli_simplified_flow', 'true');\n url.searchParams.set('originator', originator);\n\n return { verifier, url: url.toString() };\n}\n\ntype OAuthServerInfo = {\n redirectUri: string;\n warning?: string;\n close: () => void;\n cancelWait: () => void;\n waitForCode: () => Promise<{ code: string } | null>;\n};\n\ntype CallbackPorts = {\n defaultPort: number;\n fallbackPort: number;\n};\n\nconst CODEX_CALLBACK_PORTS: CallbackPorts = {\n defaultPort: DEFAULT_CALLBACK_PORT,\n fallbackPort: FALLBACK_CALLBACK_PORT,\n};\n\nasync function requestCancel(port: number): Promise<void> {\n try {\n await fetch(`http://127.0.0.1:${port}/cancel`, { signal: AbortSignal.timeout(200) });\n } catch {\n // The existing listener might not be a Codex auth server.\n }\n}\n\nfunction listen(server: HttpServer, port: number): Promise<boolean> {\n return new Promise(resolve => {\n const onError = () => {\n server.off('listening', onListening);\n resolve(false);\n };\n const onListening = () => {\n server.off('error', onError);\n resolve(true);\n };\n\n server.once('error', onError);\n server.once('listening', onListening);\n server.listen(port, '127.0.0.1');\n });\n}\n\nasync function bindOAuthServer(server: HttpServer, ports: CallbackPorts): Promise<number | null> {\n await requestCancel(ports.defaultPort);\n if (await listen(server, ports.defaultPort)) return ports.defaultPort;\n if (await listen(server, ports.fallbackPort)) return ports.fallbackPort;\n\n return null;\n}\n\nasync function getHttpModule() {\n if (!_http && _httpPromise) {\n _http = await _httpPromise;\n }\n if (!_http) {\n throw new Error('OpenAI Codex OAuth is only available in Node.js environments');\n }\n return _http;\n}\n\nasync function startLocalOAuthServer(\n state: string,\n ports: CallbackPorts = CODEX_CALLBACK_PORTS,\n): Promise<OAuthServerInfo> {\n const http = await getHttpModule();\n let lastCode: string | null = null;\n let cancelled = false;\n const server = http.createServer((req, res) => {\n try {\n const url = new URL(req.url || '', 'http://localhost');\n if (url.pathname === '/cancel') {\n cancelled = true;\n res.statusCode = 200;\n res.end('Cancelled');\n return;\n }\n if (url.pathname !== '/auth/callback') {\n res.statusCode = 404;\n res.end('Not found');\n return;\n }\n if (url.searchParams.get('state') !== state) {\n res.statusCode = 400;\n res.end('State mismatch');\n return;\n }\n const code = url.searchParams.get('code');\n if (!code) {\n res.statusCode = 400;\n res.end('Missing authorization code');\n return;\n }\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.end(SUCCESS_HTML);\n lastCode = code;\n } catch {\n res.statusCode = 500;\n res.end('Internal error');\n }\n });\n\n return new Promise(resolve => {\n bindOAuthServer(server, ports).then(port => {\n if (!port) {\n resolve({\n redirectUri: `http://localhost:${ports.fallbackPort}/auth/callback`,\n warning: `OpenAI Codex OAuth requires localhost port ${ports.defaultPort} or ${ports.fallbackPort}, but both are in use. Automatic browser callback will not work until one is freed.`,\n close: () => {\n try {\n server.close();\n } catch {\n // ignore\n }\n },\n cancelWait: () => {},\n waitForCode: async () => null,\n });\n return;\n }\n\n resolve({\n redirectUri: `http://localhost:${port}/auth/callback`,\n close: () => server.close(),\n cancelWait: () => {\n cancelled = true;\n },\n waitForCode: async () => {\n const sleep = () => new Promise(r => setTimeout(r, 100));\n for (let i = 0; i < 600; i += 1) {\n if (lastCode) return { code: lastCode };\n if (cancelled) return null;\n await sleep();\n }\n return null;\n },\n });\n });\n });\n}\n\n/**\n * Serializable pending state for a Codex device-code login. Safe to persist\n * (e.g. a `pending jsonb` column) so polling can span HTTP requests — the\n * device token response carries the `code_verifier`, so no PKCE state needs\n * to be kept client-side.\n */\nexport interface CodexDeviceLoginPending {\n deviceAuthId: string;\n userCode: string;\n /** Verification URL for the user to open. */\n url: string;\n instructions: string;\n /** Poll interval in ms suggested by the server. */\n intervalMs: number;\n /** ms epoch after which the device authorization expires. */\n deadlineAt: number;\n}\n\nexport type CodexDevicePollResult =\n | { status: 'complete'; credentials: OAuthCredentials }\n | { status: 'pending'; nextPollMs: number }\n | { status: 'failed'; error: string };\n\n/**\n * Start a Codex device-code login: request a user code and return the\n * serializable pending state for subsequent polls.\n */\nexport async function startCodexDeviceLogin(options?: { signal?: AbortSignal }): Promise<CodexDeviceLoginPending> {\n const response = await fetch(DEVICE_USER_CODE_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'mastracode',\n },\n body: JSON.stringify({ client_id: CLIENT_ID, originator: 'mastracode' }),\n signal: requestSignal(options?.signal),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to initiate OpenAI Codex device authorization: ${response.status}`);\n }\n\n const deviceData = (await response.json()) as {\n device_auth_id?: string;\n user_code?: string;\n usercode?: string;\n interval?: string | number;\n };\n\n const userCode = deviceData.user_code ?? deviceData.usercode;\n\n if (!deviceData.device_auth_id || !userCode) {\n throw new Error('OpenAI Codex device authorization response missing required fields');\n }\n\n const intervalSeconds =\n typeof deviceData.interval === 'number' ? deviceData.interval : Number.parseInt(deviceData.interval ?? '', 10) || 5;\n\n return {\n deviceAuthId: deviceData.device_auth_id,\n userCode,\n url: DEVICE_AUTHORIZE_URL,\n instructions: `Enter code: ${userCode}`,\n intervalMs: Math.max(intervalSeconds, 1) * 1000,\n deadlineAt: Date.now() + DEVICE_AUTH_TIMEOUT_MS,\n };\n}\n\n/**\n * Perform exactly one upstream poll for a pending Codex device login.\n * The Codex device endpoint signals \"still pending\" via HTTP 403/404 (it is\n * not an RFC 8628 error-JSON endpoint); on success it returns the\n * authorization code plus server-held PKCE verifier, which we exchange\n * immediately for credentials. Never throws for flow-level conditions.\n */\nexport async function pollCodexDeviceLogin(\n pending: CodexDeviceLoginPending,\n options?: { signal?: AbortSignal },\n): Promise<CodexDevicePollResult> {\n if (Date.now() >= pending.deadlineAt) {\n return { status: 'failed', error: 'OpenAI Codex device authorization timed out after 15 minutes' };\n }\n\n const pollResponse = await fetch(DEVICE_TOKEN_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'mastracode',\n },\n body: JSON.stringify({\n device_auth_id: pending.deviceAuthId,\n user_code: pending.userCode,\n }),\n signal: requestSignal(options?.signal),\n });\n\n if (pollResponse.ok) {\n const data = (await pollResponse.json()) as {\n authorization_code?: string;\n code_verifier?: string;\n };\n\n if (!data.authorization_code || !data.code_verifier) {\n return { status: 'failed', error: 'OpenAI Codex device token response missing required fields' };\n }\n\n const tokenResult = await exchangeAuthorizationCode(\n data.authorization_code,\n data.code_verifier,\n DEVICE_REDIRECT_URI,\n options?.signal,\n );\n if (tokenResult.type !== 'success') {\n return { status: 'failed', error: 'Token exchange failed' };\n }\n\n let accountId: string;\n try {\n accountId = requireAccountId(tokenResult);\n } catch (error) {\n return { status: 'failed', error: error instanceof Error ? error.message : String(error) };\n }\n\n return {\n status: 'complete',\n credentials: {\n access: tokenResult.access,\n refresh: tokenResult.refresh,\n expires: tokenResult.expires,\n accountId,\n email: getEmailFromTokens(tokenResult),\n },\n };\n }\n\n if (pollResponse.status !== 403 && pollResponse.status !== 404) {\n return {\n status: 'failed',\n error: `OpenAI Codex device authorization failed: ${pollResponse.status}`,\n };\n }\n\n return { status: 'pending', nextPollMs: pending.intervalMs };\n}\n\nasync function loginOpenAICodexDevice(options: {\n onAuth: (info: { url: string; instructions?: string }) => void;\n onProgress?: (message: string) => void;\n signal?: AbortSignal;\n sleep?: (ms: number) => Promise<void>;\n}): Promise<OAuthCredentials> {\n const pending = await startCodexDeviceLogin({ signal: options.signal });\n const sleep = options.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)));\n\n options.onAuth({\n url: pending.url,\n instructions: pending.instructions,\n });\n\n await sleep(pending.intervalMs);\n\n while (true) {\n if (options.signal?.aborted) {\n throw new Error('Login cancelled');\n }\n\n const result = await pollCodexDeviceLogin(pending, { signal: options.signal });\n if (result.status === 'complete') {\n return result.credentials;\n }\n if (result.status === 'failed') {\n throw new Error(result.error);\n }\n\n options.onProgress?.('Waiting for OpenAI Codex device authorization...');\n await sleep(result.nextPollMs);\n }\n}\n\n/**\n * Login with OpenAI Codex OAuth\n *\n * @param options.onAuth - Called with URL and instructions when auth starts\n * @param options.onPrompt - Called to prompt user for manual code paste (fallback if no onManualCodeInput)\n * @param options.onProgress - Optional progress messages\n * @param options.onManualCodeInput - Optional promise that resolves with user-pasted code.\n * Races with browser callback - whichever completes first wins.\n * Useful for showing paste input immediately alongside browser flow.\n * @param options.originator - OAuth originator parameter (defaults to \"mastracode\")\n */\nexport async function loginOpenAICodex(options: {\n onAuth: (info: { url: string; instructions?: string }) => void;\n onPrompt: (prompt: OAuthPrompt) => Promise<string>;\n onProgress?: (message: string) => void;\n onManualCodeInput?: () => Promise<string>;\n signal?: AbortSignal;\n originator?: string;\n mode?: 'browser' | 'device';\n}): Promise<OAuthCredentials> {\n const envMode =\n typeof process !== 'undefined' && process.env?.MASTRACODE_OPENAI_CODEX_AUTH_MODE === 'device'\n ? 'device'\n : undefined;\n const mode = options.mode ?? envMode ?? 'browser';\n if (mode === 'device') {\n return loginOpenAICodexDevice({\n onAuth: options.onAuth,\n onProgress: options.onProgress,\n signal: options.signal,\n });\n }\n\n const state = await createState();\n const server = await startLocalOAuthServer(state);\n if (server.warning) {\n options.onProgress?.(server.warning);\n }\n const { verifier, url } = await createAuthorizationFlow(\n server.redirectUri,\n state,\n options.originator ?? 'mastracode',\n );\n\n options.onAuth({\n url,\n instructions: server.warning\n ? `${server.warning} You can still paste the authorization code or full redirect URL manually.`\n : 'A browser window should open. Complete login to finish.',\n });\n\n let code: string | undefined;\n try {\n if (options.onManualCodeInput) {\n // Race between browser callback and manual input\n let manualCode: string | undefined;\n let manualError: Error | undefined;\n const manualPromise = options\n .onManualCodeInput()\n .then(input => {\n manualCode = input;\n server.cancelWait();\n })\n .catch(err => {\n manualError = err instanceof Error ? err : new Error(String(err));\n server.cancelWait();\n });\n\n const result = await server.waitForCode();\n\n // If manual input was cancelled, throw that error\n if (manualError) {\n throw manualError;\n }\n\n if (result?.code) {\n // Browser callback won\n code = result.code;\n } else if (manualCode) {\n // Manual input won (or callback timed out and user had entered code)\n const parsed = parseAuthorizationInput(manualCode);\n if (parsed.state && parsed.state !== state) {\n throw new Error('State mismatch');\n }\n code = parsed.code;\n }\n\n // If still no code, wait for manual promise to complete and try that\n if (!code) {\n await manualPromise;\n if (manualError) {\n throw manualError;\n }\n if (manualCode) {\n const parsed = parseAuthorizationInput(manualCode);\n if (parsed.state && parsed.state !== state) {\n throw new Error('State mismatch');\n }\n code = parsed.code;\n }\n }\n } else {\n // Original flow: wait for callback, then prompt if needed\n const result = await server.waitForCode();\n if (result?.code) {\n code = result.code;\n }\n }\n\n // Fallback to onPrompt if still no code\n if (!code) {\n const input = await options.onPrompt({\n message: 'Paste the authorization code (or full redirect URL):',\n });\n const parsed = parseAuthorizationInput(input);\n if (parsed.state && parsed.state !== state) {\n throw new Error('State mismatch');\n }\n code = parsed.code;\n }\n\n if (!code) {\n throw new Error('Missing authorization code');\n }\n\n const tokenResult = await exchangeAuthorizationCode(code, verifier, server.redirectUri, options.signal);\n if (tokenResult.type !== 'success') {\n throw new Error('Token exchange failed');\n }\n\n const accountId = requireAccountId(tokenResult);\n\n return {\n access: tokenResult.access,\n refresh: tokenResult.refresh,\n expires: tokenResult.expires,\n accountId,\n email: getEmailFromTokens(tokenResult),\n };\n } finally {\n server.close();\n }\n}\n\nexport const __testing = {\n createAuthorizationFlow,\n decodeJwt,\n extractAccountIdFromClaims,\n getAccountId,\n loginOpenAICodexDevice,\n requireAccountId,\n startLocalOAuthServer,\n};\n\n/**\n * Refresh OpenAI Codex OAuth token\n */\nexport async function refreshOpenAICodexToken(\n refreshToken: string,\n previousAccountId?: string,\n previousEmail?: string,\n): Promise<OAuthCredentials> {\n const result = await refreshAccessToken(refreshToken);\n if (result.type !== 'success') {\n throw new Error('Failed to refresh OpenAI Codex token');\n }\n\n const accountId = requireAccountId(result, previousAccountId);\n\n return {\n access: result.access,\n refresh: result.refresh,\n expires: result.expires,\n accountId,\n email: getEmailFromTokens(result) ?? previousEmail,\n };\n}\n\nexport const openaiCodexOAuthProvider: OAuthProviderInterface = {\n id: 'openai-codex',\n name: 'ChatGPT Plus/Pro (Codex Subscription)',\n usesCallbackServer: true,\n authModes: OPENAI_CODEX_AUTH_MODES,\n\n async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n const mode = callbacks.authMode === 'device' || callbacks.authMode === 'browser' ? callbacks.authMode : undefined;\n return loginOpenAICodex({\n onAuth: callbacks.onAuth,\n onPrompt: callbacks.onPrompt,\n onProgress: callbacks.onProgress,\n onManualCodeInput: callbacks.onManualCodeInput,\n signal: callbacks.signal,\n mode,\n });\n },\n\n async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n return refreshOpenAICodexToken(\n credentials.refresh,\n credentials.accountId as string | undefined,\n credentials.email as string | undefined,\n );\n },\n\n getApiKey(credentials: OAuthCredentials): string {\n return credentials.access;\n },\n\n getAccountLabel(credentials: OAuthCredentials): Promise<string | undefined> {\n const email = credentials.email;\n return Promise.resolve(typeof email === 'string' && email.length > 0 ? email : undefined);\n },\n\n /**\n * The ChatGPT account/workspace id from the token, when present — stable\n * across refreshes and re-authorizations of one subscription.\n */\n getAccountIdentity(credentials: OAuthCredentials): string | undefined {\n const accountId = credentials.accountId;\n return typeof accountId === 'string' && accountId.length > 0 ? accountId : undefined;\n },\n};\n"],"mappings":";;;;;;;;;;;;AAYA,IAAI,eAA2D;AAE/D,IAAI,QAA2C;AAO/C,IAAI,OAAO,YAAY,gBAAgB,QAAQ,UAAU,QAAQ,QAAQ,UAAU,MACjF,eAAe,OAAO,OAAY,CAAC,MAAK,MAAK;CAC3C,QAAQ;CACR,OAAO;AACT,CAAC;AAOH,MAAa,0BAAmD,CAC9D;CACE,IAAI;CACJ,MAAM;CACN,aAAa;AACf,GACA;CACE,IAAI;CACJ,MAAM;CACN,aAAa;AACf,CACF;AAEA,MAAM,YAAY;AAClB,MAAM,SAAS;AACf,MAAM,gBAAgB,GAAG,OAAO;AAChC,MAAM,YAAY,GAAG,OAAO;AAC5B,MAAM,uBAAuB,GAAG,OAAO;AACvC,MAAM,mBAAmB,GAAG,OAAO;AACnC,MAAM,uBAAuB,GAAG,OAAO;AACvC,MAAM,sBAAsB,GAAG,OAAO;AACtC,MAAM,wBAAwB;AAC9B,MAAM,yBAAyB;AAC/B,MAAM,mCAAmC;AACzC,MAAM,yBAAyB,MAAU;AACzC,MAAM,2BAA2B;AACjC,MAAM,QAAQ;AACd,MAAM,iBAAiB;AAEvB,MAAM,eAAe;;;;;;;;;;;AAgCrB,SAAS,cAAc,QAAmC;CACxD,MAAM,UAAU,YAAY,QAAQ,wBAAwB;CAC5D,OAAO,SAAS,YAAY,IAAI,CAAC,SAAS,MAAM,CAAC,IAAI;AACvD;AAEA,eAAe,cAA+B;CAC5C,IAAI,OAAO,YAAY,eAAe,EAAE,QAAQ,UAAU,QAAQ,QAAQ,UAAU,MAClF,MAAM,IAAI,MAAM,8DAA8D;CAEhF,OAAO,OAAO,KAAK,WAAW,OAAO,gCAAgB,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK;AAC1F;AAEA,SAAS,UAAU,OAAkC;CACnD,IAAI;EACF,MAAM,QAAQ,MAAM,MAAM,GAAG;EAC7B,IAAI,MAAM,WAAW,GAAG,OAAO;EAC/B,MAAM,UAAU,MAAM,MAAM;EAC5B,MAAM,SAAS,QACZ,QAAQ,MAAM,GAAG,CAAC,CAClB,QAAQ,MAAM,GAAG,CAAC,CAClB,OAAO,KAAK,KAAK,QAAQ,SAAS,CAAC,IAAI,GAAG,GAAG;EAChD,MAAM,UAAU,KAAK,MAAM;EAC3B,OAAO,KAAK,MAAM,OAAO;CAC3B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,2BAA2B,SAAuD;CACzF,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,YAAY,QAAQ,sBAAsB,QAAQ,eAAe,EAAE;CACzE,OAAO,OAAO,cAAc,YAAY,UAAU,SAAS,IAAI,YAAY;AAC7E;AAEA,SAAS,aAAa,QAA8C,UAAuC;CACzG,MAAM,cAAc,OAAO,UAAU,2BAA2B,UAAU,OAAO,OAAO,CAAC,IAAI;CAC7F,IAAI,aAAa,OAAO;CAExB,MAAM,kBAAkB,2BAA2B,UAAU,OAAO,MAAM,CAAC;CAC3E,IAAI,iBAAiB,OAAO;CAE5B,OAAO;AACT;AAEA,SAAS,iBAAiB,QAA8C,UAA2B;CACjG,MAAM,YAAY,aAAa,QAAQ,QAAQ;CAC/C,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,8DAA8D;CAEhF,OAAO;AACT;AAEA,SAAS,uBAAuB,SAAoC;CAClE,MAAM,QAAQ,QAAQ,SAAS,QAAQ,eAAe,EAAE;CACxD,OAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEA,SAAS,mBAAmB,QAAkE;CAC5F,MAAM,cAAc,OAAO,UAAU,uBAAuB,UAAU,OAAO,OAAO,KAAK,CAAC,CAAC,IAAI;CAC/F,IAAI,aAAa,OAAO;CAExB,OADwB,uBAAuB,UAAU,OAAO,MAAM,KAAK,CAAC,CACvD,KAAK,KAAA;AAC5B;AASA,SAAS,sBAAsB,MAAyB,WAAgC;CACtF,IAAI,CAAC,KAAK,gBAAgB,CAAC,KAAK,eAAe;EAC7C,QAAQ,MACN,kBAAkB,UAAU,kDAAkD,QAAQ,KAAK,YAAY,EAAE,kBAAkB,QAAQ,KAAK,aAAa,EAAE,EACzJ;EACA,OAAO,EAAE,MAAM,SAAS;CAC1B;CAEA,OAAO;EACL,MAAM;EACN,QAAQ,KAAK;EACb,SAAS,KAAK;EACd,SAAS,KAAK,IAAI,KAAK,KAAK,cAAc,oCAAoC;EAC9E,SAAS,KAAK;CAChB;AACF;AAEA,eAAe,0BACb,MACA,UACA,aACA,QACsB;CACtB,MAAM,WAAW,MAAM,MAAM,WAAW;EACtC,QAAQ;EACR,QAAQ,cAAc,MAAM;EAC5B,SAAS,EAAE,gBAAgB,oCAAoC;EAC/D,MAAM,IAAI,gBAAgB;GACxB,YAAY;GACZ,WAAW;GACX;GACA,eAAe;GACf,cAAc;EAChB,CAAC;CACH,CAAC;CAED,IAAI,CAAC,SAAS,IAAI;EAChB,QAAQ,MAAM,sCAAsC,SAAS,MAAM;EACnE,OAAO,EAAE,MAAM,SAAS;CAC1B;CAEA,OAAO,sBAAuB,MAAM,SAAS,KAAK,GAAyB,OAAO;AACpF;AAEA,eAAe,mBAAmB,cAA4C;CAC5E,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,WAAW;GACtC,QAAQ;GACR,QAAQ,cAAc;GACtB,SAAS,EAAE,gBAAgB,oCAAoC;GAC/D,MAAM,IAAI,gBAAgB;IACxB,YAAY;IACZ,eAAe;IACf,WAAW;GACb,CAAC;EACH,CAAC;EAED,IAAI,CAAC,SAAS,IAAI;GAChB,QAAQ,MAAM,wCAAwC,SAAS,MAAM;GACrE,OAAO,EAAE,MAAM,SAAS;EAC1B;EAEA,OAAO,sBAAuB,MAAM,SAAS,KAAK,GAAyB,eAAe;CAC5F,QAAQ;EACN,QAAQ,MAAM,6CAA6C;EAC3D,OAAO,EAAE,MAAM,SAAS;CAC1B;AACF;AAEA,eAAe,wBACb,aACA,OACA,aAAqB,cACuB;CAC5C,MAAM,EAAE,UAAU,cAAc,MAAM,aAAa;CAEnD,MAAM,MAAM,IAAI,IAAI,aAAa;CACjC,IAAI,aAAa,IAAI,iBAAiB,MAAM;CAC5C,IAAI,aAAa,IAAI,aAAa,SAAS;CAC3C,IAAI,aAAa,IAAI,gBAAgB,WAAW;CAChD,IAAI,aAAa,IAAI,SAAS,KAAK;CACnC,IAAI,aAAa,IAAI,kBAAkB,SAAS;CAChD,IAAI,aAAa,IAAI,yBAAyB,MAAM;CACpD,IAAI,aAAa,IAAI,SAAS,KAAK;CACnC,IAAI,aAAa,IAAI,8BAA8B,MAAM;CACzD,IAAI,aAAa,IAAI,6BAA6B,MAAM;CACxD,IAAI,aAAa,IAAI,cAAc,UAAU;CAE7C,OAAO;EAAE;EAAU,KAAK,IAAI,SAAS;CAAE;AACzC;AAeA,MAAM,uBAAsC;CAC1C,aAAa;CACb,cAAc;AAChB;AAEA,eAAe,cAAc,MAA6B;CACxD,IAAI;EACF,MAAM,MAAM,oBAAoB,KAAK,UAAU,EAAE,QAAQ,YAAY,QAAQ,GAAG,EAAE,CAAC;CACrF,QAAQ,CAER;AACF;AAEA,SAAS,OAAO,QAAoB,MAAgC;CAClE,OAAO,IAAI,SAAQ,YAAW;EAC5B,MAAM,gBAAgB;GACpB,OAAO,IAAI,aAAa,WAAW;GACnC,QAAQ,KAAK;EACf;EACA,MAAM,oBAAoB;GACxB,OAAO,IAAI,SAAS,OAAO;GAC3B,QAAQ,IAAI;EACd;EAEA,OAAO,KAAK,SAAS,OAAO;EAC5B,OAAO,KAAK,aAAa,WAAW;EACpC,OAAO,OAAO,MAAM,WAAW;CACjC,CAAC;AACH;AAEA,eAAe,gBAAgB,QAAoB,OAA8C;CAC/F,MAAM,cAAc,MAAM,WAAW;CACrC,IAAI,MAAM,OAAO,QAAQ,MAAM,WAAW,GAAG,OAAO,MAAM;CAC1D,IAAI,MAAM,OAAO,QAAQ,MAAM,YAAY,GAAG,OAAO,MAAM;CAE3D,OAAO;AACT;AAEA,eAAe,gBAAgB;CAC7B,IAAI,CAAC,SAAS,cACZ,QAAQ,MAAM;CAEhB,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,8DAA8D;CAEhF,OAAO;AACT;AAEA,eAAe,sBACb,OACA,QAAuB,sBACG;CAC1B,MAAM,OAAO,MAAM,cAAc;CACjC,IAAI,WAA0B;CAC9B,IAAI,YAAY;CAChB,MAAM,SAAS,KAAK,cAAc,KAAK,QAAQ;EAC7C,IAAI;GACF,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,kBAAkB;GACrD,IAAI,IAAI,aAAa,WAAW;IAC9B,YAAY;IACZ,IAAI,aAAa;IACjB,IAAI,IAAI,WAAW;IACnB;GACF;GACA,IAAI,IAAI,aAAa,kBAAkB;IACrC,IAAI,aAAa;IACjB,IAAI,IAAI,WAAW;IACnB;GACF;GACA,IAAI,IAAI,aAAa,IAAI,OAAO,MAAM,OAAO;IAC3C,IAAI,aAAa;IACjB,IAAI,IAAI,gBAAgB;IACxB;GACF;GACA,MAAM,OAAO,IAAI,aAAa,IAAI,MAAM;GACxC,IAAI,CAAC,MAAM;IACT,IAAI,aAAa;IACjB,IAAI,IAAI,4BAA4B;IACpC;GACF;GACA,IAAI,aAAa;GACjB,IAAI,UAAU,gBAAgB,0BAA0B;GACxD,IAAI,IAAI,YAAY;GACpB,WAAW;EACb,QAAQ;GACN,IAAI,aAAa;GACjB,IAAI,IAAI,gBAAgB;EAC1B;CACF,CAAC;CAED,OAAO,IAAI,SAAQ,YAAW;EAC5B,gBAAgB,QAAQ,KAAK,CAAC,CAAC,MAAK,SAAQ;GAC1C,IAAI,CAAC,MAAM;IACT,QAAQ;KACN,aAAa,oBAAoB,MAAM,aAAa;KACpD,SAAS,8CAA8C,MAAM,YAAY,MAAM,MAAM,aAAa;KAClG,aAAa;MACX,IAAI;OACF,OAAO,MAAM;MACf,QAAQ,CAER;KACF;KACA,kBAAkB,CAAC;KACnB,aAAa,YAAY;IAC3B,CAAC;IACD;GACF;GAEA,QAAQ;IACN,aAAa,oBAAoB,KAAK;IACtC,aAAa,OAAO,MAAM;IAC1B,kBAAkB;KAChB,YAAY;IACd;IACA,aAAa,YAAY;KACvB,MAAM,cAAc,IAAI,SAAQ,MAAK,WAAW,GAAG,GAAG,CAAC;KACvD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,GAAG;MAC/B,IAAI,UAAU,OAAO,EAAE,MAAM,SAAS;MACtC,IAAI,WAAW,OAAO;MACtB,MAAM,MAAM;KACd;KACA,OAAO;IACT;GACF,CAAC;EACH,CAAC;CACH,CAAC;AACH;;;;;AA6BA,eAAsB,sBAAsB,SAAsE;CAChH,MAAM,WAAW,MAAM,MAAM,sBAAsB;EACjD,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,cAAc;EAChB;EACA,MAAM,KAAK,UAAU;GAAE,WAAW;GAAW,YAAY;EAAa,CAAC;EACvE,QAAQ,cAAc,SAAS,MAAM;CACvC,CAAC;CAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,yDAAyD,SAAS,QAAQ;CAG5F,MAAM,aAAc,MAAM,SAAS,KAAK;CAOxC,MAAM,WAAW,WAAW,aAAa,WAAW;CAEpD,IAAI,CAAC,WAAW,kBAAkB,CAAC,UACjC,MAAM,IAAI,MAAM,oEAAoE;CAGtF,MAAM,kBACJ,OAAO,WAAW,aAAa,WAAW,WAAW,WAAW,OAAO,SAAS,WAAW,YAAY,IAAI,EAAE,KAAK;CAEpH,OAAO;EACL,cAAc,WAAW;EACzB;EACA,KAAK;EACL,cAAc,eAAe;EAC7B,YAAY,KAAK,IAAI,iBAAiB,CAAC,IAAI;EAC3C,YAAY,KAAK,IAAI,IAAI;CAC3B;AACF;;;;;;;;AASA,eAAsB,qBACpB,SACA,SACgC;CAChC,IAAI,KAAK,IAAI,KAAK,QAAQ,YACxB,OAAO;EAAE,QAAQ;EAAU,OAAO;CAA+D;CAGnG,MAAM,eAAe,MAAM,MAAM,kBAAkB;EACjD,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,cAAc;EAChB;EACA,MAAM,KAAK,UAAU;GACnB,gBAAgB,QAAQ;GACxB,WAAW,QAAQ;EACrB,CAAC;EACD,QAAQ,cAAc,SAAS,MAAM;CACvC,CAAC;CAED,IAAI,aAAa,IAAI;EACnB,MAAM,OAAQ,MAAM,aAAa,KAAK;EAKtC,IAAI,CAAC,KAAK,sBAAsB,CAAC,KAAK,eACpC,OAAO;GAAE,QAAQ;GAAU,OAAO;EAA6D;EAGjG,MAAM,cAAc,MAAM,0BACxB,KAAK,oBACL,KAAK,eACL,qBACA,SAAS,MACX;EACA,IAAI,YAAY,SAAS,WACvB,OAAO;GAAE,QAAQ;GAAU,OAAO;EAAwB;EAG5D,IAAI;EACJ,IAAI;GACF,YAAY,iBAAiB,WAAW;EAC1C,SAAS,OAAO;GACd,OAAO;IAAE,QAAQ;IAAU,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAAE;EAC3F;EAEA,OAAO;GACL,QAAQ;GACR,aAAa;IACX,QAAQ,YAAY;IACpB,SAAS,YAAY;IACrB,SAAS,YAAY;IACrB;IACA,OAAO,mBAAmB,WAAW;GACvC;EACF;CACF;CAEA,IAAI,aAAa,WAAW,OAAO,aAAa,WAAW,KACzD,OAAO;EACL,QAAQ;EACR,OAAO,6CAA6C,aAAa;CACnE;CAGF,OAAO;EAAE,QAAQ;EAAW,YAAY,QAAQ;CAAW;AAC7D;AAEA,eAAe,uBAAuB,SAKR;CAC5B,MAAM,UAAU,MAAM,sBAAsB,EAAE,QAAQ,QAAQ,OAAO,CAAC;CACtE,MAAM,QAAQ,QAAQ,WAAU,OAAM,IAAI,SAAc,YAAW,WAAW,SAAS,EAAE,CAAC;CAE1F,QAAQ,OAAO;EACb,KAAK,QAAQ;EACb,cAAc,QAAQ;CACxB,CAAC;CAED,MAAM,MAAM,QAAQ,UAAU;CAE9B,OAAO,MAAM;EACX,IAAI,QAAQ,QAAQ,SAClB,MAAM,IAAI,MAAM,iBAAiB;EAGnC,MAAM,SAAS,MAAM,qBAAqB,SAAS,EAAE,QAAQ,QAAQ,OAAO,CAAC;EAC7E,IAAI,OAAO,WAAW,YACpB,OAAO,OAAO;EAEhB,IAAI,OAAO,WAAW,UACpB,MAAM,IAAI,MAAM,OAAO,KAAK;EAG9B,QAAQ,aAAa,kDAAkD;EACvE,MAAM,MAAM,OAAO,UAAU;CAC/B;AACF;;;;;;;;;;;;AAaA,eAAsB,iBAAiB,SAQT;CAC5B,MAAM,UACJ,OAAO,YAAY,eAAe,QAAQ,KAAK,sCAAsC,WACjF,WACA,KAAA;CAEN,KADa,QAAQ,QAAQ,WAAW,eAC3B,UACX,OAAO,uBAAuB;EAC5B,QAAQ,QAAQ;EAChB,YAAY,QAAQ;EACpB,QAAQ,QAAQ;CAClB,CAAC;CAGH,MAAM,QAAQ,MAAM,YAAY;CAChC,MAAM,SAAS,MAAM,sBAAsB,KAAK;CAChD,IAAI,OAAO,SACT,QAAQ,aAAa,OAAO,OAAO;CAErC,MAAM,EAAE,UAAU,QAAQ,MAAM,wBAC9B,OAAO,aACP,OACA,QAAQ,cAAc,YACxB;CAEA,QAAQ,OAAO;EACb;EACA,cAAc,OAAO,UACjB,GAAG,OAAO,QAAQ,8EAClB;CACN,CAAC;CAED,IAAI;CACJ,IAAI;EACF,IAAI,QAAQ,mBAAmB;GAE7B,IAAI;GACJ,IAAI;GACJ,MAAM,gBAAgB,QACnB,kBAAkB,CAAC,CACnB,MAAK,UAAS;IACb,aAAa;IACb,OAAO,WAAW;GACpB,CAAC,CAAC,CACD,OAAM,QAAO;IACZ,cAAc,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;IAChE,OAAO,WAAW;GACpB,CAAC;GAEH,MAAM,SAAS,MAAM,OAAO,YAAY;GAGxC,IAAI,aACF,MAAM;GAGR,IAAI,QAAQ,MAEV,OAAO,OAAO;QACT,IAAI,YAAY;IAErB,MAAM,SAAS,wBAAwB,UAAU;IACjD,IAAI,OAAO,SAAS,OAAO,UAAU,OACnC,MAAM,IAAI,MAAM,gBAAgB;IAElC,OAAO,OAAO;GAChB;GAGA,IAAI,CAAC,MAAM;IACT,MAAM;IACN,IAAI,aACF,MAAM;IAER,IAAI,YAAY;KACd,MAAM,SAAS,wBAAwB,UAAU;KACjD,IAAI,OAAO,SAAS,OAAO,UAAU,OACnC,MAAM,IAAI,MAAM,gBAAgB;KAElC,OAAO,OAAO;IAChB;GACF;EACF,OAAO;GAEL,MAAM,SAAS,MAAM,OAAO,YAAY;GACxC,IAAI,QAAQ,MACV,OAAO,OAAO;EAElB;EAGA,IAAI,CAAC,MAAM;GAIT,MAAM,SAAS,wBAAwB,MAHnB,QAAQ,SAAS,EACnC,SAAS,uDACX,CAAC,CAC2C;GAC5C,IAAI,OAAO,SAAS,OAAO,UAAU,OACnC,MAAM,IAAI,MAAM,gBAAgB;GAElC,OAAO,OAAO;EAChB;EAEA,IAAI,CAAC,MACH,MAAM,IAAI,MAAM,4BAA4B;EAG9C,MAAM,cAAc,MAAM,0BAA0B,MAAM,UAAU,OAAO,aAAa,QAAQ,MAAM;EACtG,IAAI,YAAY,SAAS,WACvB,MAAM,IAAI,MAAM,uBAAuB;EAGzC,MAAM,YAAY,iBAAiB,WAAW;EAE9C,OAAO;GACL,QAAQ,YAAY;GACpB,SAAS,YAAY;GACrB,SAAS,YAAY;GACrB;GACA,OAAO,mBAAmB,WAAW;EACvC;CACF,UAAU;EACR,OAAO,MAAM;CACf;AACF;AAEA,MAAa,YAAY;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;AAKA,eAAsB,wBACpB,cACA,mBACA,eAC2B;CAC3B,MAAM,SAAS,MAAM,mBAAmB,YAAY;CACpD,IAAI,OAAO,SAAS,WAClB,MAAM,IAAI,MAAM,sCAAsC;CAGxD,MAAM,YAAY,iBAAiB,QAAQ,iBAAiB;CAE5D,OAAO;EACL,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB;EACA,OAAO,mBAAmB,MAAM,KAAK;CACvC;AACF;AAEA,MAAa,2BAAmD;CAC9D,IAAI;CACJ,MAAM;CACN,oBAAoB;CACpB,WAAW;CAEX,MAAM,MAAM,WAA2D;EACrE,MAAM,OAAO,UAAU,aAAa,YAAY,UAAU,aAAa,YAAY,UAAU,WAAW,KAAA;EACxG,OAAO,iBAAiB;GACtB,QAAQ,UAAU;GAClB,UAAU,UAAU;GACpB,YAAY,UAAU;GACtB,mBAAmB,UAAU;GAC7B,QAAQ,UAAU;GAClB;EACF,CAAC;CACH;CAEA,MAAM,aAAa,aAA0D;EAC3E,OAAO,wBACL,YAAY,SACZ,YAAY,WACZ,YAAY,KACd;CACF;CAEA,UAAU,aAAuC;EAC/C,OAAO,YAAY;CACrB;CAEA,gBAAgB,aAA4D;EAC1E,MAAM,QAAQ,YAAY;EAC1B,OAAO,QAAQ,QAAQ,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA,CAAS;CAC1F;;;;;CAMA,mBAAmB,aAAmD;EACpE,MAAM,YAAY,YAAY;EAC9B,OAAO,OAAO,cAAc,YAAY,UAAU,SAAS,IAAI,YAAY,KAAA;CAC7E;AACF"}
1
+ {"version":3,"file":"openai-codex.js","names":[],"sources":["../../../src/auth/providers/openai-codex.ts"],"sourcesContent":["/**\n * OpenAI Codex (ChatGPT OAuth) flow\n *\n * Inspired by pi-mono's OAuth implementation:\n * https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/openai-codex.ts\n *\n * NOTE: This module uses Node.js http for the OAuth callback.\n * It is only intended for CLI use, not browser environments.\n */\n\n// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui)\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nlet _httpPromise: Promise<typeof import('node:http')> | null = null;\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nlet _http: typeof import('node:http') | null = null;\ntype HttpServer = {\n off: (event: 'error' | 'listening', listener: (...args: any[]) => void) => HttpServer;\n once: (event: 'error' | 'listening', listener: (...args: any[]) => void) => HttpServer;\n listen: (port: number, hostname: string) => HttpServer;\n close: () => void;\n};\nif (typeof process !== 'undefined' && (process.versions?.node || process.versions?.bun)) {\n _httpPromise = import('node:http').then(m => {\n _http = m;\n return m;\n });\n}\n\nimport { parseAuthorizationInput } from '../authorization-input.js';\nimport { generatePKCE } from '../pkce.js';\nimport type { AuthMode, OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from '../types.js';\n\nexport const OPENAI_CODEX_AUTH_MODES: ReadonlyArray<AuthMode> = [\n {\n id: 'browser',\n name: 'Browser (local callback)',\n description: 'Opens the browser and waits for the OAuth callback on localhost.',\n },\n {\n id: 'device',\n name: 'Device code (headless)',\n description: 'Shows a code to enter at openai.com — for SSH, remote, or no-browser environments.',\n },\n];\n\nconst CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';\nconst ISSUER = 'https://auth.openai.com';\nconst AUTHORIZE_URL = `${ISSUER}/oauth/authorize`;\nconst TOKEN_URL = `${ISSUER}/oauth/token`;\nconst DEVICE_USER_CODE_URL = `${ISSUER}/api/accounts/deviceauth/usercode`;\nconst DEVICE_TOKEN_URL = `${ISSUER}/api/accounts/deviceauth/token`;\nconst DEVICE_AUTHORIZE_URL = `${ISSUER}/codex/device`;\nconst DEVICE_REDIRECT_URI = `${ISSUER}/deviceauth/callback`;\nconst DEFAULT_CALLBACK_PORT = 1455;\nconst FALLBACK_CALLBACK_PORT = 1457;\nconst DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;\nconst DEVICE_AUTH_TIMEOUT_MS = 15 * 60 * 1000;\nconst OAUTH_REQUEST_TIMEOUT_MS = 15_000;\nconst SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke';\nconst JWT_CLAIM_PATH = 'https://api.openai.com/auth';\n\nconst SUCCESS_HTML = `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Authentication successful</title>\n</head>\n<body>\n <p>Authentication successful. Return to your terminal to continue.</p>\n</body>\n</html>`;\n\ntype TokenSuccess = {\n type: 'success';\n access: string;\n refresh: string;\n expires: number;\n idToken?: string;\n};\ntype TokenFailure = { type: 'failed' };\ntype TokenResult = TokenSuccess | TokenFailure;\n\ntype JwtPayload = {\n chatgpt_account_id?: string;\n email?: string;\n [JWT_CLAIM_PATH]?: {\n chatgpt_account_id?: string;\n email?: string;\n };\n [key: string]: unknown;\n};\n\nfunction requestSignal(signal?: AbortSignal): AbortSignal {\n const timeout = AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);\n return signal ? AbortSignal.any([timeout, signal]) : timeout;\n}\n\nasync function createState(): Promise<string> {\n if (typeof process === 'undefined' || !(process.versions?.node || process.versions?.bun)) {\n throw new Error('OpenAI Codex OAuth is only available in Node.js environments');\n }\n return Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(16))).toString('hex');\n}\n\nfunction decodeJwt(token: string): JwtPayload | null {\n try {\n const parts = token.split('.');\n if (parts.length !== 3) return null;\n const payload = parts[1] ?? '';\n const padded = payload\n .replace(/-/g, '+')\n .replace(/_/g, '/')\n .padEnd(Math.ceil(payload.length / 4) * 4, '=');\n const decoded = atob(padded);\n return JSON.parse(decoded) as JwtPayload;\n } catch {\n return null;\n }\n}\n\nfunction extractAccountIdFromClaims(payload: JwtPayload | null | undefined): string | null {\n if (!payload) return null;\n const accountId = payload.chatgpt_account_id ?? payload[JWT_CLAIM_PATH]?.chatgpt_account_id;\n return typeof accountId === 'string' && accountId.length > 0 ? accountId : null;\n}\n\nfunction getAccountId(tokens: { idToken?: string; access: string }, fallback?: string): string | undefined {\n const fromIdToken = tokens.idToken ? extractAccountIdFromClaims(decodeJwt(tokens.idToken)) : null;\n if (fromIdToken) return fromIdToken;\n\n const fromAccessToken = extractAccountIdFromClaims(decodeJwt(tokens.access));\n if (fromAccessToken) return fromAccessToken;\n\n return fallback;\n}\n\nfunction requireAccountId(tokens: { idToken?: string; access: string }, fallback?: string): string {\n const accountId = getAccountId(tokens, fallback);\n if (!accountId) {\n throw new Error('Failed to extract ChatGPT account id from OpenAI Codex token');\n }\n return accountId;\n}\n\nfunction extractEmailFromClaims(payload: JwtPayload): string | null {\n const email = payload.email ?? payload[JWT_CLAIM_PATH]?.email;\n return typeof email === 'string' && email.length > 0 ? email : null;\n}\n\nfunction getEmailFromTokens(tokens: { idToken?: string; access: string }): string | undefined {\n const fromIdToken = tokens.idToken ? extractEmailFromClaims(decodeJwt(tokens.idToken) ?? {}) : null;\n if (fromIdToken) return fromIdToken;\n const fromAccessToken = extractEmailFromClaims(decodeJwt(tokens.access) ?? {});\n return fromAccessToken ?? undefined;\n}\n\ntype TokenResponseJson = {\n id_token?: string;\n access_token?: string;\n refresh_token?: string;\n expires_in?: number;\n};\n\nfunction tokenResponseToResult(json: TokenResponseJson, logPrefix: string): TokenResult {\n if (!json.access_token || !json.refresh_token) {\n console.error(\n `[openai-codex] ${logPrefix} response missing required fields (access_token=${Boolean(json.access_token)}, refresh_token=${Boolean(json.refresh_token)})`,\n );\n return { type: 'failed' };\n }\n\n return {\n type: 'success',\n access: json.access_token,\n refresh: json.refresh_token,\n expires: Date.now() + (json.expires_in ?? DEFAULT_TOKEN_EXPIRES_IN_SECONDS) * 1000,\n idToken: json.id_token,\n };\n}\n\nasync function exchangeAuthorizationCode(\n code: string,\n verifier: string,\n redirectUri: string,\n signal?: AbortSignal,\n): Promise<TokenResult> {\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n signal: requestSignal(signal),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: CLIENT_ID,\n code,\n code_verifier: verifier,\n redirect_uri: redirectUri,\n }),\n });\n\n if (!response.ok) {\n console.error('[openai-codex] code->token failed:', response.status);\n return { type: 'failed' };\n }\n\n return tokenResponseToResult((await response.json()) as TokenResponseJson, 'token');\n}\n\nasync function refreshAccessToken(refreshToken: string): Promise<TokenResult> {\n try {\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n signal: requestSignal(),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: CLIENT_ID,\n }),\n });\n\n if (!response.ok) {\n console.error('[openai-codex] Token refresh failed:', response.status);\n return { type: 'failed' };\n }\n\n return tokenResponseToResult((await response.json()) as TokenResponseJson, 'Token refresh');\n } catch {\n console.error('[openai-codex] Token refresh request failed');\n return { type: 'failed' };\n }\n}\n\nasync function createAuthorizationFlow(\n redirectUri: string,\n state: string,\n originator: string = 'mastracode',\n): Promise<{ verifier: string; url: string }> {\n const { verifier, challenge } = await generatePKCE();\n\n const url = new URL(AUTHORIZE_URL);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('client_id', CLIENT_ID);\n url.searchParams.set('redirect_uri', redirectUri);\n url.searchParams.set('scope', SCOPE);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n url.searchParams.set('id_token_add_organizations', 'true');\n url.searchParams.set('codex_cli_simplified_flow', 'true');\n url.searchParams.set('originator', originator);\n\n return { verifier, url: url.toString() };\n}\n\ntype OAuthServerInfo = {\n redirectUri: string;\n warning?: string;\n close: () => void;\n cancelWait: () => void;\n waitForCode: () => Promise<{ code: string } | null>;\n};\n\ntype CallbackPorts = {\n defaultPort: number;\n fallbackPort: number;\n};\n\nconst CODEX_CALLBACK_PORTS: CallbackPorts = {\n defaultPort: DEFAULT_CALLBACK_PORT,\n fallbackPort: FALLBACK_CALLBACK_PORT,\n};\n\nasync function requestCancel(port: number): Promise<void> {\n try {\n await fetch(`http://127.0.0.1:${port}/cancel`, { signal: AbortSignal.timeout(200) });\n } catch {\n // The existing listener might not be a Codex auth server.\n }\n}\n\nfunction listen(server: HttpServer, port: number): Promise<boolean> {\n return new Promise(resolve => {\n const onError = () => {\n server.off('listening', onListening);\n resolve(false);\n };\n const onListening = () => {\n server.off('error', onError);\n resolve(true);\n };\n\n server.once('error', onError);\n server.once('listening', onListening);\n server.listen(port, '127.0.0.1');\n });\n}\n\nasync function bindOAuthServer(server: HttpServer, ports: CallbackPorts): Promise<number | null> {\n await requestCancel(ports.defaultPort);\n if (await listen(server, ports.defaultPort)) return ports.defaultPort;\n if (await listen(server, ports.fallbackPort)) return ports.fallbackPort;\n\n return null;\n}\n\nasync function getHttpModule() {\n if (!_http && _httpPromise) {\n _http = await _httpPromise;\n }\n if (!_http) {\n throw new Error('OpenAI Codex OAuth is only available in Node.js environments');\n }\n return _http;\n}\n\nasync function startLocalOAuthServer(\n state: string,\n ports: CallbackPorts = CODEX_CALLBACK_PORTS,\n): Promise<OAuthServerInfo> {\n const http = await getHttpModule();\n let lastCode: string | null = null;\n let cancelled = false;\n const server = http.createServer((req, res) => {\n try {\n const url = new URL(req.url || '', 'http://localhost');\n if (url.pathname === '/cancel') {\n cancelled = true;\n res.statusCode = 200;\n res.end('Cancelled');\n return;\n }\n if (url.pathname !== '/auth/callback') {\n res.statusCode = 404;\n res.end('Not found');\n return;\n }\n if (url.searchParams.get('state') !== state) {\n res.statusCode = 400;\n res.end('State mismatch');\n return;\n }\n const code = url.searchParams.get('code');\n if (!code) {\n res.statusCode = 400;\n res.end('Missing authorization code');\n return;\n }\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.end(SUCCESS_HTML);\n lastCode = code;\n } catch {\n res.statusCode = 500;\n res.end('Internal error');\n }\n });\n\n return new Promise(resolve => {\n bindOAuthServer(server, ports).then(port => {\n if (!port) {\n resolve({\n redirectUri: `http://localhost:${ports.fallbackPort}/auth/callback`,\n warning: `OpenAI Codex OAuth requires localhost port ${ports.defaultPort} or ${ports.fallbackPort}, but both are in use. Automatic browser callback will not work until one is freed.`,\n close: () => {\n try {\n server.close();\n } catch {\n // ignore\n }\n },\n cancelWait: () => {},\n waitForCode: async () => null,\n });\n return;\n }\n\n resolve({\n redirectUri: `http://localhost:${port}/auth/callback`,\n close: () => server.close(),\n cancelWait: () => {\n cancelled = true;\n },\n waitForCode: async () => {\n const sleep = () => new Promise(r => setTimeout(r, 100));\n for (let i = 0; i < 600; i += 1) {\n if (lastCode) return { code: lastCode };\n if (cancelled) return null;\n await sleep();\n }\n return null;\n },\n });\n });\n });\n}\n\n/**\n * Serializable pending state for a Codex device-code login. Safe to persist\n * (e.g. a `pending jsonb` column) so polling can span HTTP requests — the\n * device token response carries the `code_verifier`, so no PKCE state needs\n * to be kept client-side.\n */\nexport interface CodexDeviceLoginPending {\n deviceAuthId: string;\n userCode: string;\n /** Verification URL for the user to open. */\n url: string;\n instructions: string;\n /** Poll interval in ms suggested by the server. */\n intervalMs: number;\n /** ms epoch after which the device authorization expires. */\n deadlineAt: number;\n}\n\nexport type CodexDevicePollResult =\n | { status: 'complete'; credentials: OAuthCredentials }\n | { status: 'pending'; nextPollMs: number }\n | { status: 'failed'; error: string };\n\n/**\n * Start a Codex device-code login: request a user code and return the\n * serializable pending state for subsequent polls.\n */\nexport async function startCodexDeviceLogin(options?: { signal?: AbortSignal }): Promise<CodexDeviceLoginPending> {\n const response = await fetch(DEVICE_USER_CODE_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'mastracode',\n },\n body: JSON.stringify({ client_id: CLIENT_ID, originator: 'mastracode' }),\n signal: requestSignal(options?.signal),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to initiate OpenAI Codex device authorization: ${response.status}`);\n }\n\n const deviceData = (await response.json()) as {\n device_auth_id?: string;\n user_code?: string;\n usercode?: string;\n interval?: string | number;\n };\n\n const userCode = deviceData.user_code ?? deviceData.usercode;\n\n if (!deviceData.device_auth_id || !userCode) {\n throw new Error('OpenAI Codex device authorization response missing required fields');\n }\n\n const intervalSeconds =\n typeof deviceData.interval === 'number' ? deviceData.interval : Number.parseInt(deviceData.interval ?? '', 10) || 5;\n\n return {\n deviceAuthId: deviceData.device_auth_id,\n userCode,\n url: DEVICE_AUTHORIZE_URL,\n instructions: `Enter code: ${userCode}`,\n intervalMs: Math.max(intervalSeconds, 1) * 1000,\n deadlineAt: Date.now() + DEVICE_AUTH_TIMEOUT_MS,\n };\n}\n\n/**\n * Perform exactly one upstream poll for a pending Codex device login.\n * The Codex device endpoint signals \"still pending\" via HTTP 403/404 (it is\n * not an RFC 8628 error-JSON endpoint); on success it returns the\n * authorization code plus server-held PKCE verifier, which we exchange\n * immediately for credentials. Never throws for flow-level conditions.\n */\nexport async function pollCodexDeviceLogin(\n pending: CodexDeviceLoginPending,\n options?: { signal?: AbortSignal },\n): Promise<CodexDevicePollResult> {\n if (Date.now() >= pending.deadlineAt) {\n return { status: 'failed', error: 'OpenAI Codex device authorization timed out after 15 minutes' };\n }\n\n const pollResponse = await fetch(DEVICE_TOKEN_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'mastracode',\n },\n body: JSON.stringify({\n device_auth_id: pending.deviceAuthId,\n user_code: pending.userCode,\n }),\n signal: requestSignal(options?.signal),\n });\n\n if (pollResponse.ok) {\n const data = (await pollResponse.json()) as {\n authorization_code?: string;\n code_verifier?: string;\n };\n\n if (!data.authorization_code || !data.code_verifier) {\n return { status: 'failed', error: 'OpenAI Codex device token response missing required fields' };\n }\n\n const tokenResult = await exchangeAuthorizationCode(\n data.authorization_code,\n data.code_verifier,\n DEVICE_REDIRECT_URI,\n options?.signal,\n );\n if (tokenResult.type !== 'success') {\n return { status: 'failed', error: 'Token exchange failed' };\n }\n\n let accountId: string;\n try {\n accountId = requireAccountId(tokenResult);\n } catch (error) {\n return { status: 'failed', error: error instanceof Error ? error.message : String(error) };\n }\n\n return {\n status: 'complete',\n credentials: {\n access: tokenResult.access,\n refresh: tokenResult.refresh,\n expires: tokenResult.expires,\n accountId,\n email: getEmailFromTokens(tokenResult),\n },\n };\n }\n\n if (pollResponse.status !== 403 && pollResponse.status !== 404) {\n return {\n status: 'failed',\n error: `OpenAI Codex device authorization failed: ${pollResponse.status}`,\n };\n }\n\n return { status: 'pending', nextPollMs: pending.intervalMs };\n}\n\nasync function loginOpenAICodexDevice(options: {\n onAuth: (info: { url: string; instructions?: string }) => void;\n onProgress?: (message: string) => void;\n signal?: AbortSignal;\n sleep?: (ms: number) => Promise<void>;\n}): Promise<OAuthCredentials> {\n const pending = await startCodexDeviceLogin({ signal: options.signal });\n const sleep = options.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)));\n\n options.onAuth({\n url: pending.url,\n instructions: pending.instructions,\n });\n\n await sleep(pending.intervalMs);\n\n while (true) {\n if (options.signal?.aborted) {\n throw new Error('Login cancelled');\n }\n\n const result = await pollCodexDeviceLogin(pending, { signal: options.signal });\n if (result.status === 'complete') {\n return result.credentials;\n }\n if (result.status === 'failed') {\n throw new Error(result.error);\n }\n\n options.onProgress?.('Waiting for OpenAI Codex device authorization...');\n await sleep(result.nextPollMs);\n }\n}\n\n/**\n * Login with OpenAI Codex OAuth\n *\n * @param options.onAuth - Called with URL and instructions when auth starts\n * @param options.onPrompt - Called to prompt user for manual code paste (fallback if no onManualCodeInput)\n * @param options.onProgress - Optional progress messages\n * @param options.onManualCodeInput - Optional promise that resolves with user-pasted code.\n * Races with browser callback - whichever completes first wins.\n * Useful for showing paste input immediately alongside browser flow.\n * @param options.originator - OAuth originator parameter (defaults to \"mastracode\")\n */\nexport async function loginOpenAICodex(options: {\n onAuth: (info: { url: string; instructions?: string }) => void;\n onPrompt: (prompt: OAuthPrompt) => Promise<string>;\n onProgress?: (message: string) => void;\n onManualCodeInput?: () => Promise<string>;\n signal?: AbortSignal;\n originator?: string;\n mode?: 'browser' | 'device';\n}): Promise<OAuthCredentials> {\n const envMode =\n typeof process !== 'undefined' && process.env?.MASTRACODE_OPENAI_CODEX_AUTH_MODE === 'device'\n ? 'device'\n : undefined;\n const mode = options.mode ?? envMode ?? 'browser';\n if (mode === 'device') {\n return loginOpenAICodexDevice({\n onAuth: options.onAuth,\n onProgress: options.onProgress,\n signal: options.signal,\n });\n }\n\n const state = await createState();\n const server = await startLocalOAuthServer(state);\n if (server.warning) {\n options.onProgress?.(server.warning);\n }\n const { verifier, url } = await createAuthorizationFlow(\n server.redirectUri,\n state,\n options.originator ?? 'mastracode',\n );\n\n options.onAuth({\n url,\n instructions: server.warning\n ? `${server.warning} You can still paste the authorization code or full redirect URL manually.`\n : 'A browser window should open. Complete login to finish.',\n });\n\n let code: string | undefined;\n try {\n if (options.onManualCodeInput) {\n // Race between browser callback and manual input\n let manualCode: string | undefined;\n let manualError: Error | undefined;\n const manualPromise = options\n .onManualCodeInput()\n .then(input => {\n manualCode = input;\n server.cancelWait();\n })\n .catch(err => {\n manualError = err instanceof Error ? err : new Error(String(err));\n server.cancelWait();\n });\n\n const result = await server.waitForCode();\n\n // If manual input was cancelled, throw that error\n if (manualError) {\n throw manualError;\n }\n\n if (result?.code) {\n // Browser callback won\n code = result.code;\n } else if (manualCode) {\n // Manual input won (or callback timed out and user had entered code)\n const parsed = parseAuthorizationInput(manualCode);\n if (parsed.state && parsed.state !== state) {\n throw new Error('State mismatch');\n }\n code = parsed.code;\n }\n\n // If still no code, wait for manual promise to complete and try that\n if (!code) {\n await manualPromise;\n if (manualError) {\n throw manualError;\n }\n if (manualCode) {\n const parsed = parseAuthorizationInput(manualCode);\n if (parsed.state && parsed.state !== state) {\n throw new Error('State mismatch');\n }\n code = parsed.code;\n }\n }\n } else {\n // Original flow: wait for callback, then prompt if needed\n const result = await server.waitForCode();\n if (result?.code) {\n code = result.code;\n }\n }\n\n // Fallback to onPrompt if still no code\n if (!code) {\n const input = await options.onPrompt({\n message: 'Paste the authorization code (or full redirect URL):',\n });\n const parsed = parseAuthorizationInput(input);\n if (parsed.state && parsed.state !== state) {\n throw new Error('State mismatch');\n }\n code = parsed.code;\n }\n\n if (!code) {\n throw new Error('Missing authorization code');\n }\n\n const tokenResult = await exchangeAuthorizationCode(code, verifier, server.redirectUri, options.signal);\n if (tokenResult.type !== 'success') {\n throw new Error('Token exchange failed');\n }\n\n const accountId = requireAccountId(tokenResult);\n\n return {\n access: tokenResult.access,\n refresh: tokenResult.refresh,\n expires: tokenResult.expires,\n accountId,\n email: getEmailFromTokens(tokenResult),\n };\n } finally {\n server.close();\n }\n}\n\nexport const __testing = {\n createAuthorizationFlow,\n decodeJwt,\n extractAccountIdFromClaims,\n getAccountId,\n loginOpenAICodexDevice,\n requireAccountId,\n startLocalOAuthServer,\n};\n\n/**\n * Refresh OpenAI Codex OAuth token\n */\nexport async function refreshOpenAICodexToken(\n refreshToken: string,\n previousAccountId?: string,\n previousEmail?: string,\n): Promise<OAuthCredentials> {\n const result = await refreshAccessToken(refreshToken);\n if (result.type !== 'success') {\n throw new Error('Failed to refresh OpenAI Codex token');\n }\n\n const accountId = requireAccountId(result, previousAccountId);\n\n return {\n access: result.access,\n refresh: result.refresh,\n expires: result.expires,\n accountId,\n email: getEmailFromTokens(result) ?? previousEmail,\n };\n}\n\nexport const openaiCodexOAuthProvider: OAuthProviderInterface = {\n id: 'openai-codex',\n name: 'ChatGPT Plus/Pro (Codex Subscription)',\n usesCallbackServer: true,\n authModes: OPENAI_CODEX_AUTH_MODES,\n\n async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n const mode = callbacks.authMode === 'device' || callbacks.authMode === 'browser' ? callbacks.authMode : undefined;\n return loginOpenAICodex({\n onAuth: callbacks.onAuth,\n onPrompt: callbacks.onPrompt,\n onProgress: callbacks.onProgress,\n onManualCodeInput: callbacks.onManualCodeInput,\n signal: callbacks.signal,\n mode,\n });\n },\n\n async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n return refreshOpenAICodexToken(\n credentials.refresh,\n credentials.accountId as string | undefined,\n credentials.email as string | undefined,\n );\n },\n\n getApiKey(credentials: OAuthCredentials): string {\n return credentials.access;\n },\n\n getAccountLabel(credentials: OAuthCredentials): Promise<string | undefined> {\n const email = credentials.email;\n return Promise.resolve(typeof email === 'string' && email.length > 0 ? email : undefined);\n },\n\n /**\n * The ChatGPT account/workspace id from the token, when present — stable\n * across refreshes and re-authorizations of one subscription.\n */\n getAccountIdentity(credentials: OAuthCredentials): string | undefined {\n const accountId = credentials.accountId;\n return typeof accountId === 'string' && accountId.length > 0 ? accountId : undefined;\n },\n};\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ const SCOPE = "openid profile email offline_access grok-cli:access api:access";
19
19
  const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
20
20
  const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;
21
21
  const REQUEST_TIMEOUT_MS = 3e4;
22
- const REFRESH_SKEW_MS = 300 * 1e3;
22
+ const REFRESH_SKEW_MS = 3e5;
23
23
  async function postForm(url, params, signal) {
24
24
  const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
25
25
  return fetch(url, {
@@ -1 +1 @@
1
- {"version":3,"file":"xai.js","names":[],"sources":["../../../src/auth/providers/xai.ts"],"sourcesContent":["/**\n * xAI OAuth flow (Grok)\n *\n * Ported from pi-mono's xAI OAuth implementation:\n * https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/xai.ts\n *\n * xAI uses a standard RFC 8628 device authorization grant, so the flow needs\n * no inbound connection to the server. The primitives are split into\n * `startXAIDeviceLogin()` / `pollXAIDeviceLogin()` so web routes can persist\n * the JSON-serializable pending state between HTTP requests; the blocking\n * `loginXAI()` wraps them for TUI use.\n */\n\nimport { createDeviceCodePollState, pollDeviceCodeUntilComplete, stepDeviceCodePoll } from '../device-code.js';\nimport type { DeviceCodePollOutcome, DeviceCodePollState } from '../device-code.js';\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from '../types.js';\n\nconst CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';\nconst DEVICE_CODE_URL = 'https://auth.x.ai/oauth2/device/code';\nconst TOKEN_URL = 'https://auth.x.ai/oauth2/token';\nconst SCOPE = 'openid profile email offline_access grok-cli:access api:access';\nconst DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';\nconst DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;\nconst REQUEST_TIMEOUT_MS = 30_000;\n// Refresh 5 minutes before actual expiry (same skew as Anthropic).\nconst REFRESH_SKEW_MS = 5 * 60 * 1000;\n\nasync function postForm(url: string, params: Record<string, string>, signal?: AbortSignal): Promise<Response> {\n const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n return fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams(params).toString(),\n signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,\n });\n}\n\n/** The verification URI is opened by the user; only accept https URLs. */\nfunction validateVerificationUri(raw: string): string {\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new Error('xAI device authorization returned an invalid verification_uri');\n }\n if (parsed.protocol !== 'https:') {\n throw new Error('xAI device authorization returned a non-https verification_uri');\n }\n return parsed.toString();\n}\n\nfunction credentialsFromTokenResponse(\n data: unknown,\n previousRefreshToken?: string,\n previousIdToken?: string,\n): OAuthCredentials {\n const record = (data ?? {}) as Record<string, unknown>;\n const access = record.access_token;\n if (typeof access !== 'string' || access.length === 0) {\n throw new Error('xAI token response missing access_token');\n }\n\n // xAI may not rotate the refresh token on refresh; keep the previous one.\n const refresh =\n typeof record.refresh_token === 'string' && record.refresh_token.length > 0\n ? record.refresh_token\n : previousRefreshToken;\n if (!refresh) {\n throw new Error('xAI token response missing refresh_token');\n }\n\n const expiresIn =\n typeof record.expires_in === 'number' && record.expires_in > 0\n ? record.expires_in\n : DEFAULT_TOKEN_EXPIRES_IN_SECONDS;\n\n return {\n access,\n refresh,\n expires: Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS,\n // Kept so the account label can resolve the email claim later; xAI's\n // refresh responses re-issue it, and the previous one is carried forward\n // when they don't.\n idToken: typeof record.id_token === 'string' && record.id_token.length > 0 ? record.id_token : previousIdToken,\n };\n}\n\n/**\n * Serializable pending state for an xAI device-code login. Safe to persist\n * (e.g. a `pending jsonb` column) so polling can span HTTP requests.\n */\nexport interface XAIDeviceLoginPending {\n deviceCode: string;\n userCode: string;\n /** Verification URL for the user to open (https-only, validated). */\n url: string;\n instructions: string;\n /** RFC 8628 poll-loop state (interval growth, deadline, slow_down count). */\n state: DeviceCodePollState;\n}\n\nexport type XAIDevicePollResult =\n | { status: 'complete'; credentials: OAuthCredentials }\n | { status: 'pending'; nextPollMs: number; pending: XAIDeviceLoginPending }\n | { status: 'failed'; error: string };\n\n/**\n * Start an xAI device-code login: request a user code and return the\n * serializable pending state for subsequent polls.\n */\nexport async function startXAIDeviceLogin(options?: { signal?: AbortSignal }): Promise<XAIDeviceLoginPending> {\n const response = await postForm(DEVICE_CODE_URL, { client_id: CLIENT_ID, scope: SCOPE }, options?.signal);\n\n if (!response.ok) {\n throw new Error(`Failed to initiate xAI device authorization: ${response.status}`);\n }\n\n const data = (await response.json()) as {\n device_code?: string;\n user_code?: string;\n verification_uri?: string;\n verification_uri_complete?: string;\n interval?: number;\n expires_in?: number;\n };\n\n if (!data.device_code || !data.user_code || !data.verification_uri) {\n throw new Error('xAI device authorization response missing required fields');\n }\n\n const url = validateVerificationUri(data.verification_uri_complete ?? data.verification_uri);\n\n return {\n deviceCode: data.device_code,\n userCode: data.user_code,\n url,\n instructions: `Enter code: ${data.user_code}`,\n state: createDeviceCodePollState({\n intervalSeconds: data.interval,\n expiresInSeconds: typeof data.expires_in === 'number' && data.expires_in > 0 ? data.expires_in : 600,\n }),\n };\n}\n\nasync function pollXAITokenOnce(\n pending: XAIDeviceLoginPending,\n signal?: AbortSignal,\n): Promise<DeviceCodePollOutcome<OAuthCredentials>> {\n const response = await postForm(\n TOKEN_URL,\n {\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: pending.deviceCode,\n client_id: CLIENT_ID,\n },\n signal,\n );\n\n if (response.ok) {\n const data = (await response.json()) as unknown;\n try {\n return { status: 'complete', result: credentialsFromTokenResponse(data) };\n } catch (error) {\n return { status: 'failed', error: error instanceof Error ? error.message : String(error) };\n }\n }\n\n const text = await response.text().catch(() => '');\n let body: { error?: string; interval?: number } = {};\n try {\n body = JSON.parse(text) as { error?: string; interval?: number };\n } catch {\n // Non-JSON upstream bodies are intentionally omitted from user-visible errors.\n }\n\n switch (body.error) {\n case 'authorization_pending':\n return { status: 'pending', intervalSeconds: body.interval };\n case 'slow_down':\n return { status: 'slow_down', intervalSeconds: body.interval };\n case 'access_denied':\n case 'authorization_denied':\n return { status: 'failed', error: 'xAI authorization was denied' };\n case 'expired_token':\n return { status: 'failed', error: 'xAI device code expired before authorization completed' };\n default:\n // Status only: `body.error` is untrusted upstream text (RFC 6749 does not\n // constrain it), so echoing it would render provider-controlled content\n // in the TUI. The known flow codes above are handled explicitly.\n return {\n status: 'failed',\n error: `xAI device authorization failed: ${response.status}`,\n };\n }\n}\n\n/**\n * Perform exactly one upstream poll for a pending xAI device login.\n * Returns the updated pending state so callers (e.g. web routes) can persist\n * slow_down interval growth between polls. Never throws for flow-level\n * conditions.\n */\nexport async function pollXAIDeviceLogin(\n pending: XAIDeviceLoginPending,\n options?: { signal?: AbortSignal },\n): Promise<XAIDevicePollResult> {\n const step = await stepDeviceCodePoll(pending.state, () => pollXAITokenOnce(pending, options?.signal));\n\n switch (step.status) {\n case 'complete':\n return { status: 'complete', credentials: step.result };\n case 'failed':\n return { status: 'failed', error: step.error };\n case 'pending':\n case 'slow_down':\n return {\n status: 'pending',\n nextPollMs: step.nextPollMs,\n pending: { ...pending, state: step.state },\n };\n }\n}\n\n/**\n * Login with xAI OAuth (device-code flow), blocking until authorized.\n */\nexport async function loginXAI(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n const pending = await startXAIDeviceLogin({ signal: callbacks.signal });\n\n callbacks.onAuth({ url: pending.url, instructions: pending.instructions, userCode: pending.userCode });\n callbacks.onProgress?.('Waiting for xAI device authorization...');\n\n return pollDeviceCodeUntilComplete({\n state: pending.state,\n pollOnce: () => pollXAITokenOnce(pending, callbacks.signal),\n signal: callbacks.signal,\n });\n}\n\n/**\n * Refresh xAI OAuth token\n */\nexport async function refreshXAIToken(\n refreshToken: string,\n signal?: AbortSignal,\n previousIdToken?: string,\n): Promise<OAuthCredentials> {\n const response = await postForm(\n TOKEN_URL,\n {\n grant_type: 'refresh_token',\n client_id: CLIENT_ID,\n refresh_token: refreshToken,\n },\n signal,\n );\n\n if (!response.ok) {\n throw new Error(`xAI token refresh failed: ${response.status}`);\n }\n\n return credentialsFromTokenResponse((await response.json()) as unknown, refreshToken, previousIdToken);\n}\n\n/** Decode the email claim out of an id_token JWT payload (no verification). */\nfunction emailFromIdToken(idToken: string | undefined): string | undefined {\n if (typeof idToken !== 'string') return undefined;\n const parts = idToken.split('.');\n if (parts.length !== 3) return undefined;\n try {\n const padded = (parts[1] ?? '').replace(/-/g, '+').replace(/_/g, '/');\n const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) as { email?: unknown };\n return typeof json.email === 'string' && json.email.length > 0 ? json.email : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport const xaiOAuthProvider: OAuthProviderInterface = {\n id: 'xai',\n name: 'xAI (Grok)',\n\n async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n return loginXAI(callbacks);\n },\n\n async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n return refreshXAIToken(credentials.refresh, undefined, credentials.idToken as string | undefined);\n },\n\n getApiKey(credentials: OAuthCredentials): string {\n return credentials.access;\n },\n\n getAccountLabel(credentials: OAuthCredentials): Promise<string | undefined> {\n return Promise.resolve(emailFromIdToken(credentials.idToken as string | undefined));\n },\n\n /**\n * The account's email, read from the id token — one subscription per email,\n * so this distinguishes two xAI accounts from a re-authorization of one.\n */\n getAccountIdentity(credentials: OAuthCredentials): string | undefined {\n return emailFromIdToken(credentials.idToken as string | undefined);\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"xai.js","names":[],"sources":["../../../src/auth/providers/xai.ts"],"sourcesContent":["/**\n * xAI OAuth flow (Grok)\n *\n * Ported from pi-mono's xAI OAuth implementation:\n * https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/xai.ts\n *\n * xAI uses a standard RFC 8628 device authorization grant, so the flow needs\n * no inbound connection to the server. The primitives are split into\n * `startXAIDeviceLogin()` / `pollXAIDeviceLogin()` so web routes can persist\n * the JSON-serializable pending state between HTTP requests; the blocking\n * `loginXAI()` wraps them for TUI use.\n */\n\nimport { createDeviceCodePollState, pollDeviceCodeUntilComplete, stepDeviceCodePoll } from '../device-code.js';\nimport type { DeviceCodePollOutcome, DeviceCodePollState } from '../device-code.js';\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from '../types.js';\n\nconst CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';\nconst DEVICE_CODE_URL = 'https://auth.x.ai/oauth2/device/code';\nconst TOKEN_URL = 'https://auth.x.ai/oauth2/token';\nconst SCOPE = 'openid profile email offline_access grok-cli:access api:access';\nconst DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';\nconst DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;\nconst REQUEST_TIMEOUT_MS = 30_000;\n// Refresh 5 minutes before actual expiry (same skew as Anthropic).\nconst REFRESH_SKEW_MS = 5 * 60 * 1000;\n\nasync function postForm(url: string, params: Record<string, string>, signal?: AbortSignal): Promise<Response> {\n const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n return fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams(params).toString(),\n signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,\n });\n}\n\n/** The verification URI is opened by the user; only accept https URLs. */\nfunction validateVerificationUri(raw: string): string {\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new Error('xAI device authorization returned an invalid verification_uri');\n }\n if (parsed.protocol !== 'https:') {\n throw new Error('xAI device authorization returned a non-https verification_uri');\n }\n return parsed.toString();\n}\n\nfunction credentialsFromTokenResponse(\n data: unknown,\n previousRefreshToken?: string,\n previousIdToken?: string,\n): OAuthCredentials {\n const record = (data ?? {}) as Record<string, unknown>;\n const access = record.access_token;\n if (typeof access !== 'string' || access.length === 0) {\n throw new Error('xAI token response missing access_token');\n }\n\n // xAI may not rotate the refresh token on refresh; keep the previous one.\n const refresh =\n typeof record.refresh_token === 'string' && record.refresh_token.length > 0\n ? record.refresh_token\n : previousRefreshToken;\n if (!refresh) {\n throw new Error('xAI token response missing refresh_token');\n }\n\n const expiresIn =\n typeof record.expires_in === 'number' && record.expires_in > 0\n ? record.expires_in\n : DEFAULT_TOKEN_EXPIRES_IN_SECONDS;\n\n return {\n access,\n refresh,\n expires: Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS,\n // Kept so the account label can resolve the email claim later; xAI's\n // refresh responses re-issue it, and the previous one is carried forward\n // when they don't.\n idToken: typeof record.id_token === 'string' && record.id_token.length > 0 ? record.id_token : previousIdToken,\n };\n}\n\n/**\n * Serializable pending state for an xAI device-code login. Safe to persist\n * (e.g. a `pending jsonb` column) so polling can span HTTP requests.\n */\nexport interface XAIDeviceLoginPending {\n deviceCode: string;\n userCode: string;\n /** Verification URL for the user to open (https-only, validated). */\n url: string;\n instructions: string;\n /** RFC 8628 poll-loop state (interval growth, deadline, slow_down count). */\n state: DeviceCodePollState;\n}\n\nexport type XAIDevicePollResult =\n | { status: 'complete'; credentials: OAuthCredentials }\n | { status: 'pending'; nextPollMs: number; pending: XAIDeviceLoginPending }\n | { status: 'failed'; error: string };\n\n/**\n * Start an xAI device-code login: request a user code and return the\n * serializable pending state for subsequent polls.\n */\nexport async function startXAIDeviceLogin(options?: { signal?: AbortSignal }): Promise<XAIDeviceLoginPending> {\n const response = await postForm(DEVICE_CODE_URL, { client_id: CLIENT_ID, scope: SCOPE }, options?.signal);\n\n if (!response.ok) {\n throw new Error(`Failed to initiate xAI device authorization: ${response.status}`);\n }\n\n const data = (await response.json()) as {\n device_code?: string;\n user_code?: string;\n verification_uri?: string;\n verification_uri_complete?: string;\n interval?: number;\n expires_in?: number;\n };\n\n if (!data.device_code || !data.user_code || !data.verification_uri) {\n throw new Error('xAI device authorization response missing required fields');\n }\n\n const url = validateVerificationUri(data.verification_uri_complete ?? data.verification_uri);\n\n return {\n deviceCode: data.device_code,\n userCode: data.user_code,\n url,\n instructions: `Enter code: ${data.user_code}`,\n state: createDeviceCodePollState({\n intervalSeconds: data.interval,\n expiresInSeconds: typeof data.expires_in === 'number' && data.expires_in > 0 ? data.expires_in : 600,\n }),\n };\n}\n\nasync function pollXAITokenOnce(\n pending: XAIDeviceLoginPending,\n signal?: AbortSignal,\n): Promise<DeviceCodePollOutcome<OAuthCredentials>> {\n const response = await postForm(\n TOKEN_URL,\n {\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: pending.deviceCode,\n client_id: CLIENT_ID,\n },\n signal,\n );\n\n if (response.ok) {\n const data = (await response.json()) as unknown;\n try {\n return { status: 'complete', result: credentialsFromTokenResponse(data) };\n } catch (error) {\n return { status: 'failed', error: error instanceof Error ? error.message : String(error) };\n }\n }\n\n const text = await response.text().catch(() => '');\n let body: { error?: string; interval?: number } = {};\n try {\n body = JSON.parse(text) as { error?: string; interval?: number };\n } catch {\n // Non-JSON upstream bodies are intentionally omitted from user-visible errors.\n }\n\n switch (body.error) {\n case 'authorization_pending':\n return { status: 'pending', intervalSeconds: body.interval };\n case 'slow_down':\n return { status: 'slow_down', intervalSeconds: body.interval };\n case 'access_denied':\n case 'authorization_denied':\n return { status: 'failed', error: 'xAI authorization was denied' };\n case 'expired_token':\n return { status: 'failed', error: 'xAI device code expired before authorization completed' };\n default:\n // Status only: `body.error` is untrusted upstream text (RFC 6749 does not\n // constrain it), so echoing it would render provider-controlled content\n // in the TUI. The known flow codes above are handled explicitly.\n return {\n status: 'failed',\n error: `xAI device authorization failed: ${response.status}`,\n };\n }\n}\n\n/**\n * Perform exactly one upstream poll for a pending xAI device login.\n * Returns the updated pending state so callers (e.g. web routes) can persist\n * slow_down interval growth between polls. Never throws for flow-level\n * conditions.\n */\nexport async function pollXAIDeviceLogin(\n pending: XAIDeviceLoginPending,\n options?: { signal?: AbortSignal },\n): Promise<XAIDevicePollResult> {\n const step = await stepDeviceCodePoll(pending.state, () => pollXAITokenOnce(pending, options?.signal));\n\n switch (step.status) {\n case 'complete':\n return { status: 'complete', credentials: step.result };\n case 'failed':\n return { status: 'failed', error: step.error };\n case 'pending':\n case 'slow_down':\n return {\n status: 'pending',\n nextPollMs: step.nextPollMs,\n pending: { ...pending, state: step.state },\n };\n }\n}\n\n/**\n * Login with xAI OAuth (device-code flow), blocking until authorized.\n */\nexport async function loginXAI(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n const pending = await startXAIDeviceLogin({ signal: callbacks.signal });\n\n callbacks.onAuth({ url: pending.url, instructions: pending.instructions, userCode: pending.userCode });\n callbacks.onProgress?.('Waiting for xAI device authorization...');\n\n return pollDeviceCodeUntilComplete({\n state: pending.state,\n pollOnce: () => pollXAITokenOnce(pending, callbacks.signal),\n signal: callbacks.signal,\n });\n}\n\n/**\n * Refresh xAI OAuth token\n */\nexport async function refreshXAIToken(\n refreshToken: string,\n signal?: AbortSignal,\n previousIdToken?: string,\n): Promise<OAuthCredentials> {\n const response = await postForm(\n TOKEN_URL,\n {\n grant_type: 'refresh_token',\n client_id: CLIENT_ID,\n refresh_token: refreshToken,\n },\n signal,\n );\n\n if (!response.ok) {\n throw new Error(`xAI token refresh failed: ${response.status}`);\n }\n\n return credentialsFromTokenResponse((await response.json()) as unknown, refreshToken, previousIdToken);\n}\n\n/** Decode the email claim out of an id_token JWT payload (no verification). */\nfunction emailFromIdToken(idToken: string | undefined): string | undefined {\n if (typeof idToken !== 'string') return undefined;\n const parts = idToken.split('.');\n if (parts.length !== 3) return undefined;\n try {\n const padded = (parts[1] ?? '').replace(/-/g, '+').replace(/_/g, '/');\n const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) as { email?: unknown };\n return typeof json.email === 'string' && json.email.length > 0 ? json.email : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport const xaiOAuthProvider: OAuthProviderInterface = {\n id: 'xai',\n name: 'xAI (Grok)',\n\n async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n return loginXAI(callbacks);\n },\n\n async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n return refreshXAIToken(credentials.refresh, undefined, credentials.idToken as string | undefined);\n },\n\n getApiKey(credentials: OAuthCredentials): string {\n return credentials.access;\n },\n\n getAccountLabel(credentials: OAuthCredentials): Promise<string | undefined> {\n return Promise.resolve(emailFromIdToken(credentials.idToken as string | undefined));\n },\n\n /**\n * The account's email, read from the id token — one subscription per email,\n * so this distinguishes two xAI accounts from a re-authorization of one.\n */\n getAccountIdentity(credentials: OAuthCredentials): string | undefined {\n return emailFromIdToken(credentials.idToken as string | undefined);\n },\n};\n"],"mappings":"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"}
@@ -132,12 +132,13 @@ function getMacClipboardImage() {
132
132
  function tryReadMacClipboardImage({ coercion, extension, mimeType }) {
133
133
  const tmpFile = join(tmpdir(), `mastra-clipboard-${Date.now()}.${extension}`);
134
134
  try {
135
- execSync(`osascript -e '${`
135
+ const script = `
136
136
  set theImage to the clipboard as ${coercion}
137
137
  set theFile to open for access POSIX file "${tmpFile}" with write permission
138
138
  write theImage to theFile
139
139
  close access theFile
140
- `.replace(/'/g, "'\\''")}'`, {
140
+ `;
141
+ execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
141
142
  timeout: 5e3,
142
143
  stdio: [
143
144
  "pipe",
@@ -180,7 +181,7 @@ function getLinuxClipboardImageXclip() {
180
181
  "pipe",
181
182
  "pipe"
182
183
  ],
183
- maxBuffer: 50 * 1024 * 1024
184
+ maxBuffer: 52428800
184
185
  });
185
186
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null;
186
187
  return {
@@ -209,7 +210,7 @@ function getLinuxClipboardImageWlPaste() {
209
210
  "pipe",
210
211
  "pipe"
211
212
  ],
212
- maxBuffer: 50 * 1024 * 1024
213
+ maxBuffer: 52428800
213
214
  });
214
215
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null;
215
216
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/clipboard/index.ts"],"sourcesContent":["/**\n * Platform-specific clipboard image extraction.\n *\n * Checks the system clipboard for image data and returns it as base64.\n * Uses synchronous execution (execSync) since this only runs on paste events.\n */\n\nimport { execSync } from 'node:child_process';\nimport { readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nexport interface ClipboardImage {\n data: string; // base64-encoded image data or a remote image URL\n mimeType: string;\n}\n\n/**\n * Read plain text from the system clipboard.\n * Returns null if clipboard is empty or reading fails.\n */\nexport function getClipboardText(): string | null {\n try {\n if (process.platform === 'darwin') {\n const text = execSync('pbpaste', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return text.length > 0 ? text : null;\n }\n if (process.platform === 'linux') {\n // Try xclip first, then wl-paste\n try {\n const text = execSync('xclip -selection clipboard -o', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return text.length > 0 ? text : null;\n } catch {\n const text = execSync('wl-paste', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return text.length > 0 ? text : null;\n }\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Write plain text to the system clipboard.\n * Returns true on success, false on failure.\n */\nexport function setClipboardText(text: string): boolean {\n try {\n if (process.platform === 'darwin') {\n execSync('pbcopy', {\n input: text,\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return true;\n }\n if (process.platform === 'linux') {\n try {\n execSync('xclip -selection clipboard', {\n input: text,\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return true;\n } catch {\n execSync('wl-copy', {\n input: text,\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return true;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\n/**\n * Check the system clipboard for image data and return it as base64.\n * Returns null if no image data is found or extraction fails.\n */\nexport function getClipboardImage(): ClipboardImage | null {\n try {\n if (process.platform === 'darwin') {\n return getMacClipboardImage();\n }\n if (process.platform === 'linux') {\n return getLinuxClipboardImage();\n }\n return null;\n } catch {\n return null;\n }\n}\n\n// =============================================================================\n// macOS\n// =============================================================================\n\nfunction getMacClipboardImage(): ClipboardImage | null {\n return (\n tryReadMacClipboardImage({\n coercion: '«class PNGf»',\n extension: 'png',\n mimeType: 'image/png',\n }) ??\n tryReadMacClipboardImage({\n coercion: 'TIFF picture',\n extension: 'tiff',\n mimeType: 'image/tiff',\n }) ??\n tryReadMacClipboardImage({\n coercion: '«class TIFF»',\n extension: 'tiff',\n mimeType: 'image/tiff',\n })\n );\n}\n\nfunction tryReadMacClipboardImage({\n coercion,\n extension,\n mimeType,\n}: {\n coercion: string;\n extension: string;\n mimeType: string;\n}): ClipboardImage | null {\n const tmpFile = join(tmpdir(), `mastra-clipboard-${Date.now()}.${extension}`);\n\n try {\n const script = `\n\t\t\tset theImage to the clipboard as ${coercion}\n\t\t\tset theFile to open for access POSIX file \"${tmpFile}\" with write permission\n\t\t\twrite theImage to theFile\n\t\t\tclose access theFile\n\t\t`;\n execSync(`osascript -e '${script.replace(/'/g, \"'\\\\''\")}'`, {\n timeout: 5000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n const buffer = readFileSync(tmpFile);\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n return null;\n }\n\n return {\n data: buffer.toString('base64'),\n mimeType,\n };\n } catch {\n return null;\n } finally {\n try {\n unlinkSync(tmpFile);\n } catch {\n // ignore cleanup errors\n }\n }\n}\n\n// =============================================================================\n// Linux\n// =============================================================================\n\nfunction getLinuxClipboardImage(): ClipboardImage | null {\n // Try xclip first, then wl-paste (Wayland)\n return getLinuxClipboardImageXclip() ?? getLinuxClipboardImageWlPaste();\n}\n\nfunction getLinuxClipboardImageXclip(): ClipboardImage | null {\n try {\n // Check available targets\n const targets = execSync('xclip -selection clipboard -target TARGETS -o', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n if (!targets.includes('image/png')) {\n return null;\n }\n\n // Extract PNG data\n const buffer = execSync('xclip -selection clipboard -target image/png -o', {\n timeout: 5000,\n stdio: ['pipe', 'pipe', 'pipe'],\n maxBuffer: 50 * 1024 * 1024, // 50MB max\n });\n\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n return null;\n }\n\n return {\n data: buffer.toString('base64'),\n mimeType: 'image/png',\n };\n } catch {\n return null;\n }\n}\n\nfunction getLinuxClipboardImageWlPaste(): ClipboardImage | null {\n try {\n // Check if wl-paste is available and clipboard has image\n const types = execSync('wl-paste --list-types', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n if (!types.includes('image/png')) {\n return null;\n }\n\n // Extract PNG data\n const buffer = execSync('wl-paste --type image/png', {\n timeout: 5000,\n stdio: ['pipe', 'pipe', 'pipe'],\n maxBuffer: 50 * 1024 * 1024,\n });\n\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n return null;\n }\n\n return {\n data: buffer.toString('base64'),\n mimeType: 'image/png',\n };\n } catch {\n return null;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/clipboard/index.ts"],"sourcesContent":["/**\n * Platform-specific clipboard image extraction.\n *\n * Checks the system clipboard for image data and returns it as base64.\n * Uses synchronous execution (execSync) since this only runs on paste events.\n */\n\nimport { execSync } from 'node:child_process';\nimport { readFileSync, unlinkSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nexport interface ClipboardImage {\n data: string; // base64-encoded image data or a remote image URL\n mimeType: string;\n}\n\n/**\n * Read plain text from the system clipboard.\n * Returns null if clipboard is empty or reading fails.\n */\nexport function getClipboardText(): string | null {\n try {\n if (process.platform === 'darwin') {\n const text = execSync('pbpaste', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return text.length > 0 ? text : null;\n }\n if (process.platform === 'linux') {\n // Try xclip first, then wl-paste\n try {\n const text = execSync('xclip -selection clipboard -o', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return text.length > 0 ? text : null;\n } catch {\n const text = execSync('wl-paste', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return text.length > 0 ? text : null;\n }\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Write plain text to the system clipboard.\n * Returns true on success, false on failure.\n */\nexport function setClipboardText(text: string): boolean {\n try {\n if (process.platform === 'darwin') {\n execSync('pbcopy', {\n input: text,\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return true;\n }\n if (process.platform === 'linux') {\n try {\n execSync('xclip -selection clipboard', {\n input: text,\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return true;\n } catch {\n execSync('wl-copy', {\n input: text,\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return true;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\n/**\n * Check the system clipboard for image data and return it as base64.\n * Returns null if no image data is found or extraction fails.\n */\nexport function getClipboardImage(): ClipboardImage | null {\n try {\n if (process.platform === 'darwin') {\n return getMacClipboardImage();\n }\n if (process.platform === 'linux') {\n return getLinuxClipboardImage();\n }\n return null;\n } catch {\n return null;\n }\n}\n\n// =============================================================================\n// macOS\n// =============================================================================\n\nfunction getMacClipboardImage(): ClipboardImage | null {\n return (\n tryReadMacClipboardImage({\n coercion: '«class PNGf»',\n extension: 'png',\n mimeType: 'image/png',\n }) ??\n tryReadMacClipboardImage({\n coercion: 'TIFF picture',\n extension: 'tiff',\n mimeType: 'image/tiff',\n }) ??\n tryReadMacClipboardImage({\n coercion: '«class TIFF»',\n extension: 'tiff',\n mimeType: 'image/tiff',\n })\n );\n}\n\nfunction tryReadMacClipboardImage({\n coercion,\n extension,\n mimeType,\n}: {\n coercion: string;\n extension: string;\n mimeType: string;\n}): ClipboardImage | null {\n const tmpFile = join(tmpdir(), `mastra-clipboard-${Date.now()}.${extension}`);\n\n try {\n const script = `\n\t\t\tset theImage to the clipboard as ${coercion}\n\t\t\tset theFile to open for access POSIX file \"${tmpFile}\" with write permission\n\t\t\twrite theImage to theFile\n\t\t\tclose access theFile\n\t\t`;\n execSync(`osascript -e '${script.replace(/'/g, \"'\\\\''\")}'`, {\n timeout: 5000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n const buffer = readFileSync(tmpFile);\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n return null;\n }\n\n return {\n data: buffer.toString('base64'),\n mimeType,\n };\n } catch {\n return null;\n } finally {\n try {\n unlinkSync(tmpFile);\n } catch {\n // ignore cleanup errors\n }\n }\n}\n\n// =============================================================================\n// Linux\n// =============================================================================\n\nfunction getLinuxClipboardImage(): ClipboardImage | null {\n // Try xclip first, then wl-paste (Wayland)\n return getLinuxClipboardImageXclip() ?? getLinuxClipboardImageWlPaste();\n}\n\nfunction getLinuxClipboardImageXclip(): ClipboardImage | null {\n try {\n // Check available targets\n const targets = execSync('xclip -selection clipboard -target TARGETS -o', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n if (!targets.includes('image/png')) {\n return null;\n }\n\n // Extract PNG data\n const buffer = execSync('xclip -selection clipboard -target image/png -o', {\n timeout: 5000,\n stdio: ['pipe', 'pipe', 'pipe'],\n maxBuffer: 50 * 1024 * 1024, // 50MB max\n });\n\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n return null;\n }\n\n return {\n data: buffer.toString('base64'),\n mimeType: 'image/png',\n };\n } catch {\n return null;\n }\n}\n\nfunction getLinuxClipboardImageWlPaste(): ClipboardImage | null {\n try {\n // Check if wl-paste is available and clipboard has image\n const types = execSync('wl-paste --list-types', {\n encoding: 'utf-8',\n timeout: 3000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n if (!types.includes('image/png')) {\n return null;\n }\n\n // Extract PNG data\n const buffer = execSync('wl-paste --type image/png', {\n timeout: 5000,\n stdio: ['pipe', 'pipe', 'pipe'],\n maxBuffer: 50 * 1024 * 1024,\n });\n\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n return null;\n }\n\n return {\n data: buffer.toString('base64'),\n mimeType: 'image/png',\n };\n } catch {\n return null;\n }\n}\n"],"mappings":"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"}
@@ -74,9 +74,7 @@ function categorizeMessages(messages) {
74
74
  content: extractTextContent(msg)
75
75
  });
76
76
  break;
77
- case "assistant":
78
- outputMessages.push(msg);
79
- break;
77
+ case "assistant": outputMessages.push(msg);
80
78
  }
81
79
  return {
82
80
  inputMessages,
@@ -1 +1 @@
1
- {"version":3,"file":"context-builder.js","names":[],"sources":["../../src/evals/context-builder.ts"],"sourcesContent":["/**\n * Eval Context Builder for MastraCode\n *\n * Converts AgentController session data into scorer-compatible format.\n * Supports both agent-type scorers (ScorerRunInputForAgent) and\n * trajectory scorers (Trajectory).\n */\n\nimport type { MastraDBMessage } from '@mastra/core/agent';\nimport type { AgentController, Session } from '@mastra/core/agent-controller';\nimport { extractTrajectoryFromTrace } from '@mastra/core/evals';\nimport type { ScorerRunInputForAgent, ScorerRunOutputForAgent, Trajectory } from '@mastra/core/evals';\nimport type { CoreMessage, CoreSystemMessage } from '@mastra/core/llm';\nimport type { MastraCompositeStore } from '@mastra/core/storage';\n\nexport type MastraCodeEvalContext = {\n /** Scorer-compatible agent input (messages, system prompt, etc.) */\n agentInput: ScorerRunInputForAgent;\n /** Scorer-compatible agent output (response messages) */\n agentOutput: ScorerRunOutputForAgent;\n /** Execution trajectory extracted from trace spans */\n trajectory: Trajectory | undefined;\n /** Request context with session metadata */\n requestContext: Record<string, unknown>;\n /** The thread ID this context was built from */\n threadId: string;\n /** The trace ID for observability linkage */\n traceId: string | undefined;\n};\n\nexport type BuildContextOptions = {\n /** AgentController instance to extract data from */\n controller: AgentController<any>;\n /** Session whose state/model/thread is being evaluated */\n session: Session<any>;\n /** Thread ID to build context for (defaults to current thread) */\n threadId?: string;\n /** Limit messages to the last N turns (user+assistant pairs). Undefined = all messages */\n lastNTurns?: number;\n};\n\n/**\n * Build an evaluation context from a AgentController session.\n *\n * This extracts messages from storage, builds trajectory from trace spans,\n * and packages everything into the format scorers expect.\n */\nexport async function buildEvalContext(options: BuildContextOptions): Promise<MastraCodeEvalContext> {\n const { controller, session, lastNTurns } = options;\n const threadId = options.threadId ?? session.thread.getId();\n\n if (!threadId) {\n throw new Error('No thread ID available. Start a session before building eval context.');\n }\n\n const mastra = controller.getMastra();\n const storage = mastra?.getStorage();\n\n // 1. Get raw MastraDB messages from memory storage\n const rawMessages = await getRawMessages(storage, threadId, lastNTurns);\n\n // 2. Split messages into input/output categories\n const { inputMessages, systemMessages, outputMessages } = categorizeMessages(rawMessages);\n\n // 3. Extract trajectory from observability traces\n const { trajectory, traceId } = await extractSessionTrajectory(storage, threadId);\n\n // 4. Build request context from AgentController state\n const requestContext = buildRequestContext(session, threadId);\n\n return {\n agentInput: {\n inputMessages,\n rememberedMessages: [], // AgentController doesn't separate these; memory recall is transparent\n systemMessages,\n taggedSystemMessages: {},\n },\n agentOutput: outputMessages,\n trajectory,\n requestContext,\n threadId,\n traceId,\n };\n}\n\n/**\n * Fetch raw MastraDBMessage[] from memory storage for a thread.\n */\nasync function getRawMessages(\n storage: MastraCompositeStore | undefined,\n threadId: string,\n lastNTurns?: number,\n): Promise<MastraDBMessage[]> {\n if (!storage) return [];\n\n const memoryStore = await storage.getStore('memory');\n if (!memoryStore) return [];\n\n const result = await memoryStore.listMessages({\n threadId,\n perPage: false,\n });\n\n let messages = result.messages;\n\n if (lastNTurns !== undefined && lastNTurns > 0) {\n messages = trimToLastNTurns(messages, lastNTurns);\n }\n\n return messages;\n}\n\n/**\n * Trim messages to the last N user-assistant turn pairs.\n */\nfunction trimToLastNTurns(messages: MastraDBMessage[], turns: number): MastraDBMessage[] {\n // Walk backward, counting user messages as turn boundaries.\n // When we find the Nth user message, slice from there to include\n // that turn and everything after it.\n let turnCount = 0;\n let cutoffIndex = 0;\n\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i]!.role === 'user') {\n turnCount++;\n if (turnCount === turns) {\n cutoffIndex = i;\n break;\n }\n }\n }\n\n return messages.slice(cutoffIndex);\n}\n\n/**\n * Categorize messages into input (user), system, and output (assistant) groups.\n */\nfunction categorizeMessages(messages: MastraDBMessage[]): {\n inputMessages: MastraDBMessage[];\n systemMessages: CoreMessage[];\n outputMessages: MastraDBMessage[];\n} {\n const inputMessages: MastraDBMessage[] = [];\n const systemMessages: CoreSystemMessage[] = [];\n const outputMessages: MastraDBMessage[] = [];\n\n for (const msg of messages) {\n switch (msg.role) {\n case 'user':\n inputMessages.push(msg);\n break;\n case 'system':\n systemMessages.push({\n role: 'system',\n content: extractTextContent(msg),\n });\n break;\n case 'assistant':\n outputMessages.push(msg);\n break;\n }\n }\n\n return { inputMessages, systemMessages, outputMessages };\n}\n\n/**\n * Extract plain text content from a MastraDBMessage.\n */\nfunction extractTextContent(msg: MastraDBMessage): string {\n if (!msg.content?.parts) return '';\n\n return msg.content.parts\n .filter((p): p is { type: 'text'; text: string } => p.type === 'text' && 'text' in p)\n .map(p => p.text)\n .join('\\n');\n}\n\n/**\n * Extract trajectory from trace spans in observability storage.\n * Looks up traces by threadId metadata.\n */\nasync function extractSessionTrajectory(\n storage: MastraCompositeStore | undefined,\n threadId: string,\n): Promise<{ trajectory: Trajectory | undefined; traceId: string | undefined }> {\n if (!storage) return { trajectory: undefined, traceId: undefined };\n\n try {\n const observabilityStore = await storage.getStore('observability');\n if (!observabilityStore) return { trajectory: undefined, traceId: undefined };\n\n // Find traces associated with this thread\n const result = await observabilityStore.listTraces({\n filters: { threadId },\n pagination: { page: 0, perPage: 1 },\n orderBy: { field: 'startedAt', direction: 'DESC' },\n });\n\n if (!result?.spans?.length) return { trajectory: undefined, traceId: undefined };\n\n const traceId = result.spans[0]!.traceId;\n const trace = await observabilityStore.getTrace({ traceId });\n\n if (!trace?.spans?.length) return { trajectory: undefined, traceId };\n\n const trajectory = extractTrajectoryFromTrace(trace.spans);\n return { trajectory, traceId };\n } catch {\n // Trace extraction is best-effort\n return { trajectory: undefined, traceId: undefined };\n }\n}\n\n/**\n * Build request context from AgentController state for scorer evaluation.\n */\nfunction buildRequestContext(session: Session<any>, threadId: string): Record<string, unknown> {\n const state = session.state.get() as Record<string, unknown>;\n\n return {\n threadId,\n mode: state.currentMode ?? state.mode,\n modelId: session.model.get(),\n projectPath: state.projectPath,\n projectName: state.projectName,\n gitBranch: state.gitBranch,\n thinkingLevel: state.thinkingLevel,\n yolo: state.yolo,\n smartEditing: state.smartEditing,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"context-builder.js","names":[],"sources":["../../src/evals/context-builder.ts"],"sourcesContent":["/**\n * Eval Context Builder for MastraCode\n *\n * Converts AgentController session data into scorer-compatible format.\n * Supports both agent-type scorers (ScorerRunInputForAgent) and\n * trajectory scorers (Trajectory).\n */\n\nimport type { MastraDBMessage } from '@mastra/core/agent';\nimport type { AgentController, Session } from '@mastra/core/agent-controller';\nimport { extractTrajectoryFromTrace } from '@mastra/core/evals';\nimport type { ScorerRunInputForAgent, ScorerRunOutputForAgent, Trajectory } from '@mastra/core/evals';\nimport type { CoreMessage, CoreSystemMessage } from '@mastra/core/llm';\nimport type { MastraCompositeStore } from '@mastra/core/storage';\n\nexport type MastraCodeEvalContext = {\n /** Scorer-compatible agent input (messages, system prompt, etc.) */\n agentInput: ScorerRunInputForAgent;\n /** Scorer-compatible agent output (response messages) */\n agentOutput: ScorerRunOutputForAgent;\n /** Execution trajectory extracted from trace spans */\n trajectory: Trajectory | undefined;\n /** Request context with session metadata */\n requestContext: Record<string, unknown>;\n /** The thread ID this context was built from */\n threadId: string;\n /** The trace ID for observability linkage */\n traceId: string | undefined;\n};\n\nexport type BuildContextOptions = {\n /** AgentController instance to extract data from */\n controller: AgentController<any>;\n /** Session whose state/model/thread is being evaluated */\n session: Session<any>;\n /** Thread ID to build context for (defaults to current thread) */\n threadId?: string;\n /** Limit messages to the last N turns (user+assistant pairs). Undefined = all messages */\n lastNTurns?: number;\n};\n\n/**\n * Build an evaluation context from a AgentController session.\n *\n * This extracts messages from storage, builds trajectory from trace spans,\n * and packages everything into the format scorers expect.\n */\nexport async function buildEvalContext(options: BuildContextOptions): Promise<MastraCodeEvalContext> {\n const { controller, session, lastNTurns } = options;\n const threadId = options.threadId ?? session.thread.getId();\n\n if (!threadId) {\n throw new Error('No thread ID available. Start a session before building eval context.');\n }\n\n const mastra = controller.getMastra();\n const storage = mastra?.getStorage();\n\n // 1. Get raw MastraDB messages from memory storage\n const rawMessages = await getRawMessages(storage, threadId, lastNTurns);\n\n // 2. Split messages into input/output categories\n const { inputMessages, systemMessages, outputMessages } = categorizeMessages(rawMessages);\n\n // 3. Extract trajectory from observability traces\n const { trajectory, traceId } = await extractSessionTrajectory(storage, threadId);\n\n // 4. Build request context from AgentController state\n const requestContext = buildRequestContext(session, threadId);\n\n return {\n agentInput: {\n inputMessages,\n rememberedMessages: [], // AgentController doesn't separate these; memory recall is transparent\n systemMessages,\n taggedSystemMessages: {},\n },\n agentOutput: outputMessages,\n trajectory,\n requestContext,\n threadId,\n traceId,\n };\n}\n\n/**\n * Fetch raw MastraDBMessage[] from memory storage for a thread.\n */\nasync function getRawMessages(\n storage: MastraCompositeStore | undefined,\n threadId: string,\n lastNTurns?: number,\n): Promise<MastraDBMessage[]> {\n if (!storage) return [];\n\n const memoryStore = await storage.getStore('memory');\n if (!memoryStore) return [];\n\n const result = await memoryStore.listMessages({\n threadId,\n perPage: false,\n });\n\n let messages = result.messages;\n\n if (lastNTurns !== undefined && lastNTurns > 0) {\n messages = trimToLastNTurns(messages, lastNTurns);\n }\n\n return messages;\n}\n\n/**\n * Trim messages to the last N user-assistant turn pairs.\n */\nfunction trimToLastNTurns(messages: MastraDBMessage[], turns: number): MastraDBMessage[] {\n // Walk backward, counting user messages as turn boundaries.\n // When we find the Nth user message, slice from there to include\n // that turn and everything after it.\n let turnCount = 0;\n let cutoffIndex = 0;\n\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i]!.role === 'user') {\n turnCount++;\n if (turnCount === turns) {\n cutoffIndex = i;\n break;\n }\n }\n }\n\n return messages.slice(cutoffIndex);\n}\n\n/**\n * Categorize messages into input (user), system, and output (assistant) groups.\n */\nfunction categorizeMessages(messages: MastraDBMessage[]): {\n inputMessages: MastraDBMessage[];\n systemMessages: CoreMessage[];\n outputMessages: MastraDBMessage[];\n} {\n const inputMessages: MastraDBMessage[] = [];\n const systemMessages: CoreSystemMessage[] = [];\n const outputMessages: MastraDBMessage[] = [];\n\n for (const msg of messages) {\n switch (msg.role) {\n case 'user':\n inputMessages.push(msg);\n break;\n case 'system':\n systemMessages.push({\n role: 'system',\n content: extractTextContent(msg),\n });\n break;\n case 'assistant':\n outputMessages.push(msg);\n break;\n }\n }\n\n return { inputMessages, systemMessages, outputMessages };\n}\n\n/**\n * Extract plain text content from a MastraDBMessage.\n */\nfunction extractTextContent(msg: MastraDBMessage): string {\n if (!msg.content?.parts) return '';\n\n return msg.content.parts\n .filter((p): p is { type: 'text'; text: string } => p.type === 'text' && 'text' in p)\n .map(p => p.text)\n .join('\\n');\n}\n\n/**\n * Extract trajectory from trace spans in observability storage.\n * Looks up traces by threadId metadata.\n */\nasync function extractSessionTrajectory(\n storage: MastraCompositeStore | undefined,\n threadId: string,\n): Promise<{ trajectory: Trajectory | undefined; traceId: string | undefined }> {\n if (!storage) return { trajectory: undefined, traceId: undefined };\n\n try {\n const observabilityStore = await storage.getStore('observability');\n if (!observabilityStore) return { trajectory: undefined, traceId: undefined };\n\n // Find traces associated with this thread\n const result = await observabilityStore.listTraces({\n filters: { threadId },\n pagination: { page: 0, perPage: 1 },\n orderBy: { field: 'startedAt', direction: 'DESC' },\n });\n\n if (!result?.spans?.length) return { trajectory: undefined, traceId: undefined };\n\n const traceId = result.spans[0]!.traceId;\n const trace = await observabilityStore.getTrace({ traceId });\n\n if (!trace?.spans?.length) return { trajectory: undefined, traceId };\n\n const trajectory = extractTrajectoryFromTrace(trace.spans);\n return { trajectory, traceId };\n } catch {\n // Trace extraction is best-effort\n return { trajectory: undefined, traceId: undefined };\n }\n}\n\n/**\n * Build request context from AgentController state for scorer evaluation.\n */\nfunction buildRequestContext(session: Session<any>, threadId: string): Record<string, unknown> {\n const state = session.state.get() as Record<string, unknown>;\n\n return {\n threadId,\n mode: state.currentMode ?? state.mode,\n modelId: session.model.get(),\n projectPath: state.projectPath,\n projectName: state.projectName,\n gitBranch: state.gitBranch,\n thinkingLevel: state.thinkingLevel,\n yolo: state.yolo,\n smartEditing: state.smartEditing,\n };\n}\n"],"mappings":"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"}
@@ -284,7 +284,8 @@ function createOutcomeScorer() {
284
284
  type: "agent",
285
285
  prepareRun: filterRun({ partTypes: ["tool-invocation", "text"] })
286
286
  }).preprocess(async ({ run }) => {
287
- const allResults = extractToolCalls(run.output ?? []);
287
+ const messages = run.output ?? [];
288
+ const allResults = extractToolCalls(messages);
288
289
  const totalCalls = allResults.length;
289
290
  if (totalCalls < THRESHOLDS.minToolCalls) {
290
291
  const empty = {