@cortexkit/opencode-openai-auth 1.0.5 → 1.1.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 (102) hide show
  1. package/dist/bin.js +9 -8
  2. package/dist/{pool-lifecycle-C_6YRmsh.d.ts → codex-request-BXDXToyt.d.ts} +158 -27
  3. package/dist/index-0yh85bwb.js +69 -0
  4. package/dist/{index-826s1yxa.js → index-4c8yw54k.js} +2 -2
  5. package/dist/{index-y4gxyxqd.js → index-7c6sxtd6.js} +2 -2
  6. package/dist/index-7eb3dw37.js +90 -0
  7. package/dist/index-8re8rprk.js +2 -0
  8. package/dist/{index-66asrje2.js → index-csky2xpg.js} +1 -1
  9. package/dist/index-edbdscy7.js +65 -0
  10. package/dist/{index-pg305s90.js → index-f2fmb596.js} +1 -1
  11. package/dist/{index-fsxbncpw.js → index-j8v2xwq6.js} +1 -1
  12. package/dist/{index-fsky1rsh.js → index-k6cydkgt.js} +2 -2
  13. package/dist/{index-116q849r.js → index-m5qmvr6c.js} +2 -2
  14. package/dist/index-msx2m78k.js +8 -0
  15. package/dist/{index-nx2qh047.js → index-zpm95mt5.js} +1 -1
  16. package/dist/index.d.ts +15 -7
  17. package/dist/index.js +1 -1
  18. package/dist/{map-m6f5a_IA.d.ts → map-Buq-GW7l.d.ts} +1 -1
  19. package/dist/{protocol-DctqemRI.d.ts → model-YRg7Uowo.d.ts} +67 -25
  20. package/dist/{port-file-ZPlxXB4G.d.ts → port-file-CxEpgSHY.d.ts} +1 -1
  21. package/dist/protocol-CDV1Pc6h.d.ts +29 -0
  22. package/dist/rpc/port-file.d.ts +1 -1
  23. package/dist/rpc/port-file.js +1 -1
  24. package/dist/rpc/protocol.d.ts +1 -1
  25. package/dist/rpc/protocol.js +1 -1
  26. package/dist/rpc/rpc-client.d.ts +3 -3
  27. package/dist/rpc/rpc-client.js +1 -1
  28. package/dist/rpc/rpc-dir.js +1 -1
  29. package/dist/{sidebar-state-z_M7Trnd.d.ts → sidebar-state-DjTWUbmf.d.ts} +15 -9
  30. package/dist/sidebar-state.d.ts +1 -1
  31. package/dist/sidebar-state.js +1 -1
  32. package/dist/{token-bzhkgc29.js → token-rz4t3ay6.js} +1 -1
  33. package/dist/token-util-tpy2c7h6.js +1 -0
  34. package/dist/tui-preferences.js +1 -1
  35. package/dist/tui.d.ts +20 -2
  36. package/dist/v2/server.d.ts +73 -1
  37. package/dist/v2/server.js +3 -3
  38. package/package.json +2 -2
  39. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +13 -8
  40. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +354 -0
  41. package/src/tui-compiled/raw/shared/134b008900698fe0-backoff.js +92 -0
  42. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +393 -0
  43. package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  44. package/src/tui-compiled/raw/shared/2dce6e9613993329-quota-manager.js +473 -0
  45. package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +566 -0
  46. package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  47. package/src/tui-compiled/raw/shared/4593a4d71edf69fd-tombstone.js +25 -0
  48. package/src/tui-compiled/raw/shared/4d05c2d7a531aece-oauth.js +500 -0
  49. package/src/tui-compiled/raw/shared/6371b24a0573d79e-any-signal.js +42 -0
  50. package/src/tui-compiled/raw/shared/7c140680fc81be92-interlock.js +29 -0
  51. package/src/tui-compiled/raw/shared/85512cc5263ba259-provider.js +87 -0
  52. package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +192 -0
  53. package/src/tui-compiled/raw/shared/b9588ad4725c1d10-enrollment.js +806 -0
  54. package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  55. package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +0 -1
  56. package/src/tui-compiled/raw/shared/ca0fccaba1d87af4-errors.js +8 -0
  57. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +152 -0
  58. package/src/tui-compiled/raw/shared/d9d2892a4c371301-host-slot.js +72 -0
  59. package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +1470 -0
  60. package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  61. package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +24 -0
  62. package/src/tui-compiled/raw/sidebar-state.js +41 -14
  63. package/src/tui-compiled/raw/tui/command-dialogs.tsx +3 -34
  64. package/src/tui-compiled/raw/tui/section-text.js +21 -0
  65. package/src/tui-compiled/raw/tui/v2-dialog.js +137 -0
  66. package/src/tui-compiled/raw/tui/v2.tsx +133 -0
  67. package/src/tui-compiled/raw/tui.tsx +146 -40
  68. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +13 -8
  69. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +354 -0
  70. package/src/tui-compiled/runtime/shared/134b008900698fe0-backoff.js +92 -0
  71. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +393 -0
  72. package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  73. package/src/tui-compiled/runtime/shared/2dce6e9613993329-quota-manager.js +473 -0
  74. package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +566 -0
  75. package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  76. package/src/tui-compiled/runtime/shared/4593a4d71edf69fd-tombstone.js +25 -0
  77. package/src/tui-compiled/runtime/shared/4d05c2d7a531aece-oauth.js +500 -0
  78. package/src/tui-compiled/runtime/shared/6371b24a0573d79e-any-signal.js +42 -0
  79. package/src/tui-compiled/runtime/shared/7c140680fc81be92-interlock.js +29 -0
  80. package/src/tui-compiled/runtime/shared/85512cc5263ba259-provider.js +87 -0
  81. package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +192 -0
  82. package/src/tui-compiled/runtime/shared/b9588ad4725c1d10-enrollment.js +806 -0
  83. package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  84. package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +0 -1
  85. package/src/tui-compiled/runtime/shared/ca0fccaba1d87af4-errors.js +8 -0
  86. package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +152 -0
  87. package/src/tui-compiled/runtime/shared/d9d2892a4c371301-host-slot.js +72 -0
  88. package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +1470 -0
  89. package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  90. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +24 -0
  91. package/src/tui-compiled/runtime/sidebar-state.js +41 -14
  92. package/src/tui-compiled/runtime/tui/command-dialogs.js +3 -15
  93. package/src/tui-compiled/runtime/tui/section-text.js +21 -0
  94. package/src/tui-compiled/runtime/tui/v2-dialog.js +137 -0
  95. package/src/tui-compiled/runtime/tui/v2.js +95 -0
  96. package/src/tui-compiled/runtime/tui.js +255 -163
  97. package/dist/index-4fnghj9e.js +0 -65
  98. package/dist/index-a69gr76b.js +0 -2
  99. package/dist/index-h263aftt.js +0 -157
  100. package/dist/index-ssj7m2rd.js +0 -8
  101. package/dist/index-ydz3hbhc.js +0 -2
  102. package/dist/token-util-4a2xxnpg.js +0 -1
