@cortexkit/opencode-openai-auth 0.8.0 → 0.9.1

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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/{index-3w4vmckx.js → index-4effjmr6.js} +2 -2
  3. package/dist/{index-507jh8bv.js → index-9ehhb051.js} +1 -1
  4. package/dist/index-g3hsdjh4.js +2 -0
  5. package/dist/index-hh52v8sb.js +2 -0
  6. package/dist/{index-944gsj1s.js → index-jwn4r3sw.js} +1 -1
  7. package/dist/{index-02h7g5f9.js → index-sdt2d2ra.js} +1 -1
  8. package/dist/index-sq36b120.js +211 -0
  9. package/dist/index.d.ts +172 -341
  10. package/dist/index.js +65 -65
  11. package/dist/internal-BTndPmW2.d.ts +914 -0
  12. package/dist/quota-normalize-08hpp51v.js +1 -0
  13. package/dist/rpc/port-file.js +1 -1
  14. package/dist/rpc/protocol.d.ts +1 -1
  15. package/dist/rpc/rpc-client.d.ts +2 -1
  16. package/dist/rpc/rpc-client.js +1 -1
  17. package/dist/rpc/rpc-dir.js +1 -1
  18. package/dist/sidebar-state-CoKYKcLn.d.ts +220 -0
  19. package/dist/sidebar-state.d.ts +2 -172
  20. package/dist/sidebar-state.js +1 -1
  21. package/dist/{token-jdvpwk60.js → token-9823y0pc.js} +1 -1
  22. package/dist/token-util-ypgz7rge.js +1 -0
  23. package/dist/tui-preferences.js +1 -1
  24. package/dist/tui.d.ts +27 -5
  25. package/package.json +3 -2
  26. package/src/tui-compiled/raw/core-custody-state.ts +243 -0
  27. package/src/tui-compiled/raw/sidebar-state.ts +168 -12
  28. package/src/tui-compiled/raw/tui-command-dialogs.tsx +89 -39
  29. package/src/tui-compiled/raw/tui.tsx +138 -18
  30. package/src/tui-compiled/runtime/core-custody-state.ts +243 -0
  31. package/src/tui-compiled/runtime/sidebar-state.ts +168 -12
  32. package/src/tui-compiled/runtime/tui-command-dialogs.tsx +70 -33
  33. package/src/tui-compiled/runtime/tui.tsx +241 -143
  34. package/src/tui-compiled/shared/logger.ts +9 -2
  35. package/dist/index-2xppkvkx.js +0 -2
  36. package/dist/index-nshbgkz1.js +0 -198
  37. package/dist/index-xyg86jgt.js +0 -2
  38. package/dist/protocol-Cv9ZmU0p.d.ts +0 -24
  39. package/dist/quota-normalize-wap0rne8.js +0 -1
  40. package/dist/token-util-zzm7paz9.js +0 -1
