@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.
- package/README.md +192 -0
- package/dist/catalog.d.ts +92 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +47 -0
- package/dist/catalog.js.map +1 -1
- package/dist/enrollment/claude-code.d.ts +27 -5
- package/dist/enrollment/claude-code.d.ts.map +1 -1
- package/dist/enrollment/claude-code.js +224 -10
- package/dist/enrollment/claude-code.js.map +1 -1
- package/dist/enrollment/codex.d.ts +7 -0
- package/dist/enrollment/codex.d.ts.map +1 -1
- package/dist/enrollment/codex.js +74 -19
- package/dist/enrollment/codex.js.map +1 -1
- package/dist/enrollment/index.d.ts +1 -1
- package/dist/enrollment/index.d.ts.map +1 -1
- package/dist/enrollment/pkce.js +1 -1
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +51 -4
- package/dist/errors.js.map +1 -1
- package/dist/execution-features.d.ts +34 -0
- package/dist/execution-features.d.ts.map +1 -0
- package/dist/execution-features.js +84 -0
- package/dist/execution-features.js.map +1 -0
- package/dist/generated-model-council.d.ts +2 -2
- package/dist/generated-model-council.d.ts.map +1 -1
- package/dist/generated-model-council.js +5 -1
- package/dist/generated-model-council.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/native-messages.d.ts +130 -0
- package/dist/native-messages.d.ts.map +1 -0
- package/dist/native-messages.js +66 -0
- package/dist/native-messages.js.map +1 -0
- package/dist/providers.d.ts +5 -5
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +12 -5
- package/dist/providers.js.map +1 -1
- package/dist/responses-wire.d.ts +76 -0
- package/dist/responses-wire.d.ts.map +1 -0
- package/dist/responses-wire.js +35 -0
- package/dist/responses-wire.js.map +1 -0
- package/dist/route-attempt.d.ts +4 -0
- package/dist/route-attempt.d.ts.map +1 -1
- package/dist/route-attempt.js +8 -1
- package/dist/route-attempt.js.map +1 -1
- package/dist/route-health.d.ts +11 -0
- package/dist/route-health.d.ts.map +1 -1
- package/dist/route-health.js +32 -0
- package/dist/route-health.js.map +1 -1
- package/dist/route-planner-state.d.ts +33 -2
- package/dist/route-planner-state.d.ts.map +1 -1
- package/dist/route-planner-state.js +20 -1
- package/dist/route-planner-state.js.map +1 -1
- package/dist/route-planner.d.ts +21 -1
- package/dist/route-planner.d.ts.map +1 -1
- package/dist/route-planner.js +112 -33
- package/dist/route-planner.js.map +1 -1
- package/dist/route-quote.d.ts +16 -2
- package/dist/route-quote.d.ts.map +1 -1
- package/dist/route-quote.js +31 -5
- package/dist/route-quote.js.map +1 -1
- package/dist/route-selection.d.ts +13 -0
- package/dist/route-selection.d.ts.map +1 -1
- package/dist/route-selection.js +30 -5
- package/dist/route-selection.js.map +1 -1
- package/dist/routing-contracts.d.ts +42 -3
- package/dist/routing-contracts.d.ts.map +1 -1
- package/dist/routing-targets.d.ts +26 -0
- package/dist/routing-targets.d.ts.map +1 -1
- package/dist/routing-targets.js +96 -23
- package/dist/routing-targets.js.map +1 -1
- package/dist/service/facade.d.ts +3 -0
- package/dist/service/facade.d.ts.map +1 -1
- package/dist/service/facade.js +7 -1
- package/dist/service/facade.js.map +1 -1
- package/dist/service/local-account-transport-service.d.ts +18 -0
- package/dist/service/local-account-transport-service.d.ts.map +1 -1
- package/dist/service/local-account-transport-service.js +284 -46
- package/dist/service/local-account-transport-service.js.map +1 -1
- package/dist/transport/claude-code-runtime-client.d.ts +88 -0
- package/dist/transport/claude-code-runtime-client.d.ts.map +1 -0
- package/dist/transport/claude-code-runtime-client.js +305 -0
- package/dist/transport/claude-code-runtime-client.js.map +1 -0
- package/dist/transport/codex-runtime-client.d.ts.map +1 -1
- package/dist/transport/codex-runtime-client.js +24 -4
- package/dist/transport/codex-runtime-client.js.map +1 -1
- package/dist/transport/codex-runtime-wire.d.ts.map +1 -1
- package/dist/transport/codex-runtime-wire.js +131 -27
- package/dist/transport/codex-runtime-wire.js.map +1 -1
- package/dist/transport/mistral-runtime-client.d.ts.map +1 -1
- package/dist/transport/mistral-runtime-client.js +191 -9
- package/dist/transport/mistral-runtime-client.js.map +1 -1
- package/dist/transport/muse-runtime-client.d.ts.map +1 -1
- package/dist/transport/muse-runtime-client.js +221 -10
- package/dist/transport/muse-runtime-client.js.map +1 -1
- 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":"AAAA,OAAO,EAGL,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EACjC,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACV,aAAa,EAGb,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EAGjB,oBAAoB,EACrB,MAAM,4BAA4B,CAAC;
|
|
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
|
-
|
|
59
|
-
|
|
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 (
|
|
451
|
-
await
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
499
|
-
message:
|
|
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:
|
|
521
|
-
? '
|
|
522
|
-
: account.status === '
|
|
523
|
-
? '
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
1119
|
+
await this.writePublicRecord(accountId, JSON.stringify({ ...publicRecord, account: restoredAccount }));
|
|
882
1120
|
}
|
|
883
1121
|
}
|
|
884
1122
|
catch {
|