@frontmcp/auth 0.0.1 → 0.8.1

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 (81) hide show
  1. package/README.md +11 -0
  2. package/authorization/authorization.types.d.ts +236 -0
  3. package/authorization/authorization.types.d.ts.map +1 -0
  4. package/authorization/index.d.ts +9 -0
  5. package/authorization/index.d.ts.map +1 -0
  6. package/cimd/cimd-redis.cache.d.ts +111 -0
  7. package/cimd/cimd-redis.cache.d.ts.map +1 -0
  8. package/cimd/cimd.cache.d.ts +200 -0
  9. package/cimd/cimd.cache.d.ts.map +1 -0
  10. package/cimd/cimd.errors.d.ts +124 -0
  11. package/cimd/cimd.errors.d.ts.map +1 -0
  12. package/cimd/cimd.logger.d.ts +39 -0
  13. package/cimd/cimd.logger.d.ts.map +1 -0
  14. package/cimd/cimd.service.d.ts +88 -0
  15. package/cimd/cimd.service.d.ts.map +1 -0
  16. package/cimd/cimd.types.d.ts +178 -0
  17. package/cimd/cimd.types.d.ts.map +1 -0
  18. package/cimd/cimd.validator.d.ts +49 -0
  19. package/cimd/cimd.validator.d.ts.map +1 -0
  20. package/cimd/index.d.ts +17 -0
  21. package/cimd/index.d.ts.map +1 -0
  22. package/esm/index.mjs +4001 -0
  23. package/esm/package.json +59 -0
  24. package/index.d.ts +44 -0
  25. package/index.d.ts.map +1 -0
  26. package/index.js +4131 -0
  27. package/jwks/dev-key-persistence.d.ts +70 -0
  28. package/jwks/dev-key-persistence.d.ts.map +1 -0
  29. package/jwks/index.d.ts +20 -0
  30. package/jwks/index.d.ts.map +1 -0
  31. package/jwks/jwks.service.d.ts +69 -0
  32. package/jwks/jwks.service.d.ts.map +1 -0
  33. package/jwks/jwks.types.d.ts +33 -0
  34. package/jwks/jwks.types.d.ts.map +1 -0
  35. package/jwks/jwks.utils.d.ts +5 -0
  36. package/jwks/jwks.utils.d.ts.map +1 -0
  37. package/package.json +2 -2
  38. package/session/authorization-vault.d.ts +667 -0
  39. package/session/authorization-vault.d.ts.map +1 -0
  40. package/session/authorization.store.d.ts +311 -0
  41. package/session/authorization.store.d.ts.map +1 -0
  42. package/session/index.d.ts +19 -0
  43. package/session/index.d.ts.map +1 -0
  44. package/session/storage/in-memory-authorization-vault.d.ts +53 -0
  45. package/session/storage/in-memory-authorization-vault.d.ts.map +1 -0
  46. package/session/storage/index.d.ts +17 -0
  47. package/session/storage/index.d.ts.map +1 -0
  48. package/session/storage/storage-authorization-vault.d.ts +107 -0
  49. package/session/storage/storage-authorization-vault.d.ts.map +1 -0
  50. package/session/storage/storage-token-store.d.ts +92 -0
  51. package/session/storage/storage-token-store.d.ts.map +1 -0
  52. package/session/token.store.d.ts +39 -0
  53. package/session/token.store.d.ts.map +1 -0
  54. package/session/token.vault.d.ts +33 -0
  55. package/session/token.vault.d.ts.map +1 -0
  56. package/session/utils/index.d.ts +5 -0
  57. package/session/utils/index.d.ts.map +1 -0
  58. package/session/utils/tiny-ttl-cache.d.ts +20 -0
  59. package/session/utils/tiny-ttl-cache.d.ts.map +1 -0
  60. package/session/vault-encryption.d.ts +190 -0
  61. package/session/vault-encryption.d.ts.map +1 -0
  62. package/ui/base-layout.d.ts +170 -0
  63. package/ui/base-layout.d.ts.map +1 -0
  64. package/ui/index.d.ts +10 -0
  65. package/ui/index.d.ts.map +1 -0
  66. package/ui/templates.d.ts +134 -0
  67. package/ui/templates.d.ts.map +1 -0
  68. package/utils/audience.validator.d.ts +130 -0
  69. package/utils/audience.validator.d.ts.map +1 -0
  70. package/utils/index.d.ts +8 -0
  71. package/utils/index.d.ts.map +1 -0
  72. package/utils/www-authenticate.utils.d.ts +98 -0
  73. package/utils/www-authenticate.utils.d.ts.map +1 -0
  74. package/vault/auth-providers.types.d.ts +262 -0
  75. package/vault/auth-providers.types.d.ts.map +1 -0
  76. package/vault/credential-cache.d.ts +98 -0
  77. package/vault/credential-cache.d.ts.map +1 -0
  78. package/vault/credential-helpers.d.ts +14 -0
  79. package/vault/credential-helpers.d.ts.map +1 -0
  80. package/vault/index.d.ts +10 -0
  81. package/vault/index.d.ts.map +1 -0
