@cortexkit/anthropic-auth-core 1.20.0 → 1.21.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.
@@ -1,11 +1,13 @@
1
1
  export type ClaudeCodeIdentity = {
2
2
  deviceId: string;
3
+ accountIdentity?: string;
3
4
  accountUuid?: string;
4
5
  sessionId: string;
5
6
  };
6
7
  export declare function setBounded<K, V>(map: Map<K, V>, key: K, value: V, limit?: number): void;
7
8
  export declare function getClaudeCodeIdentity(seed: string): ClaudeCodeIdentity;
8
- export declare function resolveClaudeCodeIdentity(accessToken: string, model?: string): Promise<ClaudeCodeIdentity>;
9
+ export declare function resetClaudeCodeIdentityCachesForTest(): void;
10
+ export declare function resolveClaudeCodeIdentity(accessToken: string, model: string | undefined, accountIdentity: string | undefined): Promise<ClaudeCodeIdentity>;
9
11
  export declare function buildClaudeCodeMetadataUserId(identity: ClaudeCodeIdentity): string | null;
10
12
  export declare function applyClaudeCodeMetadata(body: Record<string, unknown>, identity: ClaudeCodeIdentity): boolean;
11
13
  export declare const CLAUDE_CODE_FULL_AGENT_BETAS: readonly ['oauth-2025-04-20', 'interleaved-thinking-2025-05-14', 'thinking-token-count-2026-05-13', 'context-management-2025-06-27', 'prompt-caching-scope-2026-01-05', 'claude-code-20250219', 'advisor-tool-2026-03-01', 'advanced-tool-use-2025-11-20', 'extended-cache-ttl-2025-04-11', 'cache-diagnosis-2026-04-07'];
@@ -10,6 +10,16 @@ export function setBounded(map, key, value, limit = IDENTITY_CACHE_LIMIT) {
10
10
  }
11
11
  map.set(key, value);
12
12
  }
