@oh-my-pi/pi-ai 16.3.4 → 16.3.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 +22 -0
- package/dist/types/auth-broker/client.d.ts +3 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -1
- package/dist/types/auth-broker/types.d.ts +13 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +46 -0
- package/dist/types/auth-storage.d.ts +40 -5
- package/dist/types/registry/oauth/types.d.ts +14 -0
- package/dist/types/types.d.ts +16 -0
- package/dist/types/utils/schema/normalize.d.ts +5 -0
- package/package.json +4 -4
- package/src/auth-broker/client.ts +41 -12
- package/src/auth-broker/remote-store.ts +189 -9
- package/src/auth-broker/server.ts +109 -7
- package/src/auth-broker/types.ts +22 -1
- package/src/auth-broker/wire-schemas.ts +22 -0
- package/src/auth-storage.ts +300 -24
- package/src/providers/ollama.ts +2 -2
- package/src/providers/openai-codex-responses.ts +6 -1
- package/src/providers/openai-completions.ts +12 -1
- package/src/providers/openai-responses.ts +10 -1
- package/src/registry/oauth/callback-server.ts +97 -9
- package/src/registry/oauth/types.ts +14 -0
- package/src/types.ts +18 -0
- package/src/usage/claude.ts +28 -8
- package/src/utils/schema/CONSTRAINTS.md +1 -0
- package/src/utils/schema/normalize.ts +131 -0
- package/src/utils/validation.ts +295 -0
|
@@ -16,13 +16,20 @@ import {
|
|
|
16
16
|
type OAuthCredential,
|
|
17
17
|
REMOTE_REFRESH_SENTINEL,
|
|
18
18
|
type StoredAuthCredential,
|
|
19
|
+
type StoredCredentialBlock,
|
|
19
20
|
} from "../auth-storage";
|
|
20
21
|
import * as AIError from "../error";
|
|
21
22
|
import type { OAuthCredentials } from "../registry/oauth/types";
|
|
22
23
|
import type { Provider } from "../types";
|
|
23
24
|
import type { UsageReport } from "../usage";
|
|
24
25
|
import { type AuthBrokerClient, AuthBrokerStreamUnsupportedError } from "./client";
|
|
25
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
CredentialBlockSnapshot,
|
|
28
|
+
RefresherSchedule,
|
|
29
|
+
SnapshotEntry,
|
|
30
|
+
SnapshotResponse,
|
|
31
|
+
SnapshotStreamEvent,
|
|
32
|
+
} from "./types";
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
35
|
* Client-side TTL for the aggregate `/v1/usage` response. The broker dedups
|
|
@@ -38,6 +45,31 @@ const BACKGROUND_WAIT_MS = 30_000;
|
|
|
38
45
|
const BACKGROUND_BACKOFF_INITIAL_MS = 500;
|
|
39
46
|
const BACKGROUND_BACKOFF_MAX_MS = 30_000;
|
|
40
47
|
|
|
48
|
+
function compareCredentialBlockSnapshots(a: CredentialBlockSnapshot, b: CredentialBlockSnapshot): number {
|
|
49
|
+
const provider = a.providerKey.localeCompare(b.providerKey);
|
|
50
|
+
if (provider !== 0) return provider;
|
|
51
|
+
const scope = a.blockScope.localeCompare(b.blockScope);
|
|
52
|
+
if (scope !== 0) return scope;
|
|
53
|
+
return a.blockedUntilMs - b.blockedUntilMs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toCredentialBlockSnapshot(block: StoredCredentialBlock): CredentialBlockSnapshot {
|
|
57
|
+
return {
|
|
58
|
+
providerKey: block.providerKey,
|
|
59
|
+
blockScope: block.blockScope,
|
|
60
|
+
blockedUntilMs: block.blockedUntilMs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function credentialEntryWithBlocks(
|
|
65
|
+
entry: AuthCredentialSnapshotEntry,
|
|
66
|
+
blocks: readonly CredentialBlockSnapshot[] | undefined,
|
|
67
|
+
): SnapshotEntry {
|
|
68
|
+
const incoming: SnapshotEntry = { ...entry, rotatesInMs: null };
|
|
69
|
+
if (blocks && blocks.length > 0) incoming.blocks = [...blocks].sort(compareCredentialBlockSnapshots);
|
|
70
|
+
return incoming;
|
|
71
|
+
}
|
|
72
|
+
|
|
41
73
|
function emptySnapshot(): SnapshotResponse {
|
|
42
74
|
return {
|
|
43
75
|
generation: 0,
|
|
@@ -169,13 +201,17 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
169
201
|
}
|
|
170
202
|
|
|
171
203
|
#applySnapshot(snapshot: SnapshotResponse, generation: number): void {
|
|
172
|
-
|
|
204
|
+
const nowMs = Date.now();
|
|
205
|
+
this.#snapshot = {
|
|
206
|
+
...snapshot,
|
|
207
|
+
credentials: snapshot.credentials.map(entry => this.#normalizeSnapshotEntryBlocks(entry, nowMs)),
|
|
208
|
+
};
|
|
173
209
|
this.#generation = generation;
|
|
174
|
-
this.#snapshotReceivedAt =
|
|
210
|
+
this.#snapshotReceivedAt = nowMs;
|
|
175
211
|
const onSnapshot = this.#onSnapshot;
|
|
176
212
|
if (!onSnapshot) return;
|
|
177
213
|
try {
|
|
178
|
-
onSnapshot(snapshot, generation);
|
|
214
|
+
onSnapshot(this.#snapshot, generation);
|
|
179
215
|
} catch (error) {
|
|
180
216
|
logger.debug("auth-broker snapshot callback failed", { error: String(error) });
|
|
181
217
|
}
|
|
@@ -266,11 +302,12 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
266
302
|
generation: number,
|
|
267
303
|
serverNowMs: number,
|
|
268
304
|
): void {
|
|
269
|
-
const
|
|
305
|
+
const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now());
|
|
306
|
+
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === incoming.id);
|
|
270
307
|
const credentials =
|
|
271
308
|
index === -1
|
|
272
|
-
? [...this.#snapshot.credentials,
|
|
273
|
-
: this.#snapshot.credentials.map((candidate, i) => (i === index ?
|
|
309
|
+
? [...this.#snapshot.credentials, incoming]
|
|
310
|
+
: this.#snapshot.credentials.map((candidate, i) => (i === index ? incoming : candidate));
|
|
274
311
|
this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
|
|
275
312
|
this.#generation = generation;
|
|
276
313
|
this.#snapshotReceivedAt = Date.now();
|
|
@@ -304,6 +341,76 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
304
341
|
return out;
|
|
305
342
|
}
|
|
306
343
|
|
|
344
|
+
getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
345
|
+
const nowMs = Date.now();
|
|
346
|
+
this.cleanExpiredCredentialBlocks(nowMs);
|
|
347
|
+
const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
|
|
348
|
+
if (!entry?.blocks) return undefined;
|
|
349
|
+
const block = entry.blocks.find(
|
|
350
|
+
candidate => candidate.providerKey === providerKey && candidate.blockScope === blockScope,
|
|
351
|
+
);
|
|
352
|
+
if (!block || block.blockedUntilMs <= nowMs) return undefined;
|
|
353
|
+
return block.blockedUntilMs;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
357
|
+
const nowMs = Date.now();
|
|
358
|
+
this.cleanExpiredCredentialBlocks(nowMs);
|
|
359
|
+
const ids = new Set(credentialIds);
|
|
360
|
+
const blocks: StoredCredentialBlock[] = [];
|
|
361
|
+
for (const entry of this.#snapshot.credentials) {
|
|
362
|
+
if (!ids.has(entry.id) || !entry.blocks) continue;
|
|
363
|
+
for (const block of entry.blocks) {
|
|
364
|
+
if (block.blockedUntilMs <= nowMs) continue;
|
|
365
|
+
blocks.push({
|
|
366
|
+
credentialId: entry.id,
|
|
367
|
+
providerKey: block.providerKey,
|
|
368
|
+
blockScope: block.blockScope,
|
|
369
|
+
blockedUntilMs: block.blockedUntilMs,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
blocks.sort((a, b) => a.credentialId - b.credentialId || compareCredentialBlockSnapshots(a, b));
|
|
374
|
+
return blocks;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
upsertCredentialBlock(block: StoredCredentialBlock): void {
|
|
378
|
+
this.#upsertSnapshotBlock(block);
|
|
379
|
+
const body = toCredentialBlockSnapshot(block);
|
|
380
|
+
void this.#client
|
|
381
|
+
.upsertCredentialBlock(block.credentialId, body)
|
|
382
|
+
.then(() => {
|
|
383
|
+
this.#maybeRefreshSnapshot("credential block");
|
|
384
|
+
})
|
|
385
|
+
.catch(error => {
|
|
386
|
+
logger.warn("auth-broker credential block propagation failed", {
|
|
387
|
+
id: block.credentialId,
|
|
388
|
+
providerKey: block.providerKey,
|
|
389
|
+
blockScope: block.blockScope,
|
|
390
|
+
error: String(error),
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
deleteCredentialBlocks(credentialId: number): void {
|
|
396
|
+
this.#deleteSnapshotBlocks(credentialId);
|
|
397
|
+
void this.#client
|
|
398
|
+
.deleteCredentialBlocks(credentialId)
|
|
399
|
+
.then(() => {
|
|
400
|
+
this.#maybeRefreshSnapshot("credential blocks delete");
|
|
401
|
+
})
|
|
402
|
+
.catch(error => {
|
|
403
|
+
logger.warn("auth-broker credential blocks delete propagation failed", {
|
|
404
|
+
id: credentialId,
|
|
405
|
+
error: String(error),
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
cleanExpiredCredentialBlocks(nowMs: number): void {
|
|
411
|
+
this.#pruneExpiredCredentialBlocks(nowMs);
|
|
412
|
+
}
|
|
413
|
+
|
|
307
414
|
/**
|
|
308
415
|
* In-memory update from a successful refresh through the broker. AuthStorage
|
|
309
416
|
* calls this after `#replaceCredentialAt`; the broker already persisted the
|
|
@@ -459,13 +566,19 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
459
566
|
// `entries` is the broker's authoritative post-upsert list of rows for
|
|
460
567
|
// `provider`. Drop our existing rows for the same provider and splice in
|
|
461
568
|
// the fresh set — preserving every other provider's rows in place.
|
|
569
|
+
const existingBlocks = new Map(
|
|
570
|
+
this.#snapshot.credentials
|
|
571
|
+
.filter(entry => entry.provider === provider && entry.blocks !== undefined)
|
|
572
|
+
.map(entry => [entry.id, entry.blocks] as const),
|
|
573
|
+
);
|
|
462
574
|
const others = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
|
|
463
|
-
const incoming = entries.map(entry => (
|
|
575
|
+
const incoming = entries.map(entry => credentialEntryWithBlocks(entry, existingBlocks.get(entry.id)));
|
|
464
576
|
this.#snapshot = { ...this.#snapshot, credentials: [...others, ...incoming] };
|
|
465
577
|
}
|
|
466
578
|
#applyCredentialEntry(entry: AuthCredentialSnapshotEntry): void {
|
|
467
|
-
const incoming = { ...entry, rotatesInMs: null };
|
|
468
579
|
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === entry.id);
|
|
580
|
+
const existingBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
|
|
581
|
+
const incoming = credentialEntryWithBlocks(entry, existingBlocks);
|
|
469
582
|
if (index === -1) {
|
|
470
583
|
this.#snapshot = { ...this.#snapshot, credentials: [...this.#snapshot.credentials, incoming] };
|
|
471
584
|
return;
|
|
@@ -485,6 +598,73 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
485
598
|
this.#snapshot = { ...this.#snapshot, credentials: next };
|
|
486
599
|
}
|
|
487
600
|
|
|
601
|
+
#normalizeSnapshotEntryBlocks(entry: SnapshotEntry, nowMs: number): SnapshotEntry {
|
|
602
|
+
if (!entry.blocks || entry.blocks.length === 0) return entry;
|
|
603
|
+
const blocks = entry.blocks
|
|
604
|
+
.filter(block => block.blockedUntilMs > nowMs)
|
|
605
|
+
.map(block => ({
|
|
606
|
+
providerKey: block.providerKey,
|
|
607
|
+
blockScope: block.blockScope,
|
|
608
|
+
blockedUntilMs: block.blockedUntilMs,
|
|
609
|
+
}))
|
|
610
|
+
.sort(compareCredentialBlockSnapshots);
|
|
611
|
+
if (blocks.length > 0) return { ...entry, blocks };
|
|
612
|
+
const next: SnapshotEntry = { ...entry };
|
|
613
|
+
delete next.blocks;
|
|
614
|
+
return next;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#upsertSnapshotBlock(block: StoredCredentialBlock): void {
|
|
618
|
+
const index = this.#snapshot.credentials.findIndex(entry => entry.id === block.credentialId);
|
|
619
|
+
if (index === -1) return;
|
|
620
|
+
const entry = this.#snapshot.credentials[index]!;
|
|
621
|
+
const incoming = toCredentialBlockSnapshot(block);
|
|
622
|
+
const blocks = entry.blocks ? [...entry.blocks] : [];
|
|
623
|
+
const blockIndex = blocks.findIndex(
|
|
624
|
+
candidate => candidate.providerKey === incoming.providerKey && candidate.blockScope === incoming.blockScope,
|
|
625
|
+
);
|
|
626
|
+
if (blockIndex === -1) {
|
|
627
|
+
blocks.push(incoming);
|
|
628
|
+
} else {
|
|
629
|
+
const existing = blocks[blockIndex]!;
|
|
630
|
+
blocks[blockIndex] = {
|
|
631
|
+
...existing,
|
|
632
|
+
blockedUntilMs: Math.max(existing.blockedUntilMs, incoming.blockedUntilMs),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
blocks.sort(compareCredentialBlockSnapshots);
|
|
636
|
+
const credentials = [...this.#snapshot.credentials];
|
|
637
|
+
credentials[index] = { ...entry, blocks };
|
|
638
|
+
this.#snapshot = { ...this.#snapshot, credentials };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
#deleteSnapshotBlocks(credentialId: number): void {
|
|
642
|
+
const index = this.#snapshot.credentials.findIndex(entry => entry.id === credentialId);
|
|
643
|
+
if (index === -1) return;
|
|
644
|
+
const entry = this.#snapshot.credentials[index]!;
|
|
645
|
+
if (!entry.blocks || entry.blocks.length === 0) return;
|
|
646
|
+
const next: SnapshotEntry = { ...entry };
|
|
647
|
+
delete next.blocks;
|
|
648
|
+
const credentials = [...this.#snapshot.credentials];
|
|
649
|
+
credentials[index] = next;
|
|
650
|
+
this.#snapshot = { ...this.#snapshot, credentials };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#pruneExpiredCredentialBlocks(nowMs: number): void {
|
|
654
|
+
let changed = false;
|
|
655
|
+
const credentials = this.#snapshot.credentials.map(entry => {
|
|
656
|
+
if (!entry.blocks || entry.blocks.length === 0) return entry;
|
|
657
|
+
const blocks = entry.blocks.filter(block => block.blockedUntilMs > nowMs);
|
|
658
|
+
if (blocks.length === entry.blocks.length) return entry;
|
|
659
|
+
changed = true;
|
|
660
|
+
if (blocks.length > 0) return { ...entry, blocks };
|
|
661
|
+
const next: SnapshotEntry = { ...entry };
|
|
662
|
+
delete next.blocks;
|
|
663
|
+
return next;
|
|
664
|
+
});
|
|
665
|
+
if (changed) this.#snapshot = { ...this.#snapshot, credentials };
|
|
666
|
+
}
|
|
667
|
+
|
|
488
668
|
/**
|
|
489
669
|
* Fire-and-forget `refreshSnapshot()` after a write. When the SSE stream is
|
|
490
670
|
* active the broker will deliver the new generation push, so the extra GET
|
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
13
13
|
import { type Type, type } from "arktype";
|
|
14
|
-
import type { AuthStorage } from "../auth-storage";
|
|
14
|
+
import type { AuthStorage, StoredCredentialBlock } from "../auth-storage";
|
|
15
15
|
import { parseBind } from "../utils/parse-bind";
|
|
16
16
|
import { AuthBrokerRefresher, type AuthBrokerRefresherSchedule } from "./refresher";
|
|
17
17
|
import type {
|
|
18
|
+
CredentialBlockResponse,
|
|
19
|
+
CredentialBlockSnapshot,
|
|
20
|
+
CredentialBlocksDeleteResponse,
|
|
18
21
|
CredentialDisableResponse,
|
|
19
22
|
CredentialRefreshResponse,
|
|
20
23
|
CredentialUploadResponse,
|
|
@@ -33,7 +36,11 @@ import {
|
|
|
33
36
|
DEFAULT_SERVER_IDLE_TIMEOUT_S,
|
|
34
37
|
DEFAULT_STREAM_KEEPALIVE_MS,
|
|
35
38
|
} from "./types";
|
|
36
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
credentialBlockRequestSchema,
|
|
41
|
+
credentialDisableRequestSchema,
|
|
42
|
+
credentialUploadRequestSchema,
|
|
43
|
+
} from "./wire-schemas";
|
|
37
44
|
|
|
38
45
|
export interface AuthBrokerServerOptions {
|
|
39
46
|
/** Underlying credential storage (wraps the local SQLite store on the broker). */
|
|
@@ -120,6 +127,8 @@ async function parseBody<t>(
|
|
|
120
127
|
|
|
121
128
|
const REFRESH_ROUTE = /^\/v1\/credential\/(\d+)\/refresh$/;
|
|
122
129
|
const DISABLE_ROUTE = /^\/v1\/credential\/(\d+)\/disable$/;
|
|
130
|
+
const BLOCK_ROUTE = /^\/v1\/credential\/(\d+)\/block$/;
|
|
131
|
+
const BLOCKS_ROUTE = /^\/v1\/credential\/(\d+)\/blocks$/;
|
|
123
132
|
|
|
124
133
|
const MAX_SNAPSHOT_WAIT_MS = 30_000;
|
|
125
134
|
const DISABLED_NEXT_SWEEP_IN_MS = Number.MAX_SAFE_INTEGER;
|
|
@@ -262,14 +271,48 @@ function computeRotatesInMs(
|
|
|
262
271
|
return Math.max(0, rotatesAt - serverNowMs);
|
|
263
272
|
}
|
|
264
273
|
|
|
274
|
+
function compareCredentialBlockSnapshots(a: CredentialBlockSnapshot, b: CredentialBlockSnapshot): number {
|
|
275
|
+
const provider = a.providerKey.localeCompare(b.providerKey);
|
|
276
|
+
if (provider !== 0) return provider;
|
|
277
|
+
const scope = a.blockScope.localeCompare(b.blockScope);
|
|
278
|
+
if (scope !== 0) return scope;
|
|
279
|
+
return a.blockedUntilMs - b.blockedUntilMs;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildCredentialBlockGroups(
|
|
283
|
+
blocks: readonly StoredCredentialBlock[],
|
|
284
|
+
serverNowMs: number,
|
|
285
|
+
): Map<number, CredentialBlockSnapshot[]> {
|
|
286
|
+
const byCredentialId = new Map<number, CredentialBlockSnapshot[]>();
|
|
287
|
+
for (const block of blocks) {
|
|
288
|
+
if (block.blockedUntilMs <= serverNowMs) continue;
|
|
289
|
+
const snapshotBlock: CredentialBlockSnapshot = {
|
|
290
|
+
providerKey: block.providerKey,
|
|
291
|
+
blockScope: block.blockScope,
|
|
292
|
+
blockedUntilMs: block.blockedUntilMs,
|
|
293
|
+
};
|
|
294
|
+
const existing = byCredentialId.get(block.credentialId);
|
|
295
|
+
if (existing) {
|
|
296
|
+
existing.push(snapshotBlock);
|
|
297
|
+
} else {
|
|
298
|
+
byCredentialId.set(block.credentialId, [snapshotBlock]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const credentialBlocks of byCredentialId.values()) credentialBlocks.sort(compareCredentialBlockSnapshots);
|
|
302
|
+
return byCredentialId;
|
|
303
|
+
}
|
|
304
|
+
|
|
265
305
|
function buildSnapshot(storage: AuthStorage, refresher: AuthBrokerRefresher | undefined): SnapshotResponse {
|
|
266
306
|
const serverNowMs = Date.now();
|
|
267
307
|
const base = storage.exportSnapshot();
|
|
268
308
|
const { wire, nextSweepAt } = resolveRefresherSchedule(refresher, serverNowMs);
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
309
|
+
const credentialIds = base.credentials.map(entry => entry.id);
|
|
310
|
+
const blocksByCredentialId = buildCredentialBlockGroups(storage.listCredentialBlocks(credentialIds), serverNowMs);
|
|
311
|
+
const credentials: SnapshotEntry[] = base.credentials.map(entry => {
|
|
312
|
+
const blocks = blocksByCredentialId.get(entry.id);
|
|
313
|
+
const rotatesInMs = computeRotatesInMs(entry, wire, nextSweepAt, serverNowMs);
|
|
314
|
+
return blocks && blocks.length > 0 ? { ...entry, rotatesInMs, blocks } : { ...entry, rotatesInMs };
|
|
315
|
+
});
|
|
273
316
|
return {
|
|
274
317
|
generation: base.generation,
|
|
275
318
|
generatedAt: base.generatedAt,
|
|
@@ -336,7 +379,14 @@ async function serveSnapshot(
|
|
|
336
379
|
* keep the stale projection.
|
|
337
380
|
*/
|
|
338
381
|
function fingerprintEntry(entry: SnapshotEntry): string {
|
|
339
|
-
return JSON.stringify([
|
|
382
|
+
return JSON.stringify([
|
|
383
|
+
entry.id,
|
|
384
|
+
entry.provider,
|
|
385
|
+
entry.identityKey,
|
|
386
|
+
entry.rotatesInMs,
|
|
387
|
+
entry.credential,
|
|
388
|
+
entry.blocks ?? [],
|
|
389
|
+
]);
|
|
340
390
|
}
|
|
341
391
|
|
|
342
392
|
function sseEvent(event: string, body: unknown): string {
|
|
@@ -593,6 +643,58 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
593
643
|
const response: CredentialDisableResponse = { ok: true };
|
|
594
644
|
return json(200, response);
|
|
595
645
|
}
|
|
646
|
+
const blockMatch = req.method === "POST" ? pathname.match(BLOCK_ROUTE) : null;
|
|
647
|
+
if (blockMatch) {
|
|
648
|
+
const id = Number.parseInt(blockMatch[1], 10);
|
|
649
|
+
const parsed = await parseBody(req, credentialBlockRequestSchema);
|
|
650
|
+
if (!parsed.ok) return parsed.response;
|
|
651
|
+
const block: StoredCredentialBlock = {
|
|
652
|
+
credentialId: id,
|
|
653
|
+
providerKey: parsed.data.providerKey,
|
|
654
|
+
blockScope: parsed.data.blockScope,
|
|
655
|
+
blockedUntilMs: parsed.data.blockedUntilMs,
|
|
656
|
+
};
|
|
657
|
+
if (!opts.storage.exportSnapshot().credentials.some(entry => entry.id === id)) {
|
|
658
|
+
logger.info("auth-broker credential block miss", { id, peer });
|
|
659
|
+
return json(404, { error: `No credential with id=${id}` });
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
opts.storage.upsertCredentialBlock(block);
|
|
663
|
+
const response: CredentialBlockResponse = { ok: true };
|
|
664
|
+
logger.info("auth-broker credential block upserted", {
|
|
665
|
+
id,
|
|
666
|
+
peer,
|
|
667
|
+
providerKey: block.providerKey,
|
|
668
|
+
blockScope: block.blockScope,
|
|
669
|
+
blockedUntilMs: block.blockedUntilMs,
|
|
670
|
+
});
|
|
671
|
+
return json(200, response);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
674
|
+
logger.warn("auth-broker credential block upsert failed", { id, peer, error: message });
|
|
675
|
+
const status = message.includes("No credential with id") ? 404 : 500;
|
|
676
|
+
return json(status, { error: message });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const blocksDeleteMatch = req.method === "DELETE" ? pathname.match(BLOCKS_ROUTE) : null;
|
|
680
|
+
if (blocksDeleteMatch) {
|
|
681
|
+
const id = Number.parseInt(blocksDeleteMatch[1], 10);
|
|
682
|
+
if (!opts.storage.exportSnapshot().credentials.some(entry => entry.id === id)) {
|
|
683
|
+
logger.info("auth-broker credential blocks delete miss", { id, peer });
|
|
684
|
+
return json(404, { error: `No credential with id=${id}` });
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
opts.storage.deleteCredentialBlocks(id);
|
|
688
|
+
const response: CredentialBlocksDeleteResponse = { ok: true };
|
|
689
|
+
logger.info("auth-broker credential blocks deleted", { id, peer });
|
|
690
|
+
return json(200, response);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
693
|
+
logger.warn("auth-broker credential blocks delete failed", { id, peer, error: message });
|
|
694
|
+
const status = message.includes("No credential with id") ? 404 : 500;
|
|
695
|
+
return json(status, { error: message });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
596
698
|
if (req.method === "POST" && pathname === "/v1/credential") {
|
|
597
699
|
const parsed = await parseBody(req, credentialUploadRequestSchema);
|
|
598
700
|
if (!parsed.ok) return parsed.response;
|
package/src/auth-broker/types.ts
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* credential expires or a 401 surfaces on a supposedly-fresh credential.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AuthCredential,
|
|
11
|
+
AuthCredentialSnapshot,
|
|
12
|
+
AuthCredentialSnapshotEntry,
|
|
13
|
+
StoredCredentialBlock,
|
|
14
|
+
} from "../auth-storage";
|
|
10
15
|
import type { UsageReport } from "../usage";
|
|
11
16
|
|
|
12
17
|
/** GET /v1/healthz response body. */
|
|
@@ -22,8 +27,11 @@ export interface RefresherSchedule {
|
|
|
22
27
|
nextSweepInMs: number;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
export type CredentialBlockSnapshot = Omit<StoredCredentialBlock, "credentialId">;
|
|
31
|
+
|
|
25
32
|
export type SnapshotEntry = AuthCredentialSnapshotEntry & {
|
|
26
33
|
rotatesInMs: number | null;
|
|
34
|
+
blocks?: CredentialBlockSnapshot[];
|
|
27
35
|
};
|
|
28
36
|
|
|
29
37
|
/** GET /v1/snapshot response body. */
|
|
@@ -54,6 +62,19 @@ export interface CredentialDisableResponse {
|
|
|
54
62
|
ok: boolean;
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
/** POST /v1/credential/:id/block request body. */
|
|
66
|
+
export type CredentialBlockRequest = CredentialBlockSnapshot;
|
|
67
|
+
|
|
68
|
+
/** POST /v1/credential/:id/block response body. */
|
|
69
|
+
export interface CredentialBlockResponse {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** DELETE /v1/credential/:id/blocks response body. */
|
|
74
|
+
export interface CredentialBlocksDeleteResponse {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
/**
|
|
58
79
|
* POST /v1/credential request body. The OAuth `refresh` must be the *real*
|
|
59
80
|
* refresh token (not the sentinel) — the broker is the canonical writer.
|
|
@@ -69,6 +69,13 @@ export const credentialSnapshotEntrySchema = type({
|
|
|
69
69
|
identityKey: "string | null",
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
export const credentialBlockSnapshotSchema = type({
|
|
73
|
+
"+": "reject",
|
|
74
|
+
providerKey: type("string").atLeastLength(1),
|
|
75
|
+
blockScope: "string",
|
|
76
|
+
blockedUntilMs: "number",
|
|
77
|
+
});
|
|
78
|
+
|
|
72
79
|
export const snapshotEntrySchema = type({
|
|
73
80
|
"+": "reject",
|
|
74
81
|
id: "number.integer",
|
|
@@ -76,6 +83,7 @@ export const snapshotEntrySchema = type({
|
|
|
76
83
|
credential: snapshotCredentialSchema,
|
|
77
84
|
identityKey: "string | null",
|
|
78
85
|
rotatesInMs: "number | null",
|
|
86
|
+
"blocks?": credentialBlockSnapshotSchema.array(),
|
|
79
87
|
});
|
|
80
88
|
|
|
81
89
|
export const refresherScheduleSchema = type({
|
|
@@ -235,6 +243,20 @@ export const credentialDisableResponseSchema = type({
|
|
|
235
243
|
ok: "boolean",
|
|
236
244
|
});
|
|
237
245
|
|
|
246
|
+
// ─── Credential blocks ──────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
export const credentialBlockRequestSchema = credentialBlockSnapshotSchema;
|
|
249
|
+
|
|
250
|
+
export const credentialBlockResponseSchema = type({
|
|
251
|
+
"+": "reject",
|
|
252
|
+
ok: "boolean",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
export const credentialBlocksDeleteResponseSchema = type({
|
|
256
|
+
"+": "reject",
|
|
257
|
+
ok: "boolean",
|
|
258
|
+
});
|
|
259
|
+
|
|
238
260
|
// ─── Upload ────────────────────────────────────────────────────────────────
|
|
239
261
|
|
|
240
262
|
export const credentialUploadRequestSchema = type({
|