package/esm/index.mjs ADDED
@@ -0,0 +1,4001 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+
12
+ // libs/auth/src/cimd/cimd.cache.ts
13
+ function extractCacheHeaders(headers) {
14
+ return {
15
+ "cache-control": headers.get("cache-control") ?? void 0,
16
+ expires: headers.get("expires") ?? void 0,
17
+ etag: headers.get("etag") ?? void 0,
18
+ "last-modified": headers.get("last-modified") ?? void 0,
19
+ age: headers.get("age") ?? void 0
20
+ };
21
+ }
22
+ function parseCacheHeaders(headers, config) {
23
+ let ttlMs = config.defaultTtlMs;
24
+ if (headers["cache-control"]) {
25
+ const cacheControl = parseCacheControlHeader(headers["cache-control"]);
26
+ if (cacheControl["no-store"] || cacheControl["no-cache"]) {
27
+ ttlMs = config.minTtlMs;
28
+ } else if (typeof cacheControl["max-age"] === "number") {
29
+ let maxAgeSecs = cacheControl["max-age"];
30
+ if (headers.age) {
31
+ const ageSeconds = parseInt(headers.age, 10);
32
+ if (!isNaN(ageSeconds)) {
33
+ maxAgeSecs = Math.max(0, maxAgeSecs - ageSeconds);
34
+ }
35
+ }
36
+ ttlMs = maxAgeSecs * 1e3;
37
+ }
38
+ if (typeof cacheControl["s-maxage"] === "number") {
39
+ let sMaxAgeSecs = cacheControl["s-maxage"];
40
+ if (headers.age) {
41
+ const ageSeconds = parseInt(headers.age, 10);
42
+ if (!isNaN(ageSeconds)) {
43
+ sMaxAgeSecs = Math.max(0, sMaxAgeSecs - ageSeconds);
44
+ }
45
+ }
46
+ ttlMs = sMaxAgeSecs * 1e3;
47
+ }
48
+ } else if (headers.expires) {
49
+ const expiresDate = new Date(headers.expires);
50
+ if (!isNaN(expiresDate.getTime())) {
51
+ ttlMs = Math.max(0, expiresDate.getTime() - Date.now());
52
+ }
53
+ }
54
+ ttlMs = Math.max(config.minTtlMs, Math.min(config.maxTtlMs, ttlMs));
55
+ return {
56
+ ttlMs,
57
+ etag: headers.etag,
58
+ lastModified: headers["last-modified"]
59
+ };
60
+ }
61
+ function parseCacheControlHeader(value) {
62
+ const result = {};
63
+ const directives = value.toLowerCase().split(",").map((d) => d.trim());
64
+ for (const directive of directives) {
65
+ const [key, val] = directive.split("=").map((s) => s.trim());
66
+ if (val !== void 0) {
67
+ const numVal = parseInt(val, 10);
68
+ if (!isNaN(numVal)) {
69
+ result[key] = numVal;
70
+ }
71
+ } else {
72
+ result[key] = true;
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+ var InMemoryCimdCache, CimdCache;
78
+ var init_cimd_cache = __esm({
79
+ "libs/auth/src/cimd/cimd.cache.ts"() {
80
+ "use strict";
81
+ InMemoryCimdCache = class {
82
+ cache = /* @__PURE__ */ new Map();
83
+ config;
84
+ constructor(config) {
85
+ this.config = {
86
+ defaultTtlMs: config?.defaultTtlMs ?? 36e5,
87
+ maxTtlMs: config?.maxTtlMs ?? 864e5,
88
+ minTtlMs: config?.minTtlMs ?? 6e4
89
+ };
90
+ }
91
+ /**
92
+ * Get a cached entry by client_id.
93
+ *
94
+ * @param clientId - The client_id URL
95
+ * @returns The cached entry if valid, or undefined
96
+ */
97
+ async get(clientId) {
98
+ const entry = this.cache.get(clientId);
99
+ if (!entry) {
100
+ return void 0;
101
+ }
102
+ if (entry.expiresAt < Date.now()) {
103
+ return void 0;
104
+ }
105
+ return entry;
106
+ }
107
+ /**
108
+ * Get a stale entry for conditional revalidation.
109
+ *
110
+ * @param clientId - The client_id URL
111
+ * @returns The stale entry (even if expired), or undefined if not cached
112
+ */
113
+ async getStale(clientId) {
114
+ return this.cache.get(clientId);
115
+ }
116
+ /**
117
+ * Store a document in the cache.
118
+ *
119
+ * @param clientId - The client_id URL
120
+ * @param document - The metadata document
121
+ * @param headers - HTTP response headers
122
+ */
123
+ async set(clientId, document, headers) {
124
+ const cacheHeaders = extractCacheHeaders(headers);
125
+ const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
126
+ const now = Date.now();
127
+ const entry = {
128
+ document,
129
+ expiresAt: now + ttlMs,
130
+ etag,
131
+ lastModified,
132
+ cachedAt: now
133
+ };
134
+ this.cache.set(clientId, entry);
135
+ }
136
+ /**
137
+ * Update an existing cache entry (after 304 Not Modified).
138
+ *
139
+ * @param clientId - The client_id URL
140
+ * @param headers - New HTTP headers with updated cache directives
141
+ */
142
+ async revalidate(clientId, headers) {
143
+ const existing = this.cache.get(clientId);
144
+ if (!existing) {
145
+ return false;
146
+ }
147
+ const cacheHeaders = extractCacheHeaders(headers);
148
+ const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
149
+ existing.expiresAt = Date.now() + ttlMs;
150
+ if (etag) existing.etag = etag;
151
+ if (lastModified) existing.lastModified = lastModified;
152
+ return true;
153
+ }
154
+ /**
155
+ * Delete a cache entry.
156
+ *
157
+ * @param clientId - The client_id URL
158
+ * @returns true if an entry was deleted
159
+ */
160
+ async delete(clientId) {
161
+ return this.cache.delete(clientId);
162
+ }
163
+ /**
164
+ * Get conditional request headers for a cached entry.
165
+ *
166
+ * @param clientId - The client_id URL
167
+ * @returns Headers for conditional request, or undefined if not cached
168
+ */
169
+ async getConditionalHeaders(clientId) {
170
+ const entry = this.cache.get(clientId);
171
+ if (!entry) {
172
+ return void 0;
173
+ }
174
+ const headers = {};
175
+ if (entry.etag) {
176
+ headers["If-None-Match"] = entry.etag;
177
+ }
178
+ if (entry.lastModified) {
179
+ headers["If-Modified-Since"] = entry.lastModified;
180
+ }
181
+ return Object.keys(headers).length > 0 ? headers : void 0;
182
+ }
183
+ /**
184
+ * Clear all cached entries.
185
+ */
186
+ async clear() {
187
+ this.cache.clear();
188
+ }
189
+ /**
190
+ * Get the number of cached entries.
191
+ */
192
+ async size() {
193
+ return this.cache.size;
194
+ }
195
+ /**
196
+ * Remove expired entries.
197
+ *
198
+ * @returns Number of entries removed
199
+ */
200
+ async cleanup() {
201
+ const now = Date.now();
202
+ let removed = 0;
203
+ for (const [clientId, entry] of this.cache) {
204
+ if (entry.expiresAt + this.config.maxTtlMs * 2 < now) {
205
+ this.cache.delete(clientId);
206
+ removed++;
207
+ }
208
+ }
209
+ return removed;
210
+ }
211
+ };
212
+ CimdCache = InMemoryCimdCache;
213
+ }
214
+ });
215
+
216
+ // libs/auth/src/jwks/jwks.service.ts
217
+ import { jwtVerify, createLocalJWKSet, decodeProtectedHeader } from "jose";
218
+ import { bytesToHex, randomBytes, rsaVerify, createKeyPersistence } from "@frontmcp/utils";
219
+
220
+ // libs/auth/src/jwks/jwks.utils.ts
221
+ function trimSlash(s) {
222
+ return (s ?? "").replace(/\/+$/, "");
223
+ }
224
+ function normalizeIssuer(u) {
225
+ return trimSlash(String(u ?? ""));
226
+ }
227
+ function decodeJwtPayloadSafe(token) {
228
+ if (!token) return void 0;
229
+ const parts = token.split(".");
230
+ if (parts.length < 2) return void 0;
231
+ try {
232
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
233
+ const json = typeof Buffer !== "undefined" ? Buffer.from(b64, "base64").toString("utf8") : (
234
+ // browser fallback
235
+ atob(b64)
236
+ );
237
+ const obj = JSON.parse(json);
238
+ return obj && typeof obj === "object" && !Array.isArray(obj) ? obj : void 0;
239
+ } catch {
240
+ return void 0;
241
+ }
242
+ }
243
+
244
+ // libs/auth/src/jwks/jwks.service.ts
245
+ var WEAK_KEY_WARNING = `
246
+ \u26A0\uFE0F SECURITY WARNING: OAuth provider is using an RSA key smaller than 2048 bits.
247
+ This is considered insecure and should be updated.
248
+ Please contact your OAuth provider to upgrade their signing keys.
249
+ Verification will proceed but with reduced security guarantees.
250
+ `;
251
+ var JwksService = class {
252
+ opts;
253
+ warnedProviders = /* @__PURE__ */ new Set();
254
+ // Orchestrator signing material
255
+ orchestratorKey;
256
+ // Provider JWKS cache (providerId -> jwks + fetchedAt)
257
+ providerJwks = /* @__PURE__ */ new Map();
258
+ // Track if key has been initialized (for async loading)
259
+ keyInitialized = false;
260
+ // Promise guard to prevent concurrent key generation
261
+ keyInitPromise;
262
+ // KeyPersistence instance for unified key storage
263
+ keyPersistence;
264
+ constructor(opts) {
265
+ this.opts = {
266
+ orchestratorAlg: opts?.orchestratorAlg ?? "RS256",
267
+ rotateDays: opts?.rotateDays ?? 30,
268
+ providerJwksTtlMs: opts?.providerJwksTtlMs ?? 6 * 60 * 60 * 1e3,
269
+ // 6h
270
+ networkTimeoutMs: opts?.networkTimeoutMs ?? 5e3,
271
+ // 5s
272
+ devKeyPersistence: opts?.devKeyPersistence
273
+ };
274
+ }
275
+ // ===========================================================================
276
+ // Key Persistence Helpers
277
+ // ===========================================================================
278
+ /**
279
+ * Check if key persistence should be enabled.
280
+ * Enabled in development by default, disabled in production unless forceEnable.
281
+ */
282
+ shouldEnablePersistence() {
283
+ const isProd = process.env["NODE_ENV"] === "production";
284
+ if (this.opts.devKeyPersistence?.forceEnable) return true;
285
+ return !isProd;
286
+ }
287
+ /**
288
+ * Get or create the KeyPersistence instance.
289
+ * Returns null if persistence is disabled.
290
+ */
291
+ async getKeyPersistence() {
292
+ if (!this.shouldEnablePersistence()) return null;
293
+ if (!this.keyPersistence) {
294
+ this.keyPersistence = await createKeyPersistence({
295
+ baseDir: ".frontmcp/keys"
296
+ });
297
+ }
298
+ return this.keyPersistence;
299
+ }
300
+ // ===========================================================================
301
+ // Public JWKS (what /.well-known/jwks.json serves)
302
+ // ===========================================================================
303
+ /** Gateway's public JWKS (publish at /.well-known/jwks.json when orchestrated). */
304
+ async getPublicJwks() {
305
+ return this.getOrchestratorJwks();
306
+ }
307
+ // ===========================================================================
308
+ // Scope-aware verification API
309
+ // ===========================================================================
310
+ /** Verify a token issued by the gateway itself (orchestrated mode). */
311
+ async verifyGatewayToken(token, expectedIssuer) {
312
+ try {
313
+ const payload = decodeJwtPayloadSafe(token);
314
+ if (!payload) {
315
+ return {
316
+ ok: false,
317
+ error: "invalid bearer token"
318
+ };
319
+ }
320
+ return {
321
+ ok: true,
322
+ issuer: expectedIssuer,
323
+ sub: payload["sub"],
324
+ payload,
325
+ header: decodeProtectedHeader(token)
326
+ };
327
+ } catch (err) {
328
+ const message = err instanceof Error ? err.message : "verification_failed";
329
+ return { ok: false, error: message };
330
+ }
331
+ }
332
+ /**
333
+ * Verify a token against candidate transparent providers.
334
+ * Ensures JWKS are available (cached/TTL/AS discovery) per provider.
335
+ */
336
+ async verifyTransparentToken(token, candidates) {
337
+ if (!candidates?.length) return { ok: false, error: "no_providers" };
338
+ let kid;
339
+ try {
340
+ const header = decodeProtectedHeader(token);
341
+ kid = typeof header?.kid === "string" ? header.kid : void 0;
342
+ } catch {
343
+ }
344
+ for (const p of candidates) {
345
+ let jwks;
346
+ try {
347
+ jwks = await this.getJwksForProvider(p);
348
+ if (!jwks?.keys?.length) continue;
349
+ const draftPayload = decodeJwtPayloadSafe(token);
350
+ const JWKS = createLocalJWKSet(jwks);
351
+ const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
352
+ issuer: [normalizeIssuer(p.issuerUrl)].concat(
353
+ draftPayload?.["iss"] ? [draftPayload["iss"]] : []
354
+ )
355
+ // used because current cloud gateway have invalid issuer
356
+ });
357
+ return {
358
+ ok: true,
359
+ issuer: payload?.iss,
360
+ sub: payload?.sub,
361
+ providerId: p.id,
362
+ header: protectedHeader,
363
+ payload
364
+ };
365
+ } catch (e) {
366
+ if (this.isWeakKeyError(e)) {
367
+ const fallbackJwks = jwks ?? await this.getJwksForProvider(p);
368
+ if (fallbackJwks?.keys?.length) {
369
+ const fallbackResult = await this.verifyWithWeakKey(token, fallbackJwks, p);
370
+ if (fallbackResult.ok) {
371
+ return fallbackResult;
372
+ }
373
+ }
374
+ }
375
+ console.log("failed to verify token for provider: ", p.id, e);
376
+ }
377
+ }
378
+ return { ok: false, error: `no_provider_verified${kid ? ` (kid=${kid})` : ""}` };
379
+ }
380
+ /**
381
+ * Check if the error is due to weak RSA key (< 2048 bits)
382
+ */
383
+ isWeakKeyError(error) {
384
+ const message = typeof error === "object" && error !== null && "message" in error && typeof error.message === "string" ? error.message : String(error);
385
+ return message.includes("modulusLength") && message.includes("2048");
386
+ }
387
+ /**
388
+ * Fallback verification for providers using RSA keys smaller than 2048 bits.
389
+ * Logs a security warning but allows verification to proceed.
390
+ */
391
+ async verifyWithWeakKey(token, jwks, provider) {
392
+ try {
393
+ const parts = token.split(".");
394
+ if (parts.length !== 3) {
395
+ return { ok: false, error: "invalid_token_format" };
396
+ }
397
+ const [headerB64, payloadB64, signatureB64] = parts;
398
+ const header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
399
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
400
+ const matchingKey = this.findMatchingKey(jwks, header);
401
+ if (!matchingKey) {
402
+ return { ok: false, error: "no_matching_key" };
403
+ }
404
+ const signatureInput = `${headerB64}.${payloadB64}`;
405
+ const signature = Buffer.from(signatureB64, "base64url");
406
+ const jwtAlg = typeof header.alg === "string" ? header.alg : void 0;
407
+ if (!jwtAlg) {
408
+ return { ok: false, error: "missing_alg" };
409
+ }
410
+ const isValid = rsaVerify(jwtAlg, Buffer.from(signatureInput), matchingKey, signature);
411
+ if (!isValid) {
412
+ return { ok: false, error: "signature_invalid" };
413
+ }
414
+ const payloadIssuerRaw = typeof payload.iss === "string" ? payload.iss : void 0;
415
+ if (!payloadIssuerRaw) {
416
+ return { ok: false, error: "issuer_mismatch" };
417
+ }
418
+ const trustedIssuers = /* @__PURE__ */ new Set([normalizeIssuer(provider.issuerUrl)]);
419
+ const payloadIssuer = normalizeIssuer(payloadIssuerRaw);
420
+ if (!trustedIssuers.has(payloadIssuer)) {
421
+ return { ok: false, error: "issuer_mismatch" };
422
+ }
423
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1e3)) {
424
+ return { ok: false, error: "token_expired" };
425
+ }
426
+ if (!this.warnedProviders.has(provider.id)) {
427
+ this.warnedProviders.add(provider.id);
428
+ console.warn(WEAK_KEY_WARNING);
429
+ console.warn(` Provider: ${provider.id} (${provider.issuerUrl})`);
430
+ }
431
+ return {
432
+ ok: true,
433
+ issuer: payload.iss,
434
+ sub: payload.sub,
435
+ providerId: provider.id,
436
+ header,
437
+ payload
438
+ };
439
+ } catch (err) {
440
+ const message = err instanceof Error ? err.message : "unknown";
441
+ return { ok: false, error: `weak_key_verification_failed: ${message}` };
442
+ }
443
+ }
444
+ /**
445
+ * Find a matching key from JWKS based on token header
446
+ */
447
+ findMatchingKey(jwks, header) {
448
+ if (header.kid) {
449
+ const byKid = jwks.keys.find((k) => k.kid === header.kid);
450
+ if (byKid) return byKid;
451
+ }
452
+ if (header.alg) {
453
+ const byAlg = jwks.keys.find((k) => k.alg === header.alg || k.kty === "RSA" && header.alg?.startsWith("RS"));
454
+ if (byAlg) return byAlg;
455
+ }
456
+ return jwks.keys.find((k) => k.kty === "RSA");
457
+ }
458
+ // ===========================================================================
459
+ // Provider JWKS (cache + preload + discovery)
460
+ // ===========================================================================
461
+ /** Directly set provider JWKS (e.g., inline keys from config). */
462
+ setProviderJwks(providerId, jwks) {
463
+ this.providerJwks.set(providerId, { jwks, fetchedAt: Date.now() });
464
+ }
465
+ /**
466
+ * Ensure JWKS for a provider:
467
+ * 1) inline jwks (if provided) → cache & return
468
+ * 2) cached & fresh (TTL) → return
469
+ * 3) explicit jwksUri → fetch, cache, return
470
+ * 4) discover jwks_uri via AS → fetch AS metadata, then jwks_uri, cache, return
471
+ */
472
+ async getJwksForProvider(ref) {
473
+ if (ref.jwks?.keys?.length) {
474
+ this.setProviderJwks(ref.id, ref.jwks);
475
+ return ref.jwks;
476
+ }
477
+ const cached = this.providerJwks.get(ref.id);
478
+ if (cached && Date.now() - cached.fetchedAt < this.opts.providerJwksTtlMs) {
479
+ return cached.jwks;
480
+ }
481
+ if (ref.jwksUri) {
482
+ const fromUri = await this.tryFetchJwks(ref.id, ref.jwksUri);
483
+ if (fromUri?.keys?.length) return fromUri;
484
+ }
485
+ const issuer = trimSlash(ref.issuerUrl);
486
+ const meta = await this.tryFetchAsMeta(`${issuer}/.well-known/oauth-authorization-server`);
487
+ const uri = meta && typeof meta === "object" && meta["jwks_uri"] ? String(meta["jwks_uri"]) : void 0;
488
+ if (uri) {
489
+ const fromMeta = await this.tryFetchJwks(ref.id, uri);
490
+ if (fromMeta?.keys?.length) return fromMeta;
491
+ }
492
+ return cached?.jwks;
493
+ }
494
+ // ===========================================================================
495
+ // Orchestrator keys (generation/rotation)
496
+ // ===========================================================================
497
+ /** Return the orchestrator public JWKS (generates/rotates as needed). */
498
+ async getOrchestratorJwks() {
499
+ await this.ensureOrchestratorKey();
500
+ return this.orchestratorKey.publicJwk;
501
+ }
502
+ /** Return private signing key + kid for issuing orchestrator tokens. */
503
+ async getOrchestratorSigningKey() {
504
+ await this.ensureOrchestratorKey();
505
+ return { kid: this.orchestratorKey.kid, key: this.orchestratorKey.privateKey, alg: this.opts.orchestratorAlg };
506
+ }
507
+ // ===========================================================================
508
+ // Internals (fetch, rotation, helpers)
509
+ // ===========================================================================
510
+ async tryFetchJwks(providerId, uri) {
511
+ try {
512
+ const jwks = await this.fetchJson(uri);
513
+ if (jwks?.keys?.length) {
514
+ this.setProviderJwks(providerId, jwks);
515
+ return jwks;
516
+ }
517
+ } catch {
518
+ }
519
+ return void 0;
520
+ }
521
+ async tryFetchAsMeta(url) {
522
+ try {
523
+ return await this.fetchJson(url);
524
+ } catch {
525
+ return void 0;
526
+ }
527
+ }
528
+ async fetchJson(url) {
529
+ const ctl = typeof AbortController !== "undefined" ? new AbortController() : void 0;
530
+ const timer = setTimeout(() => ctl?.abort(), this.opts.networkTimeoutMs);
531
+ try {
532
+ const res = await fetch(url, {
533
+ method: "GET",
534
+ headers: { accept: "application/json" },
535
+ signal: ctl?.signal
536
+ });
537
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
538
+ return await res.json();
539
+ } finally {
540
+ clearTimeout(timer);
541
+ }
542
+ }
543
+ async ensureOrchestratorKey() {
544
+ const now = Date.now();
545
+ const maxAge = this.opts.rotateDays * 24 * 60 * 60 * 1e3;
546
+ if (this.orchestratorKey && now - this.orchestratorKey.createdAt <= maxAge) {
547
+ return;
548
+ }
549
+ if (this.keyInitPromise) {
550
+ await this.keyInitPromise;
551
+ return;
552
+ }
553
+ this.keyInitPromise = this.initializeOrchestratorKey(now, maxAge);
554
+ try {
555
+ await this.keyInitPromise;
556
+ } finally {
557
+ this.keyInitPromise = void 0;
558
+ }
559
+ }
560
+ async initializeOrchestratorKey(now, maxAge) {
561
+ const persistence = await this.getKeyPersistence();
562
+ if (persistence && !this.keyInitialized) {
563
+ this.keyInitialized = true;
564
+ const loaded = await persistence.getAsymmetric("jwks-orchestrator");
565
+ if (loaded && now - loaded.createdAt <= maxAge) {
566
+ if (loaded.alg !== this.opts.orchestratorAlg) {
567
+ console.warn(
568
+ `[JwksService] Persisted key algorithm (${loaded.alg}) doesn't match config (${this.opts.orchestratorAlg}), generating new key`
569
+ );
570
+ } else {
571
+ try {
572
+ const { createPrivateKey } = __require("node:crypto");
573
+ const privateKey = createPrivateKey({
574
+ key: loaded.privateKey,
575
+ format: "jwk"
576
+ });
577
+ this.orchestratorKey = {
578
+ kid: loaded.kid,
579
+ privateKey,
580
+ publicJwk: loaded.publicJwk,
581
+ createdAt: loaded.createdAt
582
+ };
583
+ return;
584
+ } catch (error) {
585
+ console.warn(`[JwksService] Failed to load persisted key: ${error.message}, generating new key`);
586
+ }
587
+ }
588
+ }
589
+ }
590
+ this.orchestratorKey = this.generateKey(this.opts.orchestratorAlg);
591
+ this.keyInitialized = true;
592
+ if (persistence) {
593
+ try {
594
+ await persistence.set({
595
+ type: "asymmetric",
596
+ kid: this.orchestratorKey.kid,
597
+ alg: this.opts.orchestratorAlg,
598
+ privateKey: this.orchestratorKey.privateKey.export({ format: "jwk" }),
599
+ publicJwk: this.orchestratorKey.publicJwk,
600
+ createdAt: this.orchestratorKey.createdAt,
601
+ version: 1
602
+ });
603
+ } catch (error) {
604
+ console.warn(`[JwksService] Failed to persist dev key: ${error.message}`);
605
+ }
606
+ }
607
+ }
608
+ generateKey(alg) {
609
+ const { generateKeyPairSync } = __require("node:crypto");
610
+ if (alg === "RS256") {
611
+ const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
612
+ const kid = bytesToHex(randomBytes(8));
613
+ const publicJwk = publicKey.export({ format: "jwk" });
614
+ Object.assign(publicJwk, { kid, alg: "RS256", use: "sig", kty: "RSA" });
615
+ return { kid, privateKey, publicJwk: { keys: [publicJwk] }, createdAt: Date.now() };
616
+ } else {
617
+ const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
618
+ const kid = bytesToHex(randomBytes(8));
619
+ const publicJwk = publicKey.export({ format: "jwk" });
620
+ Object.assign(publicJwk, { kid, alg: "ES256", use: "sig", kty: "EC" });
621
+ return { kid, privateKey, publicJwk: { keys: [publicJwk] }, createdAt: Date.now() };
622
+ }
623
+ }
624
+ };
625
+
626
+ // libs/auth/src/jwks/dev-key-persistence.ts
627
+ import * as path from "path";
628
+ import * as crypto from "crypto";
629
+ import { z } from "zod";
630
+ import { readFile, mkdir, writeFile, rename, unlink } from "@frontmcp/utils";
631
+ var DEFAULT_KEY_PATH = ".frontmcp/dev-keys.json";
632
+ var rsaPrivateKeySchema = z.object({
633
+ kty: z.literal("RSA"),
634
+ n: z.string().min(1),
635
+ e: z.string().min(1),
636
+ d: z.string().min(1),
637
+ p: z.string().optional(),
638
+ q: z.string().optional(),
639
+ dp: z.string().optional(),
640
+ dq: z.string().optional(),
641
+ qi: z.string().optional()
642
+ }).passthrough();
643
+ var ecPrivateKeySchema = z.object({
644
+ kty: z.literal("EC"),
645
+ crv: z.string().min(1),
646
+ x: z.string().min(1),
647
+ y: z.string().min(1),
648
+ d: z.string().min(1)
649
+ }).passthrough();
650
+ var publicJwkSchema = z.object({
651
+ kty: z.enum(["RSA", "EC"]),
652
+ kid: z.string().min(1),
653
+ alg: z.enum(["RS256", "ES256"]),
654
+ use: z.literal("sig")
655
+ }).passthrough();
656
+ var jwksSchema = z.object({
657
+ keys: z.array(publicJwkSchema).min(1)
658
+ });
659
+ var devKeyDataSchema = z.object({
660
+ kid: z.string().min(1),
661
+ privateKey: z.union([rsaPrivateKeySchema, ecPrivateKeySchema]),
662
+ publicJwk: jwksSchema,
663
+ createdAt: z.number().positive().int(),
664
+ alg: z.enum(["RS256", "ES256"])
665
+ });
666
+ function validateJwkStructure(data) {
667
+ const result = devKeyDataSchema.safeParse(data);
668
+ if (!result.success) {
669
+ return { valid: false, error: result.error.issues[0]?.message ?? "Invalid JWK structure" };
670
+ }
671
+ const parsed = result.data;
672
+ if (parsed.alg === "RS256" && parsed.privateKey.kty !== "RSA") {
673
+ return { valid: false, error: "Algorithm RS256 requires RSA key type" };
674
+ }
675
+ if (parsed.alg === "ES256" && parsed.privateKey.kty !== "EC") {
676
+ return { valid: false, error: "Algorithm ES256 requires EC key type" };
677
+ }
678
+ const publicKey = parsed.publicJwk.keys[0];
679
+ if (publicKey.kty !== parsed.privateKey.kty) {
680
+ return { valid: false, error: "Public and private key types do not match" };
681
+ }
682
+ if (publicKey.kid !== parsed.kid) {
683
+ return { valid: false, error: "kid mismatch between top-level and publicJwk" };
684
+ }
685
+ const now = Date.now();
686
+ const hundredYearsMs = 100 * 365 * 24 * 60 * 60 * 1e3;
687
+ if (parsed.createdAt > now) {
688
+ return { valid: false, error: "createdAt is in the future" };
689
+ }
690
+ if (parsed.createdAt < now - hundredYearsMs) {
691
+ return { valid: false, error: "createdAt is too old" };
692
+ }
693
+ return { valid: true };
694
+ }
695
+ function isDevKeyPersistenceEnabled(options) {
696
+ const isProduction = process.env["NODE_ENV"] === "production";
697
+ if (isProduction) {
698
+ return options?.forceEnable === true;
699
+ }
700
+ return true;
701
+ }
702
+ function resolveKeyPath(options) {
703
+ const keyPath = options?.keyPath ?? DEFAULT_KEY_PATH;
704
+ if (path.isAbsolute(keyPath)) {
705
+ return keyPath;
706
+ }
707
+ return path.resolve(process.cwd(), keyPath);
708
+ }
709
+ async function loadDevKey(options) {
710
+ if (!isDevKeyPersistenceEnabled(options)) {
711
+ return null;
712
+ }
713
+ const keyPath = resolveKeyPath(options);
714
+ try {
715
+ const content = await readFile(keyPath, "utf8");
716
+ const data = JSON.parse(content);
717
+ const validation = validateJwkStructure(data);
718
+ if (!validation.valid) {
719
+ console.warn(`[DevKeyPersistence] Invalid key file format at ${keyPath}: ${validation.error}, will regenerate`);
720
+ return null;
721
+ }
722
+ console.log(`[DevKeyPersistence] Loaded key (kid=${data.kid}) from ${keyPath}`);
723
+ return data;
724
+ } catch (error) {
725
+ if (error.code === "ENOENT") {
726
+ return null;
727
+ }
728
+ console.warn(`[DevKeyPersistence] Failed to load key from ${keyPath}: ${error.message}`);
729
+ return null;
730
+ }
731
+ }
732
+ async function saveDevKey(keyData, options) {
733
+ if (!isDevKeyPersistenceEnabled(options)) {
734
+ return true;
735
+ }
736
+ const keyPath = resolveKeyPath(options);
737
+ const dir = path.dirname(keyPath);
738
+ const tempPath = `${keyPath}.tmp.${Date.now()}.${crypto.randomBytes(8).toString("hex")}`;
739
+ try {
740
+ await mkdir(dir, { recursive: true, mode: 448 });
741
+ const content = JSON.stringify(keyData, null, 2);
742
+ await writeFile(tempPath, content, { mode: 384 });
743
+ await rename(tempPath, keyPath);
744
+ console.log(`[DevKeyPersistence] Saved key (kid=${keyData.kid}) to ${keyPath}`);
745
+ return true;
746
+ } catch (error) {
747
+ console.error(`[DevKeyPersistence] Failed to save key to ${keyPath}: ${error.message}`);
748
+ try {
749
+ await unlink(tempPath);
750
+ } catch {
751
+ }
752
+ return false;
753
+ }
754
+ }
755
+ async function deleteDevKey(options) {
756
+ const keyPath = resolveKeyPath(options);
757
+ try {
758
+ await unlink(keyPath);
759
+ console.log(`[DevKeyPersistence] Deleted key at ${keyPath}`);
760
+ } catch (error) {
761
+ if (error.code !== "ENOENT") {
762
+ console.warn(`[DevKeyPersistence] Failed to delete key at ${keyPath}: ${error.message}`);
763
+ }
764
+ }
765
+ }
766
+
767
+ // libs/auth/src/ui/base-layout.ts
768
+ import { escapeHtml } from "@frontmcp/utils";
769
+ import { escapeHtml as escapeHtml2 } from "@frontmcp/utils";
770
+ var CDN = {
771
+ /** Tailwind CSS v4 Browser CDN - generates styles on-the-fly with @theme support */
772
+ tailwind: "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4",
773
+ /** Google Fonts - Inter for modern UI */
774
+ fonts: {
775
+ preconnect: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
776
+ stylesheet: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
777
+ }
778
+ };
779
+ var DEFAULT_THEME = {
780
+ colors: {
781
+ primary: "#3b82f6",
782
+ // blue-500
783
+ "primary-dark": "#2563eb",
784
+ // blue-600
785
+ secondary: "#8b5cf6",
786
+ // violet-500
787
+ accent: "#06b6d4",
788
+ // cyan-500
789
+ success: "#22c55e",
790
+ // green-500
791
+ warning: "#f59e0b",
792
+ // amber-500
793
+ danger: "#ef4444"
794
+ // red-500
795
+ },
796
+ fonts: {
797
+ sans: 'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
798
+ }
799
+ };
800
+ function buildThemeCss(theme) {
801
+ const lines = [];
802
+ if (theme.colors) {
803
+ for (const [key, value] of Object.entries(theme.colors)) {
804
+ if (value) {
805
+ lines.push(`--color-${key}: ${value};`);
806
+ }
807
+ }
808
+ }
809
+ if (theme.fonts) {
810
+ for (const [key, value] of Object.entries(theme.fonts)) {
811
+ if (value) {
812
+ lines.push(`--font-${key}: ${value};`);
813
+ }
814
+ }
815
+ }
816
+ if (theme.customVars) {
817
+ lines.push(theme.customVars);
818
+ }
819
+ return lines.join("\n ");
820
+ }
821
+ function baseLayout(content, options) {
822
+ const {
823
+ title,
824
+ description,
825
+ includeTailwind = true,
826
+ includeFonts = true,
827
+ headExtra = "",
828
+ bodyClass = "bg-gray-50 min-h-screen font-sans antialiased",
829
+ theme
830
+ } = options;
831
+ const mergedTheme = {
832
+ colors: { ...DEFAULT_THEME.colors, ...theme?.colors },
833
+ fonts: { ...DEFAULT_THEME.fonts, ...theme?.fonts },
834
+ customVars: theme?.customVars,
835
+ customCss: theme?.customCss
836
+ };
837
+ const fontPreconnect = includeFonts ? CDN.fonts.preconnect.map((url, i) => `<link rel="preconnect" href="${url}"${i > 0 ? " crossorigin" : ""}>`).join("\n ") : "";
838
+ const fontStylesheet = includeFonts ? `<link href="${CDN.fonts.stylesheet}" rel="stylesheet">` : "";
839
+ const themeCss = buildThemeCss(mergedTheme);
840
+ const customCss = mergedTheme.customCss || "";
841
+ const tailwindBlock = includeTailwind ? `<script src="${CDN.tailwind}"></script>
842
+ <style type="text/tailwindcss">
843
+ @theme {
844
+ ${themeCss}
845
+ }
846
+ ${customCss}
847
+ </style>` : "";
848
+ const metaDescription = description ? `<meta name="description" content="${escapeHtml(description)}">` : "";
849
+ return `<!DOCTYPE html>
850
+ <html lang="en">
851
+ <head>
852
+ <meta charset="UTF-8">
853
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
854
+ <title>${escapeHtml(title)} - FrontMCP</title>
855
+ ${metaDescription}
856
+
857
+ <!-- Google Fonts CDN - Inter (modern UI font) -->
858
+ ${fontPreconnect}
859
+ ${fontStylesheet}
860
+
861
+ <!-- Tailwind CSS v4 Browser CDN with @theme support -->
862
+ ${tailwindBlock}
863
+ ${headExtra}
864
+ </head>
865
+ <body class="${escapeHtml(bodyClass)}">
866
+ ${content}
867
+ </body>
868
+ </html>`;
869
+ }
870
+ function createLayout(defaultOptions) {
871
+ return (content, options) => {
872
+ const mergedTheme = defaultOptions.theme || options.theme ? {
873
+ colors: { ...defaultOptions.theme?.colors, ...options.theme?.colors },
874
+ fonts: { ...defaultOptions.theme?.fonts, ...options.theme?.fonts },
875
+ customVars: options.theme?.customVars ?? defaultOptions.theme?.customVars,
876
+ customCss: options.theme?.customCss ?? defaultOptions.theme?.customCss
877
+ } : void 0;
878
+ return baseLayout(content, {
879
+ ...defaultOptions,
880
+ ...options,
881
+ theme: mergedTheme
882
+ });
883
+ };
884
+ }
885
+ var authLayout = createLayout({
886
+ bodyClass: "bg-gray-50 min-h-screen font-sans antialiased"
887
+ });
888
+ function centeredCardLayout(content, options) {
889
+ const wrappedContent = `
890
+ <div class="min-h-screen flex items-center justify-center p-4">
891
+ <div class="w-full max-w-md">
892
+ ${content}
893
+ </div>
894
+ </div>`;
895
+ return baseLayout(wrappedContent, {
896
+ ...options,
897
+ bodyClass: "bg-gradient-to-br from-primary to-secondary min-h-screen font-sans antialiased"
898
+ });
899
+ }
900
+ function wideLayout(content, options) {
901
+ const wrappedContent = `
902
+ <div class="min-h-screen py-8 px-4">
903
+ <div class="max-w-2xl mx-auto">
904
+ ${content}
905
+ </div>
906
+ </div>`;
907
+ return baseLayout(wrappedContent, options);
908
+ }
909
+ function extraWideLayout(content, options) {
910
+ const wrappedContent = `
911
+ <div class="min-h-screen py-8 px-4">
912
+ <div class="max-w-3xl mx-auto">
913
+ ${content}
914
+ </div>
915
+ </div>`;
916
+ return baseLayout(wrappedContent, options);
917
+ }
918
+
919
+ // libs/auth/src/ui/templates.ts
920
+ var escapeHtml3 = escapeHtml2;
921
+ function buildConsentPage(params) {
922
+ const { apps, clientName, pendingAuthId, csrfToken, callbackPath } = params;
923
+ const appCards = apps.map((app) => buildAppCardHtml(app, pendingAuthId, csrfToken, callbackPath)).join("\n");
924
+ const content = `
925
+ <h1 class="text-3xl font-bold text-gray-900 mb-4">Authorize ${escapeHtml3(clientName)}</h1>
926
+ <p class="text-gray-600 mb-8">
927
+ Select which apps you want to authorize. You can skip apps and authorize them later when needed.
928
+ </p>
929
+
930
+ <div class="space-y-4" id="app-list">
931
+ ${appCards}
932
+ </div>
933
+
934
+ <div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
935
+ Skipped apps can be authorized later when you try to use their tools (progressive authorization).
936
+ </div>`;
937
+ return wideLayout(content, { title: `Authorize ${clientName}` });
938
+ }
939
+ function buildAppCardHtml(app, pendingAuthId, csrfToken, callbackPath) {
940
+ const icon = app.iconUrl ? `<img src="${escapeHtml3(app.iconUrl)}" alt="${escapeHtml3(
941
+ app.appName
942
+ )}" class="w-12 h-12 rounded-lg object-cover">` : `<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg">${escapeHtml3(
943
+ app.appName.charAt(0).toUpperCase()
944
+ )}</div>`;
945
+ const description = app.description ? `<p class="text-sm text-gray-500">${escapeHtml3(app.description)}</p>` : "";
946
+ const scopes = app.requiredScopes?.length ? `<div class="mb-4">
947
+ <p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">Permissions</p>
948
+ <div class="flex flex-wrap gap-2">
949
+ ${app.requiredScopes.map(
950
+ (scope) => `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">${escapeHtml3(
951
+ scope
952
+ )}</span>`
953
+ ).join("")}
954
+ </div>
955
+ </div>` : "";
956
+ return `<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow" data-app-id="${escapeHtml3(
957
+ app.appId
958
+ )}">
959
+ <div class="flex items-center gap-4 mb-4">
960
+ ${icon}
961
+ <div class="flex-1">
962
+ <h3 class="font-semibold text-gray-900">${escapeHtml3(app.appName)}</h3>
963
+ ${description}
964
+ </div>
965
+ </div>
966
+
967
+ ${scopes}
968
+
969
+ <form method="POST" action="${escapeHtml3(callbackPath)}" class="flex gap-3 pt-4 border-t border-gray-100">
970
+ <input type="hidden" name="csrf" value="${escapeHtml3(csrfToken)}">
971
+ <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
972
+ <input type="hidden" name="app" value="${escapeHtml3(app.appId)}">
973
+ <button type="submit" name="action" value="authorize"
974
+ class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors">
975
+ Authorize
976
+ </button>
977
+ <button type="submit" name="action" value="skip"
978
+ class="px-4 py-2.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 font-medium rounded-lg transition-colors">
979
+ Skip
980
+ </button>
981
+ </form>
982
+ </div>`;
983
+ }
984
+ function buildIncrementalAuthPage(params) {
985
+ const { app, toolId, sessionHint, callbackPath } = params;
986
+ const description = app.description ? `<p class="text-gray-500 mt-2">${escapeHtml3(app.description)}</p>` : "";
987
+ const content = `
988
+ <!-- Warning icon -->
989
+ <div class="flex justify-center mb-6">
990
+ <div class="w-16 h-16 rounded-full bg-amber-100 flex items-center justify-center">
991
+ <svg class="w-8 h-8 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
992
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
993
+ </svg>
994
+ </div>
995
+ </div>
996
+
997
+ <h1 class="text-2xl font-bold text-gray-900 text-center mb-2">Authorization Required</h1>
998
+ <p class="text-gray-600 text-center mb-8">
999
+ To use "<span class="font-mono text-sm bg-gray-100 px-1 rounded">${escapeHtml3(
1000
+ toolId
1001
+ )}</span>", you need to authorize ${escapeHtml3(app.appName)}.
1002
+ </p>
1003
+
1004
+ <!-- App card -->
1005
+ <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
1006
+ <div class="flex items-center gap-4 mb-4">
1007
+ <div class="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg">
1008
+ ${escapeHtml3(app.appName.charAt(0).toUpperCase())}
1009
+ </div>
1010
+ <div class="flex-1">
1011
+ <h3 class="font-semibold text-gray-900">${escapeHtml3(app.appName)}</h3>
1012
+ ${description}
1013
+ </div>
1014
+ </div>
1015
+
1016
+ <form method="GET" action="${escapeHtml3(callbackPath)}" class="flex gap-3 pt-4 border-t border-gray-100">
1017
+ <input type="hidden" name="pending_auth_id" value="${escapeHtml3(sessionHint)}">
1018
+ <input type="hidden" name="app_id" value="${escapeHtml3(app.appId)}">
1019
+ <input type="hidden" name="incremental" value="true">
1020
+ <button type="button" onclick="window.close()"
1021
+ class="flex-1 px-4 py-2.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 font-medium rounded-lg transition-colors">
1022
+ Cancel
1023
+ </button>
1024
+ <button type="submit"
1025
+ class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors">
1026
+ Authorize
1027
+ </button>
1028
+ </form>
1029
+ </div>
1030
+
1031
+ <div class="p-4 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800 text-center">
1032
+ This is an incremental authorization. Your existing session will be expanded to include ${escapeHtml3(
1033
+ app.appName
1034
+ )}.
1035
+ </div>`;
1036
+ return centeredCardLayout(content, { title: `Authorize ${app.appName}` });
1037
+ }
1038
+ function buildFederatedLoginPage(params) {
1039
+ const { providers, clientName, pendingAuthId, callbackPath } = params;
1040
+ const providerCards = providers.map((provider) => {
1041
+ const isPrimaryBadge = provider.isPrimary ? `<span class="px-2 py-0.5 text-xs font-medium bg-blue-600 text-white rounded-full">Primary</span>` : "";
1042
+ const providerUrl = provider.providerUrl ? `<p class="text-xs text-gray-400 font-mono truncate">${escapeHtml3(provider.providerUrl)}</p>` : "";
1043
+ const appIds = provider.appIds.length > 0 ? `<p class="text-xs text-gray-500 mt-1">Apps: ${provider.appIds.map((id) => escapeHtml3(id)).join(", ")}</p>` : "";
1044
+ return `<label class="flex items-start gap-4 p-4 bg-white border-2 border-gray-200 rounded-xl cursor-pointer hover:border-blue-300 transition-colors has-[:checked]:border-blue-500 has-[:checked]:bg-blue-50">
1045
+ <input type="checkbox" name="providers" value="${escapeHtml3(provider.providerId)}"
1046
+ class="mt-1 w-5 h-5 rounded border-gray-300" ${provider.isPrimary ? "checked" : ""}>
1047
+ <div class="flex-1">
1048
+ <div class="flex items-center gap-2 mb-1">
1049
+ <span class="font-semibold text-gray-900">${escapeHtml3(provider.providerName)}</span>
1050
+ ${isPrimaryBadge}
1051
+ </div>
1052
+ <p class="text-sm text-gray-500">Mode: ${escapeHtml3(provider.mode)}</p>
1053
+ ${providerUrl}
1054
+ ${appIds}
1055
+ </div>
1056
+ </label>`;
1057
+ }).join("\n");
1058
+ const content = `
1059
+ <h1 class="text-3xl font-bold text-gray-900 mb-4">Select Authorization Providers</h1>
1060
+ <p class="text-gray-600 mb-8">
1061
+ ${escapeHtml3(clientName)} uses multiple authentication providers. Select which ones you want to authorize.
1062
+ </p>
1063
+
1064
+ <form method="GET" action="${escapeHtml3(callbackPath)}" id="federated-form">
1065
+ <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1066
+ <input type="hidden" name="federated" value="true">
1067
+
1068
+ <!-- Select all toggle -->
1069
+ <label class="flex items-center gap-3 mb-6 cursor-pointer">
1070
+ <input type="checkbox" id="select-all" class="w-5 h-5 rounded border-gray-300"
1071
+ onchange="document.querySelectorAll('input[name=providers]').forEach(cb => cb.checked = this.checked)">
1072
+ <span class="text-gray-700">Select all providers</span>
1073
+ </label>
1074
+
1075
+ <!-- Provider cards -->
1076
+ <div class="space-y-4 mb-8">
1077
+ ${providerCards}
1078
+ </div>
1079
+
1080
+ <!-- Email input -->
1081
+ <div class="mb-6">
1082
+ <label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
1083
+ <input type="email" id="email" name="email" required placeholder="you@example.com"
1084
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
1085
+ </div>
1086
+
1087
+ <!-- Buttons -->
1088
+ <div class="flex gap-4">
1089
+ <button type="button"
1090
+ class="flex-1 px-6 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 font-medium rounded-lg transition-colors"
1091
+ onclick="document.querySelectorAll('input[name=providers]').forEach(cb => cb.checked = false); document.getElementById('federated-form').submit();">
1092
+ Skip All
1093
+ </button>
1094
+ <button type="submit"
1095
+ class="flex-1 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
1096
+ Continue
1097
+ </button>
1098
+ </div>
1099
+ </form>
1100
+
1101
+ <div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
1102
+ Skipped providers can be authorized later when you try to use their tools (progressive authorization).
1103
+ </div>`;
1104
+ return wideLayout(content, { title: "Select Providers" });
1105
+ }
1106
+ function buildToolConsentPage(params) {
1107
+ const { tools, clientName, pendingAuthId, csrfToken, callbackPath, userName, userEmail } = params;
1108
+ const toolsByApp = {};
1109
+ for (const tool of tools) {
1110
+ if (!toolsByApp[tool.appId]) {
1111
+ toolsByApp[tool.appId] = { appName: tool.appName, tools: [] };
1112
+ }
1113
+ toolsByApp[tool.appId].tools.push(tool);
1114
+ }
1115
+ const userInfo = userName || userEmail ? `<div class="p-3 bg-gray-50 rounded-lg mb-6 text-sm">
1116
+ <span class="text-gray-500">Signed in as: </span>
1117
+ <span class="font-medium text-gray-900">${escapeHtml3(userName || userEmail || "")}</span>
1118
+ </div>` : "";
1119
+ const appGroups = Object.entries(toolsByApp).map(([appId, { appName, tools: appTools }]) => {
1120
+ const toolItems = appTools.map((tool) => {
1121
+ const desc = tool.description ? `<p class="text-sm text-gray-500 mt-0.5">${escapeHtml3(tool.description)}</p>` : "";
1122
+ return `<label class="flex items-start gap-3 p-3 bg-white rounded-lg cursor-pointer hover:bg-gray-50">
1123
+ <input type="checkbox" name="tools" value="${escapeHtml3(
1124
+ tool.toolId
1125
+ )}" class="mt-0.5 w-5 h-5 rounded border-gray-300" checked>
1126
+ <div>
1127
+ <span class="font-medium text-gray-900">${escapeHtml3(tool.toolName)}</span>
1128
+ ${desc}
1129
+ </div>
1130
+ </label>`;
1131
+ }).join("\n");
1132
+ return `<div class="bg-gray-50 rounded-xl overflow-hidden">
1133
+ <div class="flex items-center justify-between px-4 py-3 bg-gray-100">
1134
+ <div class="flex items-center gap-3">
1135
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm">
1136
+ ${escapeHtml3(appName.charAt(0).toUpperCase())}
1137
+ </div>
1138
+ <span class="font-semibold text-gray-900">${escapeHtml3(appName)}</span>
1139
+ </div>
1140
+ <button type="button" class="text-sm text-blue-600 hover:text-blue-800"
1141
+ onclick="const container = this.closest('.bg-gray-50').querySelector('[data-app]'); const cbs = container.querySelectorAll('input[name=tools]'); const allChecked = [...cbs].every(cb => cb.checked); cbs.forEach(cb => cb.checked = !allChecked); updateCount();">
1142
+ Toggle All
1143
+ </button>
1144
+ </div>
1145
+ <div class="p-4 space-y-2" data-app="${escapeHtml3(appId)}">
1146
+ ${toolItems}
1147
+ </div>
1148
+ </div>`;
1149
+ }).join("\n");
1150
+ const updateCountScript = `
1151
+ <script>
1152
+ function updateCount() {
1153
+ const all = document.querySelectorAll('input[name="tools"]');
1154
+ const checked = document.querySelectorAll('input[name="tools"]:checked');
1155
+ document.getElementById('selection-count').textContent = checked.length + ' of ' + all.length + ' selected';
1156
+ document.getElementById('select-all').checked = all.length > 0 && all.length === checked.length;
1157
+ }
1158
+ document.querySelectorAll('input[name="tools"]').forEach(cb => cb.addEventListener('change', updateCount));
1159
+ </script>`;
1160
+ const content = `
1161
+ <h1 class="text-3xl font-bold text-gray-900 mb-4">Select Tools to Enable</h1>
1162
+ <p class="text-gray-600 mb-6">
1163
+ Choose which tools ${escapeHtml3(clientName)} can access. You can change this later.
1164
+ </p>
1165
+
1166
+ ${userInfo}
1167
+
1168
+ <form method="POST" action="${escapeHtml3(callbackPath)}" id="consent-form">
1169
+ <input type="hidden" name="csrf" value="${escapeHtml3(csrfToken)}">
1170
+ <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1171
+
1172
+ <!-- Select all toggle -->
1173
+ <div class="flex items-center justify-between mb-6">
1174
+ <label class="flex items-center gap-3 cursor-pointer">
1175
+ <input type="checkbox" id="select-all" class="w-5 h-5 rounded border-gray-300" checked
1176
+ onchange="document.querySelectorAll('input[name=tools]').forEach(cb => cb.checked = this.checked); updateCount();">
1177
+ <span class="text-gray-700">Select all tools</span>
1178
+ </label>
1179
+ <span id="selection-count" class="text-sm text-gray-500">${tools.length} of ${tools.length} selected</span>
1180
+ </div>
1181
+
1182
+ <!-- Tool groups by app -->
1183
+ <div class="space-y-6 mb-8">
1184
+ ${appGroups}
1185
+ </div>
1186
+
1187
+ <!-- Buttons -->
1188
+ <div class="flex gap-4">
1189
+ <button type="button" onclick="history.back()"
1190
+ class="flex-1 px-6 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 font-medium rounded-lg transition-colors">
1191
+ Cancel
1192
+ </button>
1193
+ <button type="submit"
1194
+ class="flex-1 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
1195
+ Confirm Selection
1196
+ </button>
1197
+ </div>
1198
+ </form>
1199
+ ${updateCountScript}`;
1200
+ return extraWideLayout(content, { title: "Select Tools" });
1201
+ }
1202
+ function buildLoginPage(params) {
1203
+ const { clientName, scope, pendingAuthId, callbackPath } = params;
1204
+ const scopesHtml = scope ? `<div class="p-4 bg-gray-50 rounded-lg mb-6">
1205
+ <p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">Requested permissions</p>
1206
+ <p class="text-gray-700">${scope.split(" ").map((s) => escapeHtml3(s)).join(", ") || "Default access"}</p>
1207
+ </div>` : "";
1208
+ const content = `
1209
+ <div class="bg-white rounded-2xl shadow-xl p-8">
1210
+ <h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">Sign In</h1>
1211
+ <p class="text-gray-600 mb-8 text-center">Authorize access to ${escapeHtml3(clientName)}</p>
1212
+
1213
+ ${scopesHtml}
1214
+
1215
+ <form method="GET" action="${escapeHtml3(callbackPath)}">
1216
+ <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1217
+
1218
+ <!-- Email -->
1219
+ <div class="mb-4">
1220
+ <label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
1221
+ <input type="email" id="email" name="email" required placeholder="you@example.com"
1222
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
1223
+ </div>
1224
+
1225
+ <!-- Name (optional) -->
1226
+ <div class="mb-6">
1227
+ <label for="name" class="block text-sm font-medium text-gray-700 mb-2">Name (optional)</label>
1228
+ <input type="text" id="name" name="name" placeholder="Your name"
1229
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
1230
+ </div>
1231
+
1232
+ <!-- Submit -->
1233
+ <button type="submit"
1234
+ class="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
1235
+ Authorize
1236
+ </button>
1237
+ </form>
1238
+
1239
+ <p class="text-center text-sm text-gray-500 mt-6">Client: ${escapeHtml3(clientName)}</p>
1240
+ </div>`;
1241
+ return centeredCardLayout(content, { title: "Sign In" });
1242
+ }
1243
+ function buildErrorPage(params) {
1244
+ const { error, description } = params;
1245
+ const content = `
1246
+ <div class="bg-white rounded-2xl shadow-xl p-8 text-center">
1247
+ <!-- Error icon -->
1248
+ <div class="flex justify-center mb-6">
1249
+ <div class="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center">
1250
+ <svg class="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1251
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
1252
+ </svg>
1253
+ </div>
1254
+ </div>
1255
+
1256
+ <h1 class="text-2xl font-bold text-gray-900 mb-4">Authorization Error</h1>
1257
+ <p class="mb-4">
1258
+ <code class="px-2 py-1 bg-gray-100 rounded text-red-600 font-mono text-sm">${escapeHtml3(error)}</code>
1259
+ </p>
1260
+ <p class="text-gray-600">${escapeHtml3(description)}</p>
1261
+ </div>`;
1262
+ return centeredCardLayout(content, { title: "Error" });
1263
+ }
1264
+ function renderToHtml(html, _options) {
1265
+ return html;
1266
+ }
1267
+
1268
+ // libs/auth/src/session/authorization.store.ts
1269
+ import { randomUUID, sha256Base64url } from "@frontmcp/utils";
1270
+ import { z as z2 } from "zod";
1271
+ var pkceChallengeSchema = z2.object({
1272
+ challenge: z2.string().min(43).max(128),
1273
+ method: z2.literal("S256")
1274
+ });
1275
+ var authorizationCodeRecordSchema = z2.object({
1276
+ code: z2.string().min(1),
1277
+ clientId: z2.string().min(1),
1278
+ redirectUri: z2.string().url(),
1279
+ scopes: z2.array(z2.string()),
1280
+ pkce: pkceChallengeSchema,
1281
+ userSub: z2.string().min(1),
1282
+ userEmail: z2.string().email().optional(),
1283
+ userName: z2.string().optional(),
1284
+ state: z2.string().optional(),
1285
+ createdAt: z2.number(),
1286
+ expiresAt: z2.number(),
1287
+ used: z2.boolean(),
1288
+ resource: z2.string().url().optional(),
1289
+ // Consent and federated login fields
1290
+ selectedToolIds: z2.array(z2.string()).optional(),
1291
+ selectedProviderIds: z2.array(z2.string()).optional(),
1292
+ skippedProviderIds: z2.array(z2.string()).optional(),
1293
+ consentEnabled: z2.boolean().optional(),
1294
+ federatedLoginUsed: z2.boolean().optional(),
1295
+ pendingAuthId: z2.string().optional()
1296
+ });
1297
+ function verifyPkce(codeVerifier, challenge) {
1298
+ if (challenge.method !== "S256") {
1299
+ return false;
1300
+ }
1301
+ const hash = sha256Base64url(codeVerifier);
1302
+ return hash === challenge.challenge;
1303
+ }
1304
+ function generatePkceChallenge(codeVerifier) {
1305
+ const challenge = sha256Base64url(codeVerifier);
1306
+ return { challenge, method: "S256" };
1307
+ }
1308
+ var InMemoryAuthorizationStore = class {
1309
+ codes = /* @__PURE__ */ new Map();
1310
+ pending = /* @__PURE__ */ new Map();
1311
+ refreshTokens = /* @__PURE__ */ new Map();
1312
+ /** Default TTL for authorization codes (60 seconds) */
1313
+ codeTtlMs = 60 * 1e3;
1314
+ /** Default TTL for pending authorizations (10 minutes) */
1315
+ pendingTtlMs = 10 * 60 * 1e3;
1316
+ /** Default TTL for refresh tokens (30 days) */
1317
+ refreshTtlMs = 30 * 24 * 60 * 60 * 1e3;
1318
+ generateCode() {
1319
+ return randomUUID().replace(/-/g, "") + randomUUID().replace(/-/g, "");
1320
+ }
1321
+ generateRefreshToken() {
1322
+ return randomUUID() + "-" + randomUUID();
1323
+ }
1324
+ async storeAuthorizationCode(record) {
1325
+ this.codes.set(record.code, record);
1326
+ }
1327
+ async getAuthorizationCode(code) {
1328
+ const record = this.codes.get(code);
1329
+ if (!record) return null;
1330
+ if (Date.now() > record.expiresAt) {
1331
+ this.codes.delete(code);
1332
+ return null;
1333
+ }
1334
+ return record;
1335
+ }
1336
+ async markCodeUsed(code) {
1337
+ const record = this.codes.get(code);
1338
+ if (record) {
1339
+ record.used = true;
1340
+ }
1341
+ }
1342
+ async deleteAuthorizationCode(code) {
1343
+ this.codes.delete(code);
1344
+ }
1345
+ async storePendingAuthorization(record) {
1346
+ this.pending.set(record.id, record);
1347
+ }
1348
+ async getPendingAuthorization(id) {
1349
+ const record = this.pending.get(id);
1350
+ if (!record) return null;
1351
+ if (Date.now() > record.expiresAt) {
1352
+ this.pending.delete(id);
1353
+ return null;
1354
+ }
1355
+ return record;
1356
+ }
1357
+ async deletePendingAuthorization(id) {
1358
+ this.pending.delete(id);
1359
+ }
1360
+ async storeRefreshToken(record) {
1361
+ this.refreshTokens.set(record.token, record);
1362
+ }
1363
+ async getRefreshToken(token) {
1364
+ const record = this.refreshTokens.get(token);
1365
+ if (!record) return null;
1366
+ if (Date.now() > record.expiresAt || record.revoked) {
1367
+ return null;
1368
+ }
1369
+ return record;
1370
+ }
1371
+ async revokeRefreshToken(token) {
1372
+ const record = this.refreshTokens.get(token);
1373
+ if (record) {
1374
+ record.revoked = true;
1375
+ }
1376
+ }
1377
+ async rotateRefreshToken(oldToken, newRecord) {
1378
+ await this.revokeRefreshToken(oldToken);
1379
+ newRecord.previousToken = oldToken;
1380
+ await this.storeRefreshToken(newRecord);
1381
+ }
1382
+ async cleanup() {
1383
+ const now = Date.now();
1384
+ for (const [code, record] of this.codes) {
1385
+ if (now > record.expiresAt) {
1386
+ this.codes.delete(code);
1387
+ }
1388
+ }
1389
+ for (const [id, record] of this.pending) {
1390
+ if (now > record.expiresAt) {
1391
+ this.pending.delete(id);
1392
+ }
1393
+ }
1394
+ for (const [token, record] of this.refreshTokens) {
1395
+ if (now > record.expiresAt || record.revoked) {
1396
+ this.refreshTokens.delete(token);
1397
+ }
1398
+ }
1399
+ }
1400
+ /**
1401
+ * Create an authorization code record with defaults
1402
+ */
1403
+ createCodeRecord(params) {
1404
+ const now = Date.now();
1405
+ return {
1406
+ code: this.generateCode(),
1407
+ clientId: params.clientId,
1408
+ redirectUri: params.redirectUri,
1409
+ scopes: params.scopes,
1410
+ pkce: params.pkce,
1411
+ userSub: params.userSub,
1412
+ userEmail: params.userEmail,
1413
+ userName: params.userName,
1414
+ state: params.state,
1415
+ resource: params.resource,
1416
+ createdAt: now,
1417
+ expiresAt: now + this.codeTtlMs,
1418
+ used: false,
1419
+ // Consent and Federated Login Data
1420
+ selectedToolIds: params.selectedToolIds,
1421
+ selectedProviderIds: params.selectedProviderIds,
1422
+ skippedProviderIds: params.skippedProviderIds,
1423
+ consentEnabled: params.consentEnabled,
1424
+ federatedLoginUsed: params.federatedLoginUsed,
1425
+ // Token migration ID (for federated auth)
1426
+ pendingAuthId: params.pendingAuthId
1427
+ };
1428
+ }
1429
+ /**
1430
+ * Create a pending authorization record with defaults
1431
+ */
1432
+ createPendingRecord(params) {
1433
+ const now = Date.now();
1434
+ return {
1435
+ id: randomUUID(),
1436
+ clientId: params.clientId,
1437
+ redirectUri: params.redirectUri,
1438
+ scopes: params.scopes,
1439
+ pkce: params.pkce,
1440
+ state: params.state,
1441
+ resource: params.resource,
1442
+ createdAt: now,
1443
+ expiresAt: now + this.pendingTtlMs,
1444
+ // Progressive/Incremental Authorization Fields
1445
+ isIncremental: params.isIncremental,
1446
+ targetAppId: params.targetAppId,
1447
+ targetToolId: params.targetToolId,
1448
+ existingSessionId: params.existingSessionId,
1449
+ existingAuthorizationId: params.existingAuthorizationId,
1450
+ // Federated Login State
1451
+ federatedLogin: params.federatedLogin,
1452
+ // Consent State
1453
+ consent: params.consent
1454
+ };
1455
+ }
1456
+ /**
1457
+ * Create a refresh token record with defaults
1458
+ */
1459
+ createRefreshTokenRecord(params) {
1460
+ const now = Date.now();
1461
+ return {
1462
+ token: this.generateRefreshToken(),
1463
+ clientId: params.clientId,
1464
+ userSub: params.userSub,
1465
+ scopes: params.scopes,
1466
+ resource: params.resource,
1467
+ createdAt: now,
1468
+ expiresAt: now + this.refreshTtlMs,
1469
+ revoked: false
1470
+ };
1471
+ }
1472
+ };
1473
+ var RedisAuthorizationStore = class {
1474
+ constructor(redis, namespace = "oauth:") {
1475
+ this.redis = redis;
1476
+ this.namespace = namespace;
1477
+ }
1478
+ key(type, id) {
1479
+ return `${this.namespace}${type}:${id}`;
1480
+ }
1481
+ generateCode() {
1482
+ return randomUUID().replace(/-/g, "") + randomUUID().replace(/-/g, "");
1483
+ }
1484
+ generateRefreshToken() {
1485
+ return randomUUID() + "-" + randomUUID();
1486
+ }
1487
+ async storeAuthorizationCode(record) {
1488
+ const ttl = Math.max(Math.ceil((record.expiresAt - Date.now()) / 1e3), 1);
1489
+ await this.redis.set(this.key("code", record.code), JSON.stringify(record), "EX", Math.max(ttl, 1));
1490
+ }
1491
+ async getAuthorizationCode(code) {
1492
+ const data = await this.redis.get(this.key("code", code));
1493
+ if (!data) return null;
1494
+ return JSON.parse(data);
1495
+ }
1496
+ async markCodeUsed(code) {
1497
+ const record = await this.getAuthorizationCode(code);
1498
+ if (record) {
1499
+ record.used = true;
1500
+ const ttl = Math.ceil((record.expiresAt - Date.now()) / 1e3);
1501
+ await this.redis.set(this.key("code", code), JSON.stringify(record), "EX", Math.max(ttl, 1));
1502
+ }
1503
+ }
1504
+ async deleteAuthorizationCode(code) {
1505
+ await this.redis.del(this.key("code", code));
1506
+ }
1507
+ async storePendingAuthorization(record) {
1508
+ const ttl = Math.max(Math.ceil((record.expiresAt - Date.now()) / 1e3), 1);
1509
+ await this.redis.set(this.key("pending", record.id), JSON.stringify(record), "EX", ttl);
1510
+ }
1511
+ async getPendingAuthorization(id) {
1512
+ const data = await this.redis.get(this.key("pending", id));
1513
+ if (!data) return null;
1514
+ return JSON.parse(data);
1515
+ }
1516
+ async deletePendingAuthorization(id) {
1517
+ await this.redis.del(this.key("pending", id));
1518
+ }
1519
+ async storeRefreshToken(record) {
1520
+ const ttl = Math.ceil((record.expiresAt - Date.now()) / 1e3);
1521
+ await this.redis.set(this.key("refresh", record.token), JSON.stringify(record), "EX", ttl);
1522
+ }
1523
+ async getRefreshToken(token) {
1524
+ const data = await this.redis.get(this.key("refresh", token));
1525
+ if (!data) return null;
1526
+ const record = JSON.parse(data);
1527
+ if (record.revoked) return null;
1528
+ return record;
1529
+ }
1530
+ async revokeRefreshToken(token) {
1531
+ const record = await this.getRefreshToken(token);
1532
+ if (record) {
1533
+ record.revoked = true;
1534
+ const ttl = Math.ceil((record.expiresAt - Date.now()) / 1e3);
1535
+ await this.redis.set(this.key("refresh", token), JSON.stringify(record), "EX", Math.max(ttl, 1));
1536
+ }
1537
+ }
1538
+ async rotateRefreshToken(oldToken, newRecord) {
1539
+ await this.revokeRefreshToken(oldToken);
1540
+ newRecord.previousToken = oldToken;
1541
+ await this.storeRefreshToken(newRecord);
1542
+ }
1543
+ async cleanup() {
1544
+ }
1545
+ };
1546
+
1547
+ // libs/auth/src/session/authorization-vault.ts
1548
+ import { z as z3 } from "zod";
1549
+ var credentialTypeSchema = z3.enum([
1550
+ "oauth",
1551
+ // OAuth 2.0 tokens
1552
+ "api_key",
1553
+ // API key (header or query param)
1554
+ "basic",
1555
+ // Basic auth (username:password)
1556
+ "bearer",
1557
+ // Bearer token (static)
1558
+ "private_key",
1559
+ // Private key for signing (JWT, etc.)
1560
+ "mtls",
1561
+ // Mutual TLS certificate
1562
+ "custom",
1563
+ // Custom credential type
1564
+ "ssh_key",
1565
+ // SSH key for authentication
1566
+ "service_account",
1567
+ // Cloud provider service accounts
1568
+ "oauth_pkce"
1569
+ // OAuth 2.0 with PKCE for public clients
1570
+ ]);
1571
+ var oauthCredentialSchema = z3.object({
1572
+ type: z3.literal("oauth"),
1573
+ /** Access token */
1574
+ accessToken: z3.string(),
1575
+ /** Refresh token (optional) */
1576
+ refreshToken: z3.string().optional(),
1577
+ /** Token type (usually 'Bearer') */
1578
+ tokenType: z3.string().default("Bearer"),
1579
+ /** Token expiration timestamp (epoch ms) */
1580
+ expiresAt: z3.number().optional(),
1581
+ /** Granted scopes */
1582
+ scopes: z3.array(z3.string()).default([]),
1583
+ /** ID token for OIDC (optional) */
1584
+ idToken: z3.string().optional()
1585
+ });
1586
+ var apiKeyCredentialSchema = z3.object({
1587
+ type: z3.literal("api_key"),
1588
+ /** The API key value */
1589
+ key: z3.string().min(1),
1590
+ /** Header name to use (e.g., 'X-API-Key', 'Authorization') */
1591
+ headerName: z3.string().default("X-API-Key"),
1592
+ /** Prefix for the header value (e.g., 'Bearer ', 'Api-Key ') */
1593
+ headerPrefix: z3.string().optional(),
1594
+ /** Alternative: send as query parameter */
1595
+ queryParam: z3.string().optional()
1596
+ });
1597
+ var basicAuthCredentialSchema = z3.object({
1598
+ type: z3.literal("basic"),
1599
+ /** Username */
1600
+ username: z3.string().min(1),
1601
+ /** Password */
1602
+ password: z3.string(),
1603
+ /** Pre-computed base64 encoded value (optional, for caching) */
1604
+ encodedValue: z3.string().optional()
1605
+ });
1606
+ var bearerCredentialSchema = z3.object({
1607
+ type: z3.literal("bearer"),
1608
+ /** The bearer token value */
1609
+ token: z3.string().min(1),
1610
+ /** Token expiration (optional, for static tokens that expire) */
1611
+ expiresAt: z3.number().optional()
1612
+ });
1613
+ var privateKeyCredentialSchema = z3.object({
1614
+ type: z3.literal("private_key"),
1615
+ /** Key format */
1616
+ format: z3.enum(["pem", "jwk", "pkcs8", "pkcs12"]),
1617
+ /** The key data (PEM string or JWK JSON) */
1618
+ keyData: z3.string(),
1619
+ /** Key ID (for JWK) */
1620
+ keyId: z3.string().optional(),
1621
+ /** Algorithm to use for signing */
1622
+ algorithm: z3.string().optional(),
1623
+ /** Passphrase if key is encrypted */
1624
+ passphrase: z3.string().optional(),
1625
+ /** Associated certificate (for mTLS) */
1626
+ certificate: z3.string().optional()
1627
+ });
1628
+ var mtlsCredentialSchema = z3.object({
1629
+ type: z3.literal("mtls"),
1630
+ /** Client certificate (PEM format) */
1631
+ certificate: z3.string(),
1632
+ /** Private key (PEM format) */
1633
+ privateKey: z3.string(),
1634
+ /** Passphrase if private key is encrypted */
1635
+ passphrase: z3.string().optional(),
1636
+ /** CA certificate chain (optional) */
1637
+ caCertificate: z3.string().optional()
1638
+ });
1639
+ var customCredentialSchema = z3.object({
1640
+ type: z3.literal("custom"),
1641
+ /** Custom type identifier */
1642
+ customType: z3.string().min(1),
1643
+ /** Arbitrary credential data */
1644
+ data: z3.record(z3.string(), z3.unknown()),
1645
+ /** Headers to include in requests */
1646
+ headers: z3.record(z3.string(), z3.string()).optional()
1647
+ });
1648
+ var sshKeyCredentialSchema = z3.object({
1649
+ type: z3.literal("ssh_key"),
1650
+ /** Private key (PEM format) */
1651
+ privateKey: z3.string().min(1),
1652
+ /** Public key (optional, can be derived from private key) */
1653
+ publicKey: z3.string().optional(),
1654
+ /** Passphrase if private key is encrypted */
1655
+ passphrase: z3.string().optional(),
1656
+ /** Key type */
1657
+ keyType: z3.enum(["rsa", "ed25519", "ecdsa", "dsa"]).default("ed25519"),
1658
+ /** Key fingerprint (SHA256 hash) */
1659
+ fingerprint: z3.string().optional(),
1660
+ /** Username for SSH connections */
1661
+ username: z3.string().optional()
1662
+ });
1663
+ var serviceAccountCredentialSchema = z3.object({
1664
+ type: z3.literal("service_account"),
1665
+ /** Cloud provider */
1666
+ provider: z3.enum(["gcp", "aws", "azure", "custom"]),
1667
+ /** Raw credentials (JSON key file content, access keys, etc.) */
1668
+ credentials: z3.record(z3.string(), z3.unknown()),
1669
+ /** Project/Account ID */
1670
+ projectId: z3.string().optional(),
1671
+ /** Region for regional services */
1672
+ region: z3.string().optional(),
1673
+ /** AWS: Role ARN to assume */
1674
+ assumeRoleArn: z3.string().optional(),
1675
+ /** AWS: External ID for cross-account access */
1676
+ externalId: z3.string().optional(),
1677
+ /** Service account email (GCP) or ARN (AWS) */
1678
+ serviceAccountId: z3.string().optional(),
1679
+ /** Expiration timestamp for temporary credentials */
1680
+ expiresAt: z3.number().optional()
1681
+ });
1682
+ var pkceOAuthCredentialSchema = z3.object({
1683
+ type: z3.literal("oauth_pkce"),
1684
+ /** Access token */
1685
+ accessToken: z3.string(),
1686
+ /** Refresh token (optional) */
1687
+ refreshToken: z3.string().optional(),
1688
+ /** Token type (usually 'Bearer') */
1689
+ tokenType: z3.string().default("Bearer"),
1690
+ /** Token expiration timestamp (epoch ms) */
1691
+ expiresAt: z3.number().optional(),
1692
+ /** Granted scopes */
1693
+ scopes: z3.array(z3.string()).default([]),
1694
+ /** ID token for OIDC (optional) */
1695
+ idToken: z3.string().optional(),
1696
+ /** Authorization server issuer */
1697
+ issuer: z3.string().optional()
1698
+ });
1699
+ var credentialSchema = z3.discriminatedUnion("type", [
1700
+ oauthCredentialSchema,
1701
+ apiKeyCredentialSchema,
1702
+ basicAuthCredentialSchema,
1703
+ bearerCredentialSchema,
1704
+ privateKeyCredentialSchema,
1705
+ mtlsCredentialSchema,
1706
+ customCredentialSchema,
1707
+ sshKeyCredentialSchema,
1708
+ serviceAccountCredentialSchema,
1709
+ pkceOAuthCredentialSchema
1710
+ ]);
1711
+ var appCredentialSchema = z3.object({
1712
+ /** App ID this credential belongs to */
1713
+ appId: z3.string().min(1),
1714
+ /** Provider ID within the app (for apps with multiple auth providers) */
1715
+ providerId: z3.string().min(1),
1716
+ /** The credential data */
1717
+ credential: credentialSchema,
1718
+ /** Timestamp when credential was acquired */
1719
+ acquiredAt: z3.number(),
1720
+ /** Timestamp when credential was last used */
1721
+ lastUsedAt: z3.number().optional(),
1722
+ /** Credential expiration (if applicable) */
1723
+ expiresAt: z3.number().optional(),
1724
+ /** Whether this credential is currently valid */
1725
+ isValid: z3.boolean().default(true),
1726
+ /** Error message if credential is invalid */
1727
+ invalidReason: z3.string().optional(),
1728
+ /** User info associated with this credential */
1729
+ userInfo: z3.object({
1730
+ sub: z3.string().optional(),
1731
+ email: z3.string().optional(),
1732
+ name: z3.string().optional()
1733
+ }).optional(),
1734
+ /** Metadata for tracking/debugging */
1735
+ metadata: z3.record(z3.string(), z3.unknown()).optional()
1736
+ });
1737
+ var vaultConsentRecordSchema = z3.object({
1738
+ /** Whether consent was enabled */
1739
+ enabled: z3.boolean(),
1740
+ /** Selected tool IDs (user approved these) */
1741
+ selectedToolIds: z3.array(z3.string()),
1742
+ /** Available tool IDs at time of consent */
1743
+ availableToolIds: z3.array(z3.string()),
1744
+ /** Timestamp when consent was given */
1745
+ consentedAt: z3.number(),
1746
+ /** Consent version for tracking changes */
1747
+ version: z3.string().default("1.0")
1748
+ });
1749
+ var vaultFederatedRecordSchema = z3.object({
1750
+ /** Provider IDs that were selected */
1751
+ selectedProviderIds: z3.array(z3.string()),
1752
+ /** Provider IDs that were skipped (can be authorized later) */
1753
+ skippedProviderIds: z3.array(z3.string()),
1754
+ /** Primary provider ID */
1755
+ primaryProviderId: z3.string().optional(),
1756
+ /** Timestamp when federated login was completed */
1757
+ completedAt: z3.number()
1758
+ });
1759
+ var pendingIncrementalAuthSchema = z3.object({
1760
+ /** Unique ID for this request */
1761
+ id: z3.string(),
1762
+ /** App ID being authorized */
1763
+ appId: z3.string(),
1764
+ /** Tool ID that triggered the auth request */
1765
+ toolId: z3.string().optional(),
1766
+ /** Authorization URL */
1767
+ authUrl: z3.string(),
1768
+ /** Required scopes */
1769
+ requiredScopes: z3.array(z3.string()).optional(),
1770
+ /** Whether elicit is being used */
1771
+ elicitId: z3.string().optional(),
1772
+ /** Timestamp when request was created */
1773
+ createdAt: z3.number(),
1774
+ /** Expiration timestamp */
1775
+ expiresAt: z3.number(),
1776
+ /** Status of the request */
1777
+ status: z3.enum(["pending", "completed", "cancelled", "expired"])
1778
+ });
1779
+ var authorizationVaultEntrySchema = z3.object({
1780
+ /** Vault ID (maps to access token jti claim) */
1781
+ id: z3.string(),
1782
+ /** User subject identifier */
1783
+ userSub: z3.string(),
1784
+ /** User email */
1785
+ userEmail: z3.string().optional(),
1786
+ /** User name */
1787
+ userName: z3.string().optional(),
1788
+ /** Client ID that created this session */
1789
+ clientId: z3.string(),
1790
+ /** Creation timestamp */
1791
+ createdAt: z3.number(),
1792
+ /** Last access timestamp */
1793
+ lastAccessAt: z3.number(),
1794
+ /** App credentials (keyed by `${appId}:${providerId}`) */
1795
+ appCredentials: z3.record(z3.string(), appCredentialSchema).default({}),
1796
+ /** Consent record */
1797
+ consent: vaultConsentRecordSchema.optional(),
1798
+ /** Federated login record */
1799
+ federated: vaultFederatedRecordSchema.optional(),
1800
+ /** Pending incremental authorization requests */
1801
+ pendingAuths: z3.array(pendingIncrementalAuthSchema),
1802
+ /** Apps that are fully authorized */
1803
+ authorizedAppIds: z3.array(z3.string()),
1804
+ /** Apps that were skipped (not yet authorized) */
1805
+ skippedAppIds: z3.array(z3.string())
1806
+ });
1807
+
1808
+ // libs/auth/src/session/vault-encryption.ts
1809
+ import { z as z4 } from "zod";
1810
+ import {
1811
+ hkdfSha256,
1812
+ encryptAesGcm,
1813
+ decryptAesGcm,
1814
+ randomBytes as randomBytes3,
1815
+ base64urlEncode,
1816
+ base64urlDecode
1817
+ } from "@frontmcp/utils";
1818
+ var encryptedDataSchema = z4.object({
1819
+ /** Version for future algorithm changes */
1820
+ v: z4.literal(1),
1821
+ /** Algorithm identifier */
1822
+ alg: z4.literal("aes-256-gcm"),
1823
+ /** Initialization vector (base64) */
1824
+ iv: z4.string(),
1825
+ /** Ciphertext (base64) */
1826
+ ct: z4.string(),
1827
+ /** Authentication tag (base64) */
1828
+ tag: z4.string()
1829
+ });
1830
+ var VaultEncryption = class {
1831
+ pepper;
1832
+ hkdfInfo;
1833
+ constructor(config = {}) {
1834
+ this.pepper = new TextEncoder().encode(config.pepper ?? "");
1835
+ this.hkdfInfo = config.hkdfInfo ?? "frontmcp-vault-v1";
1836
+ }
1837
+ /**
1838
+ * Derive an encryption key from JWT claims
1839
+ *
1840
+ * The key derivation uses HKDF:
1841
+ * 1. Combine jti + vaultKey + sub + iat + pepper as IKM
1842
+ * 2. Apply HKDF-SHA256 to derive a 256-bit key
1843
+ *
1844
+ * @param claims - JWT claims containing key material
1845
+ * @returns 32-byte encryption key as Uint8Array
1846
+ */
1847
+ async deriveKey(claims) {
1848
+ const ikmParts = [
1849
+ claims.jti,
1850
+ claims.vaultKey ?? "",
1851
+ claims.sub,
1852
+ claims.iat.toString(),
1853
+ new TextDecoder().decode(this.pepper)
1854
+ ];
1855
+ const ikm = new TextEncoder().encode(ikmParts.join(""));
1856
+ const infoBytes = new TextEncoder().encode(this.hkdfInfo);
1857
+ const key = await hkdfSha256(ikm, new Uint8Array(0), infoBytes, 32);
1858
+ return key;
1859
+ }
1860
+ /**
1861
+ * Derive a key directly from the raw JWT token string
1862
+ *
1863
+ * This is useful when you want to derive the key from the token
1864
+ * before or without fully parsing the claims. Uses the token's
1865
+ * signature portion as additional entropy.
1866
+ *
1867
+ * @param token - The raw JWT token string
1868
+ * @param claims - Parsed JWT claims
1869
+ * @returns 32-byte encryption key as Uint8Array
1870
+ */
1871
+ async deriveKeyFromToken(token, claims) {
1872
+ const parts = token.split(".");
1873
+ const signature = parts[2] ?? "";
1874
+ const ikmParts = [
1875
+ claims.jti,
1876
+ claims.vaultKey ?? "",
1877
+ claims.sub,
1878
+ claims.iat.toString(),
1879
+ signature,
1880
+ new TextDecoder().decode(this.pepper)
1881
+ ];
1882
+ const ikm = new TextEncoder().encode(ikmParts.join(""));
1883
+ const infoBytes = new TextEncoder().encode(this.hkdfInfo);
1884
+ const key = await hkdfSha256(ikm, new Uint8Array(0), infoBytes, 32);
1885
+ return key;
1886
+ }
1887
+ /**
1888
+ * Encrypt plaintext data using AES-256-GCM
1889
+ *
1890
+ * @param plaintext - Data to encrypt (typically JSON string)
1891
+ * @param key - 32-byte encryption key from deriveKey()
1892
+ * @returns Encrypted data object (safe to store in Redis)
1893
+ */
1894
+ async encrypt(plaintext, key) {
1895
+ if (key.length !== 32) {
1896
+ throw new Error("Encryption key must be 32 bytes");
1897
+ }
1898
+ const iv = randomBytes3(12);
1899
+ const { ciphertext, tag } = await encryptAesGcm(key, new TextEncoder().encode(plaintext), iv);
1900
+ return {
1901
+ v: 1,
1902
+ alg: "aes-256-gcm",
1903
+ iv: base64urlEncode(iv),
1904
+ ct: base64urlEncode(ciphertext),
1905
+ tag: base64urlEncode(tag)
1906
+ };
1907
+ }
1908
+ /**
1909
+ * Decrypt encrypted data using AES-256-GCM
1910
+ *
1911
+ * @param encrypted - Encrypted data object from encrypt()
1912
+ * @param key - 32-byte encryption key from deriveKey()
1913
+ * @returns Decrypted plaintext
1914
+ * @throws Error if decryption fails (wrong key, tampered data, etc.)
1915
+ */
1916
+ async decrypt(encrypted, key) {
1917
+ if (key.length !== 32) {
1918
+ throw new Error("Encryption key must be 32 bytes");
1919
+ }
1920
+ const parsed = encryptedDataSchema.safeParse(encrypted);
1921
+ if (!parsed.success) {
1922
+ throw new Error("Invalid encrypted data format");
1923
+ }
1924
+ const { iv, ct, tag } = parsed.data;
1925
+ const ivBuffer = base64urlDecode(iv);
1926
+ const ciphertext = base64urlDecode(ct);
1927
+ const tagBuffer = base64urlDecode(tag);
1928
+ try {
1929
+ const plaintext = await decryptAesGcm(key, ciphertext, ivBuffer, tagBuffer);
1930
+ return new TextDecoder().decode(plaintext);
1931
+ } catch (_error) {
1932
+ throw new Error("Decryption failed: invalid key or corrupted data");
1933
+ }
1934
+ }
1935
+ /**
1936
+ * Encrypt a JavaScript object (serializes to JSON first)
1937
+ *
1938
+ * @param data - Object to encrypt
1939
+ * @param key - Encryption key
1940
+ * @returns Encrypted data
1941
+ */
1942
+ async encryptObject(data, key) {
1943
+ return this.encrypt(JSON.stringify(data), key);
1944
+ }
1945
+ /**
1946
+ * Decrypt and parse a JavaScript object
1947
+ *
1948
+ * @param encrypted - Encrypted data
1949
+ * @param key - Encryption key
1950
+ * @returns Decrypted and parsed object
1951
+ */
1952
+ async decryptObject(encrypted, key) {
1953
+ const plaintext = await this.decrypt(encrypted, key);
1954
+ return JSON.parse(plaintext);
1955
+ }
1956
+ /**
1957
+ * Check if data is in encrypted format
1958
+ *
1959
+ * @param data - Data to check
1960
+ * @returns True if data appears to be encrypted
1961
+ */
1962
+ isEncrypted(data) {
1963
+ return encryptedDataSchema.safeParse(data).success;
1964
+ }
1965
+ };
1966
+ var encryptedVaultEntrySchema = z4.object({
1967
+ /** Vault ID (maps to JWT jti claim) */
1968
+ id: z4.string(),
1969
+ /** User subject identifier */
1970
+ userSub: z4.string(),
1971
+ /** User email (unencrypted for display) */
1972
+ userEmail: z4.string().optional(),
1973
+ /** User name (unencrypted for display) */
1974
+ userName: z4.string().optional(),
1975
+ /** Client ID that created this session */
1976
+ clientId: z4.string(),
1977
+ /** Creation timestamp */
1978
+ createdAt: z4.number(),
1979
+ /** Last access timestamp */
1980
+ lastAccessAt: z4.number(),
1981
+ /** Encrypted sensitive data (provider tokens, credentials, consent) */
1982
+ encryptedData: encryptedDataSchema,
1983
+ /** Apps that are fully authorized (unencrypted for quick lookup) */
1984
+ authorizedAppIds: z4.array(z4.string()),
1985
+ /** Apps that were skipped (unencrypted for quick lookup) */
1986
+ skippedAppIds: z4.array(z4.string()),
1987
+ /** Pending auth IDs (unencrypted for lookup, actual URLs encrypted) */
1988
+ pendingAuthIds: z4.array(z4.string()).default([])
1989
+ });
1990
+
1991
+ // libs/auth/src/session/token.vault.ts
1992
+ import { encryptAesGcm as encryptAesGcm2, decryptAesGcm as decryptAesGcm2, randomBytes as randomBytes4, base64urlEncode as base64urlEncode2, base64urlDecode as base64urlDecode2 } from "@frontmcp/utils";
1993
+ var TokenVault = class {
1994
+ /** Active key used for new encryptions */
1995
+ active;
1996
+ /** All known keys by kid for decryption (includes active) */
1997
+ keys = /* @__PURE__ */ new Map();
1998
+ constructor(keys) {
1999
+ if (!Array.isArray(keys) || keys.length === 0) {
2000
+ throw new Error("TokenVault requires at least one key");
2001
+ }
2002
+ for (const k of keys) {
2003
+ if (!(k.key instanceof Uint8Array) || k.key.length !== 32) {
2004
+ throw new Error(`TokenVault key "${k.kid}" must be a 32-byte Uint8Array for AES-256-GCM`);
2005
+ }
2006
+ if (this.keys.has(k.kid)) {
2007
+ throw new Error(`TokenVault duplicate kid: "${k.kid}"`);
2008
+ }
2009
+ this.keys.set(k.kid, k.key);
2010
+ }
2011
+ this.active = keys[0];
2012
+ }
2013
+ rotateTo(k) {
2014
+ if (!(k.key instanceof Uint8Array) || k.key.length !== 32) {
2015
+ throw new Error(`TokenVault key "${k.kid}" must be a 32-byte Uint8Array for AES-256-GCM`);
2016
+ }
2017
+ this.active = k;
2018
+ this.keys.set(k.kid, k.key);
2019
+ }
2020
+ async encrypt(plaintext, opts) {
2021
+ const iv = randomBytes4(12);
2022
+ const { ciphertext, tag } = await encryptAesGcm2(this.active.key, new TextEncoder().encode(plaintext), iv);
2023
+ return {
2024
+ alg: "A256GCM",
2025
+ kid: this.active.kid,
2026
+ iv: base64urlEncode2(iv),
2027
+ tag: base64urlEncode2(tag),
2028
+ data: base64urlEncode2(ciphertext),
2029
+ exp: opts?.exp,
2030
+ meta: opts?.meta
2031
+ };
2032
+ }
2033
+ async decrypt(blob) {
2034
+ if (blob.exp !== void 0) {
2035
+ const nowSeconds = Math.floor(Date.now() / 1e3);
2036
+ if (nowSeconds > blob.exp) {
2037
+ throw new Error(`vault_expired:${blob.kid}`);
2038
+ }
2039
+ }
2040
+ const key = this.keys.get(blob.kid);
2041
+ if (!key) throw new Error(`vault_unknown_kid:${blob.kid}`);
2042
+ const iv = base64urlDecode2(blob.iv);
2043
+ const tag = base64urlDecode2(blob.tag);
2044
+ const data = base64urlDecode2(blob.data);
2045
+ const plaintext = await decryptAesGcm2(key, data, iv, tag);
2046
+ return new TextDecoder().decode(plaintext);
2047
+ }
2048
+ };
2049
+
2050
+ // libs/auth/src/session/index.ts
2051
+ import {
2052
+ hkdfSha256 as hkdfSha2562,
2053
+ encryptValue,
2054
+ decryptValue,
2055
+ encryptAesGcm as encryptAesGcm3,
2056
+ decryptAesGcm as decryptAesGcm3
2057
+ } from "@frontmcp/utils";
2058
+
2059
+ // libs/auth/src/session/utils/tiny-ttl-cache.ts
2060
+ var TinyTtlCache = class {
2061
+ constructor(ttlMs) {
2062
+ this.ttlMs = ttlMs;
2063
+ }
2064
+ map = /* @__PURE__ */ new Map();
2065
+ get(k) {
2066
+ const hit = this.map.get(k);
2067
+ if (!hit) return void 0;
2068
+ if (hit.exp < Date.now()) {
2069
+ this.map.delete(k);
2070
+ return void 0;
2071
+ }
2072
+ return hit.v;
2073
+ }
2074
+ set(k, v) {
2075
+ this.map.set(k, { v, exp: Date.now() + this.ttlMs });
2076
+ }
2077
+ delete(k) {
2078
+ return this.map.delete(k);
2079
+ }
2080
+ clear() {
2081
+ this.map.clear();
2082
+ }
2083
+ size() {
2084
+ return this.map.size;
2085
+ }
2086
+ /**
2087
+ * Remove all expired entries
2088
+ */
2089
+ cleanup() {
2090
+ const now = Date.now();
2091
+ let removed = 0;
2092
+ for (const [k, { exp }] of this.map) {
2093
+ if (exp < now) {
2094
+ this.map.delete(k);
2095
+ removed++;
2096
+ }
2097
+ }
2098
+ return removed;
2099
+ }
2100
+ };
2101
+
2102
+ // libs/auth/src/session/storage/index.ts
2103
+ import { TypedStorage as TypedStorage3 } from "@frontmcp/utils";
2104
+ import { EncryptedTypedStorage, EncryptedStorageError } from "@frontmcp/utils";
2105
+
2106
+ // libs/auth/src/session/storage/storage-token-store.ts
2107
+ import { randomUUID as randomUUID2, TypedStorage } from "@frontmcp/utils";
2108
+ var StorageTokenStore = class {
2109
+ storage;
2110
+ namespace;
2111
+ defaultTtlSeconds;
2112
+ /** Track if original storage was namespaced to avoid double-prefixing */
2113
+ storageIsNamespaced;
2114
+ constructor(storage, options = {}) {
2115
+ this.namespace = options.namespace ?? "tok";
2116
+ this.defaultTtlSeconds = options.defaultTtlSeconds;
2117
+ this.storageIsNamespaced = "namespace" in storage && typeof storage.namespace === "function";
2118
+ const namespacedStorage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
2119
+ this.storage = new TypedStorage(namespacedStorage);
2120
+ }
2121
+ /**
2122
+ * Allocate a new unique ID for a token record.
2123
+ */
2124
+ allocId() {
2125
+ return randomUUID2();
2126
+ }
2127
+ /**
2128
+ * Store an encrypted token blob.
2129
+ *
2130
+ * TTL is calculated from blob.exp (epoch seconds) if present.
2131
+ * Falls back to defaultTtlSeconds if configured.
2132
+ *
2133
+ * @param id - Token record ID
2134
+ * @param blob - Encrypted token blob
2135
+ */
2136
+ async put(id, blob) {
2137
+ const record = {
2138
+ id,
2139
+ blob,
2140
+ updatedAt: Date.now()
2141
+ };
2142
+ const ttlSeconds = this.calculateTtl(blob.exp);
2143
+ await this.storage.set(this.key(id), record, ttlSeconds ? { ttlSeconds } : void 0);
2144
+ }
2145
+ /**
2146
+ * Retrieve a token record by ID.
2147
+ *
2148
+ * @param id - Token record ID
2149
+ * @returns The secret record, or undefined if not found
2150
+ */
2151
+ async get(id) {
2152
+ const record = await this.storage.get(this.key(id));
2153
+ return record ?? void 0;
2154
+ }
2155
+ /**
2156
+ * Delete a token record.
2157
+ *
2158
+ * @param id - Token record ID
2159
+ */
2160
+ async del(id) {
2161
+ await this.storage.delete(this.key(id));
2162
+ }
2163
+ /**
2164
+ * Calculate TTL in seconds from expiration timestamp.
2165
+ *
2166
+ * @param exp - Expiration timestamp in epoch seconds
2167
+ * @returns TTL in seconds, or undefined if no TTL should be applied
2168
+ */
2169
+ calculateTtl(exp) {
2170
+ if (exp) {
2171
+ const nowSeconds = Math.floor(Date.now() / 1e3);
2172
+ const ttl = exp - nowSeconds;
2173
+ return ttl > 0 ? ttl : 1;
2174
+ }
2175
+ return this.defaultTtlSeconds;
2176
+ }
2177
+ /**
2178
+ * Build the storage key for a token ID.
2179
+ * For namespaced storage, the namespace is handled by the storage layer.
2180
+ * For non-namespaced storage, includes the namespace prefix in the key.
2181
+ */
2182
+ key(id) {
2183
+ return this.storageIsNamespaced ? id : `${this.namespace}:${id}`;
2184
+ }
2185
+ };
2186
+
2187
+ // libs/auth/src/session/storage/storage-authorization-vault.ts
2188
+ import { randomUUID as randomUUID3, TypedStorage as TypedStorage2 } from "@frontmcp/utils";
2189
+ var StorageAuthorizationVault = class {
2190
+ storage;
2191
+ namespace;
2192
+ pendingAuthTtlMs;
2193
+ constructor(storage, options = {}) {
2194
+ this.namespace = options.namespace ?? "vault";
2195
+ this.pendingAuthTtlMs = options.pendingAuthTtlMs ?? 10 * 60 * 1e3;
2196
+ const namespacedStorage = this.isNamespacedStorage(storage) ? storage.namespace(this.namespace) : storage;
2197
+ this.storage = new TypedStorage2(namespacedStorage, {
2198
+ schema: options.validateOnRead ? authorizationVaultEntrySchema : void 0,
2199
+ throwOnInvalid: false
2200
+ });
2201
+ }
2202
+ // ============================================
2203
+ // Core CRUD Methods
2204
+ // ============================================
2205
+ async create(params) {
2206
+ const now = Date.now();
2207
+ const entry = {
2208
+ id: randomUUID3(),
2209
+ userSub: params.userSub,
2210
+ userEmail: params.userEmail,
2211
+ userName: params.userName,
2212
+ clientId: params.clientId,
2213
+ createdAt: now,
2214
+ lastAccessAt: now,
2215
+ appCredentials: {},
2216
+ consent: params.consent,
2217
+ federated: params.federated,
2218
+ pendingAuths: [],
2219
+ authorizedAppIds: params.authorizedAppIds ?? [],
2220
+ skippedAppIds: params.skippedAppIds ?? []
2221
+ };
2222
+ await this.storage.set(this.key(entry.id), entry);
2223
+ return entry;
2224
+ }
2225
+ async get(id) {
2226
+ return this.storage.get(this.key(id));
2227
+ }
2228
+ async update(id, updates) {
2229
+ const entry = await this.get(id);
2230
+ if (!entry) {
2231
+ console.warn(`[StorageAuthorizationVault] Update failed: vault entry not found for id ${id}`);
2232
+ return;
2233
+ }
2234
+ const updated = {
2235
+ ...entry,
2236
+ ...updates,
2237
+ lastAccessAt: Date.now()
2238
+ };
2239
+ await this.storage.set(this.key(id), updated);
2240
+ }
2241
+ async delete(id) {
2242
+ await this.storage.delete(this.key(id));
2243
+ }
2244
+ // ============================================
2245
+ // Consent Methods
2246
+ // ============================================
2247
+ async updateConsent(vaultId, consent) {
2248
+ const entry = await this.get(vaultId);
2249
+ if (!entry) return;
2250
+ entry.consent = consent;
2251
+ entry.lastAccessAt = Date.now();
2252
+ await this.storage.set(this.key(vaultId), entry);
2253
+ }
2254
+ // ============================================
2255
+ // App Authorization Methods
2256
+ // ============================================
2257
+ async authorizeApp(vaultId, appId) {
2258
+ const entry = await this.get(vaultId);
2259
+ if (!entry) return;
2260
+ entry.skippedAppIds = entry.skippedAppIds.filter((id) => id !== appId);
2261
+ if (!entry.authorizedAppIds.includes(appId)) {
2262
+ entry.authorizedAppIds.push(appId);
2263
+ }
2264
+ entry.lastAccessAt = Date.now();
2265
+ await this.storage.set(this.key(vaultId), entry);
2266
+ }
2267
+ async isAppAuthorized(vaultId, appId) {
2268
+ const entry = await this.get(vaultId);
2269
+ if (!entry) return false;
2270
+ return entry.authorizedAppIds.includes(appId);
2271
+ }
2272
+ // ============================================
2273
+ // Pending Auth Methods
2274
+ // ============================================
2275
+ async createPendingAuth(vaultId, params) {
2276
+ const entry = await this.get(vaultId);
2277
+ if (!entry) {
2278
+ throw new Error(`Vault not found: ${vaultId}`);
2279
+ }
2280
+ const now = Date.now();
2281
+ const pendingAuth = {
2282
+ id: randomUUID3(),
2283
+ appId: params.appId,
2284
+ toolId: params.toolId,
2285
+ authUrl: params.authUrl,
2286
+ requiredScopes: params.requiredScopes,
2287
+ elicitId: params.elicitId,
2288
+ createdAt: now,
2289
+ expiresAt: now + (params.ttlMs ?? this.pendingAuthTtlMs),
2290
+ status: "pending"
2291
+ };
2292
+ entry.pendingAuths.push(pendingAuth);
2293
+ entry.lastAccessAt = now;
2294
+ await this.storage.set(this.key(vaultId), entry);
2295
+ return pendingAuth;
2296
+ }
2297
+ async getPendingAuth(vaultId, pendingAuthId) {
2298
+ const entry = await this.get(vaultId);
2299
+ if (!entry) return null;
2300
+ const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
2301
+ if (!pendingAuth) return null;
2302
+ if (Date.now() > pendingAuth.expiresAt && pendingAuth.status === "pending") {
2303
+ pendingAuth.status = "expired";
2304
+ await this.storage.set(this.key(vaultId), entry);
2305
+ }
2306
+ return pendingAuth;
2307
+ }
2308
+ async completePendingAuth(vaultId, pendingAuthId) {
2309
+ const entry = await this.get(vaultId);
2310
+ if (!entry) return;
2311
+ const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
2312
+ if (pendingAuth) {
2313
+ pendingAuth.status = "completed";
2314
+ entry.skippedAppIds = entry.skippedAppIds.filter((id) => id !== pendingAuth.appId);
2315
+ if (!entry.authorizedAppIds.includes(pendingAuth.appId)) {
2316
+ entry.authorizedAppIds.push(pendingAuth.appId);
2317
+ }
2318
+ entry.lastAccessAt = Date.now();
2319
+ await this.storage.set(this.key(vaultId), entry);
2320
+ }
2321
+ }
2322
+ async cancelPendingAuth(vaultId, pendingAuthId) {
2323
+ const entry = await this.get(vaultId);
2324
+ if (!entry) return;
2325
+ const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
2326
+ if (pendingAuth) {
2327
+ pendingAuth.status = "cancelled";
2328
+ await this.storage.set(this.key(vaultId), entry);
2329
+ }
2330
+ }
2331
+ async getPendingAuths(vaultId) {
2332
+ const entry = await this.get(vaultId);
2333
+ if (!entry) return [];
2334
+ const now = Date.now();
2335
+ let updated = false;
2336
+ const pending = entry.pendingAuths.filter((p) => {
2337
+ if (now > p.expiresAt && p.status === "pending") {
2338
+ p.status = "expired";
2339
+ updated = true;
2340
+ }
2341
+ return p.status === "pending";
2342
+ });
2343
+ if (updated) {
2344
+ await this.storage.set(this.key(vaultId), entry);
2345
+ }
2346
+ return pending;
2347
+ }
2348
+ // ============================================
2349
+ // App Credential Methods
2350
+ // ============================================
2351
+ credentialKey(appId, providerId) {
2352
+ return `${appId}:${providerId}`;
2353
+ }
2354
+ async addAppCredential(vaultId, credential) {
2355
+ const entry = await this.get(vaultId);
2356
+ if (!entry) return;
2357
+ const shouldStore = await this.shouldStoreCredential(vaultId, credential.appId);
2358
+ if (!shouldStore) {
2359
+ return;
2360
+ }
2361
+ const key = this.credentialKey(credential.appId, credential.providerId);
2362
+ entry.appCredentials[key] = credential;
2363
+ entry.lastAccessAt = Date.now();
2364
+ await this.storage.set(this.key(vaultId), entry);
2365
+ }
2366
+ async removeAppCredential(vaultId, appId, providerId) {
2367
+ const entry = await this.get(vaultId);
2368
+ if (!entry) return;
2369
+ const key = this.credentialKey(appId, providerId);
2370
+ delete entry.appCredentials[key];
2371
+ entry.lastAccessAt = Date.now();
2372
+ await this.storage.set(this.key(vaultId), entry);
2373
+ }
2374
+ async getAppCredentials(vaultId, appId) {
2375
+ const entry = await this.get(vaultId);
2376
+ if (!entry) return [];
2377
+ const prefix = `${appId}:`;
2378
+ return Object.entries(entry.appCredentials).filter(([key]) => key.startsWith(prefix)).map(([, cred]) => cred);
2379
+ }
2380
+ async getCredential(vaultId, appId, providerId) {
2381
+ const entry = await this.get(vaultId);
2382
+ if (!entry) return null;
2383
+ const key = this.credentialKey(appId, providerId);
2384
+ return entry.appCredentials[key] ?? null;
2385
+ }
2386
+ async getAllCredentials(vaultId, filterByConsent = false) {
2387
+ const entry = await this.get(vaultId);
2388
+ if (!entry) return [];
2389
+ const allCredentials = Object.values(entry.appCredentials);
2390
+ if (!filterByConsent || !entry.consent?.enabled) {
2391
+ return allCredentials;
2392
+ }
2393
+ const consentedToolIds = new Set(entry.consent.selectedToolIds);
2394
+ return allCredentials.filter((cred) => {
2395
+ return Array.from(consentedToolIds).some((toolId) => toolId.startsWith(`${cred.appId}:`));
2396
+ });
2397
+ }
2398
+ async updateCredential(vaultId, appId, providerId, updates) {
2399
+ const entry = await this.get(vaultId);
2400
+ if (!entry) return;
2401
+ const key = this.credentialKey(appId, providerId);
2402
+ const credential = entry.appCredentials[key];
2403
+ if (!credential) return;
2404
+ Object.assign(credential, updates);
2405
+ entry.lastAccessAt = Date.now();
2406
+ await this.storage.set(this.key(vaultId), entry);
2407
+ }
2408
+ async shouldStoreCredential(vaultId, appId, toolIds) {
2409
+ const entry = await this.get(vaultId);
2410
+ if (!entry) return false;
2411
+ const consent = entry.consent;
2412
+ if (!consent?.enabled) {
2413
+ return true;
2414
+ }
2415
+ if (toolIds && toolIds.length > 0) {
2416
+ return toolIds.some((toolId) => consent.selectedToolIds.includes(toolId));
2417
+ }
2418
+ const consentedToolIds = consent.selectedToolIds;
2419
+ return consentedToolIds.some((toolId) => toolId.startsWith(`${appId}:`));
2420
+ }
2421
+ async invalidateCredential(vaultId, appId, providerId, reason) {
2422
+ await this.updateCredential(vaultId, appId, providerId, {
2423
+ isValid: false,
2424
+ invalidReason: reason
2425
+ });
2426
+ }
2427
+ async refreshOAuthCredential(vaultId, appId, providerId, tokens) {
2428
+ const entry = await this.get(vaultId);
2429
+ if (!entry) return;
2430
+ const key = this.credentialKey(appId, providerId);
2431
+ const credential = entry.appCredentials[key];
2432
+ if (!credential || credential.credential.type !== "oauth" && credential.credential.type !== "oauth_pkce") return;
2433
+ credential.credential.accessToken = tokens.accessToken;
2434
+ if (tokens.refreshToken !== void 0) {
2435
+ credential.credential.refreshToken = tokens.refreshToken;
2436
+ }
2437
+ if (tokens.expiresAt !== void 0) {
2438
+ credential.credential.expiresAt = tokens.expiresAt;
2439
+ credential.expiresAt = tokens.expiresAt;
2440
+ }
2441
+ credential.isValid = true;
2442
+ credential.invalidReason = void 0;
2443
+ entry.lastAccessAt = Date.now();
2444
+ await this.storage.set(this.key(vaultId), entry);
2445
+ }
2446
+ // ============================================
2447
+ // Cleanup
2448
+ // ============================================
2449
+ async cleanup() {
2450
+ const keys = await this.storage.keys("*");
2451
+ const now = Date.now();
2452
+ for (const key of keys) {
2453
+ const entry = await this.storage.get(key);
2454
+ if (!entry) continue;
2455
+ const originalLength = entry.pendingAuths.length;
2456
+ entry.pendingAuths = entry.pendingAuths.filter((p) => {
2457
+ if (now > p.expiresAt && p.status === "pending") {
2458
+ p.status = "expired";
2459
+ }
2460
+ return p.status === "pending";
2461
+ });
2462
+ if (entry.pendingAuths.length !== originalLength) {
2463
+ await this.storage.set(key, entry);
2464
+ }
2465
+ }
2466
+ }
2467
+ // ============================================
2468
+ // Helpers
2469
+ // ============================================
2470
+ /**
2471
+ * Build the storage key for a vault ID.
2472
+ * For non-namespaced storage, includes the namespace prefix.
2473
+ */
2474
+ key(id) {
2475
+ return this.isNamespacedStorage(this.storage.raw) ? id : `${this.namespace}:${id}`;
2476
+ }
2477
+ /**
2478
+ * Type guard to check if storage is a NamespacedStorage.
2479
+ */
2480
+ isNamespacedStorage(storage) {
2481
+ return "namespace" in storage && typeof storage.namespace === "function";
2482
+ }
2483
+ };
2484
+
2485
+ // libs/auth/src/session/storage/in-memory-authorization-vault.ts
2486
+ import { MemoryStorageAdapter } from "@frontmcp/utils";
2487
+ var InMemoryAuthorizationVault = class extends StorageAuthorizationVault {
2488
+ memoryAdapter;
2489
+ constructor(options = {}) {
2490
+ const memoryAdapter = new MemoryStorageAdapter();
2491
+ super(memoryAdapter, {
2492
+ namespace: options.namespace ?? "vault",
2493
+ pendingAuthTtlMs: options.pendingAuthTtlMs
2494
+ });
2495
+ this.memoryAdapter = memoryAdapter;
2496
+ void this.memoryAdapter.connect();
2497
+ }
2498
+ /**
2499
+ * Clear all stored data.
2500
+ * Useful for testing.
2501
+ */
2502
+ async clear() {
2503
+ const keys = await this.memoryAdapter.keys("*");
2504
+ for (const key of keys) {
2505
+ await this.memoryAdapter.delete(key);
2506
+ }
2507
+ }
2508
+ };
2509
+
2510
+ // libs/auth/src/authorization/authorization.types.ts
2511
+ import { z as z5 } from "zod";
2512
+ var authModeSchema = z5.enum(["public", "transparent", "orchestrated"]);
2513
+ var authUserSchema = z5.object({
2514
+ sub: z5.string(),
2515
+ name: z5.string().optional(),
2516
+ email: z5.string().email().optional(),
2517
+ picture: z5.string().url().optional(),
2518
+ anonymous: z5.boolean().optional()
2519
+ });
2520
+ var authorizedToolSchema = z5.object({
2521
+ executionPath: z5.tuple([z5.string(), z5.string()]),
2522
+ scopes: z5.array(z5.string()).optional(),
2523
+ details: z5.record(z5.string(), z5.unknown()).optional()
2524
+ });
2525
+ var authorizedPromptSchema = z5.object({
2526
+ executionPath: z5.tuple([z5.string(), z5.string()]),
2527
+ scopes: z5.array(z5.string()).optional(),
2528
+ details: z5.record(z5.string(), z5.unknown()).optional()
2529
+ });
2530
+ var llmSafeAuthContextSchema = z5.object({
2531
+ authorizationId: z5.string(),
2532
+ sessionId: z5.string(),
2533
+ mode: authModeSchema,
2534
+ isAnonymous: z5.boolean(),
2535
+ user: z5.object({
2536
+ sub: z5.string(),
2537
+ name: z5.string().optional()
2538
+ }),
2539
+ scopes: z5.array(z5.string()),
2540
+ authorizedToolIds: z5.array(z5.string()),
2541
+ authorizedPromptIds: z5.array(z5.string())
2542
+ });
2543
+ var AppAuthState = /* @__PURE__ */ ((AppAuthState2) => {
2544
+ AppAuthState2["AUTHORIZED"] = "authorized";
2545
+ AppAuthState2["SKIPPED"] = "skipped";
2546
+ AppAuthState2["PENDING"] = "pending";
2547
+ return AppAuthState2;
2548
+ })(AppAuthState || {});
2549
+ var appAuthStateSchema = z5.nativeEnum(AppAuthState);
2550
+ var appAuthorizationRecordSchema = z5.object({
2551
+ appId: z5.string(),
2552
+ state: appAuthStateSchema,
2553
+ stateChangedAt: z5.number(),
2554
+ grantedScopes: z5.array(z5.string()).optional(),
2555
+ authProviderId: z5.string().optional(),
2556
+ toolIds: z5.array(z5.string())
2557
+ });
2558
+ var progressiveAuthStateSchema = z5.object({
2559
+ apps: z5.record(z5.string(), appAuthorizationRecordSchema),
2560
+ initiallyAuthorized: z5.array(z5.string()),
2561
+ initiallySkipped: z5.array(z5.string())
2562
+ });
2563
+
2564
+ // libs/auth/src/utils/www-authenticate.utils.ts
2565
+ function buildWwwAuthenticate(options = {}) {
2566
+ const parts = ["Bearer"];
2567
+ const params = [];
2568
+ if (options.resourceMetadataUrl) {
2569
+ params.push(`resource_metadata="${escapeQuotedString(options.resourceMetadataUrl)}"`);
2570
+ }
2571
+ if (options.realm) {
2572
+ params.push(`realm="${escapeQuotedString(options.realm)}"`);
2573
+ }
2574
+ if (options.error) {
2575
+ params.push(`error="${options.error}"`);
2576
+ }
2577
+ if (options.errorDescription) {
2578
+ params.push(`error_description="${escapeQuotedString(options.errorDescription)}"`);
2579
+ }
2580
+ if (options.errorUri) {
2581
+ params.push(`error_uri="${escapeQuotedString(options.errorUri)}"`);
2582
+ }
2583
+ if (options.scope) {
2584
+ const scopeValue = Array.isArray(options.scope) ? options.scope.join(" ") : options.scope;
2585
+ params.push(`scope="${escapeQuotedString(scopeValue)}"`);
2586
+ }
2587
+ if (params.length > 0) {
2588
+ parts.push(params.join(", "));
2589
+ }
2590
+ return parts.join(" ");
2591
+ }
2592
+ function buildPrmUrl(baseUrl, entryPath, routeBase) {
2593
+ const normalizedEntry = normalizePathSegment(entryPath);
2594
+ const normalizedRoute = normalizePathSegment(routeBase);
2595
+ return `${baseUrl}/.well-known/oauth-protected-resource${normalizedEntry}${normalizedRoute}`;
2596
+ }
2597
+ function buildUnauthorizedHeader(prmUrl) {
2598
+ return buildWwwAuthenticate({
2599
+ resourceMetadataUrl: prmUrl
2600
+ });
2601
+ }
2602
+ function buildInvalidTokenHeader(prmUrl, description) {
2603
+ return buildWwwAuthenticate({
2604
+ resourceMetadataUrl: prmUrl,
2605
+ error: "invalid_token",
2606
+ errorDescription: description ?? "The access token is invalid or expired"
2607
+ });
2608
+ }
2609
+ function buildInsufficientScopeHeader(prmUrl, requiredScopes, description) {
2610
+ return buildWwwAuthenticate({
2611
+ resourceMetadataUrl: prmUrl,
2612
+ error: "insufficient_scope",
2613
+ scope: requiredScopes,
2614
+ errorDescription: description ?? "The request requires higher privileges"
2615
+ });
2616
+ }
2617
+ function buildInvalidRequestHeader(prmUrl, description) {
2618
+ return buildWwwAuthenticate({
2619
+ resourceMetadataUrl: prmUrl,
2620
+ error: "invalid_request",
2621
+ errorDescription: description ?? "The request is missing required parameters"
2622
+ });
2623
+ }
2624
+ function parseWwwAuthenticate(header) {
2625
+ const result = {};
2626
+ if (!header.toLowerCase().startsWith("bearer")) {
2627
+ return result;
2628
+ }
2629
+ const paramString = header.substring(6).trim();
2630
+ const paramRegex = /(\w+)="([^"\\]*(?:\\.[^"\\]*)*)"/g;
2631
+ let match;
2632
+ while ((match = paramRegex.exec(paramString)) !== null) {
2633
+ const [, key, value] = match;
2634
+ const unescapedValue = unescapeQuotedString(value);
2635
+ switch (key.toLowerCase()) {
2636
+ case "resource_metadata":
2637
+ result.resourceMetadataUrl = unescapedValue;
2638
+ break;
2639
+ case "realm":
2640
+ result.realm = unescapedValue;
2641
+ break;
2642
+ case "error":
2643
+ result.error = unescapedValue;
2644
+ break;
2645
+ case "error_description":
2646
+ result.errorDescription = unescapedValue;
2647
+ break;
2648
+ case "error_uri":
2649
+ result.errorUri = unescapedValue;
2650
+ break;
2651
+ case "scope":
2652
+ result.scope = unescapedValue;
2653
+ break;
2654
+ }
2655
+ }
2656
+ return result;
2657
+ }
2658
+ function escapeQuotedString(value) {
2659
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2660
+ }
2661
+ function unescapeQuotedString(value) {
2662
+ return value.replace(/\\(.)/g, "$1");
2663
+ }
2664
+ function normalizePathSegment(path2) {
2665
+ if (!path2 || path2 === "/") return "";
2666
+ const normalized = path2.startsWith("/") ? path2 : `/${path2}`;
2667
+ return normalized.replace(/\/+$/, "");
2668
+ }
2669
+
2670
+ // libs/auth/src/utils/audience.validator.ts
2671
+ function validateAudience(tokenAudience, options) {
2672
+ const { expectedAudiences, allowNoAudience = false, caseSensitive = true, allowWildcards = false } = options;
2673
+ if (tokenAudience === void 0 || tokenAudience === null) {
2674
+ if (allowNoAudience) {
2675
+ return { valid: true };
2676
+ }
2677
+ return {
2678
+ valid: false,
2679
+ error: "Token is missing audience claim"
2680
+ };
2681
+ }
2682
+ if (expectedAudiences.length === 0) {
2683
+ return {
2684
+ valid: false,
2685
+ error: "No expected audiences configured - cannot validate token"
2686
+ };
2687
+ }
2688
+ const tokenAuds = Array.isArray(tokenAudience) ? tokenAudience : [tokenAudience];
2689
+ for (const tokenAud of tokenAuds) {
2690
+ for (const expectedAud of expectedAudiences) {
2691
+ if (matchesAudience(tokenAud, expectedAud, caseSensitive, allowWildcards)) {
2692
+ return { valid: true, matchedAudience: tokenAud };
2693
+ }
2694
+ }
2695
+ }
2696
+ return {
2697
+ valid: false,
2698
+ error: `Token audience does not match expected audiences. Got: ${tokenAuds.join(
2699
+ ", "
2700
+ )}. Expected one of: ${expectedAudiences.join(", ")}`
2701
+ };
2702
+ }
2703
+ function matchesAudience(tokenAud, expectedAud, caseSensitive, allowWildcards) {
2704
+ if (caseSensitive) {
2705
+ if (tokenAud === expectedAud) return true;
2706
+ } else {
2707
+ if (tokenAud.toLowerCase() === expectedAud.toLowerCase()) return true;
2708
+ }
2709
+ if (allowWildcards && expectedAud.includes("*")) {
2710
+ const wildcardCount = (expectedAud.match(/\*/g) || []).length;
2711
+ if (wildcardCount > 2) {
2712
+ return false;
2713
+ }
2714
+ const pattern = expectedAud.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^.]*");
2715
+ const regex = new RegExp(`^${pattern}$`, caseSensitive ? "" : "i");
2716
+ if (regex.test(tokenAud)) return true;
2717
+ }
2718
+ return false;
2719
+ }
2720
+ function createAudienceValidator(options) {
2721
+ return (audience) => validateAudience(audience, options);
2722
+ }
2723
+ function deriveExpectedAudience(resourceUrl) {
2724
+ const audiences = [];
2725
+ try {
2726
+ const url = new URL(resourceUrl);
2727
+ audiences.push(resourceUrl.replace(/\/$/, ""));
2728
+ if (url.pathname !== "/" && url.pathname !== "") {
2729
+ audiences.push(url.origin);
2730
+ }
2731
+ audiences.push(url.host);
2732
+ } catch {
2733
+ audiences.push(resourceUrl);
2734
+ }
2735
+ return audiences;
2736
+ }
2737
+ var AudienceValidator = class _AudienceValidator {
2738
+ options;
2739
+ constructor(options = {}) {
2740
+ this.options = {
2741
+ expectedAudiences: [...options.expectedAudiences ?? []],
2742
+ allowNoAudience: options.allowNoAudience ?? false,
2743
+ caseSensitive: options.caseSensitive ?? true,
2744
+ allowWildcards: options.allowWildcards ?? false
2745
+ };
2746
+ }
2747
+ /**
2748
+ * Validate an audience claim
2749
+ */
2750
+ validate(audience) {
2751
+ return validateAudience(audience, this.options);
2752
+ }
2753
+ /**
2754
+ * Add expected audiences
2755
+ */
2756
+ addAudiences(...audiences) {
2757
+ this.options.expectedAudiences.push(...audiences);
2758
+ }
2759
+ /**
2760
+ * Set expected audiences (replace existing)
2761
+ */
2762
+ setAudiences(audiences) {
2763
+ this.options.expectedAudiences = audiences;
2764
+ }
2765
+ /**
2766
+ * Create validator from resource URL
2767
+ */
2768
+ static fromResourceUrl(resourceUrl, options = {}) {
2769
+ return new _AudienceValidator({
2770
+ ...options,
2771
+ expectedAudiences: deriveExpectedAudience(resourceUrl)
2772
+ });
2773
+ }
2774
+ };
2775
+
2776
+ // libs/auth/src/vault/auth-providers.types.ts
2777
+ import { z as z6 } from "zod";
2778
+ var credentialScopeSchema = z6.enum(["global", "user", "session"]);
2779
+ var loadingStrategySchema = z6.enum(["eager", "lazy"]);
2780
+ var getCredentialOptionsSchema = z6.object({
2781
+ forceRefresh: z6.boolean().optional(),
2782
+ scopes: z6.array(z6.string()).optional(),
2783
+ timeout: z6.number().positive().optional()
2784
+ }).strict();
2785
+ var credentialProviderConfigSchema = z6.object({
2786
+ name: z6.string().min(1),
2787
+ description: z6.string().optional(),
2788
+ scope: credentialScopeSchema,
2789
+ loading: loadingStrategySchema,
2790
+ cacheTtl: z6.number().nonnegative().optional(),
2791
+ // Functions validated at runtime, not via Zod (Zod 4 compatibility)
2792
+ factory: z6.any(),
2793
+ refresh: z6.any().optional(),
2794
+ toHeaders: z6.any().optional(),
2795
+ metadata: z6.record(z6.string(), z6.unknown()).optional(),
2796
+ required: z6.boolean().optional()
2797
+ }).strict();
2798
+ var authProviderMappingSchema = z6.union([
2799
+ z6.string(),
2800
+ z6.object({
2801
+ name: z6.string().min(1),
2802
+ required: z6.boolean().optional().default(true),
2803
+ scopes: z6.array(z6.string()).optional(),
2804
+ alias: z6.string().optional()
2805
+ }).strict()
2806
+ ]);
2807
+ var authProvidersVaultOptionsSchema = z6.object({
2808
+ enabled: z6.boolean().optional(),
2809
+ useSharedStorage: z6.boolean().optional().default(true),
2810
+ namespace: z6.string().optional().default("authproviders:"),
2811
+ defaultCacheTtl: z6.number().nonnegative().optional().default(36e5),
2812
+ maxCredentialsPerSession: z6.number().positive().optional().default(100),
2813
+ providers: z6.array(credentialProviderConfigSchema).optional()
2814
+ }).strict();
2815
+
2816
+ // libs/auth/src/vault/credential-helpers.ts
2817
+ function extractCredentialExpiry(credential) {
2818
+ switch (credential.type) {
2819
+ case "oauth":
2820
+ case "oauth_pkce":
2821
+ case "bearer":
2822
+ case "service_account":
2823
+ return credential.expiresAt;
2824
+ default:
2825
+ return void 0;
2826
+ }
2827
+ }
2828
+
2829
+ // libs/auth/src/vault/credential-cache.ts
2830
+ var CredentialCache = class {
2831
+ cache = /* @__PURE__ */ new Map();
2832
+ maxSize;
2833
+ stats = { hits: 0, misses: 0, evictions: 0, size: 0 };
2834
+ constructor(maxSize = 100) {
2835
+ this.maxSize = maxSize;
2836
+ }
2837
+ /**
2838
+ * Get a cached credential
2839
+ *
2840
+ * @param providerId - Provider name
2841
+ * @returns Resolved credential or undefined if not cached or expired
2842
+ */
2843
+ get(providerId) {
2844
+ const entry = this.cache.get(providerId);
2845
+ if (!entry) {
2846
+ this.stats.misses++;
2847
+ return void 0;
2848
+ }
2849
+ if (this.isExpired(entry)) {
2850
+ this.cache.delete(providerId);
2851
+ this.stats.size = this.cache.size;
2852
+ this.stats.misses++;
2853
+ this.stats.evictions++;
2854
+ return void 0;
2855
+ }
2856
+ if (entry.resolved.expiresAt && Date.now() >= entry.resolved.expiresAt) {
2857
+ this.cache.delete(providerId);
2858
+ this.stats.size = this.cache.size;
2859
+ this.stats.misses++;
2860
+ this.stats.evictions++;
2861
+ return void 0;
2862
+ }
2863
+ this.stats.hits++;
2864
+ return entry.resolved;
2865
+ }
2866
+ /**
2867
+ * Cache a resolved credential
2868
+ *
2869
+ * @param providerId - Provider name
2870
+ * @param resolved - Resolved credential to cache
2871
+ * @param ttl - TTL in milliseconds (0 = no TTL, rely on credential expiry)
2872
+ */
2873
+ set(providerId, resolved, ttl = 0) {
2874
+ if (this.cache.size >= this.maxSize && !this.cache.has(providerId)) {
2875
+ this.evictOldest();
2876
+ }
2877
+ const entry = {
2878
+ resolved,
2879
+ cachedAt: Date.now(),
2880
+ ttl
2881
+ };
2882
+ this.cache.set(providerId, entry);
2883
+ this.stats.size = this.cache.size;
2884
+ }
2885
+ /**
2886
+ * Check if a credential is cached and valid
2887
+ *
2888
+ * @param providerId - Provider name
2889
+ * @returns true if cached and not expired
2890
+ */
2891
+ has(providerId) {
2892
+ const entry = this.cache.get(providerId);
2893
+ if (!entry) return false;
2894
+ if (this.isExpired(entry)) {
2895
+ this.cache.delete(providerId);
2896
+ this.stats.size = this.cache.size;
2897
+ this.stats.evictions++;
2898
+ return false;
2899
+ }
2900
+ return true;
2901
+ }
2902
+ /**
2903
+ * Invalidate (remove) a cached credential
2904
+ *
2905
+ * @param providerId - Provider name to invalidate
2906
+ * @returns true if credential was removed
2907
+ */
2908
+ invalidate(providerId) {
2909
+ const deleted = this.cache.delete(providerId);
2910
+ if (deleted) {
2911
+ this.stats.size = this.cache.size;
2912
+ }
2913
+ return deleted;
2914
+ }
2915
+ /**
2916
+ * Invalidate all cached credentials
2917
+ */
2918
+ invalidateAll() {
2919
+ this.cache.clear();
2920
+ this.stats.size = 0;
2921
+ }
2922
+ /**
2923
+ * Invalidate credentials by scope
2924
+ *
2925
+ * @param scope - Credential scope to invalidate
2926
+ */
2927
+ invalidateByScope(scope) {
2928
+ for (const [key, entry] of this.cache) {
2929
+ if (entry.resolved.scope === scope) {
2930
+ this.cache.delete(key);
2931
+ }
2932
+ }
2933
+ this.stats.size = this.cache.size;
2934
+ }
2935
+ /**
2936
+ * Get all cached provider IDs
2937
+ */
2938
+ keys() {
2939
+ this.cleanup();
2940
+ return [...this.cache.keys()];
2941
+ }
2942
+ /**
2943
+ * Get cache size
2944
+ */
2945
+ get size() {
2946
+ return this.cache.size;
2947
+ }
2948
+ /**
2949
+ * Get cache statistics
2950
+ */
2951
+ getStats() {
2952
+ return { ...this.stats };
2953
+ }
2954
+ /**
2955
+ * Reset cache statistics
2956
+ */
2957
+ resetStats() {
2958
+ this.stats = { hits: 0, misses: 0, evictions: 0, size: this.cache.size };
2959
+ }
2960
+ /**
2961
+ * Clean up expired entries
2962
+ */
2963
+ cleanup() {
2964
+ const now = Date.now();
2965
+ for (const [key, entry] of this.cache) {
2966
+ if (this.isExpiredAt(entry, now)) {
2967
+ this.cache.delete(key);
2968
+ this.stats.evictions++;
2969
+ }
2970
+ }
2971
+ this.stats.size = this.cache.size;
2972
+ }
2973
+ /**
2974
+ * Check if entry is expired based on TTL
2975
+ */
2976
+ isExpired(entry) {
2977
+ return this.isExpiredAt(entry, Date.now());
2978
+ }
2979
+ /**
2980
+ * Check if entry is expired at a given timestamp
2981
+ */
2982
+ isExpiredAt(entry, now) {
2983
+ if (entry.ttl > 0 && now - entry.cachedAt >= entry.ttl) {
2984
+ return true;
2985
+ }
2986
+ if (entry.resolved.expiresAt && now >= entry.resolved.expiresAt) {
2987
+ return true;
2988
+ }
2989
+ if (!entry.resolved.isValid) {
2990
+ return true;
2991
+ }
2992
+ return false;
2993
+ }
2994
+ /**
2995
+ * Evict the oldest entry from cache
2996
+ */
2997
+ evictOldest() {
2998
+ let oldestKey;
2999
+ let oldestTime = Infinity;
3000
+ for (const [key, entry] of this.cache) {
3001
+ if (entry.cachedAt < oldestTime) {
3002
+ oldestTime = entry.cachedAt;
3003
+ oldestKey = key;
3004
+ }
3005
+ }
3006
+ if (oldestKey) {
3007
+ this.cache.delete(oldestKey);
3008
+ this.stats.evictions++;
3009
+ this.stats.size = this.cache.size;
3010
+ }
3011
+ }
3012
+ };
3013
+
3014
+ // libs/auth/src/cimd/cimd.logger.ts
3015
+ var noopLogger = {
3016
+ child: () => noopLogger,
3017
+ debug: () => {
3018
+ },
3019
+ info: () => {
3020
+ },
3021
+ warn: () => {
3022
+ },
3023
+ error: () => {
3024
+ }
3025
+ };
3026
+
3027
+ // libs/auth/src/cimd/cimd.types.ts
3028
+ import { z as z7 } from "zod";
3029
+ var clientMetadataDocumentSchema = z7.object({
3030
+ // REQUIRED per CIMD spec
3031
+ /**
3032
+ * Client identifier - MUST match the URL from which this document was fetched.
3033
+ */
3034
+ client_id: z7.string().url(),
3035
+ /**
3036
+ * Human-readable name of the client.
3037
+ */
3038
+ client_name: z7.string().min(1),
3039
+ /**
3040
+ * Array of redirect URIs for authorization responses.
3041
+ * At least one is required.
3042
+ */
3043
+ redirect_uris: z7.array(z7.string().url()).min(1),
3044
+ // OPTIONAL per RFC 7591
3045
+ /**
3046
+ * Token endpoint authentication method.
3047
+ * @default 'none'
3048
+ */
3049
+ token_endpoint_auth_method: z7.enum(["none", "client_secret_basic", "client_secret_post", "private_key_jwt"]).default("none"),
3050
+ /**
3051
+ * OAuth grant types the client can use.
3052
+ * @default ['authorization_code']
3053
+ */
3054
+ grant_types: z7.array(z7.string()).default(["authorization_code"]),
3055
+ /**
3056
+ * OAuth response types the client can request.
3057
+ * @default ['code']
3058
+ */
3059
+ response_types: z7.array(z7.string()).default(["code"]),
3060
+ /**
3061
+ * URL of the client's home page.
3062
+ */
3063
+ client_uri: z7.string().url().optional(),
3064
+ /**
3065
+ * URL of the client's logo image.
3066
+ */
3067
+ logo_uri: z7.string().url().optional(),
3068
+ /**
3069
+ * URL of the client's JWKS (for private_key_jwt).
3070
+ */
3071
+ jwks_uri: z7.string().url().optional(),
3072
+ /**
3073
+ * Inline JWKS (for private_key_jwt).
3074
+ */
3075
+ jwks: z7.object({
3076
+ keys: z7.array(z7.record(z7.string(), z7.unknown()))
3077
+ }).optional(),
3078
+ /**
3079
+ * URL of the client's terms of service.
3080
+ */
3081
+ tos_uri: z7.string().url().optional(),
3082
+ /**
3083
+ * URL of the client's privacy policy.
3084
+ */
3085
+ policy_uri: z7.string().url().optional(),
3086
+ /**
3087
+ * Requested OAuth scopes.
3088
+ */
3089
+ scope: z7.string().optional(),
3090
+ /**
3091
+ * Array of contact emails for the client.
3092
+ */
3093
+ contacts: z7.array(z7.string().email()).optional(),
3094
+ /**
3095
+ * Software statement (signed JWT).
3096
+ */
3097
+ software_statement: z7.string().optional(),
3098
+ /**
3099
+ * Unique identifier for the client software.
3100
+ */
3101
+ software_id: z7.string().optional(),
3102
+ /**
3103
+ * Version of the client software.
3104
+ */
3105
+ software_version: z7.string().optional()
3106
+ });
3107
+ var cimdRedisCacheConfigSchema = z7.object({
3108
+ /**
3109
+ * Redis connection URL.
3110
+ * e.g., "redis://user:pass@host:6379/0"
3111
+ */
3112
+ url: z7.string().optional(),
3113
+ /**
3114
+ * Redis host.
3115
+ */
3116
+ host: z7.string().optional(),
3117
+ /**
3118
+ * Redis port.
3119
+ * @default 6379
3120
+ */
3121
+ port: z7.number().optional(),
3122
+ /**
3123
+ * Redis password.
3124
+ */
3125
+ password: z7.string().optional(),
3126
+ /**
3127
+ * Redis database number.
3128
+ * @default 0
3129
+ */
3130
+ db: z7.number().optional(),
3131
+ /**
3132
+ * Enable TLS for Redis connection.
3133
+ * @default false
3134
+ */
3135
+ tls: z7.boolean().optional(),
3136
+ /**
3137
+ * Key prefix for CIMD cache entries.
3138
+ * @default 'cimd:'
3139
+ */
3140
+ keyPrefix: z7.string().default("cimd:")
3141
+ });
3142
+ var cimdCacheConfigSchema = z7.object({
3143
+ /**
3144
+ * Cache storage type.
3145
+ * - 'memory': In-memory cache (default, suitable for dev/single-instance)
3146
+ * - 'redis': Redis-backed cache (for production/distributed deployments)
3147
+ * @default 'memory'
3148
+ */
3149
+ type: z7.enum(["memory", "redis"]).default("memory"),
3150
+ /**
3151
+ * Default TTL for cached metadata documents.
3152
+ * @default 3600000 (1 hour)
3153
+ */
3154
+ defaultTtlMs: z7.number().min(0).default(36e5),
3155
+ /**
3156
+ * Maximum TTL (even if server suggests longer).
3157
+ * @default 86400000 (24 hours)
3158
+ */
3159
+ maxTtlMs: z7.number().min(0).default(864e5),
3160
+ /**
3161
+ * Minimum TTL (even if server suggests shorter).
3162
+ * @default 60000 (1 minute)
3163
+ */
3164
+ minTtlMs: z7.number().min(0).default(6e4),
3165
+ /**
3166
+ * Redis configuration (required when type is 'redis').
3167
+ */
3168
+ redis: cimdRedisCacheConfigSchema.optional()
3169
+ });
3170
+ var cimdSecurityConfigSchema = z7.object({
3171
+ /**
3172
+ * Block fetching from private/internal IP addresses (SSRF protection).
3173
+ * @default true
3174
+ */
3175
+ blockPrivateIPs: z7.boolean().default(true),
3176
+ /**
3177
+ * Explicit list of allowed domains.
3178
+ * If set, only these domains can host CIMD documents.
3179
+ */
3180
+ allowedDomains: z7.array(z7.string()).optional(),
3181
+ /**
3182
+ * Explicit list of blocked domains.
3183
+ * These domains cannot host CIMD documents.
3184
+ */
3185
+ blockedDomains: z7.array(z7.string()).optional(),
3186
+ /**
3187
+ * Warn when a client has only localhost redirect URIs.
3188
+ * @default true
3189
+ */
3190
+ warnOnLocalhostRedirects: z7.boolean().default(true),
3191
+ /**
3192
+ * Allow HTTP (instead of HTTPS) for localhost CIMD URLs.
3193
+ *
3194
+ * **WARNING: This is for testing purposes only. Never enable in production!**
3195
+ *
3196
+ * When enabled, permits HTTP URLs for localhost CIMD client IDs during testing.
3197
+ * The CIMD spec requires HTTPS, but e2e test servers typically use HTTP.
3198
+ *
3199
+ * @default false
3200
+ */
3201
+ allowInsecureForTesting: z7.boolean().default(false)
3202
+ });
3203
+ var cimdNetworkConfigSchema = z7.object({
3204
+ /**
3205
+ * Request timeout in milliseconds.
3206
+ * @default 5000 (5 seconds)
3207
+ */
3208
+ timeoutMs: z7.number().min(100).default(5e3),
3209
+ /**
3210
+ * Maximum response body size in bytes.
3211
+ * @default 65536 (64KB)
3212
+ */
3213
+ maxResponseSizeBytes: z7.number().min(1024).default(65536),
3214
+ /**
3215
+ * Redirect handling policy for CIMD fetches.
3216
+ * - 'deny': reject redirects (default, safest)
3217
+ * - 'same-origin': allow redirects only to the same origin
3218
+ * - 'allow': allow redirects to any origin
3219
+ * @default 'deny'
3220
+ */
3221
+ redirectPolicy: z7.enum(["deny", "same-origin", "allow"]).default("deny"),
3222
+ /**
3223
+ * Maximum number of redirects to follow when redirects are allowed.
3224
+ * @default 5
3225
+ */
3226
+ maxRedirects: z7.number().int().min(0).default(5)
3227
+ });
3228
+ var cimdConfigSchema = z7.object({
3229
+ /**
3230
+ * Enable CIMD support.
3231
+ * @default true
3232
+ */
3233
+ enabled: z7.boolean().default(true),
3234
+ /**
3235
+ * Cache configuration.
3236
+ */
3237
+ cache: cimdCacheConfigSchema.optional(),
3238
+ /**
3239
+ * Security configuration.
3240
+ */
3241
+ security: cimdSecurityConfigSchema.optional(),
3242
+ /**
3243
+ * Network configuration.
3244
+ */
3245
+ network: cimdNetworkConfigSchema.optional()
3246
+ });
3247
+
3248
+ // libs/auth/src/cimd/cimd.errors.ts
3249
+ var CimdError = class extends Error {
3250
+ /**
3251
+ * Error code for machine-readable identification.
3252
+ */
3253
+ code;
3254
+ /**
3255
+ * HTTP status code to return when this error occurs.
3256
+ */
3257
+ statusCode;
3258
+ /**
3259
+ * The client_id URL that caused the error.
3260
+ */
3261
+ clientIdUrl;
3262
+ constructor(message, code, statusCode, clientIdUrl) {
3263
+ super(message);
3264
+ this.name = this.constructor.name;
3265
+ this.code = code;
3266
+ this.statusCode = statusCode;
3267
+ this.clientIdUrl = clientIdUrl;
3268
+ if (Error.captureStackTrace) {
3269
+ Error.captureStackTrace(this, this.constructor);
3270
+ }
3271
+ }
3272
+ /**
3273
+ * Get a message safe to return to clients.
3274
+ * Override in subclasses to provide user-friendly messages.
3275
+ */
3276
+ getPublicMessage() {
3277
+ return this.message;
3278
+ }
3279
+ };
3280
+ var InvalidClientIdUrlError = class extends CimdError {
3281
+ reason;
3282
+ constructor(clientIdUrl, reason) {
3283
+ super(`Invalid CIMD client_id URL: ${reason}`, "INVALID_CLIENT_ID_URL", 400, clientIdUrl);
3284
+ this.reason = reason;
3285
+ }
3286
+ getPublicMessage() {
3287
+ return `Invalid client_id URL: ${this.reason}`;
3288
+ }
3289
+ };
3290
+ var CimdFetchError = class extends CimdError {
3291
+ httpStatus;
3292
+ originalError;
3293
+ constructor(clientIdUrl, message, options) {
3294
+ super(`Failed to fetch CIMD document from ${clientIdUrl}: ${message}`, "CIMD_FETCH_ERROR", 502, clientIdUrl);
3295
+ this.httpStatus = options?.httpStatus;
3296
+ this.originalError = options?.originalError;
3297
+ }
3298
+ getPublicMessage() {
3299
+ if (this.httpStatus) {
3300
+ return `Failed to fetch client metadata: HTTP ${this.httpStatus}`;
3301
+ }
3302
+ return "Failed to fetch client metadata document";
3303
+ }
3304
+ };
3305
+ var CimdValidationError = class extends CimdError {
3306
+ validationErrors;
3307
+ constructor(clientIdUrl, errors) {
3308
+ super(`CIMD document validation failed: ${errors.join("; ")}`, "CIMD_VALIDATION_ERROR", 400, clientIdUrl);
3309
+ this.validationErrors = errors;
3310
+ }
3311
+ getPublicMessage() {
3312
+ return `Client metadata document validation failed: ${this.validationErrors.join("; ")}`;
3313
+ }
3314
+ };
3315
+ var CimdClientIdMismatchError = class extends CimdError {
3316
+ documentClientId;
3317
+ constructor(urlClientId, documentClientId) {
3318
+ super(
3319
+ `CIMD client_id mismatch: URL is "${urlClientId}" but document contains "${documentClientId}"`,
3320
+ "CIMD_CLIENT_ID_MISMATCH",
3321
+ 400,
3322
+ urlClientId
3323
+ );
3324
+ this.documentClientId = documentClientId;
3325
+ }
3326
+ getPublicMessage() {
3327
+ return "Client ID in metadata document does not match the request";
3328
+ }
3329
+ };
3330
+ var CimdSecurityError = class extends CimdError {
3331
+ securityReason;
3332
+ constructor(clientIdUrl, reason) {
3333
+ super(`CIMD security check failed for ${clientIdUrl}: ${reason}`, "CIMD_SECURITY_ERROR", 403, clientIdUrl);
3334
+ this.securityReason = reason;
3335
+ }
3336
+ getPublicMessage() {
3337
+ return "Client ID URL is not allowed by security policy";
3338
+ }
3339
+ };
3340
+ var RedirectUriMismatchError = class extends CimdError {
3341
+ requestedRedirectUri;
3342
+ allowedRedirectUris;
3343
+ constructor(clientIdUrl, requestedRedirectUri, allowedRedirectUris) {
3344
+ super(
3345
+ `Redirect URI "${requestedRedirectUri}" is not registered for client "${clientIdUrl}"`,
3346
+ "REDIRECT_URI_MISMATCH",
3347
+ 400,
3348
+ clientIdUrl
3349
+ );
3350
+ this.requestedRedirectUri = requestedRedirectUri;
3351
+ this.allowedRedirectUris = allowedRedirectUris;
3352
+ }
3353
+ getPublicMessage() {
3354
+ return "The redirect_uri is not registered for this client";
3355
+ }
3356
+ };
3357
+ var CimdResponseTooLargeError = class extends CimdError {
3358
+ maxBytes;
3359
+ actualBytes;
3360
+ constructor(clientIdUrl, maxBytes, actualBytes) {
3361
+ super(
3362
+ `CIMD response from ${clientIdUrl} exceeds maximum size of ${maxBytes} bytes${actualBytes ? ` (received ${actualBytes} bytes)` : ""}`,
3363
+ "CIMD_RESPONSE_TOO_LARGE",
3364
+ 502,
3365
+ clientIdUrl
3366
+ );
3367
+ this.maxBytes = maxBytes;
3368
+ this.actualBytes = actualBytes;
3369
+ }
3370
+ getPublicMessage() {
3371
+ return "Client metadata document is too large";
3372
+ }
3373
+ };
3374
+ var CimdDisabledError = class extends CimdError {
3375
+ constructor() {
3376
+ super("CIMD (Client ID Metadata Documents) is disabled on this server", "CIMD_DISABLED", 400);
3377
+ }
3378
+ };
3379
+
3380
+ // libs/auth/src/cimd/cimd.validator.ts
3381
+ function isCimdClientId(clientId, allowInsecure = false) {
3382
+ if (!clientId || typeof clientId !== "string") {
3383
+ return false;
3384
+ }
3385
+ try {
3386
+ const url = new URL(clientId);
3387
+ if (url.protocol === "https:") {
3388
+ } else if (url.protocol === "http:" && allowInsecure && isLocalhostHost(url.hostname)) {
3389
+ } else {
3390
+ return false;
3391
+ }
3392
+ if (!url.pathname || url.pathname === "/") {
3393
+ return false;
3394
+ }
3395
+ return true;
3396
+ } catch {
3397
+ return false;
3398
+ }
3399
+ }
3400
+ function isLocalhostHost(hostname) {
3401
+ const lower = hostname.toLowerCase();
3402
+ return lower === "localhost" || lower === "127.0.0.1" || lower === "[::1]" || lower.endsWith(".localhost");
3403
+ }
3404
+ function validateClientIdUrl(clientId, securityConfig) {
3405
+ if (!clientId || typeof clientId !== "string") {
3406
+ throw new InvalidClientIdUrlError(clientId || "", "client_id must be a non-empty string");
3407
+ }
3408
+ let url;
3409
+ try {
3410
+ url = new URL(clientId);
3411
+ } catch {
3412
+ throw new InvalidClientIdUrlError(clientId, "Invalid URL format");
3413
+ }
3414
+ const allowInsecure = securityConfig?.allowInsecureForTesting ?? false;
3415
+ if (url.protocol === "https:") {
3416
+ } else if (url.protocol === "http:" && allowInsecure && isLocalhostHost(url.hostname)) {
3417
+ } else {
3418
+ throw new InvalidClientIdUrlError(clientId, `CIMD requires HTTPS, got ${url.protocol.replace(":", "")}`);
3419
+ }
3420
+ if (!url.pathname || url.pathname === "/") {
3421
+ throw new InvalidClientIdUrlError(
3422
+ clientId,
3423
+ "CIMD client_id URL must have a path component (e.g., /oauth/client-metadata.json)"
3424
+ );
3425
+ }
3426
+ const config = {
3427
+ blockPrivateIPs: securityConfig?.blockPrivateIPs ?? true,
3428
+ allowedDomains: securityConfig?.allowedDomains,
3429
+ blockedDomains: securityConfig?.blockedDomains
3430
+ };
3431
+ if (config.allowedDomains?.length) {
3432
+ if (!isDomainInList(url.hostname, config.allowedDomains)) {
3433
+ throw new CimdSecurityError(clientId, `Domain "${url.hostname}" is not in the allowed domains list`);
3434
+ }
3435
+ }
3436
+ if (config.blockedDomains?.length) {
3437
+ if (isDomainInList(url.hostname, config.blockedDomains)) {
3438
+ throw new CimdSecurityError(clientId, `Domain "${url.hostname}" is blocked`);
3439
+ }
3440
+ }
3441
+ if (config.blockPrivateIPs && !allowInsecure) {
3442
+ const ssrfCheck = checkSsrfProtection(url.hostname);
3443
+ if (!ssrfCheck.allowed) {
3444
+ throw new CimdSecurityError(clientId, ssrfCheck.reason);
3445
+ }
3446
+ }
3447
+ return url;
3448
+ }
3449
+ function checkSsrfProtection(hostname) {
3450
+ const lowercaseHostname = hostname.toLowerCase();
3451
+ if (lowercaseHostname === "localhost" || lowercaseHostname === "localhost.localdomain" || lowercaseHostname.endsWith(".localhost")) {
3452
+ return { allowed: false, reason: "Localhost addresses are not allowed" };
3453
+ }
3454
+ if (isIpAddress(hostname)) {
3455
+ const ipCheck = checkIpAddress(hostname);
3456
+ if (!ipCheck.allowed) {
3457
+ return ipCheck;
3458
+ }
3459
+ }
3460
+ return { allowed: true, reason: "" };
3461
+ }
3462
+ function isIpAddress(hostname) {
3463
+ const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
3464
+ if (ipv4Pattern.test(hostname)) {
3465
+ return true;
3466
+ }
3467
+ const cleanHostname = hostname.replace(/^\[|\]$/g, "");
3468
+ if (cleanHostname.includes(":")) {
3469
+ return true;
3470
+ }
3471
+ return false;
3472
+ }
3473
+ function checkIpAddress(ip) {
3474
+ const cleanIp = ip.replace(/^\[|\]$/g, "");
3475
+ if (cleanIp.includes(".") && !cleanIp.includes(":")) {
3476
+ return checkIpv4(cleanIp);
3477
+ }
3478
+ return checkIpv6(cleanIp);
3479
+ }
3480
+ function checkIpv4(ip) {
3481
+ const parts = ip.split(".").map(Number);
3482
+ if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255)) {
3483
+ return { allowed: false, reason: "Invalid IPv4 address" };
3484
+ }
3485
+ const [a, b, c, d] = parts;
3486
+ if (a === 127) {
3487
+ return { allowed: false, reason: "Loopback addresses (127.x.x.x) are not allowed" };
3488
+ }
3489
+ if (a === 10) {
3490
+ return { allowed: false, reason: "Private IP addresses (10.x.x.x) are not allowed" };
3491
+ }
3492
+ if (a === 172 && b >= 16 && b <= 31) {
3493
+ return { allowed: false, reason: "Private IP addresses (172.16-31.x.x) are not allowed" };
3494
+ }
3495
+ if (a === 192 && b === 168) {
3496
+ return { allowed: false, reason: "Private IP addresses (192.168.x.x) are not allowed" };
3497
+ }
3498
+ if (a === 169 && b === 254) {
3499
+ return { allowed: false, reason: "Link-local addresses (169.254.x.x) are not allowed" };
3500
+ }
3501
+ if (a === 0) {
3502
+ return { allowed: false, reason: "Current network addresses (0.x.x.x) are not allowed" };
3503
+ }
3504
+ if (a === 255 && b === 255 && c === 255 && d === 255) {
3505
+ return { allowed: false, reason: "Broadcast address is not allowed" };
3506
+ }
3507
+ if (a >= 224 && a <= 239) {
3508
+ return { allowed: false, reason: "Multicast addresses are not allowed" };
3509
+ }
3510
+ return { allowed: true, reason: "" };
3511
+ }
3512
+ function checkIpv6(ip) {
3513
+ const normalizedIp = ip.toLowerCase();
3514
+ if (normalizedIp === "::1") {
3515
+ return { allowed: false, reason: "IPv6 loopback address (::1) is not allowed" };
3516
+ }
3517
+ if (normalizedIp === "::" || normalizedIp === "0:0:0:0:0:0:0:0") {
3518
+ return { allowed: false, reason: "IPv6 unspecified address (::) is not allowed" };
3519
+ }
3520
+ if (normalizedIp.startsWith("fe8") || normalizedIp.startsWith("fe9") || normalizedIp.startsWith("fea") || normalizedIp.startsWith("feb")) {
3521
+ return { allowed: false, reason: "IPv6 link-local addresses (fe80::/10) are not allowed" };
3522
+ }
3523
+ if (normalizedIp.startsWith("fc") || normalizedIp.startsWith("fd")) {
3524
+ return { allowed: false, reason: "IPv6 unique local addresses (fc00::/7) are not allowed" };
3525
+ }
3526
+ const ipv4MappedMatch = normalizedIp.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
3527
+ if (ipv4MappedMatch) {
3528
+ return checkIpv4(ipv4MappedMatch[1]);
3529
+ }
3530
+ return { allowed: true, reason: "" };
3531
+ }
3532
+ function isDomainInList(hostname, domainList) {
3533
+ const lowerHostname = hostname.toLowerCase();
3534
+ for (const domain of domainList) {
3535
+ const lowerDomain = domain.toLowerCase();
3536
+ if (lowerHostname === lowerDomain) {
3537
+ return true;
3538
+ }
3539
+ if (lowerHostname.endsWith("." + lowerDomain)) {
3540
+ return true;
3541
+ }
3542
+ if (lowerDomain.startsWith("*.")) {
3543
+ const baseDomain = lowerDomain.slice(2);
3544
+ if (lowerHostname === baseDomain || lowerHostname.endsWith("." + baseDomain)) {
3545
+ return true;
3546
+ }
3547
+ }
3548
+ }
3549
+ return false;
3550
+ }
3551
+ function hasOnlyLocalhostRedirectUris(redirectUris) {
3552
+ if (!redirectUris.length) {
3553
+ return false;
3554
+ }
3555
+ return redirectUris.every((uri) => {
3556
+ try {
3557
+ const url = new URL(uri);
3558
+ const hostname = url.hostname.toLowerCase();
3559
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
3560
+ } catch {
3561
+ return false;
3562
+ }
3563
+ });
3564
+ }
3565
+
3566
+ // libs/auth/src/cimd/index.ts
3567
+ init_cimd_cache();
3568
+
3569
+ // libs/auth/src/cimd/cimd.service.ts
3570
+ init_cimd_cache();
3571
+ var CimdService = class {
3572
+ config;
3573
+ cacheConfig;
3574
+ securityConfig;
3575
+ networkConfig;
3576
+ cache;
3577
+ logger;
3578
+ /**
3579
+ * Whether CIMD is enabled.
3580
+ */
3581
+ get enabled() {
3582
+ return this.config.enabled;
3583
+ }
3584
+ /**
3585
+ * Create a new CIMD service.
3586
+ *
3587
+ * @param logger - Optional logger. If not provided, logging is disabled.
3588
+ * @param config - Optional configuration.
3589
+ */
3590
+ constructor(logger, config) {
3591
+ this.logger = (logger ?? noopLogger).child("CimdService");
3592
+ this.config = cimdConfigSchema.parse(config ?? {});
3593
+ this.cacheConfig = cimdCacheConfigSchema.parse(this.config.cache ?? {});
3594
+ this.securityConfig = cimdSecurityConfigSchema.parse(this.config.security ?? {});
3595
+ this.networkConfig = cimdNetworkConfigSchema.parse(this.config.network ?? {});
3596
+ this.cache = new CimdCache(this.cacheConfig);
3597
+ this.logger.debug("CimdService initialized", {
3598
+ enabled: this.config.enabled,
3599
+ cacheDefaultTtlMs: this.cacheConfig.defaultTtlMs,
3600
+ networkTimeoutMs: this.networkConfig.timeoutMs
3601
+ });
3602
+ if (this.securityConfig.allowInsecureForTesting) {
3603
+ this.logger.warn(
3604
+ "CIMD allowInsecureForTesting is enabled. HTTP is allowed for localhost CIMD URLs. This should NEVER be enabled in production!"
3605
+ );
3606
+ }
3607
+ }
3608
+ /**
3609
+ * Check if a client_id is a CIMD URL.
3610
+ *
3611
+ * @param clientId - The client_id to check
3612
+ * @returns true if this is a CIMD client_id (HTTPS URL with path, or HTTP for localhost when testing)
3613
+ */
3614
+ isCimdClientId(clientId) {
3615
+ return isCimdClientId(clientId, this.securityConfig.allowInsecureForTesting);
3616
+ }
3617
+ /**
3618
+ * Resolve a client_id to its metadata document.
3619
+ *
3620
+ * If the client_id is a CIMD URL, this fetches and validates the metadata document.
3621
+ * Non-CIMD client IDs return a result with isCimdClient: false.
3622
+ *
3623
+ * @param clientId - The client_id to resolve
3624
+ * @returns Resolution result with metadata if available
3625
+ */
3626
+ async resolveClientMetadata(clientId) {
3627
+ if (!this.isCimdClientId(clientId)) {
3628
+ return {
3629
+ isCimdClient: false,
3630
+ fromCache: false
3631
+ };
3632
+ }
3633
+ validateClientIdUrl(clientId, this.securityConfig);
3634
+ const cached = await this.cache.get(clientId);
3635
+ if (cached) {
3636
+ this.logger.debug(`Cache hit for CIMD client: ${clientId}`);
3637
+ return {
3638
+ isCimdClient: true,
3639
+ metadata: cached.document,
3640
+ fromCache: true,
3641
+ expiresAt: cached.expiresAt,
3642
+ etag: cached.etag,
3643
+ lastModified: cached.lastModified
3644
+ };
3645
+ }
3646
+ this.logger.info(`Fetching CIMD document: ${clientId}`);
3647
+ const { document, headers } = await this.fetchMetadataDocument(clientId);
3648
+ this.validateDocument(clientId, document);
3649
+ if (this.securityConfig.warnOnLocalhostRedirects && hasOnlyLocalhostRedirectUris(document.redirect_uris)) {
3650
+ this.logger.warn(`CIMD client "${clientId}" has only localhost redirect URIs - this may be a development client`);
3651
+ }
3652
+ await this.cache.set(clientId, document, headers);
3653
+ const entry = await this.cache.get(clientId);
3654
+ return {
3655
+ isCimdClient: true,
3656
+ metadata: document,
3657
+ fromCache: false,
3658
+ expiresAt: entry?.expiresAt,
3659
+ etag: entry?.etag,
3660
+ lastModified: entry?.lastModified
3661
+ };
3662
+ }
3663
+ /**
3664
+ * Validate that a redirect_uri is registered for the client.
3665
+ *
3666
+ * @param redirectUri - The redirect_uri from the authorization request
3667
+ * @param metadata - The client's metadata document
3668
+ * @throws RedirectUriMismatchError if the redirect_uri is not registered
3669
+ */
3670
+ validateRedirectUri(redirectUri, metadata) {
3671
+ const normalizedRequestUri = normalizeRedirectUri(redirectUri);
3672
+ const normalizedAllowed = metadata.redirect_uris.map(normalizeRedirectUri);
3673
+ if (!normalizedAllowed.includes(normalizedRequestUri)) {
3674
+ throw new RedirectUriMismatchError(metadata.client_id, redirectUri, metadata.redirect_uris);
3675
+ }
3676
+ }
3677
+ /**
3678
+ * Clear the cache for a specific client or all clients.
3679
+ *
3680
+ * @param clientId - Optional client_id to clear; clears all if not provided
3681
+ */
3682
+ async clearCache(clientId) {
3683
+ if (clientId) {
3684
+ await this.cache.delete(clientId);
3685
+ this.logger.debug(`Cache cleared for: ${clientId}`);
3686
+ } else {
3687
+ await this.cache.clear();
3688
+ this.logger.debug("Cache cleared");
3689
+ }
3690
+ }
3691
+ /**
3692
+ * Get cache statistics.
3693
+ */
3694
+ async getCacheStats() {
3695
+ return {
3696
+ size: await this.cache.size()
3697
+ };
3698
+ }
3699
+ /**
3700
+ * Fetch a metadata document from a CIMD URL.
3701
+ *
3702
+ * Following the JwksService.fetchJson() pattern.
3703
+ */
3704
+ async fetchMetadataDocument(clientId) {
3705
+ const controller = new AbortController();
3706
+ const timer = setTimeout(() => controller.abort(), this.networkConfig.timeoutMs);
3707
+ try {
3708
+ const conditionalHeaders = await this.cache.getConditionalHeaders(clientId);
3709
+ const originalOrigin = new URL(clientId).origin;
3710
+ const maxRedirects = this.networkConfig.maxRedirects;
3711
+ let currentUrl = clientId;
3712
+ let redirectCount = 0;
3713
+ let isFirstRequest = true;
3714
+ while (true) {
3715
+ const headers = {
3716
+ Accept: "application/json"
3717
+ };
3718
+ if (isFirstRequest && conditionalHeaders) {
3719
+ Object.assign(headers, conditionalHeaders);
3720
+ }
3721
+ const response = await fetch(currentUrl, {
3722
+ method: "GET",
3723
+ headers,
3724
+ signal: controller.signal,
3725
+ redirect: "manual"
3726
+ });
3727
+ if (response.status === 304) {
3728
+ const staleEntry = await this.cache.getStale(clientId);
3729
+ if (staleEntry) {
3730
+ await this.cache.revalidate(clientId, response.headers);
3731
+ this.logger.debug(`CIMD document not modified: ${clientId}`);
3732
+ return {
3733
+ document: staleEntry.document,
3734
+ headers: response.headers
3735
+ };
3736
+ }
3737
+ throw new CimdFetchError(clientId, "304 Not Modified but no cached entry", {
3738
+ httpStatus: 304
3739
+ });
3740
+ }
3741
+ if (response.status >= 300 && response.status < 400) {
3742
+ const location = response.headers.get("location");
3743
+ if (!location) {
3744
+ throw new CimdFetchError(clientId, "Redirect response missing Location header", {
3745
+ httpStatus: response.status
3746
+ });
3747
+ }
3748
+ const nextUrl = new URL(location, currentUrl).toString();
3749
+ const redirectPolicy = this.networkConfig.redirectPolicy;
3750
+ if (redirectPolicy === "deny") {
3751
+ throw new CimdFetchError(
3752
+ clientId,
3753
+ `CIMD fetch redirected to "${nextUrl}" but redirects are disabled. Host the metadata at the client_id URL or set auth.cimd.network.redirectPolicy to "same-origin" or "allow".`,
3754
+ { httpStatus: response.status }
3755
+ );
3756
+ }
3757
+ if (redirectPolicy === "same-origin") {
3758
+ const nextOrigin = new URL(nextUrl).origin;
3759
+ if (nextOrigin !== originalOrigin) {
3760
+ throw new CimdFetchError(
3761
+ clientId,
3762
+ `CIMD fetch redirected to "${nextUrl}" which is not the same origin as "${originalOrigin}". Host the metadata at the client_id URL or set auth.cimd.network.redirectPolicy to "allow".`,
3763
+ { httpStatus: response.status }
3764
+ );
3765
+ }
3766
+ }
3767
+ validateClientIdUrl(nextUrl, this.securityConfig);
3768
+ redirectCount += 1;
3769
+ if (redirectCount > maxRedirects) {
3770
+ throw new CimdFetchError(
3771
+ clientId,
3772
+ `CIMD fetch exceeded max redirects (${maxRedirects}). Host the metadata at the client_id URL or increase auth.cimd.network.maxRedirects.`
3773
+ );
3774
+ }
3775
+ this.logger.warn(`CIMD redirect ${redirectCount}/${maxRedirects}: ${currentUrl} -> ${nextUrl}`);
3776
+ currentUrl = nextUrl;
3777
+ isFirstRequest = false;
3778
+ continue;
3779
+ }
3780
+ if (!response.ok) {
3781
+ throw new CimdFetchError(clientId, `HTTP ${response.status} ${response.statusText}`, {
3782
+ httpStatus: response.status
3783
+ });
3784
+ }
3785
+ const contentLength = response.headers.get("content-length");
3786
+ if (contentLength) {
3787
+ const length = parseInt(contentLength, 10);
3788
+ if (!isNaN(length) && length > this.networkConfig.maxResponseSizeBytes) {
3789
+ throw new CimdResponseTooLargeError(clientId, this.networkConfig.maxResponseSizeBytes, length);
3790
+ }
3791
+ }
3792
+ const text = await this.readResponseWithLimit(response, this.networkConfig.maxResponseSizeBytes, clientId);
3793
+ let document;
3794
+ try {
3795
+ document = JSON.parse(text);
3796
+ } catch (e) {
3797
+ throw new CimdFetchError(clientId, "Invalid JSON response", {
3798
+ originalError: e instanceof Error ? e : new Error(String(e))
3799
+ });
3800
+ }
3801
+ return { document, headers: response.headers };
3802
+ }
3803
+ } catch (error) {
3804
+ if (error instanceof CimdFetchError || error instanceof CimdResponseTooLargeError) {
3805
+ throw error;
3806
+ }
3807
+ if (error instanceof Error && error.name === "AbortError") {
3808
+ throw new CimdFetchError(clientId, "Request timeout", {
3809
+ originalError: error
3810
+ });
3811
+ }
3812
+ throw new CimdFetchError(clientId, error instanceof Error ? error.message : "Unknown error", {
3813
+ originalError: error instanceof Error ? error : void 0
3814
+ });
3815
+ } finally {
3816
+ clearTimeout(timer);
3817
+ }
3818
+ }
3819
+ /**
3820
+ * Read response body with size limit.
3821
+ */
3822
+ async readResponseWithLimit(response, maxBytes, clientId) {
3823
+ const reader = response.body?.getReader();
3824
+ if (!reader) {
3825
+ const buffer = await response.arrayBuffer();
3826
+ if (buffer.byteLength > maxBytes) {
3827
+ throw new CimdResponseTooLargeError(clientId, maxBytes, buffer.byteLength);
3828
+ }
3829
+ return new TextDecoder().decode(buffer);
3830
+ }
3831
+ const chunks = [];
3832
+ let totalBytes = 0;
3833
+ try {
3834
+ while (true) {
3835
+ const { done, value } = await reader.read();
3836
+ if (done) break;
3837
+ totalBytes += value.length;
3838
+ if (totalBytes > maxBytes) {
3839
+ throw new CimdResponseTooLargeError(clientId, maxBytes, totalBytes);
3840
+ }
3841
+ chunks.push(value);
3842
+ }
3843
+ } finally {
3844
+ reader.releaseLock();
3845
+ }
3846
+ const combined = new Uint8Array(totalBytes);
3847
+ let offset = 0;
3848
+ for (const chunk of chunks) {
3849
+ combined.set(chunk, offset);
3850
+ offset += chunk.length;
3851
+ }
3852
+ return new TextDecoder().decode(combined);
3853
+ }
3854
+ /**
3855
+ * Validate a fetched document against the schema.
3856
+ *
3857
+ * @param clientId - The URL from which the document was fetched
3858
+ * @param document - The document to validate
3859
+ */
3860
+ validateDocument(clientId, document) {
3861
+ const result = clientMetadataDocumentSchema.safeParse(document);
3862
+ if (!result.success) {
3863
+ const errors = result.error.issues.map((issue) => {
3864
+ const path2 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
3865
+ return `${path2}${issue.message}`;
3866
+ });
3867
+ throw new CimdValidationError(clientId, errors);
3868
+ }
3869
+ if (result.data.client_id !== clientId) {
3870
+ throw new CimdClientIdMismatchError(clientId, result.data.client_id);
3871
+ }
3872
+ }
3873
+ };
3874
+ function normalizeRedirectUri(uri) {
3875
+ try {
3876
+ const url = new URL(uri);
3877
+ let normalized = `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}`;
3878
+ normalized += url.pathname.replace(/\/+$/, "") || "/";
3879
+ if (url.search) normalized += url.search;
3880
+ return normalized;
3881
+ } catch {
3882
+ return uri;
3883
+ }
3884
+ }
3885
+ export {
3886
+ AppAuthState,
3887
+ AudienceValidator,
3888
+ CDN,
3889
+ CimdCache,
3890
+ CimdClientIdMismatchError,
3891
+ CimdDisabledError,
3892
+ CimdError,
3893
+ CimdFetchError,
3894
+ CimdResponseTooLargeError,
3895
+ CimdSecurityError,
3896
+ CimdService,
3897
+ CimdValidationError,
3898
+ CredentialCache,
3899
+ DEFAULT_THEME,
3900
+ EncryptedStorageError,
3901
+ EncryptedTypedStorage,
3902
+ InMemoryAuthorizationStore,
3903
+ InMemoryAuthorizationVault,
3904
+ InvalidClientIdUrlError,
3905
+ JwksService,
3906
+ RedirectUriMismatchError,
3907
+ RedisAuthorizationStore,
3908
+ StorageAuthorizationVault,
3909
+ StorageTokenStore,
3910
+ TinyTtlCache,
3911
+ TokenVault,
3912
+ TypedStorage3 as TypedStorage,
3913
+ VaultEncryption,
3914
+ apiKeyCredentialSchema,
3915
+ appAuthStateSchema,
3916
+ appAuthorizationRecordSchema,
3917
+ appCredentialSchema,
3918
+ authLayout,
3919
+ authModeSchema,
3920
+ authProviderMappingSchema,
3921
+ authProvidersVaultOptionsSchema,
3922
+ authUserSchema,
3923
+ authorizationCodeRecordSchema,
3924
+ authorizationVaultEntrySchema,
3925
+ authorizedPromptSchema,
3926
+ authorizedToolSchema,
3927
+ baseLayout,
3928
+ basicAuthCredentialSchema,
3929
+ bearerCredentialSchema,
3930
+ buildConsentPage,
3931
+ buildErrorPage,
3932
+ buildFederatedLoginPage,
3933
+ buildIncrementalAuthPage,
3934
+ buildInsufficientScopeHeader,
3935
+ buildInvalidRequestHeader,
3936
+ buildInvalidTokenHeader,
3937
+ buildLoginPage,
3938
+ buildPrmUrl,
3939
+ buildToolConsentPage,
3940
+ buildUnauthorizedHeader,
3941
+ buildWwwAuthenticate,
3942
+ centeredCardLayout,
3943
+ checkSsrfProtection,
3944
+ cimdCacheConfigSchema,
3945
+ cimdConfigSchema,
3946
+ cimdNetworkConfigSchema,
3947
+ cimdSecurityConfigSchema,
3948
+ clientMetadataDocumentSchema,
3949
+ createAudienceValidator,
3950
+ createLayout,
3951
+ credentialProviderConfigSchema,
3952
+ credentialSchema,
3953
+ credentialScopeSchema,
3954
+ credentialTypeSchema,
3955
+ customCredentialSchema,
3956
+ decodeJwtPayloadSafe,
3957
+ decryptAesGcm3 as decryptAesGcm,
3958
+ decryptValue,
3959
+ deleteDevKey,
3960
+ deriveExpectedAudience,
3961
+ encryptAesGcm3 as encryptAesGcm,
3962
+ encryptValue,
3963
+ encryptedDataSchema,
3964
+ encryptedVaultEntrySchema,
3965
+ escapeHtml2 as escapeHtml,
3966
+ extraWideLayout,
3967
+ extractCacheHeaders,
3968
+ extractCredentialExpiry,
3969
+ generatePkceChallenge,
3970
+ getCredentialOptionsSchema,
3971
+ hasOnlyLocalhostRedirectUris,
3972
+ hkdfSha2562 as hkdfSha256,
3973
+ isCimdClientId,
3974
+ isDevKeyPersistenceEnabled,
3975
+ llmSafeAuthContextSchema,
3976
+ loadDevKey,
3977
+ loadingStrategySchema,
3978
+ mtlsCredentialSchema,
3979
+ noopLogger,
3980
+ normalizeIssuer,
3981
+ oauthCredentialSchema,
3982
+ parseCacheHeaders,
3983
+ parseWwwAuthenticate,
3984
+ pendingIncrementalAuthSchema,
3985
+ pkceChallengeSchema,
3986
+ pkceOAuthCredentialSchema,
3987
+ privateKeyCredentialSchema,
3988
+ progressiveAuthStateSchema,
3989
+ renderToHtml,
3990
+ resolveKeyPath,
3991
+ saveDevKey,
3992
+ serviceAccountCredentialSchema,
3993
+ sshKeyCredentialSchema,
3994
+ trimSlash,
3995
+ validateAudience,
3996
+ validateClientIdUrl,
3997
+ vaultConsentRecordSchema,
3998
+ vaultFederatedRecordSchema,
3999
+ verifyPkce,
4000
+ wideLayout
4001
+ };