@cortexkit/opencode-openai-auth 0.1.1 → 0.1.3
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/README.md +27 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +14 -0
- package/dist/commands.d.ts +49 -0
- package/dist/core/accounts.d.ts +227 -0
- package/dist/core/atomic-write.d.ts +1 -0
- package/dist/core/backoff.d.ts +53 -0
- package/dist/core/cachekeep.d.ts +95 -0
- package/dist/core/oauth.d.ts +126 -0
- package/dist/core/provider.d.ts +59 -0
- package/dist/core/quota-manager.d.ts +136 -0
- package/dist/core/refresh-all-quota.d.ts +52 -0
- package/dist/core/refresh-file-lock.d.ts +15 -0
- package/dist/index-0s5tftne.js +3 -0
- package/dist/index-6qjg8qfw.js +2 -0
- package/dist/index-dz7tcrxb.js +2 -0
- package/dist/index-eks9xm6s.js +1 -0
- package/dist/index-hrg6bdgb.js +2 -0
- package/dist/index-jshhys8t.js +2 -0
- package/dist/index-mrh6k1zf.js +92 -0
- package/dist/index-sgfft5d3.js +2 -0
- package/dist/index.d.ts +3 -19
- package/dist/index.js +118 -124
- package/dist/logger.d.ts +11 -1
- package/dist/quota-normalize-qh5n0p7v.js +1 -0
- package/dist/quota-normalize.d.ts +33 -0
- package/dist/response-stream-error.d.ts +12 -1
- package/dist/rpc/notifications.d.ts +5 -0
- package/dist/rpc/port-file.d.ts +12 -0
- package/dist/rpc/port-file.js +1 -0
- package/dist/rpc/protocol.d.ts +20 -0
- package/dist/rpc/protocol.js +0 -0
- package/dist/rpc/rpc-client.d.ts +7 -0
- package/dist/rpc/rpc-client.js +1 -0
- package/dist/rpc/rpc-dir.d.ts +3 -0
- package/dist/rpc/rpc-dir.js +1 -0
- package/dist/rpc/rpc-server.d.ts +13 -0
- package/dist/sidebar-state.d.ts +83 -0
- package/dist/sidebar-state.js +1 -0
- package/dist/token-util-sqwtzxz0.js +1 -0
- package/dist/token-zzzp7w2y.js +1 -0
- package/dist/tui/command-dialogs.d.ts +14 -0
- package/dist/tui-preferences.d.ts +40 -0
- package/dist/tui-preferences.js +6 -0
- package/dist/tui.d.ts +9 -0
- package/dist/util/open-url.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/ws-pool.d.ts +8 -0
- package/dist/ws.d.ts +4 -0
- package/package.json +25 -4
- package/src/rpc/notifications.ts +54 -0
- package/src/rpc/port-file.ts +70 -0
- package/src/rpc/protocol.ts +31 -0
- package/src/rpc/rpc-client.ts +78 -0
- package/src/rpc/rpc-dir.ts +22 -0
- package/src/rpc/rpc-server.ts +129 -0
- package/src/sidebar-state.ts +312 -0
- package/src/tui/command-dialogs.tsx +742 -0
- package/src/tui-preferences.ts +328 -0
- package/src/tui.tsx +837 -0
- package/dist/index-6z8w05g9.js +0 -2
- package/dist/index-xqe8xm96.js +0 -2
- package/dist/token-util-fs3z8kkp.js +0 -1
- package/dist/token-yyjm093c.js +0 -1
package/README.md
CHANGED
|
@@ -26,8 +26,35 @@ Restart OpenCode after changing plugin config, then authenticate:
|
|
|
26
26
|
- Codex request rewriting for OAuth requests, with Codex identity parity.
|
|
27
27
|
- OAuth model filtering and zero-cost display.
|
|
28
28
|
- Prompt-cache stabilizer (`web_search`) that keeps tool-continuation requests on the backend's cached path (on by default).
|
|
29
|
+
- Multiple ChatGPT accounts with automatic reactive fallback on rate limits, configurable routing, and a per-account quota killswitch.
|
|
30
|
+
- Per-turn quota tracking (5-hour + weekly windows) on both transports, with a sidebar readout and an explicit all-accounts refresh.
|
|
31
|
+
- Idle prompt-cache keep-warm, with an optional subagent mode.
|
|
32
|
+
- Leveled, secret-redacting, rotating log file.
|
|
33
|
+
- Interactive in-TUI control surfaces for every command, plus an `openai-auth` CLI for managing fallback accounts headlessly.
|
|
29
34
|
- Optional OpenAI Responses WebSocket transport (HTTP is the default).
|
|
30
35
|
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
Each opens an interactive dialog in the TUI and also accepts explicit arguments:
|
|
39
|
+
|
|
40
|
+
| Command | Arguments | Purpose |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `/openai-quota` | — | Show 5h + weekly quota for all accounts. |
|
|
43
|
+
| `/openai-account` | `add [label]` · `switch <id>` · `remove <id>` · `order <a> <b>` | Manage main + fallback accounts (non-destructive switch). |
|
|
44
|
+
| `/openai-routing` | `main-first` · `fallback-first` | Account preference order. |
|
|
45
|
+
| `/openai-killswitch` | `on` · `off` · `set <acct>:<5h>,<1w> ...` | Hard-block accounts below quota thresholds. |
|
|
46
|
+
| `/openai-cachekeep` | `on` · `off` · `subagents on` · `subagents off` | Idle prompt-cache keep-warm. |
|
|
47
|
+
| `/openai-logging` | `<level>` | Set log level live. |
|
|
48
|
+
| `/openai-dump` | `on` · `off` | Toggle transport request dumps. |
|
|
49
|
+
|
|
50
|
+
CLI (fallback accounts only; the main account comes from `/login openai`):
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
openai-auth login [--label <name>] [--headless]
|
|
54
|
+
openai-auth list
|
|
55
|
+
openai-auth remove <id>
|
|
56
|
+
```
|
|
57
|
+
|
|
31
58
|
## Configuration
|
|
32
59
|
|
|
33
60
|
Settings resolve as environment variable → config file (`~/.config/opencode/openai-auth.json`) → default.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{D as $,H as Q,I as W,y as Y,z as Z}from"./index-mrh6k1zf.js";import"./index-0s5tftne.js";import{ea as X}from"./index-hrg6bdgb.js";import{execFileSync as q}from"node:child_process";function T(D,J=process.platform,H=q){try{if(J==="win32"){H("cmd",["/c","start","",D],{stdio:"ignore",timeout:3000});return}H(J==="darwin"?"open":"xdg-open",[D],{stdio:"ignore",timeout:3000})}catch{}}function V(){console.log(`Usage:
|
|
3
|
+
openai-auth login [--label <name>] [--headless]
|
|
4
|
+
openai-auth list
|
|
5
|
+
openai-auth remove <id>
|
|
6
|
+
|
|
7
|
+
Fallback accounts are stored in:
|
|
8
|
+
${$()}`)}function B(D){let J=[],H={};for(let M=0;M<D.length;M++){let z=D[M];if(!z)continue;if(z.startsWith("--")){let C=z.slice(2),G=D[M+1];if(G&&!G.startsWith("--"))H[C]=G,M++;else H[C]=!0}else J.push(z)}return{positional:J,flags:H}}async function E(){let{positional:D,flags:J}=B(process.argv.slice(2)),[H,...M]=D;if(!H||H==="help")V(),process.exit(0);switch(H){case"login":{let z=typeof J.label==="string"?J.label:void 0,C=Boolean(J.headless),{url:G,instructions:K,completion:j}=await Z({label:z,headless:C});if(console.log(`
|
|
9
|
+
Open this URL in your browser and complete sign-in:
|
|
10
|
+
`),console.log(`${G}
|
|
11
|
+
`),K)console.log(`${K}
|
|
12
|
+
`);T(G);let N=await j,O=await Q()??{version:1,accounts:[]};if(N.accountId&&O.mainAccountId&&N.accountId===O.mainAccountId)console.error(`
|
|
13
|
+
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(Y(O.accounts,N),await W(O),console.log(`
|
|
14
|
+
✓ Added account ${N.id}`),N.label)console.log(` Label: ${N.label}`);break}case"list":{let z=await Q();if(!z||z.accounts.length===0)console.log("No fallback accounts configured.");else for(let C of z.accounts){let G=C.label,K=[` ${C.id}`];if(G)K.push(`(${G})`);K.push(C.enabled!==!1?"[enabled]":"[disabled]"),console.log(K.join(" "))}break}case"remove":{let z=M[0];if(!z)console.error("Error: remove requires an account ID."),V(),process.exit(1);let C=await Q();if(!C)console.error("No account store found."),process.exit(1);let G=C.accounts.findIndex((K)=>K.id===z);if(G===-1)console.error(`No account with id "${z}".`),process.exit(1);C.accounts.splice(G,1),await W(C),console.log(`Removed account ${z}.`);break}default:console.error(`Unknown command: ${H}`),V(),process.exit(1)}}if(X.main==X.module)E().catch((D)=>{console.error(D instanceof Error?D.message:String(D)),process.exit(1)});export{T as openBrowserForLogin};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type loadAccounts as defaultLoadAccounts } from './core/accounts';
|
|
2
|
+
import type { CacheKeepManager } from './core/cachekeep';
|
|
3
|
+
import type { QuotaManager } from './core/quota-manager';
|
|
4
|
+
import type { RefreshAllQuotaResult } from './core/refresh-all-quota';
|
|
5
|
+
import type { ApplyRequest, ApplyResult, CommandModalName, OpenDialogPayload } from './rpc/protocol';
|
|
6
|
+
export declare const OPENAI_QUOTA_COMMAND_NAME = "openai-quota";
|
|
7
|
+
export declare const OPENAI_ACCOUNT_COMMAND_NAME = "openai-account";
|
|
8
|
+
export declare const OPENAI_ROUTING_COMMAND_NAME = "openai-routing";
|
|
9
|
+
export declare const OPENAI_KILLSWITCH_COMMAND_NAME = "openai-killswitch";
|
|
10
|
+
export declare const OPENAI_DUMP_COMMAND_NAME = "openai-dump";
|
|
11
|
+
export declare const OPENAI_LOGGING_COMMAND_NAME = "openai-logging";
|
|
12
|
+
export declare const OPENAI_CACHEKEEP_COMMAND_NAME = "openai-cachekeep";
|
|
13
|
+
export declare const MODAL_COMMANDS: CommandModalName[];
|
|
14
|
+
export interface CommandContext {
|
|
15
|
+
accountStoragePath: string;
|
|
16
|
+
quotaManager: QuotaManager;
|
|
17
|
+
loadAccounts: typeof defaultLoadAccounts;
|
|
18
|
+
client: {
|
|
19
|
+
auth: {
|
|
20
|
+
set: (input: {
|
|
21
|
+
path: {
|
|
22
|
+
id: string;
|
|
23
|
+
};
|
|
24
|
+
body: {
|
|
25
|
+
type: string;
|
|
26
|
+
access?: string;
|
|
27
|
+
refresh: string;
|
|
28
|
+
expires?: number;
|
|
29
|
+
};
|
|
30
|
+
}) => Promise<unknown>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** Session ID for pushNotification delivery. */
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
/** If set, pushNotification is wired up and can deliver feedback to the user. */
|
|
36
|
+
notify?: (payload: OpenDialogPayload) => void;
|
|
37
|
+
/** Refresh the sidebar-state file so the TUI modal shows current data. */
|
|
38
|
+
refreshSidebar?: () => Promise<void>;
|
|
39
|
+
/** Actively poll wham/usage for all accounts (main + fallbacks). */
|
|
40
|
+
refreshAllQuota?: () => Promise<RefreshAllQuotaResult[]>;
|
|
41
|
+
/** Prompt-cache cachekeep manager. Set when the command is wired. */
|
|
42
|
+
cacheKeepManager?: CacheKeepManager | null;
|
|
43
|
+
/** Updates the live loader's persisted-enabled cachekeep gate. */
|
|
44
|
+
setCacheKeepEnabled?: (enabled: boolean) => void;
|
|
45
|
+
/** Updates the live loader's persisted-subagent cachekeep gate. */
|
|
46
|
+
setCacheKeepSubagents?: (enabled: boolean) => void;
|
|
47
|
+
}
|
|
48
|
+
export declare function buildDialogPayload(command: CommandModalName, args: string, ctx: CommandContext): Promise<OpenDialogPayload>;
|
|
49
|
+
export declare function applyCommand(request: ApplyRequest, ctx: CommandContext): Promise<ApplyResult>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { ProviderQuotaFn, ProviderRefreshFn, QuotaWindowName } from './provider.ts';
|
|
2
|
+
import { PRIMARY, SECONDARY } from './provider.ts';
|
|
3
|
+
export declare const ACCOUNT_FILE_NAME = "openai-auth.json";
|
|
4
|
+
export declare const ACCOUNT_STATE_FILE_NAME = "openai-auth-state.json";
|
|
5
|
+
export declare function getAccountStoragePath(): string;
|
|
6
|
+
export declare function getAccountStatePath(configPath?: string): string;
|
|
7
|
+
export type { QuotaWindowName };
|
|
8
|
+
export { PRIMARY, SECONDARY };
|
|
9
|
+
export type AccountQuotaWindow = {
|
|
10
|
+
usedPercent: number;
|
|
11
|
+
remainingPercent: number;
|
|
12
|
+
resetsAt?: string;
|
|
13
|
+
checkedAt: number;
|
|
14
|
+
};
|
|
15
|
+
export type OAuthQuotaSnapshot = Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
|
|
16
|
+
export type AccountBase = {
|
|
17
|
+
id: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
addedAt?: number;
|
|
21
|
+
lastUsed?: number;
|
|
22
|
+
/** Stable ChatGPT account identifier extracted from the OAuth token claims. */
|
|
23
|
+
accountId?: string;
|
|
24
|
+
};
|
|
25
|
+
export type AccountOperationError = {
|
|
26
|
+
message: string;
|
|
27
|
+
checkedAt: number;
|
|
28
|
+
nextRetryAt?: number;
|
|
29
|
+
retryCount?: number;
|
|
30
|
+
tokenHash?: string;
|
|
31
|
+
};
|
|
32
|
+
export type OAuthAccount = AccountBase & {
|
|
33
|
+
type: 'oauth';
|
|
34
|
+
access?: string;
|
|
35
|
+
refresh: string;
|
|
36
|
+
expires?: number;
|
|
37
|
+
lastRefreshedAt?: number;
|
|
38
|
+
lastRefreshError?: AccountOperationError;
|
|
39
|
+
lastQuotaRefreshError?: AccountOperationError;
|
|
40
|
+
quota?: Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
|
|
41
|
+
};
|
|
42
|
+
export type ApiKeyAccount = AccountBase & {
|
|
43
|
+
type: 'api';
|
|
44
|
+
apiKey?: string;
|
|
45
|
+
baseURL: string;
|
|
46
|
+
authHeader?: 'authorization-bearer' | 'x-api-key';
|
|
47
|
+
};
|
|
48
|
+
export type FallbackAccount = OAuthAccount | ApiKeyAccount;
|
|
49
|
+
export declare function isOAuthAccount(account: FallbackAccount): account is OAuthAccount;
|
|
50
|
+
export declare function isApiKeyAccount(account: FallbackAccount): account is ApiKeyAccount;
|
|
51
|
+
export declare function isValidApiBaseURL(value: string | undefined): boolean;
|
|
52
|
+
export type RoutingMode = 'main-first' | 'fallback-first';
|
|
53
|
+
export type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
|
|
54
|
+
export type KillswitchConfig = {
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
main?: KillswitchThresholds;
|
|
57
|
+
accounts?: Record<string, KillswitchThresholds>;
|
|
58
|
+
};
|
|
59
|
+
export type AccountStorage = {
|
|
60
|
+
version: 1;
|
|
61
|
+
main?: {
|
|
62
|
+
type: 'opencode';
|
|
63
|
+
provider: 'openai';
|
|
64
|
+
};
|
|
65
|
+
routing?: {
|
|
66
|
+
mode?: RoutingMode;
|
|
67
|
+
activeId?: string;
|
|
68
|
+
};
|
|
69
|
+
fallbackOn?: number[];
|
|
70
|
+
refresh?: {
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
intervalMinutes?: number;
|
|
73
|
+
refreshBeforeExpiryMinutes?: number;
|
|
74
|
+
mainLastRefreshError?: AccountOperationError;
|
|
75
|
+
mainRefreshLeaseId?: string;
|
|
76
|
+
mainRefreshLeaseUntil?: number;
|
|
77
|
+
mainRefreshLeaseTokenHash?: string;
|
|
78
|
+
};
|
|
79
|
+
quota?: {
|
|
80
|
+
enabled?: boolean;
|
|
81
|
+
checkIntervalMinutes?: number;
|
|
82
|
+
refreshEveryNRequests?: number;
|
|
83
|
+
minimumRemaining?: Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
|
|
84
|
+
failClosedOnUnknownQuota?: boolean;
|
|
85
|
+
showToasts?: boolean;
|
|
86
|
+
mainQuota?: OAuthQuotaSnapshot;
|
|
87
|
+
mainQuotaCheckedAt?: number;
|
|
88
|
+
mainQuotaToken?: string;
|
|
89
|
+
mainLastQuotaApiError?: AccountOperationError;
|
|
90
|
+
};
|
|
91
|
+
dump?: {
|
|
92
|
+
enabled?: boolean;
|
|
93
|
+
};
|
|
94
|
+
costZeroing?: {
|
|
95
|
+
enabled?: boolean;
|
|
96
|
+
};
|
|
97
|
+
killswitch?: KillswitchConfig;
|
|
98
|
+
logging?: {
|
|
99
|
+
level?: string;
|
|
100
|
+
};
|
|
101
|
+
cachekeep?: {
|
|
102
|
+
enabled?: boolean;
|
|
103
|
+
subagents?: boolean;
|
|
104
|
+
};
|
|
105
|
+
/** Stable ChatGPT account identifier of the main account (extracted from OAuth token). */
|
|
106
|
+
mainAccountId?: string;
|
|
107
|
+
accounts: FallbackAccount[];
|
|
108
|
+
};
|
|
109
|
+
export declare function isCostZeroingEnabled(storage: Pick<AccountStorage, 'costZeroing'>): boolean;
|
|
110
|
+
export type AccountRuntimeEntry = Partial<Pick<OAuthAccount, 'access' | 'refresh' | 'expires' | 'lastUsed' | 'lastRefreshedAt' | 'lastRefreshError' | 'lastQuotaRefreshError' | 'quota'> & Pick<ApiKeyAccount, 'apiKey' | 'lastUsed'>>;
|
|
111
|
+
export type AccountRuntimeState = {
|
|
112
|
+
version: 1;
|
|
113
|
+
main?: {
|
|
114
|
+
quota?: OAuthQuotaSnapshot;
|
|
115
|
+
quotaCheckedAt?: number;
|
|
116
|
+
quotaToken?: string;
|
|
117
|
+
lastQuotaApiError?: AccountOperationError;
|
|
118
|
+
lastRefreshError?: AccountOperationError;
|
|
119
|
+
refreshLeaseId?: string;
|
|
120
|
+
refreshLeaseUntil?: number;
|
|
121
|
+
refreshLeaseTokenHash?: string;
|
|
122
|
+
};
|
|
123
|
+
accounts?: Record<string, AccountRuntimeEntry>;
|
|
124
|
+
};
|
|
125
|
+
export type AccountStateSaveScope = {
|
|
126
|
+
mainQuota?: boolean;
|
|
127
|
+
mainRefresh?: boolean;
|
|
128
|
+
accounts?: true | string[];
|
|
129
|
+
};
|
|
130
|
+
export type AccountManagerOptions = {
|
|
131
|
+
now?: () => number;
|
|
132
|
+
fetchImpl?: typeof fetch;
|
|
133
|
+
configPath?: string;
|
|
134
|
+
onFallbackStorageChanged?: () => void;
|
|
135
|
+
/** Provider token-refresh function (constructor-injected). */
|
|
136
|
+
refreshFn?: ProviderRefreshFn;
|
|
137
|
+
/** Provider quota-fetch function (constructor-injected, OPTIONAL wham/usage supplement). */
|
|
138
|
+
fetchQuotaFn?: ProviderQuotaFn;
|
|
139
|
+
/** QuotaManager instance for unified cache (constructor-injected). */
|
|
140
|
+
quotaManager?: import('./quota-manager.ts').QuotaManager;
|
|
141
|
+
};
|
|
142
|
+
export type AccountRefreshError = {
|
|
143
|
+
accountId: string;
|
|
144
|
+
message: string;
|
|
145
|
+
};
|
|
146
|
+
export declare const DEFAULT_KILLSWITCH_THRESHOLDS: {
|
|
147
|
+
primary: number;
|
|
148
|
+
secondary: number;
|
|
149
|
+
};
|
|
150
|
+
export declare function loadAccounts(path?: string): Promise<AccountStorage | null>;
|
|
151
|
+
export declare function saveAccounts(storage: AccountStorage, path?: string): Promise<void>;
|
|
152
|
+
export declare function saveAccountState(storage: AccountStorage, path?: string, scope?: AccountStateSaveScope): Promise<void>;
|
|
153
|
+
export declare function shouldFallbackStatus(status: number, storage: AccountStorage | null): boolean;
|
|
154
|
+
export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null): boolean;
|
|
155
|
+
export declare function isKillswitchEnabled(storage: AccountStorage | null): boolean;
|
|
156
|
+
export declare function killswitchPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, accountId?: string): boolean;
|
|
157
|
+
export declare function killswitchRetryAfterSeconds(mainQuota: OAuthQuotaSnapshot | undefined, fallbackAccounts: Array<{
|
|
158
|
+
quota?: OAuthQuotaSnapshot;
|
|
159
|
+
}>, now: number): number;
|
|
160
|
+
/**
|
|
161
|
+
* Migrate an existing single-slot token into the multi-account store.
|
|
162
|
+
*
|
|
163
|
+
* Reads the existing token via the caller-provided `getAuth` (the ONLY
|
|
164
|
+
* read path — there is no `client.auth.get`). If a token exists and the
|
|
165
|
+
* config file is NOT yet an account store (content discriminator), seeds
|
|
166
|
+
* it as the primary OAuth account.
|
|
167
|
+
*
|
|
168
|
+
* Idempotent: a second run is a no-op because the content discriminator
|
|
169
|
+
* will already match.
|
|
170
|
+
*
|
|
171
|
+
* Tolerates expired/revoked tokens (migrates them; refresh handles validity).
|
|
172
|
+
*
|
|
173
|
+
* Guards against first-run races with `acquireRefreshFileLock`.
|
|
174
|
+
*/
|
|
175
|
+
export declare function migrateIfNeeded(existingToken: {
|
|
176
|
+
type: 'oauth';
|
|
177
|
+
access: string;
|
|
178
|
+
refresh: string;
|
|
179
|
+
expires: number;
|
|
180
|
+
} | undefined, path?: string): Promise<void>;
|
|
181
|
+
export declare function getQuotaCheckIntervalMs(storage: AccountStorage | null): number;
|
|
182
|
+
export declare function getRefreshIntervalMs(storage: AccountStorage | null): number;
|
|
183
|
+
export declare class FallbackAccountManager {
|
|
184
|
+
private readonly now;
|
|
185
|
+
private readonly fetchImpl;
|
|
186
|
+
private readonly configPath;
|
|
187
|
+
private readonly refreshPromises;
|
|
188
|
+
private refreshTimer;
|
|
189
|
+
private quotaTimer;
|
|
190
|
+
readonly quotaManager: import('./quota-manager.ts').QuotaManager | null;
|
|
191
|
+
private readonly onFallbackStorageChanged;
|
|
192
|
+
private readonly options;
|
|
193
|
+
constructor(options?: AccountManagerOptions);
|
|
194
|
+
/**
|
|
195
|
+
* Seed QuotaManager from persisted account.quota if no cache entry exists
|
|
196
|
+
* yet. Prevents unnecessary API calls when the on-disk snapshot is fresh.
|
|
197
|
+
*/
|
|
198
|
+
private seedFallbackQuota;
|
|
199
|
+
load(): Promise<AccountStorage | null>;
|
|
200
|
+
save(storage: AccountStorage, accountIds?: string[]): Promise<void>;
|
|
201
|
+
startBackgroundRefresh(): void;
|
|
202
|
+
stopBackgroundRefresh(): void;
|
|
203
|
+
getUsableFallbackAccounts(existingStorage?: AccountStorage | null): Promise<OAuthAccount[]>;
|
|
204
|
+
markUsed(account: FallbackAccount): Promise<void>;
|
|
205
|
+
accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null): boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Return the account with its quota overlaid from the unified QuotaManager
|
|
208
|
+
* cache (token-bound) when available, so quota-policy decisions use the same
|
|
209
|
+
* source of truth as the staleness check. Falls back to the stored
|
|
210
|
+
* account.quota when no manager is wired or the cache has no entry.
|
|
211
|
+
*/
|
|
212
|
+
private quotaPolicyAccount;
|
|
213
|
+
refreshDueAccounts(): Promise<void>;
|
|
214
|
+
refreshQuotaForDueAccounts(): Promise<void>;
|
|
215
|
+
refreshQuotaForAllAccounts(options?: {
|
|
216
|
+
force?: boolean;
|
|
217
|
+
}): Promise<{
|
|
218
|
+
storage: AccountStorage | null;
|
|
219
|
+
errors: AccountRefreshError[];
|
|
220
|
+
}>;
|
|
221
|
+
refreshAccount(account: OAuthAccount, storage: AccountStorage, options?: {
|
|
222
|
+
force?: boolean;
|
|
223
|
+
}): Promise<OAuthAccount>;
|
|
224
|
+
private waitForConcurrentFallbackRefresh;
|
|
225
|
+
private refreshAccountNow;
|
|
226
|
+
refreshAccountQuota(account: OAuthAccount, storage: AccountStorage): Promise<void>;
|
|
227
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeJsonAtomic(path: string, value: unknown): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const NON_TRANSIENT_REFRESH_RETRY_DELAY_MS: number;
|
|
2
|
+
export declare function isTransientRefreshError(error: unknown): boolean;
|
|
3
|
+
export declare function isTransientQuotaError(error: unknown): boolean;
|
|
4
|
+
export declare function hashRefreshToken(refreshToken: string): string;
|
|
5
|
+
export declare function buildRefreshOperationError(input: {
|
|
6
|
+
error: unknown;
|
|
7
|
+
now: number;
|
|
8
|
+
refreshToken: string;
|
|
9
|
+
previous?: {
|
|
10
|
+
message: string;
|
|
11
|
+
checkedAt: number;
|
|
12
|
+
nextRetryAt?: number;
|
|
13
|
+
retryCount?: number;
|
|
14
|
+
tokenHash?: string;
|
|
15
|
+
};
|
|
16
|
+
}): {
|
|
17
|
+
message: string;
|
|
18
|
+
checkedAt: number;
|
|
19
|
+
nextRetryAt: number;
|
|
20
|
+
retryCount: number;
|
|
21
|
+
tokenHash: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function refreshBackoffActive(error: {
|
|
24
|
+
nextRetryAt?: number;
|
|
25
|
+
tokenHash?: string;
|
|
26
|
+
} | undefined, refreshToken: string | undefined, now: number): boolean;
|
|
27
|
+
export declare function formatRefreshBackoffMessage(error: {
|
|
28
|
+
message: string;
|
|
29
|
+
nextRetryAt?: number;
|
|
30
|
+
}, now: number): string;
|
|
31
|
+
export declare function buildQuotaOperationError(input: {
|
|
32
|
+
error: unknown;
|
|
33
|
+
now: number;
|
|
34
|
+
previous?: {
|
|
35
|
+
message: string;
|
|
36
|
+
checkedAt: number;
|
|
37
|
+
nextRetryAt?: number;
|
|
38
|
+
retryCount?: number;
|
|
39
|
+
};
|
|
40
|
+
}): {
|
|
41
|
+
message: string;
|
|
42
|
+
checkedAt: number;
|
|
43
|
+
nextRetryAt: number;
|
|
44
|
+
retryCount: number;
|
|
45
|
+
};
|
|
46
|
+
export declare function quotaBackoffActive(error: {
|
|
47
|
+
nextRetryAt?: number;
|
|
48
|
+
} | undefined, now: number): boolean;
|
|
49
|
+
export declare function formatQuotaBackoffMessage(error: {
|
|
50
|
+
message: string;
|
|
51
|
+
nextRetryAt?: number;
|
|
52
|
+
}, now: number): string;
|
|
53
|
+
export declare function parseRetryAfter(value: string | undefined | null): number | undefined;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export interface Target {
|
|
2
|
+
bodyText: string;
|
|
3
|
+
accountId: string | undefined;
|
|
4
|
+
chatgptAccountId?: string;
|
|
5
|
+
route: 'main';
|
|
6
|
+
cacheExpiresAt: number;
|
|
7
|
+
lastRealRequestAt: number;
|
|
8
|
+
lastWarmedAt?: number;
|
|
9
|
+
backoffUntil?: number;
|
|
10
|
+
replayHeaders: Record<string, string>;
|
|
11
|
+
isSubagent?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface CacheKeepManagerOptions {
|
|
14
|
+
fetchImpl: typeof fetch;
|
|
15
|
+
getMainToken: () => Promise<string>;
|
|
16
|
+
refreshFallback: (accountId: string) => Promise<string>;
|
|
17
|
+
codexResponsesUrl: string;
|
|
18
|
+
logger: {
|
|
19
|
+
info: (msg: string, data?: unknown) => void;
|
|
20
|
+
warn: (msg: string, data?: unknown) => void;
|
|
21
|
+
debug: (msg: string, data?: unknown) => void;
|
|
22
|
+
error: (msg: string, data?: unknown) => void;
|
|
23
|
+
};
|
|
24
|
+
now: () => number;
|
|
25
|
+
ttlMs?: number;
|
|
26
|
+
leadMs?: number;
|
|
27
|
+
maxDurationMs?: number;
|
|
28
|
+
maxIdleWarmMs?: number;
|
|
29
|
+
maxSubagentIdleMs?: number;
|
|
30
|
+
tickIntervalMs?: number;
|
|
31
|
+
maxTargets?: number;
|
|
32
|
+
maxBytes?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface CacheKeepStatus {
|
|
35
|
+
running: boolean;
|
|
36
|
+
tracked: number;
|
|
37
|
+
generatedAt: number;
|
|
38
|
+
startedAt: number | null;
|
|
39
|
+
maxIdleWarmMs: number;
|
|
40
|
+
maxSubagentIdleMs: number;
|
|
41
|
+
ttlMs: number;
|
|
42
|
+
leadMs: number;
|
|
43
|
+
targets: Array<{
|
|
44
|
+
sessionKey: string;
|
|
45
|
+
accountId: string | undefined;
|
|
46
|
+
route: 'main';
|
|
47
|
+
cacheExpiresAt: number;
|
|
48
|
+
lastRealRequestAt: number;
|
|
49
|
+
lastWarmedAt?: number;
|
|
50
|
+
backoffUntil?: number;
|
|
51
|
+
bodyBytes: number;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
export interface KeepwarmCapture {
|
|
55
|
+
sessionKey: string;
|
|
56
|
+
bodyText: string;
|
|
57
|
+
replayHeaders: Record<string, string>;
|
|
58
|
+
isSubagent: boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare function buildKeepwarmCapture(input: {
|
|
61
|
+
enabled: boolean;
|
|
62
|
+
includeSubagents: boolean;
|
|
63
|
+
headers: Headers;
|
|
64
|
+
body: unknown;
|
|
65
|
+
}): KeepwarmCapture | undefined;
|
|
66
|
+
export declare function buildKeepwarmBody(bodyText: string): string;
|
|
67
|
+
export declare class CacheKeepManager {
|
|
68
|
+
private readonly targets;
|
|
69
|
+
private readonly fetchImpl;
|
|
70
|
+
private readonly getMainToken;
|
|
71
|
+
private readonly refreshFallback;
|
|
72
|
+
private readonly codexResponsesUrl;
|
|
73
|
+
private readonly log;
|
|
74
|
+
private readonly now;
|
|
75
|
+
private readonly ttlMs;
|
|
76
|
+
private readonly leadMs;
|
|
77
|
+
private readonly maxIdleWarmMs;
|
|
78
|
+
private readonly maxSubagentIdleMs;
|
|
79
|
+
private readonly tickIntervalMs;
|
|
80
|
+
private readonly maxTargets;
|
|
81
|
+
private readonly maxBytes;
|
|
82
|
+
private timer;
|
|
83
|
+
private startedAt;
|
|
84
|
+
private totalBytes;
|
|
85
|
+
private tickInFlight;
|
|
86
|
+
private logPayload;
|
|
87
|
+
constructor(options: CacheKeepManagerOptions);
|
|
88
|
+
track(sessionKey: string, bodyText: string, accountId: string | undefined, chatgptAccountId?: string, replayHeaders?: Record<string, string>, isSubagent?: boolean): void;
|
|
89
|
+
private pruneStale;
|
|
90
|
+
start(): void;
|
|
91
|
+
stop(): void;
|
|
92
|
+
status(): CacheKeepStatus;
|
|
93
|
+
tick(): Promise<void>;
|
|
94
|
+
private prewarm;
|
|
95
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export declare const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
2
|
+
export declare const ISSUER = "https://auth.openai.com";
|
|
3
|
+
export declare const OAUTH_PORT = 1455;
|
|
4
|
+
export declare const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
5
|
+
export declare const USER_AGENT = "cortexkit-opencode-openai-auth/0.1.3";
|
|
6
|
+
export interface PkceCodes {
|
|
7
|
+
verifier: string;
|
|
8
|
+
challenge: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function base64UrlEncode(buffer: ArrayBuffer): string;
|
|
11
|
+
export declare function generatePKCE(): Promise<PkceCodes>;
|
|
12
|
+
export interface IdTokenClaims {
|
|
13
|
+
chatgpt_account_id?: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
organizations?: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
}>;
|
|
18
|
+
'https://api.openai.com/auth'?: {
|
|
19
|
+
chatgpt_account_id?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare function parseJwtClaims(token: string): IdTokenClaims | undefined;
|
|
23
|
+
export declare function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined;
|
|
24
|
+
export interface TokenResponse {
|
|
25
|
+
id_token: string;
|
|
26
|
+
access_token: string;
|
|
27
|
+
refresh_token: string;
|
|
28
|
+
expires_in?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function extractAccountId(tokens: TokenResponse): string | undefined;
|
|
31
|
+
export declare function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string;
|
|
32
|
+
export declare function exchangeCodeForTokens(code: string, redirectUri: string, pkce: PkceCodes): Promise<TokenResponse>;
|
|
33
|
+
export declare function escapeHtml(value: string): string;
|
|
34
|
+
export declare const HTML_SUCCESS = "<!doctype html>\n<html>\n <head>\n <title>CortexKit OpenAI Auth - Authorization Successful</title>\n <style>\n body {\n font-family:\n system-ui,\n -apple-system,\n sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n margin: 0;\n background: #131010;\n color: #f1ecec;\n }\n .container {\n text-align: center;\n padding: 2rem;\n }\n h1 {\n color: #f1ecec;\n margin-bottom: 1rem;\n }\n p {\n color: #b7b1b1;\n }\n </style>\n </head>\n <body>\n <div class=\"container\">\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to OpenCode.</p>\n </div>\n <script>\n setTimeout(() => window.close(), 2000)\n </script>\n </body>\n</html>";
|
|
35
|
+
export declare const HTML_ERROR: (error: string) => string;
|
|
36
|
+
export interface PendingOAuth {
|
|
37
|
+
pkce: PkceCodes;
|
|
38
|
+
state: string;
|
|
39
|
+
resolve: (tokens: TokenResponse) => void;
|
|
40
|
+
reject: (error: Error) => void;
|
|
41
|
+
}
|
|
42
|
+
export declare function flowCleanup(state: string): void;
|
|
43
|
+
export declare function startOAuthServer(): Promise<{
|
|
44
|
+
port: number;
|
|
45
|
+
redirectUri: string;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function stopOAuthServer(): void;
|
|
48
|
+
export declare function resetOAuthStateForTest(): void;
|
|
49
|
+
export declare function waitForOAuthCallback(pkce: PkceCodes, state: string, timeoutMs?: number): Promise<TokenResponse>;
|
|
50
|
+
export interface DeviceAuthInit {
|
|
51
|
+
device_auth_id: string;
|
|
52
|
+
user_code: string;
|
|
53
|
+
interval: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function beginDeviceAuth(): Promise<{
|
|
56
|
+
deviceData: DeviceAuthInit;
|
|
57
|
+
url: string;
|
|
58
|
+
instructions: string;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function completeDeviceAuth(deviceData: DeviceAuthInit, signal?: AbortSignal): Promise<TokenResponse>;
|
|
61
|
+
/**
|
|
62
|
+
* Minimal shape of a fallback OAuth account for ingestion. Matches the
|
|
63
|
+
* fields used by upsertAccount and beginAccountLogin, compatible with
|
|
64
|
+
* the full OAuthAccount type in accounts.ts.
|
|
65
|
+
*/
|
|
66
|
+
export interface IngestAccount {
|
|
67
|
+
id: string;
|
|
68
|
+
label?: string;
|
|
69
|
+
type: 'oauth';
|
|
70
|
+
access?: string;
|
|
71
|
+
refresh: string;
|
|
72
|
+
expires?: number;
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
addedAt: number;
|
|
75
|
+
lastUsed: number;
|
|
76
|
+
/** Stable ChatGPT account identifier from the OAuth token claims. */
|
|
77
|
+
accountId?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface AccountStorageLike {
|
|
80
|
+
version: 1;
|
|
81
|
+
mainAccountId?: string;
|
|
82
|
+
accounts: IngestAccount[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Dedup by stable accountId first (strongest signal — same ChatGPT account
|
|
86
|
+
* added twice with different labels must merge), then by id, then by label.
|
|
87
|
+
* If found, merge-update preserving addedAt. Otherwise push.
|
|
88
|
+
* Re-running `add --label work` is idempotent.
|
|
89
|
+
*
|
|
90
|
+
* Accepts the accounts array directly (not the whole storage object) to
|
|
91
|
+
* avoid coupling to any particular storage shape.
|
|
92
|
+
*/
|
|
93
|
+
export declare function upsertAccount<T extends {
|
|
94
|
+
id: string;
|
|
95
|
+
label?: string;
|
|
96
|
+
accountId?: string;
|
|
97
|
+
addedAt?: number;
|
|
98
|
+
}>(accounts: T[], account: T): number;
|
|
99
|
+
export interface BeginAccountLoginOptions {
|
|
100
|
+
label?: string;
|
|
101
|
+
headless?: boolean;
|
|
102
|
+
signal?: AbortSignal;
|
|
103
|
+
}
|
|
104
|
+
export interface BeginAccountLoginResult {
|
|
105
|
+
url: string;
|
|
106
|
+
instructions: string;
|
|
107
|
+
/** Resolves after the user completes the OAuth flow with a ready-to-ingest account. */
|
|
108
|
+
completion: Promise<IngestAccount>;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Split-return OAuth flow entry point.
|
|
112
|
+
*
|
|
113
|
+
* Browser flow (default):
|
|
114
|
+
* 1. Start OAuth server, generate PKCE, build authorize URL
|
|
115
|
+
* 2. Return { url, instructions, completion } — url is ready immediately
|
|
116
|
+
* 3. completion resolves after browser callback + token exchange
|
|
117
|
+
*
|
|
118
|
+
* Headless flow:
|
|
119
|
+
* 1. Begin device auth
|
|
120
|
+
* 2. Return { url, instructions, completion }
|
|
121
|
+
* 3. completion polls device endpoint + exchanges for tokens
|
|
122
|
+
*
|
|
123
|
+
* The split return allows the TUI command to show the URL before the
|
|
124
|
+
* (potentially 30-60s) wait, avoiding a deadlock.
|
|
125
|
+
*/
|
|
126
|
+
export declare function beginAccountLogin(opts?: BeginAccountLoginOptions): Promise<BeginAccountLoginResult>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider injection seam — shared types and the 2 Codex provider fns.
|
|
3
|
+
*
|
|
4
|
+
* Both fns are constructor-injected into FallbackAccountManager and
|
|
5
|
+
* QuotaManager so the generic core never imports provider specifics
|
|
6
|
+
* directly.
|
|
7
|
+
*/
|
|
8
|
+
import type { OAuthQuotaSnapshot } from './accounts.ts';
|
|
9
|
+
export type QuotaWindowName = string;
|
|
10
|
+
export declare const PRIMARY: QuotaWindowName;
|
|
11
|
+
export declare const SECONDARY: QuotaWindowName;
|
|
12
|
+
export interface ProviderHttpError extends Error {
|
|
13
|
+
status?: number;
|
|
14
|
+
retryAfter?: number;
|
|
15
|
+
/** True when this error originated from a token-refresh call (not a quota fetch). */
|
|
16
|
+
isRefreshError?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export type ProviderRefreshFn = (input: {
|
|
19
|
+
refreshToken: string;
|
|
20
|
+
fetchImpl: typeof fetch;
|
|
21
|
+
now: () => number;
|
|
22
|
+
}) => Promise<{
|
|
23
|
+
access: string;
|
|
24
|
+
refresh: string;
|
|
25
|
+
expires: number;
|
|
26
|
+
expiresIn: number;
|
|
27
|
+
}>;
|
|
28
|
+
export type ProviderQuotaFn = (input: {
|
|
29
|
+
accessToken: string;
|
|
30
|
+
fetchImpl: typeof fetch;
|
|
31
|
+
now: () => number;
|
|
32
|
+
}) => Promise<OAuthQuotaSnapshot>;
|
|
33
|
+
export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
34
|
+
export declare const CODEX_ISSUER = "https://auth.openai.com";
|
|
35
|
+
/**
|
|
36
|
+
* Core Codex OAuth refresh — the single entry point for refreshing an
|
|
37
|
+
* OpenAI OAuth token. Used by both the legacy refreshAccessToken in
|
|
38
|
+
* index.ts AND the FallbackAccountManager (via constructor injection).
|
|
39
|
+
*
|
|
40
|
+
* Throws a ProviderHttpError carrying `.status` (from the HTTP response)
|
|
41
|
+
* and `.retryAfter` (parsed from Retry-After) so backoff machinery can
|
|
42
|
+
* duck-type them without instanceof checks.
|
|
43
|
+
*/
|
|
44
|
+
export declare function codexRefreshFn(input: {
|
|
45
|
+
refreshToken: string;
|
|
46
|
+
fetchImpl: typeof fetch;
|
|
47
|
+
now: () => number;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
access: string;
|
|
50
|
+
refresh: string;
|
|
51
|
+
expires: number;
|
|
52
|
+
expiresIn: number;
|
|
53
|
+
}>;
|
|
54
|
+
export declare function whamUsageFn(input: {
|
|
55
|
+
accessToken: string;
|
|
56
|
+
fetchImpl: typeof fetch;
|
|
57
|
+
now: () => number;
|
|
58
|
+
accountId?: string;
|
|
59
|
+
}): Promise<OAuthQuotaSnapshot>;
|