@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.
Files changed (48) hide show
  1. package/dist/accounts.d.ts +59 -2
  2. package/dist/accounts.js +666 -115
  3. package/dist/auth.js +3 -0
  4. package/dist/cachekeep.d.ts +18 -1
  5. package/dist/cachekeep.js +108 -82
  6. package/dist/cch.d.ts +11 -1
  7. package/dist/cch.js +48 -3
  8. package/dist/claude-code.d.ts +7 -1
  9. package/dist/claude-code.js +18 -3
  10. package/dist/claustrum-enrollment-registry.d.ts +17 -0
  11. package/dist/claustrum-enrollment-registry.js +194 -0
  12. package/dist/claustrum-enrollment.d.ts +70 -0
  13. package/dist/claustrum-enrollment.js +485 -0
  14. package/dist/claustrum-scoped-roster.d.ts +23 -0
  15. package/dist/claustrum-scoped-roster.js +194 -0
  16. package/dist/claustrum-scoped-runtime.d.ts +28 -0
  17. package/dist/claustrum-scoped-runtime.js +230 -0
  18. package/dist/claustrum-scoped.d.ts +39 -0
  19. package/dist/claustrum-scoped.js +193 -0
  20. package/dist/claustrum.d.ts +149 -6
  21. package/dist/claustrum.js +990 -48
  22. package/dist/commands/account.d.ts +40 -12
  23. package/dist/commands/account.js +138 -9
  24. package/dist/constants.d.ts +3 -2
  25. package/dist/constants.js +3 -2
  26. package/dist/custom-headers.d.ts +3 -0
  27. package/dist/custom-headers.js +102 -0
  28. package/dist/fast.js +1 -1
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +7 -0
  31. package/dist/logger.js +3 -1
  32. package/dist/model-remap.d.ts +27 -0
  33. package/dist/model-remap.js +72 -0
  34. package/dist/models.d.ts +19 -0
  35. package/dist/models.js +48 -12
  36. package/dist/oauth-profile.d.ts +2 -1
  37. package/dist/oauth-profile.js +6 -0
  38. package/dist/prime.d.ts +11 -3
  39. package/dist/prime.js +25 -7
  40. package/dist/quota-header-feed.d.ts +13 -0
  41. package/dist/quota-header-feed.js +60 -8
  42. package/dist/quota-manager.d.ts +15 -1
  43. package/dist/quota-manager.js +73 -22
  44. package/dist/relay.d.ts +8 -0
  45. package/dist/relay.js +72 -12
  46. package/dist/sticky-routing.d.ts +2 -0
  47. package/dist/sticky-routing.js +19 -1
  48. package/package.json +3 -2
package/dist/auth.js CHANGED
@@ -37,6 +37,9 @@ export class ClaudeOAuthRefreshError extends Error {
37
37
  }