13
+ function clearCachedAccountUuid(key, identity) {
14
+ if (!identity.accountUuid)
15
+ return identity;
16
+ if (identityCache.get(key) !== identity) {
17
+ return { ...identity, accountUuid: undefined };
18
+ }
19
+ const cleared = { ...identity, accountUuid: undefined };
20
+ setBounded(identityCache, key, cleared);
21
+ return cleared;
22
+ }
13
23
  export function getClaudeCodeIdentity(seed) {
14
24
  const cacheKey = seed || 'anonymous';
15
25
  const cached = identityCache.get(cacheKey);
@@ -26,6 +36,46 @@ const BOOTSTRAP_IDENTITY_CACHE_TTL_MS = 24 * 60 * 60_000;
26
36
  const BOOTSTRAP_IDENTITY_NEGATIVE_TTL_MS = 5 * 60_000;
27
37
  const bootstrapFetches = new Map();
28
38
  const bootstrapResults = new Map();
39
+ export function resetClaudeCodeIdentityCachesForTest() {
40
+ identityCache.clear();
41
+ bootstrapFetches.clear();
42
+ bootstrapResults.clear();
43
+ }
44
+ function compatibilityCacheKey(accessToken) {
45
+ return `compat:${accessToken || 'anonymous'}`;
46
+ }
47
+ function explicitCacheKey(accountIdentity) {
48
+ return `identity:${accountIdentity}`;
49
+ }
50
+ function accountCacheKey(accountUuid, accountIdentity) {
51
+ return `account:${accountUuid}:${accountIdentity}`;
52
+ }
53
+ function adoptCachedIdentity(cached, accountIdentity, accountUuid) {
54
+ if (!cached)
55
+ return undefined;
56
+ if (cached.accountIdentity !== undefined &&
57
+ cached.accountIdentity !== accountIdentity) {
58
+ return undefined;
59
+ }
60
+ if (accountUuid !== undefined &&
61
+ cached.accountUuid !== undefined &&
62
+ cached.accountUuid !== accountUuid) {
63
+ return undefined;
64
+ }
65
+ if (cached.accountIdentity === accountIdentity &&
66
+ (accountUuid === undefined || cached.accountUuid === accountUuid)) {
67
+ return cached;
68
+ }
69
+ return {
70
+ ...cached,
71
+ accountIdentity,
72
+ ...(accountUuid !== undefined && { accountUuid }),
73
+ };
74
+ }
75
+ function cacheAccountIdentity(identity, accountIdentity, accountUuid) {
76
+ setBounded(identityCache, explicitCacheKey(accountIdentity), identity);
77
+ setBounded(identityCache, accountCacheKey(accountUuid, accountIdentity), identity);
78
+ }
29
79
  async function fetchClaudeCodeAccountUuid(accessToken, model) {
30
80
  if (!accessToken.startsWith('sk-ant-oat'))
31
81
  return null;
@@ -60,47 +110,100 @@ async function fetchClaudeCodeAccountUuid(accessToken, model) {
60
110
  clearTimeout(timeout);
61
111
  }
62
112
  }
63
- export async function resolveClaudeCodeIdentity(accessToken, model) {
64
- const identity = getClaudeCodeIdentity(accessToken);
113
+ export async function resolveClaudeCodeIdentity(accessToken, model, accountIdentity) {
114
+ const stableAccountIdentity = accountIdentity?.trim() || undefined;
115
+ const cacheKey = stableAccountIdentity
116
+ ? explicitCacheKey(stableAccountIdentity)
117
+ : compatibilityCacheKey(accessToken);
118
+ let identity;
119
+ if (stableAccountIdentity) {
120
+ const cachedIdentity = adoptCachedIdentity(identityCache.get(cacheKey) ?? identityCache.get(accessToken), stableAccountIdentity);
121
+ identity = cachedIdentity ?? getClaudeCodeIdentity(cacheKey);
122
+ identity = adoptCachedIdentity(identity, stableAccountIdentity) ?? identity;
123
+ setBounded(identityCache, cacheKey, identity);
124
+ }
125
+ else {
126
+ identity =
127
+ identityCache.get(cacheKey) ??
128
+ identityCache.get(accessToken) ??
129
+ getClaudeCodeIdentity(cacheKey);
130
+ setBounded(identityCache, cacheKey, identity);
131
+ setBounded(identityCache, accessToken, identity);
132
+ }
65
133
  if (!accessToken.startsWith('sk-ant-oat'))
66
134
  return identity;
67
135
  const now = Date.now();
68
- const cachedResult = bootstrapResults.get(accessToken);
136
+ // A slot-stable identity survives account replacement; bootstrap is the
137
+ // account lookup that must be repeated for each credential presented to it.
138
+ const bootstrapKey = `${cacheKey}:${accessToken}`;
139
+ const cachedResult = bootstrapResults.get(bootstrapKey);
69
140
  if (cachedResult && cachedResult.expiresAt > now) {
70
- if (!cachedResult.accountUuid)
141
+ if (!cachedResult.accountUuid) {
142
+ identity = clearCachedAccountUuid(cacheKey, identity);
143
+ return identity;
144
+ }
145
+ if (!stableAccountIdentity) {
146
+ if (identity.accountUuid === cachedResult.accountUuid)
147
+ return identity;
148
+ identity = { ...identity, accountUuid: cachedResult.accountUuid };
149
+ setBounded(identityCache, bootstrapKey, identity);
71
150
  return identity;
72
- const accountCacheKey = `account:${cachedResult.accountUuid}`;
73
- const accountIdentity = identityCache.get(accountCacheKey);
74
- if (accountIdentity) {
75
- setBounded(identityCache, accessToken, accountIdentity);
76
- return accountIdentity;
77
151
  }
152
+ const cachedAccountIdentity = adoptCachedIdentity(identityCache.get(accountCacheKey(cachedResult.accountUuid, stableAccountIdentity)) ?? identityCache.get(`account:${cachedResult.accountUuid}`), stableAccountIdentity, cachedResult.accountUuid);
153
+ if (cachedAccountIdentity) {
154
+ identity = cachedAccountIdentity;
155
+ cacheAccountIdentity(identity, stableAccountIdentity, cachedResult.accountUuid);
156
+ return identity;
157
+ }
158
+ if (identity.accountUuid === cachedResult.accountUuid)
159
+ return identity;
160
+ identity = {
161
+ ...identity,
162
+ accountIdentity: stableAccountIdentity,
163
+ accountUuid: cachedResult.accountUuid,
164
+ };
165
+ cacheAccountIdentity(identity, stableAccountIdentity, cachedResult.accountUuid);
166
+ return identity;
78
167
  }
79
- let fetchPromise = bootstrapFetches.get(accessToken);
168
+ let fetchPromise = bootstrapFetches.get(bootstrapKey);
80
169
  if (!fetchPromise) {
81
170
  fetchPromise = fetchClaudeCodeAccountUuid(accessToken, model);
82
- bootstrapFetches.set(accessToken, fetchPromise);
171
+ setBounded(bootstrapFetches, bootstrapKey, fetchPromise);
83
172
  }
84
173
  const accountUuid = await fetchPromise.finally(() => {
85
- bootstrapFetches.delete(accessToken);
174
+ if (bootstrapFetches.get(bootstrapKey) === fetchPromise) {
175
+ bootstrapFetches.delete(bootstrapKey);
176
+ }
86
177
  });
87
- setBounded(bootstrapResults, accessToken, {
178
+ setBounded(bootstrapResults, bootstrapKey, {
88
179
  accountUuid,
89
180
  expiresAt: now +
90
181
  (accountUuid
91
182
  ? BOOTSTRAP_IDENTITY_CACHE_TTL_MS
92
183
  : BOOTSTRAP_IDENTITY_NEGATIVE_TTL_MS),
93
184
  });
94
- if (!accountUuid)
185
+ if (!accountUuid) {
186
+ identity = clearCachedAccountUuid(cacheKey, identity);
187
+ return identity;
188
+ }
189
+ if (!stableAccountIdentity) {
190
+ identity = { ...identity, accountUuid };
191
+ setBounded(identityCache, bootstrapKey, identity);
95
192
  return identity;
96
- const accountCacheKey = `account:${accountUuid}`;
97
- const accountIdentity = identityCache.get(accountCacheKey);
98
- if (accountIdentity) {
99
- setBounded(identityCache, accessToken, accountIdentity);
100
- return accountIdentity;
101
193
  }
102
- identity.accountUuid = accountUuid;
103
- setBounded(identityCache, accountCacheKey, identity);
194
+ const cachedAccountIdentity = adoptCachedIdentity(identityCache.get(accountCacheKey(accountUuid, stableAccountIdentity)) ??
195
+ identityCache.get(`account:${accountUuid}`), stableAccountIdentity, accountUuid);
196
+ if (cachedAccountIdentity) {
197
+ identity = cachedAccountIdentity;
198
+ cacheAccountIdentity(identity, stableAccountIdentity, accountUuid);
199
+ return identity;
200
+ }
201
+ identity = {
202
+ ...identity,
203
+ accountIdentity: stableAccountIdentity,
204
+ accountUuid,
205
+ };
206
+ cacheAccountIdentity(identity, stableAccountIdentity, accountUuid);
104
207
  return identity;
105
208
  }
106
209
  function isRecord(value) {
package/dist/dump.d.ts CHANGED
@@ -71,5 +71,6 @@ export declare function dumpRelayRequest(input: {
71
71
  export declare function dumpResponseArtifact(handle: DumpHandle | null, input: {
72
72
  status: number;
73
73
  message: unknown;
74
+ complete?: boolean;
74
75
  }): Promise<void>;
75
76
  export {};
package/dist/dump.js CHANGED
@@ -592,7 +592,10 @@ export async function dumpResponseArtifact(handle, input) {
592
592
  !Array.isArray(input.message)
593
593
  ? input.message
594
594
  : {};
595
- const artifact = { status: input.status };
595
+ const artifact = {
596
+ status: input.status,
597
+ stream_complete: input.complete ?? true,
598
+ };
596
599
  if (typeof message.id === 'string' && message.id.length > 0)
597
600
  artifact.message_id = message.id;
598
601
  if (typeof message.model === 'string' && message.model.length > 0)
@@ -601,6 +604,8 @@ export async function dumpResponseArtifact(handle, input) {
601
604
  artifact.usage = message.usage;
602
605
  if (Object.hasOwn(message, 'diagnostics'))
603
606
  artifact.diagnostics = message.diagnostics;
607
+ if (typeof message.stop_reason === 'string' && message.stop_reason.length > 0)
608
+ artifact.stop_reason = message.stop_reason;
604
609
  try {
605
610
  await writeDumpFile(handle.responsePath, `${JSON.stringify(artifact, null, 2)}\n`);
606
611
  }
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export * from './oauth-profile.ts';
17
17
  export * from './pkce.ts';
18
18
  export * from './prime.ts';
19
19
  export * from './provider.ts';
20
+ export * from './quota-header-feed.ts';
20
21
  export * from './quota-headers.ts';
21
22
  export * from './quota-manager.ts';
22
23
  export * from './quotas.ts';
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ export * from './oauth-profile.js';
17
17
  export * from './pkce.js';
18
18
  export * from './prime.js';
19
19
  export * from './provider.js';
20
+ export * from './quota-header-feed.js';
20
21
  export * from './quota-headers.js';
21
22
  export * from './quota-manager.js';
22
23
  export * from './quotas.js';
@@ -2,10 +2,11 @@ import type { OAuthAccountProfile } from './accounts.ts';
2
2
  export declare const PROFILE_TTL_MS: number;
3
3
  export declare function fetchOAuthAccountProfile(input: {
4
4
  accessToken: string;
5
+ accountIdentity?: string;
5
6
  fetchImpl?: typeof fetch;
6
7
  now?: () => number;
7
8
  signal?: AbortSignal;
8
9
  }): Promise<OAuthAccountProfile>;
9
- export declare function oauthProfileMatchesToken(profile: OAuthAccountProfile | undefined, accessToken: string): boolean;
10
+ export declare function oauthProfileMatchesIdentity(profile: OAuthAccountProfile | undefined, accountIdentity: string | undefined): boolean;
10
11
  export declare function oauthProfileIsFresh(profile: OAuthAccountProfile | undefined, now?: number): boolean;
11
12
  export declare function formatOAuthAccountTier(profile: OAuthAccountProfile | undefined): string | undefined;
@@ -1,4 +1,3 @@
1
- import { tokenFingerprint } from './quota-manager.js';
2
1
  const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
3
2
  export const PROFILE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
4
3
  export async function fetchOAuthAccountProfile(input) {
@@ -27,11 +26,18 @@ export async function fetchOAuthAccountProfile(input) {
27
26
  tier,
28
27
  orgType,
29
28
  checkedAt: input.now?.() ?? Date.now(),
30
- tokenFingerprint: tokenFingerprint(input.accessToken),
29
+ ...(input.accountIdentity !== undefined && {
30
+ accountIdentity: input.accountIdentity,
31
+ }),
31
32
  };
32
33
  }
33
- export function oauthProfileMatchesToken(profile, accessToken) {
34
- return profile?.tokenFingerprint === tokenFingerprint(accessToken);
34
+ export function oauthProfileMatchesIdentity(profile, accountIdentity) {
35
+ if (!profile)
36
+ return false;
37
+ if (accountIdentity === undefined)
38
+ return true;
39
+ return (profile.accountIdentity === undefined ||
40
+ profile.accountIdentity === accountIdentity);
35
41
  }
36
42
  export function oauthProfileIsFresh(profile, now = Date.now()) {
37
43
  return Boolean(profile &&
@@ -0,0 +1,40 @@
1
+ import { type AccountStorage, type OAuthQuotaSnapshot } from './accounts.ts';
2
+ export declare const QUOTA_HEADER_FEED_SCHEMA_VERSION = 2;
3
+ export declare const QUOTA_HEADER_FEED_LEASE_MS = 180000;
4
+ export type QuotaHeaderFeedIdentity = {
5
+ identity_source: 'credential_id';
6
+ credential_id: string;
7
+ } | {
8
+ identity_source: 'account_ref';
9
+ account_ref: string;
10
+ } | {
11
+ identity_source: 'none';
12
+ };
13
+ export type QuotaHeaderFeedEntry = QuotaHeaderFeedIdentity & {
14
+ schema_version: typeof QUOTA_HEADER_FEED_SCHEMA_VERSION;
15
+ provider: 'anthropic';
16
+ configured_account_count: number;
17
+ observed_at_ms: number;
18
+ quota: Pick<OAuthQuotaSnapshot, 'five_hour' | 'seven_day' | 'bindingWindow' | 'fallbackAdvised' | 'scoped' | 'extraUsage'>;
19
+ };
20
+ export declare function getDefaultQuotaHeaderFeedDirectory(): string;
21
+ export declare function configuredAnthropicOAuthAccountCount(input: {
22
+ storage: AccountStorage | null;
23
+ mainOAuthConfigured: boolean;
24
+ }): number;
25
+ export declare class QuotaHeaderFeedRegistry {
26
+ private readonly options;
27
+ private readonly filePath;
28
+ private writeChain;
29
+ constructor(options?: {
30
+ directory?: string;
31
+ now?: () => number;
32
+ leaseMs?: number;
33
+ instanceId?: string;
34
+ });
35
+ publish(entry: QuotaHeaderFeedEntry & {
36
+ accountKey: string;
37
+ }): Promise<void>;
38
+ list(): Promise<QuotaHeaderFeedEntry[]>;
39
+ dispose(): Promise<void>;
40
+ }
@@ -0,0 +1,248 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { isOAuthAccount, } from './accounts.js';
6
+ export const QUOTA_HEADER_FEED_SCHEMA_VERSION = 2;
7
+ export const QUOTA_HEADER_FEED_LEASE_MS = 180_000;
8
+ export function getDefaultQuotaHeaderFeedDirectory() {
9
+ return (process.env.OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR ??
10
+ join(tmpdir(), 'opencode-anthropic-auth', 'quota-header-feed'));
11
+ }
12
+ export function configuredAnthropicOAuthAccountCount(input) {
13
+ const fallbackCount = input.storage?.accounts.filter(isOAuthAccount).length ?? 0;
14
+ return fallbackCount + (input.mainOAuthConfigured ? 1 : 0);
15
+ }
16
+ function validIdentity(entry) {
17
+ if (entry.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
18
+ entry.provider !== 'anthropic')
19
+ return false;
20
+ if (!Number.isFinite(entry.configured_account_count) ||
21
+ !Number.isFinite(entry.observed_at_ms))
22
+ return false;
23
+ if (!entry.quota || typeof entry.quota !== 'object')
24
+ return false;
25
+ if (entry.identity_source === 'none') {
26
+ return !('credential_id' in entry) && !('account_ref' in entry);
27
+ }
28
+ if (entry.identity_source === 'credential_id') {
29
+ return (typeof entry.credential_id === 'string' &&
30
+ entry.credential_id.length > 0 &&
31
+ entry.credential_id !== 'main' &&
32
+ !('account_ref' in entry));
33
+ }
34
+ if (entry.identity_source === 'account_ref') {
35
+ return (typeof entry.account_ref === 'string' &&
36
+ entry.account_ref.length > 0 &&
37
+ entry.account_ref !== 'main' &&
38
+ !('credential_id' in entry));
39
+ }
40
+ return false;
41
+ }
42
+ function validatePublishEntry(entry) {
43
+ if (entry.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
44
+ entry.provider !== 'anthropic' ||
45
+ !validIdentity(entry)) {
46
+ throw new Error('Invalid quota header feed entry');
47
+ }
48
+ }
49
+ function finiteNumber(value) {
50
+ return typeof value === 'number' && Number.isFinite(value);
51
+ }
52
+ function projectQuotaWindow(value) {
53
+ if (!value || typeof value !== 'object')
54
+ return undefined;
55
+ const candidate = value;
56
+ if (!finiteNumber(candidate.usedPercent) ||
57
+ !finiteNumber(candidate.remainingPercent) ||
58
+ !finiteNumber(candidate.checkedAt)) {
59
+ return undefined;
60
+ }
61
+ return {
62
+ usedPercent: candidate.usedPercent,
63
+ remainingPercent: candidate.remainingPercent,
64
+ ...(typeof candidate.resetsAt === 'string' && {
65
+ resetsAt: candidate.resetsAt,
66
+ }),
67
+ checkedAt: candidate.checkedAt,
68
+ };
69
+ }
70
+ function projectScopedQuotaWindow(value) {
71
+ if (!value || typeof value !== 'object')
72
+ return undefined;
73
+ const candidate = value;
74
+ const window = projectQuotaWindow(candidate);
75
+ if (!window ||
76
+ typeof candidate.id !== 'string' ||
77
+ !candidate.id.trim() ||
78
+ typeof candidate.title !== 'string' ||
79
+ !candidate.title.trim() ||
80
+ typeof candidate.modelName !== 'string' ||
81
+ !candidate.modelName.trim()) {
82
+ return undefined;
83
+ }
84
+ return {
85
+ ...window,
86
+ id: candidate.id,
87
+ title: candidate.title,
88
+ ...(typeof candidate.modelId === 'string' &&
89
+ candidate.modelId.trim() && {
90
+ modelId: candidate.modelId,
91
+ }),
92
+ modelName: candidate.modelName,
93
+ };
94
+ }
95
+ function projectQuotaMoney(value) {
96
+ if (!value || typeof value !== 'object')
97
+ return undefined;
98
+ const candidate = value;
99
+ if (!finiteNumber(candidate.amountMinor) ||
100
+ typeof candidate.currency !== 'string' ||
101
+ !candidate.currency.trim() ||
102
+ !finiteNumber(candidate.exponent)) {
103
+ return undefined;
104
+ }
105
+ return {
106
+ amountMinor: candidate.amountMinor,
107
+ currency: candidate.currency,
108
+ exponent: candidate.exponent,
109
+ };
110
+ }
111
+ function projectExtraUsage(value) {
112
+ if (!value || typeof value !== 'object')
113
+ return undefined;
114
+ const candidate = value;
115
+ const used = projectQuotaMoney(candidate.used);
116
+ const limit = projectQuotaMoney(candidate.limit);
117
+ if (!used || !limit || typeof candidate.exhausted !== 'boolean') {
118
+ return undefined;
119
+ }
120
+ return {
121
+ used,
122
+ limit,
123
+ ...(finiteNumber(candidate.utilizationPercent) && {
124
+ utilizationPercent: candidate.utilizationPercent,
125
+ }),
126
+ ...(typeof candidate.severity === 'string' && {
127
+ severity: candidate.severity,
128
+ }),
129
+ exhausted: candidate.exhausted,
130
+ };
131
+ }
132
+ function projectQuota(quota) {
133
+ const fiveHour = projectQuotaWindow(quota.five_hour);
134
+ const sevenDay = projectQuotaWindow(quota.seven_day);
135
+ const scoped = Array.isArray(quota.scoped)
136
+ ? quota.scoped
137
+ .map(projectScopedQuotaWindow)
138
+ .filter((entry) => entry != null)
139
+ : undefined;
140
+ const extraUsage = projectExtraUsage(quota.extraUsage);
141
+ return {
142
+ ...(fiveHour && { five_hour: fiveHour }),
143
+ ...(sevenDay && { seven_day: sevenDay }),
144
+ ...(typeof quota.bindingWindow === 'string' && {
145
+ bindingWindow: quota.bindingWindow,
146
+ }),
147
+ ...(typeof quota.fallbackAdvised === 'boolean' && {
148
+ fallbackAdvised: quota.fallbackAdvised,
149
+ }),
150
+ ...(scoped && { scoped }),
151
+ ...(extraUsage && { extraUsage }),
152
+ };
153
+ }
154
+ export class QuotaHeaderFeedRegistry {
155
+ options;
156
+ filePath;
157
+ writeChain = Promise.resolve();
158
+ constructor(options = {}) {
159
+ this.options = options;
160
+ const instanceId = options.instanceId ?? `${process.pid}-${randomUUID()}`;
161
+ this.filePath = join(options.directory ?? getDefaultQuotaHeaderFeedDirectory(), `${instanceId}.json`);
162
+ }
163
+ publish(entry) {
164
+ const { accountKey, quota, ...entryWithoutQuota } = entry;
165
+ const cleanEntry = {
166
+ ...entryWithoutQuota,
167
+ quota: projectQuota(quota),
168
+ };
169
+ try {
170
+ validatePublishEntry(cleanEntry);
171
+ }
172
+ catch (error) {
173
+ return Promise.reject(error);
174
+ }
175
+ if (!accountKey)
176
+ return Promise.reject(new Error('Invalid quota header feed account key'));
177
+ this.writeChain = this.writeChain
178
+ .catch(() => { })
179
+ .then(async () => {
180
+ const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
181
+ await mkdir(directory, { recursive: true, mode: 0o700 });
182
+ await chmod(directory, 0o700);
183
+ let entries = {};
184
+ try {
185
+ const record = JSON.parse(await readFile(this.filePath, 'utf8'));
186
+ if (record.version === QUOTA_HEADER_FEED_SCHEMA_VERSION &&
187
+ record.entries &&
188
+ typeof record.entries === 'object')
189
+ entries = record.entries;
190
+ }
191
+ catch { }
192
+ entries[accountKey] = cleanEntry;
193
+ const tempPath = `${this.filePath}.${randomUUID()}.tmp`;
194
+ try {
195
+ await writeFile(tempPath, `${JSON.stringify({ version: QUOTA_HEADER_FEED_SCHEMA_VERSION, entries })}\n`, { mode: 0o600 });
196
+ await chmod(tempPath, 0o600);
197
+ await rename(tempPath, this.filePath);
198
+ }
199
+ finally {
200
+ await rm(tempPath, { force: true });
201
+ }
202
+ });
203
+ return this.writeChain;
204
+ }
205
+ async list() {
206
+ await this.writeChain.catch(() => { });
207
+ const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
208
+ const now = this.options.now?.() ?? Date.now();
209
+ const leaseMs = this.options.leaseMs ?? QUOTA_HEADER_FEED_LEASE_MS;
210
+ let names;
211
+ try {
212
+ names = (await readdir(directory)).filter((name) => name.endsWith('.json'));
213
+ }
214
+ catch {
215
+ return [];
216
+ }
217
+ const newest = new Map();
218
+ await Promise.all(names.map(async (name) => {
219
+ try {
220
+ const record = JSON.parse(await readFile(join(directory, name), 'utf8'));
221
+ if (record.version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
222
+ !record.entries ||
223
+ typeof record.entries !== 'object')
224
+ return;
225
+ for (const [accountKey, candidate] of Object.entries(record.entries)) {
226
+ if (!candidate ||
227
+ typeof candidate !== 'object' ||
228
+ candidate.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
229
+ candidate.provider !== 'anthropic' ||
230
+ !validIdentity(candidate))
231
+ continue;
232
+ const observed = candidate.observed_at_ms;
233
+ if (observed > now || now - observed >= leaseMs)
234
+ continue;
235
+ const existing = newest.get(accountKey);
236
+ if (!existing || observed > existing.observed_at_ms)
237
+ newest.set(accountKey, candidate);
238
+ }
239
+ }
240
+ catch { }
241
+ }));
242
+ return [...newest.values()].sort((a, b) => a.observed_at_ms - b.observed_at_ms);
243
+ }
244
+ async dispose() {
245
+ await this.writeChain.catch(() => { });
246
+ await rm(this.filePath, { force: true });
247
+ }
248
+ }
@@ -49,11 +49,13 @@ export function normalizeQuotaHeaders(headers, now = Date.now()) {
49
49
  return snapshot;
50
50
  }
51
51
  export function mergeHeaderQuotaSnapshot(existing, incoming) {
52
+ const { scoped: existingScoped, extraUsage: existingExtraUsage, ...existingWithoutPollOwnedFields } = existing ?? {};
53
+ const { scoped: _incomingScoped, extraUsage: _incomingExtraUsage, ...incomingWithoutPollOwnedFields } = incoming;
52
54
  return {
53
- ...existing,
54
- ...incoming,
55
- scoped: existing && 'scoped' in existing ? existing.scoped : undefined,
56
- extraUsage: existing?.extraUsage,
55
+ ...existingWithoutPollOwnedFields,
56
+ ...incomingWithoutPollOwnedFields,
57
+ ...(Array.isArray(existingScoped) && { scoped: existingScoped }),
58
+ ...(existingExtraUsage != null && { extraUsage: existingExtraUsage }),
57
59
  bindingWindow: existing?.bindingWindowSource === 'poll'
58
60
  ? existing.bindingWindow
59
61
  : (incoming.bindingWindow ?? existing?.bindingWindow),