@gajae-code/ai 0.15.5 → 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 +15 -0
- 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 +92 -24
- package/dist/types/provider-models/openai-compat.d.ts +1 -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 +22 -2
- 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/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 +658 -154
- package/src/cli.ts +1 -0
- package/src/models.json +1023 -0
- package/src/provider-models/descriptors.ts +3 -1
- package/src/provider-models/openai-compat.ts +41 -1
- 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-codewhisperer.ts +24 -8
- package/src/providers/ollama.ts +3 -0
- package/src/providers/openai-codex-responses.ts +20 -6
- package/src/providers/openai-completions.ts +11 -1
- 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 +175 -67
- package/src/types.d.ts +22 -2
- package/src/types.ts +27 -1
- 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
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* Transport security is delegated to the operator (Tailscale / Wireguard);
|
|
10
10
|
* the server only checks a bearer token against an allow-list per request.
|
|
11
11
|
*/
|
|
12
|
+
import * as crypto from "node:crypto";
|
|
12
13
|
import { logger } from "@gajae-code/utils";
|
|
13
14
|
import { timingSafeEqual } from "../auth-gateway/http";
|
|
14
15
|
import type { AuthStorage } from "../auth-storage";
|
|
16
|
+
import type { Provider } from "../types";
|
|
15
17
|
import { assertAuthenticatedOrLoopback, parseBind } from "../utils/parse-bind";
|
|
18
|
+
import { AUTH_BROKER_EPOCH_HEADER } from "./client";
|
|
16
19
|
import { cleanReason } from "./redact";
|
|
17
20
|
import { AuthBrokerRefresher, type AuthBrokerRefresherSchedule } from "./refresher";
|
|
18
21
|
import type {
|
|
@@ -26,8 +29,6 @@ import type {
|
|
|
26
29
|
RefresherSchedule,
|
|
27
30
|
SnapshotEntry,
|
|
28
31
|
SnapshotResponse,
|
|
29
|
-
SnapshotStreamEntryEvent,
|
|
30
|
-
SnapshotStreamRemovedEvent,
|
|
31
32
|
SnapshotStreamSnapshotEvent,
|
|
32
33
|
} from "./types";
|
|
33
34
|
import {
|
|
@@ -147,24 +148,27 @@ const DISABLE_ROUTE = /^\/v1\/credential\/(\d+)\/disable$/;
|
|
|
147
148
|
|
|
148
149
|
const MAX_SNAPSHOT_WAIT_MS = 30_000;
|
|
149
150
|
const DISABLED_NEXT_SWEEP_IN_MS = Number.MAX_SAFE_INTEGER;
|
|
151
|
+
const BROKER_EPOCH_SEQUENCE_CACHE_KEY = "auth-broker:epoch-sequence";
|
|
150
152
|
|
|
151
|
-
function snapshotHeaders(generation: number): Record<string, string> {
|
|
153
|
+
function snapshotHeaders(epoch: string, generation: number, includeEpoch: boolean): Record<string, string> {
|
|
152
154
|
return {
|
|
153
|
-
ETag: `"${generation}"`,
|
|
155
|
+
ETag: `"${includeEpoch ? `${epoch}:` : ""}${generation}"`,
|
|
154
156
|
"Cache-Control": "no-store",
|
|
155
157
|
};
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
function
|
|
160
|
+
function parseSnapshotTag(header: string | null): { epoch?: string; generation: number } | undefined {
|
|
159
161
|
if (!header) return undefined;
|
|
160
162
|
let value = header.trim();
|
|
161
163
|
if (value.startsWith("W/")) value = value.slice(2).trim();
|
|
162
164
|
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
163
165
|
value = value.slice(1, -1);
|
|
164
166
|
}
|
|
165
|
-
const
|
|
167
|
+
const separator = value.lastIndexOf(":");
|
|
168
|
+
const epoch = separator > 0 ? value.slice(0, separator) : undefined;
|
|
169
|
+
const generation = Number(separator > 0 ? value.slice(separator + 1) : value);
|
|
166
170
|
if (!Number.isInteger(generation) || generation < 0) return undefined;
|
|
167
|
-
return generation;
|
|
171
|
+
return { epoch, generation };
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
function parseWaitMs(url: URL): number {
|
|
@@ -286,7 +290,12 @@ function computeRotatesInMs(
|
|
|
286
290
|
return Math.max(0, rotatesAt - serverNowMs);
|
|
287
291
|
}
|
|
288
292
|
|
|
289
|
-
function buildSnapshot(
|
|
293
|
+
function buildSnapshot(
|
|
294
|
+
storage: AuthStorage,
|
|
295
|
+
refresher: AuthBrokerRefresher | undefined,
|
|
296
|
+
epoch: string,
|
|
297
|
+
includeEpoch: boolean,
|
|
298
|
+
): SnapshotResponse {
|
|
290
299
|
const serverNowMs = Date.now();
|
|
291
300
|
const base = storage.exportSnapshot();
|
|
292
301
|
const { wire, nextSweepAt } = resolveRefresherSchedule(refresher, serverNowMs);
|
|
@@ -295,6 +304,7 @@ function buildSnapshot(storage: AuthStorage, refresher: AuthBrokerRefresher | un
|
|
|
295
304
|
rotatesInMs: computeRotatesInMs(entry, wire, nextSweepAt, serverNowMs),
|
|
296
305
|
}));
|
|
297
306
|
return {
|
|
307
|
+
...(includeEpoch ? { epoch } : {}),
|
|
298
308
|
generation: base.generation,
|
|
299
309
|
generatedAt: base.generatedAt,
|
|
300
310
|
serverNowMs,
|
|
@@ -304,15 +314,20 @@ function buildSnapshot(storage: AuthStorage, refresher: AuthBrokerRefresher | un
|
|
|
304
314
|
}
|
|
305
315
|
|
|
306
316
|
/** Build the payload-free credential inventory projection for the metadata route. */
|
|
307
|
-
function buildCredentialMetadata(
|
|
317
|
+
function buildCredentialMetadata(
|
|
318
|
+
storage: AuthStorage,
|
|
319
|
+
epoch: string,
|
|
320
|
+
includeEpoch: boolean,
|
|
321
|
+
): CredentialMetadataResponse {
|
|
308
322
|
const credentials = storage.listCredentialInventory().map(record => ({
|
|
309
323
|
id: record.id,
|
|
310
324
|
provider: record.provider,
|
|
311
325
|
type: record.credentialKind,
|
|
312
326
|
identity: record.identityLabel,
|
|
313
|
-
disabledCause: record.disabledCause,
|
|
327
|
+
disabledCause: record.disabledCause === null ? null : "disabled via auth-broker",
|
|
314
328
|
}));
|
|
315
329
|
return {
|
|
330
|
+
...(includeEpoch ? { epoch } : {}),
|
|
316
331
|
generation: storage.getGeneration(),
|
|
317
332
|
generatedAt: Date.now(),
|
|
318
333
|
credentials,
|
|
@@ -325,21 +340,31 @@ async function serveSnapshot(
|
|
|
325
340
|
storage: AuthStorage,
|
|
326
341
|
gate: GenerationGate,
|
|
327
342
|
refresher: AuthBrokerRefresher | undefined,
|
|
343
|
+
epoch: string,
|
|
328
344
|
peer: string,
|
|
329
345
|
): Promise<Response> {
|
|
330
346
|
await storage.reload();
|
|
331
347
|
let currentGeneration = storage.getGeneration();
|
|
332
|
-
const
|
|
348
|
+
const clientTag = parseSnapshotTag(req.headers.get("if-none-match"));
|
|
349
|
+
const includeEpoch = req.headers.get(AUTH_BROKER_EPOCH_HEADER) === "1";
|
|
350
|
+
const clientGeneration = clientTag?.generation;
|
|
333
351
|
const waitMs = parseWaitMs(url);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
352
|
+
const legacyConditional = clientTag !== undefined && !includeEpoch;
|
|
353
|
+
|
|
354
|
+
if (
|
|
355
|
+
legacyConditional ||
|
|
356
|
+
clientGeneration === undefined ||
|
|
357
|
+
(includeEpoch && clientTag?.epoch !== epoch) ||
|
|
358
|
+
currentGeneration !== clientGeneration ||
|
|
359
|
+
waitMs <= 0
|
|
360
|
+
) {
|
|
361
|
+
const body = buildSnapshot(storage, refresher, epoch, includeEpoch);
|
|
337
362
|
logger.info("auth-broker snapshot served", {
|
|
338
363
|
peer,
|
|
339
364
|
credentials: body.credentials.length,
|
|
340
365
|
generation: body.generation,
|
|
341
366
|
});
|
|
342
|
-
return json(200, body, snapshotHeaders(body.generation));
|
|
367
|
+
return json(200, body, snapshotHeaders(epoch, body.generation, includeEpoch));
|
|
343
368
|
}
|
|
344
369
|
|
|
345
370
|
const delay = delayResult(waitMs);
|
|
@@ -348,35 +373,23 @@ async function serveSnapshot(
|
|
|
348
373
|
const result = await Promise.race([gate.waitForChange(clientGeneration, waitSignal), delay.promise]);
|
|
349
374
|
delay.cancel();
|
|
350
375
|
waitController.abort();
|
|
351
|
-
if (result === "aborted" || req.signal.aborted)
|
|
376
|
+
if (result === "aborted" || req.signal.aborted)
|
|
377
|
+
return empty(499, snapshotHeaders(epoch, currentGeneration, includeEpoch));
|
|
352
378
|
|
|
353
379
|
await storage.reload();
|
|
354
380
|
currentGeneration = storage.getGeneration();
|
|
355
|
-
if (currentGeneration !== clientGeneration) {
|
|
356
|
-
const body = buildSnapshot(storage, refresher);
|
|
381
|
+
if (currentGeneration !== clientGeneration || (includeEpoch && clientTag?.epoch !== epoch)) {
|
|
382
|
+
const body = buildSnapshot(storage, refresher, epoch, includeEpoch);
|
|
357
383
|
logger.info("auth-broker snapshot long-poll changed", {
|
|
358
384
|
peer,
|
|
359
385
|
credentials: body.credentials.length,
|
|
360
386
|
generation: body.generation,
|
|
361
387
|
});
|
|
362
|
-
return json(200, body, snapshotHeaders(body.generation));
|
|
388
|
+
return json(200, body, snapshotHeaders(epoch, body.generation, includeEpoch));
|
|
363
389
|
}
|
|
364
390
|
|
|
365
391
|
logger.info("auth-broker snapshot long-poll unchanged", { peer, generation: currentGeneration });
|
|
366
|
-
return empty(304, snapshotHeaders(currentGeneration));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Stable per-credential fingerprint for SSE delta detection. Field order is
|
|
371
|
-
* fixed by this serializer (NOT by entry insertion order) so a credential
|
|
372
|
-
* built by two different paths still produces the same fingerprint.
|
|
373
|
-
*
|
|
374
|
-
* `rotatesInMs` is intentionally part of the fingerprint: when it shifts we
|
|
375
|
-
* want the client to recompute its `prepareForRequest` deadline rather than
|
|
376
|
-
* keep the stale projection.
|
|
377
|
-
*/
|
|
378
|
-
function fingerprintEntry(entry: SnapshotEntry): string {
|
|
379
|
-
return JSON.stringify([entry.id, entry.provider, entry.identityKey, entry.rotatesInMs, entry.credential]);
|
|
392
|
+
return empty(304, snapshotHeaders(epoch, currentGeneration, includeEpoch));
|
|
380
393
|
}
|
|
381
394
|
|
|
382
395
|
function sseEvent(event: string, body: unknown): string {
|
|
@@ -387,18 +400,20 @@ function serveSnapshotStream(
|
|
|
387
400
|
req: Request,
|
|
388
401
|
storage: AuthStorage,
|
|
389
402
|
refresher: AuthBrokerRefresher | undefined,
|
|
403
|
+
epoch: string,
|
|
390
404
|
peer: string,
|
|
391
405
|
keepaliveMs: number,
|
|
392
406
|
): Response {
|
|
393
407
|
const encoder = new TextEncoder();
|
|
408
|
+
const includeEpoch = req.headers.get(AUTH_BROKER_EPOCH_HEADER) === "1";
|
|
394
409
|
const openedAt = Date.now();
|
|
395
|
-
const lastByCredId = new Map<number, string>();
|
|
396
410
|
let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
397
411
|
let unsubscribe: (() => void) | null = null;
|
|
398
412
|
let keepaliveTimer: NodeJS.Timeout | undefined;
|
|
399
413
|
let abortHandler: (() => void) | null = null;
|
|
400
414
|
let processing = false;
|
|
401
415
|
let pendingBumps = 0;
|
|
416
|
+
let initializing = true;
|
|
402
417
|
let closed = false;
|
|
403
418
|
let lastGeneration = -1;
|
|
404
419
|
|
|
@@ -447,9 +462,18 @@ function serveSnapshotStream(
|
|
|
447
462
|
try {
|
|
448
463
|
do {
|
|
449
464
|
pendingBumps = 0;
|
|
450
|
-
|
|
465
|
+
try {
|
|
466
|
+
await storage.reload();
|
|
467
|
+
} catch (error) {
|
|
468
|
+
logger.warn("auth-broker stream generation reload failed", {
|
|
469
|
+
peer,
|
|
470
|
+
error: cleanReason(error) ?? "credential snapshot reload failed",
|
|
471
|
+
});
|
|
472
|
+
cleanup();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
451
475
|
if (closed) return;
|
|
452
|
-
const snapshot = buildSnapshot(storage, refresher);
|
|
476
|
+
const snapshot = buildSnapshot(storage, refresher, epoch, includeEpoch);
|
|
453
477
|
// Generation must move forward; a duplicate listener firing without a
|
|
454
478
|
// real bump is a no-op below (fingerprints unchanged).
|
|
455
479
|
if (snapshot.generation < lastGeneration) {
|
|
@@ -460,40 +484,13 @@ function serveSnapshotStream(
|
|
|
460
484
|
});
|
|
461
485
|
}
|
|
462
486
|
lastGeneration = snapshot.generation;
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
kind: "entry",
|
|
471
|
-
generation: snapshot.generation,
|
|
472
|
-
serverNowMs: snapshot.serverNowMs,
|
|
473
|
-
refresher: snapshot.refresher,
|
|
474
|
-
entry,
|
|
475
|
-
};
|
|
476
|
-
if (!write(sseEvent("entry", payload))) return;
|
|
477
|
-
logger.debug("auth-broker stream entry", {
|
|
478
|
-
peer,
|
|
479
|
-
id: entry.id,
|
|
480
|
-
provider: entry.provider,
|
|
481
|
-
generation: snapshot.generation,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
for (const id of [...lastByCredId.keys()]) {
|
|
485
|
-
if (seenIds.has(id)) continue;
|
|
486
|
-
lastByCredId.delete(id);
|
|
487
|
-
const payload: SnapshotStreamRemovedEvent = {
|
|
488
|
-
kind: "removed",
|
|
489
|
-
generation: snapshot.generation,
|
|
490
|
-
serverNowMs: snapshot.serverNowMs,
|
|
491
|
-
refresher: snapshot.refresher,
|
|
492
|
-
id,
|
|
493
|
-
};
|
|
494
|
-
if (!write(sseEvent("removed", payload))) return;
|
|
495
|
-
logger.debug("auth-broker stream removed", { peer, id, generation: snapshot.generation });
|
|
496
|
-
}
|
|
487
|
+
const snapshotEvent: SnapshotStreamSnapshotEvent = { kind: "snapshot", ...snapshot };
|
|
488
|
+
if (!write(sseEvent("snapshot", snapshotEvent))) return;
|
|
489
|
+
logger.debug("auth-broker stream snapshot", {
|
|
490
|
+
peer,
|
|
491
|
+
credentialCount: snapshot.credentials.length,
|
|
492
|
+
generation: snapshot.generation,
|
|
493
|
+
});
|
|
497
494
|
} while (pendingBumps > 0 && !closed);
|
|
498
495
|
} finally {
|
|
499
496
|
processing = false;
|
|
@@ -503,22 +500,39 @@ function serveSnapshotStream(
|
|
|
503
500
|
const stream = new ReadableStream<Uint8Array>({
|
|
504
501
|
async start(c) {
|
|
505
502
|
controller = c;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
lastGeneration = initial.generation;
|
|
509
|
-
for (const entry of initial.credentials) lastByCredId.set(entry.id, fingerprintEntry(entry));
|
|
510
|
-
const initialEvent: SnapshotStreamSnapshotEvent = { kind: "snapshot", ...initial };
|
|
511
|
-
if (!write(sseEvent("snapshot", initialEvent))) return;
|
|
512
|
-
keepaliveTimer = setInterval(() => {
|
|
513
|
-
write(": keepalive\n\n");
|
|
514
|
-
}, keepaliveMs);
|
|
515
|
-
keepaliveTimer.unref?.();
|
|
503
|
+
abortHandler = (): void => cleanup();
|
|
504
|
+
req.signal.addEventListener("abort", abortHandler, { once: true });
|
|
516
505
|
unsubscribe = storage.onGenerationChanged(() => {
|
|
506
|
+
if (initializing) {
|
|
507
|
+
pendingBumps += 1;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
517
510
|
void processGenerationBump();
|
|
518
511
|
});
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
512
|
+
try {
|
|
513
|
+
await storage.reload();
|
|
514
|
+
if (closed || req.signal.aborted) {
|
|
515
|
+
cleanup();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const initial = buildSnapshot(storage, refresher, epoch, includeEpoch);
|
|
519
|
+
lastGeneration = initial.generation;
|
|
520
|
+
const initialEvent: SnapshotStreamSnapshotEvent = { kind: "snapshot", ...initial };
|
|
521
|
+
if (!write(sseEvent("snapshot", initialEvent))) return;
|
|
522
|
+
initializing = false;
|
|
523
|
+
keepaliveTimer = setInterval(() => {
|
|
524
|
+
write(": keepalive\n\n");
|
|
525
|
+
}, keepaliveMs);
|
|
526
|
+
keepaliveTimer.unref?.();
|
|
527
|
+
if (pendingBumps > 0) void processGenerationBump();
|
|
528
|
+
logger.info("auth-broker stream opened", { peer, generation: initial.generation });
|
|
529
|
+
} catch (error) {
|
|
530
|
+
logger.warn("auth-broker stream initialization failed", {
|
|
531
|
+
peer,
|
|
532
|
+
error: cleanReason(error) ?? "credential snapshot initialization failed",
|
|
533
|
+
});
|
|
534
|
+
cleanup();
|
|
535
|
+
}
|
|
522
536
|
},
|
|
523
537
|
cancel() {
|
|
524
538
|
cleanup();
|
|
@@ -543,6 +557,18 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
543
557
|
assertAuthenticatedOrLoopback(bind, tokens.size, "auth-broker");
|
|
544
558
|
const version = opts.version;
|
|
545
559
|
const streamKeepaliveMs = opts.streamKeepaliveMs ?? DEFAULT_STREAM_KEEPALIVE_MS;
|
|
560
|
+
let epochSequence: number;
|
|
561
|
+
try {
|
|
562
|
+
epochSequence = opts.storage.allocateMonotonicSequence(
|
|
563
|
+
BROKER_EPOCH_SEQUENCE_CACHE_KEY,
|
|
564
|
+
Math.floor(Date.now() / 1000) + 315_360_000,
|
|
565
|
+
);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`Auth broker storage must support atomic monotonic sequence allocation: ${cleanReason(error) ?? "unsupported storage"}`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const epoch = `${epochSequence}-${crypto.randomUUID()}`;
|
|
546
572
|
|
|
547
573
|
const refresher = opts.disableRefresher
|
|
548
574
|
? undefined
|
|
@@ -574,7 +600,11 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
574
600
|
}
|
|
575
601
|
if (req.method === "GET" && pathname === "/v1/credentials/metadata") {
|
|
576
602
|
await opts.storage.reload();
|
|
577
|
-
const body = buildCredentialMetadata(
|
|
603
|
+
const body = buildCredentialMetadata(
|
|
604
|
+
opts.storage,
|
|
605
|
+
epoch,
|
|
606
|
+
req.headers.get(AUTH_BROKER_EPOCH_HEADER) === "1",
|
|
607
|
+
);
|
|
578
608
|
logger.info("auth-broker credential metadata served", {
|
|
579
609
|
generation: body.generation,
|
|
580
610
|
status: "ok",
|
|
@@ -582,19 +612,27 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
582
612
|
return json(200, body);
|
|
583
613
|
}
|
|
584
614
|
if (req.method === "GET" && pathname === "/v1/snapshot/stream") {
|
|
585
|
-
return serveSnapshotStream(req, opts.storage, refresher, peer, streamKeepaliveMs);
|
|
615
|
+
return serveSnapshotStream(req, opts.storage, refresher, epoch, peer, streamKeepaliveMs);
|
|
586
616
|
}
|
|
587
617
|
if (req.method === "GET" && pathname === "/v1/snapshot") {
|
|
588
|
-
return serveSnapshot(req, url, opts.storage, generationGate, refresher, peer);
|
|
618
|
+
return serveSnapshot(req, url, opts.storage, generationGate, refresher, epoch, peer);
|
|
589
619
|
}
|
|
590
|
-
if (req.method === "GET" && pathname === "/v1/usage") {
|
|
620
|
+
if (req.method === "GET" && (pathname === "/v1/usage" || pathname === "/v1/usage/scoped")) {
|
|
591
621
|
try {
|
|
592
622
|
// AuthStorage caches usage reports internally with a 5-minute per-credential
|
|
593
623
|
// TTL (USAGE_REPORT_TTL_MS) so back-to-back widget polls re-use the
|
|
594
624
|
// last fetch instead of hitting provider endpoints repeatedly.
|
|
595
625
|
// `req.signal` propagates HTTP-client disconnects all the way to the
|
|
596
626
|
// per-caller cancel without touching the shared upstream fetch.
|
|
597
|
-
const
|
|
627
|
+
const scopedProvider = url.searchParams.get("provider") as Provider | null;
|
|
628
|
+
if (pathname === "/v1/usage/scoped" && !scopedProvider) {
|
|
629
|
+
return json(400, { error: "provider is required" });
|
|
630
|
+
}
|
|
631
|
+
const reports =
|
|
632
|
+
(await opts.storage.fetchUsageReports?.({
|
|
633
|
+
provider: scopedProvider ?? undefined,
|
|
634
|
+
signal: req.signal,
|
|
635
|
+
})) ?? [];
|
|
598
636
|
// Drop the `raw` field — it's the provider-specific upstream body,
|
|
599
637
|
// large and unstable. Everything UI-relevant lives in `limits` and
|
|
600
638
|
// `metadata`.
|
|
@@ -635,9 +673,11 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
635
673
|
const id = Number.parseInt(disableMatch[1], 10);
|
|
636
674
|
const parsed = await parseBody(req, credentialDisableRequestSchema, { allowEmpty: true });
|
|
637
675
|
if (!parsed.ok) return parsed.response;
|
|
638
|
-
const cause =
|
|
639
|
-
|
|
640
|
-
|
|
676
|
+
const cause = "disabled via auth-broker";
|
|
677
|
+
const ok =
|
|
678
|
+
parsed.data.expectedRevision === undefined
|
|
679
|
+
? opts.storage.disableCredentialById(id, cause)
|
|
680
|
+
: opts.storage.disableCredentialByIdIfRevision(id, parsed.data.expectedRevision, cause);
|
|
641
681
|
if (!ok) {
|
|
642
682
|
logger.info("auth-broker disable miss", { id, peer });
|
|
643
683
|
return json(404, { error: `No credential with id=${id}` });
|
package/src/auth-broker/types.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface CredentialMetadataRecord {
|
|
|
26
26
|
|
|
27
27
|
/** GET /v1/credentials/metadata response body. */
|
|
28
28
|
export interface CredentialMetadataResponse {
|
|
29
|
+
epoch?: string;
|
|
29
30
|
generation: number;
|
|
30
31
|
generatedAt: number;
|
|
31
32
|
credentials: CredentialMetadataRecord[];
|
|
@@ -50,6 +51,8 @@ export type SnapshotEntry = AuthCredentialSnapshotEntry & {
|
|
|
50
51
|
|
|
51
52
|
/** GET /v1/snapshot response body. */
|
|
52
53
|
export interface SnapshotResponse extends Omit<AuthCredentialSnapshot, "credentials"> {
|
|
54
|
+
/** Stable for one broker process; changes when the broker restarts. */
|
|
55
|
+
epoch?: string;
|
|
53
56
|
serverNowMs: number;
|
|
54
57
|
refresher: RefresherSchedule;
|
|
55
58
|
credentials: SnapshotEntry[];
|
|
@@ -72,6 +75,7 @@ export type CredentialRefreshRequest = MCPOAuthRefreshClient;
|
|
|
72
75
|
/** POST /v1/credential/:id/disable request body. */
|
|
73
76
|
export interface CredentialDisableRequest {
|
|
74
77
|
cause: string;
|
|
78
|
+
expectedRevision?: number;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
/** POST /v1/credential/:id/disable response body. */
|
|
@@ -115,6 +119,7 @@ export interface SnapshotStreamSnapshotEvent extends SnapshotResponse {
|
|
|
115
119
|
/** Single credential added/changed (upsert or refresh). */
|
|
116
120
|
export interface SnapshotStreamEntryEvent {
|
|
117
121
|
kind: "entry";
|
|
122
|
+
epoch?: string;
|
|
118
123
|
generation: number;
|
|
119
124
|
serverNowMs: number;
|
|
120
125
|
refresher: RefresherSchedule;
|
|
@@ -124,6 +129,7 @@ export interface SnapshotStreamEntryEvent {
|
|
|
124
129
|
/** Single credential disabled/deleted. */
|
|
125
130
|
export interface SnapshotStreamRemovedEvent {
|
|
126
131
|
kind: "removed";
|
|
132
|
+
epoch?: string;
|
|
127
133
|
generation: number;
|
|
128
134
|
serverNowMs: number;
|
|
129
135
|
refresher: RefresherSchedule;
|
|
@@ -81,6 +81,7 @@ export const credentialSnapshotEntrySchema = z
|
|
|
81
81
|
provider: z.string().min(1),
|
|
82
82
|
credential: snapshotCredentialSchema,
|
|
83
83
|
identityKey: z.string().nullable(),
|
|
84
|
+
revision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),
|
|
84
85
|
})
|
|
85
86
|
.strict();
|
|
86
87
|
|
|
@@ -101,6 +102,7 @@ export const refresherScheduleSchema = z
|
|
|
101
102
|
|
|
102
103
|
export const snapshotResponseSchema = z
|
|
103
104
|
.object({
|
|
105
|
+
epoch: z.string().min(1).optional(),
|
|
104
106
|
generation: z.number().int(),
|
|
105
107
|
generatedAt: z.number(),
|
|
106
108
|
serverNowMs: z.number(),
|
|
@@ -124,6 +126,7 @@ export const credentialMetadataRecordSchema = z
|
|
|
124
126
|
|
|
125
127
|
export const credentialMetadataResponseSchema = z
|
|
126
128
|
.object({
|
|
129
|
+
epoch: z.string().min(1).optional(),
|
|
127
130
|
generation: z.number().int().nonnegative(),
|
|
128
131
|
generatedAt: z.number().finite().nonnegative(),
|
|
129
132
|
credentials: z.array(credentialMetadataRecordSchema),
|
|
@@ -143,6 +146,7 @@ export const snapshotStreamSnapshotEventSchema = snapshotResponseSchema
|
|
|
143
146
|
export const snapshotStreamEntryEventSchema = z
|
|
144
147
|
.object({
|
|
145
148
|
kind: z.literal("entry"),
|
|
149
|
+
epoch: z.string().min(1).optional(),
|
|
146
150
|
generation: z.number().int(),
|
|
147
151
|
serverNowMs: z.number(),
|
|
148
152
|
refresher: refresherScheduleSchema,
|
|
@@ -154,6 +158,7 @@ export const snapshotStreamEntryEventSchema = z
|
|
|
154
158
|
export const snapshotStreamRemovedEventSchema = z
|
|
155
159
|
.object({
|
|
156
160
|
kind: z.literal("removed"),
|
|
161
|
+
epoch: z.string().min(1).optional(),
|
|
157
162
|
generation: z.number().int(),
|
|
158
163
|
serverNowMs: z.number(),
|
|
159
164
|
refresher: refresherScheduleSchema,
|
|
@@ -212,6 +217,7 @@ export const credentialRefreshResponseSchema = z
|
|
|
212
217
|
export const credentialDisableRequestSchema = z
|
|
213
218
|
.object({
|
|
214
219
|
cause: z.string().optional(),
|
|
220
|
+
expectedRevision: z.number().int().positive().optional(),
|
|
215
221
|
})
|
|
216
222
|
.strict();
|
|
217
223
|
|