@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/esm/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
2
  const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
3
  // __frontmcpCreateRequire
4
+ var __defProp = Object.defineProperty;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
6
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
7
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -16,6 +17,249 @@ var __esm = (fn, res, err) => function __init() {
16
17
  throw err = [e], e;
17
18
  }
18
19
  };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, { get: all[name], enumerable: true });
23
+ };
24
+
25
+ // libs/auth/src/cimd/cimd-redis.cache.ts
26
+ var cimd_redis_cache_exports = {};
27
+ __export(cimd_redis_cache_exports, {
28
+ RedisCimdCache: () => RedisCimdCache
29
+ });
30
+ import { RedisStorageAdapter as RedisStorageAdapter3 } from "@frontmcp/utils";
31
+ import { sha256Hex as sha256Hex5 } from "@frontmcp/utils";
32
+ var RedisCimdCache;
33
+ var init_cimd_redis_cache = __esm({
34
+ "libs/auth/src/cimd/cimd-redis.cache.ts"() {
35
+ "use strict";
36
+ init_cimd_cache();
37
+ RedisCimdCache = class {
38
+ redis;
39
+ keyPrefix;
40
+ config;
41
+ constructor(config) {
42
+ if (!config.redis) {
43
+ throw new Error("Redis configuration is required for RedisCimdCache");
44
+ }
45
+ this.config = {
46
+ defaultTtlMs: config.defaultTtlMs ?? 36e5,
47
+ maxTtlMs: config.maxTtlMs ?? 864e5,
48
+ minTtlMs: config.minTtlMs ?? 6e4
49
+ };
50
+ this.keyPrefix = config.redis.keyPrefix ?? "cimd:";
51
+ const redisOptions = {};
52
+ if (config.redis.url) {
53
+ redisOptions.url = config.redis.url;
54
+ } else if (config.redis.host) {
55
+ redisOptions.config = {
56
+ host: config.redis.host,
57
+ port: config.redis.port,
58
+ password: config.redis.password,
59
+ db: config.redis.db,
60
+ tls: config.redis.tls
61
+ };
62
+ }
63
+ this.redis = new RedisStorageAdapter3(redisOptions);
64
+ }
65
+ /**
66
+ * Connect to Redis.
67
+ * Must be called before using any cache operations.
68
+ */
69
+ async connect() {
70
+ await this.redis.connect();
71
+ }
72
+ /**
73
+ * Generate a Redis key for a client ID.
74
+ * Uses SHA-256 hash to handle URLs with special characters.
75
+ */
76
+ cacheKey(clientId) {
77
+ const hash = sha256Hex5(clientId);
78
+ return `${this.keyPrefix}${hash}`;
79
+ }
80
+ /**
81
+ * Get a cached entry by client_id.
82
+ *
83
+ * @param clientId - The client_id URL
84
+ * @returns The cached entry if valid, or undefined
85
+ */
86
+ async get(clientId) {
87
+ const key = this.cacheKey(clientId);
88
+ const data = await this.redis.get(key);
89
+ if (!data) {
90
+ return void 0;
91
+ }
92
+ try {
93
+ const entry = JSON.parse(data);
94
+ if (entry.expiresAt < Date.now()) {
95
+ return void 0;
96
+ }
97
+ return entry;
98
+ } catch {
99
+ await this.redis.delete(key);
100
+ return void 0;
101
+ }
102
+ }
103
+ /**
104
+ * Get a stale entry for conditional revalidation.
105
+ *
106
+ * @param clientId - The client_id URL
107
+ * @returns The stale entry (even if expired), or undefined if not cached
108
+ */
109
+ async getStale(clientId) {
110
+ const key = this.cacheKey(clientId);
111
+ const data = await this.redis.get(key);
112
+ if (!data) {
113
+ return void 0;
114
+ }
115
+ try {
116
+ return JSON.parse(data);
117
+ } catch {
118
+ return void 0;
119
+ }
120
+ }
121
+ /**
122
+ * Store a document in the cache.
123
+ *
124
+ * @param clientId - The client_id URL
125
+ * @param document - The metadata document
126
+ * @param headers - HTTP response headers
127
+ */
128
+ async set(clientId, document, headers) {
129
+ const cacheHeaders = extractCacheHeaders(headers);
130
+ const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
131
+ const now = Date.now();
132
+ const entry = {
133
+ document,
134
+ expiresAt: now + ttlMs,
135
+ etag,
136
+ lastModified,
137
+ cachedAt: now
138
+ };
139
+ const key = this.cacheKey(clientId);
140
+ const value = JSON.stringify(entry);
141
+ const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
142
+ await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
143
+ }
144
+ /**
145
+ * Update an existing cache entry (after 304 Not Modified).
146
+ *
147
+ * @param clientId - The client_id URL
148
+ * @param headers - New HTTP headers with updated cache directives
149
+ */
150
+ async revalidate(clientId, headers) {
151
+ const key = this.cacheKey(clientId);
152
+ const data = await this.redis.get(key);
153
+ if (!data) {
154
+ return false;
155
+ }
156
+ try {
157
+ const existing = JSON.parse(data);
158
+ const cacheHeaders = extractCacheHeaders(headers);
159
+ const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
160
+ existing.expiresAt = Date.now() + ttlMs;
161
+ if (etag) existing.etag = etag;
162
+ if (lastModified) existing.lastModified = lastModified;
163
+ const value = JSON.stringify(existing);
164
+ const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
165
+ await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
166
+ return true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ /**
172
+ * Delete a cache entry.
173
+ *
174
+ * @param clientId - The client_id URL
175
+ * @returns true if an entry was deleted
176
+ */
177
+ async delete(clientId) {
178
+ const key = this.cacheKey(clientId);
179
+ return this.redis.delete(key);
180
+ }
181
+ /**
182
+ * Get conditional request headers for a cached entry.
183
+ *
184
+ * @param clientId - The client_id URL
185
+ * @returns Headers for conditional request, or undefined if not cached
186
+ */
187
+ async getConditionalHeaders(clientId) {
188
+ const key = this.cacheKey(clientId);
189
+ const data = await this.redis.get(key);
190
+ if (!data) {
191
+ return void 0;
192
+ }
193
+ try {
194
+ const entry = JSON.parse(data);
195
+ const headers = {};
196
+ if (entry.etag) {
197
+ headers["If-None-Match"] = entry.etag;
198
+ }
199
+ if (entry.lastModified) {
200
+ headers["If-Modified-Since"] = entry.lastModified;
201
+ }
202
+ return Object.keys(headers).length > 0 ? headers : void 0;
203
+ } catch {
204
+ return void 0;
205
+ }
206
+ }
207
+ /**
208
+ * Clear all cached entries.
209
+ * Uses Redis SCAN to find and delete all keys with our prefix.
210
+ */
211
+ async clear() {
212
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
213
+ if (keys.length > 0) {
214
+ await this.redis.mdelete(keys);
215
+ }
216
+ }
217
+ /**
218
+ * Get the number of cached entries.
219
+ * Uses Redis SCAN to count keys with our prefix.
220
+ */
221
+ async size() {
222
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
223
+ return keys.length;
224
+ }
225
+ /**
226
+ * Remove expired entries.
227
+ *
228
+ * Note: Redis handles expiration automatically via TTL.
229
+ * This method is primarily for explicit cleanup of entries that are
230
+ * well past their HTTP cache expiration but still within Redis TTL.
231
+ *
232
+ * @returns Number of entries removed
233
+ */
234
+ async cleanup() {
235
+ const now = Date.now();
236
+ let removed = 0;
237
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
238
+ for (const key of keys) {
239
+ const data = await this.redis.get(key);
240
+ if (!data) continue;
241
+ try {
242
+ const entry = JSON.parse(data);
243
+ if (entry.expiresAt + this.config.maxTtlMs * 2 < now) {
244
+ await this.redis.delete(key);
245
+ removed++;
246
+ }
247
+ } catch {
248
+ await this.redis.delete(key);
249
+ removed++;
250
+ }
251
+ }
252
+ return removed;
253
+ }
254
+ /**
255
+ * Close the Redis connection.
256
+ */
257
+ async close() {
258
+ await this.redis.disconnect();
259
+ }
260
+ };
261
+ }
262
+ });
19
263
 
20
264
  // libs/auth/src/cimd/cimd.cache.ts
21
265
  function extractCacheHeaders(headers) {
@@ -82,6 +326,19 @@ function parseCacheControlHeader(value) {
82
326
  }
83
327
  return result;
84
328
  }
329
+ async function createCimdCache(config) {
330
+ const cacheType = config?.type ?? "memory";
331
+ if (cacheType === "redis") {
332
+ if (!config?.redis) {
333
+ throw new Error('Redis configuration is required when cache type is "redis"');
334
+ }
335
+ const { RedisCimdCache: RedisCimdCache2 } = await Promise.resolve().then(() => (init_cimd_redis_cache(), cimd_redis_cache_exports));
336
+ const cache = new RedisCimdCache2(config);
337
+ await cache.connect();
338
+ return cache;
339
+ }
340
+ return new InMemoryCimdCache(config);
341
+ }
85
342
  var InMemoryCimdCache;
86
343
  var init_cimd_cache = __esm({
87
344
  "libs/auth/src/cimd/cimd.cache.ts"() {
@@ -620,6 +877,7 @@ var TRANSPARENT_JWT_ALGS = [
620
877
  ];
621
878
  var MAX_JWKS_FETCH_BYTES = 1048576;
622
879
  var MAX_JWKS_FETCH_HOPS = 3;
880
+ var UNKNOWN_KID_REFETCH_COOLDOWN_MS = 6e4;
623
881
  var MISSING_EXP_ERROR = 'missing required "exp" claim';
624
882
  function isMissingExpError(error) {
625
883
  if (typeof error !== "object" || error === null) return false;
@@ -640,6 +898,8 @@ var JwksService = class {
640
898
  orchestratorKey;
641
899
  // Provider JWKS cache (providerId -> jwks + fetchedAt)
642
900
  providerJwks = /* @__PURE__ */ new Map();
901
+ // The last key refetch an unknown `kid` started, per provider (see refetchForUnknownKid)
902
+ unknownKidRefetches = /* @__PURE__ */ new Map();
643
903
  // Track if key has been initialized (for async loading)
644
904
  keyInitialized = false;
645
905
  // Promise guard to prevent concurrent key generation
@@ -700,6 +960,9 @@ var JwksService = class {
700
960
  let jwks;
701
961
  try {
702
962
  jwks = await this.getJwksForProvider(p);
963
+ if (kid && !p.jwks?.keys?.length && !jwks?.keys?.some((key) => key.kid === kid)) {
964
+ jwks = await this.refetchForUnknownKid(p) ?? jwks;
965
+ }
703
966
  if (!jwks?.keys?.length) continue;
704
967
  const JWKS = createLocalJWKSet(jwks);
705
968
  const issuerConstraint = this.issuerConstraintFor(p);
@@ -867,7 +1130,8 @@ var JwksService = class {
867
1130
  * 1) inline jwks (if provided) → cache & return
868
1131
  * 2) cached & fresh (TTL) → return
869
1132
  * 3) explicit jwksUri → fetch, cache, return
870
- * 4) discover jwks_uri → the issuer's OAuth authorization server
1133
+ * 4) `<issuer>/.well-known/jwks.json` → fetch, cache, return
1134
+ * 5) discover jwks_uri → the issuer's OAuth authorization server
871
1135
  * metadata (RFC 8414), else its OpenID configuration (OpenID Connect
872
1136
  * Discovery 1.0 §4, where Google, Okta, Auth0 and Entra publish it),
873
1137
  * then fetch jwks_uri, cache, return
@@ -881,11 +1145,42 @@ var JwksService = class {
881
1145
  if (cached && Date.now() - cached.fetchedAt < this.opts.providerJwksTtlMs) {
882
1146
  return cached.jwks;
883
1147
  }
1148
+ const fetched = await this.fetchProviderJwks(ref);
1149
+ if (fetched) return fetched;
1150
+ const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
1151
+ if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
1152
+ return cached.jwks;
1153
+ }
1154
+ return void 0;
1155
+ }
1156
+ /**
1157
+ * Fetch a provider's keys again for a token whose `kid` the cached keys don't have (a key the
1158
+ * provider started using since), ahead of the cache's TTL. At most one refetch per provider per
1159
+ * {@link UNKNOWN_KID_REFETCH_COOLDOWN_MS}: tokens that arrive meanwhile wait for the one in flight or
1160
+ * use its result, so a flood of tokens with made-up `kid`s can't hammer the provider. Only freshly
1161
+ * fetched keys are returned: a failed refetch never revives cached keys past the stale window.
1162
+ */
1163
+ refetchForUnknownKid(ref) {
1164
+ let refetch = this.unknownKidRefetches.get(ref.id);
1165
+ if (!refetch || Date.now() - refetch.startedAt >= UNKNOWN_KID_REFETCH_COOLDOWN_MS) {
1166
+ refetch = { startedAt: Date.now(), done: this.fetchProviderJwks(ref) };
1167
+ this.unknownKidRefetches.set(ref.id, refetch);
1168
+ }
1169
+ return refetch.done;
1170
+ }
1171
+ /**
1172
+ * Fetch a provider's keys, and cache them: from `jwksUri`, else the conventional location, else
1173
+ * the `jwks_uri` of the issuer's OAuth or OpenID metadata. Every URL goes through the same scheme
1174
+ * and SSRF checks (`fetchJson`), `jwks_uri` included.
1175
+ */
1176
+ async fetchProviderJwks(ref) {
884
1177
  if (ref.jwksUri) {
885
1178
  const fromUri = await this.tryFetchJwks(ref.id, ref.jwksUri);
886
1179
  if (fromUri?.keys?.length) return fromUri;
887
1180
  }
888
1181
  const issuer = trimSlash(ref.issuerUrl);
1182
+ const fromWellKnown = await this.tryFetchJwks(ref.id, `${issuer}/.well-known/jwks.json`);
1183
+ if (fromWellKnown?.keys?.length) return fromWellKnown;
889
1184
  for (const document of ["oauth-authorization-server", "openid-configuration"]) {
890
1185
  const meta = await this.tryFetchAsMeta(`${issuer}/.well-known/${document}`);
891
1186
  const uri = meta && typeof meta === "object" && meta["jwks_uri"] ? String(meta["jwks_uri"]) : void 0;
@@ -894,10 +1189,6 @@ var JwksService = class {
894
1189
  if (fromMeta?.keys?.length) return fromMeta;
895
1190
  }
896
1191
  }
897
- const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
898
- if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
899
- return cached.jwks;
900
- }
901
1192
  return void 0;
902
1193
  }
903
1194
  // ===========================================================================
@@ -1864,7 +2155,8 @@ function buildRefreshTokenRecord(params) {
1864
2155
  customClaims: params.customClaims,
1865
2156
  federatedLoginUsed: params.federatedLoginUsed,
1866
2157
  selectedProviderIds: params.selectedProviderIds,
1867
- skippedProviderIds: params.skippedProviderIds
2158
+ skippedProviderIds: params.skippedProviderIds,
2159
+ providerTokensId: params.providerTokensId
1868
2160
  };
1869
2161
  }
1870
2162
  function verifyPkce(codeVerifier, challenge) {
@@ -3935,7 +4227,8 @@ function deriveTypedUser(claims) {
3935
4227
  ...claims,
3936
4228
  iss: extractClaimValue(claims, "iss", isString) ?? "",
3937
4229
  sid: extractClaimValue(claims, "sid", isString),
3938
- sub: extractClaimValue(claims, "sub", isString) ?? "",
4230
+ // RFC 9068 §2.2: a client-credentials token names the client as its subject; some IdPs only put it in client_id / azp
4231
+ sub: extractClaimValue(claims, "sub", isString) ?? extractClaimValue(claims, "client_id", isString) ?? extractClaimValue(claims, "azp", isString) ?? "",
3939
4232
  exp: extractClaimValue(claims, "exp", isNumber),
3940
4233
  iat: extractClaimValue(claims, "iat", isNumber),
3941
4234
  aud: extractClaimValue(claims, "aud", isStringOrStringArray),
@@ -4881,7 +5174,49 @@ var VercelKvSessionStore = class {
4881
5174
  };
4882
5175
 
4883
5176
  // libs/auth/src/session/orchestrated-token.store.ts
4884
- import { encryptAesGcm as encryptAesGcm4, decryptAesGcm as decryptAesGcm4, randomBytes as randomBytes7, hkdfSha256 as hkdfSha2563 } from "@frontmcp/utils";
5177
+ import { decryptAesGcm as decryptAesGcm5, encryptAesGcm as encryptAesGcm5, hkdfSha256 as hkdfSha2564, randomBytes as randomBytes8 } from "@frontmcp/utils";
5178
+
5179
+ // libs/auth/src/session/orchestrated-token.crypto.ts
5180
+ import { decryptAesGcm as decryptAesGcm4, encryptAesGcm as encryptAesGcm4, hkdfSha256 as hkdfSha2563, randomBytes as randomBytes7 } from "@frontmcp/utils";
5181
+ function recordDropAt(record) {
5182
+ if (!record.refreshToken) return record.expiresAt;
5183
+ return Math.max(record.expiresAt ?? 0, record.updatedAt + REFRESH_TOKEN_TTL_MS);
5184
+ }
5185
+ function hasLiveAccessToken(record, refreshSkewMs = 0) {
5186
+ if (record.expiresAt === void 0) return true;
5187
+ return record.expiresAt - (record.refreshToken ? refreshSkewMs : 0) > Date.now();
5188
+ }
5189
+ var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
5190
+ function deriveKeyForRecord(masterKey, compositeKey, cache) {
5191
+ const cached = cache.get(compositeKey);
5192
+ if (cached) {
5193
+ return cached;
5194
+ }
5195
+ const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
5196
+ const derived = hkdfSha2563(masterKey, HKDF_SALT, info, 32);
5197
+ cache.set(compositeKey, derived);
5198
+ return derived;
5199
+ }
5200
+ function encryptRecord(key, record) {
5201
+ const plaintext = JSON.stringify(record);
5202
+ const iv = randomBytes7(12);
5203
+ const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
5204
+ return JSON.stringify({
5205
+ iv: Buffer.from(iv).toString("base64url"),
5206
+ tag: Buffer.from(tag).toString("base64url"),
5207
+ data: Buffer.from(ciphertext).toString("base64url")
5208
+ });
5209
+ }
5210
+ function decryptRecord(key, encrypted) {
5211
+ const { iv, tag, data } = JSON.parse(encrypted);
5212
+ const ivBytes = Buffer.from(iv, "base64url");
5213
+ const tagBytes = Buffer.from(tag, "base64url");
5214
+ const ciphertextBytes = Buffer.from(data, "base64url");
5215
+ const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
5216
+ return JSON.parse(new TextDecoder().decode(plaintext));
5217
+ }
5218
+
5219
+ // libs/auth/src/session/orchestrated-token.store.ts
4885
5220
  var InMemoryOrchestratedTokenStore = class {
4886
5221
  /** Token storage: Map<compositeKey, ProviderTokenRecord> */
4887
5222
  tokens = /* @__PURE__ */ new Map();
@@ -4893,9 +5228,12 @@ var InMemoryOrchestratedTokenStore = class {
4893
5228
  cleanupTimer;
4894
5229
  /** Default TTL for records */
4895
5230
  defaultTtlMs;
5231
+ /** How long before its expiry a refreshable access token stops being returned */
5232
+ refreshSkewMs;
4896
5233
  constructor(options = {}) {
4897
5234
  this.encryptionKey = options.encryptionKey;
4898
5235
  this.defaultTtlMs = options.defaultTtlMs;
5236
+ this.refreshSkewMs = options.refreshSkewMs ?? 0;
4899
5237
  const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
4900
5238
  this.cleanupTimer = setInterval(() => {
4901
5239
  void this.cleanup();
@@ -4923,7 +5261,7 @@ var InMemoryOrchestratedTokenStore = class {
4923
5261
  }
4924
5262
  const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
4925
5263
  const salt = new TextEncoder().encode("frontmcp-token-store");
4926
- const derivedKey = hkdfSha2563(this.encryptionKey, salt, info, 32);
5264
+ const derivedKey = hkdfSha2564(this.encryptionKey, salt, info, 32);
4927
5265
  this.derivedKeys.set(compositeKey, derivedKey);
4928
5266
  return derivedKey;
4929
5267
  }
@@ -4933,8 +5271,8 @@ var InMemoryOrchestratedTokenStore = class {
4933
5271
  async encryptRecord(compositeKey, record) {
4934
5272
  const key = await this.deriveKeyForRecord(compositeKey);
4935
5273
  const plaintext = JSON.stringify(record);
4936
- const iv = randomBytes7(12);
4937
- const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
5274
+ const iv = randomBytes8(12);
5275
+ const { ciphertext, tag } = encryptAesGcm5(key, new TextEncoder().encode(plaintext), iv);
4938
5276
  return JSON.stringify({
4939
5277
  iv: Buffer.from(iv).toString("base64url"),
4940
5278
  tag: Buffer.from(tag).toString("base64url"),
@@ -4950,7 +5288,7 @@ var InMemoryOrchestratedTokenStore = class {
4950
5288
  const ivBytes = Buffer.from(iv, "base64url");
4951
5289
  const tagBytes = Buffer.from(tag, "base64url");
4952
5290
  const ciphertextBytes = Buffer.from(data, "base64url");
4953
- const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
5291
+ const plaintext = decryptAesGcm5(key, ciphertextBytes, ivBytes, tagBytes);
4954
5292
  return JSON.parse(new TextDecoder().decode(plaintext));
4955
5293
  }
4956
5294
  /**
@@ -4976,18 +5314,20 @@ var InMemoryOrchestratedTokenStore = class {
4976
5314
  } else {
4977
5315
  record = stored;
4978
5316
  }
4979
- if (record.expiresAt && record.expiresAt < Date.now()) {
5317
+ const dropAt = recordDropAt(record);
5318
+ if (dropAt && dropAt < Date.now()) {
4980
5319
  this.tokens.delete(key);
4981
5320
  return null;
4982
5321
  }
4983
5322
  return record;
4984
5323
  }
4985
5324
  /**
4986
- * Retrieve decrypted access token for a provider
5325
+ * Retrieve decrypted access token for a provider, or null once it has expired (a record with a
5326
+ * refresh token keeps that, to renew it)
4987
5327
  */
4988
5328
  async getAccessToken(authorizationId, providerId) {
4989
5329
  const record = await this.getRecord(authorizationId, providerId);
4990
- return record?.accessToken ?? null;
5330
+ return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
4991
5331
  }
4992
5332
  /**
4993
5333
  * Retrieve decrypted refresh token for a provider
@@ -5080,7 +5420,8 @@ var InMemoryOrchestratedTokenStore = class {
5080
5420
  } else {
5081
5421
  record = stored;
5082
5422
  }
5083
- if (record?.expiresAt && record.expiresAt < now) {
5423
+ const dropAt = record ? recordDropAt(record) : void 0;
5424
+ if (dropAt && dropAt < now) {
5084
5425
  keysToDelete.push(key);
5085
5426
  }
5086
5427
  }
@@ -5120,74 +5461,48 @@ var InMemoryOrchestratedTokenStore = class {
5120
5461
  * @param toAuthId - Target authorization ID (e.g., "def456")
5121
5462
  */
5122
5463
  async migrateTokens(fromAuthId, toAuthId) {
5123
- const prefix = `${fromAuthId}:`;
5124
- const keysToMigrate = [];
5125
- for (const key of this.tokens.keys()) {
5126
- if (key.startsWith(prefix)) {
5127
- keysToMigrate.push(key);
5128
- }
5464
+ for (const oldKey of await this.copyRecords(fromAuthId, toAuthId)) {
5465
+ this.tokens.delete(oldKey);
5466
+ this.derivedKeys.delete(oldKey);
5129
5467
  }
5130
- for (const oldKey of keysToMigrate) {
5131
- const providerId = oldKey.slice(prefix.length);
5132
- const newKey = this.buildKey(toAuthId, providerId);
5468
+ }
5469
+ /**
5470
+ * Copy tokens from one authorization ID to another, keeping them under the source ID too.
5471
+ */
5472
+ async copyTokens(fromAuthId, toAuthId) {
5473
+ await this.copyRecords(fromAuthId, toAuthId);
5474
+ }
5475
+ /** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the keys copied. */
5476
+ async copyRecords(fromAuthId, toAuthId) {
5477
+ const prefix = `${fromAuthId}:`;
5478
+ const sourceKeys = [...this.tokens.keys()].filter((key) => key.startsWith(prefix));
5479
+ const copiedKeys = [];
5480
+ for (const oldKey of sourceKeys) {
5133
5481
  const stored = this.tokens.get(oldKey);
5134
- if (!stored) {
5135
- continue;
5136
- }
5482
+ if (!stored) continue;
5483
+ const newKey = this.buildKey(toAuthId, oldKey.slice(prefix.length));
5137
5484
  if (this.encryptionKey) {
5138
5485
  try {
5139
5486
  const record = await this.decryptRecord(oldKey, stored);
5140
- const encrypted = await this.encryptRecord(newKey, record);
5141
- this.tokens.set(newKey, encrypted);
5487
+ this.tokens.set(newKey, await this.encryptRecord(newKey, record));
5142
5488
  } catch {
5143
5489
  continue;
5144
5490
  }
5145
5491
  } else {
5146
5492
  this.tokens.set(newKey, stored);
5147
5493
  }
5148
- this.tokens.delete(oldKey);
5149
- this.derivedKeys.delete(oldKey);
5494
+ copiedKeys.push(oldKey);
5150
5495
  }
5496
+ return copiedKeys;
5151
5497
  }
5152
5498
  };
5153
5499
 
5154
- // libs/auth/src/session/orchestrated-token.crypto.ts
5155
- import { decryptAesGcm as decryptAesGcm5, encryptAesGcm as encryptAesGcm5, hkdfSha256 as hkdfSha2564, randomBytes as randomBytes8 } from "@frontmcp/utils";
5156
- var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
5157
- function deriveKeyForRecord(masterKey, compositeKey, cache) {
5158
- const cached = cache.get(compositeKey);
5159
- if (cached) {
5160
- return cached;
5161
- }
5162
- const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
5163
- const derived = hkdfSha2564(masterKey, HKDF_SALT, info, 32);
5164
- cache.set(compositeKey, derived);
5165
- return derived;
5166
- }
5167
- function encryptRecord(key, record) {
5168
- const plaintext = JSON.stringify(record);
5169
- const iv = randomBytes8(12);
5170
- const { ciphertext, tag } = encryptAesGcm5(key, new TextEncoder().encode(plaintext), iv);
5171
- return JSON.stringify({
5172
- iv: Buffer.from(iv).toString("base64url"),
5173
- tag: Buffer.from(tag).toString("base64url"),
5174
- data: Buffer.from(ciphertext).toString("base64url")
5175
- });
5176
- }
5177
- function decryptRecord(key, encrypted) {
5178
- const { iv, tag, data } = JSON.parse(encrypted);
5179
- const ivBytes = Buffer.from(iv, "base64url");
5180
- const tagBytes = Buffer.from(tag, "base64url");
5181
- const ciphertextBytes = Buffer.from(data, "base64url");
5182
- const plaintext = decryptAesGcm5(key, ciphertextBytes, ivBytes, tagBytes);
5183
- return JSON.parse(new TextDecoder().decode(plaintext));
5184
- }
5185
-
5186
5500
  // libs/auth/src/session/storage-orchestrated-token.store.ts
5187
5501
  var StorageOrchestratedTokenStore = class {
5188
5502
  storage;
5189
5503
  encryptionKey;
5190
5504
  defaultTtlMs;
5505
+ refreshSkewMs;
5191
5506
  namespace;
5192
5507
  storageIsNamespaced;
5193
5508
  /** HKDF-derived subkey cache, keyed by composite key. */
@@ -5195,6 +5510,7 @@ var StorageOrchestratedTokenStore = class {
5195
5510
  constructor(storage, options = {}) {
5196
5511
  this.encryptionKey = options.encryptionKey;
5197
5512
  this.defaultTtlMs = options.defaultTtlMs;
5513
+ this.refreshSkewMs = options.refreshSkewMs ?? 0;
5198
5514
  this.namespace = options.namespace ?? "otok";
5199
5515
  this.storageIsNamespaced = this.isNamespacedStorage(storage);
5200
5516
  this.storage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
@@ -5204,7 +5520,7 @@ var StorageOrchestratedTokenStore = class {
5204
5520
  // ============================================
5205
5521
  async getAccessToken(authorizationId, providerId) {
5206
5522
  const record = await this.getRecord(authorizationId, providerId);
5207
- return record?.accessToken ?? null;
5523
+ return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
5208
5524
  }
5209
5525
  async getRefreshToken(authorizationId, providerId) {
5210
5526
  const record = await this.getRecord(authorizationId, providerId);
@@ -5221,7 +5537,7 @@ var StorageOrchestratedTokenStore = class {
5221
5537
  updatedAt: now
5222
5538
  };
5223
5539
  const value = this.encryptionKey ? encryptRecord(this.keyFor(compositeKey), record) : JSON.stringify(record);
5224
- await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(record.expiresAt));
5540
+ await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(recordDropAt(record)));
5225
5541
  }
5226
5542
  async deleteTokens(authorizationId, providerId) {
5227
5543
  const compositeKey = this.compositeKey(authorizationId, providerId);
@@ -5247,12 +5563,37 @@ var StorageOrchestratedTokenStore = class {
5247
5563
  return providerIds;
5248
5564
  }
5249
5565
  async migrateTokens(fromAuthId, toAuthId) {
5566
+ for (const oldComposite of await this.copyRecords(fromAuthId, toAuthId)) {
5567
+ await this.storage.delete(this.storageKey(oldComposite));
5568
+ this.derivedKeys.delete(oldComposite);
5569
+ }
5570
+ }
5571
+ async copyTokens(fromAuthId, toAuthId) {
5572
+ await this.copyRecords(fromAuthId, toAuthId);
5573
+ }
5574
+ /**
5575
+ * Delete all tokens for an authorization id.
5576
+ */
5577
+ async deleteAllForAuthorization(authorizationId) {
5578
+ const prefix = `${authorizationId}:`;
5579
+ const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
5580
+ for (const compositeKey of compositeKeys) {
5581
+ if (!compositeKey.startsWith(prefix)) continue;
5582
+ await this.storage.delete(this.storageKey(compositeKey));
5583
+ this.derivedKeys.delete(compositeKey);
5584
+ }
5585
+ }
5586
+ // ============================================
5587
+ // Internals
5588
+ // ============================================
5589
+ /** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the composite keys copied. */
5590
+ async copyRecords(fromAuthId, toAuthId) {
5250
5591
  const prefix = `${fromAuthId}:`;
5251
5592
  const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
5593
+ const copiedKeys = [];
5252
5594
  for (const oldComposite of compositeKeys) {
5253
5595
  if (!oldComposite.startsWith(prefix)) continue;
5254
- const providerId = oldComposite.slice(prefix.length);
5255
- const newComposite = this.compositeKey(toAuthId, providerId);
5596
+ const newComposite = this.compositeKey(toAuthId, oldComposite.slice(prefix.length));
5256
5597
  const stored = await this.storage.get(this.storageKey(oldComposite));
5257
5598
  if (stored === null) continue;
5258
5599
  if (this.encryptionKey) {
@@ -5263,35 +5604,20 @@ var StorageOrchestratedTokenStore = class {
5263
5604
  continue;
5264
5605
  }
5265
5606
  const value = encryptRecord(this.keyFor(newComposite), record);
5266
- await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(record.expiresAt));
5607
+ await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(recordDropAt(record)));
5267
5608
  } else {
5268
- let expiresAt;
5609
+ let dropAt;
5269
5610
  try {
5270
- expiresAt = JSON.parse(stored).expiresAt;
5611
+ dropAt = recordDropAt(JSON.parse(stored));
5271
5612
  } catch {
5272
- expiresAt = void 0;
5613
+ dropAt = void 0;
5273
5614
  }
5274
- await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(expiresAt));
5615
+ await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(dropAt));
5275
5616
  }
5276
- await this.storage.delete(this.storageKey(oldComposite));
5277
- this.derivedKeys.delete(oldComposite);
5278
- }
5279
- }
5280
- /**
5281
- * Delete all tokens for an authorization id.
5282
- */
5283
- async deleteAllForAuthorization(authorizationId) {
5284
- const prefix = `${authorizationId}:`;
5285
- const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
5286
- for (const compositeKey of compositeKeys) {
5287
- if (!compositeKey.startsWith(prefix)) continue;
5288
- await this.storage.delete(this.storageKey(compositeKey));
5289
- this.derivedKeys.delete(compositeKey);
5617
+ copiedKeys.push(oldComposite);
5290
5618
  }
5619
+ return copiedKeys;
5291
5620
  }
5292
- // ============================================
5293
- // Internals
5294
- // ============================================
5295
5621
  async getRecord(authorizationId, providerId) {
5296
5622
  const compositeKey = this.compositeKey(authorizationId, providerId);
5297
5623
  const stored = await this.storage.get(this.storageKey(compositeKey));
@@ -5314,7 +5640,8 @@ var StorageOrchestratedTokenStore = class {
5314
5640
  return null;
5315
5641
  }
5316
5642
  }
5317
- if (record.expiresAt && record.expiresAt < Date.now()) {
5643
+ const dropAt = recordDropAt(record);
5644
+ if (dropAt && dropAt < Date.now()) {
5318
5645
  await this.storage.delete(this.storageKey(compositeKey));
5319
5646
  return null;
5320
5647
  }
@@ -6766,7 +7093,7 @@ var OrchestratedAuthorization = class _OrchestratedAuthorization extends Authori
6766
7093
  const result = await this.#onTokenRefresh(providerId, refreshToken);
6767
7094
  await this.#tokenStore.storeTokens(this.id, providerId, {
6768
7095
  accessToken: result.accessToken,
6769
- refreshToken: result.refreshToken,
7096
+ refreshToken: result.refreshToken ?? refreshToken,
6770
7097
  expiresAt: result.expiresIn ? Date.now() + result.expiresIn * 1e3 : void 0
6771
7098
  });
6772
7099
  const currentState = this.#providerStates.get(providerId);
@@ -7535,7 +7862,7 @@ var CimdService = class {
7535
7862
  cacheConfig;
7536
7863
  securityConfig;
7537
7864
  networkConfig;
7538
- cache;
7865
+ cacheBackend;
7539
7866
  logger;
7540
7867
  /**
7541
7868
  * Whether CIMD is enabled.
@@ -7555,7 +7882,6 @@ var CimdService = class {
7555
7882
  this.cacheConfig = cimdCacheConfigSchema.parse(this.config.cache ?? {});
7556
7883
  this.securityConfig = cimdSecurityConfigSchema.parse(this.config.security ?? {});
7557
7884
  this.networkConfig = cimdNetworkConfigSchema.parse(this.config.network ?? {});
7558
- this.cache = new InMemoryCimdCache(this.cacheConfig);
7559
7885
  this.logger.debug("CimdService initialized", {
7560
7886
  enabled: this.config.enabled,
7561
7887
  cacheDefaultTtlMs: this.cacheConfig.defaultTtlMs,
@@ -7567,6 +7893,34 @@ var CimdService = class {
7567
7893
  );
7568
7894
  }
7569
7895
  }
7896
+ /**
7897
+ * Create the cache now (connecting to Redis with `cache.type: 'redis'`), so a misconfigured or
7898
+ * unreachable cache fails at startup rather than on the first CIMD client.
7899
+ */
7900
+ async initialize() {
7901
+ await this.cache();
7902
+ }
7903
+ /**
7904
+ * The cache `cache.type` selects, created once through the `createCimdCache` factory. A failed
7905
+ * creation is retried on the next use.
7906
+ */
7907
+ cache() {
7908
+ if (!this.cacheBackend) {
7909
+ this.cacheBackend = createCimdCache(this.cacheConfig).catch((error) => {
7910
+ this.cacheBackend = void 0;
7911
+ throw error;
7912
+ });
7913
+ }
7914
+ return this.cacheBackend;
7915
+ }
7916
+ /** Close the cache's connection (Redis with `cache.type: 'redis'`), if one was created. */
7917
+ async dispose() {
7918
+ const backend = this.cacheBackend;
7919
+ this.cacheBackend = void 0;
7920
+ if (!backend) return;
7921
+ const createdBackend = await backend.catch(() => void 0);
7922
+ await createdBackend?.close?.();
7923
+ }
7570
7924
  /**
7571
7925
  * Check if a client_id is a CIMD URL.
7572
7926
  *
@@ -7593,7 +7947,8 @@ var CimdService = class {
7593
7947
  };
7594
7948
  }
7595
7949
  validateClientIdUrl(clientId, this.securityConfig);
7596
- const cached = await this.cache.get(clientId);
7950
+ const cache = await this.cache();
7951
+ const cached = await cache.get(clientId);
7597
7952
  if (cached) {
7598
7953
  this.logger.debug(`Cache hit for CIMD client: ${clientId}`);
7599
7954
  return {
@@ -7611,8 +7966,8 @@ var CimdService = class {
7611
7966
  if (this.securityConfig.warnOnLocalhostRedirects && hasOnlyLocalhostRedirectUris(document.redirect_uris)) {
7612
7967
  this.logger.warn(`CIMD client "${clientId}" has only localhost redirect URIs - this may be a development client`);
7613
7968
  }
7614
- await this.cache.set(clientId, document, headers);
7615
- const entry = await this.cache.get(clientId);
7969
+ await cache.set(clientId, document, headers);
7970
+ const entry = await cache.get(clientId);
7616
7971
  return {
7617
7972
  isCimdClient: true,
7618
7973
  metadata: document,
@@ -7642,11 +7997,12 @@ var CimdService = class {
7642
7997
  * @param clientId - Optional client_id to clear; clears all if not provided
7643
7998
  */
7644
7999
  async clearCache(clientId) {
8000
+ const cache = await this.cache();
7645
8001
  if (clientId) {
7646
- await this.cache.delete(clientId);
8002
+ await cache.delete(clientId);
7647
8003
  this.logger.debug(`Cache cleared for: ${clientId}`);
7648
8004
  } else {
7649
- await this.cache.clear();
8005
+ await cache.clear();
7650
8006
  this.logger.debug("Cache cleared");
7651
8007
  }
7652
8008
  }
@@ -7654,8 +8010,9 @@ var CimdService = class {
7654
8010
  * Get cache statistics.
7655
8011
  */
7656
8012
  async getCacheStats() {
8013
+ const cache = await this.cache();
7657
8014
  return {
7658
- size: await this.cache.size()
8015
+ size: await cache.size()
7659
8016
  };
7660
8017
  }
7661
8018
  /**
@@ -7667,7 +8024,8 @@ var CimdService = class {
7667
8024
  const controller = new AbortController();
7668
8025
  const timer = setTimeout(() => controller.abort(), this.networkConfig.timeoutMs);
7669
8026
  try {
7670
- const conditionalHeaders = await this.cache.getConditionalHeaders(clientId);
8027
+ const cache = await this.cache();
8028
+ const conditionalHeaders = await cache.getConditionalHeaders(clientId);
7671
8029
  const originalOrigin = new URL(clientId).origin;
7672
8030
  const maxRedirects = this.networkConfig.maxRedirects;
7673
8031
  let currentUrl = clientId;
@@ -7690,9 +8048,9 @@ var CimdService = class {
7690
8048
  redirect: "manual"
7691
8049
  });
7692
8050
  if (response.status === 304) {
7693
- const staleEntry = await this.cache.getStale(clientId);
8051
+ const staleEntry = await cache.getStale(clientId);
7694
8052
  if (staleEntry) {
7695
- await this.cache.revalidate(clientId, response.headers);
8053
+ await cache.revalidate(clientId, response.headers);
7696
8054
  this.logger.debug(`CIMD document not modified: ${clientId}`);
7697
8055
  return {
7698
8056
  document: staleEntry.document,
@@ -8200,7 +8558,7 @@ var tokenRefreshConfigSchema = z10.object({
8200
8558
  * Refresh token before expiry by this many seconds
8201
8559
  * @default 60
8202
8560
  */
8203
- skewSeconds: z10.number().default(60)
8561
+ skewSeconds: z10.number().nonnegative().default(60)
8204
8562
  });
8205
8563
  var skippedAppBehaviorSchema = z10.enum(["anonymous", "require-auth"]);
8206
8564
  var consentConfigSchema = z10.object({
@@ -8313,7 +8671,7 @@ var publicAuthOptionsSchema = z11.object({
8313
8671
  * Anonymous session TTL in seconds
8314
8672
  * @default 3600 (1 hour)
8315
8673
  */
8316
- sessionTtl: z11.number().default(3600),
8674
+ sessionTtl: z11.number().int().positive().default(3600),
8317
8675
  /**
8318
8676
  * Scopes granted to anonymous sessions
8319
8677
  * @default ['anonymous']
@@ -8760,9 +9118,17 @@ function detectAuthProviders(parentAuth, apps) {
8760
9118
  `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(", ")}`
8761
9119
  );
8762
9120
  }
8763
- if (uniqueProviderCount > 1 && parentAuth && isPublicMode(parentAuth)) {
8764
- warnings.push(
8765
- `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.`
9121
+ const sharedEndpointUnenforced = !parentAuth || isPublicMode(parentAuth) || isStaticMode(parentAuth);
9122
+ const unenforcedAppIds = apps.filter((app) => app.auth && !isPublicMode(app.auth) && !staticParentEnforces(parentAuth, app.auth)).map((app) => app.id);
9123
+ if (sharedEndpointUnenforced && unenforcedAppIds.length > 0) {
9124
+ validationErrors.push(
9125
+ `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).`
9126
+ );
9127
+ }
9128
+ const tokenMintingAppIds = apps.filter((app) => app.remoteAuthMode === "forward" && !(parentAuth && isTransparentMode(parentAuth))).map((app) => app.id);
9129
+ if (tokenMintingAppIds.length > 0) {
9130
+ validationErrors.push(
9131
+ `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.`
8766
9132
  );
8767
9133
  }
8768
9134
  return {
@@ -8775,6 +9141,14 @@ function detectAuthProviders(parentAuth, apps) {
8775
9141
  warnings
8776
9142
  };
8777
9143
  }
9144
+ function staticParentEnforces(parentAuth, appAuth) {
9145
+ if (!parentAuth || !isStaticMode(parentAuth) || !isStaticMode(appAuth)) return false;
9146
+ const parent = staticAuthOptionsSchema.safeParse(parentAuth);
9147
+ const app = staticAuthOptionsSchema.safeParse(appAuth);
9148
+ if (!parent.success || !app.success) return false;
9149
+ const sameChallenge = parent.data.header.toLowerCase() === app.data.header.toLowerCase() && parent.data.scheme === app.data.scheme;
9150
+ return sameChallenge && parent.data.tokens.every((token) => app.data.tokens.includes(token));
9151
+ }
8778
9152
  function getProviderUrl(options) {
8779
9153
  if (isTransparentMode(options)) {
8780
9154
  return options.provider;
@@ -9115,6 +9489,16 @@ function isLoopbackRedirectUri(uri) {
9115
9489
  return false;
9116
9490
  }
9117
9491
 
9492
+ // libs/auth/src/utils/token-scopes.utils.ts
9493
+ function scopesFromClaims(claims) {
9494
+ const scopes = [claims?.scope, claims?.scp].flatMap((claim) => {
9495
+ if (typeof claim === "string") return claim.split(/\s+/).filter(Boolean);
9496
+ if (Array.isArray(claim)) return claim.filter((scope) => typeof scope === "string");
9497
+ return [];
9498
+ });
9499
+ return [...new Set(scopes)];
9500
+ }
9501
+
9118
9502
  // libs/auth/src/vault/auth-providers.types.ts
9119
9503
  import { z as z19 } from "@frontmcp/lazy-zod";
9120
9504
  var credentialScopeSchema = z19.enum(["global", "user", "session"]);
@@ -10132,6 +10516,7 @@ var LazyCredentialLoader = class {
10132
10516
  };
10133
10517
 
10134
10518
  // libs/auth/src/authorities/authorities.context.ts
10519
+ import { getEnv as getEnv4 } from "@frontmcp/utils";
10135
10520
  function resolveDotPath(obj, path) {
10136
10521
  if (path in obj) {
10137
10522
  return obj[path];
@@ -10172,6 +10557,10 @@ function signedInSubject(rawSub, mappedSub) {
10172
10557
  if (rawSub !== void 0 && isAnonymousSubject(rawSub)) return void 0;
10173
10558
  return [mappedSub, rawSub].find((candidate) => !isAnonymousSubject(candidate));
10174
10559
  }
10560
+ var runtimeEnv = new Proxy(
10561
+ {},
10562
+ { get: (_target, name) => typeof name === "string" ? getEnv4(name) : void 0 }
10563
+ );
10175
10564
  var AuthoritiesContextBuilder = class {
10176
10565
  claimsMapping;
10177
10566
  claimsResolver;
@@ -10184,7 +10573,7 @@ var AuthoritiesContextBuilder = class {
10184
10573
  /**
10185
10574
  * Build an evaluation context from AuthInfo and tool input.
10186
10575
  */
10187
- build(authInfo, input = {}, env = {}) {
10576
+ build(authInfo, input = {}, env = runtimeEnv) {
10188
10577
  if (this.claimsResolver) {
10189
10578
  const resolved = this.claimsResolver(authInfo);
10190
10579
  return {
@@ -10254,9 +10643,9 @@ var FrontMcpAuthContextImpl = class {
10254
10643
  claims;
10255
10644
  roles;
10256
10645
  permissions;
10257
- constructor(source, claimsMapping) {
10646
+ constructor(source, claimsMapping, claimsResolver) {
10258
10647
  const rawUser = resolveAuthUser(source);
10259
- const mappedSub = claimsMapping?.userId ? resolveDotPath(this.buildRawClaims(source), claimsMapping.userId) : void 0;
10648
+ const mappedSub = !claimsResolver && claimsMapping?.userId ? resolveDotPath(this.buildRawClaims(source), claimsMapping.userId) : void 0;
10260
10649
  const signedInSub = signedInSubject(rawUser.sub, mappedSub);
10261
10650
  const sub = signedInSub ?? (typeof rawUser.sub === "string" ? rawUser.sub : "");
10262
10651
  this.user = Object.freeze({
@@ -10274,6 +10663,13 @@ var FrontMcpAuthContextImpl = class {
10274
10663
  this.scopes = Object.freeze(
10275
10664
  Array.isArray(source.scopes) ? source.scopes.filter((s) => typeof s === "string") : []
10276
10665
  );
10666
+ if (claimsResolver) {
10667
+ const resolved = claimsResolver(source);
10668
+ this.claims = Object.freeze({ ...resolved.claims });
10669
+ this.roles = Object.freeze([...toStringArray2(resolved.roles)]);
10670
+ this.permissions = Object.freeze([...toStringArray2(resolved.permissions)]);
10671
+ return;
10672
+ }
10277
10673
  const rawClaims = this.buildRawClaims(source);
10278
10674
  this.claims = Object.freeze({ ...rawClaims });
10279
10675
  if (claimsMapping?.roles) {
@@ -10334,8 +10730,8 @@ var FrontMcpAuthContextImpl = class {
10334
10730
  };
10335
10731
 
10336
10732
  // libs/auth/src/context/frontmcp-auth-context.factory.ts
10337
- function buildAuthContext(authInfo, claimsMapping, pipes) {
10338
- const base = new FrontMcpAuthContextImpl(authInfo, claimsMapping);
10733
+ function buildAuthContext(authInfo, claimsMapping, pipes, claimsResolver) {
10734
+ const base = new FrontMcpAuthContextImpl(authInfo, claimsMapping, claimsResolver);
10339
10735
  if (!pipes || pipes.length === 0) {
10340
10736
  return base;
10341
10737
  }
@@ -10587,6 +10983,10 @@ function evaluateCondition(condition, envelope, ctx) {
10587
10983
  const expected = resolveValue(condition.value, ctx);
10588
10984
  return applyOperator(actual, condition.op, expected);
10589
10985
  }
10986
+ var REDACTED_ENV_VALUE = "[redacted]";
10987
+ function reportableActual(path, actual) {
10988
+ return path === "env" || path.startsWith("env.") ? REDACTED_ENV_VALUE : actual;
10989
+ }
10590
10990
  function evaluateAbac(policy, ctx) {
10591
10991
  const envelope = buildContextEnvelope(ctx);
10592
10992
  if (policy.match) {
@@ -10594,10 +10994,11 @@ function evaluateAbac(policy, ctx) {
10594
10994
  const actual = resolveDotPath(envelope, path);
10595
10995
  const expected = resolveValue(expectedRaw, ctx);
10596
10996
  if (expected === void 0 || actual !== expected) {
10997
+ const reported = reportableActual(path, actual);
10597
10998
  return {
10598
10999
  granted: false,
10599
- deniedBy: `attributes.match: '${path}' expected '${String(expected)}' but got '${String(actual)}'`,
10600
- denial: { kind: "attributes", path: "attributes.match", expected, actual },
11000
+ deniedBy: `attributes.match: '${path}' expected '${String(expected)}' but got '${String(reported)}'`,
11001
+ denial: { kind: "attributes", path: "attributes.match", expected, actual: reported },
10601
11002
  evaluatedPolicies: ["attributes.match"]
10602
11003
  };
10603
11004
  }
@@ -10608,7 +11009,7 @@ function evaluateAbac(policy, ctx) {
10608
11009
  const condition = policy.conditions[i];
10609
11010
  if (!evaluateCondition(condition, envelope, ctx)) {
10610
11011
  const expected = resolveValue(condition.value, ctx);
10611
- const actual = resolveDotPath(envelope, condition.path);
11012
+ const actual = reportableActual(condition.path, resolveDotPath(envelope, condition.path));
10612
11013
  return {
10613
11014
  granted: false,
10614
11015
  deniedBy: `attributes.conditions: '${condition.path}' failed '${condition.op}' check against '${String(expected)}'`,
@@ -11347,6 +11748,21 @@ var basicApplier = {
11347
11748
  function isFrontMcpCredentials(creds) {
11348
11749
  return typeof creds === "object" && creds !== null && "provider" in creds && typeof creds.provider === "string" && creds.provider.length > 0;
11349
11750
  }
11751
+ function providerHeadersCredentials(resolveHeaders) {
11752
+ return {
11753
+ async applyCredentials(_url, init) {
11754
+ const { credentials, ...cleanInit } = init;
11755
+ if (!isFrontMcpCredentials(credentials)) {
11756
+ return { init: typeof credentials === "string" ? init : cleanInit };
11757
+ }
11758
+ const headers = new Headers(cleanInit.headers);
11759
+ for (const [name, value] of Object.entries(await resolveHeaders(credentials.provider))) {
11760
+ headers.set(name, value);
11761
+ }
11762
+ return { init: { ...cleanInit, headers } };
11763
+ }
11764
+ };
11765
+ }
11350
11766
  var FetchCredentialMiddleware = class {
11351
11767
  constructor(tokenAccessor, appliers) {
11352
11768
  this.tokenAccessor = tokenAccessor;
@@ -11647,6 +12063,7 @@ export {
11647
12063
  privateKeyCredentialSchema,
11648
12064
  progressiveAuthStateSchema,
11649
12065
  providerConfigSchema,
12066
+ providerHeadersCredentials,
11650
12067
  publicAccessConfigSchema,
11651
12068
  publicAuthOptionsSchema,
11652
12069
  queryApplier,
@@ -11664,6 +12081,7 @@ export {
11664
12081
  resourceIdRefSchema,
11665
12082
  resourceScopesFor,
11666
12083
  safeDecrypt,
12084
+ scopesFromClaims,
11667
12085
  sealPendingLogin,
11668
12086
  secureStoreConfigSchema,
11669
12087
  secureStoreCustomBackendSchema,