@@ -0,0 +1,914 @@
1
+ import { Endpoint } from "@cortexkit/subc-client";
2
+ //#region ../../node_modules/.bun/@cortexkit+claustrum-client@0.3.0/node_modules/@cortexkit/claustrum-client/dist/detect.d.ts
3
+ type ClaustrumEndpoint = Endpoint;
4
+ type ClaustrumDetection = {
5
+ status: 'available';
6
+ schema: number;
7
+ wireVersion: number;
8
+ endpoints: ClaustrumEndpoint[];
9
+ } | {
10
+ status: 'absent';
11
+ path: string;
12
+ } | {
13
+ status: 'malformed';
14
+ path: string;
15
+ reason: string;
16
+ };
17
+ declare function detectClaustrumConnection(explicitPath?: string): Promise<ClaustrumDetection>;
18
+ //#endregion
19
+ //#region ../../node_modules/.bun/@cortexkit+claustrum-client@0.3.0/node_modules/@cortexkit/claustrum-client/dist/handles.d.ts
20
+ type HandleAccount = {
21
+ label: string;
22
+ handle: string;
23
+ credential_id: string;
24
+ superseded?: string[];
25
+ minTtlMs?: number;
26
+ };
27
+ type HandleProvider = {
28
+ provider: string;
29
+ shape: 'api' | 'oauth';
30
+ serve: string;
31
+ accounts: HandleAccount[];
32
+ };
33
+ type OpenCodeHandleFileV1 = {
34
+ version: 1;
35
+ providers: HandleProvider[];
36
+ };
37
+ //#endregion
38
+ //#region ../../node_modules/.bun/@cortexkit+claustrum-client@0.3.0/node_modules/@cortexkit/claustrum-client/dist/manifest-lock.d.ts
39
+ type ManifestHandleFile = OpenCodeHandleFileV1;
40
+ //#endregion
41
+ //#region ../core/dist/provider.d.ts
42
+ type QuotaWindowName = string;
43
+ type ProviderQuotaFn = (input: {
44
+ accessToken: string;
45
+ fetchImpl: typeof fetch;
46
+ now: () => number;
47
+ }) => Promise<OAuthQuotaSnapshot>;
48
+ //#endregion
49
+ //#region ../core/dist/quota-manager.d.ts
50
+ type QuotaEntry = {
51
+ quota: OAuthQuotaSnapshot;
52
+ refreshAfter: number;
53
+ checkedAt: number;
54
+ accountId?: string;
55
+ };
56
+ type QuotaManagerOptions = {
57
+ storage: AccountStorage | null;
58
+ fetchImpl?: typeof fetch;
59
+ /**
60
+ * Account config file this manager's cross-process refresh locks are named
61
+ * after. Host-resolved and required, so two hosts pointing at different
62
+ * stores never contend on one another's lock.
63
+ */
64
+ configPath: string;
65
+ now?: () => number;
66
+ /** Injected quota-fetch function — replaces the hard-imported Anthropic fetchOAuthQuotaSnapshot */
67
+ fetchQuotaFn?: ProviderQuotaFn;
68
+ onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, fingerprint: string, fetchStartedAt: number) => void;
69
+ onApiError?: (error: AccountOperationError) => void;
70
+ };
71
+ declare class QuotaManager {
72
+ private main;
73
+ private mainTokenFp;
74
+ private mainQuotaAccountId;
75
+ private fallbacks;
76
+ private fallbackTokenFps;
77
+ private inflightMain;
78
+ private inflightMainFp;
79
+ private inflightFallbacks;
80
+ private mainLastApiError;
81
+ private fallbackApiErrors;
82
+ private fallbackErrorTokenFps;
83
+ private rateLimitedUntilMap;
84
+ private apiGate;
85
+ private lastApiCallAt;
86
+ private storage;
87
+ private readonly fetchImpl;
88
+ private readonly now;
89
+ private readonly fetchQuotaFn;
90
+ private readonly onMainQuotaFetched;
91
+ private readonly onApiError;
92
+ private readonly configPath;
93
+ constructor(opts: QuotaManagerOptions);
94
+ /**
95
+ * Cached main quota entry. Pass the live access token to enforce token
96
+ * binding: if the cached entry was produced by a different token (main
97
+ * account switched), it is dropped and null is returned so the caller
98
+ * refetches for the current account. Called without a token (e.g. for
99
+ * display) it returns whatever is cached.
100
+ */
101
+ getMain(accessToken?: string): QuotaEntry | null;
102
+ /**
103
+ * Cached fallback quota entry. Pass the live access token to enforce token
104
+ * binding: if the entry was produced by a different token (account re-login),
105
+ * it is dropped and null is returned so the caller refetches.
106
+ */
107
+ getFallback(accountId: string, accessToken?: string): QuotaEntry | null;
108
+ getAllFallbacks(): Map<string, QuotaEntry>;
109
+ setMain(accessToken: string, entry: QuotaEntry, accountId?: string, completeSnapshot?: boolean): void;
110
+ /**
111
+ * Non-invalidating read of the cached main quota for POLICY decisions
112
+ * (killswitch). Unlike getMain(token), a token mismatch does NOT drop the
113
+ * cache — an access-token refresh is a same-account event and must not turn a
114
+ * known-exhausted account into "unknown" (which would fail open and spend).
115
+ *
116
+ * Pass the live ChatGPT accountId to still drop on a genuine account SWITCH:
117
+ * if the cached entry's account identity is known and differs, return null so
118
+ * the killswitch treats it as unknown rather than judging account A by account
119
+ * B's quota. When identity is unknown on either side, the cached snapshot is
120
+ * returned (best-effort) — the same fail-open-on-unknown stance as elsewhere.
121
+ */
122
+ peekMainForPolicy(accountId?: string): QuotaEntry | null;
123
+ /**
124
+ * Non-invalidating read of a cached fallback quota for POLICY decisions
125
+ * (killswitch, admission). Keyed by the stable internal account id, so a
126
+ * token refresh for the same account does not drop it (getFallback(id, token)
127
+ * would).
128
+ *
129
+ * Pass the live ChatGPT identity to still drop on a genuine re-login: if the
130
+ * cached entry's identity is known and differs, return null so the previous
131
+ * identity's (possibly exhausted) quota is never judged against its
132
+ * replacement. Unknown on either side fails open, matching peekMainForPolicy.
133
+ */
134
+ peekFallbackForPolicy(accountId: string, identity?: string): QuotaEntry | null;
135
+ setFallback(accountId: string, entry: QuotaEntry, accessToken?: string, completeSnapshot?: boolean, identity?: string): void;
136
+ /**
137
+ * Mark accountId ('main' or a fallback id) rate-limited until resetAtMs.
138
+ * Keeps the LATER reset: skips only when an existing mark is strictly
139
+ * later than resetAtMs, so an equal-reset re-mark still applies (idempotent)
140
+ * instead of being silently dropped by a stale `>=` comparison.
141
+ */
142
+ markRateLimited(accountId: string, resetAtMs: number): void;
143
+ /** True iff accountId has a mark whose reset has not yet passed. */
144
+ isRateLimited(accountId: string): boolean;
145
+ /**
146
+ * Raw stored reset for accountId's mark, or undefined if none. Unlike
147
+ * isRateLimited, this does NOT apply read-time expiry — callers on the
148
+ * block path already know the mark is live (isRateLimited just returned
149
+ * true) and want its exact reset estimate for a Retry-After computation.
150
+ */
151
+ rateLimitedUntil(accountId: string): number | undefined;
152
+ /** Explicit early clear — read-time expiry in isRateLimited is primary. */
153
+ clearRateLimited(accountId: string): void;
154
+ refreshMain(accessToken: string): Promise<OAuthQuotaSnapshot>;
155
+ refreshFallback(accountId: string, accessToken: string): Promise<OAuthQuotaSnapshot>;
156
+ refreshAllFallbacks(accounts: OAuthAccount[]): Promise<void>;
157
+ /**
158
+ * Fire-and-forget refresh. Does not await, swallows errors.
159
+ */
160
+ refreshMainInBackground(accessToken: string): void;
161
+ isMainStale(): boolean;
162
+ isFallbackStale(accountId: string, accessToken?: string): boolean;
163
+ shouldRefreshOnRequestCount(requestCount: number): boolean;
164
+ /**
165
+ * Combined check: should a refresh happen right now?
166
+ * True if main is stale by time OR triggered by request count.
167
+ */
168
+ needsRefresh(requestCount: number): boolean;
169
+ updateStorage(storage: AccountStorage | null): void;
170
+ /**
171
+ * Seed/update the main quota cache from persisted state. This is deliberately
172
+ * callable after every disk load so another plugin process's fresh quota write
173
+ * can stop this process from showing "checking…" or making a redundant quota
174
+ * API call.
175
+ */
176
+ seedMainFromStorage(storage: AccountStorage | null, accessToken?: string): void;
177
+ private seedMainBackoffFromStorage;
178
+ /**
179
+ * Seed fallback cache entries from persisted account.quota data.
180
+ * Updates older in-memory entries so a fresh quota write from another plugin
181
+ * process prevents redundant checks and stale sidebar writes.
182
+ */
183
+ seedFallbacksFromAccounts(accounts: OAuthAccount[]): void;
184
+ /**
185
+ * Whether the MAIN quota API is currently in backoff. Scoped to the main
186
+ * account — a fallback account's 429 never reports here.
187
+ */
188
+ isBackedOff(): boolean;
189
+ /**
190
+ * Whether a specific fallback account's quota API is in backoff.
191
+ */
192
+ isFallbackBackedOff(accountId: string, accessToken?: string): boolean;
193
+ getLastApiError(): AccountOperationError | undefined;
194
+ /** Minimum gap between consecutive quota API calls (ms). */
195
+ private static readonly API_CALL_GAP_MS;
196
+ private static fallbackInflightKey;
197
+ private static quotaLockName;
198
+ /**
199
+ * Serialize API calls through a shared gate so only one
200
+ * quota API request runs at a time, with a minimum gap
201
+ * between calls. Prevents concurrent and rapid-fire calls
202
+ * from triggering rate limits.
203
+ */
204
+ private _enqueueApiFetch;
205
+ private _fetchMain;
206
+ private _fetchFallback;
207
+ /** Route through injected fetchQuotaFn, or throw if unset. */
208
+ private _fetchQuota;
209
+ private static isAuthError;
210
+ /** Main quota failure: arms main-only backoff and persists via onApiError. */
211
+ private _handleMainFetchError;
212
+ /**
213
+ * Fallback quota failure: arms backoff for THIS account only. Never touches
214
+ * main backoff state and never calls onApiError (which persists the main
215
+ * quota error) — the per-account error is recorded by the caller via the
216
+ * account's lastQuotaRefreshError.
217
+ */
218
+ private _handleFallbackFetchError;
219
+ }
220
+ //#endregion
221
+ //#region ../core/dist/custody-manifest.d.ts
222
+ type CustodyManifestReadResult = {
223
+ ok: true;
224
+ value: ManifestHandleFile;
225
+ revision: string;
226
+ } | {
227
+ ok: false;
228
+ reason: 'absent';
229
+ } | {
230
+ ok: false;
231
+ reason: 'tooLarge';
232
+ message: string;
233
+ } | {
234
+ ok: false;
235
+ reason: 'permissions' | 'unsafeParent' | 'notRegular' | 'symlink';
236
+ message: string;
237
+ } | {
238
+ ok: false;
239
+ reason: 'invalid';
240
+ message: string;
241
+ } | {
242
+ ok: false;
243
+ reason: 'unreadable';
244
+ message: string;
245
+ };
246
+ type CustodyManifestIo = {
247
+ lstat?: (path: string) => Promise<CustodyStat>;
248
+ stat?: (path: string) => Promise<CustodyStat>;
249
+ readFile?: (path: string, encoding: 'utf8') => Promise<string>;
250
+ open?: (path: string) => Promise<CustodyDescriptor>;
251
+ currentUid?: () => number | undefined;
252
+ };
253
+ type CustodyStat = {
254
+ isFile(): boolean;
255
+ isDirectory?(): boolean;
256
+ isSymbolicLink?(): boolean;
257
+ mode: number;
258
+ size?: number;
259
+ uid?: number;
260
+ mtimeMs?: number;
261
+ };
262
+ type CustodyDescriptor = {
263
+ stat(): Promise<CustodyStat>;
264
+ read(options: {
265
+ buffer: Buffer;
266
+ offset: number;
267
+ length: number;
268
+ position: number;
269
+ }): Promise<{
270
+ bytesRead: number;
271
+ }>;
272
+ close(): Promise<void>;
273
+ };
274
+ //#endregion
275
+ //#region ../core/dist/refresh-file-lock.d.ts
276
+ declare function acquireRefreshFileLock(options: {
277
+ name: string;
278
+ ttlMs: number;
279
+ /**
280
+ * File the lock is named after. Required: the lock and the write it guards
281
+ * must target the same file, and a default resolved in here could only ever
282
+ * be one host's path.
283
+ */
284
+ path: string;
285
+ now?: () => number;
286
+ renew?: boolean;
287
+ renewIntervalMs?: number;
288
+ onStep?: (step: 'stale-marker-stat' | 'stale-marker-claimed' | 'stale-lock-confirmed' | 'eviction-marker-acquired' | 'renewal-owner-confirmed' | 'renewal-marker-unavailable' | 'renewal-write-fenced' | 'renewal-write-ready' | 'relinquish-read' | 'renewal-finished' | 'release-owner-confirmed') => void | Promise<void>;
289
+ }): Promise<{
290
+ release: () => Promise<void>;
291
+ } | null>;
292
+ //#endregion
293
+ //#region ../core/dist/custody.d.ts
294
+ type ClaustrumMode = 'local' | 'claustrum';
295
+ type CustodyTransitionState = {
296
+ manifestRevision: string;
297
+ storeGeneration: string;
298
+ fingerprints: {
299
+ main?: string;
300
+ fallbacks: Record<string, string>;
301
+ };
302
+ };
303
+ declare const CUSTODY_REFUSE: unique symbol;
304
+ declare const CUSTODY_EXCLUDED: unique symbol;
305
+ type VaultProvenance = {
306
+ handle: string;
307
+ recordVersion: number;
308
+ };
309
+ type FallbackAccessResolution = {
310
+ token: string;
311
+ provenance: 'local';
312
+ } | {
313
+ token: string;
314
+ provenance: VaultProvenance;
315
+ };
316
+ type ResolveFallbackAccessOptions = {
317
+ cache?: ClaustrumCredentialCache;
318
+ manifestHandle?: string;
319
+ completeEnrollmentDeps?: CompleteEnrollmentDeps;
320
+ refreshBeforeExpiryMs?: number;
321
+ now?: () => number;
322
+ requestPath?: boolean;
323
+ };
324
+ declare function resolveFallbackAccess(account: OAuthAccount, storage: Pick<AccountStorage, 'claustrum'>, manifest?: CustodyManifestReadResult, options?: ResolveFallbackAccessOptions): Promise<FallbackAccessResolution | typeof CUSTODY_REFUSE | typeof CUSTODY_EXCLUDED>;
325
+ type ClaustrumCredentialCacheOptions = {
326
+ connector: (options: {
327
+ connectionFile: string;
328
+ handshakeTimeoutMs?: number;
329
+ }) => Promise<ClaustrumCacheTransport>;
330
+ now?: () => number;
331
+ };
332
+ type ClaustrumCacheTransport = {
333
+ getCredential(handle: string, minTtlMs?: number): Promise<{
334
+ material: string;
335
+ recordVersion: number;
336
+ expiresAtMs: number | null;
337
+ accountId?: string;
338
+ }>;
339
+ statusCredential(handle: string): Promise<{
340
+ ready: boolean;
341
+ lastErrorCode: string | null;
342
+ leaseHeld: boolean;
343
+ recordVersion: number;
344
+ }>;
345
+ reportAuthFailure(params: {
346
+ handle: string;
347
+ providerStatus: number;
348
+ recordVersion: number;
349
+ reporterSource: 'direct' | 'relay_status_field' | 'relay_message_parse';
350
+ }): Promise<void>;
351
+ close(): void;
352
+ };
353
+ type ResidentRecord = {
354
+ payload: {
355
+ access: string;
356
+ };
357
+ recordVersion: number;
358
+ expiresAtMs: number;
359
+ servedAccountId?: string;
360
+ };
361
+ declare class ClaustrumCredentialCache {
362
+ #private;
363
+ constructor(options: ClaustrumCredentialCacheOptions);
364
+ /**
365
+ * Peek the resident record without I/O. Returns `undefined` if absent,
366
+ * including the case where the record was invalidated by `reportAuthFailure`.
367
+ */
368
+ peek(handle: string): Promise<ResidentRecord | undefined>;
369
+ /**
370
+ * Public read-only view of a peek result. Exposes only the version and
371
+ * expiry — never the credential material — so a projection layer can
372
+ * surface a vault `recordVersion` without ever seeing a token.
373
+ */
374
+ peekMetadata(handle: string): Promise<{
375
+ recordVersion: number;
376
+ expiresAtMs: number;
377
+ } | undefined>;
378
+ /**
379
+ * True iff a recent auth failure on this handle has been reported to the
380
+ * daemon. The blocked set is informational; the resolver still attempts a
381
+ * fetch on `get`, which clears the flag on success.
382
+ */
383
+ isBlocked(handle: string): boolean;
384
+ /**
385
+ * True iff the bound-and-reauth fence has fired for this handle and the
386
+ * reauth window has not yet elapsed. Callers that project "needs reauth"
387
+ * UI should gate on this. The fence clears after a proven 2xx vault request
388
+ * or when the reauth window expires.
389
+ */
390
+ isReauth(handle: string, now?: number): boolean;
391
+ markVaultSuccess(handle: string): void;
392
+ /**
393
+ * Get the live resident record. If a live record exists and the caller
394
+ * did not pass `force:true`, it returns immediately (no daemon I/O).
395
+ * Otherwise it issues a single-flight `getCredential` call.
396
+ *
397
+ * `force:true` ALWAYS issues a new daemon call (it bypasses both the
398
+ * resident record and any pending in-flight that originated from a
399
+ * non-force caller). Two concurrent force:true calls share the same
400
+ * new in-flight promise, so the second and subsequent force:true verifies
401
+ * collapse onto the first one. A non-force caller issued while a
402
+ * force:true fetch is pending still joins that force-fetch in-flight —
403
+ * the cache never races two parallel daemon calls for the same handle.
404
+ */
405
+ get(handle: string, minTtlMs?: number, options?: {
406
+ force?: boolean;
407
+ }): Promise<ResidentRecord>;
408
+ /**
409
+ * Report an upstream auth failure on a served record. The report is
410
+ * version-fenced (one report per handle per recordVersion), and after
411
+ * two reports on the same version a one-hour bound fires — the third
412
+ * and subsequent reports on the same version are suppressed, and the
413
+ * handle is moved into the `reauth` set for an hour. A proven 2xx vault
414
+ * request or expiry of that window clears the bound and reauth entry.
415
+ *
416
+ * The reported version is invalidated from the resident record in
417
+ * `finally`, so a follow-up `get` cannot accidentally serve the same
418
+ * version the daemon already rejected.
419
+ */
420
+ reportAuthFailure(params: {
421
+ handle: string;
422
+ providerStatus: number;
423
+ recordVersion: number;
424
+ }): Promise<void>;
425
+ close(): void;
426
+ }
427
+ type CompleteEnrollmentDeps = {
428
+ loadAccounts: typeof loadAccounts;
429
+ readCustodyManifest: (path?: string) => Promise<CustodyManifestReadResult>;
430
+ acquireRefreshFileLock: typeof acquireRefreshFileLock;
431
+ configPath: string;
432
+ paths: AccountPaths;
433
+ manifestPath?: string;
434
+ cache: ClaustrumCredentialCache;
435
+ /** Minimum TTL (ms) handed to the vault for the verify-get. */
436
+ minTtlMs: number;
437
+ /**
438
+ * Read-modify-write the account store. The sweep calls this once on a
439
+ * successful verify, setting empty `access`, sentinel `refresh`, and
440
+ * `expires = 0` — that single write is the only durable effect.
441
+ */
442
+ mutateAccounts: typeof mutateAccounts;
443
+ /** Provider name; defaults to the owning provider (openai). */
444
+ provider?: string;
445
+ /** Caller-supplied clock so tests can drive the boot/hour log dedupe. */
446
+ now?: () => number;
447
+ };
448
+ //#endregion
449
+ //#region ../core/dist/paths.d.ts
450
+ /**
451
+ * The config file and the runtime-state file a store operation reads and writes.
452
+ *
453
+ * Both are required. A store function never derives one from the other, because
454
+ * a host may point its state file somewhere the derivation would not reach and
455
+ * a silently derived path would lock one file while writing another.
456
+ */
457
+ interface AccountPaths {
458
+ configPath: string;
459
+ statePath: string;
460
+ }
461
+ //#endregion
462
+ //#region ../core/dist/accounts.d.ts
463
+ type AccountQuotaWindow = {
464
+ usedPercent: number;
465
+ remainingPercent: number;
466
+ resetsAt?: string;
467
+ checkedAt: number;
468
+ windowMinutes?: number;
469
+ };
470
+ interface OAuthQuotaSnapshot {
471
+ primary?: AccountQuotaWindow;
472
+ secondary?: AccountQuotaWindow;
473
+ resetCreditsAvailable?: number;
474
+ resetCreditsApplicable?: number;
475
+ spendControl?: OAuthSpendControlReading;
476
+ /**
477
+ * Set when the source explicitly reported that the account has no credit
478
+ * budget (wham's `spend_control` or `individual_limit` is null). An absent
479
+ * `spendControl` alone only means the source did not carry the field: header
480
+ * and WebSocket pushes never do, so merges keep the previous budget for them.
481
+ * This marker is what lets a merge drop a budget that no longer exists.
482
+ */
483
+ spendControlCleared?: true;
484
+ credits?: OAuthCredits;
485
+ }
486
+ interface OAuthSpendControlReading {
487
+ limit: number;
488
+ used: number;
489
+ remaining: number;
490
+ usedPercent: number;
491
+ remainingPercent: number;
492
+ resetsAt?: string;
493
+ unit?: string;
494
+ source?: string;
495
+ reached: boolean;
496
+ }
497
+ interface OAuthCredits {
498
+ hasCredits: boolean;
499
+ unlimited: boolean;
500
+ overageLimitReached: boolean;
501
+ balance?: number;
502
+ }
503
+ type AccountBase = {
504
+ id: string;
505
+ label?: string;
506
+ enabled?: boolean;
507
+ addedAt?: number;
508
+ lastUsed?: number;
509
+ /** Stable ChatGPT account identifier extracted from the OAuth token claims. */
510
+ accountId?: string;
511
+ };
512
+ type AccountOperationError = {
513
+ message: string;
514
+ checkedAt: number;
515
+ nextRetryAt?: number;
516
+ retryCount?: number;
517
+ tokenHash?: string;
518
+ };
519
+ type OAuthAccount = AccountBase & {
520
+ type: 'oauth';
521
+ corrupt?: false;
522
+ access?: string;
523
+ refresh: string;
524
+ expires?: number;
525
+ lastRefreshedAt?: number;
526
+ lastRefreshError?: AccountOperationError;
527
+ lastQuotaRefreshError?: AccountOperationError;
528
+ quota?: OAuthQuotaSnapshot;
529
+ };
530
+ type CorruptOAuthAccount = AccountBase & {
531
+ type: 'oauth';
532
+ corrupt: true;
533
+ access?: undefined;
534
+ refresh?: undefined;
535
+ expires?: undefined;
536
+ lastRefreshedAt?: undefined;
537
+ lastRefreshError?: undefined;
538
+ lastQuotaRefreshError?: undefined;
539
+ quota?: undefined;
540
+ };
541
+ type ApiKeyAccount = AccountBase & {
542
+ type: 'api';
543
+ apiKey?: string;
544
+ baseURL: string;
545
+ authHeader?: 'authorization-bearer' | 'x-api-key';
546
+ };
547
+ type FallbackAccount = OAuthAccount | CorruptOAuthAccount | ApiKeyAccount;
548
+ type RoutingMode = 'main-first' | 'fallback-first' | 'sticky-balanced';
549
+ type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
550
+ type KillswitchConfig = {
551
+ enabled?: boolean;
552
+ main?: KillswitchThresholds;
553
+ accounts?: Record<string, KillswitchThresholds>;
554
+ };
555
+ interface ResetInFlight {
556
+ redeemRequestId: string;
557
+ creditId: string;
558
+ startedAt: number;
559
+ }
560
+ interface ResetLastOutcome {
561
+ code: string;
562
+ at: number;
563
+ previousOutcome?: {
564
+ code: string;
565
+ at: number;
566
+ };
567
+ }
568
+ interface ResetAccountState {
569
+ inFlight?: ResetInFlight | Record<string, unknown>;
570
+ lastOutcome?: ResetLastOutcome;
571
+ cooldownUntil?: number;
572
+ }
573
+ type ResetStateByAccount = Record<string, ResetAccountState>;
574
+ type AccountStorage = {
575
+ version: 1;
576
+ main?: {
577
+ type: 'opencode';
578
+ provider: 'openai';
579
+ };
580
+ routing?: {
581
+ mode?: RoutingMode;
582
+ };
583
+ fallbackOn?: number[];
584
+ refresh?: {
585
+ enabled?: boolean;
586
+ refreshBeforeExpiryMinutes?: number;
587
+ mainLastRefreshError?: AccountOperationError;
588
+ mainRefreshLeaseId?: string;
589
+ mainRefreshLeaseUntil?: number;
590
+ mainRefreshLeaseTokenHash?: string;
591
+ };
592
+ quota?: {
593
+ enabled?: boolean;
594
+ checkIntervalMinutes?: number;
595
+ refreshEveryNRequests?: number;
596
+ minimumRemaining?: Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
597
+ failClosedOnUnknownQuota?: boolean;
598
+ mainQuota?: OAuthQuotaSnapshot;
599
+ mainQuotaCheckedAt?: number;
600
+ mainQuotaToken?: string;
601
+ mainLastQuotaApiError?: AccountOperationError;
602
+ };
603
+ reset?: ResetStateByAccount;
604
+ dump?: {
605
+ enabled?: boolean;
606
+ };
607
+ costZeroing?: {
608
+ enabled?: boolean;
609
+ };
610
+ killswitch?: KillswitchConfig;
611
+ logging?: {
612
+ level?: string;
613
+ };
614
+ cachekeep?: {
615
+ enabled?: boolean;
616
+ subagents?: boolean;
617
+ sustain?: boolean;
618
+ /** Clock-hour window start (0-23, inclusive) — keeps cachekeep idle warming
619
+ * inside `[startHour, endHour)` local hours. Omit to warm unconditionally. */
620
+ startHour?: number;
621
+ /** Clock-hour window end (0-23, exclusive) — must differ from startHour
622
+ * to be honored; an unset or equal hour falls back to "always warm". */
623
+ endHour?: number;
624
+ };
625
+ /** Stable ChatGPT account identifier of the main account (extracted from OAuth token). */
626
+ mainAccountId?: string;
627
+ claustrum?: {
628
+ mode?: ClaustrumMode;
629
+ transition?: CustodyTransitionState;
630
+ rowHistory?: string[];
631
+ };
632
+ accounts: FallbackAccount[];
633
+ };
634
+ declare function loadAccounts(paths: AccountPaths): Promise<AccountStorage | null>;
635
+ type AccountStoreTransaction = {
636
+ read(): Promise<AccountStorage>;
637
+ write(storage: AccountStorage): Promise<void>;
638
+ writeMode(mode: ClaustrumMode, transition?: CustodyTransitionState): Promise<void>;
639
+ };
640
+ declare function withAccountStoreTransaction<T>(action: (transaction: AccountStoreTransaction) => Promise<T>, paths: AccountPaths): Promise<T>;
641
+ /**
642
+ * Read-modify-write the account store atomically under the save lock.
643
+ *
644
+ * Unlike saveAccounts (which UNION-merges the incoming accounts with the latest
645
+ * on-disk set so concurrent ADDS from another process are never lost), this
646
+ * reads the freshest state under the lock and writes the mutator's result
647
+ * AUTHORITATIVELY — no union. That is required for structural edits (remove,
648
+ * reorder): a union cannot express a deletion (the removed id reappears from
649
+ * `latest`) or a reordering (the union is seeded latest-first). Because the
650
+ * mutator runs against freshly-read state under the lock, an add committed by a
651
+ * concurrent process is still visible to it and preserved.
652
+ *
653
+ * The mutator may edit `current` in place and return it, or return a new
654
+ * storage object. Returning undefined means "no change" and still rewrites the
655
+ * freshly-read state (a harmless idempotent write).
656
+ *
657
+ * Load-time drop preservation: if normalizeAccount (called inside
658
+ * normalizeStorage) rejects an account whose id IS in the raw config roster,
659
+ * the previous behavior would erase that id silently on the next write. This
660
+ * function now carries the dropped raw entry through to the written config
661
+ * verbatim, so the on-disk state always matches the operator's intent (an
662
+ * account they added, even if temporarily un-loadable, stays in their list
663
+ * until they deliberately remove it).
664
+ *
665
+ * Removal seam: when the caller knows they are removing an id (e.g. the CLI
666
+ * `remove` command) and that id may be load-dropped — in which case the
667
+ * mutator cannot find it in `current.accounts` to splice it — the caller can
668
+ * pass `options.allowDrop: [id]`. Ids in `allowDrop` are NOT preserved; the
669
+ * mutator's splice still no-ops on a dropped id, but the absence of
670
+ * preservation completes the removal end-to-end.
671
+ */
672
+ interface MutateAccountsContext {
673
+ /** Valid string ids read from the raw config while both store locks are held. */
674
+ rawRosterIds: readonly string[];
675
+ /** Ids retained after normalization and validation, before the mutator ran. */
676
+ loadedRosterIds: readonly string[];
677
+ }
678
+ declare function mutateAccounts(mutate: (current: AccountStorage, context?: MutateAccountsContext) => AccountStorage | undefined, paths: AccountPaths, options?: {
679
+ allowDrop?: readonly string[];
680
+ }): Promise<AccountStorage>;
681
+ //#endregion
682
+ //#region ../core/dist/oauth.d.ts
683
+ interface IdTokenClaims {
684
+ chatgpt_account_id?: string;
685
+ email?: string;
686
+ organizations?: Array<{
687
+ id: string;
688
+ }>;
689
+ 'https://api.openai.com/auth'?: {
690
+ chatgpt_account_id?: string;
691
+ };
692
+ }
693
+ declare function parseJwtClaims(token: string): IdTokenClaims | undefined;
694
+ declare function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined;
695
+ interface TokenResponse {
696
+ id_token: string;
697
+ access_token: string;
698
+ refresh_token: string;
699
+ expires_in?: number;
700
+ }
701
+ /**
702
+ * Minimal shape of a fallback OAuth account for ingestion. Matches the
703
+ * fields used by upsertAccount and beginAccountLogin, compatible with
704
+ * the full OAuthAccount type in accounts.ts.
705
+ */
706
+ interface IngestAccount {
707
+ id: string;
708
+ label?: string;
709
+ type: 'oauth';
710
+ access?: string;
711
+ refresh: string;
712
+ expires?: number;
713
+ enabled: boolean;
714
+ addedAt: number;
715
+ lastUsed: number;
716
+ /**
717
+ * When the token was obtained. Stamped at login so a freshly added account
718
+ * carries a refresh marker — without it, the runtime-state merge cannot tell a
719
+ * rotated token from a stale one (both default to 0) and a concurrent stale
720
+ * save could roll the token back.
721
+ */
722
+ lastRefreshedAt?: number;
723
+ /** Stable ChatGPT account identifier from the OAuth token claims. */
724
+ accountId?: string;
725
+ }
726
+ interface BeginAccountLoginOptions {
727
+ label?: string;
728
+ headless?: boolean;
729
+ signal?: AbortSignal;
730
+ /** Host package version, sent as the version half of the `User-Agent`. */
731
+ version: string;
732
+ }
733
+ interface BeginAccountLoginResult {
734
+ url: string;
735
+ instructions: string;
736
+ /** Resolves after the user completes the OAuth flow with a ready-to-ingest account. */
737
+ completion: Promise<IngestAccount>;
738
+ }
739
+ /**
740
+ * Split-return OAuth flow entry point.
741
+ *
742
+ * Browser flow (default):
743
+ * 1. Start OAuth server, generate PKCE, build authorize URL
744
+ * 2. Return { url, instructions, completion } — url is ready immediately
745
+ * 3. completion resolves after browser callback + token exchange
746
+ *
747
+ * Headless flow:
748
+ * 1. Begin device auth
749
+ * 2. Return { url, instructions, completion }
750
+ * 3. completion polls device endpoint + exchanges for tokens
751
+ *
752
+ * The split return allows the TUI command to show the URL before the
753
+ * (potentially 30-60s) wait, avoiding a deadlock.
754
+ */
755
+ declare function beginAccountLogin(opts: BeginAccountLoginOptions): Promise<BeginAccountLoginResult>;
756
+ //#endregion
757
+ //#region ../core/dist/protocol.d.ts
758
+ type CommandModalName = 'openai-quota' | 'openai-account' | 'openai-routing' | 'openai-killswitch' | 'openai-dump' | 'openai-logging' | 'openai-cachekeep' | 'openai-reset';
759
+ interface OpenDialogPayload {
760
+ command: CommandModalName;
761
+ text: string;
762
+ knobs: Record<string, unknown>;
763
+ }
764
+ interface RpcNotification {
765
+ id: number;
766
+ type: 'open-dialog';
767
+ payload: OpenDialogPayload;
768
+ sessionId?: string;
769
+ }
770
+ interface ApplyRequest {
771
+ command: CommandModalName;
772
+ arguments: string;
773
+ sessionId?: string;
774
+ }
775
+ interface ApplyResult {
776
+ text: string;
777
+ knobs: Record<string, unknown>;
778
+ }
779
+ //#endregion
780
+ //#region ../core/dist/refresh-all-quota.d.ts
781
+ interface RefreshAllQuotaResult {
782
+ account: string;
783
+ ok: boolean;
784
+ error?: string;
785
+ /**
786
+ * The account cannot recover on its own: the provider has rejected its
787
+ * credentials, so no amount of retrying will help until the operator re-adds
788
+ * it.
789
+ *
790
+ * Carried as a flag rather than left for the caller to infer from `error`,
791
+ * because sniffing an error string for a user-facing decision breaks the
792
+ * moment the wording changes.
793
+ */
794
+ permanent?: boolean;
795
+ }
796
+ //#endregion
797
+ //#region ../core/dist/commands.d.ts
798
+ /**
799
+ * The prompt-cache manager, as the commands use it.
800
+ *
801
+ * Declared structurally rather than imported: the manager itself is tied to one
802
+ * host's live request loader and stays there. Only the members reached through
803
+ * this field appear here — a status snapshot, the two lifecycle calls the
804
+ * cachekeep command makes, and the per-session drop the host performs when a
805
+ * session ends.
806
+ */
807
+ interface CacheKeepManager {
808
+ status(): {
809
+ running: boolean;
810
+ sustain: boolean;
811
+ window?: {
812
+ startHour: number;
813
+ endHour: number;
814
+ } | undefined;
815
+ tracked: number;
816
+ generatedAt: number;
817
+ ttlMs: number;
818
+ leadMs: number;
819
+ maxIdleWarmMs: number;
820
+ maxSubagentIdleMs: number;
821
+ targets: ReadonlyArray<{
822
+ sessionKey: string;
823
+ accountId?: string;
824
+ cacheExpiresAt: number;
825
+ lastWarmedAt?: number;
826
+ backoffUntil?: number;
827
+ }>;
828
+ };
829
+ start(): void;
830
+ stop(): void;
831
+ remove(sessionKey: string): void;
832
+ }
833
+ interface CommandContext {
834
+ accountStoragePath: string;
835
+ /**
836
+ * Runtime-state file that goes with `accountStoragePath`. Required, and
837
+ * resolved by the host: nothing here derives one path from the other.
838
+ */
839
+ accountStatePath: string;
840
+ /** Host package version, sent as the version half of the OAuth `User-Agent`. */
841
+ packageVersion: string;
842
+ quotaManager: QuotaManager;
843
+ loadAccounts: typeof loadAccounts;
844
+ client: {
845
+ auth: {
846
+ set: (input: {
847
+ path: {
848
+ id: string;
849
+ };
850
+ body: {
851
+ type: string;
852
+ access?: string;
853
+ refresh: string;
854
+ expires?: number;
855
+ };
856
+ }) => Promise<unknown>;
857
+ };
858
+ };
859
+ /** Session ID for pushNotification delivery. */
860
+ sessionId?: string;
861
+ /** If set, pushNotification is wired up and can deliver feedback to the user. */
862
+ notify?: (payload: OpenDialogPayload) => void;
863
+ /** Refresh the sidebar-state file so the TUI modal shows current data. */
864
+ refreshSidebar?: () => Promise<void>;
865
+ /** Actively poll wham/usage for all accounts (main + fallbacks). */
866
+ refreshAllQuota?: () => Promise<RefreshAllQuotaResult[]>;
867
+ /** Prompt-cache cachekeep manager. Set when the command is wired. */
868
+ cacheKeepManager?: CacheKeepManager | null;
869
+ /** Updates the live loader's persisted-enabled cachekeep gate. */
870
+ setCacheKeepEnabled?: (enabled: boolean) => void;
871
+ /** Updates the live loader's persisted-subagent cachekeep gate. */
872
+ setCacheKeepSubagents?: (enabled: boolean) => void;
873
+ /** Updates the live loader's main-agent idle-cap bypass gate. */
874
+ setCacheKeepSustain?: (enabled: boolean) => void;
875
+ /** Updates the live loader's clock-hour warm window. undefined = no window. */
876
+ setCacheKeepWindow?: (window: {
877
+ startHour: number;
878
+ endHour: number;
879
+ } | undefined) => void;
880
+ /** Clears only the sticky account assignment for one OpenCode session. */
881
+ clearStickyRouting?: (sessionId: string) => Promise<boolean>;
882
+ /** Resolves the current session's usable sticky account, if one exists. */
883
+ getStickyRouting?: (sessionId: string) => Promise<string | undefined>;
884
+ resolveResetTarget?: (accountKey: string) => Promise<ResetTargetIdentity>;
885
+ fetchImpl?: typeof fetch;
886
+ now?: () => number;
887
+ randomUUID?: () => string;
888
+ /** Starts an OAuth account-add flow; injected by the runtime boundary. */
889
+ beginAccountLogin?: typeof beginAccountLogin;
890
+ refreshResetTargetQuota?: (accountKey: string) => Promise<RefreshAllQuotaResult>;
891
+ enterClaustrumMode?: () => Promise<{
892
+ status: 'completed' | 'incomplete' | 'aborted';
893
+ outcomes: Record<string, string>;
894
+ reason?: string;
895
+ }>;
896
+ leaveClaustrumMode?: () => Promise<void>;
897
+ withFallbackAccountLock?: <T>(accountId: string, action: () => Promise<T>) => Promise<T>;
898
+ checkUsableCustodyBinding?: (account: OAuthAccount) => Promise<{
899
+ ready: true;
900
+ accountId: string;
901
+ } | {
902
+ ready: false;
903
+ reason: string;
904
+ }>;
905
+ }
906
+ interface ResetTargetIdentity {
907
+ accountKey: string;
908
+ label: string;
909
+ accessToken: string;
910
+ chatgptAccountId?: string;
911
+ onAuthFailure?: (status: number) => Promise<void>;
912
+ }
913
+ //#endregion
914
+ export { CustodyManifestIo as C, detectClaustrumConnection as E, acquireRefreshFileLock as S, QuotaManager as T, loadAccounts as _, CommandModalName as a, ClaustrumCredentialCache as b, IdTokenClaims as c, parseJwtClaims as d, AccountStorage as f, OAuthQuotaSnapshot as g, OAuthAccount as h, ApplyResult as i, TokenResponse as l, FallbackAccount as m, ResetTargetIdentity as n, OpenDialogPayload as o, AccountStoreTransaction as p, ApplyRequest as r, RpcNotification as s, CommandContext as t, extractAccountIdFromClaims as u, mutateAccounts as v, CustodyManifestReadResult as w, resolveFallbackAccess as x, withAccountStoreTransaction as y };