@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/dist/types/auth-broker/client.d.ts +6 -2
  3. package/dist/types/auth-broker/remote-store.d.ts +14 -2
  4. package/dist/types/auth-broker/types.d.ts +6 -0
  5. package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
  6. package/dist/types/auth-gateway/server.d.ts +39 -5
  7. package/dist/types/auth-gateway/types.d.ts +16 -2
  8. package/dist/types/auth-storage.d.ts +116 -34
  9. package/dist/types/provider-models/openai-compat.d.ts +1 -0
  10. package/dist/types/provider-models/special.d.ts +2 -1
  11. package/dist/types/providers/kiro-api-key.d.ts +50 -0
  12. package/dist/types/providers/kiro-codewhisperer.d.ts +3 -0
  13. package/dist/types/providers/register-builtins.d.ts +12 -12
  14. package/dist/types/stream.d.ts +2 -1
  15. package/dist/types/types.d.ts +35 -24
  16. package/dist/types/utils/fallback-transport.d.ts +7 -0
  17. package/dist/types/utils/json-parse.d.ts +5 -3
  18. package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
  19. package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
  20. package/dist/types/utils/oauth/commandcode.d.ts +1 -0
  21. package/dist/types/utils/oauth/types.d.ts +1 -1
  22. package/dist/types/utils/retry.d.ts +2 -0
  23. package/dist/types/utils/tool-call-healing.d.ts +4 -4
  24. package/package.json +3 -3
  25. package/src/auth-broker/client.ts +41 -13
  26. package/src/auth-broker/redact.ts +25 -1
  27. package/src/auth-broker/remote-store.ts +374 -115
  28. package/src/auth-broker/server.ts +131 -91
  29. package/src/auth-broker/types.ts +6 -0
  30. package/src/auth-broker/wire-schemas.ts +6 -0
  31. package/src/auth-gateway/server.ts +447 -79
  32. package/src/auth-gateway/types.ts +28 -2
  33. package/src/auth-storage.ts +742 -157
  34. package/src/cli.ts +1 -0
  35. package/src/model-thinking.ts +16 -0
  36. package/src/models.json +1054 -0
  37. package/src/models.ts +9 -1
  38. package/src/provider-models/descriptors.ts +3 -1
  39. package/src/provider-models/openai-compat.ts +41 -1
  40. package/src/provider-models/special.ts +15 -3
  41. package/src/providers/anthropic.ts +7 -1
  42. package/src/providers/azure-openai-responses.ts +4 -1
  43. package/src/providers/cursor.ts +256 -101
  44. package/src/providers/gitlab-duo.ts +18 -1
  45. package/src/providers/google-gemini-cli.ts +3 -0
  46. package/src/providers/google-shared.ts +3 -0
  47. package/src/providers/kiro-api-key.d.ts +50 -0
  48. package/src/providers/kiro-api-key.ts +786 -0
  49. package/src/providers/kiro-codewhisperer.d.ts +3 -0
  50. package/src/providers/kiro-codewhisperer.ts +34 -9
  51. package/src/providers/ollama.ts +3 -0
  52. package/src/providers/openai-codex-responses.ts +24 -6
  53. package/src/providers/openai-completions.ts +11 -1
  54. package/src/providers/openai-responses-shared.ts +23 -2
  55. package/src/providers/openai-responses.ts +10 -1
  56. package/src/providers/pi-native-client.ts +1 -0
  57. package/src/providers/pi-native-server.ts +24 -0
  58. package/src/providers/register-builtins.d.ts +12 -12
  59. package/src/providers/register-builtins.ts +16 -3
  60. package/src/stream.d.ts +2 -1
  61. package/src/stream.ts +180 -70
  62. package/src/types.d.ts +35 -24
  63. package/src/types.ts +40 -23
  64. package/src/utils/fallback-transport.d.ts +7 -0
  65. package/src/utils/fallback-transport.ts +21 -4
  66. package/src/utils/json-parse.d.ts +5 -3
  67. package/src/utils/json-parse.ts +6 -6
  68. package/src/utils/oauth/api-key-login.ts +13 -2
  69. package/src/utils/oauth/api-key-validation.ts +242 -41
  70. package/src/utils/oauth/commandcode.ts +17 -0
  71. package/src/utils/oauth/index.ts +20 -5
  72. package/src/utils/oauth/types.d.ts +1 -1
  73. package/src/utils/oauth/types.ts +1 -0
  74. package/src/utils/retry.d.ts +2 -0
  75. package/src/utils/retry.ts +15 -2
  76. package/src/utils/tool-call-healing.d.ts +4 -4
  77. package/src/utils/tool-call-healing.ts +4 -4
@@ -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 parseGenerationTag(header: string | null): number | undefined {
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 generation = Number(value);
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(storage: AuthStorage, refresher: AuthBrokerRefresher | undefined): SnapshotResponse {
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(storage: AuthStorage): CredentialMetadataResponse {
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 clientGeneration = parseGenerationTag(req.headers.get("if-none-match"));
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
- if (clientGeneration === undefined || currentGeneration !== clientGeneration || waitMs <= 0) {
336
- const body = buildSnapshot(storage, refresher);
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) return empty(499, snapshotHeaders(currentGeneration));
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
- await storage.reload();
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 seenIds = new Set<number>();
464
- for (const entry of snapshot.credentials) {
465
- seenIds.add(entry.id);
466
- const fp = fingerprintEntry(entry);
467
- if (lastByCredId.get(entry.id) === fp) continue;
468
- lastByCredId.set(entry.id, fp);
469
- const payload: SnapshotStreamEntryEvent = {
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
- await storage.reload();
507
- const initial = buildSnapshot(storage, refresher);
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
- abortHandler = (): void => cleanup();
520
- req.signal.addEventListener("abort", abortHandler);
521
- logger.info("auth-broker stream opened", { peer, generation: initial.generation });
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(opts.storage);
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 reports = (await opts.storage.fetchUsageReports?.({ signal: req.signal })) ?? [];
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
- parsed.data.cause && parsed.data.cause.length > 0 ? parsed.data.cause : "disabled via auth-broker";
640
- const ok = opts.storage.disableCredentialById(id, cause);
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}` });
@@ -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