@frontmcp/auth 1.9.1 → 1.9.3

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 (37) hide show
  1. package/authorities/authorities.context.d.ts.map +1 -1
  2. package/authorities/authorities.evaluator.d.ts.map +1 -1
  3. package/authorization/orchestrated.authorization.d.ts +10 -2
  4. package/authorization/orchestrated.authorization.d.ts.map +1 -1
  5. package/cimd/cimd.service.d.ts +13 -1
  6. package/cimd/cimd.service.d.ts.map +1 -1
  7. package/context/frontmcp-auth-context.factory.d.ts +7 -5
  8. package/context/frontmcp-auth-context.factory.d.ts.map +1 -1
  9. package/context/frontmcp-auth-context.impl.d.ts +2 -1
  10. package/context/frontmcp-auth-context.impl.d.ts.map +1 -1
  11. package/detection/auth-provider-detection.d.ts +2 -0
  12. package/detection/auth-provider-detection.d.ts.map +1 -1
  13. package/esm/index.mjs +534 -116
  14. package/esm/package.json +5 -5
  15. package/fetch/fetch-credential-middleware.d.ts +11 -1
  16. package/fetch/fetch-credential-middleware.d.ts.map +1 -1
  17. package/fetch/index.d.ts +2 -2
  18. package/fetch/index.d.ts.map +1 -1
  19. package/index.d.ts +3 -3
  20. package/index.d.ts.map +1 -1
  21. package/index.js +534 -119
  22. package/jwks/jwks.service.d.ts +17 -1
  23. package/jwks/jwks.service.d.ts.map +1 -1
  24. package/package.json +5 -5
  25. package/session/authorization.store.d.ts +6 -0
  26. package/session/authorization.store.d.ts.map +1 -1
  27. package/session/orchestrated-token.crypto.d.ts +11 -0
  28. package/session/orchestrated-token.crypto.d.ts.map +1 -1
  29. package/session/orchestrated-token.store.d.ts +16 -1
  30. package/session/orchestrated-token.store.d.ts.map +1 -1
  31. package/session/storage-orchestrated-token.store.d.ts +10 -0
  32. package/session/storage-orchestrated-token.store.d.ts.map +1 -1
  33. package/session/utils/auth-token.utils.d.ts.map +1 -1
  34. package/utils/index.d.ts +1 -0
  35. package/utils/index.d.ts.map +1 -1
  36. package/utils/token-scopes.utils.d.ts +11 -0
  37. package/utils/token-scopes.utils.d.ts.map +1 -0
package/index.js CHANGED
@@ -25,6 +25,245 @@ var __copyProps = (to, from, except, desc) => {
25
25
  };
26
26
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
27
 
