github-router 0.3.273 → 0.3.274
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.
- package/dist/{attribution-settings-4nEQq_tn.js → attribution-settings-Cqh_A9yx.js} +3 -3
- package/dist/{attribution-settings-4nEQq_tn.js.map → attribution-settings-Cqh_A9yx.js.map} +1 -1
- package/dist/{auth-3so8RT91.js → auth-Q1tfmfCT.js} +2 -2
- package/dist/{auth-3so8RT91.js.map → auth-Q1tfmfCT.js.map} +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{check-usage-goXyc3qS.js → check-usage-CHLz4tU_.js} +3 -3
- package/dist/{check-usage-goXyc3qS.js.map → check-usage-CHLz4tU_.js.map} +1 -1
- package/dist/{claude-BNZD-bQN.js → claude-CqFkkQGo.js} +7 -7
- package/dist/{claude-BNZD-bQN.js.map → claude-CqFkkQGo.js.map} +1 -1
- package/dist/{codex-C_MA9uy5.js → codex-gGkDJHZJ.js} +5 -5
- package/dist/{codex-C_MA9uy5.js.map → codex-gGkDJHZJ.js.map} +1 -1
- package/dist/engine-BM2mIXsc.js +2 -0
- package/dist/{gate-discovery-DvA4xObh.js → gate-discovery-e114xla0.js} +2 -2
- package/dist/{gate-discovery-DvA4xObh.js.map → gate-discovery-e114xla0.js.map} +1 -1
- package/dist/{get-copilot-usage-nZXbvSi8.js → get-copilot-usage-BRN18uMJ.js} +2 -2
- package/dist/{get-copilot-usage-nZXbvSi8.js.map → get-copilot-usage-BRN18uMJ.js.map} +1 -1
- package/dist/{internal-stop-hook-WVgnwt5U.js → internal-stop-hook-BLPxEImO.js} +2 -2
- package/dist/{internal-stop-hook-WVgnwt5U.js.map → internal-stop-hook-BLPxEImO.js.map} +1 -1
- package/dist/main.js +8 -8
- package/dist/{models-BmGGH6pP.js → models-d0gkuYGy.js} +2 -2
- package/dist/{models-BmGGH6pP.js.map → models-d0gkuYGy.js.map} +1 -1
- package/dist/{peer-mcp-personas-DAZU1OfN.js → peer-mcp-personas-pzk1zcDa.js} +2 -2
- package/dist/{peer-mcp-personas-DAZU1OfN.js.map → peer-mcp-personas-pzk1zcDa.js.map} +1 -1
- package/dist/{provision-C3UIA1Tf.js → provision-BIUvGrMS.js} +2 -2
- package/dist/{provision-C3UIA1Tf.js.map → provision-BIUvGrMS.js.map} +1 -1
- package/dist/{serve-DPgawWWq.js → serve-BYbEyMoF.js} +6 -6
- package/dist/{serve-DPgawWWq.js.map → serve-BYbEyMoF.js.map} +1 -1
- package/dist/{server-setup-I5KYkuxE.js → server-setup-CNKsKERG.js} +6 -5
- package/dist/{server-setup-I5KYkuxE.js.map → server-setup-CNKsKERG.js.map} +1 -1
- package/dist/{start-CWWqjjwm.js → start-Cy_fEtEw.js} +3 -3
- package/dist/{start-CWWqjjwm.js.map → start-Cy_fEtEw.js.map} +1 -1
- package/dist/{token-Bz7iN8US.js → token-Bz6HoH2H.js} +365 -46
- package/dist/token-Bz6HoH2H.js.map +1 -0
- package/package.json +1 -1
- package/dist/engine-CHN8j6BV.js +0 -2
- package/dist/token-Bz7iN8US.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as browserToolsEnabled, Ct as provisionAndIndexColbert, Et as warmTreeSitterPool, St as colbertDegradedWarning, bt as provisionBrowserAssets } from "./peer-mcp-personas-
|
|
2
|
-
import { a as sharedServerArgs, i as setupAndServe, n as getCodexEnvVars, o as startKeepAwake, r as parseSharedArgs, t as getClaudeCodeEnvVars, u as runSelfUpdate } from "./server-setup-
|
|
1
|
+
import { $ as browserToolsEnabled, Ct as provisionAndIndexColbert, Et as warmTreeSitterPool, St as colbertDegradedWarning, bt as provisionBrowserAssets } from "./peer-mcp-personas-pzk1zcDa.js";
|
|
2
|
+
import { a as sharedServerArgs, i as setupAndServe, n as getCodexEnvVars, o as startKeepAwake, r as parseSharedArgs, t as getClaudeCodeEnvVars, u as runSelfUpdate } from "./server-setup-CNKsKERG.js";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import consola from "consola";
|
|
5
5
|
import process$1 from "node:process";
|
|
@@ -129,4 +129,4 @@ const start = defineCommand({
|
|
|
129
129
|
//#endregion
|
|
130
130
|
export { start };
|
|
131
131
|
|
|
132
|
-
//# sourceMappingURL=start-
|
|
132
|
+
//# sourceMappingURL=start-Cy_fEtEw.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-CWWqjjwm.js","names":["process"],"sources":["../src/lib/shell.ts","../src/start.ts"],"sourcesContent":["import process from \"node:process\"\n\ntype ShellName = \"bash\" | \"zsh\" | \"fish\" | \"powershell\" | \"cmd\" | \"sh\"\ntype EnvVars = Record<string, string | undefined>\n\nfunction getShell(): ShellName {\n const { platform, env } = process\n\n if (platform === \"win32\") {\n // Git Bash / MSYS2 / Cygwin set SHELL even on Windows\n if (env.SHELL) {\n if (env.SHELL.endsWith(\"zsh\")) return \"zsh\"\n if (env.SHELL.endsWith(\"fish\")) return \"fish\"\n if (env.SHELL.endsWith(\"bash\")) return \"bash\"\n return \"sh\"\n }\n\n // Windows PowerShell 5.x sets this\n if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return \"powershell\"\n\n // PowerShell (both 5.x and 7+/pwsh) adds user-scoped module paths\n // at runtime. The system-level PSModulePath in CMD lacks these paths.\n if (env.PSModulePath) {\n const lower = env.PSModulePath.toLowerCase()\n if (\n lower.includes(\"documents\\\\powershell\")\n || lower.includes(\"documents\\\\windowspowershell\")\n ) {\n return \"powershell\"\n }\n }\n\n return \"cmd\"\n }\n\n const shellPath = env.SHELL\n if (shellPath) {\n if (shellPath.endsWith(\"zsh\")) return \"zsh\"\n if (shellPath.endsWith(\"fish\")) return \"fish\"\n if (shellPath.endsWith(\"bash\")) return \"bash\"\n }\n\n return \"sh\"\n}\n\nfunction quotePosixValue(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`\n}\n\nfunction quotePowerShellValue(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/**\n * Generates a copy-pasteable script to set multiple environment variables\n * and run a subsequent command.\n * @param {EnvVars} envVars - An object of environment variables to set.\n * @param {string} commandToRun - The command to run after setting the variables.\n * @returns {string} The formatted script string.\n */\nexport function generateEnvScript(\n envVars: EnvVars,\n commandToRun: string = \"\",\n): string {\n const shell = getShell()\n const filteredEnvVars = Object.entries(envVars).filter(\n ([, value]) => value !== undefined,\n ) as Array<[string, string]>\n\n let commandBlock: string\n\n switch (shell) {\n case \"powershell\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`)\n .join(\"; \")\n break\n }\n case \"cmd\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set \"${key}=${value}\"`)\n .join(\" & \")\n break\n }\n case \"fish\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`)\n .join(\"; \")\n break\n }\n default: {\n // bash, zsh, sh\n const assignments = filteredEnvVars\n .map(([key, value]) => `${key}=${quotePosixValue(value)}`)\n .join(\" \")\n commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : \"\"\n break\n }\n }\n\n if (commandBlock && commandToRun) {\n const separator =\n shell === \"cmd\" ? \" & \" : shell === \"powershell\" ? \"; \" : \" && \"\n return `${commandBlock}${separator}${commandToRun}`\n }\n\n return commandBlock || commandToRun\n}\n","#!/usr/bin/env node\n\nimport { defineCommand } from \"citty\"\nimport clipboard from \"clipboardy\"\nimport consola from \"consola\"\n\nimport { generateEnvScript } from \"./lib/shell\"\nimport { DEFAULT_CODEX_MODEL, DEFAULT_PORT } from \"./lib/port\"\nimport { provisionBrowserAssets } from \"./lib/browser-mcp/provision\"\nimport { browserToolsEnabled } from \"./lib/mcp-capabilities\"\nimport { colbertDegradedWarning, provisionAndIndexColbert } from \"./lib/colbert\"\nimport { startKeepAwake } from \"./lib/keep-awake\"\nimport { warmTreeSitterPool } from \"./lib/tree-sitter-pool/pool\"\nimport {\n getClaudeCodeEnvVars,\n getCodexEnvVars,\n parseSharedArgs,\n setupAndServe,\n sharedServerArgs,\n} from \"./lib/server-setup\"\nimport { runSelfUpdate } from \"./lib/self-update\"\n\nfunction printAndCopyCommand(command: string, label: string): void {\n consola.box(`${label}\\n\\n${command}`)\n try {\n clipboard.writeSync(command)\n consola.success(`Copied ${label} command to clipboard!`)\n } catch {\n consola.warn(\"Failed to copy to clipboard. Copy the command above manually.\")\n }\n}\n\nfunction generateClaudeCodeCommand(serverUrl: string, model?: string) {\n const envVars = getClaudeCodeEnvVars(serverUrl, model)\n const command = generateEnvScript(envVars, \"claude --dangerously-skip-permissions\")\n printAndCopyCommand(command, \"Claude Code\")\n}\n\nfunction generateCodexCommand(serverUrl: string, model?: string) {\n const codexModel = model ?? DEFAULT_CODEX_MODEL\n const envVars = getCodexEnvVars(serverUrl)\n const command = generateEnvScript(envVars, `codex -m ${codexModel}`)\n printAndCopyCommand(command, \"Codex CLI\")\n}\n\nexport const start = defineCommand({\n meta: {\n name: \"start\",\n description: \"Start the github-router server\",\n },\n args: {\n ...sharedServerArgs,\n cc: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Claude Code with Copilot API config\",\n },\n cx: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Codex CLI with Copilot API config\",\n },\n model: {\n alias: \"m\",\n type: \"string\",\n description: \"Override the default model (used with --cc or --cx)\",\n },\n },\n async run({ args }) {\n const parsed = parseSharedArgs(args as unknown as Record<string, unknown>)\n\n const { serverUrl } = await setupAndServe({\n ...parsed,\n port: parsed.port ?? DEFAULT_PORT,\n silent: false,\n })\n\n // Best-effort self-update (detached, applies next launch). Runs\n // after the server is listening so the bounded probe can't delay it.\n void runSelfUpdate({ selfUpdate: args[\"self-update\"] !== false })\n\n // Best-effort ColBERT semantic-search provision + background index of\n // the launch cwd (if a git repo). ON by default; never blocks launch,\n // never throws. Opt out with GH_ROUTER_DISABLE_SEMANTIC_SEARCH=1.\n void provisionAndIndexColbert()\n\n // Surface a terminally-failed semantic index to the HUMAN (see the note\n // in claude.ts). Fire-and-forget; lexical search still works.\n void colbertDegradedWarning()\n .then((warning) => {\n if (warning) process.stderr.write(`${warning}\\n`)\n })\n .catch(() => {})\n\n // Ready one tree-sitter worker in the background so the first code search\n // avoids WASM/grammar initialization. One worker keeps launch CPU bounded;\n // demand grows the pool lazily. Opt out with GH_ROUTER_DISABLE_TS_POOL_WARMUP=1.\n void warmTreeSitterPool()\n\n // Best-effort: keep the machine awake while the proxy serves (win32\n // default-on; opt out with GH_ROUTER_DISABLE_KEEP_AWAKE=1). Holds a\n // SetThreadExecutionState assertion via a persistent PowerShell\n // helper. Self-registers its own SIGINT/SIGTERM/exit reaper — load-\n // bearing here, because `start` has no launchChild/onShutdown.\n startKeepAwake()\n\n // Best-effort: materialize the browser extension + bridge into the\n // stable app-dir and stamp the running version, so a one-time \"Load\n // unpacked\" survives npx/bunx upgrades. Gated on --browse; never\n // blocks launch, never throws.\n if (browserToolsEnabled()) {\n void provisionBrowserAssets().catch((err) =>\n consola.debug(\"Browser extension provisioning failed:\", err),\n )\n }\n\n if (args.cc) generateClaudeCodeCommand(serverUrl, args.model)\n if (args.cx) generateCodexCommand(serverUrl, args.model)\n\n consola.box(\n `🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`,\n )\n },\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"start-Cy_fEtEw.js","names":["process"],"sources":["../src/lib/shell.ts","../src/start.ts"],"sourcesContent":["import process from \"node:process\"\n\ntype ShellName = \"bash\" | \"zsh\" | \"fish\" | \"powershell\" | \"cmd\" | \"sh\"\ntype EnvVars = Record<string, string | undefined>\n\nfunction getShell(): ShellName {\n const { platform, env } = process\n\n if (platform === \"win32\") {\n // Git Bash / MSYS2 / Cygwin set SHELL even on Windows\n if (env.SHELL) {\n if (env.SHELL.endsWith(\"zsh\")) return \"zsh\"\n if (env.SHELL.endsWith(\"fish\")) return \"fish\"\n if (env.SHELL.endsWith(\"bash\")) return \"bash\"\n return \"sh\"\n }\n\n // Windows PowerShell 5.x sets this\n if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return \"powershell\"\n\n // PowerShell (both 5.x and 7+/pwsh) adds user-scoped module paths\n // at runtime. The system-level PSModulePath in CMD lacks these paths.\n if (env.PSModulePath) {\n const lower = env.PSModulePath.toLowerCase()\n if (\n lower.includes(\"documents\\\\powershell\")\n || lower.includes(\"documents\\\\windowspowershell\")\n ) {\n return \"powershell\"\n }\n }\n\n return \"cmd\"\n }\n\n const shellPath = env.SHELL\n if (shellPath) {\n if (shellPath.endsWith(\"zsh\")) return \"zsh\"\n if (shellPath.endsWith(\"fish\")) return \"fish\"\n if (shellPath.endsWith(\"bash\")) return \"bash\"\n }\n\n return \"sh\"\n}\n\nfunction quotePosixValue(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`\n}\n\nfunction quotePowerShellValue(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/**\n * Generates a copy-pasteable script to set multiple environment variables\n * and run a subsequent command.\n * @param {EnvVars} envVars - An object of environment variables to set.\n * @param {string} commandToRun - The command to run after setting the variables.\n * @returns {string} The formatted script string.\n */\nexport function generateEnvScript(\n envVars: EnvVars,\n commandToRun: string = \"\",\n): string {\n const shell = getShell()\n const filteredEnvVars = Object.entries(envVars).filter(\n ([, value]) => value !== undefined,\n ) as Array<[string, string]>\n\n let commandBlock: string\n\n switch (shell) {\n case \"powershell\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`)\n .join(\"; \")\n break\n }\n case \"cmd\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set \"${key}=${value}\"`)\n .join(\" & \")\n break\n }\n case \"fish\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`)\n .join(\"; \")\n break\n }\n default: {\n // bash, zsh, sh\n const assignments = filteredEnvVars\n .map(([key, value]) => `${key}=${quotePosixValue(value)}`)\n .join(\" \")\n commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : \"\"\n break\n }\n }\n\n if (commandBlock && commandToRun) {\n const separator =\n shell === \"cmd\" ? \" & \" : shell === \"powershell\" ? \"; \" : \" && \"\n return `${commandBlock}${separator}${commandToRun}`\n }\n\n return commandBlock || commandToRun\n}\n","#!/usr/bin/env node\n\nimport { defineCommand } from \"citty\"\nimport clipboard from \"clipboardy\"\nimport consola from \"consola\"\n\nimport { generateEnvScript } from \"./lib/shell\"\nimport { DEFAULT_CODEX_MODEL, DEFAULT_PORT } from \"./lib/port\"\nimport { provisionBrowserAssets } from \"./lib/browser-mcp/provision\"\nimport { browserToolsEnabled } from \"./lib/mcp-capabilities\"\nimport { colbertDegradedWarning, provisionAndIndexColbert } from \"./lib/colbert\"\nimport { startKeepAwake } from \"./lib/keep-awake\"\nimport { warmTreeSitterPool } from \"./lib/tree-sitter-pool/pool\"\nimport {\n getClaudeCodeEnvVars,\n getCodexEnvVars,\n parseSharedArgs,\n setupAndServe,\n sharedServerArgs,\n} from \"./lib/server-setup\"\nimport { runSelfUpdate } from \"./lib/self-update\"\n\nfunction printAndCopyCommand(command: string, label: string): void {\n consola.box(`${label}\\n\\n${command}`)\n try {\n clipboard.writeSync(command)\n consola.success(`Copied ${label} command to clipboard!`)\n } catch {\n consola.warn(\"Failed to copy to clipboard. Copy the command above manually.\")\n }\n}\n\nfunction generateClaudeCodeCommand(serverUrl: string, model?: string) {\n const envVars = getClaudeCodeEnvVars(serverUrl, model)\n const command = generateEnvScript(envVars, \"claude --dangerously-skip-permissions\")\n printAndCopyCommand(command, \"Claude Code\")\n}\n\nfunction generateCodexCommand(serverUrl: string, model?: string) {\n const codexModel = model ?? DEFAULT_CODEX_MODEL\n const envVars = getCodexEnvVars(serverUrl)\n const command = generateEnvScript(envVars, `codex -m ${codexModel}`)\n printAndCopyCommand(command, \"Codex CLI\")\n}\n\nexport const start = defineCommand({\n meta: {\n name: \"start\",\n description: \"Start the github-router server\",\n },\n args: {\n ...sharedServerArgs,\n cc: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Claude Code with Copilot API config\",\n },\n cx: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Codex CLI with Copilot API config\",\n },\n model: {\n alias: \"m\",\n type: \"string\",\n description: \"Override the default model (used with --cc or --cx)\",\n },\n },\n async run({ args }) {\n const parsed = parseSharedArgs(args as unknown as Record<string, unknown>)\n\n const { serverUrl } = await setupAndServe({\n ...parsed,\n port: parsed.port ?? DEFAULT_PORT,\n silent: false,\n })\n\n // Best-effort self-update (detached, applies next launch). Runs\n // after the server is listening so the bounded probe can't delay it.\n void runSelfUpdate({ selfUpdate: args[\"self-update\"] !== false })\n\n // Best-effort ColBERT semantic-search provision + background index of\n // the launch cwd (if a git repo). ON by default; never blocks launch,\n // never throws. Opt out with GH_ROUTER_DISABLE_SEMANTIC_SEARCH=1.\n void provisionAndIndexColbert()\n\n // Surface a terminally-failed semantic index to the HUMAN (see the note\n // in claude.ts). Fire-and-forget; lexical search still works.\n void colbertDegradedWarning()\n .then((warning) => {\n if (warning) process.stderr.write(`${warning}\\n`)\n })\n .catch(() => {})\n\n // Ready one tree-sitter worker in the background so the first code search\n // avoids WASM/grammar initialization. One worker keeps launch CPU bounded;\n // demand grows the pool lazily. Opt out with GH_ROUTER_DISABLE_TS_POOL_WARMUP=1.\n void warmTreeSitterPool()\n\n // Best-effort: keep the machine awake while the proxy serves (win32\n // default-on; opt out with GH_ROUTER_DISABLE_KEEP_AWAKE=1). Holds a\n // SetThreadExecutionState assertion via a persistent PowerShell\n // helper. Self-registers its own SIGINT/SIGTERM/exit reaper — load-\n // bearing here, because `start` has no launchChild/onShutdown.\n startKeepAwake()\n\n // Best-effort: materialize the browser extension + bridge into the\n // stable app-dir and stamp the running version, so a one-time \"Load\n // unpacked\" survives npx/bunx upgrades. Gated on --browse; never\n // blocks launch, never throws.\n if (browserToolsEnabled()) {\n void provisionBrowserAssets().catch((err) =>\n consola.debug(\"Browser extension provisioning failed:\", err),\n )\n }\n\n if (args.cc) generateClaudeCodeCommand(serverUrl, args.model)\n if (args.cx) generateCodexCommand(serverUrl, args.model)\n\n consola.box(\n `🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`,\n )\n },\n})\n"],"mappings":"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"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { t as PATHS } from "./paths-Bg1_DWhi.js";
|
|
2
2
|
import consola from "consola";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import process$1 from "node:process";
|
|
7
7
|
//#region src/lib/state.ts
|
|
8
8
|
const state = {
|
|
9
9
|
accountType: "enterprise",
|
|
10
|
+
copilotTokenGeneration: 0,
|
|
10
11
|
manualApprove: false,
|
|
11
12
|
rateLimitWait: false,
|
|
12
13
|
showToken: false,
|
|
@@ -631,9 +632,113 @@ function isAllowedCopilotHost(rawUrl) {
|
|
|
631
632
|
if (parsed.protocol !== "https:") return false;
|
|
632
633
|
return COPILOT_HOST_ALLOWLIST.includes(parsed.hostname);
|
|
633
634
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
635
|
+
/**
|
|
636
|
+
* Short, non-reversible identifier for a credential, safe to log.
|
|
637
|
+
*
|
|
638
|
+
* During the 2026-08-08 incident the logs could not answer the one question
|
|
639
|
+
* that mattered: was the process holding a stale credential, or was the
|
|
640
|
+
* credential on disk itself dead? The operator re-authenticated at 13:53 and
|
|
641
|
+
* the proxy kept failing, and nothing recorded whether those were the same
|
|
642
|
+
* secret. A truncated SHA-256 makes two credentials comparable across log
|
|
643
|
+
* lines without ever printing one. 8 hex chars is ample to distinguish the
|
|
644
|
+
* handful of credentials a machine sees, and far too short to attack.
|
|
645
|
+
*/
|
|
646
|
+
function credentialFingerprint(token) {
|
|
647
|
+
if (!token) return "none";
|
|
648
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 8);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* A failed `/copilot_internal/v2/token` exchange, carrying enough to diagnose
|
|
652
|
+
* it after the fact.
|
|
653
|
+
*
|
|
654
|
+
* The predecessor threw `HTTPError("Failed to get Copilot token", response)`
|
|
655
|
+
* and every catch logged only `.message`, so status and body were dropped —
|
|
656
|
+
* which is why the incident is unattributable.
|
|
657
|
+
*/
|
|
658
|
+
var CopilotTokenExchangeError = class extends HTTPError {
|
|
659
|
+
kind;
|
|
660
|
+
status;
|
|
661
|
+
/** Fingerprint of the credential that was rejected, never the credential. */
|
|
662
|
+
credential;
|
|
663
|
+
constructor(opts) {
|
|
664
|
+
super(`Failed to get Copilot token: HTTP ${opts.status} (${opts.kind}, credential ${opts.credential})${opts.detail ? ` — ${opts.detail}` : ""}`, opts.response);
|
|
665
|
+
this.kind = opts.kind;
|
|
666
|
+
this.status = opts.status;
|
|
667
|
+
this.credential = opts.credential;
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
/** Upper bound on how much of an error body we quote into a log line. */
|
|
671
|
+
const ERROR_BODY_SNIPPET_CHARS = 300;
|
|
672
|
+
/**
|
|
673
|
+
* `notification_id` values that mean "the credential is valid but the
|
|
674
|
+
* entitlement is not". These need a different remedy than a bad credential:
|
|
675
|
+
* telling someone whose subscription lapsed to re-authenticate sends them
|
|
676
|
+
* around a loop that cannot help.
|
|
677
|
+
*/
|
|
678
|
+
const ENTITLEMENT_NOTIFICATIONS = /* @__PURE__ */ new Set([
|
|
679
|
+
"access_revoked",
|
|
680
|
+
"subscription_ended",
|
|
681
|
+
"no_copilot_access"
|
|
682
|
+
]);
|
|
683
|
+
/**
|
|
684
|
+
* Classify a non-OK exchange response.
|
|
685
|
+
*
|
|
686
|
+
* Deliberately asymmetric: only a 401, or a 403 that NAMES an entitlement
|
|
687
|
+
* problem, is treated as terminal. A bare 403 stays transient because GitHub
|
|
688
|
+
* returns 403 for primary rate limits (`x-ratelimit-remaining: 0`) and
|
|
689
|
+
* secondary abuse limits — telling a user with a perfectly good credential to
|
|
690
|
+
* re-authenticate because they were briefly rate-limited would be a worse
|
|
691
|
+
* failure than the one being fixed. The costs are not symmetric: calling a
|
|
692
|
+
* terminal failure transient wastes some retries, while calling a transient
|
|
693
|
+
* failure terminal kills a working session.
|
|
694
|
+
*/
|
|
695
|
+
function classifyExchangeFailure(status, body) {
|
|
696
|
+
if (status === 401) return "credential_rejected";
|
|
697
|
+
if (status === 403) {
|
|
698
|
+
let notificationId;
|
|
699
|
+
try {
|
|
700
|
+
const parsed = JSON.parse(body);
|
|
701
|
+
notificationId = parsed.notification_id ?? parsed.error?.notification_id;
|
|
702
|
+
} catch {}
|
|
703
|
+
if (typeof notificationId === "string" && ENTITLEMENT_NOTIFICATIONS.has(notificationId)) return "entitlement_lapsed";
|
|
704
|
+
}
|
|
705
|
+
return "transient";
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Exchange the GitHub credential for a short-lived Copilot token.
|
|
709
|
+
*
|
|
710
|
+
* `credentialOverride` lets a caller try a candidate credential WITHOUT
|
|
711
|
+
* publishing it to the process-global `state` first. That matters because
|
|
712
|
+
* `state` is shared by every concurrent request: installing an unvalidated
|
|
713
|
+
* credential globally, even briefly, would make unrelated in-flight requests
|
|
714
|
+
* send it and 401 while a perfectly good credential was still in hand.
|
|
715
|
+
*/
|
|
716
|
+
const getCopilotToken = async (credentialOverride) => {
|
|
717
|
+
const credential = credentialFingerprint(credentialOverride ?? state.githubToken);
|
|
718
|
+
const authState = credentialOverride === void 0 ? state : {
|
|
719
|
+
...state,
|
|
720
|
+
githubToken: credentialOverride
|
|
721
|
+
};
|
|
722
|
+
const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(authState) }), { label: "/copilot_internal/v2/token" });
|
|
723
|
+
if (!response.ok) {
|
|
724
|
+
let body;
|
|
725
|
+
try {
|
|
726
|
+
body = await response.text();
|
|
727
|
+
} catch {
|
|
728
|
+
body = "(could not read error body)";
|
|
729
|
+
}
|
|
730
|
+
throw new CopilotTokenExchangeError({
|
|
731
|
+
kind: classifyExchangeFailure(response.status, body),
|
|
732
|
+
status: response.status,
|
|
733
|
+
credential,
|
|
734
|
+
detail: body.slice(0, ERROR_BODY_SNIPPET_CHARS).replaceAll(/\s+/g, " ").trim(),
|
|
735
|
+
response: new Response(body, {
|
|
736
|
+
status: response.status,
|
|
737
|
+
statusText: response.statusText,
|
|
738
|
+
headers: response.headers
|
|
739
|
+
})
|
|
740
|
+
});
|
|
741
|
+
}
|
|
637
742
|
const data = await response.json();
|
|
638
743
|
if (data.endpoints?.api) if (isAllowedCopilotHost(data.endpoints.api)) state.copilotApiUrl = data.endpoints.api;
|
|
639
744
|
else consola.warn(`Refusing to honor Copilot API endpoint "${data.endpoints.api}" from the token-exchange response — not in allowlist (${COPILOT_HOST_ALLOWLIST.join(", ")}). ` + (state.copilotApiUrl ? `Keeping existing override "${state.copilotApiUrl}".` : `Falling back to the default api.githubcopilot.com.`));
|
|
@@ -1164,6 +1269,43 @@ async function pollAccessToken(deviceCode, clientId = GITHUB_CLIENT_ID) {
|
|
|
1164
1269
|
//#region src/lib/token.ts
|
|
1165
1270
|
const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
|
|
1166
1271
|
/**
|
|
1272
|
+
* Transient filesystem errors from a read that raced the temp+rename write.
|
|
1273
|
+
*
|
|
1274
|
+
* On Windows a read landing in that window fails with one of these even
|
|
1275
|
+
* though a perfectly good file exists microseconds later. Treating one as
|
|
1276
|
+
* fatal at startup would cost a launch for no reason.
|
|
1277
|
+
*/
|
|
1278
|
+
const TRANSIENT_READ_CODES = /* @__PURE__ */ new Set([
|
|
1279
|
+
"EPERM",
|
|
1280
|
+
"EBUSY",
|
|
1281
|
+
"EACCES",
|
|
1282
|
+
"ENOENT"
|
|
1283
|
+
]);
|
|
1284
|
+
function isTransientReadError(err) {
|
|
1285
|
+
const code = err?.code;
|
|
1286
|
+
return typeof code === "string" && TRANSIENT_READ_CODES.has(code);
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Read the credential file, retrying briefly through a racing rename.
|
|
1290
|
+
*
|
|
1291
|
+
* Same delays and the same reasoning as {@link RENAME_RETRY_DELAYS_MS} on the
|
|
1292
|
+
* write side: the contending handle closes almost immediately, so a short
|
|
1293
|
+
* bounded retry converts a spurious launch failure into a non-event. A
|
|
1294
|
+
* persistent failure still propagates — the caller must not believe a
|
|
1295
|
+
* credential was read when it was not.
|
|
1296
|
+
*/
|
|
1297
|
+
async function readGithubTokenWithRetry() {
|
|
1298
|
+
let lastErr;
|
|
1299
|
+
for (let attempt = 0; attempt <= RENAME_RETRY_DELAYS_MS.length; attempt++) try {
|
|
1300
|
+
return await readGithubToken();
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
lastErr = err;
|
|
1303
|
+
if (!isTransientReadError(err)) throw err;
|
|
1304
|
+
if (attempt < RENAME_RETRY_DELAYS_MS.length) await new Promise((resolve) => setTimeout(resolve, RENAME_RETRY_DELAYS_MS[attempt]));
|
|
1305
|
+
}
|
|
1306
|
+
throw lastErr;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1167
1309
|
* Backoff for a transient Windows rename failure. On Windows `fs.rename` over
|
|
1168
1310
|
* an existing destination transiently fails with EPERM / EBUSY / EACCES when
|
|
1169
1311
|
* anything else holds the target open for a moment (antivirus, the search
|
|
@@ -1226,10 +1368,60 @@ const writeGithubToken = (token) => writeTokenFileAtomic(PATHS.GITHUB_TOKEN_PATH
|
|
|
1226
1368
|
const readGithubAgentToken = () => fs.readFile(PATHS.GITHUB_AGENT_TOKEN_PATH, "utf8");
|
|
1227
1369
|
const writeGithubAgentToken = (token) => writeTokenFileAtomic(PATHS.GITHUB_AGENT_TOKEN_PATH, token);
|
|
1228
1370
|
/**
|
|
1371
|
+
* Safety margin subtracted from the derived refresh deadline, so a request
|
|
1372
|
+
* arriving just before expiry refreshes rather than racing it.
|
|
1373
|
+
*/
|
|
1374
|
+
const REFRESH_SKEW_MS = 12e4;
|
|
1375
|
+
/**
|
|
1376
|
+
* Bounds on a `refresh_in` we are willing to turn into a timer delay. An
|
|
1377
|
+
* absurd or missing upstream value must not produce a zero-length timer (a
|
|
1378
|
+
* hot loop hammering GitHub) or an effectively infinite one (a token that is
|
|
1379
|
+
* never refreshed). 60s floor, 6h ceiling, 1500s default — the value GitHub
|
|
1380
|
+
* actually returns today.
|
|
1381
|
+
*/
|
|
1382
|
+
const MIN_REFRESH_IN_S = 60;
|
|
1383
|
+
const MAX_REFRESH_IN_S = 21600;
|
|
1384
|
+
const DEFAULT_REFRESH_IN_S = 1500;
|
|
1385
|
+
/** Backoff schedule after a failed refresh, then hold at the last value. */
|
|
1386
|
+
const REFRESH_BACKOFF_MS = [
|
|
1387
|
+
5e3,
|
|
1388
|
+
15e3,
|
|
1389
|
+
6e4,
|
|
1390
|
+
3e5
|
|
1391
|
+
];
|
|
1392
|
+
function clampRefreshIn(refreshIn) {
|
|
1393
|
+
if (typeof refreshIn !== "number" || !Number.isFinite(refreshIn)) return DEFAULT_REFRESH_IN_S;
|
|
1394
|
+
return Math.min(Math.max(refreshIn, MIN_REFRESH_IN_S), MAX_REFRESH_IN_S);
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Adopt an exchange result into `state`.
|
|
1398
|
+
*
|
|
1399
|
+
* The generation bump is what lets an in-flight request know a newer token
|
|
1400
|
+
* exists (see `State.copilotTokenGeneration`), so it happens on EVERY success
|
|
1401
|
+
* — including one that returns a byte-identical token, which upstream
|
|
1402
|
+
* demonstrably does.
|
|
1403
|
+
*/
|
|
1404
|
+
/**
|
|
1405
|
+
* Floor on how far ahead the derived refresh deadline may be placed.
|
|
1406
|
+
*
|
|
1407
|
+
* The deadline is `now + refresh_in - skew`, and the skew (120s) is larger
|
|
1408
|
+
* than the `refresh_in` floor (60s) — so a short upstream `refresh_in` would
|
|
1409
|
+
* put the deadline in the PAST, the scheduler would floor its delay to 1s,
|
|
1410
|
+
* and the proxy would hammer the token endpoint at 1 Hz forever. The deadline
|
|
1411
|
+
* must always be in the future, whatever upstream says.
|
|
1412
|
+
*/
|
|
1413
|
+
const MIN_REFRESH_LEAD_MS = 3e4;
|
|
1414
|
+
function commitCopilotToken(token, refreshIn) {
|
|
1415
|
+
state.copilotToken = token;
|
|
1416
|
+
state.copilotTokenGeneration += 1;
|
|
1417
|
+
const lead = Math.max(clampRefreshIn(refreshIn) * 1e3 - REFRESH_SKEW_MS, MIN_REFRESH_LEAD_MS);
|
|
1418
|
+
state.copilotTokenRefreshAt = Date.now() + lead;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1229
1421
|
* Fetch the Copilot token and keep it fresh in the background.
|
|
1230
1422
|
*
|
|
1231
1423
|
* Returns a disposer that stops the refresh loop. **Long-lived callers**
|
|
1232
|
-
* (`start`/`claude`/`codex`, via `setupAndServe`) can ignore it — the
|
|
1424
|
+
* (`start`/`claude`/`codex`, via `setupAndServe`) can ignore it — the timer
|
|
1233
1425
|
* is `unref()`d, so it never holds the event loop open on its own. The one-shot
|
|
1234
1426
|
* `models` command calls it, so ownership of the timer is explicit rather than
|
|
1235
1427
|
* implied by a runtime flag. (`check-usage` does not call this function at all;
|
|
@@ -1238,95 +1430,222 @@ const writeGithubAgentToken = (token) => writeTokenFileAtomic(PATHS.GITHUB_AGENT
|
|
|
1238
1430
|
* Both halves are load-bearing, for different failure modes. Without the
|
|
1239
1431
|
* `unref()`, `github-router models` printed its full correct output and then
|
|
1240
1432
|
* hung forever: its success path just returns (only the failure branches call
|
|
1241
|
-
* `process.exit`), and the un-unref'd
|
|
1433
|
+
* `process.exit`), and the un-unref'd timer pinned the event loop. Without
|
|
1242
1434
|
* the disposer, that fix would depend on a property no caller can see, so the
|
|
1243
1435
|
* next one-shot command would inherit the same trap.
|
|
1436
|
+
*
|
|
1437
|
+
* The schedule is a SELF-RE-ARMING `setTimeout`, not a fixed `setInterval`.
|
|
1438
|
+
* Three reasons: the delay tracks each response's own `refresh_in` instead of
|
|
1439
|
+
* the one captured at startup; a failure can back off (5s → 15s → 60s → 300s)
|
|
1440
|
+
* instead of waiting a full period during which the token is already dead;
|
|
1441
|
+
* and one timer cannot disagree with itself the way a period timer plus a
|
|
1442
|
+
* separate retry timer can.
|
|
1244
1443
|
*/
|
|
1245
1444
|
const setupCopilotToken = async () => {
|
|
1246
1445
|
const { token, refresh_in } = await getCopilotToken();
|
|
1247
|
-
|
|
1248
|
-
consola.debug(
|
|
1446
|
+
commitCopilotToken(token, refresh_in);
|
|
1447
|
+
consola.debug(`GitHub Copilot Token fetched successfully! (credential ${credentialFingerprint(state.githubToken)})`);
|
|
1249
1448
|
if (state.showToken) consola.info("Copilot token:", token);
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1449
|
+
let handle;
|
|
1450
|
+
let disposed = false;
|
|
1451
|
+
let failures = 0;
|
|
1452
|
+
const arm = (delayMs) => {
|
|
1453
|
+
if (disposed) return;
|
|
1454
|
+
handle = setTimeout(() => {
|
|
1455
|
+
tick();
|
|
1456
|
+
}, delayMs);
|
|
1457
|
+
handle.unref?.();
|
|
1458
|
+
};
|
|
1459
|
+
const tick = async () => {
|
|
1460
|
+
const outcome = await refreshCopilotToken("interval");
|
|
1461
|
+
if (disposed) return;
|
|
1462
|
+
if (outcome === "refreshed") {
|
|
1463
|
+
failures = 0;
|
|
1464
|
+
arm(nextDelayFromState());
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const delay = REFRESH_BACKOFF_MS[Math.min(failures, REFRESH_BACKOFF_MS.length - 1)];
|
|
1468
|
+
failures += 1;
|
|
1469
|
+
arm(delay);
|
|
1470
|
+
};
|
|
1471
|
+
arm(nextDelayFromState());
|
|
1256
1472
|
return () => {
|
|
1257
|
-
if (
|
|
1258
|
-
|
|
1259
|
-
|
|
1473
|
+
if (disposed) return;
|
|
1474
|
+
disposed = true;
|
|
1475
|
+
if (handle) clearTimeout(handle);
|
|
1260
1476
|
};
|
|
1261
1477
|
};
|
|
1478
|
+
/**
|
|
1479
|
+
* Delay until the next scheduled refresh, from the deadline already stored on
|
|
1480
|
+
* `state`. Floored at 1s so a pathological deadline can never busy-loop.
|
|
1481
|
+
*/
|
|
1482
|
+
function nextDelayFromState() {
|
|
1483
|
+
const deadline = state.copilotTokenRefreshAt;
|
|
1484
|
+
if (deadline === void 0) return DEFAULT_REFRESH_IN_S * 1e3;
|
|
1485
|
+
return Math.max(deadline - Date.now(), 1e3);
|
|
1486
|
+
}
|
|
1262
1487
|
let inflightRefresh;
|
|
1263
1488
|
let lastRefreshSuccess = 0;
|
|
1264
1489
|
let lastRefreshFailure = 0;
|
|
1265
1490
|
const REFRESH_SUCCESS_COOLDOWN_MS = 3e4;
|
|
1266
1491
|
const REFRESH_FAILURE_COOLDOWN_MS = 5e3;
|
|
1492
|
+
/**
|
|
1493
|
+
* Read the credential from disk, returning it only if it is usable.
|
|
1494
|
+
*
|
|
1495
|
+
* Every failure mode here resolves to `undefined` (meaning "keep using what
|
|
1496
|
+
* is in memory") rather than throwing or returning a partial value. The file
|
|
1497
|
+
* is replaced by temp+rename (`writeTokenFileAtomic`), and on Windows a read
|
|
1498
|
+
* racing that rename fails with EPERM/EBUSY/EACCES/ENOENT. None of those mean
|
|
1499
|
+
* the in-memory credential went bad, so none of them may clear it — and a
|
|
1500
|
+
* filesystem error must never escape into the refresh loop and kill the timer.
|
|
1501
|
+
*/
|
|
1502
|
+
async function readGithubTokenIfUsable() {
|
|
1503
|
+
if (state.githubTokenSource === "explicit") return void 0;
|
|
1504
|
+
try {
|
|
1505
|
+
const trimmed = (await readGithubToken()).trim();
|
|
1506
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
consola.debug("Could not re-read GitHub credential from disk:", err);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Perform one exchange, preferring a newer credential from disk if there is one.
|
|
1514
|
+
*
|
|
1515
|
+
* The disk token is a CANDIDATE: it is passed to the exchange directly and
|
|
1516
|
+
* published to `state.githubToken` only after that exchange succeeds. It is
|
|
1517
|
+
* never installed globally on spec — `state` is shared by every concurrent
|
|
1518
|
+
* request, so an unvalidated credential visible there would be sent by
|
|
1519
|
+
* unrelated in-flight requests and 401 them while a working credential was
|
|
1520
|
+
* still in hand.
|
|
1521
|
+
*/
|
|
1522
|
+
async function exchangeWithFreshestCredential() {
|
|
1523
|
+
const onDisk = await readGithubTokenIfUsable();
|
|
1524
|
+
const inMemory = state.githubToken;
|
|
1525
|
+
if (onDisk && onDisk !== inMemory) {
|
|
1526
|
+
consola.info(`GitHub credential on disk differs from the one in memory (${credentialFingerprint(inMemory)} → ${credentialFingerprint(onDisk)}); trying it.`);
|
|
1527
|
+
const result = await getCopilotToken(onDisk);
|
|
1528
|
+
state.githubToken = onDisk;
|
|
1529
|
+
state.githubTokenSource = "file";
|
|
1530
|
+
return result;
|
|
1531
|
+
}
|
|
1532
|
+
return getCopilotToken();
|
|
1533
|
+
}
|
|
1267
1534
|
async function refreshCopilotToken(reason) {
|
|
1268
1535
|
if (inflightRefresh) return inflightRefresh;
|
|
1269
|
-
|
|
1270
|
-
const now = Date.now();
|
|
1271
|
-
if (now - lastRefreshSuccess < REFRESH_SUCCESS_COOLDOWN_MS) {
|
|
1272
|
-
consola.debug(`refreshCopilotToken(${reason}) skipped: prior success within ${REFRESH_SUCCESS_COOLDOWN_MS}ms`);
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
if (now - lastRefreshFailure < REFRESH_FAILURE_COOLDOWN_MS) {
|
|
1276
|
-
consola.debug(`refreshCopilotToken(${reason}) skipped: prior failure within ${REFRESH_FAILURE_COOLDOWN_MS}ms`);
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
inflightRefresh = (async () => {
|
|
1281
|
-
consola.debug(`Refreshing Copilot token (reason=${reason})`);
|
|
1536
|
+
const run = async () => {
|
|
1282
1537
|
try {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1538
|
+
if (reason === "401-retry" || reason === "expiry") {
|
|
1539
|
+
const now = Date.now();
|
|
1540
|
+
const onDisk = await readGithubTokenIfUsable();
|
|
1541
|
+
if (!Boolean(onDisk && onDisk !== state.githubToken)) {
|
|
1542
|
+
if (now - lastRefreshSuccess < REFRESH_SUCCESS_COOLDOWN_MS) {
|
|
1543
|
+
consola.debug(`refreshCopilotToken(${reason}) skipped: prior success within ${REFRESH_SUCCESS_COOLDOWN_MS}ms`);
|
|
1544
|
+
return "skipped";
|
|
1545
|
+
}
|
|
1546
|
+
if (now - lastRefreshFailure < REFRESH_FAILURE_COOLDOWN_MS) {
|
|
1547
|
+
consola.debug(`refreshCopilotToken(${reason}) skipped: prior failure within ${REFRESH_FAILURE_COOLDOWN_MS}ms`);
|
|
1548
|
+
return "skipped";
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
consola.debug(`Refreshing Copilot token (reason=${reason})`);
|
|
1553
|
+
try {
|
|
1554
|
+
const { token, refresh_in } = await exchangeWithFreshestCredential();
|
|
1555
|
+
commitCopilotToken(token, refresh_in);
|
|
1556
|
+
lastRefreshSuccess = Date.now();
|
|
1557
|
+
consola.debug(`Copilot token refreshed (credential ${credentialFingerprint(state.githubToken)}, generation ${state.copilotTokenGeneration})`);
|
|
1558
|
+
if (state.showToken) consola.info("Refreshed Copilot token:", token);
|
|
1559
|
+
return "refreshed";
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
lastRefreshFailure = Date.now();
|
|
1562
|
+
consola.error(`Failed to refresh Copilot token (reason=${reason}):`, error instanceof Error ? error.message : error);
|
|
1563
|
+
if (error instanceof CopilotTokenExchangeError) {
|
|
1564
|
+
if (error.kind === "credential_rejected") return "credential_rejected";
|
|
1565
|
+
if (error.kind === "entitlement_lapsed") return "entitlement_lapsed";
|
|
1566
|
+
}
|
|
1567
|
+
return "transient_failure";
|
|
1568
|
+
}
|
|
1291
1569
|
} finally {
|
|
1292
|
-
inflightRefresh = void 0;
|
|
1570
|
+
if (inflightRefresh === attempt) inflightRefresh = void 0;
|
|
1293
1571
|
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1572
|
+
};
|
|
1573
|
+
const attempt = run();
|
|
1574
|
+
inflightRefresh = attempt;
|
|
1575
|
+
return attempt;
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Refresh the Copilot token if its derived deadline has passed.
|
|
1579
|
+
*
|
|
1580
|
+
* Called on EVERY request. The common path is one `Date.now()` and one
|
|
1581
|
+
* integer compare — no I/O — against an upstream call that costs hundreds of
|
|
1582
|
+
* milliseconds, so the overhead is not measurable. Sampling (every Nth
|
|
1583
|
+
* request) was considered and rejected: a session that idles and then resumes
|
|
1584
|
+
* may issue only a handful of requests, so a sampled guard would fail to fire
|
|
1585
|
+
* in precisely the scenario it exists for. Actual refreshes do not get more
|
|
1586
|
+
* frequent, because `refreshCopilotToken` is single-flighted and the deadline
|
|
1587
|
+
* moves forward on success.
|
|
1588
|
+
*/
|
|
1589
|
+
async function ensureFreshCopilotToken() {
|
|
1590
|
+
const deadline = state.copilotTokenRefreshAt;
|
|
1591
|
+
if (deadline === void 0 || Date.now() < deadline) return;
|
|
1592
|
+
await refreshCopilotToken("expiry");
|
|
1296
1593
|
}
|
|
1297
1594
|
/**
|
|
1298
1595
|
* Try `request()`. If it returns a 401, refresh the Copilot token (subject
|
|
1299
1596
|
* to the single-flight + refresh-storm-protection of `refreshCopilotToken`)
|
|
1300
|
-
* and retry once
|
|
1301
|
-
*
|
|
1597
|
+
* and retry once — but ONLY if the token actually moved on.
|
|
1598
|
+
*
|
|
1599
|
+
* The retry criterion is the generation counter, not the refresh's verdict
|
|
1600
|
+
* and not a token-string comparison. Both alternatives are wrong:
|
|
1601
|
+
*
|
|
1602
|
+
* - By verdict: requests A and B both hold token T0 and both 401. A
|
|
1603
|
+
* refreshes to T1. B's refresh call lands inside the 30s success cooldown
|
|
1604
|
+
* and reports "skipped", so a verdict-based rule would give up and fail B
|
|
1605
|
+
* with a 503 — even though T1 is sitting in state and would have worked.
|
|
1606
|
+
* - By string: upstream demonstrably returns a byte-identical token from
|
|
1607
|
+
* two consecutive successful exchanges, so an unchanged string does not
|
|
1608
|
+
* mean an unchanged credential state.
|
|
1609
|
+
*
|
|
1610
|
+
* Comparing generations answers the question the request actually has: is
|
|
1611
|
+
* what is in state now different from what I already tried? When it is not,
|
|
1612
|
+
* we skip a re-send that is guaranteed to 401 again.
|
|
1302
1613
|
*
|
|
1303
1614
|
* The `request` callback is responsible for capturing `state.copilotToken`
|
|
1304
1615
|
* locally before any await; this helper does NOT re-build the request
|
|
1305
1616
|
* itself, just re-invokes the callback after a refresh.
|
|
1306
1617
|
*/
|
|
1307
1618
|
async function tryRefreshAndRetry(request, routePath) {
|
|
1619
|
+
await ensureFreshCopilotToken();
|
|
1620
|
+
const generationBefore = state.copilotTokenGeneration;
|
|
1308
1621
|
const first = await request();
|
|
1309
1622
|
if (first.status !== 401) return first;
|
|
1310
1623
|
consola.warn(`${routePath}: upstream returned 401, attempting one token refresh + retry`);
|
|
1311
1624
|
await refreshCopilotToken("401-retry");
|
|
1625
|
+
if (state.copilotTokenGeneration === generationBefore) {
|
|
1626
|
+
consola.debug(`${routePath}: no newer Copilot token available (generation ${generationBefore}); not retrying`);
|
|
1627
|
+
return first;
|
|
1628
|
+
}
|
|
1312
1629
|
return request();
|
|
1313
1630
|
}
|
|
1314
1631
|
async function setupGitHubToken(options) {
|
|
1315
1632
|
try {
|
|
1316
|
-
const githubToken = await
|
|
1633
|
+
const githubToken = await readGithubTokenWithRetry();
|
|
1317
1634
|
if (githubToken && !options?.force) {
|
|
1318
1635
|
state.githubToken = githubToken;
|
|
1636
|
+
state.githubTokenSource = "file";
|
|
1319
1637
|
if (state.showToken) consola.info("GitHub token:", githubToken);
|
|
1320
1638
|
await logUser();
|
|
1321
1639
|
return;
|
|
1322
1640
|
}
|
|
1323
|
-
consola.info("Not logged in, getting new access token");
|
|
1641
|
+
consola.info(options?.force ? "Re-authenticating, getting new access token" : "Not logged in, getting new access token");
|
|
1324
1642
|
const response = await getDeviceCode();
|
|
1325
1643
|
consola.debug("Device code response:", response);
|
|
1326
1644
|
consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
|
|
1327
1645
|
const token = await pollAccessToken(response);
|
|
1328
1646
|
await writeGithubToken(token);
|
|
1329
1647
|
state.githubToken = token;
|
|
1648
|
+
state.githubTokenSource = "file";
|
|
1330
1649
|
if (state.showToken) consola.info("GitHub token:", token);
|
|
1331
1650
|
await logUser();
|
|
1332
1651
|
} catch (error) {
|
|
@@ -1410,4 +1729,4 @@ async function warnIfAgentScopesInsufficient() {
|
|
|
1410
1729
|
//#endregion
|
|
1411
1730
|
export { state as A, GITHUB_GRAPHQL_URL as C, githubAgentGraphQLHeaders as D, copilotVersion as E, githubAgentHeaders as O, GITHUB_API_BASE_URL as S, copilotHeaders as T, forwardError as _, cacheCopilotVersion as a, sanitizeTransportText as b, filterBetaHeader as c, resolveModel as d, sleep as f, HTTPError as g, isAllowedCopilotHost as h, tryRefreshAndRetry as i, githubHeaders as k, isNullish as l, getGitHubUser as m, setupGitHubAgentToken as n, cacheModels as o, getModels as p, setupGitHubToken as r, cacheVSCodeVersion as s, setupCopilotToken as t, resolveCodexModel as u, classifyTransportError as v, copilotBaseUrl as w, withTransientRetry as x, fetchWithTransientRetry as y };
|
|
1412
1731
|
|
|
1413
|
-
//# sourceMappingURL=token-
|
|
1732
|
+
//# sourceMappingURL=token-Bz6HoH2H.js.map
|