@gajae-code/ai 0.15.4 → 0.15.6
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/CHANGELOG.md +27 -1
- package/dist/types/auth-broker/client.d.ts +6 -2
- package/dist/types/auth-broker/remote-store.d.ts +14 -2
- package/dist/types/auth-broker/types.d.ts +6 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
- package/dist/types/auth-gateway/server.d.ts +39 -5
- package/dist/types/auth-gateway/types.d.ts +16 -2
- package/dist/types/auth-storage.d.ts +116 -34
- package/dist/types/provider-models/openai-compat.d.ts +1 -0
- package/dist/types/provider-models/special.d.ts +2 -1
- package/dist/types/providers/kiro-api-key.d.ts +50 -0
- package/dist/types/providers/kiro-codewhisperer.d.ts +3 -0
- package/dist/types/providers/register-builtins.d.ts +12 -12
- package/dist/types/stream.d.ts +2 -1
- package/dist/types/types.d.ts +35 -24
- package/dist/types/utils/fallback-transport.d.ts +7 -0
- package/dist/types/utils/json-parse.d.ts +5 -3
- package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
- package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
- package/dist/types/utils/oauth/commandcode.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/retry.d.ts +2 -0
- package/dist/types/utils/tool-call-healing.d.ts +4 -4
- package/package.json +3 -3
- package/src/auth-broker/client.ts +41 -13
- package/src/auth-broker/redact.ts +25 -1
- package/src/auth-broker/remote-store.ts +374 -115
- package/src/auth-broker/server.ts +131 -91
- package/src/auth-broker/types.ts +6 -0
- package/src/auth-broker/wire-schemas.ts +6 -0
- package/src/auth-gateway/server.ts +447 -79
- package/src/auth-gateway/types.ts +28 -2
- package/src/auth-storage.ts +742 -157
- package/src/cli.ts +1 -0
- package/src/model-thinking.ts +16 -0
- package/src/models.json +1054 -0
- package/src/models.ts +9 -1
- package/src/provider-models/descriptors.ts +3 -1
- package/src/provider-models/openai-compat.ts +41 -1
- package/src/provider-models/special.ts +15 -3
- package/src/providers/anthropic.ts +7 -1
- package/src/providers/azure-openai-responses.ts +4 -1
- package/src/providers/cursor.ts +256 -101
- package/src/providers/gitlab-duo.ts +18 -1
- package/src/providers/google-gemini-cli.ts +3 -0
- package/src/providers/google-shared.ts +3 -0
- package/src/providers/kiro-api-key.d.ts +50 -0
- package/src/providers/kiro-api-key.ts +786 -0
- package/src/providers/kiro-codewhisperer.d.ts +3 -0
- package/src/providers/kiro-codewhisperer.ts +34 -9
- package/src/providers/ollama.ts +3 -0
- package/src/providers/openai-codex-responses.ts +24 -6
- package/src/providers/openai-completions.ts +11 -1
- package/src/providers/openai-responses-shared.ts +23 -2
- package/src/providers/openai-responses.ts +10 -1
- package/src/providers/pi-native-client.ts +1 -0
- package/src/providers/pi-native-server.ts +24 -0
- package/src/providers/register-builtins.d.ts +12 -12
- package/src/providers/register-builtins.ts +16 -3
- package/src/stream.d.ts +2 -1
- package/src/stream.ts +180 -70
- package/src/types.d.ts +35 -24
- package/src/types.ts +40 -23
- package/src/utils/fallback-transport.d.ts +7 -0
- package/src/utils/fallback-transport.ts +21 -4
- package/src/utils/json-parse.d.ts +5 -3
- package/src/utils/json-parse.ts +6 -6
- package/src/utils/oauth/api-key-login.ts +13 -2
- package/src/utils/oauth/api-key-validation.ts +242 -41
- package/src/utils/oauth/commandcode.ts +17 -0
- package/src/utils/oauth/index.ts +20 -5
- package/src/utils/oauth/types.d.ts +1 -1
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/retry.d.ts +2 -0
- package/src/utils/retry.ts +15 -2
- package/src/utils/tool-call-healing.d.ts +4 -4
- package/src/utils/tool-call-healing.ts +4 -4
|
@@ -17,11 +17,11 @@ import { getConfigRootDir, isEnoent, logger } from "@gajae-code/utils";
|
|
|
17
17
|
import {
|
|
18
18
|
type AuthCredential,
|
|
19
19
|
type AuthCredentialIfAbsentResult,
|
|
20
|
-
type AuthCredentialSnapshotEntry,
|
|
21
20
|
type AuthCredentialStore,
|
|
22
21
|
assertCanonicalMCPOAuthBinding,
|
|
23
22
|
type CachedCredentialHealth,
|
|
24
23
|
type CachedUsagePresentation,
|
|
24
|
+
type CredentialDispatchTicket,
|
|
25
25
|
type CredentialInventoryRecord,
|
|
26
26
|
type MCPOAuthRefreshClient,
|
|
27
27
|
type OAuthCredential,
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
AuthBrokerCredentialMetadataUnsupportedError,
|
|
38
38
|
AuthBrokerStreamUnsupportedError,
|
|
39
39
|
} from "./client";
|
|
40
|
+
import { cleanReason } from "./redact";
|
|
40
41
|
import type {
|
|
41
42
|
CredentialMetadataRecord,
|
|
42
43
|
RefresherSchedule,
|
|
@@ -72,6 +73,11 @@ const MAX_WAIT_MS = 5_000;
|
|
|
72
73
|
const BACKGROUND_WAIT_MS = 30_000;
|
|
73
74
|
const BACKGROUND_BACKOFF_INITIAL_MS = 500;
|
|
74
75
|
const BACKGROUND_BACKOFF_MAX_MS = 30_000;
|
|
76
|
+
|
|
77
|
+
function epochRank(epoch: string): number | undefined {
|
|
78
|
+
const match = /^(\d+)-/.exec(epoch);
|
|
79
|
+
return match ? Number(match[1]) : undefined;
|
|
80
|
+
}
|
|
75
81
|
const PRESENTATION_FRESH_MS = 5 * 60_000;
|
|
76
82
|
const PRESENTATION_RETENTION_MS = 24 * 60 * 60_000;
|
|
77
83
|
const PRESENTATION_SIDECAR_VERSION = 1;
|
|
@@ -138,12 +144,7 @@ function presentationRecordKey(provider: string, identityDigest: string): string
|
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
function safePresentationReason(value: unknown): string | null {
|
|
141
|
-
|
|
142
|
-
let reason = value instanceof Error ? value.message : String(value);
|
|
143
|
-
reason = reason.replace(/bearer\s+[^\s,;]+/gi, "Bearer [redacted]");
|
|
144
|
-
reason = reason.replace(/(api[_-]?key|token|secret|authorization)[=:]\s*[^\s,;]+/gi, "$1=[redacted]");
|
|
145
|
-
reason = reason.replace(/[\r\n\t ]+/g, " ").trim();
|
|
146
|
-
return reason.length > 256 ? `${reason.slice(0, 253)}...` : reason || null;
|
|
147
|
+
return cleanReason(value) ?? null;
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
export interface RemoteAuthCredentialStoreOptions {
|
|
@@ -168,10 +169,17 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
168
169
|
#snapshot: SnapshotResponse = emptySnapshot();
|
|
169
170
|
#snapshotReceivedAt = Date.now();
|
|
170
171
|
#generation = 0;
|
|
172
|
+
#epoch?: string;
|
|
173
|
+
#retiredEpochs = new Set<string>();
|
|
174
|
+
#snapshotAuthorityTail: Promise<void> = Promise.resolve();
|
|
175
|
+
#snapshotListeners = new Set<() => void>();
|
|
171
176
|
#backgroundAbort = new AbortController();
|
|
172
177
|
#cache: Map<string, CacheEntry> = new Map();
|
|
173
178
|
#usageCache?: UsageCacheEntry;
|
|
174
179
|
#usageInflight?: Promise<UsageReport[] | null>;
|
|
180
|
+
#scopedUsageCache = new Map<Provider, UsageCacheEntry>();
|
|
181
|
+
#scopedUsageInflight = new Map<Provider, Promise<UsageReport[] | null>>();
|
|
182
|
+
#scopedUsageFailures = new Map<Provider, number>();
|
|
175
183
|
#usageCacheEpoch = 0;
|
|
176
184
|
#inventoryMetadata = new Map<number, CredentialMetadataRecord>();
|
|
177
185
|
#inventoryMetadataGeneration = -1;
|
|
@@ -201,6 +209,8 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
201
209
|
#streamingActive = false;
|
|
202
210
|
/** Latched once the broker has answered 404 — never try the stream again. */
|
|
203
211
|
#streamingUnsupported = false;
|
|
212
|
+
#loadingInitialSnapshot = true;
|
|
213
|
+
#snapshotAuthoritative = false;
|
|
204
214
|
|
|
205
215
|
constructor(opts: RemoteAuthCredentialStoreOptions) {
|
|
206
216
|
this.#client = opts.client;
|
|
@@ -208,6 +218,8 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
208
218
|
this.#presentationPath = opts.presentationPath ?? defaultPresentationSidecarPath();
|
|
209
219
|
this.#presentationAuthority = createHash("sha256").update(this.#client.baseUrl).digest("hex");
|
|
210
220
|
this.#applySnapshot(opts.initialSnapshot ?? emptySnapshot(), opts.initialSnapshot?.generation ?? 0, false);
|
|
221
|
+
this.#loadingInitialSnapshot = false;
|
|
222
|
+
this.#snapshotAuthoritative = opts.initialSnapshot !== undefined;
|
|
211
223
|
this.#setInventoryState("pending", this.#generation, {
|
|
212
224
|
status: "pending",
|
|
213
225
|
reason: "credential metadata sync pending",
|
|
@@ -269,9 +281,11 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
269
281
|
if (!record.health && !record.usage) continue;
|
|
270
282
|
this.#persistedPresentations.set(key, record);
|
|
271
283
|
}
|
|
272
|
-
this.#
|
|
273
|
-
|
|
274
|
-
|
|
284
|
+
if (this.#snapshotAuthoritative) {
|
|
285
|
+
this.#reconcilePersistedPresentations();
|
|
286
|
+
this.#hydratePresentations();
|
|
287
|
+
this.#queuePresentationWrite();
|
|
288
|
+
}
|
|
275
289
|
} catch (error) {
|
|
276
290
|
logger.debug("auth-broker presentation sidecar invalid", { error: String(error) });
|
|
277
291
|
}
|
|
@@ -373,10 +387,50 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
373
387
|
};
|
|
374
388
|
}
|
|
375
389
|
|
|
376
|
-
#applySnapshot(snapshot: SnapshotResponse, generation: number, scheduleMetadata = true):
|
|
377
|
-
|
|
390
|
+
#applySnapshot(snapshot: SnapshotResponse, generation: number, scheduleMetadata = true): boolean {
|
|
391
|
+
// Broker generations are process-local and reset when the broker restarts.
|
|
392
|
+
// A lower generation from a newer server timestamp is therefore a new
|
|
393
|
+
// broker epoch, while an older timestamp is an out-of-order response from
|
|
394
|
+
// the current epoch and must be discarded.
|
|
395
|
+
if (this.#epoch) {
|
|
396
|
+
if (!snapshot.epoch) return false;
|
|
397
|
+
if (snapshot.epoch === this.#epoch) {
|
|
398
|
+
if (generation < this.#generation) return false;
|
|
399
|
+
if (generation === this.#generation && snapshot.serverNowMs < this.#snapshot.serverNowMs) return false;
|
|
400
|
+
} else {
|
|
401
|
+
if (this.#retiredEpochs.has(snapshot.epoch)) return false;
|
|
402
|
+
const currentRank = epochRank(this.#epoch);
|
|
403
|
+
const incomingRank = epochRank(snapshot.epoch);
|
|
404
|
+
// An unseen epoch is only safe to accept when both broker epochs carry
|
|
405
|
+
// the authoritative monotonic sequence prefix. Opaque epoch strings
|
|
406
|
+
// cannot prove that a delayed response is newer, so fail closed rather
|
|
407
|
+
// than letting it replace the current credential snapshot.
|
|
408
|
+
if (currentRank === undefined || incomingRank === undefined || incomingRank <= currentRank) return false;
|
|
409
|
+
this.#retiredEpochs.add(this.#epoch);
|
|
410
|
+
}
|
|
411
|
+
} else if (snapshot.epoch) {
|
|
412
|
+
// First epoch-bearing response establishes the authority namespace.
|
|
413
|
+
} else if (
|
|
414
|
+
(generation < this.#generation && snapshot.serverNowMs <= this.#snapshot.serverNowMs) ||
|
|
415
|
+
(generation === this.#generation && snapshot.serverNowMs < this.#snapshot.serverNowMs)
|
|
416
|
+
) {
|
|
417
|
+
// Epoch-less legacy brokers use serverNowMs as the restart discriminator:
|
|
418
|
+
// accept a reset generation only when the broker proves it is newer than
|
|
419
|
+
// the last accepted legacy snapshot, while delayed old responses fail closed.
|
|
420
|
+
if (
|
|
421
|
+
(generation < this.#generation && snapshot.serverNowMs <= this.#snapshot.serverNowMs) ||
|
|
422
|
+
(generation === this.#generation && snapshot.serverNowMs < this.#snapshot.serverNowMs)
|
|
423
|
+
)
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
const generationChanged =
|
|
427
|
+
generation !== this.#generation ||
|
|
428
|
+
snapshot.epoch !== this.#epoch ||
|
|
429
|
+
this.#inventoryMetadataGeneration !== generation;
|
|
378
430
|
this.#snapshot = snapshot;
|
|
379
431
|
this.#generation = generation;
|
|
432
|
+
if (!this.#loadingInitialSnapshot) this.#snapshotAuthoritative = true;
|
|
433
|
+
this.#epoch = snapshot.epoch ?? this.#epoch;
|
|
380
434
|
this.#snapshotReceivedAt = Date.now();
|
|
381
435
|
if (generationChanged) {
|
|
382
436
|
this.#inventoryMetadata.clear();
|
|
@@ -398,8 +452,76 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
398
452
|
if (scheduleMetadata) this.#scheduleInventoryMetadataSync();
|
|
399
453
|
}
|
|
400
454
|
}
|
|
401
|
-
this.#
|
|
402
|
-
|
|
455
|
+
if (this.#snapshotAuthoritative) {
|
|
456
|
+
this.#reconcilePersistedPresentations();
|
|
457
|
+
this.#hydratePresentations();
|
|
458
|
+
}
|
|
459
|
+
for (const listener of this.#snapshotListeners) listener();
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* A full GET can race the SSE stream. Once a newer snapshot from the same
|
|
465
|
+
* broker epoch has been accepted, an older GET is merely superseded data —
|
|
466
|
+
* not an authority failure that should turn a healthy request into a 503.
|
|
467
|
+
* Epoch regressions remain rejected so a delayed response from a retired
|
|
468
|
+
* broker cannot cross an authority boundary.
|
|
469
|
+
*/
|
|
470
|
+
#isSupersededSnapshot(snapshot: SnapshotResponse, generation: number): boolean {
|
|
471
|
+
if (this.#epoch !== undefined || snapshot.epoch !== undefined) {
|
|
472
|
+
return (
|
|
473
|
+
snapshot.epoch === this.#epoch &&
|
|
474
|
+
(generation < this.#generation ||
|
|
475
|
+
(generation === this.#generation && snapshot.serverNowMs < this.#snapshot.serverNowMs))
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return (
|
|
479
|
+
(generation < this.#generation && snapshot.serverNowMs <= this.#snapshot.serverNowMs) ||
|
|
480
|
+
(generation === this.#generation && snapshot.serverNowMs < this.#snapshot.serverNowMs)
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
onSnapshotChanged(listener: () => void): () => void {
|
|
485
|
+
this.#snapshotListeners.add(listener);
|
|
486
|
+
return () => this.#snapshotListeners.delete(listener);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#withSnapshotAuthority<T>(operation: () => Promise<T>): Promise<T> {
|
|
490
|
+
const run = this.#snapshotAuthorityTail.then(operation);
|
|
491
|
+
this.#snapshotAuthorityTail = run.then(
|
|
492
|
+
() => undefined,
|
|
493
|
+
() => undefined,
|
|
494
|
+
);
|
|
495
|
+
return run;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Queue a provider-admission ticket behind all currently pending snapshot
|
|
500
|
+
* authority work. Snapshot applications that arrive after this ticket wait
|
|
501
|
+
* for its release, so a revocation is ordered either before admission or
|
|
502
|
+
* after it — never in the middle of the dispatch boundary.
|
|
503
|
+
*/
|
|
504
|
+
async acquireCredentialDispatchTicket(_provider: Provider, signal?: AbortSignal): Promise<CredentialDispatchTicket> {
|
|
505
|
+
const previous = this.#snapshotAuthorityTail;
|
|
506
|
+
const deferred = Promise.withResolvers<void>();
|
|
507
|
+
const ticketTail = previous.then(
|
|
508
|
+
() => deferred.promise,
|
|
509
|
+
() => deferred.promise,
|
|
510
|
+
);
|
|
511
|
+
this.#snapshotAuthorityTail = ticketTail;
|
|
512
|
+
let released = false;
|
|
513
|
+
const release = (): void => {
|
|
514
|
+
if (released) return;
|
|
515
|
+
released = true;
|
|
516
|
+
deferred.resolve();
|
|
517
|
+
};
|
|
518
|
+
try {
|
|
519
|
+
await this.#raceWithSignal(previous, signal);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
release();
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
return { release };
|
|
403
525
|
}
|
|
404
526
|
|
|
405
527
|
#setInventoryState(
|
|
@@ -471,6 +593,20 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
471
593
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
472
594
|
try {
|
|
473
595
|
const metadata = await this.#client.fetchCredentialMetadata();
|
|
596
|
+
if (metadata.epoch !== this.#epoch) {
|
|
597
|
+
if (attempt === 0) {
|
|
598
|
+
await this.refreshSnapshot().catch(() => {});
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
this.#inventoryMetadata.clear();
|
|
602
|
+
this.#inventoryMetadataGeneration = -1;
|
|
603
|
+
this.#setInventoryState("mismatch", this.#generation, {
|
|
604
|
+
status: "mismatch",
|
|
605
|
+
reason: "credential metadata epoch mismatch",
|
|
606
|
+
generation: this.#generation,
|
|
607
|
+
});
|
|
608
|
+
return this.getInventoryMetadataState();
|
|
609
|
+
}
|
|
474
610
|
if (metadata.generation !== this.#generation) {
|
|
475
611
|
if (attempt === 0) {
|
|
476
612
|
await this.refreshSnapshot().catch(() => {});
|
|
@@ -542,10 +678,19 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
542
678
|
try {
|
|
543
679
|
const result = await this.#client.fetchSnapshot({
|
|
544
680
|
ifGenerationGt: this.#generation,
|
|
681
|
+
ifEpoch: this.#epoch,
|
|
545
682
|
waitMs: BACKGROUND_WAIT_MS,
|
|
546
683
|
signal: this.#backgroundAbort.signal,
|
|
547
684
|
});
|
|
548
|
-
if (result.status === 200)
|
|
685
|
+
if (result.status === 200) {
|
|
686
|
+
await this.#withSnapshotAuthority(async () => {
|
|
687
|
+
if (!this.#applySnapshot(result.snapshot, result.generation)) {
|
|
688
|
+
if (!this.#isSupersededSnapshot(result.snapshot, result.generation)) {
|
|
689
|
+
throw new Error("Auth broker background snapshot authority was rejected");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
549
694
|
backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
|
|
550
695
|
} catch (error) {
|
|
551
696
|
if (this.#closed || this.#backgroundAbort.signal.aborted) break;
|
|
@@ -562,39 +707,61 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
562
707
|
for await (const event of iterator) {
|
|
563
708
|
if (this.#closed || this.#backgroundAbort.signal.aborted) break;
|
|
564
709
|
this.#streamingActive = true;
|
|
565
|
-
this.#applyStreamEvent(event);
|
|
710
|
+
await this.#applyStreamEvent(event);
|
|
566
711
|
}
|
|
567
712
|
} finally {
|
|
568
713
|
this.#streamingActive = false;
|
|
569
714
|
}
|
|
570
715
|
}
|
|
571
716
|
|
|
572
|
-
#applyStreamEvent(event: SnapshotStreamEvent): void {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
717
|
+
async #applyStreamEvent(event: SnapshotStreamEvent): Promise<void> {
|
|
718
|
+
await this.#withSnapshotAuthority(async () => {
|
|
719
|
+
switch (event.kind) {
|
|
720
|
+
case "snapshot": {
|
|
721
|
+
// Strip the discriminator so we store the wire-shape SnapshotResponse.
|
|
722
|
+
const { kind: _kind, ...snapshot } = event;
|
|
723
|
+
if (!this.#applySnapshot(snapshot, snapshot.generation)) {
|
|
724
|
+
if (!this.#isSupersededSnapshot(snapshot, snapshot.generation)) {
|
|
725
|
+
throw new Error("Auth broker stream snapshot authority was rejected");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
case "entry": {
|
|
731
|
+
const applied = this.#applyStreamEntry(
|
|
732
|
+
event.entry,
|
|
733
|
+
event.refresher,
|
|
734
|
+
event.generation,
|
|
735
|
+
event.serverNowMs,
|
|
736
|
+
event.epoch,
|
|
737
|
+
);
|
|
738
|
+
if (!applied && !this.#isSupersededStreamEvent(event)) {
|
|
739
|
+
throw new Error("Auth broker stream entry authority was rejected");
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
case "removed": {
|
|
744
|
+
const applied = this.#removeStreamCredential(
|
|
745
|
+
event.id,
|
|
746
|
+
event.refresher,
|
|
747
|
+
event.generation,
|
|
748
|
+
event.serverNowMs,
|
|
749
|
+
event.epoch,
|
|
750
|
+
);
|
|
751
|
+
if (!applied && !this.#isSupersededStreamEvent(event)) {
|
|
752
|
+
throw new Error("Auth broker stream removal authority was rejected");
|
|
753
|
+
}
|
|
582
754
|
return;
|
|
583
755
|
}
|
|
584
|
-
this.#applySnapshot(snapshot, snapshot.generation);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
case "entry": {
|
|
588
|
-
if (event.generation < this.#generation) return;
|
|
589
|
-
this.#applyStreamEntry(event.entry, event.refresher, event.generation, event.serverNowMs);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
case "removed": {
|
|
593
|
-
if (event.generation < this.#generation) return;
|
|
594
|
-
this.#removeStreamCredential(event.id, event.refresher, event.generation, event.serverNowMs);
|
|
595
|
-
return;
|
|
596
756
|
}
|
|
597
|
-
}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#isSupersededStreamEvent(event: SnapshotStreamEvent): boolean {
|
|
761
|
+
return this.#isSupersededSnapshot(
|
|
762
|
+
{ ...this.#snapshot, epoch: event.epoch, generation: event.generation, serverNowMs: event.serverNowMs },
|
|
763
|
+
event.generation,
|
|
764
|
+
);
|
|
598
765
|
}
|
|
599
766
|
|
|
600
767
|
#applyStreamEntry(
|
|
@@ -602,18 +769,33 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
602
769
|
refresher: RefresherSchedule,
|
|
603
770
|
generation: number,
|
|
604
771
|
serverNowMs: number,
|
|
605
|
-
|
|
772
|
+
epoch?: string,
|
|
773
|
+
): boolean {
|
|
774
|
+
if (!epoch && this.#epoch && epochRank(this.#epoch) !== undefined) return false;
|
|
606
775
|
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === entry.id);
|
|
607
776
|
const credentials =
|
|
608
777
|
index === -1
|
|
609
778
|
? [...this.#snapshot.credentials, entry]
|
|
610
779
|
: this.#snapshot.credentials.map((candidate, i) => (i === index ? entry : candidate));
|
|
611
|
-
this.#applySnapshot(
|
|
780
|
+
return this.#applySnapshot(
|
|
781
|
+
{ ...this.#snapshot, epoch, generation, serverNowMs, refresher, credentials },
|
|
782
|
+
generation,
|
|
783
|
+
);
|
|
612
784
|
}
|
|
613
785
|
|
|
614
|
-
#removeStreamCredential(
|
|
786
|
+
#removeStreamCredential(
|
|
787
|
+
id: number,
|
|
788
|
+
refresher: RefresherSchedule,
|
|
789
|
+
generation: number,
|
|
790
|
+
serverNowMs: number,
|
|
791
|
+
epoch?: string,
|
|
792
|
+
): boolean {
|
|
793
|
+
if (!epoch && this.#epoch && epochRank(this.#epoch) !== undefined) return false;
|
|
615
794
|
const credentials = this.#snapshot.credentials.filter(entry => entry.id !== id);
|
|
616
|
-
this.#applySnapshot(
|
|
795
|
+
return this.#applySnapshot(
|
|
796
|
+
{ ...this.#snapshot, epoch, generation, serverNowMs, refresher, credentials },
|
|
797
|
+
generation,
|
|
798
|
+
);
|
|
617
799
|
}
|
|
618
800
|
|
|
619
801
|
/**
|
|
@@ -628,9 +810,20 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
628
810
|
}
|
|
629
811
|
|
|
630
812
|
/** Re-hydrate the in-memory snapshot from the broker. */
|
|
631
|
-
async refreshSnapshot(): Promise<SnapshotResponse> {
|
|
632
|
-
const result = await this.#client.fetchSnapshot();
|
|
633
|
-
if (result.status === 200)
|
|
813
|
+
async refreshSnapshot(signal?: AbortSignal): Promise<SnapshotResponse> {
|
|
814
|
+
const result = await this.#client.fetchSnapshot({ signal });
|
|
815
|
+
if (result.status === 200) {
|
|
816
|
+
await this.#raceWithSignal(
|
|
817
|
+
this.#withSnapshotAuthority(async () => {
|
|
818
|
+
if (!this.#applySnapshot(result.snapshot, result.generation)) {
|
|
819
|
+
if (!this.#isSupersededSnapshot(result.snapshot, result.generation)) {
|
|
820
|
+
throw new Error("Auth broker snapshot authority was rejected");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}),
|
|
824
|
+
signal,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
634
827
|
return this.#snapshot;
|
|
635
828
|
}
|
|
636
829
|
|
|
@@ -643,6 +836,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
643
836
|
provider: entry.provider,
|
|
644
837
|
credential: entry.credential as AuthCredential,
|
|
645
838
|
disabledCause: null,
|
|
839
|
+
...(entry.revision === undefined ? {} : { revision: entry.revision }),
|
|
646
840
|
});
|
|
647
841
|
}
|
|
648
842
|
return out;
|
|
@@ -654,30 +848,68 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
654
848
|
* authoritative row, so we just mirror it.
|
|
655
849
|
*/
|
|
656
850
|
updateAuthCredential(id: number, credential: AuthCredential): void {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
851
|
+
void id;
|
|
852
|
+
void credential;
|
|
853
|
+
throw new Error("Remote auth-broker credentials must be updated through broker authority");
|
|
662
854
|
}
|
|
663
855
|
|
|
664
856
|
deleteAuthCredential(_id: number, _disabledCause: string): void {
|
|
665
857
|
throw new Error("Remote auth-broker credentials can only be disabled on the broker host");
|
|
666
858
|
}
|
|
667
859
|
|
|
860
|
+
allocateMonotonicSequence(_key: string, _expiresAtSec: number): number {
|
|
861
|
+
throw new Error("Remote auth-broker credentials cannot allocate broker incarnation sequences");
|
|
862
|
+
}
|
|
863
|
+
|
|
668
864
|
tryDisableAuthCredentialIfMatches(_id: number, _expectedData: string, _disabledCause: string): boolean {
|
|
669
865
|
return false;
|
|
670
866
|
}
|
|
671
867
|
|
|
868
|
+
/** Disable one credential through the authenticated broker mutation endpoint. */
|
|
869
|
+
async disableAuthCredentialRemote(
|
|
870
|
+
credentialId: number,
|
|
871
|
+
disabledCause: string,
|
|
872
|
+
signal?: AbortSignal,
|
|
873
|
+
expectedRevision?: number,
|
|
874
|
+
): Promise<boolean> {
|
|
875
|
+
try {
|
|
876
|
+
await this.#client.disableCredential(credentialId, disabledCause, signal, expectedRevision);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
if (!isErrorStatus(error, 404)) throw error;
|
|
879
|
+
// A peer may have disabled the row first. Reconcile the local mirror so
|
|
880
|
+
// AuthStorage can select a fallback without ever mutating remote state
|
|
881
|
+
// through the read-only synchronous methods.
|
|
882
|
+
await this.refreshSnapshot().catch(refreshError => {
|
|
883
|
+
logger.debug("auth-broker snapshot refresh after remote disable miss failed", {
|
|
884
|
+
error: String(refreshError),
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
this.#removeCredentialEntry(credentialId);
|
|
890
|
+
this.#maybeRefreshSnapshot("disable");
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
|
|
672
894
|
async waitForFreshSnapshot(maxWaitMs: number, opts: { signal?: AbortSignal } = {}): Promise<boolean> {
|
|
673
895
|
const previousGeneration = this.#generation;
|
|
896
|
+
const previousEpoch = this.#epoch;
|
|
674
897
|
const result = await this.#client.fetchSnapshot({
|
|
675
898
|
ifGenerationGt: this.#generation,
|
|
899
|
+
ifEpoch: this.#epoch,
|
|
676
900
|
waitMs: maxWaitMs,
|
|
677
901
|
signal: opts.signal,
|
|
678
902
|
});
|
|
679
|
-
if (result.status === 200)
|
|
680
|
-
|
|
903
|
+
if (result.status === 200) {
|
|
904
|
+
await this.#withSnapshotAuthority(async () => {
|
|
905
|
+
if (!this.#applySnapshot(result.snapshot, result.generation)) {
|
|
906
|
+
if (!this.#isSupersededSnapshot(result.snapshot, result.generation)) {
|
|
907
|
+
throw new Error("Auth broker freshness snapshot authority was rejected");
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
return this.#generation !== previousGeneration || this.#epoch !== previousEpoch;
|
|
681
913
|
}
|
|
682
914
|
|
|
683
915
|
async prepareForRequest(credentialId: number, opts: { signal?: AbortSignal } = {}): Promise<boolean> {
|
|
@@ -689,12 +921,17 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
689
921
|
}
|
|
690
922
|
|
|
691
923
|
async markCredentialSuspect(credentialId: number, opts: { signal?: AbortSignal } = {}): Promise<void> {
|
|
692
|
-
const
|
|
693
|
-
if (
|
|
694
|
-
|
|
924
|
+
const current = this.#snapshot.credentials.find(entry => entry.id === credentialId);
|
|
925
|
+
if (current?.credential.type !== "oauth") {
|
|
926
|
+
// API-key rows have no refresh token, and a stale id may already have
|
|
927
|
+
// disappeared. A 401 means the broker may have rotated or removed the
|
|
928
|
+
// row; refresh the authoritative snapshot so the caller can reselect a
|
|
929
|
+
// live key instead of hitting the OAuth endpoint.
|
|
930
|
+
await this.refreshSnapshot(opts.signal);
|
|
931
|
+
return;
|
|
695
932
|
}
|
|
696
|
-
this.#
|
|
697
|
-
this
|
|
933
|
+
await this.#client.refreshCredential(credentialId, opts.signal);
|
|
934
|
+
await this.refreshSnapshot(opts.signal);
|
|
698
935
|
}
|
|
699
936
|
|
|
700
937
|
replaceAuthCredentialsForProvider(_provider: string, _credentials: AuthCredential[]): StoredAuthCredential[] {
|
|
@@ -728,8 +965,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
728
965
|
async deleteAuthCredentialsRemote(provider: string, disabledCause: string): Promise<void> {
|
|
729
966
|
const existing = this.listAuthCredentials(provider);
|
|
730
967
|
for (const entry of existing) await this.#client.disableCredential(entry.id, disabledCause);
|
|
731
|
-
this
|
|
732
|
-
this.#maybeRefreshSnapshot("delete");
|
|
968
|
+
await this.refreshSnapshot();
|
|
733
969
|
}
|
|
734
970
|
|
|
735
971
|
/**
|
|
@@ -740,9 +976,8 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
740
976
|
* any concurrent peer (refresh, generation bump) stays in sync.
|
|
741
977
|
*/
|
|
742
978
|
async upsertAuthCredentialRemote(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]> {
|
|
743
|
-
|
|
744
|
-
this
|
|
745
|
-
this.#maybeRefreshSnapshot("upload");
|
|
979
|
+
await this.#client.uploadCredential(provider, credential);
|
|
980
|
+
await this.refreshSnapshot();
|
|
746
981
|
return this.listAuthCredentials(provider);
|
|
747
982
|
}
|
|
748
983
|
|
|
@@ -750,9 +985,8 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
750
985
|
provider: string,
|
|
751
986
|
credential: AuthCredential,
|
|
752
987
|
): Promise<AuthCredentialIfAbsentResult> {
|
|
753
|
-
const { inserted, reason
|
|
754
|
-
this
|
|
755
|
-
this.#maybeRefreshSnapshot("upload-if-absent");
|
|
988
|
+
const { inserted, reason } = await this.#client.uploadCredentialIfAbsent(provider, credential);
|
|
989
|
+
await this.refreshSnapshot();
|
|
756
990
|
return { inserted, reason, provider, entries: this.listAuthCredentials(provider) };
|
|
757
991
|
}
|
|
758
992
|
|
|
@@ -771,38 +1005,18 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
771
1005
|
}
|
|
772
1006
|
await this.refreshSnapshot();
|
|
773
1007
|
for (const credential of credentials) {
|
|
774
|
-
|
|
775
|
-
this
|
|
1008
|
+
await this.#client.uploadCredential(provider, credential);
|
|
1009
|
+
await this.refreshSnapshot();
|
|
776
1010
|
}
|
|
777
1011
|
this.#maybeRefreshSnapshot("replace");
|
|
778
1012
|
return this.listAuthCredentials(provider);
|
|
779
1013
|
}
|
|
780
1014
|
|
|
781
|
-
#
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
// the fresh set — preserving every other provider's rows in place.
|
|
785
|
-
const others = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
|
|
786
|
-
const incoming = entries.map(entry => ({ ...entry, rotatesInMs: null }));
|
|
787
|
-
this.#snapshot = { ...this.#snapshot, credentials: [...others, ...incoming] };
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
#removeProviderEntries(provider: string): void {
|
|
791
|
-
const credentials = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
|
|
1015
|
+
#removeCredentialEntry(credentialId: number): void {
|
|
1016
|
+
const credentials = this.#snapshot.credentials.filter(entry => entry.id !== credentialId);
|
|
1017
|
+
if (credentials.length === this.#snapshot.credentials.length) return;
|
|
792
1018
|
this.#applySnapshot({ ...this.#snapshot, credentials }, this.#generation, false);
|
|
793
1019
|
}
|
|
794
|
-
#applyCredentialEntry(entry: AuthCredentialSnapshotEntry): void {
|
|
795
|
-
const incoming = { ...entry, rotatesInMs: null };
|
|
796
|
-
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === entry.id);
|
|
797
|
-
if (index === -1) {
|
|
798
|
-
this.#snapshot = { ...this.#snapshot, credentials: [...this.#snapshot.credentials, incoming] };
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
const credentials = [...this.#snapshot.credentials];
|
|
802
|
-
credentials[index] = incoming;
|
|
803
|
-
this.#snapshot = { ...this.#snapshot, credentials };
|
|
804
|
-
}
|
|
805
|
-
|
|
806
1020
|
/**
|
|
807
1021
|
* Fire-and-forget `refreshSnapshot()` after a write. When the SSE stream is
|
|
808
1022
|
* active the broker will deliver the new generation push, so the extra GET
|
|
@@ -863,6 +1077,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
863
1077
|
#invalidateUsageCache(): void {
|
|
864
1078
|
this.#usageCache = undefined;
|
|
865
1079
|
this.#usageInflight = undefined;
|
|
1080
|
+
this.#scopedUsageCache.clear();
|
|
1081
|
+
this.#scopedUsageInflight.clear();
|
|
1082
|
+
this.#scopedUsageFailures.clear();
|
|
866
1083
|
this.#usageCacheEpoch += 1;
|
|
867
1084
|
}
|
|
868
1085
|
|
|
@@ -878,9 +1095,8 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
878
1095
|
_credential: OAuthCredential,
|
|
879
1096
|
signal?: AbortSignal,
|
|
880
1097
|
): Promise<OAuthCredentials> {
|
|
881
|
-
let entry: AuthCredentialSnapshotEntry;
|
|
882
1098
|
try {
|
|
883
|
-
|
|
1099
|
+
await this.#client.refreshCredential(credentialId, signal);
|
|
884
1100
|
} catch (error) {
|
|
885
1101
|
if (isErrorStatus(error, 404) && !this.#streamingActive) {
|
|
886
1102
|
await this.refreshSnapshot().catch(refreshError => {
|
|
@@ -891,15 +1107,12 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
891
1107
|
}
|
|
892
1108
|
throw error;
|
|
893
1109
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
if (entry.credential.type !== "oauth") {
|
|
900
|
-
throw new Error(`Broker returned non-OAuth credential for id=${credentialId}`);
|
|
1110
|
+
await this.refreshSnapshot(signal);
|
|
1111
|
+
const accepted = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
|
|
1112
|
+
if (accepted?.credential.type !== "oauth") {
|
|
1113
|
+
throw new Error(`Broker snapshot no longer contains OAuth credential id=${credentialId}`);
|
|
901
1114
|
}
|
|
902
|
-
const refreshed =
|
|
1115
|
+
const refreshed = accepted.credential;
|
|
903
1116
|
return {
|
|
904
1117
|
access: refreshed.access,
|
|
905
1118
|
refresh: REMOTE_REFRESH_SENTINEL,
|
|
@@ -917,21 +1130,21 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
917
1130
|
client: MCPOAuthRefreshClient,
|
|
918
1131
|
signal?: AbortSignal,
|
|
919
1132
|
): Promise<OAuthCredential> {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1133
|
+
await this.#client.refreshMCPCredential(credentialId, client, signal);
|
|
1134
|
+
await this.refreshSnapshot(signal);
|
|
1135
|
+
const accepted = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
|
|
1136
|
+
if (accepted?.credential.type !== "oauth") {
|
|
1137
|
+
throw new Error(`Broker snapshot no longer contains OAuth credential id=${credentialId}`);
|
|
923
1138
|
}
|
|
924
1139
|
assertCanonicalMCPOAuthBinding(credential.mcpBinding);
|
|
925
|
-
assertCanonicalMCPOAuthBinding(
|
|
1140
|
+
assertCanonicalMCPOAuthBinding(accepted.credential.mcpBinding);
|
|
926
1141
|
if (
|
|
927
|
-
|
|
928
|
-
|
|
1142
|
+
accepted.credential.mcpBinding.resourceOrigin !== credential.mcpBinding.resourceOrigin ||
|
|
1143
|
+
accepted.credential.mcpBinding.tokenEndpoint !== credential.mcpBinding.tokenEndpoint
|
|
929
1144
|
) {
|
|
930
1145
|
throw new Error("Broker returned mismatched MCP OAuth credential binding");
|
|
931
1146
|
}
|
|
932
|
-
|
|
933
|
-
this.#maybeRefreshSnapshot("MCP credential refresh");
|
|
934
|
-
return entry.credential;
|
|
1147
|
+
return accepted.credential;
|
|
935
1148
|
}
|
|
936
1149
|
|
|
937
1150
|
/**
|
|
@@ -944,6 +1157,10 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
944
1157
|
return this.#raceWithSignal(this.#loadUsageReports(), signal);
|
|
945
1158
|
}
|
|
946
1159
|
|
|
1160
|
+
async fetchUsageReportsForProvider(provider: Provider, signal?: AbortSignal): Promise<UsageReport[] | null> {
|
|
1161
|
+
return this.#raceWithSignal(this.#loadUsageReports(provider), signal);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
947
1164
|
/** Synchronous, zero-network usage presentation read. */
|
|
948
1165
|
peekCachedUsagePresentation(provider: Provider, credentialId: number): CachedUsagePresentation | undefined {
|
|
949
1166
|
const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
|
|
@@ -1075,7 +1292,16 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1075
1292
|
credential: OAuthCredential,
|
|
1076
1293
|
signal?: AbortSignal,
|
|
1077
1294
|
): Promise<UsageReport | null> {
|
|
1078
|
-
|
|
1295
|
+
let reports: UsageReport[] | null;
|
|
1296
|
+
try {
|
|
1297
|
+
reports = await this.#raceWithSignal(this.#loadUsageReports(provider), signal);
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
// A caller cancellation is control flow, not a missing usage report.
|
|
1300
|
+
// Preserve it so selection/dispatch can stop promptly; only ordinary
|
|
1301
|
+
// broker/provider failures degrade to "no usage".
|
|
1302
|
+
if (signal?.aborted) throw error;
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1079
1305
|
if (!reports) return null;
|
|
1080
1306
|
return matchUsageReport(reports, provider, credential);
|
|
1081
1307
|
}
|
|
@@ -1160,7 +1386,41 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1160
1386
|
}
|
|
1161
1387
|
}
|
|
1162
1388
|
|
|
1163
|
-
#loadUsageReports(): Promise<UsageReport[] | null> {
|
|
1389
|
+
#loadUsageReports(provider?: Provider): Promise<UsageReport[] | null> {
|
|
1390
|
+
if (provider) {
|
|
1391
|
+
const cached = this.#scopedUsageCache.get(provider);
|
|
1392
|
+
if (cached && Date.now() - cached.fetchedAt < USAGE_CACHE_TTL_MS) return Promise.resolve(cached.reports);
|
|
1393
|
+
const failedAt = this.#scopedUsageFailures.get(provider);
|
|
1394
|
+
if (failedAt !== undefined && Date.now() - failedAt < USAGE_CACHE_TTL_MS) return Promise.resolve(null);
|
|
1395
|
+
const existing = this.#scopedUsageInflight.get(provider);
|
|
1396
|
+
if (existing) return existing;
|
|
1397
|
+
const epoch = this.#usageCacheEpoch;
|
|
1398
|
+
const inflight = this.#client
|
|
1399
|
+
.fetchUsage(undefined, provider)
|
|
1400
|
+
.then(body => {
|
|
1401
|
+
if (this.#usageCacheEpoch === epoch) {
|
|
1402
|
+
this.#scopedUsageCache.set(provider, { reports: body.reports, fetchedAt: Date.now() });
|
|
1403
|
+
this.#scopedUsageFailures.delete(provider);
|
|
1404
|
+
this.#recordUsageReports(body.reports, epoch);
|
|
1405
|
+
}
|
|
1406
|
+
return body.reports;
|
|
1407
|
+
})
|
|
1408
|
+
.catch(error => {
|
|
1409
|
+
logger.warn("auth-broker scoped usage fetch failed", {
|
|
1410
|
+
provider,
|
|
1411
|
+
error: cleanReason(error) ?? "Usage unavailable.",
|
|
1412
|
+
});
|
|
1413
|
+
if (this.#usageCacheEpoch === epoch) this.#scopedUsageFailures.set(provider, Date.now());
|
|
1414
|
+
return null;
|
|
1415
|
+
})
|
|
1416
|
+
.finally(() => {
|
|
1417
|
+
if (this.#scopedUsageInflight.get(provider) === inflight) {
|
|
1418
|
+
this.#scopedUsageInflight.delete(provider);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
this.#scopedUsageInflight.set(provider, inflight);
|
|
1422
|
+
return inflight;
|
|
1423
|
+
}
|
|
1164
1424
|
const cached = this.#usageCache;
|
|
1165
1425
|
if (cached && Date.now() - cached.fetchedAt < USAGE_CACHE_TTL_MS) {
|
|
1166
1426
|
return Promise.resolve(cached.reports);
|
|
@@ -1194,6 +1454,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1194
1454
|
this.#persistedPresentations.clear();
|
|
1195
1455
|
});
|
|
1196
1456
|
this.#closed = true;
|
|
1457
|
+
this.#snapshotListeners.clear();
|
|
1197
1458
|
this.#cache.clear();
|
|
1198
1459
|
this.#usagePresentations.clear();
|
|
1199
1460
|
this.#inventoryMetadata.clear();
|
|
@@ -1217,9 +1478,7 @@ function snapshotIdentityLabel(entry: SnapshotEntry): string | null {
|
|
|
1217
1478
|
|
|
1218
1479
|
function safePresentationUsageReport(report: UsageReport): SafeUsageReport {
|
|
1219
1480
|
const sanitize = (value: string): string =>
|
|
1220
|
-
value
|
|
1221
|
-
.replace(/bearer\s+[^\s,;]+/gi, "Bearer [redacted]")
|
|
1222
|
-
.replace(/(api[_-]?key|token|secret|authorization)[=:]\s*[^\s,;]+/gi, "$1=[redacted]")
|
|
1481
|
+
(cleanReason(value) ?? "Usage unavailable.")
|
|
1223
1482
|
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
1224
1483
|
.replace(/\s+/g, " ")
|
|
1225
1484
|
.trim()
|