@cortexkit/opencode-openai-auth 0.7.1 → 0.8.0

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 (114) hide show
  1. package/README.md +9 -5
  2. package/dist/{index-9jpc8yct.js → index-02h7g5f9.js} +1 -1
  3. package/dist/index-2xppkvkx.js +2 -0
  4. package/dist/index-3w4vmckx.js +2 -0
  5. package/dist/{index-zxqb5ydj.js → index-507jh8bv.js} +1 -1
  6. package/dist/{index-zg1x8sty.js → index-944gsj1s.js} +1 -1
  7. package/dist/index-nshbgkz1.js +198 -0
  8. package/dist/{index-87145jkw.js → index-xyg86jgt.js} +1 -1
  9. package/dist/index.d.ts +381 -35
  10. package/dist/index.js +66 -161
  11. package/dist/protocol-Cv9ZmU0p.d.ts +24 -0
  12. package/dist/quota-normalize-wap0rne8.js +1 -0
  13. package/dist/rpc/port-file.d.ts +11 -9
  14. package/dist/rpc/port-file.js +1 -1
  15. package/dist/rpc/protocol.d.ts +2 -21
  16. package/dist/rpc/rpc-client.d.ts +6 -4
  17. package/dist/rpc/rpc-client.js +1 -1
  18. package/dist/rpc/rpc-dir.d.ts +7 -5
  19. package/dist/rpc/rpc-dir.js +1 -1
  20. package/dist/sidebar-state.d.ts +84 -83
  21. package/dist/sidebar-state.js +1 -1
  22. package/dist/{token-bvp0y166.js → token-jdvpwk60.js} +1 -1
  23. package/dist/token-util-zzm7paz9.js +1 -0
  24. package/dist/tui-preferences.d.ts +27 -26
  25. package/dist/tui-preferences.js +1 -1
  26. package/dist/tui.d.ts +18 -17
  27. package/package.json +10 -17
  28. package/src/tui/entry.mjs +2 -2
  29. package/src/tui-compiled/{core/account-paths.ts → raw/core-account-paths.ts} +29 -18
  30. package/src/tui-compiled/raw/logger.ts +53 -0
  31. package/src/tui-compiled/{rpc/port-file.ts → raw/rpc-port-file.ts} +1 -1
  32. package/src/tui-compiled/raw/rpc-protocol.ts +12 -0
  33. package/src/{rpc/rpc-client.ts → tui-compiled/raw/rpc-rpc-client.ts} +2 -2
  34. package/src/{sidebar-state.ts → tui-compiled/raw/sidebar-state.ts} +2 -2
  35. package/src/{tui/command-dialogs.tsx → tui-compiled/raw/tui-command-dialogs.tsx} +5 -5
  36. package/src/{tui.tsx → tui-compiled/raw/tui.tsx} +11 -10
  37. package/src/{core/account-paths.ts → tui-compiled/runtime/core-account-paths.ts} +29 -18
  38. package/src/tui-compiled/runtime/logger.ts +53 -0
  39. package/src/{rpc/port-file.ts → tui-compiled/runtime/rpc-port-file.ts} +1 -1
  40. package/src/tui-compiled/runtime/rpc-protocol.ts +12 -0
  41. package/src/tui-compiled/{rpc/rpc-client.ts → runtime/rpc-rpc-client.ts} +2 -2
  42. package/src/tui-compiled/{sidebar-state.ts → runtime/sidebar-state.ts} +2 -2
  43. package/src/tui-compiled/{tui/command-dialogs.tsx → runtime/tui-command-dialogs.tsx} +3 -4
  44. package/src/tui-compiled/{tui.tsx → runtime/tui.tsx} +10 -10
  45. package/src/tui-compiled/shared/internal.ts +6 -0
  46. package/src/{logger.ts → tui-compiled/shared/logger.ts} +82 -21
  47. package/src/tui-compiled/shared/paths.ts +33 -0
  48. package/src/{core → tui-compiled/shared}/refresh-file-lock.ts +7 -4
  49. package/dist/cli.d.ts +0 -2
  50. package/dist/cli.js +0 -15
  51. package/dist/codex-http.d.ts +0 -3
  52. package/dist/commands.d.ts +0 -88
  53. package/dist/config.d.ts +0 -47
  54. package/dist/core/account-paths.d.ts +0 -25
  55. package/dist/core/accounts.d.ts +0 -330
  56. package/dist/core/atomic-write.d.ts +0 -1
  57. package/dist/core/background-quota-refresh.d.ts +0 -46
  58. package/dist/core/backoff.d.ts +0 -53
  59. package/dist/core/cachekeep.d.ts +0 -118
  60. package/dist/core/oauth.d.ts +0 -138
  61. package/dist/core/provider.d.ts +0 -70
  62. package/dist/core/quota-manager.d.ts +0 -192
  63. package/dist/core/refresh-all-quota.d.ts +0 -86
  64. package/dist/core/refresh-file-lock.d.ts +0 -12
  65. package/dist/core/reset-credits.d.ts +0 -116
  66. package/dist/core/sticky-routing.d.ts +0 -43
  67. package/dist/dump.d.ts +0 -20
  68. package/dist/hosted-web-search.d.ts +0 -5
  69. package/dist/index-8f1200qx.js +0 -6
  70. package/dist/index-nrvjr97q.js +0 -3
  71. package/dist/index-qaggaze1.js +0 -2
  72. package/dist/index-sdcpzbnk.js +0 -2
  73. package/dist/index-zf3axndm.js +0 -89
  74. package/dist/logger.d.ts +0 -22
  75. package/dist/model-costs.d.ts +0 -29
  76. package/dist/prompt-context.d.ts +0 -19
  77. package/dist/quota-normalize-dvwtmy7f.js +0 -1
  78. package/dist/quota-normalize.d.ts +0 -43
  79. package/dist/raw-ws-bun.d.ts +0 -40
  80. package/dist/raw-ws-node.d.ts +0 -37
  81. package/dist/raw-ws-upgrade.d.ts +0 -2
  82. package/dist/raw-ws.d.ts +0 -3
  83. package/dist/response-stream-error.d.ts +0 -12
  84. package/dist/rpc/notifications.d.ts +0 -5
  85. package/dist/rpc/rpc-server.d.ts +0 -17
  86. package/dist/token-util-jcj7k7pa.js +0 -1
  87. package/dist/tui/command-dialogs.d.ts +0 -21
  88. package/dist/util/error.d.ts +0 -1
  89. package/dist/util/open-url.d.ts +0 -6
  90. package/dist/util/proxy-env.d.ts +0 -4
  91. package/dist/util/record.d.ts +0 -1
  92. package/dist/util/stable-json.d.ts +0 -1
  93. package/dist/util/uuid-v7.d.ts +0 -1
  94. package/dist/version.d.ts +0 -1
  95. package/dist/ws-pool.d.ts +0 -57
  96. package/dist/ws.d.ts +0 -53
  97. package/src/rpc/notifications.ts +0 -54
  98. package/src/rpc/protocol.ts +0 -33
  99. package/src/rpc/rpc-server.ts +0 -172
  100. package/src/tui-compiled/core/refresh-file-lock.ts +0 -381
  101. package/src/tui-compiled/logger.ts +0 -196
  102. package/src/tui-compiled/rpc/protocol.ts +0 -33
  103. package/src/util/error.ts +0 -9
  104. package/src/util/open-url.ts +0 -30
  105. package/src/util/proxy-env.ts +0 -42
  106. package/src/util/record.ts +0 -3
  107. package/src/util/stable-json.ts +0 -15
  108. package/src/util/uuid-v7.ts +0 -18
  109. /package/src/{rpc/rpc-dir.ts → tui-compiled/raw/rpc-rpc-dir.ts} +0 -0
  110. /package/src/tui-compiled/{tui-preferences.ts → raw/tui-preferences.ts} +0 -0
  111. /package/src/tui-compiled/{rpc/rpc-dir.ts → runtime/rpc-rpc-dir.ts} +0 -0
  112. /package/src/{tui-preferences.ts → tui-compiled/runtime/tui-preferences.ts} +0 -0
  113. /package/src/tui-compiled/{util → shared/util}/error.ts +0 -0
  114. /package/src/tui-compiled/{util → shared/util}/open-url.ts +0 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure path values shared by every host.
