@cortexkit/anthropic-auth-core 1.22.0 → 1.23.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.
- package/dist/accounts.d.ts +59 -2
- package/dist/accounts.js +666 -115
- package/dist/auth.js +3 -0
- package/dist/cachekeep.d.ts +18 -1
- package/dist/cachekeep.js +108 -82
- package/dist/cch.d.ts +11 -1
- package/dist/cch.js +48 -3
- package/dist/claude-code.d.ts +7 -1
- package/dist/claude-code.js +18 -3
- package/dist/claustrum-enrollment-registry.d.ts +17 -0
- package/dist/claustrum-enrollment-registry.js +194 -0
- package/dist/claustrum-enrollment.d.ts +70 -0
- package/dist/claustrum-enrollment.js +485 -0
- package/dist/claustrum-scoped-roster.d.ts +23 -0
- package/dist/claustrum-scoped-roster.js +194 -0
- package/dist/claustrum-scoped-runtime.d.ts +28 -0
- package/dist/claustrum-scoped-runtime.js +230 -0
- package/dist/claustrum-scoped.d.ts +39 -0
- package/dist/claustrum-scoped.js +193 -0
- package/dist/claustrum.d.ts +149 -6
- package/dist/claustrum.js +990 -48
- package/dist/commands/account.d.ts +40 -12
- package/dist/commands/account.js +138 -9
- package/dist/constants.d.ts +3 -2
- package/dist/constants.js +3 -2
- package/dist/custom-headers.d.ts +3 -0
- package/dist/custom-headers.js +102 -0
- package/dist/fast.js +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/logger.js +3 -1
- package/dist/model-remap.d.ts +27 -0
- package/dist/model-remap.js +72 -0
- package/dist/models.d.ts +19 -0
- package/dist/models.js +48 -12
- package/dist/oauth-profile.d.ts +2 -1
- package/dist/oauth-profile.js +6 -0
- package/dist/prime.d.ts +11 -3
- package/dist/prime.js +25 -7
- package/dist/quota-header-feed.d.ts +13 -0
- package/dist/quota-header-feed.js +60 -8
- package/dist/quota-manager.d.ts +15 -1
- package/dist/quota-manager.js +73 -22
- package/dist/relay.d.ts +8 -0
- package/dist/relay.js +72 -12
- package/dist/sticky-routing.d.ts +2 -0
- package/dist/sticky-routing.js +19 -1
- package/package.json +3 -2
package/dist/models.js
CHANGED
|
@@ -9,6 +9,10 @@ export const CLAUDE_MYTHOS_5_1_MODEL_ID = 'claude-mythos-5-1';
|
|
|
9
9
|
* above so per-million-token cost estimation does not conflate families.
|
|
10
10
|
*/
|
|
11
11
|
export const CLAUDE_HAIKU_4_5_MODEL_ID = 'claude-haiku-4-5';
|
|
12
|
+
/** Remove OpenCode's context-window qualifier before model-family checks. */
|
|
13
|
+
export function normalizeAnthropicModelId(model) {
|
|
14
|
+
return model.endsWith('[1m]') ? model.slice(0, -4) : model;
|
|
15
|
+
}
|
|
12
16
|
/**
|
|
13
17
|
* Per-million-token USD pricing for Haiku 4.5. Used to project the cumulative
|
|
14
18
|
* cost of prime requests from persisted usage counters — never persisted on
|
|
@@ -74,18 +78,23 @@ export const CLAUDE_FABLE_MYTHOS_5_1_MODEL_SPECS = {
|
|
|
74
78
|
[CLAUDE_MYTHOS_5_1_MODEL_ID]: CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS[CLAUDE_MYTHOS_5_1_MODEL_ID],
|
|
75
79
|
};
|
|
76
80
|
export function isClaudeFableOrMythos5Model(model) {
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
if (typeof model !== 'string')
|
|
82
|
+
return false;
|
|
83
|
+
const normalized = normalizeAnthropicModelId(model);
|
|
84
|
+
return CLAUDE_FABLE_MYTHOS_5_MODEL_IDS.some((id) => normalized === id || normalized.startsWith(`${id}-`));
|
|
79
85
|
}
|
|
80
86
|
export function isClaudeFableOrMythos51Model(model) {
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
if (typeof model !== 'string')
|
|
88
|
+
return false;
|
|
89
|
+
const normalized = normalizeAnthropicModelId(model);
|
|
90
|
+
return CLAUDE_FABLE_MYTHOS_5_1_MODEL_IDS.some((id) => normalized === id || normalized.startsWith(`${id}-`));
|
|
83
91
|
}
|
|
84
92
|
export function isClaudeFable51Model(model) {
|
|
85
93
|
if (typeof model !== 'string')
|
|
86
94
|
return false;
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
const normalized = normalizeAnthropicModelId(model);
|
|
96
|
+
return (normalized === CLAUDE_FABLE_5_1_MODEL_ID ||
|
|
97
|
+
normalized.startsWith(`${CLAUDE_FABLE_5_1_MODEL_ID}-`));
|
|
89
98
|
}
|
|
90
99
|
export function getClaudeFableMythos5ReleaseDate(model) {
|
|
91
100
|
return isClaudeFableOrMythos51Model(model)
|
|
@@ -101,9 +110,11 @@ export const CLAUDE_SONNET_5_MODEL_ID = 'claude-sonnet-5';
|
|
|
101
110
|
*/
|
|
102
111
|
export const CLAUDE_SONNET_5_ADAPTIVE_THINKING = CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING;
|
|
103
112
|
export function isClaudeSonnet5Model(model) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
if (typeof model !== 'string')
|
|
114
|
+
return false;
|
|
115
|
+
const normalized = normalizeAnthropicModelId(model);
|
|
116
|
+
return (normalized === CLAUDE_SONNET_5_MODEL_ID ||
|
|
117
|
+
normalized.startsWith(`${CLAUDE_SONNET_5_MODEL_ID}-`));
|
|
107
118
|
}
|
|
108
119
|
export const CLAUDE_OPUS_5_MODEL_ID = 'claude-opus-5';
|
|
109
120
|
/**
|
|
@@ -116,9 +127,34 @@ export const CLAUDE_OPUS_5_MODEL_ID = 'claude-opus-5';
|
|
|
116
127
|
*/
|
|
117
128
|
export const CLAUDE_OPUS_5_ADAPTIVE_THINKING = CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING;
|
|
118
129
|
export function isClaudeOpus5Model(model) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
if (typeof model !== 'string')
|
|
131
|
+
return false;
|
|
132
|
+
const normalized = normalizeAnthropicModelId(model);
|
|
133
|
+
return (!isClaudeOpus55Model(normalized) &&
|
|
134
|
+
(normalized === CLAUDE_OPUS_5_MODEL_ID ||
|
|
135
|
+
normalized.startsWith(`${CLAUDE_OPUS_5_MODEL_ID}-`)));
|
|
136
|
+
}
|
|
137
|
+
export const CLAUDE_OPUS_5_5_MODEL_ID = 'claude-opus-5-5';
|
|
138
|
+
export const CLAUDE_OPUS_5_5_RELEASE_DATE = '2026-09-18';
|
|
139
|
+
export const CLAUDE_OPUS_5_5_CONTEXT_WINDOW = 1_000_000;
|
|
140
|
+
export const CLAUDE_OPUS_5_5_MAX_OUTPUT_TOKENS = 128_000;
|
|
141
|
+
export const CLAUDE_OPUS_5_5_PRICING = {
|
|
142
|
+
input: 4,
|
|
143
|
+
output: 20,
|
|
144
|
+
cacheRead: 0.2,
|
|
145
|
+
cacheWrite5m: 5,
|
|
146
|
+
cacheWrite1h: 8,
|
|
147
|
+
};
|
|
148
|
+
export const CLAUDE_OPUS_5_5_ADAPTIVE_THINKING = CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING;
|
|
149
|
+
export function isClaudeOpus55Model(model) {
|
|
150
|
+
if (typeof model !== 'string')
|
|
151
|
+
return false;
|
|
152
|
+
const normalized = normalizeAnthropicModelId(model);
|
|
153
|
+
return (normalized === CLAUDE_OPUS_5_5_MODEL_ID ||
|
|
154
|
+
normalized.startsWith(`${CLAUDE_OPUS_5_5_MODEL_ID}-`));
|
|
155
|
+
}
|
|
156
|
+
export function isClaudeOpus5FamilyModel(model) {
|
|
157
|
+
return isClaudeOpus5Model(model) || isClaudeOpus55Model(model);
|
|
122
158
|
}
|
|
123
159
|
export function isOpenAIReasoningSignature(value) {
|
|
124
160
|
if (typeof value !== 'string')
|
package/dist/oauth-profile.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { OAuthAccountProfile } from './accounts.ts';
|
|
1
|
+
import type { OAuthAccountProfile, ProviderAccountUuid } from './accounts.ts';
|
|
2
2
|
export declare const PROFILE_TTL_MS: number;
|
|
3
3
|
export declare function fetchOAuthAccountProfile(input: {
|
|
4
4
|
accessToken: string;
|
|
5
5
|
accountIdentity?: string;
|
|
6
|
+
providerAccountUuid?: ProviderAccountUuid;
|
|
6
7
|
fetchImpl?: typeof fetch;
|
|
7
8
|
now?: () => number;
|
|
8
9
|
signal?: AbortSignal;
|
package/dist/oauth-profile.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { assertNotCustodyTombstone } from './claustrum.js';
|
|
1
2
|
const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
|
|
2
3
|
export const PROFILE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
3
4
|
export async function fetchOAuthAccountProfile(input) {
|
|
5
|
+
// The bearer value is guarded where it becomes a header, like every other builder.
|
|
6
|
+
assertNotCustodyTombstone(input.accessToken, 'anthropic');
|
|
4
7
|
const response = await (input.fetchImpl ?? fetch)(PROFILE_URL, {
|
|
5
8
|
method: 'GET',
|
|
6
9
|
signal: input.signal,
|
|
@@ -29,6 +32,9 @@ export async function fetchOAuthAccountProfile(input) {
|
|
|
29
32
|
...(input.accountIdentity !== undefined && {
|
|
30
33
|
accountIdentity: input.accountIdentity,
|
|
31
34
|
}),
|
|
35
|
+
...(input.providerAccountUuid !== undefined && {
|
|
36
|
+
providerAccountUuid: input.providerAccountUuid,
|
|
37
|
+
}),
|
|
32
38
|
};
|
|
33
39
|
}
|
|
34
40
|
export function oauthProfileMatchesIdentity(profile, accountIdentity) {
|
package/dist/prime.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type PrimeAccountStatus = {
|
|
|
30
30
|
label: string;
|
|
31
31
|
nextDueAt?: number;
|
|
32
32
|
lastPrimedAt?: number | null;
|
|
33
|
-
lastResult?: 'ok' | 'error';
|
|
33
|
+
lastResult?: 'ok' | 'error' | 'skipped';
|
|
34
34
|
usage?: import('./accounts.ts').PrimeUsageCounters;
|
|
35
35
|
estimatedCostUsd?: number;
|
|
36
36
|
};
|
|
@@ -47,7 +47,7 @@ export type PrimeSendResult = {
|
|
|
47
47
|
status?: number;
|
|
48
48
|
ms?: number;
|
|
49
49
|
error: string;
|
|
50
|
-
reason?: 'token-refresh' | 'send';
|
|
50
|
+
reason?: 'token-refresh' | 'vault-cold' | 'send';
|
|
51
51
|
};
|
|
52
52
|
/**
|
|
53
53
|
* Pure command parser — accepts `''` / `on` / `off` and treats anything else
|
|
@@ -72,7 +72,7 @@ export declare function buildPrimeAccountStatuses(storage: AccountStorage | null
|
|
|
72
72
|
now?: number;
|
|
73
73
|
transient?: ReadonlyMap<string, {
|
|
74
74
|
lastPrimedAt?: number;
|
|
75
|
-
lastResult?: 'ok' | 'error';
|
|
75
|
+
lastResult?: 'ok' | 'error' | 'skipped';
|
|
76
76
|
}>;
|
|
77
77
|
}): PrimeAccountStatus[];
|
|
78
78
|
/**
|
|
@@ -182,9 +182,17 @@ export type PrimeManagerOptions = {
|
|
|
182
182
|
now?: () => number;
|
|
183
183
|
markerDir?: string;
|
|
184
184
|
storagePath: string;
|
|
185
|
+
setIntervalImpl?: typeof globalThis.setInterval;
|
|
186
|
+
clearIntervalImpl?: typeof globalThis.clearInterval;
|
|
187
|
+
setTimeoutImpl?: typeof globalThis.setTimeout;
|
|
188
|
+
clearTimeoutImpl?: typeof globalThis.clearTimeout;
|
|
185
189
|
};
|
|
186
190
|
export declare class PrimeManager {
|
|
187
191
|
private timer;
|
|
192
|
+
private readonly setIntervalImpl;
|
|
193
|
+
private readonly clearIntervalImpl;
|
|
194
|
+
private readonly setTimeoutImpl;
|
|
195
|
+
private readonly clearTimeoutImpl;
|
|
188
196
|
private postFireRefreshTimers;
|
|
189
197
|
private lastForcedCheck;
|
|
190
198
|
private stopped;
|
package/dist/prime.js
CHANGED
|
@@ -155,6 +155,8 @@ function formatPrimed(lastPrimedAt, lastResult) {
|
|
|
155
155
|
});
|
|
156
156
|
if (lastResult === 'error')
|
|
157
157
|
return `primed ${time} err`;
|
|
158
|
+
if (lastResult === 'skipped')
|
|
159
|
+
return `primed ${time} skipped`;
|
|
158
160
|
return `primed ${time} \u2713`;
|
|
159
161
|
}
|
|
160
162
|
function formatUsd(value) {
|
|
@@ -346,6 +348,10 @@ function storedResetMs(window) {
|
|
|
346
348
|
}
|
|
347
349
|
export class PrimeManager {
|
|
348
350
|
timer = null;
|
|
351
|
+
setIntervalImpl;
|
|
352
|
+
clearIntervalImpl;
|
|
353
|
+
setTimeoutImpl;
|
|
354
|
+
clearTimeoutImpl;
|
|
349
355
|
postFireRefreshTimers = new Set();
|
|
350
356
|
lastForcedCheck = new Map();
|
|
351
357
|
// Set true by `stop()` so a tick already in flight can short-circuit
|
|
@@ -373,6 +379,11 @@ export class PrimeManager {
|
|
|
373
379
|
windowActive = new Map();
|
|
374
380
|
constructor(options) {
|
|
375
381
|
this.options = options;
|
|
382
|
+
this.setIntervalImpl = options.setIntervalImpl ?? globalThis.setInterval;
|
|
383
|
+
this.clearIntervalImpl =
|
|
384
|
+
options.clearIntervalImpl ?? globalThis.clearInterval;
|
|
385
|
+
this.setTimeoutImpl = options.setTimeoutImpl ?? globalThis.setTimeout;
|
|
386
|
+
this.clearTimeoutImpl = options.clearTimeoutImpl ?? globalThis.clearTimeout;
|
|
376
387
|
}
|
|
377
388
|
updateOptions(options) {
|
|
378
389
|
this.options = options;
|
|
@@ -382,7 +393,7 @@ export class PrimeManager {
|
|
|
382
393
|
return;
|
|
383
394
|
this.stopped = false;
|
|
384
395
|
logger.trace('prime', 'manager started');
|
|
385
|
-
this.timer =
|
|
396
|
+
this.timer = this.setIntervalImpl(() => {
|
|
386
397
|
void this.tick().catch((error) => {
|
|
387
398
|
logger.warn('prime', 'tick failed', {
|
|
388
399
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -399,11 +410,12 @@ export class PrimeManager {
|
|
|
399
410
|
const hadPostFireRefresh = this.postFireRefreshTimers.size > 0;
|
|
400
411
|
this.stopped = true;
|
|
401
412
|
if (this.timer) {
|
|
402
|
-
|
|
413
|
+
this.clearIntervalImpl(this.timer);
|
|
403
414
|
this.timer = null;
|
|
404
415
|
}
|
|
405
|
-
for (const timer of this.postFireRefreshTimers)
|
|
406
|
-
|
|
416
|
+
for (const timer of this.postFireRefreshTimers) {
|
|
417
|
+
this.clearTimeoutImpl(timer);
|
|
418
|
+
}
|
|
407
419
|
this.postFireRefreshTimers.clear();
|
|
408
420
|
this.lastForcedCheck.clear();
|
|
409
421
|
if (!hadTimer && !hadPostFireRefresh)
|
|
@@ -687,7 +699,7 @@ export class PrimeManager {
|
|
|
687
699
|
if (this.stopped)
|
|
688
700
|
return;
|
|
689
701
|
if (!result.ok) {
|
|
690
|
-
//
|
|
702
|
+
// `vault-cold` is a cache-warm skip; it is not a provider failure.
|
|
691
703
|
// - `prime token refresh failed` — token-refresh failure
|
|
692
704
|
// before the request fires (reason: 'token-refresh').
|
|
693
705
|
// - `prime fire failed` — HTTP error / fetch throw /
|
|
@@ -698,6 +710,12 @@ export class PrimeManager {
|
|
|
698
710
|
error: result.error,
|
|
699
711
|
});
|
|
700
712
|
}
|
|
713
|
+
else if (result.reason === 'vault-cold') {
|
|
714
|
+
logger.debug('prime', 'prime vault credential cold', {
|
|
715
|
+
account: evaluation.label,
|
|
716
|
+
reason: result.reason,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
701
719
|
else {
|
|
702
720
|
logger.warn('prime', 'prime fire failed', {
|
|
703
721
|
account: evaluation.label,
|
|
@@ -707,7 +725,7 @@ export class PrimeManager {
|
|
|
707
725
|
}
|
|
708
726
|
this.transient.set(evaluation.id, {
|
|
709
727
|
lastPrimedAt: now,
|
|
710
|
-
lastResult: 'error',
|
|
728
|
+
lastResult: result.reason === 'vault-cold' ? 'skipped' : 'error',
|
|
711
729
|
});
|
|
712
730
|
return;
|
|
713
731
|
}
|
|
@@ -757,7 +775,7 @@ export class PrimeManager {
|
|
|
757
775
|
// Unified fire-response headers can arm the window instantly on newer
|
|
758
776
|
// trees. This branch predates header harvesting, so a delayed poll captures
|
|
759
777
|
// the reset after the usage API's observed propagation lag.
|
|
760
|
-
const timer =
|
|
778
|
+
const timer = this.setTimeoutImpl(() => {
|
|
761
779
|
this.postFireRefreshTimers.delete(timer);
|
|
762
780
|
if (this.stopped)
|
|
763
781
|
return;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type AccountStorage, type OAuthQuotaSnapshot, type QuotaFieldName, type QuotaFieldSource, type QuotaFieldSources } from './accounts.ts';
|
|
2
2
|
export declare const QUOTA_HEADER_FEED_SCHEMA_VERSION = 3;
|
|
3
3
|
export declare const QUOTA_HEADER_FEED_LEASE_MS = 180000;
|
|
4
|
+
/**
|
|
5
|
+
* Lease files are per process; each carries only the accounts whose response
|
|
6
|
+
* headers THAT process harvested. Consumers MUST union entries across all files
|
|
7
|
+
* inside `lease_horizon_ms`, deduplicating by account; "newest file wins" is wrong.
|
|
8
|
+
* `anthropic_account_uuid` is always present: null is unresolvable, while absence
|
|
9
|
+
* identifies an old producer. A fallback `account_ref` is store-local.
|
|
10
|
+
*/
|
|
4
11
|
export type QuotaHeaderFeedIdentity = {
|
|
5
12
|
identity_source: 'credential_id';
|
|
6
13
|
credential_id: string;
|
|
@@ -22,9 +29,12 @@ type QuotaHeaderFeedMetadata = {
|
|
|
22
29
|
observed_at_ms: number;
|
|
23
30
|
};
|
|
24
31
|
export type QuotaHeaderFeedEntry = QuotaHeaderFeedIdentity & QuotaHeaderFeedMetadata & {
|
|
32
|
+
/** Always present: null means UUID resolution failed; absence denotes an old producer. */
|
|
33
|
+
anthropic_account_uuid: string | null;
|
|
25
34
|
quota: QuotaHeaderFeedQuota;
|
|
26
35
|
};
|
|
27
36
|
export type QuotaHeaderFeedPublishEntry = QuotaHeaderFeedIdentity & QuotaHeaderFeedMetadata & {
|
|
37
|
+
anthropic_account_uuid: string | null;
|
|
28
38
|
quota: Omit<QuotaHeaderFeedQuota, 'provenance'> & {
|
|
29
39
|
fieldSources?: QuotaFieldSources;
|
|
30
40
|
};
|
|
@@ -44,8 +54,11 @@ export declare class QuotaHeaderFeedRegistry {
|
|
|
44
54
|
now?: () => number;
|
|
45
55
|
leaseMs?: number;
|
|
46
56
|
instanceId?: string;
|
|
57
|
+
removeFile?: (path: string) => Promise<void>;
|
|
58
|
+
beforeRemoveFile?: (path: string) => Promise<void>;
|
|
47
59
|
});
|
|
48
60
|
publish(entry: QuotaHeaderFeedPublishEntry): Promise<void>;
|
|
61
|
+
private reapStaleSiblingLeases;
|
|
49
62
|
list(): Promise<QuotaHeaderFeedEntry[]>;
|
|
50
63
|
dispose(): Promise<void>;
|
|
51
64
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { chmod, mkdir, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
|
|
2
|
+
import { chmod, lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { isOAuthAccount, QUOTA_FIELD_NAMES, } from './accounts.js';
|
|
@@ -22,6 +22,10 @@ function validIdentity(entry) {
|
|
|
22
22
|
return false;
|
|
23
23
|
if (!entry.quota || typeof entry.quota !== 'object')
|
|
24
24
|
return false;
|
|
25
|
+
if (!Object.hasOwn(entry, 'anthropic_account_uuid') ||
|
|
26
|
+
(entry.anthropic_account_uuid !== null &&
|
|
27
|
+
typeof entry.anthropic_account_uuid !== 'string'))
|
|
28
|
+
return false;
|
|
25
29
|
if (entry.identity_source === 'none') {
|
|
26
30
|
return !('credential_id' in entry) && !('account_ref' in entry);
|
|
27
31
|
}
|
|
@@ -192,17 +196,33 @@ export class QuotaHeaderFeedRegistry {
|
|
|
192
196
|
this.filePath = join(options.directory ?? getDefaultQuotaHeaderFeedDirectory(), `${instanceId}.json`);
|
|
193
197
|
}
|
|
194
198
|
publish(entry) {
|
|
195
|
-
const { accountKey, quota
|
|
196
|
-
const cleanEntry = {
|
|
197
|
-
...entryWithoutQuota,
|
|
198
|
-
quota: projectQuota(quota),
|
|
199
|
-
};
|
|
199
|
+
const { accountKey, quota } = entry;
|
|
200
200
|
try {
|
|
201
|
-
validatePublishEntry(
|
|
201
|
+
validatePublishEntry(entry);
|
|
202
202
|
}
|
|
203
203
|
catch (error) {
|
|
204
204
|
return Promise.reject(error);
|
|
205
205
|
}
|
|
206
|
+
const identity = entry.identity_source === 'credential_id'
|
|
207
|
+
? {
|
|
208
|
+
identity_source: 'credential_id',
|
|
209
|
+
credential_id: entry.credential_id,
|
|
210
|
+
}
|
|
211
|
+
: entry.identity_source === 'account_ref'
|
|
212
|
+
? {
|
|
213
|
+
identity_source: 'account_ref',
|
|
214
|
+
account_ref: entry.account_ref,
|
|
215
|
+
}
|
|
216
|
+
: { identity_source: 'none' };
|
|
217
|
+
const cleanEntry = {
|
|
218
|
+
...identity,
|
|
219
|
+
schema_version: entry.schema_version,
|
|
220
|
+
provider: entry.provider,
|
|
221
|
+
configured_account_count: entry.configured_account_count,
|
|
222
|
+
observed_at_ms: entry.observed_at_ms,
|
|
223
|
+
anthropic_account_uuid: entry.anthropic_account_uuid,
|
|
224
|
+
quota: projectQuota(quota),
|
|
225
|
+
};
|
|
206
226
|
if (!accountKey)
|
|
207
227
|
return Promise.reject(new Error('Invalid quota header feed account key'));
|
|
208
228
|
this.writeChain = this.writeChain
|
|
@@ -211,6 +231,7 @@ export class QuotaHeaderFeedRegistry {
|
|
|
211
231
|
const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
|
|
212
232
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
213
233
|
await chmod(directory, 0o700);
|
|
234
|
+
await this.reapStaleSiblingLeases(directory);
|
|
214
235
|
let entries = {};
|
|
215
236
|
try {
|
|
216
237
|
const record = JSON.parse(await readFile(this.filePath, 'utf8'));
|
|
@@ -223,7 +244,7 @@ export class QuotaHeaderFeedRegistry {
|
|
|
223
244
|
entries[accountKey] = cleanEntry;
|
|
224
245
|
const tempPath = `${this.filePath}.${randomUUID()}.tmp`;
|
|
225
246
|
try {
|
|
226
|
-
await writeFile(tempPath, `${JSON.stringify({ version: QUOTA_HEADER_FEED_SCHEMA_VERSION, entries })}\n`, { mode: 0o600 });
|
|
247
|
+
await writeFile(tempPath, `${JSON.stringify({ version: QUOTA_HEADER_FEED_SCHEMA_VERSION, lease_horizon_ms: this.options.leaseMs ?? QUOTA_HEADER_FEED_LEASE_MS, entries })}\n`, { mode: 0o600 });
|
|
227
248
|
await chmod(tempPath, 0o600);
|
|
228
249
|
await rename(tempPath, this.filePath);
|
|
229
250
|
}
|
|
@@ -233,6 +254,37 @@ export class QuotaHeaderFeedRegistry {
|
|
|
233
254
|
});
|
|
234
255
|
return this.writeChain;
|
|
235
256
|
}
|
|
257
|
+
async reapStaleSiblingLeases(directory) {
|
|
258
|
+
const now = this.options.now?.() ?? Date.now();
|
|
259
|
+
const leaseMs = this.options.leaseMs ?? QUOTA_HEADER_FEED_LEASE_MS;
|
|
260
|
+
let names;
|
|
261
|
+
try {
|
|
262
|
+
names = await readdir(directory);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
await Promise.all(names
|
|
268
|
+
.filter((name) => /^\d+-[0-9a-f-]+\.json$/i.test(name))
|
|
269
|
+
.map(async (name) => {
|
|
270
|
+
const path = join(directory, name);
|
|
271
|
+
if (path === this.filePath)
|
|
272
|
+
return;
|
|
273
|
+
try {
|
|
274
|
+
const file = await stat(path);
|
|
275
|
+
if (file.mtimeMs > now || now - file.mtimeMs < leaseMs)
|
|
276
|
+
return;
|
|
277
|
+
await this.options.beforeRemoveFile?.(path);
|
|
278
|
+
const current = await lstat(path);
|
|
279
|
+
if (current.ino !== file.ino || current.mtimeMs !== file.mtimeMs)
|
|
280
|
+
return;
|
|
281
|
+
await (this.options.removeFile ?? ((target) => rm(target)))(path);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// A missed cleanup must not prevent this process from refreshing its lease.
|
|
285
|
+
}
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
236
288
|
async list() {
|
|
237
289
|
await this.writeChain.catch(() => { });
|
|
238
290
|
const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
|
package/dist/quota-manager.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ export type QuotaRefreshResult = {
|
|
|
20
20
|
export type QuotaManagerOptions = {
|
|
21
21
|
storage: AccountStorage | null;
|
|
22
22
|
fetchImpl?: typeof fetch;
|
|
23
|
+
/** Resolve custody credentials at the actual HTTP boundary, after quota gates. */
|
|
24
|
+
fetchQuotaSnapshot?: (request: {
|
|
25
|
+
kind: 'main' | 'fallback';
|
|
26
|
+
accountId: string | undefined;
|
|
27
|
+
accessToken: string;
|
|
28
|
+
}) => Promise<OAuthQuotaSnapshot>;
|
|
23
29
|
now?: () => number;
|
|
24
30
|
onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, tokenFingerprint: string, fetchStartedAt: number, quotaErrorGeneration: number, quotaErrorClearedAt: number | undefined) => void;
|
|
25
31
|
onApiError?: (error: AccountOperationError, quotaErrorGeneration: number) => void;
|
|
@@ -39,6 +45,8 @@ export declare class QuotaManager {
|
|
|
39
45
|
private fallbackGenerations;
|
|
40
46
|
private inflightMain;
|
|
41
47
|
private inflightMainAccountId;
|
|
48
|
+
private inflightMainAccessToken;
|
|
49
|
+
private inflightMainToken;
|
|
42
50
|
private inflightFallbacks;
|
|
43
51
|
private mainLastApiError;
|
|
44
52
|
private fallbackApiErrors;
|
|
@@ -46,6 +54,7 @@ export declare class QuotaManager {
|
|
|
46
54
|
private lastApiCallAt;
|
|
47
55
|
private storage;
|
|
48
56
|
private readonly fetchImpl;
|
|
57
|
+
private readonly fetchQuotaSnapshot;
|
|
49
58
|
private readonly now;
|
|
50
59
|
private readonly onMainQuotaFetched;
|
|
51
60
|
private readonly onApiError;
|
|
@@ -61,10 +70,13 @@ export declare class QuotaManager {
|
|
|
61
70
|
* Cached fallback quota entry, scoped to the configured account id.
|
|
62
71
|
*/
|
|
63
72
|
getFallback(accountId: string, account?: Pick<OAuthAccount, 'authLineageId'>): QuotaEntry | null;
|
|
73
|
+
/** A host transport resolves scoped credentials at dispatch time. */
|
|
74
|
+
canFetchWithoutAccessToken(): boolean;
|
|
64
75
|
getAllFallbacks(): Map<string, QuotaEntry>;
|
|
65
76
|
setMain(mainAccountId: string | undefined, entry: QuotaEntry): void;
|
|
77
|
+
clearMain(): void;
|
|
66
78
|
/** Bind quota state to the account behind the current main credential. */
|
|
67
|
-
setMainQuotaAccountIdentity(accountIdentity: string | undefined, strict?: boolean): number | undefined;
|
|
79
|
+
setMainQuotaAccountIdentity(accountIdentity: string | undefined, strict?: boolean, force?: boolean): number | undefined;
|
|
68
80
|
setFallback(accountId: string, entry: QuotaEntry, account: Pick<OAuthAccount, 'authLineageId'> | undefined): void;
|
|
69
81
|
clearFallback(accountId: string): void;
|
|
70
82
|
pushMainFromHeaders(mainAccountId: string | undefined, incoming: OAuthQuotaSnapshot): QuotaEntry;
|
|
@@ -122,6 +134,8 @@ export declare class QuotaManager {
|
|
|
122
134
|
private static quotaLockName;
|
|
123
135
|
private bindFallbackLineage;
|
|
124
136
|
private acceptFallbackQuotaObservation;
|
|
137
|
+
private isClosed;
|
|
138
|
+
close(): void;
|
|
125
139
|
/**
|
|
126
140
|
* Serialize API calls through a shared gate so only one
|
|
127
141
|
* quota API request runs at a time, with a minimum gap
|