crewx 0.9.0-rc.71 → 0.9.0-rc.72

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.
@@ -0,0 +1,451 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProviderLimitCache = exports.FAIL_BACKOFF_STEPS_MS = exports.RATE_LIMITED_STALE_TTL_MS = exports.STALE_TTL_MS = exports.FRESH_TTL_MS = exports.PROVIDER_LIMIT_BACKOFF_STEPS_MS = exports.PROVIDER_LIMIT_RATE_LIMITED_STALE_TTL_MS = exports.PROVIDER_LIMIT_STALE_TTL_MS = exports.PROVIDER_LIMIT_FRESH_TTL_MS = void 0;
4
+ exports.buildProviderLimitCacheKey = buildProviderLimitCacheKey;
5
+ exports.buildProviderLimitActiveScopeKey = buildProviderLimitActiveScopeKey;
6
+ exports.fingerprintAccountScope = fingerprintAccountScope;
7
+ exports.createProviderLimitCache = createProviderLimitCache;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const cache_db_js_1 = require("../cache-db.js");
10
+ exports.PROVIDER_LIMIT_FRESH_TTL_MS = 5 * 60 * 1000;
11
+ exports.PROVIDER_LIMIT_STALE_TTL_MS = 30 * 60 * 1000;
12
+ exports.PROVIDER_LIMIT_RATE_LIMITED_STALE_TTL_MS = 24 * 60 * 60 * 1000;
13
+ exports.PROVIDER_LIMIT_BACKOFF_STEPS_MS = [30 * 1000, 60 * 1000, 2 * 60 * 1000, 5 * 60 * 1000];
14
+ // These aliases make the cache policy easy to discover at call sites.
15
+ exports.FRESH_TTL_MS = exports.PROVIDER_LIMIT_FRESH_TTL_MS;
16
+ exports.STALE_TTL_MS = exports.PROVIDER_LIMIT_STALE_TTL_MS;
17
+ exports.RATE_LIMITED_STALE_TTL_MS = exports.PROVIDER_LIMIT_RATE_LIMITED_STALE_TTL_MS;
18
+ exports.FAIL_BACKOFF_STEPS_MS = exports.PROVIDER_LIMIT_BACKOFF_STEPS_MS;
19
+ function buildProviderLimitCacheKey(provider, accountScope, kind) {
20
+ return `usage:limits:v2:${provider}:${accountScope}:${kind}`;
21
+ }
22
+ function buildProviderLimitActiveScopeKey(provider) {
23
+ return `usage:limits:v2:${provider}:active-scope`;
24
+ }
25
+ /** Create a non-secret account discriminator from a secret account credential. */
26
+ function fingerprintAccountScope(value) {
27
+ return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
28
+ }
29
+ const VALID_STATUSES = new Set([
30
+ 'live',
31
+ 'cached',
32
+ 'fallback',
33
+ 'stale',
34
+ 'unavailable',
35
+ 'not_offered',
36
+ ]);
37
+ const DEFAULT_SUCCESS = (data) => data !== null && hasMeaningfulData(data);
38
+ function hasMeaningfulData(value) {
39
+ if (value === null || value === undefined)
40
+ return false;
41
+ if (Array.isArray(value))
42
+ return value.some((item) => hasMeaningfulData(item));
43
+ if (typeof value !== 'object')
44
+ return true;
45
+ const entries = Object.values(value);
46
+ return entries.length > 0 && entries.some((entry) => hasMeaningfulData(entry));
47
+ }
48
+ function isRecord(value) {
49
+ return typeof value === 'object' && value !== null;
50
+ }
51
+ function positiveFiniteOr(value, fallback) {
52
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
53
+ }
54
+ function validBackoffStepsOr(value) {
55
+ if (!value || value.length === 0 || value.some((step) => !Number.isFinite(step) || step <= 0)) {
56
+ return exports.PROVIDER_LIMIT_BACKOFF_STEPS_MS;
57
+ }
58
+ return [...value];
59
+ }
60
+ function isProviderLimitStatus(value) {
61
+ return VALID_STATUSES.has(value);
62
+ }
63
+ function isFailureKind(value) {
64
+ return value === 'not_authenticated'
65
+ || value === 'unauthorized'
66
+ || value === 'forbidden'
67
+ || value === 'rate_limited'
68
+ || value === 'timeout'
69
+ || value === 'network'
70
+ || value === 'upstream'
71
+ || value === 'invalid_response'
72
+ || value === 'client_unavailable';
73
+ }
74
+ function isProviderLimitSource(value) {
75
+ return value === 'codex_app_server'
76
+ || value === 'codex_jsonl'
77
+ || value === 'claude_oauth'
78
+ || value === 'grok_oauth'
79
+ || value === 'glm_oauth';
80
+ }
81
+ function isValidObservedAt(value) {
82
+ if (typeof value !== 'string')
83
+ return false;
84
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)) {
85
+ return false;
86
+ }
87
+ return Number.isFinite(Date.parse(value));
88
+ }
89
+ function errorFailureKind(error) {
90
+ if (isRecord(error) && isFailureKind(error['failureKind']))
91
+ return error['failureKind'];
92
+ if (isRecord(error) && error['name'] === 'AbortError')
93
+ return 'timeout';
94
+ if (error instanceof TypeError)
95
+ return 'network';
96
+ return 'upstream';
97
+ }
98
+ /**
99
+ * Persistent fresh/last-good cache for provider subscription limits.
100
+ *
101
+ * The account scope must already be a non-secret account identifier or a
102
+ * fingerprint. Use fingerprintAccountScope when the only available value is
103
+ * a credential. Failure state and in-flight work intentionally stay in memory.
104
+ */
105
+ class ProviderLimitCache {
106
+ configuredProvider;
107
+ configuredAccountScope;
108
+ configuredIsSuccess;
109
+ now;
110
+ freshTtlMs;
111
+ staleTtlMs;
112
+ rateLimitedStaleTtlMs;
113
+ backoffStepsMs;
114
+ inFlight = new Map();
115
+ failures = new Map();
116
+ constructor(providerOrOptions, accountScope, isSuccessOrOptions, tuning) {
117
+ const baseOptions = typeof providerOrOptions === 'object' && providerOrOptions !== null
118
+ ? providerOrOptions
119
+ : tuning ?? (typeof isSuccessOrOptions === 'object' ? isSuccessOrOptions : {});
120
+ this.configuredProvider = typeof providerOrOptions === 'string'
121
+ ? providerOrOptions
122
+ : baseOptions.provider;
123
+ this.configuredAccountScope = typeof providerOrOptions === 'string'
124
+ ? accountScope
125
+ : baseOptions.accountScope;
126
+ this.configuredIsSuccess = typeof providerOrOptions === 'string'
127
+ ? typeof isSuccessOrOptions === 'function' ? isSuccessOrOptions : DEFAULT_SUCCESS
128
+ : baseOptions.isSuccess ?? DEFAULT_SUCCESS;
129
+ this.now = baseOptions.now ?? Date.now;
130
+ this.freshTtlMs = positiveFiniteOr(baseOptions.freshTtlMs, exports.PROVIDER_LIMIT_FRESH_TTL_MS);
131
+ this.staleTtlMs = positiveFiniteOr(baseOptions.staleTtlMs, exports.PROVIDER_LIMIT_STALE_TTL_MS);
132
+ this.rateLimitedStaleTtlMs = positiveFiniteOr(baseOptions.rateLimitedStaleTtlMs, exports.PROVIDER_LIMIT_RATE_LIMITED_STALE_TTL_MS);
133
+ this.backoffStepsMs = validBackoffStepsOr(baseOptions.backoffStepsMs);
134
+ }
135
+ get(...args) {
136
+ const request = this.resolveRequest(args);
137
+ const cacheKey = buildProviderLimitCacheKey(request.provider, request.accountScope, 'fresh');
138
+ const fresh = this.readSuccessfulEntry(cacheKey, request.isSuccess);
139
+ if (fresh) {
140
+ return Promise.resolve({ ...fresh, status: 'cached', failureKind: null });
141
+ }
142
+ const running = this.inFlight.get(cacheKey);
143
+ if (running)
144
+ return running;
145
+ const now = this.now();
146
+ const failure = this.failures.get(cacheKey);
147
+ if (failure && now < failure.nextAttemptAt) {
148
+ return Promise.resolve(this.replayFailure(request, failure.failureKind, now));
149
+ }
150
+ const operation = Promise.resolve()
151
+ .then(() => this.fetchAndCache(request, cacheKey))
152
+ .finally(() => {
153
+ if (this.inFlight.get(cacheKey) === operation)
154
+ this.inFlight.delete(cacheKey);
155
+ });
156
+ this.inFlight.set(cacheKey, operation);
157
+ return operation;
158
+ }
159
+ fetch(...args) {
160
+ return this.get(...args);
161
+ }
162
+ getOrFetch(...args) {
163
+ return this.get(...args);
164
+ }
165
+ /**
166
+ * Return the last safe account fingerprint observed for this provider.
167
+ * This hint is itself short-lived and contains no credential material; it
168
+ * lets a process restart reuse a still-fresh scoped value without probing
169
+ * the provider first.
170
+ */
171
+ getActiveScope(provider = this.configuredProvider) {
172
+ const resolvedProvider = this.requireKeyPart(provider, 'provider');
173
+ const value = this.safeGet(buildProviderLimitActiveScopeKey(resolvedProvider));
174
+ if (typeof value === 'string' && value.length > 0 && !value.includes('\0'))
175
+ return value;
176
+ if (value !== undefined)
177
+ this.safeDelete(buildProviderLimitActiveScopeKey(resolvedProvider));
178
+ return undefined;
179
+ }
180
+ setActiveScope(provider, accountScope) {
181
+ const resolvedProvider = this.requireKeyPart(provider, 'provider');
182
+ const resolvedAccountScope = this.requireKeyPart(accountScope, 'accountScope');
183
+ this.safeSet(buildProviderLimitActiveScopeKey(resolvedProvider), resolvedAccountScope, this.freshTtlMs);
184
+ }
185
+ clearActiveScope(provider = this.configuredProvider) {
186
+ const resolvedProvider = this.requireKeyPart(provider, 'provider');
187
+ this.safeDelete(buildProviderLimitActiveScopeKey(resolvedProvider));
188
+ }
189
+ /**
190
+ * Persist one already-observed successful result under its safe scope. This
191
+ * is used when the first request had no scope yet and therefore could not
192
+ * enter `get()` until after the single direct refresh completed.
193
+ */
194
+ store(provider, accountScope, raw, isSuccess) {
195
+ const resolvedProvider = this.requireKeyPart(provider, 'provider');
196
+ const resolvedAccountScope = this.requireKeyPart(accountScope, 'accountScope');
197
+ const predicate = isSuccess ?? this.configuredIsSuccess;
198
+ const result = this.normalizeResult(raw);
199
+ if (!this.isSuccessfulResult(result, predicate))
200
+ return undefined;
201
+ this.storeSuccessful(resolvedProvider, resolvedAccountScope, result);
202
+ return result;
203
+ }
204
+ /**
205
+ * Retain the in-memory retry/backoff state for a direct cold refresh that
206
+ * completed before a scoped `get()` could be entered.
207
+ */
208
+ recordFailure(provider, accountScope, raw) {
209
+ const resolvedProvider = this.requireKeyPart(provider, 'provider');
210
+ const resolvedAccountScope = this.requireKeyPart(accountScope, 'accountScope');
211
+ const result = this.normalizeResult(raw);
212
+ if (result.status === 'not_offered') {
213
+ this.deletePersistentEntries(resolvedProvider, resolvedAccountScope);
214
+ this.clearActiveScope(resolvedProvider);
215
+ return;
216
+ }
217
+ this.recordFailureState(resolvedProvider, resolvedAccountScope, result);
218
+ }
219
+ clear(provider = this.configuredProvider, accountScope = this.configuredAccountScope) {
220
+ const resolvedProvider = this.requireKeyPart(provider, 'provider');
221
+ const resolvedAccountScope = this.requireKeyPart(accountScope, 'accountScope');
222
+ this.deletePersistentEntries(resolvedProvider, resolvedAccountScope);
223
+ this.failures.delete(buildProviderLimitCacheKey(resolvedProvider, resolvedAccountScope, 'fresh'));
224
+ }
225
+ reset(provider = this.configuredProvider, accountScope = this.configuredAccountScope) {
226
+ this.clear(provider, accountScope);
227
+ }
228
+ resolveRequest(args) {
229
+ const first = args[0];
230
+ if (typeof first === 'function') {
231
+ const provider = this.requireKeyPart(this.configuredProvider, 'provider');
232
+ const accountScope = this.requireKeyPart(this.configuredAccountScope, 'accountScope');
233
+ const predicate = typeof args[1] === 'function' ? args[1] : this.configuredIsSuccess;
234
+ return { provider, accountScope, fetcher: first, isSuccess: predicate };
235
+ }
236
+ if (typeof first === 'string') {
237
+ const provider = this.requireKeyPart(first, 'provider');
238
+ const account = args[1];
239
+ const fetcher = args[2];
240
+ if (typeof account !== 'string' || typeof fetcher !== 'function') {
241
+ throw new TypeError('ProviderLimitCache.get requires provider, accountScope, and fetcher');
242
+ }
243
+ const predicate = typeof args[3] === 'function' ? args[3] : this.configuredIsSuccess;
244
+ return { provider, accountScope: this.requireKeyPart(account, 'accountScope'), fetcher, isSuccess: predicate };
245
+ }
246
+ const fetcher = first.fetcher ?? first.fetch;
247
+ if (typeof fetcher !== 'function')
248
+ throw new TypeError('ProviderLimitCache request requires fetcher');
249
+ return {
250
+ provider: this.requireKeyPart(first.provider, 'provider'),
251
+ accountScope: this.requireKeyPart(first.accountScope, 'accountScope'),
252
+ fetcher,
253
+ isSuccess: first.isSuccess ?? this.configuredIsSuccess,
254
+ };
255
+ }
256
+ async fetchAndCache(request, cacheKey) {
257
+ let result;
258
+ const scopeObservation = { present: false, accountScope: null };
259
+ try {
260
+ const raw = await request.fetcher();
261
+ const rawRecord = isRecord(raw) ? raw : undefined;
262
+ scopeObservation.present = rawRecord !== undefined
263
+ && Object.prototype.hasOwnProperty.call(rawRecord, 'cacheAccountScope');
264
+ if (scopeObservation.present && rawRecord) {
265
+ const observed = rawRecord['cacheAccountScope'];
266
+ scopeObservation.accountScope = typeof observed === 'string'
267
+ && observed.length > 0
268
+ && !observed.includes('\0')
269
+ ? observed
270
+ : null;
271
+ }
272
+ result = this.normalizeResult(raw);
273
+ }
274
+ catch (error) {
275
+ result = this.unavailableResult(errorFailureKind(error));
276
+ }
277
+ if (scopeObservation.present && scopeObservation.accountScope !== request.accountScope) {
278
+ // A direct refresh is authoritative about the account that was actually
279
+ // observed. Never replay or store account A data when that observation
280
+ // says B or says that no safe account scope exists.
281
+ this.deletePersistentEntries(request.provider, request.accountScope);
282
+ this.failures.delete(cacheKey);
283
+ if (scopeObservation.accountScope === null) {
284
+ this.clearActiveScope(request.provider);
285
+ return result;
286
+ }
287
+ this.setActiveScope(request.provider, scopeObservation.accountScope);
288
+ if (this.isSuccessfulResult(result, request.isSuccess)) {
289
+ this.storeSuccessful(request.provider, scopeObservation.accountScope, result);
290
+ }
291
+ return result;
292
+ }
293
+ if (result.status === 'not_offered') {
294
+ this.deletePersistentEntries(request.provider, request.accountScope);
295
+ this.failures.delete(cacheKey);
296
+ this.clearActiveScope(request.provider);
297
+ return result;
298
+ }
299
+ if (this.isSuccessfulResult(result, request.isSuccess)) {
300
+ this.failures.delete(cacheKey);
301
+ this.storeSuccessful(request.provider, request.accountScope, result);
302
+ return result;
303
+ }
304
+ const now = this.now();
305
+ const failureKind = this.recordFailureState(request.provider, request.accountScope, result);
306
+ return this.replayFailure(request, failureKind, now);
307
+ }
308
+ recordFailureState(provider, accountScope, result) {
309
+ const cacheKey = buildProviderLimitCacheKey(provider, accountScope, 'fresh');
310
+ const failureKind = this.hasInvalidObservationMetadata(result)
311
+ ? 'invalid_response'
312
+ : result.failureKind ?? 'invalid_response';
313
+ const now = this.now();
314
+ const previous = this.failures.get(cacheKey);
315
+ const stepIndex = Math.min(previous?.count ?? 0, this.backoffStepsMs.length - 1);
316
+ this.failures.set(cacheKey, {
317
+ count: (previous?.count ?? 0) + 1,
318
+ nextAttemptAt: now + this.backoffStepsMs[stepIndex],
319
+ failureKind,
320
+ });
321
+ return failureKind;
322
+ }
323
+ readSuccessfulEntry(key, isSuccess) {
324
+ const stored = this.safeGet(key);
325
+ if (stored === undefined)
326
+ return undefined;
327
+ const normalized = this.normalizeResult(stored);
328
+ if (this.isSuccessfulResult(normalized, isSuccess))
329
+ return normalized;
330
+ this.safeDelete(key);
331
+ return undefined;
332
+ }
333
+ readLastGood(request) {
334
+ const key = buildProviderLimitCacheKey(request.provider, request.accountScope, 'last-good');
335
+ return this.readSuccessfulEntry(key, request.isSuccess);
336
+ }
337
+ replayFailure(request, failureKind, now) {
338
+ const lastGood = this.readLastGood(request);
339
+ if (lastGood && this.isWithinStaleWindow(lastGood, failureKind, now)) {
340
+ return {
341
+ ...lastGood,
342
+ status: 'stale',
343
+ failureKind,
344
+ };
345
+ }
346
+ return this.unavailableResult(failureKind);
347
+ }
348
+ isWithinStaleWindow(lastGood, failureKind, now) {
349
+ if (!lastGood.observedAt)
350
+ return false;
351
+ const observedAt = Date.parse(lastGood.observedAt);
352
+ if (!Number.isFinite(observedAt))
353
+ return false;
354
+ const ttl = failureKind === 'rate_limited' ? this.rateLimitedStaleTtlMs : this.staleTtlMs;
355
+ return now - observedAt < ttl;
356
+ }
357
+ isSuccessfulResult(result, isSuccess) {
358
+ if (result.status !== 'live' && result.status !== 'fallback')
359
+ return false;
360
+ if (!isValidObservedAt(result.observedAt) || !isProviderLimitSource(result.source))
361
+ return false;
362
+ if (result.data === null || result.data === undefined || !hasMeaningfulData(result.data))
363
+ return false;
364
+ try {
365
+ return isSuccess(result.data);
366
+ }
367
+ catch {
368
+ return false;
369
+ }
370
+ }
371
+ hasInvalidObservationMetadata(result) {
372
+ return (result.status === 'live' || result.status === 'fallback')
373
+ && result.data !== null
374
+ && result.data !== undefined
375
+ && hasMeaningfulData(result.data)
376
+ && (!isValidObservedAt(result.observedAt) || !isProviderLimitSource(result.source));
377
+ }
378
+ normalizeResult(raw) {
379
+ if (!isRecord(raw) || typeof raw['status'] !== 'string' || !isProviderLimitStatus(raw['status']) || !('data' in raw)) {
380
+ return this.unavailableResult('invalid_response');
381
+ }
382
+ const failureKind = raw['failureKind'];
383
+ const source = raw['source'];
384
+ const observedAt = raw['observedAt'];
385
+ return {
386
+ status: raw['status'],
387
+ data: raw['data'],
388
+ failureKind: failureKind === null || failureKind === undefined
389
+ ? null
390
+ : isFailureKind(failureKind) ? failureKind : 'invalid_response',
391
+ observedAt: observedAt === null || observedAt === undefined
392
+ ? null
393
+ : typeof observedAt === 'string' ? observedAt : null,
394
+ source: source === null || source === undefined
395
+ ? null
396
+ : isProviderLimitSource(source) ? source : null,
397
+ };
398
+ }
399
+ unavailableResult(failureKind) {
400
+ return {
401
+ status: 'unavailable',
402
+ data: null,
403
+ failureKind,
404
+ observedAt: null,
405
+ source: null,
406
+ };
407
+ }
408
+ storeSuccessful(provider, accountScope, result) {
409
+ const cacheKey = buildProviderLimitCacheKey(provider, accountScope, 'fresh');
410
+ this.safeSet(cacheKey, result, this.freshTtlMs);
411
+ this.safeSet(buildProviderLimitCacheKey(provider, accountScope, 'last-good'), result, Math.max(this.staleTtlMs, this.rateLimitedStaleTtlMs));
412
+ this.setActiveScope(provider, accountScope);
413
+ }
414
+ requireKeyPart(value, name) {
415
+ if (!value || value.includes('\0'))
416
+ throw new TypeError(`ProviderLimitCache requires a non-empty ${name}`);
417
+ return value;
418
+ }
419
+ deletePersistentEntries(provider, accountScope) {
420
+ this.safeDelete(buildProviderLimitCacheKey(provider, accountScope, 'fresh'));
421
+ this.safeDelete(buildProviderLimitCacheKey(provider, accountScope, 'last-good'));
422
+ }
423
+ safeGet(key) {
424
+ try {
425
+ return (0, cache_db_js_1.kvGet)(key);
426
+ }
427
+ catch {
428
+ return undefined;
429
+ }
430
+ }
431
+ safeSet(key, value, ttlMs) {
432
+ try {
433
+ (0, cache_db_js_1.kvSet)(key, value, ttlMs);
434
+ }
435
+ catch {
436
+ // Cache persistence is best effort; the live provider result remains authoritative.
437
+ }
438
+ }
439
+ safeDelete(key) {
440
+ try {
441
+ (0, cache_db_js_1.kvDel)(key);
442
+ }
443
+ catch {
444
+ // Cache cleanup is best effort.
445
+ }
446
+ }
447
+ }
448
+ exports.ProviderLimitCache = ProviderLimitCache;
449
+ function createProviderLimitCache(options = {}) {
450
+ return new ProviderLimitCache(options);
451
+ }