3
+ *
4
+ * Nothing here reads an environment variable or knows where a host keeps its
5
+ * configuration. A host resolves its own two file paths and hands them to the
6
+ * store as an `AccountPaths`; the only thing that must not drift between hosts
7
+ * is the file name itself, which is why the constants live here rather than in
8
+ * each host's own resolver.
9
+ */
10
+
11
+ import { basename, dirname, join } from 'node:path'
12
+
13
+ export const ACCOUNT_FILE_NAME = 'openai-auth.json'
14
+ export const ACCOUNT_STATE_FILE_NAME = 'openai-auth-state.json'
15
+
16
+ /**
17
+ * The config file and the runtime-state file a store operation reads and writes.
18
+ *
19
+ * Both are required. A store function never derives one from the other, because
20
+ * a host may point its state file somewhere the derivation would not reach and
21
+ * a silently derived path would lock one file while writing another.
22
+ */
23
+ export interface AccountPaths {
24
+ configPath: string
25
+ statePath: string
26
+ }
27
+
28
+ /** Derive the state-file path from the config path without reading env vars. */
29
+ export function deriveStatePath(configPath: string): string {
30
+ return basename(configPath) === ACCOUNT_FILE_NAME
31
+ ? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
32
+ : `${configPath}.state.json`
33
+ }
@@ -1,7 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
3
3
  import { dirname, join } from 'node:path'
4
- import { getAccountStoragePath } from './account-paths'
5
4
 
6
5
  const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis)
7
6
  const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis)