28
+ // libs/auth/src/cimd/cimd-redis.cache.ts
29
+ var cimd_redis_cache_exports = {};
30
+ __export(cimd_redis_cache_exports, {
31
+ RedisCimdCache: () => RedisCimdCache
32
+ });
33
+ var import_utils43, import_utils44, RedisCimdCache;
34
+ var init_cimd_redis_cache = __esm({
35
+ "libs/auth/src/cimd/cimd-redis.cache.ts"() {
36
+ "use strict";
37
+ import_utils43 = require("@frontmcp/utils");
38
+ import_utils44 = require("@frontmcp/utils");
39
+ init_cimd_cache();
40
+ RedisCimdCache = class {
41
+ redis;
42
+ keyPrefix;
43
+ config;
44
+ constructor(config) {
45
+ if (!config.redis) {
46
+ throw new Error("Redis configuration is required for RedisCimdCache");
47
+ }
48
+ this.config = {
49
+ defaultTtlMs: config.defaultTtlMs ?? 36e5,
50
+ maxTtlMs: config.maxTtlMs ?? 864e5,
51
+ minTtlMs: config.minTtlMs ?? 6e4
52
+ };
53
+ this.keyPrefix = config.redis.keyPrefix ?? "cimd:";
54
+ const redisOptions = {};
55
+ if (config.redis.url) {
56
+ redisOptions.url = config.redis.url;
57
+ } else if (config.redis.host) {
58
+ redisOptions.config = {
59
+ host: config.redis.host,
60
+ port: config.redis.port,
61
+ password: config.redis.password,
62
+ db: config.redis.db,
63
+ tls: config.redis.tls
64
+ };
65
+ }
66
+ this.redis = new import_utils43.RedisStorageAdapter(redisOptions);
67
+ }
68
+ /**
69
+ * Connect to Redis.
70
+ * Must be called before using any cache operations.
71
+ */
72
+ async connect() {
73
+ await this.redis.connect();
74
+ }
75
+ /**
76
+ * Generate a Redis key for a client ID.
77
+ * Uses SHA-256 hash to handle URLs with special characters.
78
+ */
79
+ cacheKey(clientId) {
80
+ const hash = (0, import_utils44.sha256Hex)(clientId);
81
+ return `${this.keyPrefix}${hash}`;
82
+ }
83
+ /**
84
+ * Get a cached entry by client_id.
85
+ *
86
+ * @param clientId - The client_id URL
87
+ * @returns The cached entry if valid, or undefined
88
+ */
89
+ async get(clientId) {
90
+ const key = this.cacheKey(clientId);
91
+ const data = await this.redis.get(key);
92
+ if (!data) {
93
+ return void 0;
94
+ }
95
+ try {
96
+ const entry = JSON.parse(data);
97
+ if (entry.expiresAt < Date.now()) {
98
+ return void 0;
99
+ }
100
+ return entry;
101
+ } catch {
102
+ await this.redis.delete(key);
103
+ return void 0;
104
+ }
105
+ }
106
+ /**
107
+ * Get a stale entry for conditional revalidation.
108
+ *
109
+ * @param clientId - The client_id URL
110
+ * @returns The stale entry (even if expired), or undefined if not cached
111
+ */
112
+ async getStale(clientId) {
113
+ const key = this.cacheKey(clientId);
114
+ const data = await this.redis.get(key);
115
+ if (!data) {
116
+ return void 0;
117
+ }
118
+ try {
119
+ return JSON.parse(data);
120
+ } catch {
121
+ return void 0;
122
+ }
123
+ }
124
+ /**
125
+ * Store a document in the cache.
126
+ *
127
+ * @param clientId - The client_id URL
128
+ * @param document - The metadata document
129
+ * @param headers - HTTP response headers
130
+ */
131
+ async set(clientId, document, headers) {
132
+ const cacheHeaders = extractCacheHeaders(headers);
133
+ const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
134
+ const now = Date.now();
135
+ const entry = {
136
+ document,
137
+ expiresAt: now + ttlMs,
138
+ etag,
139
+ lastModified,
140
+ cachedAt: now
141
+ };
142
+ const key = this.cacheKey(clientId);
143
+ const value = JSON.stringify(entry);
144
+ const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
145
+ await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
146
+ }
147
+ /**
148
+ * Update an existing cache entry (after 304 Not Modified).
149
+ *
150
+ * @param clientId - The client_id URL
151
+ * @param headers - New HTTP headers with updated cache directives
152
+ */
153
+ async revalidate(clientId, headers) {
154
+ const key = this.cacheKey(clientId);
155
+ const data = await this.redis.get(key);
156
+ if (!data) {
157
+ return false;
158
+ }
159
+ try {
160
+ const existing = JSON.parse(data);
161
+ const cacheHeaders = extractCacheHeaders(headers);
162
+ const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
163
+ existing.expiresAt = Date.now() + ttlMs;
164
+ if (etag) existing.etag = etag;
165
+ if (lastModified) existing.lastModified = lastModified;
166
+ const value = JSON.stringify(existing);
167
+ const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
168
+ await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
169
+ return true;
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+ /**
175
+ * Delete a cache entry.
176
+ *
177
+ * @param clientId - The client_id URL
178
+ * @returns true if an entry was deleted
179
+ */
180
+ async delete(clientId) {
181
+ const key = this.cacheKey(clientId);
182
+ return this.redis.delete(key);
183
+ }
184
+ /**
185
+ * Get conditional request headers for a cached entry.
186
+ *
187
+ * @param clientId - The client_id URL
188
+ * @returns Headers for conditional request, or undefined if not cached
189
+ */
190
+ async getConditionalHeaders(clientId) {
191
+ const key = this.cacheKey(clientId);
192
+ const data = await this.redis.get(key);
193
+ if (!data) {
194
+ return void 0;
195
+ }
196
+ try {
197
+ const entry = JSON.parse(data);
198
+ const headers = {};
199
+ if (entry.etag) {
200
+ headers["If-None-Match"] = entry.etag;
201
+ }
202
+ if (entry.lastModified) {
203
+ headers["If-Modified-Since"] = entry.lastModified;
204
+ }
205
+ return Object.keys(headers).length > 0 ? headers : void 0;
206
+ } catch {
207
+ return void 0;
208
+ }
209
+ }
210
+ /**
211
+ * Clear all cached entries.
212
+ * Uses Redis SCAN to find and delete all keys with our prefix.
213
+ */
214
+ async clear() {
215
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
216
+ if (keys.length > 0) {
217
+ await this.redis.mdelete(keys);
218
+ }
219
+ }
220
+ /**
221
+ * Get the number of cached entries.
222
+ * Uses Redis SCAN to count keys with our prefix.
223
+ */
224
+ async size() {
225
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
226
+ return keys.length;
227
+ }
228
+ /**
229
+ * Remove expired entries.
230
+ *
231
+ * Note: Redis handles expiration automatically via TTL.
232
+ * This method is primarily for explicit cleanup of entries that are
233
+ * well past their HTTP cache expiration but still within Redis TTL.
234
+ *
235
+ * @returns Number of entries removed
236
+ */
237
+ async cleanup() {
238
+ const now = Date.now();
239
+ let removed = 0;
240
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
241
+ for (const key of keys) {
242
+ const data = await this.redis.get(key);
243
+ if (!data) continue;
244
+ try {
245
+ const entry = JSON.parse(data);
246
+ if (entry.expiresAt + this.config.maxTtlMs * 2 < now) {
247
+ await this.redis.delete(key);
248
+ removed++;
249
+ }
250
+ } catch {
251
+ await this.redis.delete(key);
252
+ removed++;
253
+ }
254
+ }
255
+ return removed;
256
+ }
257
+ /**
258
+ * Close the Redis connection.
259
+ */
260
+ async close() {
261
+ await this.redis.disconnect();
262
+ }
263
+ };
264
+ }
265
+ });
266
+
28
267
  // libs/auth/src/cimd/cimd.cache.ts
29
268
  function extractCacheHeaders(headers) {
30
269
  return {
@@ -90,6 +329,19 @@ function parseCacheControlHeader(value) {
90
329
  }
91
330
  return result;
92
331
  }
332
+ async function createCimdCache(config) {
333
+ const cacheType = config?.type ?? "memory";
334
+ if (cacheType === "redis") {
335
+ if (!config?.redis) {
336
+ throw new Error('Redis configuration is required when cache type is "redis"');
337
+ }
338
+ const { RedisCimdCache: RedisCimdCache2 } = await Promise.resolve().then(() => (init_cimd_redis_cache(), cimd_redis_cache_exports));
339
+ const cache = new RedisCimdCache2(config);
340
+ await cache.connect();
341
+ return cache;
342
+ }
343
+ return new InMemoryCimdCache(config);
344
+ }
93
345
  var InMemoryCimdCache;
94
346
  var init_cimd_cache = __esm({
95
347
  "libs/auth/src/cimd/cimd.cache.ts"() {
@@ -500,6 +752,7 @@ __export(index_exports, {
500
752
  privateKeyCredentialSchema: () => privateKeyCredentialSchema,
501
753
  progressiveAuthStateSchema: () => progressiveAuthStateSchema,
502
754
  providerConfigSchema: () => providerConfigSchema,
755
+ providerHeadersCredentials: () => providerHeadersCredentials,
503
756
  publicAccessConfigSchema: () => publicAccessConfigSchema,
504
757
  publicAuthOptionsSchema: () => publicAuthOptionsSchema,
505
758
  queryApplier: () => queryApplier,
@@ -517,6 +770,7 @@ __export(index_exports, {
517
770
  resourceIdRefSchema: () => resourceIdRefSchema,
518
771
  resourceScopesFor: () => resourceScopesFor,
519
772
  safeDecrypt: () => safeDecrypt,
773
+ scopesFromClaims: () => scopesFromClaims,
520
774
  sealPendingLogin: () => sealPendingLogin,
521
775
  secureStoreConfigSchema: () => secureStoreConfigSchema,
522
776
  secureStoreCustomBackendSchema: () => secureStoreCustomBackendSchema,
@@ -960,6 +1214,7 @@ var TRANSPARENT_JWT_ALGS = [
960
1214
  ];
961
1215
  var MAX_JWKS_FETCH_BYTES = 1048576;
962
1216
  var MAX_JWKS_FETCH_HOPS = 3;
1217
+ var UNKNOWN_KID_REFETCH_COOLDOWN_MS = 6e4;
963
1218
  var MISSING_EXP_ERROR = 'missing required "exp" claim';
964
1219
  function isMissingExpError(error) {
965
1220
  if (typeof error !== "object" || error === null) return false;
@@ -980,6 +1235,8 @@ var JwksService = class {
980
1235
  orchestratorKey;
981
1236
  // Provider JWKS cache (providerId -> jwks + fetchedAt)
982
1237
  providerJwks = /* @__PURE__ */ new Map();
1238
+ // The last key refetch an unknown `kid` started, per provider (see refetchForUnknownKid)
1239
+ unknownKidRefetches = /* @__PURE__ */ new Map();
983
1240
  // Track if key has been initialized (for async loading)
984
1241
  keyInitialized = false;
985
1242
  // Promise guard to prevent concurrent key generation
@@ -1040,6 +1297,9 @@ var JwksService = class {
1040
1297
  let jwks;
1041
1298
  try {
1042
1299
  jwks = await this.getJwksForProvider(p);
1300
+ if (kid && !p.jwks?.keys?.length && !jwks?.keys?.some((key) => key.kid === kid)) {
1301
+ jwks = await this.refetchForUnknownKid(p) ?? jwks;
1302
+ }
1043
1303
  if (!jwks?.keys?.length) continue;
1044
1304
  const JWKS = (0, import_jose.createLocalJWKSet)(jwks);
1045
1305
  const issuerConstraint = this.issuerConstraintFor(p);
@@ -1207,7 +1467,8 @@ var JwksService = class {
1207
1467
  * 1) inline jwks (if provided) → cache & return
1208
1468
  * 2) cached & fresh (TTL) → return
1209
1469
  * 3) explicit jwksUri → fetch, cache, return
1210
- * 4) discover jwks_uri → the issuer's OAuth authorization server
1470
+ * 4) `<issuer>/.well-known/jwks.json` → fetch, cache, return
1471
+ * 5) discover jwks_uri → the issuer's OAuth authorization server
1211
1472
  * metadata (RFC 8414), else its OpenID configuration (OpenID Connect
1212
1473
  * Discovery 1.0 §4, where Google, Okta, Auth0 and Entra publish it),
1213
1474
  * then fetch jwks_uri, cache, return
@@ -1221,11 +1482,42 @@ var JwksService = class {
1221
1482
  if (cached && Date.now() - cached.fetchedAt < this.opts.providerJwksTtlMs) {
1222
1483
  return cached.jwks;
1223
1484
  }
1485
+ const fetched = await this.fetchProviderJwks(ref);
1486
+ if (fetched) return fetched;
1487
+ const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
1488
+ if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
1489
+ return cached.jwks;
1490
+ }
1491
+ return void 0;
1492
+ }
1493
+ /**
1494
+ * Fetch a provider's keys again for a token whose `kid` the cached keys don't have (a key the
1495
+ * provider started using since), ahead of the cache's TTL. At most one refetch per provider per
1496
+ * {@link UNKNOWN_KID_REFETCH_COOLDOWN_MS}: tokens that arrive meanwhile wait for the one in flight or
1497
+ * use its result, so a flood of tokens with made-up `kid`s can't hammer the provider. Only freshly
1498
+ * fetched keys are returned: a failed refetch never revives cached keys past the stale window.
1499
+ */
1500
+ refetchForUnknownKid(ref) {
1501
+ let refetch = this.unknownKidRefetches.get(ref.id);
1502
+ if (!refetch || Date.now() - refetch.startedAt >= UNKNOWN_KID_REFETCH_COOLDOWN_MS) {
1503
+ refetch = { startedAt: Date.now(), done: this.fetchProviderJwks(ref) };
1504
+ this.unknownKidRefetches.set(ref.id, refetch);
1505
+ }
1506
+ return refetch.done;
1507
+ }
1508
+ /**
1509
+ * Fetch a provider's keys, and cache them: from `jwksUri`, else the conventional location, else
1510
+ * the `jwks_uri` of the issuer's OAuth or OpenID metadata. Every URL goes through the same scheme
1511
+ * and SSRF checks (`fetchJson`), `jwks_uri` included.
1512
+ */
1513
+ async fetchProviderJwks(ref) {
1224
1514
  if (ref.jwksUri) {
1225
1515
  const fromUri = await this.tryFetchJwks(ref.id, ref.jwksUri);
1226
1516
  if (fromUri?.keys?.length) return fromUri;
1227
1517
  }
1228
1518
  const issuer = trimSlash(ref.issuerUrl);
1519
+ const fromWellKnown = await this.tryFetchJwks(ref.id, `${issuer}/.well-known/jwks.json`);
1520
+ if (fromWellKnown?.keys?.length) return fromWellKnown;
1229
1521
  for (const document of ["oauth-authorization-server", "openid-configuration"]) {
1230
1522
  const meta = await this.tryFetchAsMeta(`${issuer}/.well-known/${document}`);
1231
1523
  const uri = meta && typeof meta === "object" && meta["jwks_uri"] ? String(meta["jwks_uri"]) : void 0;
@@ -1234,10 +1526,6 @@ var JwksService = class {
1234
1526
  if (fromMeta?.keys?.length) return fromMeta;
1235
1527
  }
1236
1528
  }
1237
- const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
1238
- if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
1239
- return cached.jwks;
1240
- }
1241
1529
  return void 0;
1242
1530
  }
1243
1531
  // ===========================================================================
@@ -2204,7 +2492,8 @@ function buildRefreshTokenRecord(params) {
2204
2492
  customClaims: params.customClaims,
2205
2493
  federatedLoginUsed: params.federatedLoginUsed,
2206
2494
  selectedProviderIds: params.selectedProviderIds,
2207
- skippedProviderIds: params.skippedProviderIds
2495
+ skippedProviderIds: params.skippedProviderIds,
2496
+ providerTokensId: params.providerTokensId
2208
2497
  };
2209
2498
  }
2210
2499
  function verifyPkce(codeVerifier, challenge) {
@@ -4248,7 +4537,8 @@ function deriveTypedUser(claims) {
4248
4537
  ...claims,
4249
4538
  iss: extractClaimValue(claims, "iss", isString) ?? "",
4250
4539
  sid: extractClaimValue(claims, "sid", isString),
4251
- sub: extractClaimValue(claims, "sub", isString) ?? "",
4540
+ // RFC 9068 §2.2: a client-credentials token names the client as its subject; some IdPs only put it in client_id / azp
4541
+ sub: extractClaimValue(claims, "sub", isString) ?? extractClaimValue(claims, "client_id", isString) ?? extractClaimValue(claims, "azp", isString) ?? "",
4252
4542
  exp: extractClaimValue(claims, "exp", isNumber),
4253
4543
  iat: extractClaimValue(claims, "iat", isNumber),
4254
4544
  aud: extractClaimValue(claims, "aud", isStringOrStringArray),
@@ -5194,7 +5484,49 @@ var VercelKvSessionStore = class {
5194
5484
  };
5195
5485
 
5196
5486
  // libs/auth/src/session/orchestrated-token.store.ts
5487
+ var import_utils30 = require("@frontmcp/utils");
5488
+
5489
+ // libs/auth/src/session/orchestrated-token.crypto.ts
5197
5490
  var import_utils29 = require("@frontmcp/utils");
5491
+ function recordDropAt(record) {
5492
+ if (!record.refreshToken) return record.expiresAt;
5493
+ return Math.max(record.expiresAt ?? 0, record.updatedAt + REFRESH_TOKEN_TTL_MS);
5494
+ }
5495
+ function hasLiveAccessToken(record, refreshSkewMs = 0) {
5496
+ if (record.expiresAt === void 0) return true;
5497
+ return record.expiresAt - (record.refreshToken ? refreshSkewMs : 0) > Date.now();
5498
+ }
5499
+ var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
5500
+ function deriveKeyForRecord(masterKey, compositeKey, cache) {
5501
+ const cached = cache.get(compositeKey);
5502
+ if (cached) {
5503
+ return cached;
5504
+ }
5505
+ const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
5506
+ const derived = (0, import_utils29.hkdfSha256)(masterKey, HKDF_SALT, info, 32);
5507
+ cache.set(compositeKey, derived);
5508
+ return derived;
5509
+ }
5510
+ function encryptRecord(key, record) {
5511
+ const plaintext = JSON.stringify(record);
5512
+ const iv = (0, import_utils29.randomBytes)(12);
5513
+ const { ciphertext, tag } = (0, import_utils29.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
5514
+ return JSON.stringify({
5515
+ iv: Buffer.from(iv).toString("base64url"),
5516
+ tag: Buffer.from(tag).toString("base64url"),
5517
+ data: Buffer.from(ciphertext).toString("base64url")
5518
+ });
5519
+ }
5520
+ function decryptRecord(key, encrypted) {
5521
+ const { iv, tag, data } = JSON.parse(encrypted);
5522
+ const ivBytes = Buffer.from(iv, "base64url");
5523
+ const tagBytes = Buffer.from(tag, "base64url");
5524
+ const ciphertextBytes = Buffer.from(data, "base64url");
5525
+ const plaintext = (0, import_utils29.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
5526
+ return JSON.parse(new TextDecoder().decode(plaintext));
5527
+ }
5528
+
5529
+ // libs/auth/src/session/orchestrated-token.store.ts
5198
5530
  var InMemoryOrchestratedTokenStore = class {
5199
5531
  /** Token storage: Map<compositeKey, ProviderTokenRecord> */
5200
5532
  tokens = /* @__PURE__ */ new Map();
@@ -5206,9 +5538,12 @@ var InMemoryOrchestratedTokenStore = class {
5206
5538
  cleanupTimer;
5207
5539
  /** Default TTL for records */
5208
5540
  defaultTtlMs;
5541
+ /** How long before its expiry a refreshable access token stops being returned */
5542
+ refreshSkewMs;
5209
5543
  constructor(options = {}) {
5210
5544
  this.encryptionKey = options.encryptionKey;
5211
5545
  this.defaultTtlMs = options.defaultTtlMs;
5546
+ this.refreshSkewMs = options.refreshSkewMs ?? 0;
5212
5547
  const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
5213
5548
  this.cleanupTimer = setInterval(() => {
5214
5549
  void this.cleanup();
@@ -5236,7 +5571,7 @@ var InMemoryOrchestratedTokenStore = class {
5236
5571
  }
5237
5572
  const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
5238
5573
  const salt = new TextEncoder().encode("frontmcp-token-store");
5239
- const derivedKey = (0, import_utils29.hkdfSha256)(this.encryptionKey, salt, info, 32);
5574
+ const derivedKey = (0, import_utils30.hkdfSha256)(this.encryptionKey, salt, info, 32);
5240
5575
  this.derivedKeys.set(compositeKey, derivedKey);
5241
5576
  return derivedKey;
5242
5577
  }
@@ -5246,8 +5581,8 @@ var InMemoryOrchestratedTokenStore = class {
5246
5581
  async encryptRecord(compositeKey, record) {
5247
5582
  const key = await this.deriveKeyForRecord(compositeKey);
5248
5583
  const plaintext = JSON.stringify(record);
5249
- const iv = (0, import_utils29.randomBytes)(12);
5250
- const { ciphertext, tag } = (0, import_utils29.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
5584
+ const iv = (0, import_utils30.randomBytes)(12);
5585
+ const { ciphertext, tag } = (0, import_utils30.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
5251
5586
  return JSON.stringify({
5252
5587
  iv: Buffer.from(iv).toString("base64url"),
5253
5588
  tag: Buffer.from(tag).toString("base64url"),
@@ -5263,7 +5598,7 @@ var InMemoryOrchestratedTokenStore = class {
5263
5598
  const ivBytes = Buffer.from(iv, "base64url");
5264
5599
  const tagBytes = Buffer.from(tag, "base64url");
5265
5600
  const ciphertextBytes = Buffer.from(data, "base64url");
5266
- const plaintext = (0, import_utils29.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
5601
+ const plaintext = (0, import_utils30.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
5267
5602
  return JSON.parse(new TextDecoder().decode(plaintext));
5268
5603
  }
5269
5604
  /**
@@ -5289,18 +5624,20 @@ var InMemoryOrchestratedTokenStore = class {
5289
5624
  } else {
5290
5625
  record = stored;
5291
5626
  }
5292
- if (record.expiresAt && record.expiresAt < Date.now()) {
5627
+ const dropAt = recordDropAt(record);
5628
+ if (dropAt && dropAt < Date.now()) {
5293
5629
  this.tokens.delete(key);
5294
5630
  return null;
5295
5631
  }
5296
5632
  return record;
5297
5633
  }
5298
5634
  /**
5299
- * Retrieve decrypted access token for a provider
5635
+ * Retrieve decrypted access token for a provider, or null once it has expired (a record with a
5636
+ * refresh token keeps that, to renew it)
5300
5637
  */
5301
5638
  async getAccessToken(authorizationId, providerId) {
5302
5639
  const record = await this.getRecord(authorizationId, providerId);
5303
- return record?.accessToken ?? null;
5640
+ return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
5304
5641
  }
5305
5642
  /**
5306
5643
  * Retrieve decrypted refresh token for a provider
@@ -5393,7 +5730,8 @@ var InMemoryOrchestratedTokenStore = class {
5393
5730
  } else {
5394
5731
  record = stored;
5395
5732
  }
5396
- if (record?.expiresAt && record.expiresAt < now) {
5733
+ const dropAt = record ? recordDropAt(record) : void 0;
5734
+ if (dropAt && dropAt < now) {
5397
5735
  keysToDelete.push(key);
5398
5736
  }
5399
5737
  }
@@ -5433,74 +5771,48 @@ var InMemoryOrchestratedTokenStore = class {
5433
5771
  * @param toAuthId - Target authorization ID (e.g., "def456")
5434
5772
  */
5435
5773
  async migrateTokens(fromAuthId, toAuthId) {
5436
- const prefix = `${fromAuthId}:`;
5437
- const keysToMigrate = [];
5438
- for (const key of this.tokens.keys()) {
5439
- if (key.startsWith(prefix)) {
5440
- keysToMigrate.push(key);
5441
- }
5774
+ for (const oldKey of await this.copyRecords(fromAuthId, toAuthId)) {
5775
+ this.tokens.delete(oldKey);
5776
+ this.derivedKeys.delete(oldKey);
5442
5777
  }
5443
- for (const oldKey of keysToMigrate) {
5444
- const providerId = oldKey.slice(prefix.length);
5445
- const newKey = this.buildKey(toAuthId, providerId);
5778
+ }
5779
+ /**
5780
+ * Copy tokens from one authorization ID to another, keeping them under the source ID too.
5781
+ */
5782
+ async copyTokens(fromAuthId, toAuthId) {
5783
+ await this.copyRecords(fromAuthId, toAuthId);
5784
+ }
5785
+ /** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the keys copied. */
5786
+ async copyRecords(fromAuthId, toAuthId) {
5787
+ const prefix = `${fromAuthId}:`;
5788
+ const sourceKeys = [...this.tokens.keys()].filter((key) => key.startsWith(prefix));
5789
+ const copiedKeys = [];
5790
+ for (const oldKey of sourceKeys) {
5446
5791
  const stored = this.tokens.get(oldKey);
5447
- if (!stored) {
5448
- continue;
5449
- }
5792
+ if (!stored) continue;
5793
+ const newKey = this.buildKey(toAuthId, oldKey.slice(prefix.length));
5450
5794
  if (this.encryptionKey) {
5451
5795
  try {
5452
5796
  const record = await this.decryptRecord(oldKey, stored);
5453
- const encrypted = await this.encryptRecord(newKey, record);
5454
- this.tokens.set(newKey, encrypted);
5797
+ this.tokens.set(newKey, await this.encryptRecord(newKey, record));
5455
5798
  } catch {
5456
5799
  continue;
5457
5800
  }
5458
5801
  } else {
5459
5802
  this.tokens.set(newKey, stored);
5460
5803
  }
5461
- this.tokens.delete(oldKey);
5462
- this.derivedKeys.delete(oldKey);
5804
+ copiedKeys.push(oldKey);
5463
5805
  }
5806
+ return copiedKeys;
5464
5807
  }
5465
5808
  };
5466
5809
 
5467
- // libs/auth/src/session/orchestrated-token.crypto.ts
5468
- var import_utils30 = require("@frontmcp/utils");
5469
- var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
5470
- function deriveKeyForRecord(masterKey, compositeKey, cache) {
5471
- const cached = cache.get(compositeKey);
5472
- if (cached) {
5473
- return cached;
5474
- }
5475
- const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
5476
- const derived = (0, import_utils30.hkdfSha256)(masterKey, HKDF_SALT, info, 32);
5477
- cache.set(compositeKey, derived);
5478
- return derived;
5479
- }
5480
- function encryptRecord(key, record) {
5481
- const plaintext = JSON.stringify(record);
5482
- const iv = (0, import_utils30.randomBytes)(12);
5483
- const { ciphertext, tag } = (0, import_utils30.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
5484
- return JSON.stringify({
5485
- iv: Buffer.from(iv).toString("base64url"),
5486
- tag: Buffer.from(tag).toString("base64url"),
5487
- data: Buffer.from(ciphertext).toString("base64url")
5488
- });
5489
- }
5490
- function decryptRecord(key, encrypted) {
5491
- const { iv, tag, data } = JSON.parse(encrypted);
5492
- const ivBytes = Buffer.from(iv, "base64url");
5493
- const tagBytes = Buffer.from(tag, "base64url");
5494
- const ciphertextBytes = Buffer.from(data, "base64url");
5495
- const plaintext = (0, import_utils30.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
5496
- return JSON.parse(new TextDecoder().decode(plaintext));
5497
- }
5498
-
5499
5810
  // libs/auth/src/session/storage-orchestrated-token.store.ts
5500
5811
  var StorageOrchestratedTokenStore = class {
5501
5812
  storage;
5502
5813
  encryptionKey;
5503
5814
  defaultTtlMs;
5815
+ refreshSkewMs;
5504
5816
  namespace;
5505
5817
  storageIsNamespaced;
5506
5818
  /** HKDF-derived subkey cache, keyed by composite key. */
@@ -5508,6 +5820,7 @@ var StorageOrchestratedTokenStore = class {
5508
5820
  constructor(storage, options = {}) {
5509
5821
  this.encryptionKey = options.encryptionKey;
5510
5822
  this.defaultTtlMs = options.defaultTtlMs;
5823
+ this.refreshSkewMs = options.refreshSkewMs ?? 0;
5511
5824
  this.namespace = options.namespace ?? "otok";
5512
5825
  this.storageIsNamespaced = this.isNamespacedStorage(storage);
5513
5826
  this.storage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
@@ -5517,7 +5830,7 @@ var StorageOrchestratedTokenStore = class {
5517
5830
  // ============================================
5518
5831
  async getAccessToken(authorizationId, providerId) {
5519
5832
  const record = await this.getRecord(authorizationId, providerId);
5520
- return record?.accessToken ?? null;
5833
+ return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
5521
5834
  }
5522
5835
  async getRefreshToken(authorizationId, providerId) {
5523
5836
  const record = await this.getRecord(authorizationId, providerId);
@@ -5534,7 +5847,7 @@ var StorageOrchestratedTokenStore = class {
5534
5847
  updatedAt: now
5535
5848
  };
5536
5849
  const value = this.encryptionKey ? encryptRecord(this.keyFor(compositeKey), record) : JSON.stringify(record);
5537
- await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(record.expiresAt));
5850
+ await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(recordDropAt(record)));
5538
5851
  }
5539
5852
  async deleteTokens(authorizationId, providerId) {
5540
5853
  const compositeKey = this.compositeKey(authorizationId, providerId);
@@ -5560,12 +5873,37 @@ var StorageOrchestratedTokenStore = class {
5560
5873
  return providerIds;
5561
5874
  }
5562
5875
  async migrateTokens(fromAuthId, toAuthId) {
5876
+ for (const oldComposite of await this.copyRecords(fromAuthId, toAuthId)) {
5877
+ await this.storage.delete(this.storageKey(oldComposite));
5878
+ this.derivedKeys.delete(oldComposite);
5879
+ }
5880
+ }
5881
+ async copyTokens(fromAuthId, toAuthId) {
5882
+ await this.copyRecords(fromAuthId, toAuthId);
5883
+ }
5884
+ /**
5885
+ * Delete all tokens for an authorization id.
5886
+ */
5887
+ async deleteAllForAuthorization(authorizationId) {
5888
+ const prefix = `${authorizationId}:`;
5889
+ const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
5890
+ for (const compositeKey of compositeKeys) {
5891
+ if (!compositeKey.startsWith(prefix)) continue;
5892
+ await this.storage.delete(this.storageKey(compositeKey));
5893
+ this.derivedKeys.delete(compositeKey);
5894
+ }
5895
+ }
5896
+ // ============================================
5897
+ // Internals
5898
+ // ============================================
5899
+ /** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the composite keys copied. */
5900
+ async copyRecords(fromAuthId, toAuthId) {
5563
5901
  const prefix = `${fromAuthId}:`;
5564
5902
  const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
5903
+ const copiedKeys = [];
5565
5904
  for (const oldComposite of compositeKeys) {
5566
5905
  if (!oldComposite.startsWith(prefix)) continue;
5567
- const providerId = oldComposite.slice(prefix.length);
5568
- const newComposite = this.compositeKey(toAuthId, providerId);
5906
+ const newComposite = this.compositeKey(toAuthId, oldComposite.slice(prefix.length));
5569
5907
  const stored = await this.storage.get(this.storageKey(oldComposite));
5570
5908
  if (stored === null) continue;
5571
5909
  if (this.encryptionKey) {
@@ -5576,35 +5914,20 @@ var StorageOrchestratedTokenStore = class {
5576
5914
  continue;
5577
5915
  }
5578
5916
  const value = encryptRecord(this.keyFor(newComposite), record);
5579
- await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(record.expiresAt));
5917
+ await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(recordDropAt(record)));
5580
5918
  } else {
5581
- let expiresAt;
5919
+ let dropAt;
5582
5920
  try {
5583
- expiresAt = JSON.parse(stored).expiresAt;
5921
+ dropAt = recordDropAt(JSON.parse(stored));
5584
5922
  } catch {
5585
- expiresAt = void 0;
5923
+ dropAt = void 0;
5586
5924
  }
5587
- await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(expiresAt));
5925
+ await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(dropAt));
5588
5926
  }
5589
- await this.storage.delete(this.storageKey(oldComposite));
5590
- this.derivedKeys.delete(oldComposite);
5591
- }
5592
- }
5593
- /**
5594
- * Delete all tokens for an authorization id.
5595
- */
5596
- async deleteAllForAuthorization(authorizationId) {
5597
- const prefix = `${authorizationId}:`;
5598
- const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
5599
- for (const compositeKey of compositeKeys) {
5600
- if (!compositeKey.startsWith(prefix)) continue;
5601
- await this.storage.delete(this.storageKey(compositeKey));
5602
- this.derivedKeys.delete(compositeKey);
5927
+ copiedKeys.push(oldComposite);
5603
5928
  }
5929
+ return copiedKeys;
5604
5930
  }
5605
- // ============================================
5606
- // Internals
5607
- // ============================================
5608
5931
  async getRecord(authorizationId, providerId) {
5609
5932
  const compositeKey = this.compositeKey(authorizationId, providerId);
5610
5933
  const stored = await this.storage.get(this.storageKey(compositeKey));
@@ -5627,7 +5950,8 @@ var StorageOrchestratedTokenStore = class {
5627
5950
  return null;
5628
5951
  }
5629
5952
  }
5630
- if (record.expiresAt && record.expiresAt < Date.now()) {
5953
+ const dropAt = recordDropAt(record);
5954
+ if (dropAt && dropAt < Date.now()) {
5631
5955
  await this.storage.delete(this.storageKey(compositeKey));
5632
5956
  return null;
5633
5957
  }
@@ -7079,7 +7403,7 @@ var OrchestratedAuthorization = class _OrchestratedAuthorization extends Authori
7079
7403
  const result = await this.#onTokenRefresh(providerId, refreshToken);
7080
7404
  await this.#tokenStore.storeTokens(this.id, providerId, {
7081
7405
  accessToken: result.accessToken,
7082
- refreshToken: result.refreshToken,
7406
+ refreshToken: result.refreshToken ?? refreshToken,
7083
7407
  expiresAt: result.expiresIn ? Date.now() + result.expiresIn * 1e3 : void 0
7084
7408
  });
7085
7409
  const currentState = this.#providerStates.get(providerId);
@@ -7841,14 +8165,14 @@ var cimdConfigSchema = import_lazy_zod9.z.object({
7841
8165
  init_cimd_cache();
7842
8166
 
7843
8167
  // libs/auth/src/cimd/cimd.service.ts
7844
- var import_utils43 = require("@frontmcp/utils");
8168
+ var import_utils45 = require("@frontmcp/utils");
7845
8169
  init_cimd_cache();
7846
8170
  var CimdService = class {
7847
8171
  config;
7848
8172
  cacheConfig;
7849
8173
  securityConfig;
7850
8174
  networkConfig;
7851
- cache;
8175
+ cacheBackend;
7852
8176
  logger;
7853
8177
  /**
7854
8178
  * Whether CIMD is enabled.
@@ -7868,7 +8192,6 @@ var CimdService = class {
7868
8192
  this.cacheConfig = cimdCacheConfigSchema.parse(this.config.cache ?? {});
7869
8193
  this.securityConfig = cimdSecurityConfigSchema.parse(this.config.security ?? {});
7870
8194
  this.networkConfig = cimdNetworkConfigSchema.parse(this.config.network ?? {});
7871
- this.cache = new InMemoryCimdCache(this.cacheConfig);
7872
8195
  this.logger.debug("CimdService initialized", {
7873
8196
  enabled: this.config.enabled,
7874
8197
  cacheDefaultTtlMs: this.cacheConfig.defaultTtlMs,
@@ -7880,6 +8203,34 @@ var CimdService = class {
7880
8203
  );
7881
8204
  }
7882
8205
  }
8206
+ /**
8207
+ * Create the cache now (connecting to Redis with `cache.type: 'redis'`), so a misconfigured or
8208
+ * unreachable cache fails at startup rather than on the first CIMD client.
8209
+ */
8210
+ async initialize() {
8211
+ await this.cache();
8212
+ }
8213
+ /**
8214
+ * The cache `cache.type` selects, created once through the `createCimdCache` factory. A failed
8215
+ * creation is retried on the next use.
8216
+ */
8217
+ cache() {
8218
+ if (!this.cacheBackend) {
8219
+ this.cacheBackend = createCimdCache(this.cacheConfig).catch((error) => {
8220
+ this.cacheBackend = void 0;
8221
+ throw error;
8222
+ });
8223
+ }
8224
+ return this.cacheBackend;
8225
+ }
8226
+ /** Close the cache's connection (Redis with `cache.type: 'redis'`), if one was created. */
8227
+ async dispose() {
8228
+ const backend = this.cacheBackend;
8229
+ this.cacheBackend = void 0;
8230
+ if (!backend) return;
8231
+ const createdBackend = await backend.catch(() => void 0);
8232
+ await createdBackend?.close?.();
8233
+ }
7883
8234
  /**
7884
8235
  * Check if a client_id is a CIMD URL.
7885
8236
  *
@@ -7906,7 +8257,8 @@ var CimdService = class {
7906
8257
  };
7907
8258
  }
7908
8259
  validateClientIdUrl(clientId, this.securityConfig);
7909
- const cached = await this.cache.get(clientId);
8260
+ const cache = await this.cache();
8261
+ const cached = await cache.get(clientId);
7910
8262
  if (cached) {
7911
8263
  this.logger.debug(`Cache hit for CIMD client: ${clientId}`);
7912
8264
  return {
@@ -7924,8 +8276,8 @@ var CimdService = class {
7924
8276
  if (this.securityConfig.warnOnLocalhostRedirects && hasOnlyLocalhostRedirectUris(document.redirect_uris)) {
7925
8277
  this.logger.warn(`CIMD client "${clientId}" has only localhost redirect URIs - this may be a development client`);
7926
8278
  }
7927
- await this.cache.set(clientId, document, headers);
7928
- const entry = await this.cache.get(clientId);
8279
+ await cache.set(clientId, document, headers);
8280
+ const entry = await cache.get(clientId);
7929
8281
  return {
7930
8282
  isCimdClient: true,
7931
8283
  metadata: document,
@@ -7955,11 +8307,12 @@ var CimdService = class {
7955
8307
  * @param clientId - Optional client_id to clear; clears all if not provided
7956
8308
  */
7957
8309
  async clearCache(clientId) {
8310
+ const cache = await this.cache();
7958
8311
  if (clientId) {
7959
- await this.cache.delete(clientId);
8312
+ await cache.delete(clientId);
7960
8313
  this.logger.debug(`Cache cleared for: ${clientId}`);
7961
8314
  } else {
7962
- await this.cache.clear();
8315
+ await cache.clear();
7963
8316
  this.logger.debug("Cache cleared");
7964
8317
  }
7965
8318
  }
@@ -7967,8 +8320,9 @@ var CimdService = class {
7967
8320
  * Get cache statistics.
7968
8321
  */
7969
8322
  async getCacheStats() {
8323
+ const cache = await this.cache();
7970
8324
  return {
7971
- size: await this.cache.size()
8325
+ size: await cache.size()
7972
8326
  };
7973
8327
  }
7974
8328
  /**
@@ -7980,7 +8334,8 @@ var CimdService = class {
7980
8334
  const controller = new AbortController();
7981
8335
  const timer = setTimeout(() => controller.abort(), this.networkConfig.timeoutMs);
7982
8336
  try {
7983
- const conditionalHeaders = await this.cache.getConditionalHeaders(clientId);
8337
+ const cache = await this.cache();
8338
+ const conditionalHeaders = await cache.getConditionalHeaders(clientId);
7984
8339
  const originalOrigin = new URL(clientId).origin;
7985
8340
  const maxRedirects = this.networkConfig.maxRedirects;
7986
8341
  let currentUrl = clientId;
@@ -8003,9 +8358,9 @@ var CimdService = class {
8003
8358
  redirect: "manual"
8004
8359
  });
8005
8360
  if (response.status === 304) {
8006
- const staleEntry = await this.cache.getStale(clientId);
8361
+ const staleEntry = await cache.getStale(clientId);
8007
8362
  if (staleEntry) {
8008
- await this.cache.revalidate(clientId, response.headers);
8363
+ await cache.revalidate(clientId, response.headers);
8009
8364
  this.logger.debug(`CIMD document not modified: ${clientId}`);
8010
8365
  return {
8011
8366
  document: staleEntry.document,
@@ -8016,7 +8371,7 @@ var CimdService = class {
8016
8371
  httpStatus: 304
8017
8372
  });
8018
8373
  }
8019
- if ((0, import_utils43.isRedirectResponse)(response)) {
8374
+ if ((0, import_utils45.isRedirectResponse)(response)) {
8020
8375
  const location = response.headers.get("location");
8021
8376
  if (!location) {
8022
8377
  throw new CimdFetchError(clientId, "Redirect response missing Location header", {
@@ -8513,7 +8868,7 @@ var tokenRefreshConfigSchema = import_lazy_zod10.z.object({
8513
8868
  * Refresh token before expiry by this many seconds
8514
8869
  * @default 60
8515
8870
  */
8516
- skewSeconds: import_lazy_zod10.z.number().default(60)
8871
+ skewSeconds: import_lazy_zod10.z.number().nonnegative().default(60)
8517
8872
  });
8518
8873
  var skippedAppBehaviorSchema = import_lazy_zod10.z.enum(["anonymous", "require-auth"]);
8519
8874
  var consentConfigSchema = import_lazy_zod10.z.object({
@@ -8626,7 +8981,7 @@ var publicAuthOptionsSchema = import_lazy_zod11.z.object({
8626
8981
  * Anonymous session TTL in seconds
8627
8982
  * @default 3600 (1 hour)
8628
8983
  */
8629
- sessionTtl: import_lazy_zod11.z.number().default(3600),
8984
+ sessionTtl: import_lazy_zod11.z.number().int().positive().default(3600),
8630
8985
  /**
8631
8986
  * Scopes granted to anonymous sessions
8632
8987
  * @default ['anonymous']
@@ -9073,9 +9428,17 @@ function detectAuthProviders(parentAuth, apps) {
9073
9428
  `Invalid auth configuration: Parent uses transparent mode but apps have their own auth providers. Transparent mode passes tokens through without modification, which is incompatible with multi-provider setups. Change parent auth to local or remote mode to properly manage tokens for each provider. Detected providers: ${[...providers.keys()].join(", ")}`
9074
9429
  );
9075
9430
  }
9076
- if (uniqueProviderCount > 1 && parentAuth && isPublicMode(parentAuth)) {
9077
- warnings.push(
9078
- `Parent uses public mode but apps have auth providers configured. App-level auth will be used, but consider using local or remote mode at parent for unified auth management.`
9431
+ const sharedEndpointUnenforced = !parentAuth || isPublicMode(parentAuth) || isStaticMode(parentAuth);
9432
+ const unenforcedAppIds = apps.filter((app) => app.auth && !isPublicMode(app.auth) && !staticParentEnforces(parentAuth, app.auth)).map((app) => app.id);
9433
+ if (sharedEndpointUnenforced && unenforcedAppIds.length > 0) {
9434
+ validationErrors.push(
9435
+ `App-level auth is not enforced on the shared endpoint of a server in ${parentAuth?.mode ?? "public"} mode, so the tools of ${unenforcedAppIds.join(", ")} would be served without it. Serve the app on its own endpoint with standalone: true or splitByApp: true, or run the server in local or remote mode with incrementalAuth enabled, which checks each tool call against the apps the caller has authorized (without incrementalAuth, local and remote mode do not check app grants per tool call).`
9436
+ );
9437
+ }
9438
+ const tokenMintingAppIds = apps.filter((app) => app.remoteAuthMode === "forward" && !(parentAuth && isTransparentMode(parentAuth))).map((app) => app.id);
9439
+ if (tokenMintingAppIds.length > 0) {
9440
+ validationErrors.push(
9441
+ `remoteAuth: { mode: 'forward' } on ${tokenMintingAppIds.join(", ")} would send the remote a token this server minted or holds itself: only a server in transparent mode receives its callers' tokens from the identity provider. Use remoteAuth: { mode: 'static', credentials } (or transportOptions.headers) for the remote, or run the server in transparent mode.`
9079
9442
  );
9080
9443
  }
9081
9444
  return {
@@ -9088,6 +9451,14 @@ function detectAuthProviders(parentAuth, apps) {
9088
9451
  warnings
9089
9452
  };
9090
9453
  }
9454
+ function staticParentEnforces(parentAuth, appAuth) {
9455
+ if (!parentAuth || !isStaticMode(parentAuth) || !isStaticMode(appAuth)) return false;
9456
+ const parent = staticAuthOptionsSchema.safeParse(parentAuth);
9457
+ const app = staticAuthOptionsSchema.safeParse(appAuth);
9458
+ if (!parent.success || !app.success) return false;
9459
+ const sameChallenge = parent.data.header.toLowerCase() === app.data.header.toLowerCase() && parent.data.scheme === app.data.scheme;
9460
+ return sameChallenge && parent.data.tokens.every((token) => app.data.tokens.includes(token));
9461
+ }
9091
9462
  function getProviderUrl(options) {
9092
9463
  if (isTransparentMode(options)) {
9093
9464
  return options.provider;
@@ -9428,6 +9799,16 @@ function isLoopbackRedirectUri(uri) {
9428
9799
  return false;
9429
9800
  }
9430
9801
 
9802
+ // libs/auth/src/utils/token-scopes.utils.ts
9803
+ function scopesFromClaims(claims) {
9804
+ const scopes = [claims?.scope, claims?.scp].flatMap((claim) => {
9805
+ if (typeof claim === "string") return claim.split(/\s+/).filter(Boolean);
9806
+ if (Array.isArray(claim)) return claim.filter((scope) => typeof scope === "string");
9807
+ return [];
9808
+ });
9809
+ return [...new Set(scopes)];
9810
+ }
9811
+
9431
9812
  // libs/auth/src/vault/auth-providers.types.ts
9432
9813
  var import_lazy_zod19 = require("@frontmcp/lazy-zod");
9433
9814
  var credentialScopeSchema = import_lazy_zod19.z.enum(["global", "user", "session"]);
@@ -9670,7 +10051,7 @@ var CredentialCache = class {
9670
10051
  var AUTH_PROVIDERS_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:AUTH_PROVIDERS_ACCESSOR");
9671
10052
 
9672
10053
  // libs/auth/src/vault/auth-providers.accessor.impl.ts
9673
- var import_utils46 = require("@frontmcp/utils");
10054
+ var import_utils48 = require("@frontmcp/utils");
9674
10055
  var AuthProvidersAccessorImpl = class {
9675
10056
  constructor(registry, vault, cache, loader, context, logger) {
9676
10057
  this.registry = registry;
@@ -9885,7 +10266,7 @@ var AuthProvidersAccessorImpl = class {
9885
10266
  return { [credential.headerName]: credential.key };
9886
10267
  case "basic": {
9887
10268
  const value = `${credential.username}:${credential.password}`;
9888
- const encoded = credential.encodedValue ?? (0, import_utils46.base64Encode)(new TextEncoder().encode(value));
10269
+ const encoded = credential.encodedValue ?? (0, import_utils48.base64Encode)(new TextEncoder().encode(value));
9889
10270
  return { Authorization: `Basic ${encoded}` };
9890
10271
  }
9891
10272
  case "custom":
@@ -10445,6 +10826,7 @@ var LazyCredentialLoader = class {
10445
10826
  };
10446
10827
 
10447
10828
  // libs/auth/src/authorities/authorities.context.ts
10829
+ var import_utils49 = require("@frontmcp/utils");
10448
10830
  function resolveDotPath(obj, path) {
10449
10831
  if (path in obj) {
10450
10832
  return obj[path];
@@ -10485,6 +10867,10 @@ function signedInSubject(rawSub, mappedSub) {
10485
10867
  if (rawSub !== void 0 && isAnonymousSubject(rawSub)) return void 0;
10486
10868
  return [mappedSub, rawSub].find((candidate) => !isAnonymousSubject(candidate));
10487
10869
  }
10870
+ var runtimeEnv = new Proxy(
10871
+ {},
10872
+ { get: (_target, name) => typeof name === "string" ? (0, import_utils49.getEnv)(name) : void 0 }
10873
+ );
10488
10874
  var AuthoritiesContextBuilder = class {
10489
10875
  claimsMapping;
10490
10876
  claimsResolver;
@@ -10497,7 +10883,7 @@ var AuthoritiesContextBuilder = class {
10497
10883
  /**
10498
10884
  * Build an evaluation context from AuthInfo and tool input.
10499
10885
  */
10500
- build(authInfo, input = {}, env = {}) {
10886
+ build(authInfo, input = {}, env = runtimeEnv) {
10501
10887
  if (this.claimsResolver) {
10502
10888
  const resolved = this.claimsResolver(authInfo);
10503
10889
  return {
@@ -10567,9 +10953,9 @@ var FrontMcpAuthContextImpl = class {
10567
10953
  claims;
10568
10954
  roles;
10569
10955
  permissions;
10570
- constructor(source, claimsMapping) {
10956
+ constructor(source, claimsMapping, claimsResolver) {
10571
10957
  const rawUser = resolveAuthUser(source);
10572
- const mappedSub = claimsMapping?.userId ? resolveDotPath(this.buildRawClaims(source), claimsMapping.userId) : void 0;
10958
+ const mappedSub = !claimsResolver && claimsMapping?.userId ? resolveDotPath(this.buildRawClaims(source), claimsMapping.userId) : void 0;
10573
10959
  const signedInSub = signedInSubject(rawUser.sub, mappedSub);
10574
10960
  const sub = signedInSub ?? (typeof rawUser.sub === "string" ? rawUser.sub : "");
10575
10961
  this.user = Object.freeze({
@@ -10587,6 +10973,13 @@ var FrontMcpAuthContextImpl = class {
10587
10973
  this.scopes = Object.freeze(
10588
10974
  Array.isArray(source.scopes) ? source.scopes.filter((s) => typeof s === "string") : []
10589
10975
  );
10976
+ if (claimsResolver) {
10977
+ const resolved = claimsResolver(source);
10978
+ this.claims = Object.freeze({ ...resolved.claims });
10979
+ this.roles = Object.freeze([...toStringArray2(resolved.roles)]);
10980
+ this.permissions = Object.freeze([...toStringArray2(resolved.permissions)]);
10981
+ return;
10982
+ }
10590
10983
  const rawClaims = this.buildRawClaims(source);
10591
10984
  this.claims = Object.freeze({ ...rawClaims });
10592
10985
  if (claimsMapping?.roles) {
@@ -10647,8 +11040,8 @@ var FrontMcpAuthContextImpl = class {
10647
11040
  };
10648
11041
 
10649
11042
  // libs/auth/src/context/frontmcp-auth-context.factory.ts
10650
- function buildAuthContext(authInfo, claimsMapping, pipes) {
10651
- const base = new FrontMcpAuthContextImpl(authInfo, claimsMapping);
11043
+ function buildAuthContext(authInfo, claimsMapping, pipes, claimsResolver) {
11044
+ const base = new FrontMcpAuthContextImpl(authInfo, claimsMapping, claimsResolver);
10652
11045
  if (!pipes || pipes.length === 0) {
10653
11046
  return base;
10654
11047
  }
@@ -10900,6 +11293,10 @@ function evaluateCondition(condition, envelope, ctx) {
10900
11293
  const expected = resolveValue(condition.value, ctx);
10901
11294
  return applyOperator(actual, condition.op, expected);
10902
11295
  }
11296
+ var REDACTED_ENV_VALUE = "[redacted]";
11297
+ function reportableActual(path, actual) {
11298
+ return path === "env" || path.startsWith("env.") ? REDACTED_ENV_VALUE : actual;
11299
+ }
10903
11300
  function evaluateAbac(policy, ctx) {
10904
11301
  const envelope = buildContextEnvelope(ctx);
10905
11302
  if (policy.match) {
@@ -10907,10 +11304,11 @@ function evaluateAbac(policy, ctx) {
10907
11304
  const actual = resolveDotPath(envelope, path);
10908
11305
  const expected = resolveValue(expectedRaw, ctx);
10909
11306
  if (expected === void 0 || actual !== expected) {
11307
+ const reported = reportableActual(path, actual);
10910
11308
  return {
10911
11309
  granted: false,
10912
- deniedBy: `attributes.match: '${path}' expected '${String(expected)}' but got '${String(actual)}'`,
10913
- denial: { kind: "attributes", path: "attributes.match", expected, actual },
11310
+ deniedBy: `attributes.match: '${path}' expected '${String(expected)}' but got '${String(reported)}'`,
11311
+ denial: { kind: "attributes", path: "attributes.match", expected, actual: reported },
10914
11312
  evaluatedPolicies: ["attributes.match"]
10915
11313
  };
10916
11314
  }
@@ -10921,7 +11319,7 @@ function evaluateAbac(policy, ctx) {
10921
11319
  const condition = policy.conditions[i];
10922
11320
  if (!evaluateCondition(condition, envelope, ctx)) {
10923
11321
  const expected = resolveValue(condition.value, ctx);
10924
- const actual = resolveDotPath(envelope, condition.path);
11322
+ const actual = reportableActual(condition.path, resolveDotPath(envelope, condition.path));
10925
11323
  return {
10926
11324
  granted: false,
10927
11325
  deniedBy: `attributes.conditions: '${condition.path}' failed '${condition.op}' check against '${String(expected)}'`,
@@ -11660,6 +12058,21 @@ var basicApplier = {
11660
12058
  function isFrontMcpCredentials(creds) {
11661
12059
  return typeof creds === "object" && creds !== null && "provider" in creds && typeof creds.provider === "string" && creds.provider.length > 0;
11662
12060
  }
12061
+ function providerHeadersCredentials(resolveHeaders) {
12062
+ return {
12063
+ async applyCredentials(_url, init) {
12064
+ const { credentials, ...cleanInit } = init;
12065
+ if (!isFrontMcpCredentials(credentials)) {
12066
+ return { init: typeof credentials === "string" ? init : cleanInit };
12067
+ }
12068
+ const headers = new Headers(cleanInit.headers);
12069
+ for (const [name, value] of Object.entries(await resolveHeaders(credentials.provider))) {
12070
+ headers.set(name, value);
12071
+ }
12072
+ return { init: { ...cleanInit, headers } };
12073
+ }
12074
+ };
12075
+ }
11663
12076
  var FetchCredentialMiddleware = class {
11664
12077
  constructor(tokenAccessor, appliers) {
11665
12078
  this.tokenAccessor = tokenAccessor;
@@ -11961,6 +12374,7 @@ var FetchCredentialMiddleware = class {
11961
12374
  privateKeyCredentialSchema,
11962
12375
  progressiveAuthStateSchema,
11963
12376
  providerConfigSchema,
12377
+ providerHeadersCredentials,
11964
12378
  publicAccessConfigSchema,
11965
12379
  publicAuthOptionsSchema,
11966
12380
  queryApplier,
@@ -11978,6 +12392,7 @@ var FetchCredentialMiddleware = class {
11978
12392
  resourceIdRefSchema,
11979
12393
  resourceScopesFor,
11980
12394
  safeDecrypt,
12395
+ scopesFromClaims,
11981
12396
  sealPendingLogin,
11982
12397
  secureStoreConfigSchema,
11983
12398
  secureStoreCustomBackendSchema,