@@ -0,0 +1,354 @@
1
+ import { ClaustrumCredentialError, } from "@cortexkit/claustrum-client";
2
+ import { createLogger } from "./cc9e0702a752cea7-index.js";
3
+ import { readClaustrumEnrollmentToken } from "./b9588ad4725c1d10-enrollment.js";
4
+ import { ClaustrumConsumerError } from "./ca0fccaba1d87af4-errors.js";
5
+ import { CUSTODY_PLACEHOLDER_PREFIX } from "./d9d2892a4c371301-host-slot.js";
6
+ /** Only a new version of the same account may replace an in-flight 401. */
7
+ export function isScopedCredentialRotation(served, current) {
8
+ return (current !== undefined &&
9
+ current.credentialId === served.credentialId &&
10
+ current.accountIdentity === served.accountIdentity &&
11
+ current.recordVersion !== served.recordVersion);
12
+ }
13
+ function scopedRetryReason(served, current) {
14
+ if (current === undefined)
15
+ return 'reauthorize-failed';
16
+ if (current.credentialId !== served.credentialId)
17
+ return 'credential-changed';
18
+ if (current.accountIdentity !== served.accountIdentity)
19
+ return 'account-changed';
20
+ if (current.recordVersion === served.recordVersion)
21
+ return 'version-unchanged';
22
+ return 'rotated';
23
+ }
24
+ const defaultLogger = createLogger('claustrum');
25
+ /**
26
+ * Decide whether a request that got a 401 should retry with the freshly
27
+ * re-authorized receipt, and log the decision. The vault can refresh a
28
+ * credential between the send and the 401; the log line lets that refresh be
29
+ * matched against the consumer's retry. `site` names the kind of request.
30
+ */
31
+ export function decideScopedRetryAfter401(site, served, current, logger = defaultLogger) {
32
+ const retry = isScopedCredentialRotation(served, current);
33
+ logger.debug('scoped 401 re-authorized', {
34
+ site,
35
+ credentialId: served.credentialId,
36
+ servedVersion: served.recordVersion,
37
+ currentVersion: current?.recordVersion ?? null,
38
+ retry,
39
+ reason: scopedRetryReason(served, current),
40
+ });
41
+ return retry;
42
+ }
43
+ /**
44
+ * How long a served OAuth token must stay valid after the vault hands it out,
45
+ * so it cannot expire while a long request is still using it.
46
+ */
47
+ export const SERVING_MARGIN_MS = 300_000;
48
+ function invalidMaterial() {
49
+ return new ClaustrumConsumerError('invalid-material', 'Claustrum returned invalid credential material');
50
+ }
51
+ function accessTokenFromMaterial(material) {
52
+ let access = material.trim();
53
+ if (access.startsWith('{')) {
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(access);
57
+ }
58
+ catch {
59
+ throw invalidMaterial();
60
+ }
61
+ if (!parsed || typeof parsed !== 'object')
62
+ throw invalidMaterial();
63
+ const record = parsed;
64
+ const value = record.access_token ?? record.access;
65
+ access = typeof value === 'string' ? value : '';
66
+ }
67
+ // A header-safe token only: control characters could split the request,
68
+ // and the custody placeholder must never be sent as a credential.
69
+ if (!/^[\x21-\x7e]+$/.test(access) ||
70
+ access.startsWith(CUSTODY_PLACEHOLDER_PREFIX)) {
71
+ throw invalidMaterial();
72
+ }
73
+ return access;
74
+ }
75
+ function credentialTypeOf(row, family) {
76
+ if (!row.operations.includes('read'))
77
+ return undefined;
78
+ if (!row.categories.includes(family.category))
79
+ return undefined;
80
+ if (row.credentialType === 'oauth')
81
+ return row.refreshAdapter === family.refreshAdapter ? 'oauth' : undefined;
82
+ if (row.credentialType === 'api_key' && family.apiKeys)
83
+ return row.refreshAdapter === undefined ? 'api_key' : undefined;
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Reads this consumer's vault credentials (list, fetch, 401 report), with the
88
+ * enrollment token as authorization. Used by every host of a plugin. There is
89
+ * deliberately no credential cache and no single-flight of credential reads:
90
+ * each physical send is authorized by the vault, so a revoked enrollment or a
91
+ * changed record takes effect on the next send. The enrollment token is
92
+ * re-read per operation so an operator's reissue on disk is picked up.
93
+ */
94
+ export class ClaustrumScopedCustody {
95
+ #client;
96
+ #readToken;
97
+ #now;
98
+ #family;
99
+ #parseIdentity;
100
+ #requireAssertion;
101
+ #logger;
102
+ #provenance = new WeakMap();
103
+ #reports = new WeakMap();
104
+ #closed = false;
105
+ constructor(options) {
106
+ const tokenPath = options.tokenPath;
107
+ if (options.readToken) {
108
+ this.#readToken = options.readToken;
109
+ }
110
+ else if (tokenPath) {
111
+ this.#readToken = () => readClaustrumEnrollmentToken(tokenPath);
112
+ }
113
+ else {
114
+ throw new ClaustrumConsumerError('not-enrolled', 'Claustrum enrollment token path is required');
115
+ }
116
+ this.#client = options.client;
117
+ this.#family = options.family;
118
+ this.#parseIdentity = options.parseIdentity;
119
+ this.#requireAssertion = options.requireAssertion ?? false;
120
+ this.#now = options.now ?? Date.now;
121
+ this.#logger = options.logger ?? defaultLogger;
122
+ }
123
+ #check(signal) {
124
+ if (this.#closed)
125
+ throw new ClaustrumConsumerError('closed', 'Claustrum scoped custody is closed');
126
+ signal?.throwIfAborted();
127
+ }
128
+ async #token(signal) {
129
+ this.#check(signal);
130
+ const value = await this.#readToken();
131
+ this.#check(signal);
132
+ if (!/^[0-9a-f]{64}$/.test(value.token) ||
133
+ !Number.isSafeInteger(value.token_generation) ||
134
+ value.token_generation < 1) {
135
+ throw new ClaustrumConsumerError('invalid-token', 'Invalid Claustrum enrollment token');
136
+ }
137
+ return value.token;
138
+ }
139
+ async #call(operation, signal) {
140
+ this.#check(signal);
141
+ let result;
142
+ let removeAbortListener;
143
+ try {
144
+ const pending = operation();
145
+ result = signal
146
+ ? await Promise.race([
147
+ pending,
148
+ new Promise((_resolve, reject) => {
149
+ const abort = () => reject(signal.reason);
150
+ signal.addEventListener('abort', abort, { once: true });
151
+ removeAbortListener = () => signal.removeEventListener('abort', abort);
152
+ if (signal.aborted)
153
+ abort();
154
+ }),
155
+ ])
156
+ : await pending;
157
+ }
158
+ catch (error) {
159
+ this.#check(signal);
160
+ // Keep the vault's own refusals (ClaustrumCredentialError, carrying
161
+ // code, class and action); replace any other error, whose text may echo
162
+ // request params that include the enrollment token.
163
+ if (error instanceof ClaustrumCredentialError)
164
+ throw error;
165
+ throw new ClaustrumConsumerError('unavailable', 'Claustrum scoped operation unavailable');
166
+ }
167
+ finally {
168
+ removeAbortListener?.();
169
+ }
170
+ this.#check(signal);
171
+ return result;
172
+ }
173
+ /**
174
+ * List this consumer's credentials. A record that cannot be used is skipped
175
+ * and warned about rather than failing the whole list, so one bad record
176
+ * never hides every other account. Records outside the family are not
177
+ * skipped records: they simply are not this consumer's.
178
+ */
179
+ async discover(signal) {
180
+ const token = await this.#token(signal);
181
+ const inventory = await this.#call(() => this.#client.listScoped(token), signal);
182
+ const skipped = [];
183
+ const candidates = [];
184
+ const counts = new Map();
185
+ for (const row of inventory.rows) {
186
+ const type = credentialTypeOf(row, this.#family);
187
+ if (!type)
188
+ continue;
189
+ candidates.push({ row, type });
190
+ counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
191
+ }
192
+ const credentials = [];
193
+ for (const { row, type } of candidates) {
194
+ let reason;
195
+ if (!row.id.trim())
196
+ reason = 'empty credential id';
197
+ else if ((counts.get(row.id) ?? 0) > 1)
198
+ reason = 'duplicate credential id';
199
+ else if (row.accountId !== undefined && !row.accountId.trim())
200
+ reason = 'blank account identity';
201
+ else if (!row.state.trim())
202
+ reason = 'empty state';
203
+ if (reason) {
204
+ skipped.push({ ...(row.id.trim() && { credentialId: row.id }), reason });
205
+ continue;
206
+ }
207
+ credentials.push(Object.freeze({
208
+ credentialId: row.id,
209
+ credentialType: type,
210
+ ...(row.accountId !== undefined && {
211
+ accountIdentity: row.accountId,
212
+ }),
213
+ state: row.state,
214
+ ...(row.email !== undefined && { email: row.email }),
215
+ ...(row.orgName !== undefined && { orgName: row.orgName }),
216
+ }));
217
+ }
218
+ for (const record of skipped)
219
+ this.#logger.warn('skipped malformed vault record', record);
220
+ return {
221
+ view: inventory.view,
222
+ credentials: Object.freeze(credentials),
223
+ skipped: Object.freeze(skipped),
224
+ };
225
+ }
226
+ /**
227
+ * Fetch the credential for one physical send and wrap it in a fresh receipt.
228
+ * The vault's served identity (or, without one, the plugin's parse of the
229
+ * token) must equal the roster's when both are present. Without
230
+ * `requireAssertion`, absence on either side proves nothing and does not
231
+ * refuse; the receipt records what the vault asserted separately from what
232
+ * the roster expected. With it, a reply that does not itself name the
233
+ * credential id and the expected account is refused.
234
+ */
235
+ async authorize(identity, signal) {
236
+ if (!identity.credentialId)
237
+ throw new ClaustrumConsumerError('route-unavailable', 'Claustrum dispatch requires a credential id');
238
+ // Capture the caller's fields before yielding so a later mutation cannot move the fence.
239
+ const { credentialId, credentialType, accountIdentity } = identity;
240
+ if (this.#requireAssertion && accountIdentity === undefined)
241
+ throw new ClaustrumConsumerError('identity-unasserted', 'Claustrum dispatch requires a known account identity');
242
+ const token = await this.#token(signal);
243
+ const served = await this.#call(() => this.#client.getScoped({
244
+ credentialId,
245
+ enrollmentToken: token,
246
+ ...(credentialType === 'oauth' && { minTtlMs: SERVING_MARGIN_MS }),
247
+ }), signal);
248
+ if (served.credentialId !== undefined &&
249
+ served.credentialId !== credentialId) {
250
+ throw new ClaustrumConsumerError('identity-changed', 'Claustrum served credential identity changed');
251
+ }
252
+ const expiresAtMs = served.expiresAtMs;
253
+ if (!Number.isSafeInteger(served.recordVersion) ||
254
+ served.recordVersion < 0 ||
255
+ (expiresAtMs === null && credentialType === 'oauth') ||
256
+ (expiresAtMs !== null &&
257
+ (!Number.isFinite(expiresAtMs) ||
258
+ expiresAtMs - this.#now() < SERVING_MARGIN_MS))) {
259
+ throw new ClaustrumConsumerError('insufficient-validity', 'Claustrum served credential has insufficient validity');
260
+ }
261
+ const accessToken = accessTokenFromMaterial(served.material);
262
+ const assertedIdentity = served.accountId?.trim()
263
+ ? served.accountId
264
+ : undefined;
265
+ if (this.#requireAssertion &&
266
+ (served.credentialId === undefined || assertedIdentity === undefined))
267
+ throw new ClaustrumConsumerError('identity-unasserted', 'Claustrum served credential did not assert its identity');
268
+ const parsedIdentity = assertedIdentity === undefined
269
+ ? this.#parseIdentity?.(accessToken)
270
+ : undefined;
271
+ const servedIdentity = assertedIdentity ?? parsedIdentity;
272
+ if (accountIdentity !== undefined &&
273
+ servedIdentity !== undefined &&
274
+ servedIdentity !== accountIdentity) {
275
+ throw new ClaustrumConsumerError('identity-changed', 'Claustrum served credential identity changed');
276
+ }
277
+ const resolvedIdentity = servedIdentity ?? accountIdentity;
278
+ const source = assertedIdentity !== undefined
279
+ ? 'asserted'
280
+ : parsedIdentity !== undefined
281
+ ? 'parsed'
282
+ : accountIdentity !== undefined
283
+ ? 'expected'
284
+ : 'none';
285
+ const attempt = Object.freeze(Object.defineProperty({
286
+ credentialId,
287
+ credentialType,
288
+ ...(resolvedIdentity !== undefined && {
289
+ accountIdentity: resolvedIdentity,
290
+ }),
291
+ accountIdentitySource: source,
292
+ ...(accountIdentity !== undefined && {
293
+ expectedAccountIdentity: accountIdentity,
294
+ }),
295
+ ...(served.credentialId !== undefined && {
296
+ assertedCredentialId: served.credentialId,
297
+ }),
298
+ ...(assertedIdentity !== undefined && {
299
+ assertedAccountIdentity: assertedIdentity,
300
+ }),
301
+ ...(served.projectId !== undefined && {
302
+ projectId: served.projectId,
303
+ }),
304
+ recordVersion: served.recordVersion,
305
+ expiresAtMs,
306
+ }, 'accessToken', { value: accessToken, enumerable: false }));
307
+ this.#provenance.set(attempt, token);
308
+ return attempt;
309
+ }
310
+ /**
311
+ * Report that a send this consumer actually made was rejected. Only a 401
312
+ * is reported (the vault refuses 429, 402 and 5xx reports), addressed by
313
+ * credential id with the exact record version that send was served, and
314
+ * signed with the enrollment token that authorized it. A receipt this
315
+ * custody did not issue is refused rather than reported.
316
+ */
317
+ async reportFailure(attempt, status, reporterSource) {
318
+ if (status !== 401)
319
+ return;
320
+ this.#check();
321
+ const token = this.#provenance.get(attempt);
322
+ if (!token)
323
+ throw new ClaustrumConsumerError('no-receipt', 'Claustrum failure report requires an original dispatch receipt');
324
+ const pending = this.#reports.get(attempt);
325
+ if (pending)
326
+ return pending;
327
+ const report = this.#call(() => this.#client.reportAuthFailureScoped({
328
+ credentialId: attempt.credentialId,
329
+ enrollmentToken: token,
330
+ providerStatus: 401,
331
+ recordVersion: attempt.recordVersion,
332
+ reporterSource,
333
+ }));
334
+ this.#reports.set(attempt, report);
335
+ try {
336
+ await report;
337
+ this.#logger.debug('scoped 401 reported', {
338
+ credentialId: attempt.credentialId,
339
+ recordVersion: attempt.recordVersion,
340
+ reporterSource,
341
+ });
342
+ }
343
+ catch (error) {
344
+ this.#reports.delete(attempt);
345
+ throw error;
346
+ }
347
+ }
348
+ close() {
349
+ if (this.#closed)
350
+ return;
351
+ this.#closed = true;
352
+ this.#client.close();
353
+ }
354
+ }
@@ -0,0 +1,92 @@
1
+ import { createHash } from "node:crypto";
2
+ export const NON_TRANSIENT_REFRESH_RETRY_DELAY_MS = 86400000;
3
+ const MIN_REFRESH_RETRY_DELAY_MS = 300000, MAX_REFRESH_RETRY_DELAY_MS = 3600000, MIN_QUOTA_RETRY_DELAY_MS = 60000, MAX_QUOTA_RETRY_DELAY_MS = 900000, NON_TRANSIENT_QUOTA_RETRY_DELAY_MS = 300000;
4
+ function formatErrorMessage(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ export function isTransientRefreshError(error) {
8
+ const status = error?.status;
9
+ if (typeof status === "number")
10
+ return status === 429 || status >= 500;
11
+ if (!(error instanceof Error))
12
+ return !1;
13
+ return error.name === "AbortError" || error.name === "TimeoutError" || error.message.includes("fetch failed") || "code" in error && (error.code === "ECONNRESET" || error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT" || error.code === "UND_ERR_CONNECT_TIMEOUT");
14
+ }
15
+ export function isTransientQuotaError(error) {
16
+ const message = formatErrorMessage(error), status = error?.status;
17
+ if (typeof status === "number")
18
+ return status === 429 || status >= 500;
19
+ if (!(error instanceof Error))
20
+ return !1;
21
+ const code = error.code;
22
+ return error.name === "AbortError" || error.name === "TimeoutError" || message.includes("fetch failed") || code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT";
23
+ }
24
+ export function hashRefreshToken(refreshToken) {
25
+ return createHash("sha256").update(refreshToken).digest("hex");
26
+ }
27
+ export function buildRefreshOperationError(input) {
28
+ const tokenHash = hashRefreshToken(input.refreshToken), retryCount = (input.previous?.tokenHash === tokenHash ? input.previous.retryCount ?? 0 : 0) + 1;
29
+ let delay;
30
+ const retryAfter = input.error?.retryAfter;
31
+ if (typeof retryAfter === "number" && retryAfter > 0)
32
+ delay = retryAfter * 1000;
33
+ else if (isTransientRefreshError(input.error))
34
+ delay = Math.min(MAX_REFRESH_RETRY_DELAY_MS, MIN_REFRESH_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6));
35
+ else
36
+ delay = NON_TRANSIENT_REFRESH_RETRY_DELAY_MS;
37
+ return {
38
+ message: formatErrorMessage(input.error),
39
+ checkedAt: input.now,
40
+ nextRetryAt: input.now + delay,
41
+ retryCount,
42
+ tokenHash
43
+ };
44
+ }
45
+ export function refreshBackoffActive(error, refreshToken, now) {
46
+ if (!error?.nextRetryAt || error.nextRetryAt <= now)
47
+ return !1;
48
+ if (!refreshToken)
49
+ return !0;
50
+ return error.tokenHash === hashRefreshToken(refreshToken);
51
+ }
52
+ export function formatRefreshBackoffMessage(error, now) {
53
+ return `Codex OAuth refresh is backed off for ${Math.max(1, Math.ceil(((error.nextRetryAt ?? now) - now) / 1000))}s after: ${error.message}`;
54
+ }
55
+ export function buildQuotaOperationError(input) {
56
+ const retryCount = (input.previous?.retryCount ?? 0) + 1;
57
+ let delay;
58
+ const retryAfter = input.error?.retryAfter;
59
+ if (typeof retryAfter === "number" && retryAfter > 0)
60
+ delay = retryAfter * 1000;
61
+ else if (isTransientQuotaError(input.error))
62
+ delay = Math.min(MAX_QUOTA_RETRY_DELAY_MS, MIN_QUOTA_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6));
63
+ else
64
+ delay = NON_TRANSIENT_QUOTA_RETRY_DELAY_MS;
65
+ return {
66
+ message: formatErrorMessage(input.error),
67
+ checkedAt: input.now,
68
+ nextRetryAt: input.now + delay,
69
+ retryCount
70
+ };
71
+ }
72
+ export function quotaBackoffActive(error, now) {
73
+ if (!error?.nextRetryAt || error.nextRetryAt <= now)
74
+ return !1;
75
+ return !0;
76
+ }
77
+ export function formatQuotaBackoffMessage(error, now) {
78
+ return `Quota API backed off for ${Math.max(1, Math.ceil(((error.nextRetryAt ?? now) - now) / 1000))}s after: ${error.message}`;
79
+ }
80
+ export function parseRetryAfter(value) {
81
+ if (!value)
82
+ return;
83
+ const seconds = Number(value);
84
+ if (Number.isFinite(seconds) && seconds > 0)
85
+ return Math.ceil(seconds);
86
+ const date = Date.parse(value);
87
+ if (Number.isFinite(date)) {
88
+ const delta = Math.ceil((date - Date.now()) / 1000);
89
+ return delta > 0 ? delta : void 0;
90
+ }
91
+ return;
92
+ }