@slashfi/agents-sdk 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,273 @@
1
+ import { describe, test, expect, afterEach } from "bun:test";
2
+ import { createKeyManager, type KeyStore, type StoredKey, type KeyManager } from "./key-manager";
3
+ import { jwtVerify, createLocalJWKSet } from "jose";
4
+
5
+ // In-memory KeyStore for testing (no DB needed)
6
+ function createMemoryKeyStore(): KeyStore & { keys: StoredKey[] } {
7
+ const keys: StoredKey[] = [];
8
+ return {
9
+ keys,
10
+ async loadKeys() {
11
+ return keys.filter((k) => k.status !== "revoked");
12
+ },
13
+ async insertKey(key: StoredKey) {
14
+ keys.push(key);
15
+ },
16
+ async deprecateAllActive() {
17
+ for (const k of keys) {
18
+ if (k.status === "active") k.status = "deprecated";
19
+ }
20
+ },
21
+ async cleanupExpired() {
22
+ const now = Date.now();
23
+ const before = keys.length;
24
+ const remaining = keys.filter((k) => k.expiresAt.getTime() > now);
25
+ keys.length = 0;
26
+ keys.push(...remaining);
27
+ return before - remaining.length;
28
+ },
29
+ // Transaction support: snapshot + rollback on error
30
+ async transaction<T>(fn: () => Promise<T>): Promise<T> {
31
+ const snapshot = keys.map((k) => ({ ...k }));
32
+ try {
33
+ return await fn();
34
+ } catch (err) {
35
+ // Rollback
36
+ keys.length = 0;
37
+ keys.push(...snapshot);
38
+ throw err;
39
+ }
40
+ },
41
+ };
42
+ }
43
+
44
+ describe("KeyManager", () => {
45
+ let km: KeyManager;
46
+
47
+ afterEach(() => {
48
+ km?.stop();
49
+ });
50
+
51
+ test("creates initial key on startup", async () => {
52
+ const store = createMemoryKeyStore();
53
+ km = await createKeyManager({
54
+ store,
55
+ issuer: "http://test:3000",
56
+ checkIntervalMs: 60_000,
57
+ });
58
+
59
+ const jwks = km.getJwks();
60
+ expect(jwks.keys).toHaveLength(1);
61
+ expect(jwks.keys[0].alg).toBe("ES256");
62
+ expect(jwks.keys[0].kid).toMatch(/^key-/);
63
+ // No private key exposed
64
+ expect(jwks.keys[0].d).toBeUndefined();
65
+ });
66
+
67
+ test("signs a valid JWT", async () => {
68
+ const store = createMemoryKeyStore();
69
+ km = await createKeyManager({
70
+ store,
71
+ issuer: "http://test:3000",
72
+ checkIntervalMs: 60_000,
73
+ });
74
+
75
+ const token = await km.signJwt({ sub: "test-service" });
76
+ const parts = token.split(".");
77
+ expect(parts).toHaveLength(3);
78
+
79
+ const payload = JSON.parse(
80
+ Buffer.from(parts[1], "base64url").toString()
81
+ );
82
+ expect(payload.sub).toBe("test-service");
83
+ expect(payload.iss).toBe("http://test:3000");
84
+ expect(payload.exp - payload.iat).toBe(300); // default 5 min TTL
85
+ });
86
+
87
+ test("token verifies against JWKS", async () => {
88
+ const store = createMemoryKeyStore();
89
+ km = await createKeyManager({
90
+ store,
91
+ issuer: "http://test:3000",
92
+ checkIntervalMs: 60_000,
93
+ });
94
+
95
+ const token = await km.signJwt({ sub: "verify-me" });
96
+ const jwks = km.getJwks();
97
+ const JWKS = createLocalJWKSet(jwks);
98
+ const { payload } = await jwtVerify(token, JWKS);
99
+ expect(payload.sub).toBe("verify-me");
100
+ });
101
+
102
+ test("custom token TTL", async () => {
103
+ const store = createMemoryKeyStore();
104
+ km = await createKeyManager({
105
+ store,
106
+ issuer: "http://test:3000",
107
+ tokenTtlSeconds: 60,
108
+ checkIntervalMs: 60_000,
109
+ });
110
+
111
+ const token = await km.signJwt({ sub: "short-lived" });
112
+ const payload = JSON.parse(
113
+ Buffer.from(token.split(".")[1], "base64url").toString()
114
+ );
115
+ expect(payload.exp - payload.iat).toBe(60);
116
+ });
117
+
118
+ // ---- Rotation tests ----
119
+
120
+ test("rotation: creates new key when threshold exceeded", async () => {
121
+ const store = createMemoryKeyStore();
122
+ km = await createKeyManager({
123
+ store,
124
+ issuer: "http://test:3000",
125
+ rotationThresholdMs: 0, // immediate rotation
126
+ checkIntervalMs: 60_000,
127
+ });
128
+
129
+ const initialKid = km.getJwks().keys.find(
130
+ (k) => store.keys.find((sk) => sk.kid === k.kid)?.status === "active"
131
+ )?.kid;
132
+
133
+ // Force rotation
134
+ await km.rotate();
135
+
136
+ const activeKeys = store.keys.filter((k) => k.status === "active");
137
+ expect(activeKeys).toHaveLength(1);
138
+ expect(activeKeys[0].kid).not.toBe(initialKid);
139
+
140
+ const deprecatedKeys = store.keys.filter((k) => k.status === "deprecated");
141
+ expect(deprecatedKeys.length).toBeGreaterThanOrEqual(1);
142
+ });
143
+
144
+ test("rotation: deprecated keys still in JWKS for verification", async () => {
145
+ const store = createMemoryKeyStore();
146
+ km = await createKeyManager({
147
+ store,
148
+ issuer: "http://test:3000",
149
+ rotationThresholdMs: 0,
150
+ checkIntervalMs: 60_000,
151
+ });
152
+
153
+ await km.rotate();
154
+
155
+ const jwksKids = km.getJwks().keys.map((k) => k.kid);
156
+ const nonRevoked = store.keys.filter((k) => k.status !== "revoked");
157
+ for (const key of nonRevoked) {
158
+ expect(jwksKids).toContain(key.kid);
159
+ }
160
+ });
161
+
162
+ test("rotation: pre-rotation tokens still verify", async () => {
163
+ const store = createMemoryKeyStore();
164
+ km = await createKeyManager({
165
+ store,
166
+ issuer: "http://test:3000",
167
+ rotationThresholdMs: 0,
168
+ checkIntervalMs: 60_000,
169
+ });
170
+
171
+ const preRotationToken = await km.signJwt({ sub: "pre-rotate" });
172
+ await km.rotate();
173
+
174
+ // Pre-rotation token should still verify (deprecated key still in JWKS)
175
+ const jwks = km.getJwks();
176
+ const JWKS = createLocalJWKSet(jwks);
177
+ const { payload } = await jwtVerify(preRotationToken, JWKS);
178
+ expect(payload.sub).toBe("pre-rotate");
179
+ });
180
+
181
+ test("rotation: exactly 1 active key", async () => {
182
+ const store = createMemoryKeyStore();
183
+ km = await createKeyManager({
184
+ store,
185
+ issuer: "http://test:3000",
186
+ rotationThresholdMs: 0,
187
+ checkIntervalMs: 60_000,
188
+ });
189
+
190
+ await km.rotate();
191
+ await km.rotate();
192
+
193
+ const activeKeys = store.keys.filter((k) => k.status === "active");
194
+ expect(activeKeys).toHaveLength(1);
195
+ });
196
+
197
+ test("rotation: tokens fail verification after key expires and is cleaned up", async () => {
198
+ const store = createMemoryKeyStore();
199
+ km = await createKeyManager({
200
+ store,
201
+ issuer: "http://test:3000",
202
+ rotationThresholdMs: 0,
203
+ keyLifetimeMs: 1, // 1ms — keys expire almost immediately
204
+ checkIntervalMs: 60_000,
205
+ });
206
+
207
+ const token = await km.signJwt({ sub: "will-expire" });
208
+
209
+ // Token should verify now (key is active)
210
+ const JWKS = createLocalJWKSet(km.getJwks());
211
+ const { payload } = await jwtVerify(token, JWKS);
212
+ expect(payload.sub).toBe("will-expire");
213
+
214
+ // Wait for key to expire, then rotate (which cleans up expired keys)
215
+ await new Promise((r) => setTimeout(r, 5));
216
+ await km.rotate();
217
+
218
+ // Old key should be gone from JWKS — verification should fail
219
+ const jwksAfter = km.getJwks();
220
+ const JWKS2 = createLocalJWKSet(jwksAfter);
221
+ let failed = false;
222
+ try {
223
+ await jwtVerify(token, JWKS2);
224
+ } catch {
225
+ failed = true;
226
+ }
227
+ expect(failed).toBe(true);
228
+ });
229
+
230
+ // ---- Transaction tests ----
231
+
232
+ test("transaction: rotate is atomic (rollback on failure)", async () => {
233
+ const store = createMemoryKeyStore();
234
+ km = await createKeyManager({
235
+ store,
236
+ issuer: "http://test:3000",
237
+ checkIntervalMs: 60_000,
238
+ });
239
+
240
+ const initialKid = store.keys[0].kid;
241
+
242
+ // Monkey-patch insertKey to fail mid-transaction
243
+ const origInsert = store.insertKey;
244
+ store.insertKey = async () => { throw new Error("simulated failure"); };
245
+
246
+ // Rotation should fail, but state should be rolled back
247
+ try { await km.rotate(); } catch {}
248
+
249
+ // Original key should still be active (not deprecated)
250
+ const active = store.keys.filter((k) => k.status === "active");
251
+ expect(active).toHaveLength(1);
252
+ expect(active[0].kid).toBe(initialKid);
253
+
254
+ // Restore
255
+ store.insertKey = origInsert;
256
+ });
257
+
258
+ // ---- enableRotation option ----
259
+
260
+ test("enableRotation: false skips background interval", async () => {
261
+ const store = createMemoryKeyStore();
262
+ km = await createKeyManager({
263
+ store,
264
+ issuer: "http://test:3000",
265
+ enableRotation: false,
266
+ });
267
+
268
+ // Should still have an initial key
269
+ expect(km.getJwks().keys).toHaveLength(1);
270
+ // But stop() should be a no-op (no interval to clear)
271
+ km.stop();
272
+ });
273
+ });
@@ -0,0 +1,257 @@
1
+ /**
2
+ * JWKS Key Manager
3
+ *
4
+ * Manages ES256 signing keys with automatic rotation and revocation.
5
+ * Store-agnostic — provide a KeyStore implementation for your DB.
6
+ *
7
+ * Features:
8
+ * - Automatic key rotation on a configurable schedule
9
+ * - Key lifecycle: active → deprecated → revoked → cleaned up
10
+ * - Multi-instance safe: checks DB before rotating (another instance may have already rotated)
11
+ * - Periodic background checks (configurable interval)
12
+ * - Exposes JWKS for /.well-known/jwks.json
13
+ * - Signs JWTs with the active key
14
+ */
15
+
16
+ import {
17
+ generateKeyPair,
18
+ exportJWK,
19
+ importJWK,
20
+ SignJWT,
21
+ type JWK,
22
+ } from "jose";
23
+
24
+ // ── Types ──
25
+
26
+ export type KeyStatus = "active" | "deprecated" | "revoked";
27
+
28
+ export interface StoredKey {
29
+ kid: string;
30
+ alg: string;
31
+ status: KeyStatus;
32
+ publicJwk: JWK;
33
+ privateJwk: JWK;
34
+ createdAt: Date;
35
+ expiresAt: Date;
36
+ }
37
+
38
+ interface CachedKey extends StoredKey {
39
+ privateKey: CryptoKey;
40
+ }
41
+
42
+ /**
43
+ * Pluggable store interface for key persistence.
44
+ * Implement this for your database (CockroachDB, Postgres, SQLite, etc.)
45
+ */
46
+ export interface KeyStore {
47
+ /** Load all non-revoked keys */
48
+ loadKeys(): Promise<StoredKey[]>;
49
+ /** Insert a new key */
50
+ insertKey(key: StoredKey): Promise<void>;
51
+ /** Set all active keys to deprecated */
52
+ deprecateAllActive(): Promise<void>;
53
+ /** Delete expired keys, return count deleted */
54
+ cleanupExpired(): Promise<number>;
55
+ /**
56
+ * Run operations atomically. Implementations with transaction support
57
+ * should wrap the callback in a DB transaction to ensure rotate() is
58
+ * atomic (load + deprecate + insert + cleanup all succeed or all fail).
59
+ * For stores without real tx support, pass a no-op wrapper that runs fn sequentially.
60
+ */
61
+ transaction<T>(fn: () => Promise<T>): Promise<T>;
62
+ }
63
+
64
+ export interface KeyManager {
65
+ /** Get the JWKS for /.well-known/jwks.json */
66
+ getJwks(): { keys: JWK[] };
67
+ /** Sign a JWT with the active key */
68
+ signJwt(claims: Record<string, unknown>): Promise<string>;
69
+ /** Force a key rotation */
70
+ rotate(): Promise<void>;
71
+ /** Stop the background rotation check */
72
+ stop(): void;
73
+ }
74
+
75
+ export interface KeyManagerOptions {
76
+ /** Key store implementation */
77
+ store: KeyStore;
78
+ /** Issuer URL for the iss claim */
79
+ issuer: string;
80
+ /** How often to check if rotation is needed (default: 5 min) */
81
+ checkIntervalMs?: number;
82
+ /** Max age of an active key before rotation (default: 1 hour) */
83
+ rotationThresholdMs?: number;
84
+ /** How long deprecated keys stay in JWKS for verification (default: 2 hours) */
85
+ keyLifetimeMs?: number;
86
+ /** Token TTL in seconds (default: 300 = 5 min) */
87
+ tokenTtlSeconds?: number;
88
+ /** Enable background key rotation (default: true). Set to false on read-only replicas. */
89
+ enableRotation?: boolean;
90
+ }
91
+
92
+ // ── Constants ──
93
+
94
+ const FIVE_MINUTES = 5 * 60 * 1000;
95
+ const ONE_HOUR = 60 * 60 * 1000;
96
+ const TWO_HOURS = 2 * ONE_HOUR;
97
+ const ALG = "ES256";
98
+
99
+ // ── Key generation ──
100
+
101
+ function generateKid(): string {
102
+ return `key-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
103
+ }
104
+
105
+ async function generateNewKey(keyLifetimeMs: number): Promise<StoredKey> {
106
+ const { privateKey, publicKey } = await generateKeyPair(ALG, { extractable: true });
107
+ const kid = generateKid();
108
+
109
+ const publicJwk = await exportJWK(publicKey);
110
+ publicJwk.kid = kid;
111
+ publicJwk.alg = ALG;
112
+ publicJwk.use = "sig";
113
+
114
+ const privateJwk = await exportJWK(privateKey);
115
+ privateJwk.kid = kid;
116
+ privateJwk.alg = ALG;
117
+
118
+ return {
119
+ kid,
120
+ alg: ALG,
121
+ status: "active",
122
+ publicJwk,
123
+ privateJwk,
124
+ createdAt: new Date(),
125
+ expiresAt: new Date(Date.now() + keyLifetimeMs),
126
+ };
127
+ }
128
+
129
+ async function toCachedKey(stored: StoredKey): Promise<CachedKey> {
130
+ const privateKey = await importJWK(stored.privateJwk, ALG) as CryptoKey;
131
+ return { ...stored, privateKey };
132
+ }
133
+
134
+ // ── Key Manager ──
135
+
136
+ export async function createKeyManager(opts: KeyManagerOptions): Promise<KeyManager> {
137
+ const {
138
+ store,
139
+ issuer,
140
+ checkIntervalMs = FIVE_MINUTES,
141
+ rotationThresholdMs = ONE_HOUR,
142
+ keyLifetimeMs = TWO_HOURS,
143
+ tokenTtlSeconds = 300,
144
+ enableRotation = true,
145
+ } = opts;
146
+
147
+ let keys: CachedKey[] = [];
148
+
149
+ /** Generate a new key, deprecate old ones, cleanup expired, refresh cache — all in one transaction */
150
+ async function rotate(): Promise<void> {
151
+ await store.transaction(async () => {
152
+ const newKey = await generateNewKey(keyLifetimeMs);
153
+ await store.deprecateAllActive();
154
+ await store.insertKey(newKey);
155
+ await store.cleanupExpired();
156
+ const updated = await store.loadKeys();
157
+ keys = await Promise.all(updated.map(toCachedKey));
158
+ });
159
+ }
160
+
161
+ /** Check if rotation is needed and rotate if so — all within a single transaction */
162
+ async function checkAndRotate(): Promise<void> {
163
+ // Quick check against cache — no DB/store hit if key is fresh
164
+ const cached = keys.find((k) => k.status === "active");
165
+ if (cached) {
166
+ const age = Date.now() - cached.createdAt.getTime();
167
+ if (age < rotationThresholdMs) return;
168
+ }
169
+
170
+ // Cache says stale (or empty) — take a lock via transaction to check + rotate atomically
171
+ await store.transaction(async () => {
172
+ const stored = await store.loadKeys();
173
+ const active = stored.find((k) => k.status === "active");
174
+
175
+ if (active) {
176
+ const age = Date.now() - active.createdAt.getTime();
177
+ if (age < rotationThresholdMs) {
178
+ // Another instance already rotated — just update our cache
179
+ keys = await Promise.all(stored.map(toCachedKey));
180
+ return;
181
+ }
182
+ }
183
+
184
+ // Still stale (or no active key) — rotate within this tx
185
+ const newKey = await generateNewKey(keyLifetimeMs);
186
+ await store.deprecateAllActive();
187
+ await store.insertKey(newKey);
188
+ await store.cleanupExpired();
189
+
190
+ // Refresh cache inside the tx for a consistent read
191
+ const updated = await store.loadKeys();
192
+ keys = await Promise.all(updated.map(toCachedKey));
193
+ });
194
+ }
195
+
196
+ // Initial load + ensure we have at least one key
197
+ // Initial load from store
198
+ const stored = await store.loadKeys();
199
+ keys = await Promise.all(stored.map(toCachedKey));
200
+ if (!keys.some((k) => k.status === "active")) {
201
+ if (enableRotation) {
202
+ await rotate();
203
+ } else {
204
+ // Read-only mode: generate a key in memory only (no store writes)
205
+ // This ensures signJwt works even without rotation enabled
206
+ const newKey = await generateNewKey(keyLifetimeMs);
207
+ keys.push(await toCachedKey(newKey));
208
+ }
209
+ } else if (enableRotation) {
210
+ await checkAndRotate();
211
+ }
212
+
213
+ // Periodic background check (only if rotation enabled)
214
+ const interval = enableRotation
215
+ ? setInterval(async () => {
216
+ try {
217
+ await checkAndRotate();
218
+ } catch (err) {
219
+ console.error("[key-manager] Check/rotation failed:", err);
220
+ }
221
+ }, checkIntervalMs)
222
+ : null;
223
+
224
+ function getActiveKey(): CachedKey {
225
+ const active = keys.find((k) => k.status === "active");
226
+ if (!active) throw new Error("[key-manager] No active signing key");
227
+ return active;
228
+ }
229
+
230
+ return {
231
+ getJwks(): { keys: JWK[] } {
232
+ return {
233
+ keys: keys
234
+ .filter((k) => k.status !== "revoked")
235
+ .map((k) => k.publicJwk),
236
+ };
237
+ },
238
+
239
+ async signJwt(claims: Record<string, unknown>): Promise<string> {
240
+ const key = getActiveKey();
241
+ return new SignJWT({ ...claims } as any)
242
+ .setProtectedHeader({ alg: ALG, kid: key.kid })
243
+ .setIssuer(issuer)
244
+ .setIssuedAt()
245
+ .setExpirationTime(`${tokenTtlSeconds}s`)
246
+ .sign(key.privateKey);
247
+ },
248
+
249
+ async rotate(): Promise<void> {
250
+ await rotate();
251
+ },
252
+
253
+ stop(): void {
254
+ if (interval) clearInterval(interval);
255
+ },
256
+ };
257
+ }
package/src/server.ts CHANGED
@@ -100,6 +100,8 @@ export interface AgentServerOptions {
100
100
  signingKey?: SigningKey;
101
101
  /** OAuth identity provider for cross-registry user linking */
102
102
  oauthIdentityProvider?: OAuthIdentityProvider;
103
+ /** Key store for managed key rotation (if provided, uses createKeyManager instead of simple key gen) */
104
+ keyStore?: import("./key-manager.js").KeyStore;
103
105
  }
104
106
 
105
107
  export interface AgentServer {
@@ -588,6 +590,14 @@ export function createAgentServer(
588
590
  // OAuth2 token handler
589
591
  // ──────────────────────────────────────────
590
592
 
593
+ // Resolve public-facing base URL, respecting reverse proxy headers
594
+ const resolveBaseUrl = (r: Request): string => {
595
+ const fwdProto = r.headers.get("x-forwarded-proto");
596
+ const fwdHost = r.headers.get("x-forwarded-host");
597
+ if (fwdProto && fwdHost) return `${fwdProto}://${fwdHost}`;
598
+ return new URL(r.url).origin;
599
+ };
600
+
591
601
  async function handleOAuthToken(req: Request): Promise<Response> {
592
602
  if (!authConfig) {
593
603
  return jsonResponse({ error: "auth_not_configured" }, 404);
@@ -638,27 +648,26 @@ export function createAgentServer(
638
648
  try {
639
649
  const assertionParts = assertion.split(".");
640
650
  if (assertionParts.length === 3) {
641
- const assertionPayload = JSON.parse(atob(assertionParts[1].replace(/-/g, "+").replace(/_/g, "/"))) as any;
651
+ const assertionPayload = JSON.parse(Buffer.from(assertionParts[1], "base64url").toString()) as any;
642
652
  if (assertionPayload.type === "agent-registry" && assertionPayload.iss) {
643
- // Find or create @remote-registry agent and store the reverse connection
644
- const rrAgent = registry.get("@remote-registry") ?? registry.get("/agents/@remote-registry");
645
- if (rrAgent) {
646
- const setupTool = (rrAgent as any).tools?.find((t: any) => t.name === "setup_integration");
647
- if (setupTool?.execute) {
648
- try {
649
- await setupTool.execute(
650
- { url: assertionPayload.iss, name: assertionPayload.name ?? "remote-registry" },
651
- { callerId: "system", callerType: "system", tenantId: "default", agentPath: "@remote-registry" },
652
- );
653
- console.error(`[jwt_exchange] Reverse connection stored for ${assertionPayload.iss}`);
654
- } catch (setupErr) {
655
- console.error(`[jwt_exchange] Reverse registration setup failed:`, setupErr);
656
- }
657
- } else {
658
- console.error("[jwt_exchange] @remote-registry has no setup_integration tool — reverse registration skipped");
659
- }
653
+ // Use add_connection (direct store) instead of setup_integration (which would cause infinite loop)
654
+ const addResult = await registry.call({
655
+ action: "execute_tool",
656
+ path: "@remote-registry",
657
+ tool: "add_connection",
658
+ params: {
659
+ id: assertionPayload.name ?? "remote-registry",
660
+ name: assertionPayload.name ?? "remote-registry",
661
+ url: assertionPayload.iss,
662
+ remoteTenantId: assertionPayload.tenantId ?? "default",
663
+ },
664
+ callerId: "system",
665
+ callerType: "system",
666
+ });
667
+ if (addResult.success) {
668
+ console.error(`[jwt_exchange] Reverse connection stored for ${assertionPayload.iss}`);
660
669
  } else {
661
- console.error("[jwt_exchange] @remote-registry agent not found reverse registration skipped");
670
+ console.error(`[jwt_exchange] Reverse connection failed:`, (addResult as any).error);
662
671
  }
663
672
  }
664
673
  }
@@ -675,7 +684,7 @@ export function createAgentServer(
675
684
 
676
685
  // User not linked yet — needs OAuth identity linking
677
686
  if (exchangeResult.needsAuth) {
678
- const baseUrl = new URL(req.url).origin;
687
+ const baseUrl = resolveBaseUrl(req);
679
688
  const authorizeUrl = new URL(`${baseUrl}${basePath}/oauth/authorize`);
680
689
  authorizeUrl.searchParams.set("token", assertion);
681
690
  if (params.redirect_uri) {
@@ -707,7 +716,7 @@ export function createAgentServer(
707
716
  },
708
717
  sigKey.privateKey,
709
718
  sigKey.kid,
710
- new URL(req.url).origin,
719
+ resolveBaseUrl(req),
711
720
  `${authConfig.tokenTtl ?? 3600}s`,
712
721
  );
713
722
 
@@ -789,6 +798,7 @@ export function createAgentServer(
789
798
 
790
799
  async function fetch(req: Request): Promise<Response> {
791
800
  try {
801
+
792
802
  const url = new URL(req.url);
793
803
  const path = url.pathname.replace(basePath, "") || "/";
794
804
 
@@ -856,17 +866,23 @@ export function createAgentServer(
856
866
  return cors ? addCors(res) : res;
857
867
  }
858
868
 
859
- // Verify the JWT against trusted issuers
869
+ // Verify the JWT against trusted issuers (from store, falling back to config)
860
870
  let claims: Record<string, unknown> | null = null;
861
- const issuerUrls = configTrustedIssuers.map(i => typeof i === "string" ? i : i.issuer);
862
- for (const issuerUrl of issuerUrls) {
871
+ const storeIssuers = authConfig?.store?.listTrustedIssuers
872
+ ? await authConfig.store.listTrustedIssuers()
873
+ : [];
874
+ const configIssuerUrls = configTrustedIssuers.map(i => typeof i === "string" ? i : i.issuer);
875
+ const allIssuerUrls = [...new Set([...storeIssuers, ...configIssuerUrls])];
876
+ console.log("[oauth/authorize] storeIssuers:", storeIssuers.length, "configIssuers:", configIssuerUrls.length, "total:", allIssuerUrls.length, "urls:", allIssuerUrls);
877
+ for (const issuerUrl of allIssuerUrls) {
863
878
  try {
864
879
  const result = await verifyJwtFromIssuer(token, issuerUrl);
880
+ console.log("[oauth/authorize] verify", issuerUrl, "->", result ? "OK" : "null");
865
881
  if (result) {
866
882
  claims = result as unknown as Record<string, unknown>;
867
883
  break;
868
884
  }
869
- } catch { /* try next issuer */ }
885
+ } catch (e: any) { console.log("[oauth/authorize] verify", issuerUrl, "-> ERROR:", e.message); }
870
886
  }
871
887
  if (!claims) {
872
888
  const res = jsonResponse(
@@ -876,7 +892,7 @@ export function createAgentServer(
876
892
  return cors ? addCors(res) : res;
877
893
  }
878
894
 
879
- const baseUrl = new URL(req.url).origin;
895
+ const baseUrl = resolveBaseUrl(req);
880
896
  const scope = url.searchParams.get("scope") ?? undefined;
881
897
  const res = await oauthIdentityProvider.authorize(req, {
882
898
  token,
@@ -897,7 +913,7 @@ export function createAgentServer(
897
913
  );
898
914
  return cors ? addCors(res) : res;
899
915
  }
900
- const baseUrl = new URL(req.url).origin;
916
+ const baseUrl = resolveBaseUrl(req);
901
917
  const res = await oauthIdentityProvider.callback(req, {
902
918
  baseUrl: baseUrl + basePath,
903
919
  });
@@ -921,7 +937,7 @@ export function createAgentServer(
921
937
 
922
938
  // ── GET /.well-known/configuration → Server discovery ──
923
939
  if (path === "/.well-known/configuration" && req.method === "GET") {
924
- const baseUrl = new URL(req.url).origin;
940
+ const baseUrl = resolveBaseUrl(req);
925
941
  const res = jsonResponse({
926
942
  issuer: baseUrl,
927
943
  jwks_uri: `${baseUrl}/.well-known/jwks.json`,