@@ -18,7 +17,12 @@ export function isLostMarkerRaceError(error: unknown): boolean {
18
17
  export async function acquireRefreshFileLock(options: {
19
18
  name: string
20
19
  ttlMs: number
21
- path?: string
20
+ /**
21
+ * File the lock is named after. Required: the lock and the write it guards
22
+ * must target the same file, and a default resolved in here could only ever
23
+ * be one host's path.
24
+ */
25
+ path: string
22
26
  now?: () => number
23
27
  renew?: boolean
24
28
  renewIntervalMs?: number
@@ -37,8 +41,7 @@ export async function acquireRefreshFileLock(options: {
37
41
  | 'release-owner-confirmed',
38
42
  ) => void | Promise<void>
39
43
  }): Promise<{ release: () => Promise<void> } | null> {
40
- const accountPath = options.path ?? getAccountStoragePath()
41
- const lockPath = `${accountPath}.${options.name}.lock`
44
+ const lockPath = `${options.path}.${options.name}.lock`
42
45
  const legacyOwnerPath = join(lockPath, 'owner.json')
43
46
  const ownerId = randomUUID()
44
47
  const now = options.now ?? Date.now
package/dist/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export { openUrl as openBrowserForLogin } from './util/open-url';
package/dist/cli.js DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env node
2
- import{J as C,K as E,L as X,r as $,y as q,z as B}from"./index-zf3axndm.js";import{ua as Y}from"./index-8f1200qx.js";import"./index-nrvjr97q.js";import{Ka as Z}from"./index-zxqb5ydj.js";import{execFileSync as U}from"node:child_process";function V(z,K=process.platform,H=U){try{if(K==="win32"){H("cmd",["/c","start","",z],{stdio:"ignore",timeout:3000});return}H(K==="darwin"?"open":"xdg-open",[z],{stdio:"ignore",timeout:3000})}catch{}}function W(){console.log(`Usage:
3
- npx @cortexkit/opencode-openai-auth login [--label <name>] [--headless]
4
- npx @cortexkit/opencode-openai-auth list
5
- npx @cortexkit/opencode-openai-auth remove <id>
6
-
7
- Fallback accounts are stored in:
8
- ${Y()}`)}function P(z){let K=[],H={};for(let N=0;N<z.length;N++){let j=z[N];if(!j)continue;if(j.startsWith("--")){let J=j.slice(2),D=z[N+1];if(D&&!D.startsWith("--"))H[J]=D,N++;else H[J]=!0}else K.push(j)}return{positional:K,flags:H}}async function R(){let{positional:z,flags:K}=P(process.argv.slice(2)),[H,...N]=z;if(!H||H==="help")W(),process.exit(0);switch(H){case"login":{let j=typeof K.label==="string"?K.label:void 0,J=Boolean(K.headless);try{$(j)}catch(G){console.error(`
9
- Error: ${G instanceof Error?G.message:String(G)}`),process.exit(1)}let{url:D,instructions:M,completion:T}=await B({label:j,headless:J});if(console.log(`
10
- Open this URL in your browser and complete sign-in:
11
- `),console.log(`${D}
12
- `),M)console.log(`${M}
13
- `);V(D);let O=await T,Q=!1;if(await X((G)=>{if(O.accountId&&G.mainAccountId&&O.accountId===G.mainAccountId)return Q=!0,G;return q(G.accounts,O),G}),Q)console.error(`
14
- Error: that account is already your main (same ChatGPT account).`),console.error("A self-fallback would retry on the account that just returned 429."),process.exit(1);if(console.log(`
15
- ✓ Added account ${O.id}`),O.label)console.log(` Label: ${O.label}`);break}case"list":{let j=await E();if(!j||j.accounts.length===0)console.log("No fallback accounts configured.");else for(let J of j.accounts){let D=J.label,M=[` ${J.id}`];if(D)M.push(`(${D})`);M.push(J.enabled!==!1?"[enabled]":"[disabled]"),console.log(M.join(" "))}break}case"remove":{let j=N[0];if(!j)console.error("Error: remove requires an account ID."),W(),process.exit(1);let J=Y(),D=await C(J),M=D?D.has(j):!1,T=!1;if(await X((Q)=>{let G=Q.accounts.findIndex((L)=>L.id===j);if(G===-1)return Q;return Q.accounts.splice(G,1),T=!0,Q},J,{allowDrop:[j]}),!(T||M))console.error(`No account with id "${j}".`),process.exit(1);console.log(`Removed account ${j}.`);break}default:console.error(`Unknown command: ${H}`),W(),process.exit(1)}}if(Z.main==Z.module)R().catch((z)=>{console.error(z instanceof Error?z.message:String(z)),process.exit(1)});export{V as openBrowserForLogin};
@@ -1,3 +0,0 @@
1
- export declare function hasWebSocketResponsesLiteMetadata(body: BodyInit | null | undefined): boolean;
2
- export declare function sanitizeHttpFallbackBody(body: BodyInit | null | undefined): string | ArrayBuffer | ArrayBufferView<ArrayBuffer> | Blob | FormData | ReadableStream<any> | URLSearchParams | null | undefined;
3
- export declare function sanitizeHttpFallbackInit(init: RequestInit | undefined): RequestInit | undefined;
@@ -1,88 +0,0 @@
1
- import { type loadAccounts as defaultLoadAccounts } from './core/accounts';
2
- import type { CacheKeepManager, CacheKeepWindow } from './core/cachekeep';
3
- import type { QuotaManager } from './core/quota-manager';
4
- import type { RefreshAllQuotaResult } from './core/refresh-all-quota';
5
- import { type RunResetCreditResult } from './core/reset-credits';
6
- import type { ApplyRequest, ApplyResult, CommandModalName, OpenDialogPayload } from './rpc/protocol';
7
- export declare const OPENAI_QUOTA_COMMAND_NAME = "openai-quota";
8
- export declare const OPENAI_ACCOUNT_COMMAND_NAME = "openai-account";
9
- export declare const OPENAI_ROUTING_COMMAND_NAME = "openai-routing";
10
- export declare const OPENAI_KILLSWITCH_COMMAND_NAME = "openai-killswitch";
11
- export declare const OPENAI_DUMP_COMMAND_NAME = "openai-dump";
12
- export declare const OPENAI_LOGGING_COMMAND_NAME = "openai-logging";
13
- export declare const OPENAI_CACHEKEEP_COMMAND_NAME = "openai-cachekeep";
14
- export declare const OPENAI_RESET_COMMAND_NAME = "openai-reset";
15
- export declare const MODAL_COMMANDS: CommandModalName[];
16
- export interface CommandContext {
17
- accountStoragePath: string;
18
- quotaManager: QuotaManager;
19
- loadAccounts: typeof defaultLoadAccounts;
20
- client: {
21
- auth: {
22
- set: (input: {
23
- path: {
24
- id: string;
25
- };
26
- body: {
27
- type: string;
28
- access?: string;
29
- refresh: string;
30
- expires?: number;
31
- };
32
- }) => Promise<unknown>;
33
- };
34
- };
35
- /** Session ID for pushNotification delivery. */
36
- sessionId?: string;
37
- /** If set, pushNotification is wired up and can deliver feedback to the user. */
38
- notify?: (payload: OpenDialogPayload) => void;
39
- /** Refresh the sidebar-state file so the TUI modal shows current data. */
40
- refreshSidebar?: () => Promise<void>;
41
- /** Actively poll wham/usage for all accounts (main + fallbacks). */
42
- refreshAllQuota?: () => Promise<RefreshAllQuotaResult[]>;
43
- /** Prompt-cache cachekeep manager. Set when the command is wired. */
44
- cacheKeepManager?: CacheKeepManager | null;
45
- /** Updates the live loader's persisted-enabled cachekeep gate. */
46
- setCacheKeepEnabled?: (enabled: boolean) => void;
47
- /** Updates the live loader's persisted-subagent cachekeep gate. */
48
- setCacheKeepSubagents?: (enabled: boolean) => void;
49
- /** Updates the live loader's main-agent idle-cap bypass gate. */
50
- setCacheKeepSustain?: (enabled: boolean) => void;
51
- /** Updates the live loader's clock-hour warm window. undefined = no window. */
52
- setCacheKeepWindow?: (window: CacheKeepWindow | undefined) => void;
53
- /** Clears only the sticky account assignment for one OpenCode session. */
54
- clearStickyRouting?: (sessionId: string) => Promise<boolean>;
55
- /** Resolves the current session's usable sticky account, if one exists. */
56
- getStickyRouting?: (sessionId: string) => Promise<string | undefined>;
57
- resolveResetTarget?: (accountKey: string) => Promise<ResetTargetIdentity>;
58
- fetchImpl?: typeof fetch;
59
- now?: () => number;
60
- randomUUID?: () => string;
61
- refreshResetTargetQuota?: (accountKey: string) => Promise<RefreshAllQuotaResult>;
62
- }
63
- export interface ResetTargetIdentity {
64
- accountKey: string;
65
- label: string;
66
- accessToken: string;
67
- chatgptAccountId?: string;
68
- }
69
- type ResetCommandContext = CommandContext & Required<Pick<CommandContext, 'resolveResetTarget' | 'fetchImpl' | 'now' | 'randomUUID' | 'refreshResetTargetQuota'>>;
70
- export declare function renderResetCoordinatorResult(result: RunResetCreditResult, ctx: ResetCommandContext, boundChatgptAccountId?: string): Promise<OpenDialogPayload>;
71
- /**
72
- * Strip credential-shaped fields from a dialog payload's knobs.
73
- *
74
- * Knobs are returned across the loopback RPC and JSON-serialized to the TUI, so
75
- * a knob is a published surface. Individual commands project their own knobs
76
- * deliberately (see accountKnob), but this is the boundary backstop: a future
77
- * command that returns a stored object directly cannot leak credentials even if
78
- * the projection is forgotten, because nothing credential-shaped survives here.
79
- *
80
- * Scrub rather than throw. A rejected dialog is a visible outage for a live
81
- * command, while a scrubbed one keeps working with the leak removed; the warning
82
- * is what gets the projection fixed. Recurses into nested objects and arrays,
83
- * since the account list arrives as an array of records.
84
- */
85
- export declare function scrubKnobs(value: unknown, path: string, found: string[]): unknown;
86
- export declare function buildDialogPayload(command: CommandModalName, args: string, ctx: CommandContext): Promise<OpenDialogPayload>;
87
- export declare function applyCommand(request: ApplyRequest, ctx: CommandContext): Promise<ApplyResult>;
88
- export {};
package/dist/config.d.ts DELETED
@@ -1,47 +0,0 @@
1
- export declare const DEFAULT_CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
2
- /** Shape of ~/.config/opencode/openai-auth.json (all fields optional). */
3
- export interface OpenAIAuthConfig {
4
- /** Inject the native web_search tool to keep the Codex prompt cache stable. Default true. */
5
- webSearch?: boolean;
6
- /** Use the WebSocket transport for /responses instead of plain HTTP. Default false. */
7
- webSockets?: boolean;
8
- /** Use the hand-rolled raw TCP/TLS WebSocket client (incremental streaming). Default false. */
9
- rawWebSocket?: boolean;
10
- /** Use Codex's Responses Lite request shape. Default false. */
11
- responsesLite?: boolean;
12
- /** Dump final Codex request bodies for cache debugging. Default false. */
13
- dump?: boolean | {
14
- enabled?: boolean;
15
- };
16
- /** Directory for request dumps. Defaults to the OS temp directory. */
17
- dumpDir?: string;
18
- /** Codex-compatible Responses endpoint. Defaults to ChatGPT's Codex backend. */
19
- codexApiEndpoint?: string;
20
- }
21
- export interface ResolvedSettings {
22
- webSearch: boolean;
23
- webSockets: boolean;
24
- rawWebSocket: boolean;
25
- responsesLite: boolean;
26
- dump: boolean;
27
- dumpDir: string;
28
- codexApiEndpoint: string;
29
- }
30
- export declare function getConfigDir(): string;
31
- export declare function getConfigPath(): string;
32
- /**
33
- * Resolved settings, memoized per process.
34
- *
35
- * Env and the config file are read once because neither changes under a running
36
- * process — except through a command that writes the config itself. Those
37
- * commands must call `refreshSettings()`, or the process keeps serving the
38
- * values it read at startup while the file on disk says otherwise.
39
- */
40
- export declare function getSettings(): ResolvedSettings;
41
- /**
42
- * Drop the memoized settings after a command writes the config file, so the
43
- * running process picks the change up without a restart.
44
- */
45
- export declare function refreshSettings(): ResolvedSettings;
46
- /** Test-only: drop the memoized settings so a later getSettings() re-reads env + config. */
47
- export declare function resetSettingsForTest(): void;
@@ -1,25 +0,0 @@
1
- /**
2
- * Where the account store lives.
3
- *
4
- * This is the ONLY definition. It used to be duplicated verbatim in both
5
- * `accounts.ts` and `refresh-file-lock.ts`, because `accounts.ts` imports the
6
- * lock and the reverse import would have been a cycle. Two copies of a path
7
- * resolver is a bad trade for that: the lock and the write must agree on the
8
- * exact file, and if the copies ever drifted, a writer would take a lock on one
9
- * path while writing another — mutual exclusion silently lost on a file holding
10
- * credentials. Nothing would fail a test, since each copy works on its own.
11
- *
12
- * This module imports nothing from either side, so both can depend on it.
13
- */
14
- export declare const ACCOUNT_FILE_NAME = "openai-auth.json";
15
- export declare const ACCOUNT_STATE_FILE_NAME = "openai-auth-state.json";
16
- export declare function getAccountStoragePath(): string;
17
- /** Derive the state-file path from the config path without reading env vars. */
18
- export declare function deriveStatePath(configPath: string): string;
19
- /**
20
- * Detect path aliases without requiring either file to exist. This is defense
21
- * in depth: hardlinks and bind mounts can still make distinct paths share a
22
- * file, because neither is distinguishable through pathname identity.
23
- */
24
- export declare function accountPathsCollide(configPath: string, statePath: string, platform?: NodeJS.Platform): boolean;
25
- export declare function getAccountStatePath(configPath?: string): string;
@@ -1,330 +0,0 @@
1
- import { ACCOUNT_FILE_NAME, ACCOUNT_STATE_FILE_NAME, deriveStatePath, getAccountStatePath, getAccountStoragePath } from './account-paths';
2
- import type { ProviderQuotaFn, ProviderRefreshFn, QuotaWindowName } from './provider.ts';
3
- import { PRIMARY, SECONDARY } from './provider.ts';
4
- export { ACCOUNT_FILE_NAME, ACCOUNT_STATE_FILE_NAME, deriveStatePath, getAccountStatePath, getAccountStoragePath, };
5
- export type { QuotaWindowName };
6
- export { PRIMARY, SECONDARY };
7
- export type AccountQuotaWindow = {
8
- usedPercent: number;
9
- remainingPercent: number;
10
- resetsAt?: string;
11
- checkedAt: number;
12
- windowMinutes?: number;
13
- };
14
- export interface OAuthQuotaSnapshot {
15
- primary?: AccountQuotaWindow;
16
- secondary?: AccountQuotaWindow;
17
- resetCreditsAvailable?: number;
18
- resetCreditsApplicable?: number;
19
- }
20
- export type AccountBase = {
21
- id: string;
22
- label?: string;
23
- enabled?: boolean;
24
- addedAt?: number;
25
- lastUsed?: number;
26
- /** Stable ChatGPT account identifier extracted from the OAuth token claims. */
27
- accountId?: string;
28
- };
29
- export type AccountOperationError = {
30
- message: string;
31
- checkedAt: number;
32
- nextRetryAt?: number;
33
- retryCount?: number;
34
- tokenHash?: string;
35
- };
36
- export type OAuthAccount = AccountBase & {
37
- type: 'oauth';
38
- access?: string;
39
- refresh: string;
40
- expires?: number;
41
- lastRefreshedAt?: number;
42
- lastRefreshError?: AccountOperationError;
43
- lastQuotaRefreshError?: AccountOperationError;
44
- quota?: OAuthQuotaSnapshot;
45
- };
46
- export type ApiKeyAccount = AccountBase & {
47
- type: 'api';
48
- apiKey?: string;
49
- baseURL: string;
50
- authHeader?: 'authorization-bearer' | 'x-api-key';
51
- };
52
- export type FallbackAccount = OAuthAccount | ApiKeyAccount;
53
- export declare function isOAuthAccount(account: FallbackAccount): account is OAuthAccount;
54
- export declare function isApiKeyAccount(account: FallbackAccount): account is ApiKeyAccount;
55
- export declare function isValidApiBaseURL(value: string | undefined): boolean;
56
- export type RoutingMode = 'main-first' | 'fallback-first' | 'sticky-balanced';
57
- export type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
58
- export type KillswitchConfig = {
59
- enabled?: boolean;
60
- main?: KillswitchThresholds;
61
- accounts?: Record<string, KillswitchThresholds>;
62
- };
63
- export interface ResetInFlight {
64
- redeemRequestId: string;
65
- creditId: string;
66
- startedAt: number;
67
- }
68
- export interface ResetLastOutcome {
69
- code: string;
70
- at: number;
71
- previousOutcome?: {
72
- code: string;
73
- at: number;
74
- };
75
- }
76
- export interface ResetAccountState {
77
- inFlight?: ResetInFlight | Record<string, unknown>;
78
- lastOutcome?: ResetLastOutcome;
79
- cooldownUntil?: number;
80
- }
81
- export type ResetStateByAccount = Record<string, ResetAccountState>;
82
- export type AccountStorage = {
83
- version: 1;
84
- main?: {
85
- type: 'opencode';
86
- provider: 'openai';
87
- };
88
- routing?: {
89
- mode?: RoutingMode;
90
- };
91
- fallbackOn?: number[];
92
- refresh?: {
93
- enabled?: boolean;
94
- intervalMinutes?: number;
95
- refreshBeforeExpiryMinutes?: number;
96
- mainLastRefreshError?: AccountOperationError;
97
- mainRefreshLeaseId?: string;
98
- mainRefreshLeaseUntil?: number;
99
- mainRefreshLeaseTokenHash?: string;
100
- };
101
- quota?: {
102
- enabled?: boolean;
103
- checkIntervalMinutes?: number;
104
- refreshEveryNRequests?: number;
105
- minimumRemaining?: Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
106
- failClosedOnUnknownQuota?: boolean;
107
- showToasts?: boolean;
108
- mainQuota?: OAuthQuotaSnapshot;
109
- mainQuotaCheckedAt?: number;
110
- mainQuotaToken?: string;
111
- mainLastQuotaApiError?: AccountOperationError;
112
- };
113
- reset?: ResetStateByAccount;
114
- dump?: {
115
- enabled?: boolean;
116
- };
117
- costZeroing?: {
118
- enabled?: boolean;
119
- };
120
- killswitch?: KillswitchConfig;
121
- logging?: {
122
- level?: string;
123
- };
124
- cachekeep?: {
125
- enabled?: boolean;
126
- subagents?: boolean;
127
- sustain?: boolean;
128
- /** Clock-hour window start (0-23, inclusive) — keeps cachekeep idle warming
129
- * inside `[startHour, endHour)` local hours. Omit to warm unconditionally. */
130
- startHour?: number;
131
- /** Clock-hour window end (0-23, exclusive) — must differ from startHour
132
- * to be honored; an unset or equal hour falls back to "always warm". */
133
- endHour?: number;
134
- };
135
- /** Stable ChatGPT account identifier of the main account (extracted from OAuth token). */
136
- mainAccountId?: string;
137
- accounts: FallbackAccount[];
138
- };
139
- export declare function isCostZeroingEnabled(storage: Pick<AccountStorage, 'costZeroing'>): boolean;
140
- export type AccountRuntimeEntry = Partial<Pick<OAuthAccount, 'access' | 'refresh' | 'expires' | 'lastUsed' | 'lastRefreshedAt' | 'lastRefreshError' | 'lastQuotaRefreshError' | 'quota'> & Pick<ApiKeyAccount, 'apiKey' | 'lastUsed'>>;
141
- export type AccountRuntimeState = {
142
- version: 1;
143
- main?: {
144
- quota?: OAuthQuotaSnapshot;
145
- quotaCheckedAt?: number;
146
- quotaToken?: string;
147
- lastQuotaApiError?: AccountOperationError;
148
- lastRefreshError?: AccountOperationError;
149
- refreshLeaseId?: string;
150
- refreshLeaseUntil?: number;
151
- refreshLeaseTokenHash?: string;
152
- };
153
- accounts?: Record<string, AccountRuntimeEntry>;
154
- };
155
- export type AccountStateSaveScope = {
156
- mainQuota?: boolean;
157
- mainRefresh?: boolean;
158
- accounts?: true | string[];
159
- };
160
- export type AccountManagerOptions = {
161
- now?: () => number;
162
- fetchImpl?: typeof fetch;
163
- configPath?: string;
164
- onFallbackStorageChanged?: () => void;
165
- /** Provider token-refresh function (constructor-injected). */
166
- refreshFn?: ProviderRefreshFn;
167
- /** Provider quota-fetch function (constructor-injected, OPTIONAL wham/usage supplement). */
168
- fetchQuotaFn?: ProviderQuotaFn;
169
- /** QuotaManager instance for unified cache (constructor-injected). */
170
- quotaManager?: import('./quota-manager.ts').QuotaManager;
171
- };
172
- export type AccountRefreshError = {
173
- accountId: string;
174
- message: string;
175
- };
176
- export declare class AccountRemovedDuringRefreshError extends Error {
177
- readonly code = "ACCOUNT_REMOVED_DURING_REFRESH";
178
- constructor(accountId: string);
179
- }
180
- export declare const DEFAULT_KILLSWITCH_THRESHOLDS: {
181
- primary: number;
182
- secondary: number;
183
- };
184
- export declare function isSafeResetAccountKey(accountKey: string): boolean;
185
- /**
186
- * The set of account ids present in the CONFIG file (the authoritative account
187
- * roster). Returns null when the config is absent or unreadable/malformed, so
188
- * callers can fall back to non-pruning behavior rather than risk wiping live
189
- * state. The config never holds secrets (see accountConfig), so reading it here
190
- * is safe. Reads are lock-free but the file is written atomically, so a
191
- * concurrent write is seen as either the complete old or complete new file.
192
- *
193
- * Trims and skips blank ids per the rule in collectConfigRosterIds above.
194
- */
195
- export declare function readConfigRosterIds(path: string): Promise<Set<string> | null>;
196
- export declare function loadAccounts(path?: string): Promise<AccountStorage | null>;
197
- export declare function saveAccounts(storage: AccountStorage, path?: string): Promise<void>;
198
- /**
199
- * Read-modify-write the account store atomically under the save lock.
200
- *
201
- * Unlike saveAccounts (which UNION-merges the incoming accounts with the latest
202
- * on-disk set so concurrent ADDS from another process are never lost), this
203
- * reads the freshest state under the lock and writes the mutator's result
204
- * AUTHORITATIVELY — no union. That is required for structural edits (remove,
205
- * reorder): a union cannot express a deletion (the removed id reappears from
206
- * `latest`) or a reordering (the union is seeded latest-first). Because the
207
- * mutator runs against freshly-read state under the lock, an add committed by a
208
- * concurrent process is still visible to it and preserved.
209
- *
210
- * The mutator may edit `current` in place and return it, or return a new
211
- * storage object. Returning undefined means "no change" and still rewrites the
212
- * freshly-read state (a harmless idempotent write).
213
- *
214
- * Load-time drop preservation: if normalizeAccount (called inside
215
- * normalizeStorage) rejects an account whose id IS in the raw config roster,
216
- * the previous behavior would erase that id silently on the next write. This
217
- * function now carries the dropped raw entry through to the written config
218
- * verbatim, so the on-disk state always matches the operator's intent (an
219
- * account they added, even if temporarily un-loadable, stays in their list
220
- * until they deliberately remove it).
221
- *
222
- * Removal seam: when the caller knows they are removing an id (e.g. the CLI
223
- * `remove` command) and that id may be load-dropped — in which case the
224
- * mutator cannot find it in `current.accounts` to splice it — the caller can
225
- * pass `options.allowDrop: [id]`. Ids in `allowDrop` are NOT preserved; the
226
- * mutator's splice still no-ops on a dropped id, but the absence of
227
- * preservation completes the removal end-to-end.
228
- */
229
- export declare function mutateAccounts(mutate: (current: AccountStorage) => AccountStorage | undefined, path?: string, options?: {
230
- allowDrop?: readonly string[];
231
- }): Promise<AccountStorage>;
232
- export declare function saveAccountState(storage: AccountStorage, path?: string, scope?: AccountStateSaveScope): Promise<void>;
233
- export declare function shouldFallbackStatus(status: number, storage: AccountStorage | null): boolean;
234
- export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, now?: number): boolean;
235
- export declare function isKillswitchEnabled(storage: AccountStorage | null): boolean;
236
- export declare function getKillswitchThresholdsForAccount(storage: AccountStorage | null, accountId?: string): {
237
- primary: number;
238
- secondary: number;
239
- };
240
- export declare function killswitchPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, accountId?: string, now?: number): boolean;
241
- export declare function killswitchRetryAfterSeconds(mainQuota: OAuthQuotaSnapshot | undefined, fallbackAccounts: Array<{
242
- accountId: string;
243
- quota?: OAuthQuotaSnapshot;
244
- }>, now: number, storage: AccountStorage | null): number;
245
- /**
246
- * Migrate an existing single-slot token into the multi-account store.
247
- *
248
- * Reads the existing token via the caller-provided `getAuth` (the ONLY
249
- * read path — there is no `client.auth.get`). If a token exists and the
250
- * config file is NOT yet an account store (content discriminator), seeds
251
- * it as the primary OAuth account.
252
- *
253
- * Idempotent: a second run is a no-op because the content discriminator
254
- * will already match.
255
- *
256
- * Tolerates expired/revoked tokens (migrates them; refresh handles validity).
257
- *
258
- * Guards against first-run races with the same save-lock order used by
259
- * structural account mutations.
260
- */
261
- export declare function migrateIfNeeded(existingToken: {
262
- type: 'oauth';
263
- access: string;
264
- refresh: string;
265
- expires: number;
266
- } | undefined, path?: string): Promise<void>;
267
- export declare function getQuotaCheckIntervalMs(storage: AccountStorage | null): number;
268
- export declare function getRefreshIntervalMs(storage: AccountStorage | null): number;
269
- export declare class FallbackAccountManager {
270
- private readonly now;
271
- private readonly fetchImpl;
272
- private readonly configPath;
273
- private readonly refreshPromises;
274
- private refreshTimer;
275
- private quotaTimer;
276
- readonly quotaManager: import('./quota-manager.ts').QuotaManager | null;
277
- private readonly onFallbackStorageChanged;
278
- private readonly options;
279
- constructor(options?: AccountManagerOptions);
280
- /**
281
- * Seed QuotaManager from persisted account.quota if no cache entry exists
282
- * yet. Prevents unnecessary API calls when the on-disk snapshot is fresh.
283
- */
284
- private seedFallbackQuota;
285
- load(): Promise<AccountStorage | null>;
286
- save(storage: AccountStorage, accountIds?: string[]): Promise<void>;
287
- startBackgroundRefresh(): void;
288
- stopBackgroundRefresh(): void;
289
- getUsableFallbackAccounts(existingStorage?: AccountStorage | null): Promise<OAuthAccount[]>;
290
- /**
291
- * Stamp `lastUsed` on a served account.
292
- *
293
- * This runs on the request path after a response is already in hand, and
294
- * `lastUsed` is telemetry: nothing reads it to make a routing, quota, or
295
- * killswitch decision. The WHOLE operation is therefore best-effort, read
296
- * included — loadAccounts deliberately rethrows anything that is not ENOENT so
297
- * corruption surfaces to callers that must act on it, and a store lock shared
298
- * by every session in the host process can legitimately time out under a burst
299
- * of concurrent turns. Neither may reach this caller: it would discard a
300
- * successful, already-billed provider response to record a timestamp. Losing
301
- * the stamp costs nothing a later turn cannot redo.
302
- *
303
- * Contrast refreshAccount, whose save persists rotated tokens and MUST
304
- * propagate: dropping it silently would strand a refresh and invalidate the
305
- * account's credentials.
306
- */
307
- markUsed(account: FallbackAccount): Promise<void>;
308
- accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null): boolean;
309
- /**
310
- * Return the account with its quota overlaid from the unified QuotaManager
311
- * cache (token-bound) when available, so quota-policy decisions use the same
312
- * source of truth as the staleness check. Falls back to the stored
313
- * account.quota when no manager is wired or the cache has no entry.
314
- */
315
- private quotaPolicyAccount;
316
- refreshDueAccounts(): Promise<void>;
317
- refreshQuotaForDueAccounts(): Promise<void>;
318
- refreshQuotaForAllAccounts(options?: {
319
- force?: boolean;
320
- }): Promise<{
321
- storage: AccountStorage | null;
322
- errors: AccountRefreshError[];
323
- }>;
324
- refreshAccount(account: OAuthAccount, storage: AccountStorage, options?: {
325
- force?: boolean;
326
- }): Promise<OAuthAccount>;
327
- private waitForConcurrentFallbackRefresh;
328
- private refreshAccountNow;
329
- refreshAccountQuota(account: OAuthAccount, storage: AccountStorage): Promise<void>;
330
- }
@@ -1 +0,0 @@
1
- export declare function writeJsonAtomic(path: string, value: unknown): Promise<void>;
@@ -1,46 +0,0 @@
1
- import { type RefreshAllQuotaDeps, type RefreshAllQuotaResult } from './refresh-all-quota';
2
- export declare const BACKGROUND_QUOTA_REFRESH_INTERVAL_MS: number;
3
- export declare const BACKGROUND_QUOTA_REFRESH_JITTER_MS = 30000;
4
- export declare const BACKGROUND_QUOTA_FRESHNESS_MS: number;
5
- export declare const BACKGROUND_QUOTA_REFRESH_LOCK_NAME = "bg-quota-refresh";
6
- export declare const BACKGROUND_QUOTA_REFRESH_LOCK_TTL_MS = 120000;
7
- type TimerHandle = ReturnType<typeof setInterval>;
8
- interface BackgroundQuotaRefreshOptions {
9
- setIntervalFn?: (callback: () => void, intervalMs: number) => TimerHandle;
10
- clearIntervalFn?: (timer: TimerHandle) => void;
11
- random?: () => number;
12
- onError?: (error: unknown) => void;
13
- }
14
- type RefreshAllQuotaFn = (deps: RefreshAllQuotaDeps) => Promise<RefreshAllQuotaResult[]>;
15
- type BackgroundLockHandle = {
16
- release: () => Promise<void>;
17
- };
18
- export type BackgroundLockAcquirer = () => Promise<BackgroundLockHandle | null>;
19
- /**
20
- * Acquire the cross-process lock that serializes the background refresh.
21
- * `renew` re-arms the TTL on an interval until release(), so a serial pass that
22
- * outlasts the TTL (a fleet of accounts each costing a token refresh plus a
23
- * wham fetch) never lets the lock expire mid-run — an expiry would let a second
24
- * process start a concurrent refresh. Overrides exist so tests can drive the
25
- * renewal with a mock clock and a fast interval.
26
- */
27
- export declare function acquireBackgroundRefreshLock(configPath: string, overrides?: {
28
- now?: () => number;
29
- renewIntervalMs?: number;
30
- }): Promise<BackgroundLockHandle | null>;
31
- export declare function refreshQuotaInBackground(deps: RefreshAllQuotaDeps, refreshFn?: RefreshAllQuotaFn, acquireLock?: BackgroundLockAcquirer): Promise<RefreshAllQuotaResult[]>;
32
- export declare class BackgroundQuotaRefresh {
33
- private readonly setIntervalFn;
34
- private readonly clearIntervalFn;
35
- private readonly random;
36
- private onError;
37
- private run;
38
- private timer;
39
- private tickPromise;
40
- private stopped;
41
- constructor(options?: BackgroundQuotaRefreshOptions);
42
- start(run: () => Promise<void>, onError?: ((error: unknown) => void) | undefined): void;
43
- stop(): void;
44
- isStopped(): boolean;
45
- }
46
- export {};