38
38
  export async function refreshClaudeOAuthToken(input) {
39
39
  assertNotCustodyTombstone(input.refreshToken, 'anthropic');
40
+ if (typeof input.refreshToken !== 'string' || !input.refreshToken.trim()) {
41
+ throw new Error('Local OAuth refresh credential is unavailable');
42
+ }
40
43
  const fetchImpl = input.fetchImpl ?? fetch;
41
44
  const maxRetries = input.maxRetries ?? 2;
42
45
  const baseDelayMs = input.baseDelayMs ?? 500;
@@ -76,8 +76,15 @@ export type CacheKeepTarget = {
76
76
  consecutiveFailures: number;
77
77
  dayKey: string;
78
78
  oauthAccountId?: string;
79
+ oauthAccountIdentity?: string;
80
+ accountStoragePath?: string;
79
81
  isSubagent: boolean;
80
82
  };
83
+ export type CacheKeepPrewarmAttempt = {
84
+ id: number;
85
+ /** One deadline covers credential authorization and the HTTP attempt. */
86
+ signal?: AbortSignal;
87
+ };
81
88
  export type CacheKeepPrewarmResult = {
82
89
  ok: true;
83
90
  usage?: {
@@ -96,6 +103,7 @@ export declare class CacheKeepManager {
96
103
  private readonly targets;
97
104
  private timer;
98
105
  private tickPromise;
106
+ private nextPrewarmAttemptId;
99
107
  constructor(options: {
100
108
  loadStorage: () => Promise<AccountStorage | null>;
101
109
  fetchImpl?: typeof fetch;
@@ -103,7 +111,7 @@ export declare class CacheKeepManager {
103
111
  prewarmTimeoutMs?: number;
104
112
  setIntervalImpl?: typeof globalThis.setInterval;
105
113
  clearIntervalImpl?: typeof globalThis.clearInterval;
106
- prepareHeaders?: (headers: Headers, target: CacheKeepTarget) => Promise<Headers> | Headers;
114
+ prepareHeaders?: (headers: Headers, target: CacheKeepTarget, attempt: CacheKeepPrewarmAttempt) => Promise<Headers | undefined> | Headers | undefined;
107
115
  onTrackedSessionsChanged?: (sessions: readonly CacheKeepTrackedSession[]) => Promise<void> | void;
108
116
  prepareBody?: (bodyText: string, target: CacheKeepTarget) => string | Promise<string>;
109
117
  onResponse?: (input: {
@@ -112,6 +120,11 @@ export declare class CacheKeepManager {
112
120
  status: number;
113
121
  data: unknown;
114
122
  receivedAt: number;
123
+ attempt: CacheKeepPrewarmAttempt;
124
+ }) => void | Promise<void>;
125
+ onComplete?: (input: {
126
+ target: CacheKeepTarget;
127
+ attempt: CacheKeepPrewarmAttempt;
115
128
  }) => void | Promise<void>;
116
129
  });
117
130
  start(): void;
@@ -137,6 +150,8 @@ export declare class CacheKeepManager {
137
150
  storage: AccountStorage | null;
138
151
  cacheMode: string;
139
152
  oauthAccountId?: string;
153
+ oauthAccountIdentity?: string;
154
+ accountStoragePath?: string;
140
155
  isSubagent?: boolean;
141
156
  }): {
142
157
  tracked: boolean;
@@ -151,6 +166,8 @@ export declare class CacheKeepManager {
151
166
  headers: Headers;
152
167
  bodyText: string;
153
168
  oauthAccountId?: string;
169
+ oauthAccountIdentity?: string;
170
+ accountStoragePath?: string;
154
171
  isSubagent?: boolean;
155
172
  }): Promise<CacheKeepPrewarmResult>;
156
173
  tick(): Promise<void>;
package/dist/cachekeep.js CHANGED
@@ -1,4 +1,4 @@
1
- import { signRequestBody } from './cch.js';
1
+ import { signRequestBody, stripBillingLineageFromBody } from './cch.js';
2
2
  import { orderClaudeCodeBody } from './claude-code.js';
3
3
  import { dumpDirectRequest, dumpResponseArtifact } from './dump.js';
4
4
  import { logger } from './logger.js';
@@ -216,6 +216,7 @@ export async function buildCacheKeepPrewarmBody(bodyText) {
216
216
  return { ok: false, reason: 'body has no explicit cache breakpoints' };
217
217
  }
218
218
  const warm = structuredClone(body);
219
+ stripBillingLineageFromBody(warm);
219
220
  warm.max_tokens = 0;
220
221
  delete warm.stream;
221
222
  const thinking = warm.thinking;
@@ -247,6 +248,7 @@ export class CacheKeepManager {
247
248
  targets = new Map();
248
249
  timer = null;
249
250
  tickPromise = null;
251
+ nextPrewarmAttemptId = 0;
250
252
  constructor(options) {
251
253
  this.options = options;
252
254
  }
@@ -374,6 +376,8 @@ export class CacheKeepManager {
374
376
  consecutiveFailures: 0,
375
377
  dayKey: today,
376
378
  oauthAccountId: input.oauthAccountId,
379
+ oauthAccountIdentity: input.oauthAccountIdentity,
380
+ accountStoragePath: input.accountStoragePath,
377
381
  isSubagent: input.isSubagent ?? false,
378
382
  });
379
383
  this.pruneTargets(now, today);
@@ -396,6 +400,8 @@ export class CacheKeepManager {
396
400
  consecutiveFailures: 0,
397
401
  dayKey: '',
398
402
  oauthAccountId: input.oauthAccountId,
403
+ oauthAccountIdentity: input.oauthAccountIdentity,
404
+ accountStoragePath: input.accountStoragePath,
399
405
  isSubagent: input.isSubagent ?? false,
400
406
  };
401
407
  return this.sendPrewarm(target);
@@ -454,96 +460,116 @@ export class CacheKeepManager {
454
460
  this.publishTrackedSessions();
455
461
  }
456
462
  async sendPrewarm(target) {
457
- let bodyText = target.bodyText;
458
- if (this.options.prepareBody) {
463
+ const attempt = {
464
+ id: ++this.nextPrewarmAttemptId,
465
+ signal: AbortSignal.timeout(this.options.prewarmTimeoutMs ?? CACHE_KEEP_PREWARM_TIMEOUT_MS),
466
+ };
467
+ try {
468
+ let bodyText = target.bodyText;
469
+ if (this.options.prepareBody) {
470
+ try {
471
+ bodyText = await this.options.prepareBody(bodyText, target);
472
+ }
473
+ catch (error) {
474
+ logger.warn('cachekeep', 'prepare body failed', {
475
+ session: target.id,
476
+ error: error instanceof Error ? error.message : String(error),
477
+ });
478
+ }
479
+ }
480
+ const preparedTarget = { ...target, bodyText };
481
+ const prewarm = await buildCacheKeepPrewarmBody(bodyText);
482
+ if (!prewarm.ok)
483
+ return prewarm;
484
+ const fetchImpl = this.options.fetchImpl ?? fetch;
485
+ const prewarmTarget = { ...preparedTarget, bodyText: prewarm.bodyText };
486
+ const headers = this.options.prepareHeaders
487
+ ? await this.options.prepareHeaders(new Headers(target.headers), prewarmTarget, attempt)
488
+ : new Headers(target.headers);
489
+ if (!headers) {
490
+ return {
491
+ ok: false,
492
+ reason: 'OAuth cache prewarm credential is unavailable',
493
+ transient: true,
494
+ };
495
+ }
496
+ headers.delete('content-length');
497
+ headers.delete('transfer-encoding');
498
+ let response;
459
499
  try {
460
- bodyText = await this.options.prepareBody(bodyText, target);
500
+ response = await fetchImpl(target.url, {
501
+ method: 'POST',
502
+ headers,
503
+ body: prewarm.bodyText,
504
+ signal: attempt.signal,
505
+ });
461
506
  }
462
507
  catch (error) {
463
- logger.warn('cachekeep', 'prepare body failed', {
508
+ return {
509
+ ok: false,
510
+ reason: error instanceof Error ? error.message : String(error),
511
+ transient: true,
512
+ };
513
+ }
514
+ const receivedAt = this.options.now?.() ?? Date.now();
515
+ const raw = await response.text().catch(() => '');
516
+ let data = null;
517
+ try {
518
+ data = raw ? JSON.parse(raw) : null;
519
+ }
520
+ catch { }
521
+ try {
522
+ await this.options.onResponse?.({
523
+ target,
524
+ bodyText: prewarm.bodyText,
525
+ status: response.status,
526
+ data,
527
+ receivedAt,
528
+ attempt,
529
+ });
530
+ }
531
+ catch { }
532
+ try {
533
+ const dumpHandle = await dumpDirectRequest({
534
+ affinity: target.id,
535
+ route: 'cachekeep',
536
+ status: response.status,
537
+ bodyText: prewarm.bodyText,
538
+ url: target.url,
539
+ method: 'POST',
540
+ headers,
541
+ tag: 'cachekeep',
542
+ });
543
+ await dumpResponseArtifact(dumpHandle, {
544
+ status: response.status,
545
+ message: data,
546
+ });
547
+ }
548
+ catch (error) {
549
+ logger.debug('cachekeep', 'dump failed', {
464
550
  session: target.id,
465
551
  error: error instanceof Error ? error.message : String(error),
466
552
  });
467
553
  }
554
+ if (!response.ok) {
555
+ return {
556
+ ok: false,
557
+ reason: raw || `HTTP ${response.status}`,
558
+ status: response.status,
559
+ };
560
+ }
561
+ const objectData = data && typeof data === 'object' && !Array.isArray(data)
562
+ ? data
563
+ : null;
564
+ const usage = objectData?.usage;
565
+ return { ok: true, ...(usage && { usage }) };
468
566
  }
469
- const preparedTarget = { ...target, bodyText };
470
- const prewarm = await buildCacheKeepPrewarmBody(bodyText);
471
- if (!prewarm.ok)
472
- return prewarm;
473
- const fetchImpl = this.options.fetchImpl ?? fetch;
474
- const prewarmTarget = { ...preparedTarget, bodyText: prewarm.bodyText };
475
- const headers = this.options.prepareHeaders
476
- ? await this.options.prepareHeaders(new Headers(target.headers), prewarmTarget)
477
- : new Headers(target.headers);
478
- headers.delete('content-length');
479
- headers.delete('transfer-encoding');
480
- let response;
481
- try {
482
- response = await fetchImpl(target.url, {
483
- method: 'POST',
484
- headers,
485
- body: prewarm.bodyText,
486
- signal: AbortSignal.timeout(this.options.prewarmTimeoutMs ?? CACHE_KEEP_PREWARM_TIMEOUT_MS),
487
- });
488
- }
489
- catch (error) {
490
- return {
491
- ok: false,
492
- reason: error instanceof Error ? error.message : String(error),
493
- transient: true,
494
- };
495
- }
496
- const receivedAt = this.options.now?.() ?? Date.now();
497
- const raw = await response.text().catch(() => '');
498
- let data = null;
499
- try {
500
- data = raw ? JSON.parse(raw) : null;
501
- }
502
- catch { }
503
- try {
504
- await this.options.onResponse?.({
505
- target,
506
- bodyText: prewarm.bodyText,
507
- status: response.status,
508
- data,
509
- receivedAt,
510
- });
511
- }
512
- catch { }
513
- try {
514
- const dumpHandle = await dumpDirectRequest({
515
- affinity: target.id,
516
- route: 'cachekeep',
517
- status: response.status,
518
- bodyText: prewarm.bodyText,
519
- url: target.url,
520
- method: 'POST',
521
- headers,
522
- tag: 'cachekeep',
523
- });
524
- await dumpResponseArtifact(dumpHandle, {
525
- status: response.status,
526
- message: data,
527
- });
528
- }
529
- catch (error) {
530
- logger.debug('cachekeep', 'dump failed', {
531
- session: target.id,
532
- error: error instanceof Error ? error.message : String(error),
533
- });
534
- }
535
- if (!response.ok) {
536
- return {
537
- ok: false,
538
- reason: raw || `HTTP ${response.status}`,
539
- status: response.status,
540
- };
567
+ finally {
568
+ try {
569
+ await this.options.onComplete?.({ target, attempt });
570
+ }
571
+ catch { }
541
572
  }
542
- const objectData = data && typeof data === 'object' && !Array.isArray(data)
543
- ? data
544
- : null;
545
- const usage = objectData?.usage;
546
- return { ok: true, ...(usage && { usage }) };
547
573
  }
548
574
  async prewarm(target, now) {
549
575
  const result = await this.sendPrewarm(target);
package/dist/cch.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ export interface BillingLineageFields {
2
+ previousRequestId?: string;
3
+ promptId?: string;
4
+ }
5
+ export declare function isValidAnthropicRequestId(value: string): boolean;
6
+ export declare function isValidBillingPromptId(value: string): boolean;
1
7
  type Message = {
2
8
  role?: string;
3
9
  content?: string | Array<{
@@ -38,5 +44,9 @@ export declare function computeCcVersionSuffix(firstUserText: string, version?:
38
44
  * Build the billing header with a cch placeholder.
39
45
  * signRequestBody() must run after final request serialization to replace it.
40
46
  */
41
- export declare function buildBillingHeaderValue(messages: Message[], version: string | undefined, entrypoint: string, pinnedFirstUserText?: string): string;
47
+ export declare function buildBillingHeaderValue(messages: Message[], version: string | undefined, entrypoint: string, pinnedFirstUserText?: string, lineage?: BillingLineageFields): string;
48
+ /** Remove request-scoped Claude Code lineage from a billing header string. */
49
+ export declare function stripBillingLineageFields(value: string): string;
50
+ /** Remove request-scoped Claude Code lineage before reusing a request body. */
51
+ export declare function stripBillingLineageFromBody(body: unknown): number;
42
52
  export {};
package/dist/cch.js CHANGED
@@ -1,6 +1,15 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import xxhashInit from 'xxhash-wasm';
3
3
  import { CCH_POSITIONS, CCH_SALT, CLAUDE_CODE_VERSION } from './constants.js';
4
+ const ANTHROPIC_REQUEST_ID_PATTERN = /^req_[A-Za-z0-9_-]{8,128}$/;
5
+ const PROMPT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
+ const BILLING_LINEAGE_FIELD_PATTERN = / cc_(?:prev_req|prompt_id)=[^;\r\n]*;/g;
7
+ export function isValidAnthropicRequestId(value) {
8
+ return ANTHROPIC_REQUEST_ID_PATTERN.test(value);
9
+ }
10
+ export function isValidBillingPromptId(value) {
11
+ return PROMPT_ID_PATTERN.test(value);
12
+ }
4
13
  const CCH_SEED = 0x4d659218e32a3268n;
5
14
  const CCH_PLACEHOLDER = 'cch=00000;';
6
15
  export const CCH_PATTERN = /\bcch=([0-9a-f]{5});/;
@@ -118,10 +127,46 @@ export function computeCcVersionSuffix(firstUserText, version = CLAUDE_CODE_VERS
118
127
  * Build the billing header with a cch placeholder.
119
128
  * signRequestBody() must run after final request serialization to replace it.
120
129
  */
121
- export function buildBillingHeaderValue(messages, version = CLAUDE_CODE_VERSION, entrypoint, pinnedFirstUserText) {
130
+ export function buildBillingHeaderValue(messages, version = CLAUDE_CODE_VERSION, entrypoint, pinnedFirstUserText, lineage) {
122
131
  const suffix = computeCcVersionSuffix(pinnedFirstUserText ?? extractFirstUserMessageText(messages), version);
123
- return ('x-anthropic-billing-header: ' +
132
+ let value = 'x-anthropic-billing-header: ' +
124
133
  `cc_version=${version}.${suffix}; ` +
125
134
  `cc_entrypoint=${entrypoint}; ` +
126
- 'cch=00000;');
135
+ 'cch=00000;';
136
+ if (lineage?.previousRequestId &&
137
+ isValidAnthropicRequestId(lineage.previousRequestId)) {
138
+ value += ` cc_prev_req=${lineage.previousRequestId};`;
139
+ }
140
+ if (lineage?.promptId && isValidBillingPromptId(lineage.promptId)) {
141
+ value += ` cc_prompt_id=${lineage.promptId};`;
142
+ }
143
+ return value;
144
+ }
145
+ /** Remove request-scoped Claude Code lineage from a billing header string. */
146
+ export function stripBillingLineageFields(value) {
147
+ return value.replace(BILLING_LINEAGE_FIELD_PATTERN, '');
148
+ }
149
+ /** Remove request-scoped Claude Code lineage before reusing a request body. */
150
+ export function stripBillingLineageFromBody(body) {
151
+ if (!body || typeof body !== 'object')
152
+ return 0;
153
+ const system = body.system;
154
+ if (!Array.isArray(system))
155
+ return 0;
156
+ let stripped = 0;
157
+ for (const block of system) {
158
+ if (!block || typeof block !== 'object')
159
+ continue;
160
+ const text = block.text;
161
+ if (typeof text !== 'string' ||
162
+ !text.startsWith('x-anthropic-billing-header:')) {
163
+ continue;
164
+ }
165
+ const clean = stripBillingLineageFields(text);
166
+ if (clean === text)
167
+ continue;
168
+ block.text = clean;
169
+ stripped += 1;
170
+ }
171
+ return stripped;
127
172
  }
@@ -1,12 +1,18 @@
1
+ export type ProviderAccountUuid = string & {
2
+ readonly __providerAccountUuid: unique symbol;
3
+ };
1
4
  export type ClaudeCodeIdentity = {
2
5
  deviceId: string;
3
6
  accountIdentity?: string;
4
- accountUuid?: string;
7
+ accountUuid?: ProviderAccountUuid;
5
8
  sessionId: string;
6
9
  };
7
10
  export declare function setBounded<K, V>(map: Map<K, V>, key: K, value: V, limit?: number): void;
8
11
  export declare function getClaudeCodeIdentity(seed: string): ClaudeCodeIdentity;
9
12
  export declare function resetClaudeCodeIdentityCachesForTest(): void;
13
+ /** Use the identity already verified by the credential custodian, without an
14
+ * additional bootstrap request carrying a previously authorized token. */
15
+ export declare function getClaudeCodeIdentityForVerifiedAccount(accountIdentity: string, accountUuid: ProviderAccountUuid): ClaudeCodeIdentity;
10
16
  export declare function resolveClaudeCodeIdentity(accessToken: string, model: string | undefined, accountIdentity: string | undefined): Promise<ClaudeCodeIdentity>;
11
17
  export declare function buildClaudeCodeMetadataUserId(identity: ClaudeCodeIdentity): string | null;
12
18
  export declare function applyClaudeCodeMetadata(body: Record<string, unknown>, identity: ClaudeCodeIdentity): boolean;
@@ -101,7 +101,9 @@ async function fetchClaudeCodeAccountUuid(accessToken, model) {
101
101
  return null;
102
102
  const data = (await response.json().catch(() => null));
103
103
  const accountUuid = data?.oauth_account?.account_uuid;
104
- return typeof accountUuid === 'string' && accountUuid ? accountUuid : null;
104
+ return typeof accountUuid === 'string' && accountUuid
105
+ ? accountUuid
106
+ : null;
105
107
  }
106
108
  catch {
107
109
  return null;
@@ -110,6 +112,18 @@ async function fetchClaudeCodeAccountUuid(accessToken, model) {
110
112
  clearTimeout(timeout);
111
113
  }
112
114
  }
115
+ /** Use the identity already verified by the credential custodian, without an
116
+ * additional bootstrap request carrying a previously authorized token. */
117
+ export function getClaudeCodeIdentityForVerifiedAccount(accountIdentity, accountUuid) {
118
+ if (!accountIdentity.trim() || !accountUuid.trim()) {
119
+ throw new Error('A verified provider account identity is required');
120
+ }
121
+ const base = identityCache.get(accountCacheKey(accountUuid, accountIdentity)) ??
122
+ getClaudeCodeIdentity(explicitCacheKey(accountIdentity));
123
+ const identity = { ...base, accountIdentity, accountUuid };
124
+ cacheAccountIdentity(identity, accountIdentity, accountUuid);
125
+ return identity;
126
+ }
113
127
  export async function resolveClaudeCodeIdentity(accessToken, model, accountIdentity) {
114
128
  const stableAccountIdentity = accountIdentity?.trim() || undefined;
115
129
  const cacheKey = stableAccountIdentity
@@ -130,8 +144,9 @@ export async function resolveClaudeCodeIdentity(accessToken, model, accountIdent
130
144
  setBounded(identityCache, cacheKey, identity);
131
145
  setBounded(identityCache, accessToken, identity);
132
146
  }
133
- if (!accessToken.startsWith('sk-ant-oat'))
134
- return identity;
147
+ if (!accessToken.startsWith('sk-ant-oat')) {
148
+ return clearCachedAccountUuid(cacheKey, identity);
149
+ }
135
150
  const now = Date.now();
136
151
  // A slot-stable identity survives account replacement; bootstrap is the
137
152
  // account lookup that must be repeated for each credential presented to it.
@@ -0,0 +1,17 @@
1
+ import { type ClaustrumEnrollmentConnection, type ClaustrumEnrollmentPaths, type ClaustrumEnrollmentStatus } from './claustrum-enrollment.js';
2
+ export declare function getHostClaustrumEnrollmentPaths(host: 'opencode' | 'pi', env?: NodeJS.ProcessEnv, cwd?: string): ClaustrumEnrollmentPaths;
3
+ export interface ClaustrumEnrollmentAdoption {
4
+ status(): ClaustrumEnrollmentStatus;
5
+ reconcileNow(): Promise<void>;
6
+ resetTerminal(): Promise<'reset' | 'idle' | 'refused-pending' | 'refused-approved' | 'busy'>;
7
+ release(): void;
8
+ }
9
+ export declare function adoptClaustrumEnrollment(options: {
10
+ proposedName: string;
11
+ paths: ClaustrumEnrollmentPaths;
12
+ connect: () => Promise<ClaustrumEnrollmentConnection>;
13
+ onStatus?: (status: ClaustrumEnrollmentStatus) => void;
14
+ setTimeoutImpl?: typeof setTimeout;
15
+ clearTimeoutImpl?: typeof clearTimeout;
16
+ pollIntervalMs?: number;
17
+ }): ClaustrumEnrollmentAdoption;
@@ -0,0 +1,194 @@
1
+ import { homedir } from 'node:os';
2
+ import { isAbsolute, join, resolve } from 'node:path';
3
+ import { ClaustrumEnrollmentManager, getClaustrumEnrollmentPaths, } from './claustrum-enrollment.js';
4
+ const REGISTRY_SYMBOL = Symbol.for('cortexkit.anthropic-auth.claustrum-enrollments.v2');
5
+ const PENDING_POLL_MS = 5_000;
6
+ const RETRY_POLL_MS = 60_000;
7
+ const UNAVAILABLE_RETRY_MS = 5_000;
8
+ export function getHostClaustrumEnrollmentPaths(host, env = process.env, cwd = process.cwd()) {
9
+ const configured = env[`${host.toUpperCase()}_ANTHROPIC_AUTH_CLAUSTRUM_ENROLLMENT_FILE`]?.trim();
10
+ const tokenPath = configured
11
+ ? isAbsolute(configured)
12
+ ? configured
13
+ : resolve(cwd, configured)
14
+ : join(env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'cortexkit', 'anthropic-auth', `${host}-enrollment.json`);
15
+ return getClaustrumEnrollmentPaths(tokenPath);
16
+ }
17
+ function registry() {
18
+ const global = globalThis;
19
+ if (!global[REGISTRY_SYMBOL])
20
+ global[REGISTRY_SYMBOL] = new Map();
21
+ return global[REGISTRY_SYMBOL];
22
+ }
23
+ function notify(entry, status) {
24
+ entry.status = status;
25
+ for (const listener of entry.leases.values())
26
+ listener(status);
27
+ }
28
+ function schedule(entry, delayMs) {
29
+ if (entry.closed || entry.timer)
30
+ return;
31
+ entry.timer = entry.setTimeoutImpl(() => {
32
+ entry.timer = undefined;
33
+ void run(entry);
34
+ }, delayMs);
35
+ if (typeof entry.timer === 'object' &&
36
+ entry.timer &&
37
+ 'unref' in entry.timer) {
38
+ entry.timer.unref();
39
+ }
40
+ }
41
+ function nextDelay(status, pollIntervalMs) {
42
+ if (pollIntervalMs === 0 ||
43
+ status.state === 'approved' ||
44
+ status.state === 'denied' ||
45
+ status.state === 'blocked') {
46
+ return undefined;
47
+ }
48
+ if (status.state === 'pending') {
49
+ return status.retryCode ? RETRY_POLL_MS : pollIntervalMs;
50
+ }
51
+ return UNAVAILABLE_RETRY_MS;
52
+ }
53
+ async function getClient(entry) {
54
+ if (entry.closed)
55
+ throw new Error('Claustrum enrollment registry is closed');
56
+ if (entry.client)
57
+ return entry.client;
58
+ if (!entry.clientPromise) {
59
+ entry.clientPromise = entry
60
+ .connect()
61
+ .then((client) => {
62
+ if (entry.closed) {
63
+ client.close();
64
+ throw new Error('Claustrum enrollment registry is closed');
65
+ }
66
+ entry.client = client;
67
+ return client;
68
+ })
69
+ .finally(() => {
70
+ entry.clientPromise = undefined;
71
+ });
72
+ }
73
+ return entry.clientPromise;
74
+ }
75
+ async function run(entry) {
76
+ if (entry.closed)
77
+ return;
78
+ if (entry.run)
79
+ return entry.run;
80
+ entry.run = (async () => {
81
+ let status;
82
+ try {
83
+ status = await entry.manager.reconcile();
84
+ }
85
+ catch {
86
+ status = {
87
+ state: 'unavailable',
88
+ proposedName: entry.proposedName,
89
+ // Unknown transport/storage errors may echo one-shot bearer material.
90
+ code: 'unavailable',
91
+ };
92
+ // A transport or filesystem failure is not a durable ceremony verdict. Keep
93
+ // retrying while the process is alive; only the manager persists protocol-terminal
94
+ // outcomes such as denied, superseded, or already_consumed.
95
+ if (entry.closed)
96
+ return;
97
+ notify(entry, status);
98
+ if (entry.pollIntervalMs !== 0)
99
+ schedule(entry, UNAVAILABLE_RETRY_MS);
100
+ return;
101
+ }
102
+ if (entry.closed)
103
+ return;
104
+ notify(entry, status);
105
+ const delay = nextDelay(status, entry.pollIntervalMs);
106
+ if (delay !== undefined)
107
+ schedule(entry, delay);
108
+ })().finally(() => {
109
+ entry.run = undefined;
110
+ });
111
+ return entry.run;
112
+ }
113
+ export function adoptClaustrumEnrollment(options) {
114
+ const key = resolve(options.paths.tokenPath);
115
+ const entries = registry();
116
+ let entry = entries.get(key);
117
+ if (entry && entry.proposedName !== options.proposedName) {
118
+ throw new Error('Claustrum enrollment path is already owned by a different consumer');
119
+ }
120
+ if (!entry) {
121
+ const connect = options.connect;
122
+ const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
123
+ const clearTimeoutImpl = options.clearTimeoutImpl ?? clearTimeout;
124
+ const lazyClient = {
125
+ enrollPropose: async (input) => {
126
+ const current = entry;
127
+ const client = await getClient(current);
128
+ if (current.closed)
129
+ throw new Error('Claustrum enrollment registry is closed');
130
+ return client.enrollPropose(input);
131
+ },
132
+ enrollPoll: async (input) => {
133
+ const current = entry;
134
+ const client = await getClient(current);
135
+ if (current.closed)
136
+ throw new Error('Claustrum enrollment registry is closed');
137
+ // Once a poll has been sent, let the manager durably collect its one-shot
138
+ // approval even if release wins meanwhile. Only later calls are fenced.
139
+ return client.enrollPoll(input);
140
+ },
141
+ };
142
+ entry = {
143
+ key,
144
+ proposedName: options.proposedName,
145
+ leases: new Map(),
146
+ manager: new ClaustrumEnrollmentManager({
147
+ client: lazyClient,
148
+ paths: options.paths,
149
+ proposedName: options.proposedName,
150
+ }),
151
+ connect,
152
+ setTimeoutImpl,
153
+ clearTimeoutImpl,
154
+ pollIntervalMs: options.pollIntervalMs ?? PENDING_POLL_MS,
155
+ status: { state: 'idle' },
156
+ closed: false,
157
+ };
158
+ entries.set(key, entry);
159
+ }
160
+ const lease = Symbol(key);
161
+ entry.leases.set(lease, options.onStatus ?? (() => { }));
162
+ options.onStatus?.(entry.status);
163
+ void run(entry);
164
+ let released = false;
165
+ return {
166
+ status: () => entry?.status ?? { state: 'idle' },
167
+ reconcileNow: () => (entry ? run(entry) : Promise.resolve()),
168
+ resetTerminal: async () => {
169
+ if (!entry)
170
+ return 'idle';
171
+ const result = await entry.manager.resetTerminal();
172
+ if (result === 'reset') {
173
+ notify(entry, { state: 'idle' });
174
+ await run(entry);
175
+ }
176
+ return result;
177
+ },
178
+ release: () => {
179
+ if (released || !entry)
180
+ return;
181
+ released = true;
182
+ entry.leases.delete(lease);
183
+ if (entry.leases.size > 0)
184
+ return;
185
+ entry.closed = true;
186
+ if (entry.timer) {
187
+ entry.clearTimeoutImpl(entry.timer);
188
+ entry.timer = undefined;
189
+ }
190
+ entry.client?.close();
191
+ entries.delete(key);
192
+ },
193
+ };
194
+ }