@sentropic/llm-mesh 0.24.1 → 0.26.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 (99) hide show
  1. package/README.md +192 -0
  2. package/dist/catalog.d.ts +92 -0
  3. package/dist/catalog.d.ts.map +1 -1
  4. package/dist/catalog.js +47 -0
  5. package/dist/catalog.js.map +1 -1
  6. package/dist/enrollment/claude-code.d.ts +27 -5
  7. package/dist/enrollment/claude-code.d.ts.map +1 -1
  8. package/dist/enrollment/claude-code.js +224 -10
  9. package/dist/enrollment/claude-code.js.map +1 -1
  10. package/dist/enrollment/codex.d.ts +7 -0
  11. package/dist/enrollment/codex.d.ts.map +1 -1
  12. package/dist/enrollment/codex.js +74 -19
  13. package/dist/enrollment/codex.js.map +1 -1
  14. package/dist/enrollment/index.d.ts +1 -1
  15. package/dist/enrollment/index.d.ts.map +1 -1
  16. package/dist/enrollment/pkce.js +1 -1
  17. package/dist/errors.d.ts +19 -0
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +51 -4
  20. package/dist/errors.js.map +1 -1
  21. package/dist/execution-features.d.ts +34 -0
  22. package/dist/execution-features.d.ts.map +1 -0
  23. package/dist/execution-features.js +84 -0
  24. package/dist/execution-features.js.map +1 -0
  25. package/dist/generated-model-council.d.ts +2 -2
  26. package/dist/generated-model-council.d.ts.map +1 -1
  27. package/dist/generated-model-council.js +5 -1
  28. package/dist/generated-model-council.js.map +1 -1
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +4 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/native-messages.d.ts +130 -0
  34. package/dist/native-messages.d.ts.map +1 -0
  35. package/dist/native-messages.js +66 -0
  36. package/dist/native-messages.js.map +1 -0
  37. package/dist/providers.d.ts +5 -5
  38. package/dist/providers.d.ts.map +1 -1
  39. package/dist/providers.js +12 -5
  40. package/dist/providers.js.map +1 -1
  41. package/dist/responses-wire.d.ts +76 -0
  42. package/dist/responses-wire.d.ts.map +1 -0
  43. package/dist/responses-wire.js +35 -0
  44. package/dist/responses-wire.js.map +1 -0
  45. package/dist/route-attempt.d.ts +4 -0
  46. package/dist/route-attempt.d.ts.map +1 -1
  47. package/dist/route-attempt.js +8 -1
  48. package/dist/route-attempt.js.map +1 -1
  49. package/dist/route-health.d.ts +11 -0
  50. package/dist/route-health.d.ts.map +1 -1
  51. package/dist/route-health.js +32 -0
  52. package/dist/route-health.js.map +1 -1
  53. package/dist/route-planner-state.d.ts +33 -2
  54. package/dist/route-planner-state.d.ts.map +1 -1
  55. package/dist/route-planner-state.js +20 -1
  56. package/dist/route-planner-state.js.map +1 -1
  57. package/dist/route-planner.d.ts +21 -1
  58. package/dist/route-planner.d.ts.map +1 -1
  59. package/dist/route-planner.js +112 -33
  60. package/dist/route-planner.js.map +1 -1
  61. package/dist/route-quote.d.ts +16 -2
  62. package/dist/route-quote.d.ts.map +1 -1
  63. package/dist/route-quote.js +31 -5
  64. package/dist/route-quote.js.map +1 -1
  65. package/dist/route-selection.d.ts +13 -0
  66. package/dist/route-selection.d.ts.map +1 -1
  67. package/dist/route-selection.js +30 -5
  68. package/dist/route-selection.js.map +1 -1
  69. package/dist/routing-contracts.d.ts +42 -3
  70. package/dist/routing-contracts.d.ts.map +1 -1
  71. package/dist/routing-targets.d.ts +26 -0
  72. package/dist/routing-targets.d.ts.map +1 -1
  73. package/dist/routing-targets.js +96 -23
  74. package/dist/routing-targets.js.map +1 -1
  75. package/dist/service/facade.d.ts +3 -0
  76. package/dist/service/facade.d.ts.map +1 -1
  77. package/dist/service/facade.js +7 -1
  78. package/dist/service/facade.js.map +1 -1
  79. package/dist/service/local-account-transport-service.d.ts +18 -0
  80. package/dist/service/local-account-transport-service.d.ts.map +1 -1
  81. package/dist/service/local-account-transport-service.js +284 -46
  82. package/dist/service/local-account-transport-service.js.map +1 -1
  83. package/dist/transport/claude-code-runtime-client.d.ts +88 -0
  84. package/dist/transport/claude-code-runtime-client.d.ts.map +1 -0
  85. package/dist/transport/claude-code-runtime-client.js +305 -0
  86. package/dist/transport/claude-code-runtime-client.js.map +1 -0
  87. package/dist/transport/codex-runtime-client.d.ts.map +1 -1
  88. package/dist/transport/codex-runtime-client.js +24 -4
  89. package/dist/transport/codex-runtime-client.js.map +1 -1
  90. package/dist/transport/codex-runtime-wire.d.ts.map +1 -1
  91. package/dist/transport/codex-runtime-wire.js +131 -27
  92. package/dist/transport/codex-runtime-wire.js.map +1 -1
  93. package/dist/transport/mistral-runtime-client.d.ts.map +1 -1
  94. package/dist/transport/mistral-runtime-client.js +191 -9
  95. package/dist/transport/mistral-runtime-client.js.map +1 -1
  96. package/dist/transport/muse-runtime-client.d.ts.map +1 -1
  97. package/dist/transport/muse-runtime-client.js +221 -10
  98. package/dist/transport/muse-runtime-client.js.map +1 -1
  99. package/package.json +1 -1
@@ -13,18 +13,26 @@ export declare class LocalAccountTransportService {
13
13
  private readonly accountsMap;
14
14
  private readonly credentialVersions;
15
15
  private readonly refreshInFlight;
16
+ private readonly publicWrites;
16
17
  private readonly accountRemovalBarrierRefs;
17
18
  private readonly claimedOwnerScopes;
18
19
  /** enrollmentId → providerId, so poll completion reaches the owning provider. */
19
20
  private readonly enrollmentProviders;
21
+ private readonly pendingClaudeAccounts;
22
+ private claudePersistenceTail;
23
+ private readonly claudeSessions;
20
24
  private routeAttemptSequence;
21
25
  constructor(keyring: KeyringAdapter, providers: Map<string, EnrollmentProvider>, configResolver: ConfigResolver, legacyAccountOwnerScopeRef?: string | undefined);
22
26
  enroll(providerId: string, input: StartEnrollmentInput): Promise<EnrollmentSession>;
27
+ completeClaudeEnrollment(enrollmentId: string, code: string, ownerScopeRef: string): Promise<EnrollmentCompletion>;
28
+ completeClaudeCredentialImport(credentialJson: string, ownerScopeRef: string): Promise<EnrollmentCompletion>;
23
29
  waitForCallback(enrollmentId: string): Promise<EnrollmentCompletion>;
24
30
  pollForCompletion(enrollmentId: string): Promise<EnrollmentCompletion>;
25
31
  completeMuseImport(enrollmentId: string, code: string, ownerScopeRef: string): Promise<EnrollmentCompletion>;
26
32
  completeMuseDirectImport(apiKey: string, ownerScopeRef: string): Promise<EnrollmentCompletion>;
27
33
  completeMuseDeviceImport(enrollmentId: string, ownerScopeRef: string, maxAttempts?: number): Promise<EnrollmentCompletion>;
34
+ private hasClaudeGrant;
35
+ private persistClaudeCompletion;
28
36
  cancel(enrollmentId: string): Promise<void>;
29
37
  listAccounts(ownerScope: string): Promise<readonly AccountPublic[]>;
30
38
  removeAccount(accountId: string, ownerScope: string): Promise<{
@@ -36,12 +44,22 @@ export declare class LocalAccountTransportService {
36
44
  release(acquisition: AccountTransportAcquisition): Promise<void>;
37
45
  createRouteDirectory(runtime: Pick<LlmMesh, 'generate' | 'stream'>): AccountDirectoryPort;
38
46
  private listRouteDiagnostics;
47
+ private isCredentialExpired;
48
+ /**
49
+ * True when the account cannot recover on its own: its status already
50
+ * flipped to reauth_required (a refresh failed), or its ACCESS token is
51
+ * expired with no refresh token to renew it. A merely expired access
52
+ * token with a live refresh token stays ready — acquire() refreshes it
53
+ * before dispatch.
54
+ */
55
+ private requiresReauth;
39
56
  private listRouteAccounts;
40
57
  private routeAttempt;
41
58
  private diagnosticAccountRef;
42
59
  private completeEnrollment;
43
60
  private refreshToken;
44
61
  private persistCredential;
62
+ private writePublicRecord;
45
63
  private persistAccountState;
46
64
  private parseAccountIndex;
47
65
  private requireOwnerScope;
@@ -1 +1 @@
1
- {"version":3,"file":"local-account-transport-service.d.ts","sourceRoot":"","sources":["../../src/service/local-account-transport-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"local-account-transport-service.d.ts","sourceRoot":"","sources":["../../src/service/local-account-transport-service.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { AccountTransportAcquireError, InMemoryAccountTransportCoordinator, } from '../account-transports.js';
2
2
  import { MUSE_DIRECT_BILLING_TYPE } from '../enrollment/muse.js';
3
+ import { ClaudeRefreshPreparationError } from '../enrollment/claude-code.js';
3
4
  import { listModelProfilesByProvider } from '../catalog.js';
4
5
  // Cloud Code does not currently return a per-account model inventory during
5
6
  // enrollment. Advertise only the model proven executable by the live transport
@@ -9,6 +10,7 @@ const VERIFIED_CLOUD_CODE_MODEL_IDS = [
9
10
  'claude-opus-4-6-thinking',
10
11
  'gemini-3.7-flash',
11
12
  'gemini-3.8-flash',
13
+ 'gemini-4-argon',
12
14
  'gemini-3.1-pro',
13
15
  ];
14
16
  const supportedModelIdsForAccount = (account) => {
@@ -37,10 +39,14 @@ export class LocalAccountTransportService {
37
39
  accountsMap = new Map();
38
40
  credentialVersions = new Map();
39
41
  refreshInFlight = new Map();
42
+ publicWrites = new Map();
40
43
  accountRemovalBarrierRefs = new Map();
41
44
  claimedOwnerScopes = new Map();
42
45
  /** enrollmentId → providerId, so poll completion reaches the owning provider. */
43
46
  enrollmentProviders = new Map();
47
+ pendingClaudeAccounts = new Set();
48
+ claudePersistenceTail = Promise.resolve();
49
+ claudeSessions = new Map();
44
50
  routeAttemptSequence = 0;
45
51
  constructor(keyring, providers, configResolver, legacyAccountOwnerScopeRef) {
46
52
  this.keyring = keyring;
@@ -55,10 +61,55 @@ export class LocalAccountTransportService {
55
61
  if (!provider) {
56
62
  throw new Error(`Enrollment provider '${providerId}' not registered`);
57
63
  }
58
- const session = await provider.start(input);
59
- this.enrollmentProviders.set(session.enrollmentId, providerId);
64
+ if (providerId !== 'claude-code') {
65
+ const session = await provider.start(input);
66
+ this.enrollmentProviders.set(session.enrollmentId, providerId);
67
+ return session;
68
+ }
69
+ const ownerScope = this.requireOwnerScope(input.ownerScope);
70
+ const session = await provider.start({ ...input, ownerScope });
71
+ const expiresAt = Date.parse(session.expiresAt);
72
+ const timer = setTimeout(() => { void this.cancel(session.enrollmentId); }, Math.max(0, expiresAt - Date.now()));
73
+ timer.unref?.();
74
+ this.claudeSessions.set(session.enrollmentId, { ownerScope, expiresAt, completing: false, timer });
60
75
  return session;
61
76
  }
77
+ async completeClaudeEnrollment(enrollmentId, code, ownerScopeRef) {
78
+ const owner = this.requireOwnerScope(ownerScopeRef);
79
+ const session = this.claudeSessions.get(enrollmentId);
80
+ const provider = this.providers.get('claude-code');
81
+ if (!provider || !session || session.ownerScope !== owner || session.completing) {
82
+ throw new Error('Claude enrollment session unavailable for owner');
83
+ }
84
+ session.completing = true;
85
+ const assertCurrent = () => {
86
+ if (this.claudeSessions.get(enrollmentId) !== session || Date.now() >= session.expiresAt) {
87
+ throw new Error('Claude enrollment cancelled or expired');
88
+ }
89
+ };
90
+ try {
91
+ assertCurrent();
92
+ const credential = await provider.complete({ enrollmentId, code });
93
+ assertCurrent();
94
+ return await this.persistClaudeCompletion(credential, owner, provider, assertCurrent);
95
+ }
96
+ finally {
97
+ clearTimeout(session.timer);
98
+ this.claudeSessions.delete(enrollmentId);
99
+ await provider.cancel?.(enrollmentId);
100
+ }
101
+ }
102
+ async completeClaudeCredentialImport(credentialJson, ownerScopeRef) {
103
+ const owner = this.requireOwnerScope(ownerScopeRef);
104
+ const provider = this.providers.get('claude-code');
105
+ // Capability stays Claude-local; the common provider contract remains unchanged.
106
+ if (!provider || !('importCredential' in provider) || typeof provider.importCredential !== 'function') {
107
+ throw new Error('Claude credential import unavailable');
108
+ }
109
+ const importer = provider;
110
+ const credential = await importer.importCredential(credentialJson);
111
+ return this.persistClaudeCompletion(credential, owner, provider, () => { });
112
+ }
62
113
  async waitForCallback(enrollmentId) {
63
114
  const provider = this.providers.get('cloud-code');
64
115
  if (!provider?.waitForCallback) {
@@ -311,7 +362,91 @@ export class LocalAccountTransportService {
311
362
  }, account);
312
363
  return { accountId: credential.accountId, label };
313
364
  }
365
+ async hasClaudeGrant(credential, ownerScopeRef) {
366
+ const index = await this.keyring.getSecret(LocalAccountTransportService.accountIndexKey);
367
+ for (const accountId of this.parseAccountIndex(index)) {
368
+ const record = await this.readPublicRecord(accountId);
369
+ if (!record || record.account.transportProviderId !== 'claude-code'
370
+ || record.account.ownerScopeRef !== ownerScopeRef || await this.isPublicRecordRemoved(record)
371
+ || !(await this.isPublicRecordOwnerClaimValid(record)))
372
+ continue;
373
+ const raw = await this.keyring.getSecret(`sentropic-llm-mesh:${accountId}:envelope`);
374
+ if (!raw)
375
+ continue;
376
+ try {
377
+ const stored = JSON.parse(raw);
378
+ // Compare only in process; never produce a token fingerprint or diagnostic.
379
+ if (stored.accountId === accountId && stored.refreshToken === credential.refreshToken)
380
+ return true;
381
+ }
382
+ catch {
383
+ // Ignore corrupt entries, as restore does; never log stored contents.
384
+ }
385
+ }
386
+ return false;
387
+ }
388
+ async persistClaudeCompletion(credential, ownerScopeRef, provider, assertCurrent) {
389
+ // Serialize Claude enrollment index updates; no account is executable during the writes.
390
+ const previous = this.claudePersistenceTail;
391
+ let unlock;
392
+ this.claudePersistenceTail = new Promise((resolve) => { unlock = resolve; });
393
+ await previous;
394
+ const id = credential.accountId;
395
+ let claimed = false;
396
+ let duplicate = false;
397
+ try {
398
+ assertCurrent();
399
+ duplicate = await this.hasClaudeGrant(credential, ownerScopeRef);
400
+ if (duplicate)
401
+ throw new Error('Duplicate Claude grant');
402
+ const metadata = await provider.resolve(credential);
403
+ const barrier = await this.removalBarrierForEnrollment(id, ownerScopeRef);
404
+ claimed = true;
405
+ if (barrier)
406
+ this.accountRemovalBarrierRefs.set(id, barrier);
407
+ this.pendingClaudeAccounts.add(id);
408
+ const now = new Date().toISOString();
409
+ const label = `Claude (${id})`;
410
+ const account = { accountId: id, ownerScopeRef, accountLabel: label,
411
+ targetProviderId: 'anthropic', transportProviderId: 'claude-code', status: 'active',
412
+ accessToken: credential.accessToken, refreshToken: credential.refreshToken,
413
+ expiresAt: credential.expiresAt, enrollmentCompletedAt: now, metadata };
414
+ assertCurrent();
415
+ await this.persistCredential({ accountId: id, accountLabel: label, providerId: 'claude-code',
416
+ status: 'active', createdAt: now, updatedAt: now }, credential, account);
417
+ assertCurrent();
418
+ await this.assertAccountOwnerClaim(id, ownerScopeRef);
419
+ if (await this.isAccountRemoved(id))
420
+ throw new Error('Account removed');
421
+ assertCurrent();
422
+ this.registerAccount(account, credential.authClientConfigVersion, barrier);
423
+ return { accountId: id, label };
424
+ }
425
+ catch {
426
+ if (claimed) {
427
+ // Keep the immutable owner claim and fence any partial writes on restart.
428
+ await this.keyring.setSecret(this.removalKey(id), JSON.stringify({ v: 1,
429
+ accountId: id, ownerScopeRef, removedAt: this.nextRemovalTimestamp() })).catch(() => { });
430
+ await this.keyring.deleteSecret(`sentropic-llm-mesh:${id}:public`).catch(() => { });
431
+ await this.keyring.deleteSecret(`sentropic-llm-mesh:${id}:envelope`).catch(() => { });
432
+ this.coordinator.removeAccount(id);
433
+ this.accountsMap.delete(id);
434
+ this.credentialVersions.delete(id);
435
+ this.accountRemovalBarrierRefs.delete(id);
436
+ }
437
+ throw new Error(duplicate ? 'This Claude credential is already enrolled'
438
+ : 'Claude enrollment could not be saved; reauthenticate');
439
+ }
440
+ finally {
441
+ this.pendingClaudeAccounts.delete(id);
442
+ unlock();
443
+ }
444
+ }
314
445
  async cancel(enrollmentId) {
446
+ const session = this.claudeSessions.get(enrollmentId);
447
+ if (session)
448
+ clearTimeout(session.timer);
449
+ this.claudeSessions.delete(enrollmentId);
315
450
  for (const provider of this.providers.values()) {
316
451
  if (provider.cancel) {
317
452
  await provider.cancel(enrollmentId);
@@ -324,6 +459,8 @@ export class LocalAccountTransportService {
324
459
  const rawIndex = await this.keyring.getSecret(LocalAccountTransportService.accountIndexKey);
325
460
  const accounts = [];
326
461
  for (const accountId of this.parseAccountIndex(rawIndex)) {
462
+ if (this.pendingClaudeAccounts.has(accountId))
463
+ continue;
327
464
  const record = await this.readPublicRecord(accountId);
328
465
  if (!record
329
466
  || record.account.ownerScopeRef !== scope
@@ -408,7 +545,7 @@ export class LocalAccountTransportService {
408
545
  // Check if access token is expired
409
546
  if (account && account.expiresAt) {
410
547
  const expiresAtMs = new Date(account.expiresAt).getTime();
411
- if (expiresAtMs <= nowMs) {
548
+ if (expiresAtMs <= nowMs || this.refreshInFlight.has(account.accountId)) {
412
549
  try {
413
550
  const version = this.credentialVersions.get(account.accountId) ?? 'v1.0.0';
414
551
  const refreshed = await this.refreshToken({
@@ -416,30 +553,6 @@ export class LocalAccountTransportService {
416
553
  refreshToken: account.refreshToken ?? undefined,
417
554
  credentialVersion: version,
418
555
  });
419
- if (await this.isAccountRemoved(account.accountId)) {
420
- await acquisition.release?.();
421
- throw new AccountTransportAcquireError(`Account ${account.accountId} has been removed`, 'no_active_account');
422
- }
423
- // Atomic persistence of updated credentials
424
- account.accessToken = refreshed.accessToken;
425
- if (refreshed.refreshToken) {
426
- account.refreshToken = refreshed.refreshToken;
427
- }
428
- account.expiresAt = refreshed.expiresAt;
429
- await this.persistCredential({
430
- accountId: account.accountId,
431
- accountLabel: account.accountLabel ?? undefined,
432
- providerId: account.transportProviderId,
433
- status: 'active',
434
- createdAt: new Date().toISOString(),
435
- updatedAt: new Date().toISOString(),
436
- }, {
437
- accountId: account.accountId,
438
- accessToken: refreshed.accessToken,
439
- refreshToken: account.refreshToken ?? undefined,
440
- expiresAt: refreshed.expiresAt,
441
- authClientConfigVersion: refreshed.authClientConfigVersion,
442
- }, account);
443
556
  // Update material with refreshed token
444
557
  acquisition.material.accessToken = refreshed.accessToken;
445
558
  if (refreshed.refreshToken) {
@@ -447,11 +560,17 @@ export class LocalAccountTransportService {
447
560
  }
448
561
  acquisition.material.expiresAt = refreshed.expiresAt;
449
562
  }
450
- catch (refreshErr) {
451
- await this.markReauthRequired(account.accountId);
452
- throw new AccountTransportAcquireError(`Account ${account.accountId} token refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`, 'no_active_account');
563
+ catch (error) {
564
+ await acquisition.release?.();
565
+ if (error instanceof AccountTransportAcquireError)
566
+ throw error;
567
+ throw new AccountTransportAcquireError(`Account ${account.accountId} token refresh failed; reauthentication required`, 'no_active_account');
453
568
  }
454
569
  }
570
+ // Acquisition may have snapshotted the old token before another caller published.
571
+ acquisition.material.accessToken = account.accessToken ?? undefined;
572
+ acquisition.material.refreshToken = account.refreshToken ?? undefined;
573
+ acquisition.material.expiresAt = account.expiresAt ?? undefined;
455
574
  }
456
575
  return acquisition;
457
576
  }
@@ -486,21 +605,65 @@ export class LocalAccountTransportService {
486
605
  diagnostics.push({
487
606
  code: 'reenrollment-required',
488
607
  transportProviderId: 'codex',
489
- message: 'Codex enrollment must be renewed for owner-scoped routing',
608
+ // Lot 7: the repair action rides in the message so a 503 body can
609
+ // tell the operator exactly what to do.
610
+ message: 'Codex enrollment must be renewed for owner-scoped routing;'
611
+ + ' re-enroll the codex account to restore routing',
490
612
  });
491
613
  }
492
614
  for (const account of this.accountsMap.values()) {
493
- if (account.ownerScopeRef !== subject.ownerScopeRef || account.status !== 'reauth_required') {
615
+ // A credential is reauth-required when its status has already flipped
616
+ // (a refresh failed) or when it is expired with no refresh token to
617
+ // mint a fresh access token silently — otherwise expiry dies silently
618
+ // as no-eligible-account.
619
+ if (account.ownerScopeRef !== subject.ownerScopeRef
620
+ || !this.requiresReauth(account)) {
494
621
  continue;
495
622
  }
623
+ const transport = account.transportProviderId;
624
+ // The stored expiry is owner data, never a validated date, and this
625
+ // message is client-facing. The expired-at wording applies only when
626
+ // the expiry predicate holds (a past instant) — a revoked-but-
627
+ // unexpired credential keeps the generic renewal wording. The emitted
628
+ // timestamp is the canonical ISO instant, never the raw stored string
629
+ // (which may carry non-canonical annotations).
630
+ const rawExpiry = account.expiresAt;
631
+ const parsedExpiry = typeof rawExpiry === 'string' && rawExpiry
632
+ ? Date.parse(rawExpiry)
633
+ : Number.NaN;
634
+ const expiredAt = Number.isFinite(parsedExpiry) && parsedExpiry <= Date.now()
635
+ ? new Date(parsedExpiry).toISOString()
636
+ : undefined;
496
637
  diagnostics.push({
497
638
  code: 'reauth-required',
498
- transportProviderId: account.transportProviderId,
499
- message: `${account.transportProviderId} authentication must be renewed`,
639
+ transportProviderId: transport,
640
+ message: expiredAt
641
+ ? `${transport} credential expired at ${expiredAt};`
642
+ + ` reauthenticate the ${transport} account to restore routing`
643
+ : `${transport} authentication must be renewed;`
644
+ + ` reauthenticate the ${transport} account to restore routing`,
500
645
  });
501
646
  }
502
647
  return diagnostics;
503
648
  }
649
+ isCredentialExpired(account) {
650
+ if (!account.expiresAt)
651
+ return false;
652
+ const expiresAtMs = Date.parse(account.expiresAt);
653
+ return Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now();
654
+ }
655
+ /**
656
+ * True when the account cannot recover on its own: its status already
657
+ * flipped to reauth_required (a refresh failed), or its ACCESS token is
658
+ * expired with no refresh token to renew it. A merely expired access
659
+ * token with a live refresh token stays ready — acquire() refreshes it
660
+ * before dispatch.
661
+ */
662
+ requiresReauth(account) {
663
+ if (account.status === 'reauth_required')
664
+ return true;
665
+ return this.isCredentialExpired(account) && !account.refreshToken;
666
+ }
504
667
  async listRouteAccounts(subject) {
505
668
  await this.restorePersistedAccounts();
506
669
  const recovered = this.coordinator.refreshLifecycle();
@@ -517,11 +680,13 @@ export class LocalAccountTransportService {
517
680
  transportProviderId: account.transportProviderId,
518
681
  supportedModelIds: supportedModelIdsForAccount(account),
519
682
  enrollmentCompletedAt: account.enrollmentCompletedAt ?? '1970-01-01T00:00:00.000Z',
520
- readiness: account.status === 'active' || account.status === undefined
521
- ? 'ready'
522
- : account.status === 'reauth_required'
523
- ? 'reauth-required'
524
- : account.status,
683
+ readiness: this.requiresReauth(account)
684
+ ? 'reauth-required'
685
+ : account.status === 'active' || account.status === undefined
686
+ ? 'ready'
687
+ : account.status === 'reauth_required'
688
+ ? 'reauth-required'
689
+ : account.status,
525
690
  revision: [
526
691
  this.credentialVersions.get(account.accountId) ?? 'v1.0.0',
527
692
  account.expiresAt ?? '', account.status ?? 'active', account.enrollmentCompletedAt ?? '',
@@ -590,14 +755,70 @@ export class LocalAccountTransportService {
590
755
  return this.refreshInFlight.get(input.accountId);
591
756
  }
592
757
  const refreshPromise = (async () => {
758
+ const account = this.accountsMap.get(input.accountId);
759
+ let requestMayHaveBeenSent = false;
593
760
  try {
594
- const account = this.accountsMap.get(input.accountId);
595
761
  const providerId = account?.transportProviderId ?? 'cloud-code';
596
762
  const provider = this.providers.get(providerId);
597
763
  if (!provider) {
598
764
  throw new Error(`No provider registered for refresh: ${providerId}`);
599
765
  }
600
- return await provider.refresh(input);
766
+ if (account?.status === 'reauth_required')
767
+ throw new Error('Reauthentication required');
768
+ // Fence persisted grants before a potentially rotating request. A crash or
769
+ // storage outage afterwards must not make a restart replay the old token.
770
+ const record = await this.readPublicRecord(input.accountId);
771
+ if (record) {
772
+ await this.assertAccountOwnerClaim(input.accountId, account?.ownerScopeRef);
773
+ if (await this.isAccountRemoved(input.accountId))
774
+ throw new Error('Account removed');
775
+ await this.writePublicRecord(input.accountId, JSON.stringify({
776
+ ...record, status: 'reauth_required', account: { ...record.account, status: 'reauth_required' },
777
+ }));
778
+ }
779
+ requestMayHaveBeenSent = true;
780
+ const refreshInput = providerId === 'claude-code'
781
+ ? { ...input, grantedScopes: account?.metadata?.scopes } : input;
782
+ const refreshed = await provider.refresh(refreshInput);
783
+ if (!account || refreshed.accountId !== input.accountId
784
+ || !refreshed.accessToken?.trim() || /[\r\n]/.test(refreshed.accessToken)
785
+ || !Number.isFinite(Date.parse(refreshed.expiresAt))
786
+ || Date.parse(refreshed.expiresAt) <= Date.now()
787
+ || (providerId === 'claude-code' && refreshed.authClientConfigVersion !== input.credentialVersion)
788
+ || (refreshed.refreshToken !== undefined && (!refreshed.refreshToken.trim()
789
+ || /[\r\n]/.test(refreshed.refreshToken))))
790
+ throw new Error('Invalid refresh grant');
791
+ const updated = { ...account, accessToken: refreshed.accessToken,
792
+ refreshToken: refreshed.refreshToken ?? account.refreshToken, expiresAt: refreshed.expiresAt };
793
+ if (providerId === 'claude-code') {
794
+ const metadata = await provider.resolve(refreshed);
795
+ updated.metadata = { ...account.metadata, ...metadata,
796
+ enrollmentMethod: account.metadata?.enrollmentMethod };
797
+ }
798
+ const now = new Date().toISOString();
799
+ await this.persistCredential({ accountId: account.accountId,
800
+ accountLabel: account.accountLabel ?? undefined, providerId, status: 'active',
801
+ createdAt: account.enrollmentCompletedAt ?? now, updatedAt: now }, { ...refreshed, refreshToken: updated.refreshToken ?? undefined }, updated);
802
+ if (await this.isAccountRemoved(input.accountId))
803
+ throw new Error('Account removed');
804
+ Object.assign(account, updated);
805
+ this.credentialVersions.set(input.accountId, refreshed.authClientConfigVersion);
806
+ return { ...refreshed, refreshToken: updated.refreshToken ?? undefined };
807
+ }
808
+ catch (error) {
809
+ if ((account && !this.accountsMap.has(input.accountId))
810
+ || await this.isAccountRemoved(input.accountId).catch(() => false)) {
811
+ throw new AccountTransportAcquireError(`Account ${input.accountId} has been removed`, 'no_active_account');
812
+ }
813
+ if ((!requestMayHaveBeenSent || error instanceof ClaudeRefreshPreparationError)
814
+ && account?.status !== 'reauth_required') {
815
+ // A fence write can commit and then throw, but no provider request occurred.
816
+ if (account)
817
+ await this.persistAccountState(account, true).catch(() => { });
818
+ throw new AccountTransportAcquireError('Token refresh preparation failed; retry later', 'no_active_account');
819
+ }
820
+ await this.markReauthRequired(input.accountId).catch(() => { });
821
+ throw new AccountTransportAcquireError('Token refresh failed; reauthentication required', 'no_active_account');
601
822
  }
602
823
  finally {
603
824
  this.refreshInFlight.delete(input.accountId);
@@ -621,7 +842,7 @@ export class LocalAccountTransportService {
621
842
  await this.assertAccountOwnerClaim(pub.accountId, account.ownerScopeRef);
622
843
  await this.keyring.setSecret(`sentropic-llm-mesh:${pub.accountId}:envelope`, JSON.stringify(env));
623
844
  await this.assertAccountOwnerClaim(pub.accountId, account.ownerScopeRef);
624
- await this.keyring.setSecret(`sentropic-llm-mesh:${pub.accountId}:public`, JSON.stringify(publicRecord));
845
+ await this.writePublicRecord(pub.accountId, JSON.stringify(publicRecord));
625
846
  if (await this.isAccountRemoved(pub.accountId)) {
626
847
  await this.cleanupRemovedPersistence(pub.accountId, removalBarrierRef);
627
848
  throw new Error(`Account '${pub.accountId}' has been removed`);
@@ -638,7 +859,20 @@ export class LocalAccountTransportService {
638
859
  throw new Error(`Account '${pub.accountId}' has been removed`);
639
860
  }
640
861
  }
641
- async persistAccountState(account) {
862
+ async writePublicRecord(accountId, value) {
863
+ // A fence must land after every earlier route write, even one already in the keyring.
864
+ const previous = this.publicWrites.get(accountId) ?? Promise.resolve();
865
+ const pending = previous.catch(() => { }).then(() => this.keyring.setSecret(`sentropic-llm-mesh:${accountId}:public`, value));
866
+ this.publicWrites.set(accountId, pending);
867
+ try {
868
+ await pending;
869
+ }
870
+ finally {
871
+ if (this.publicWrites.get(accountId) === pending)
872
+ this.publicWrites.delete(accountId);
873
+ }
874
+ }
875
+ async persistAccountState(account, restoreBeforeRequest = false) {
642
876
  await this.assertAccountOwnerClaim(account.accountId, account.ownerScopeRef);
643
877
  if (await this.isAccountRemoved(account.accountId))
644
878
  return;
@@ -653,7 +887,11 @@ export class LocalAccountTransportService {
653
887
  const { accessToken: _accessToken, refreshToken: _refreshToken, expiresAt: _expiresAt, ...persistedAccount } = account;
654
888
  if (await this.isAccountRemoved(account.accountId))
655
889
  return;
656
- await this.keyring.setSecret(key, JSON.stringify({
890
+ // Route outcomes must not clear the durable no-replay fence during rotation.
891
+ if (!restoreBeforeRequest && this.refreshInFlight.has(account.accountId)
892
+ && account.status !== 'reauth_required')
893
+ return;
894
+ await this.writePublicRecord(account.accountId, JSON.stringify({
657
895
  ...current,
658
896
  status: account.status ?? current.status,
659
897
  updatedAt: new Date().toISOString(),
@@ -831,7 +1069,7 @@ export class LocalAccountTransportService {
831
1069
  }
832
1070
  const rawIndex = await this.keyring.getSecret(LocalAccountTransportService.accountIndexKey);
833
1071
  for (const accountId of this.parseAccountIndex(rawIndex)) {
834
- if (this.accountsMap.has(accountId))
1072
+ if (this.accountsMap.has(accountId) || this.pendingClaudeAccounts.has(accountId))
835
1073
  continue;
836
1074
  const publicRaw = await this.keyring.getSecret(`sentropic-llm-mesh:${accountId}:public`);
837
1075
  const envelopeRaw = await this.keyring.getSecret(`sentropic-llm-mesh:${accountId}:envelope`);
@@ -878,7 +1116,7 @@ export class LocalAccountTransportService {
878
1116
  this.claimedOwnerScopes.set(accountId, ownerClaim.ownerScopeRef);
879
1117
  }
880
1118
  if (migrateLegacyOwner && restoredAccount.ownerScopeRef) {
881
- await this.keyring.setSecret(`sentropic-llm-mesh:${accountId}:public`, JSON.stringify({ ...publicRecord, account: restoredAccount }));
1119
+ await this.writePublicRecord(accountId, JSON.stringify({ ...publicRecord, account: restoredAccount }));
882
1120
  }
883
1121
  }
884
1122
  catch {