@frontmcp/auth 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/authorities/authorities.profiles.d.ts +14 -1
  2. package/authorities/authorities.profiles.d.ts.map +1 -1
  3. package/authorization/dcr-client.registry.d.ts +83 -0
  4. package/authorization/dcr-client.registry.d.ts.map +1 -0
  5. package/authorization/index.d.ts +2 -0
  6. package/authorization/index.d.ts.map +1 -1
  7. package/cimd/cimd.service.d.ts +2 -9
  8. package/cimd/cimd.service.d.ts.map +1 -1
  9. package/common/session.types.d.ts +4 -4
  10. package/esm/index.mjs +1821 -214
  11. package/esm/package.json +8 -8
  12. package/index.d.ts +5 -5
  13. package/index.d.ts.map +1 -1
  14. package/index.js +2030 -383
  15. package/jwks/jwks.service.d.ts +2 -4
  16. package/jwks/jwks.service.d.ts.map +1 -1
  17. package/options/app-auth.schema.d.ts +226 -0
  18. package/options/app-auth.schema.d.ts.map +1 -1
  19. package/options/index.d.ts +4 -4
  20. package/options/index.d.ts.map +1 -1
  21. package/options/interfaces.d.ts +580 -4
  22. package/options/interfaces.d.ts.map +1 -1
  23. package/options/orchestrated.schema.d.ts +492 -1
  24. package/options/orchestrated.schema.d.ts.map +1 -1
  25. package/options/schema.d.ts +226 -0
  26. package/options/schema.d.ts.map +1 -1
  27. package/options/shared.schemas.d.ts +237 -2
  28. package/options/shared.schemas.d.ts.map +1 -1
  29. package/package.json +8 -8
  30. package/session/authorization.store.d.ts +117 -47
  31. package/session/authorization.store.d.ts.map +1 -1
  32. package/session/consent.store.d.ts +86 -0
  33. package/session/consent.store.d.ts.map +1 -0
  34. package/session/credential-resume-link.d.ts +64 -0
  35. package/session/credential-resume-link.d.ts.map +1 -0
  36. package/session/credentials-accessor.d.ts +102 -0
  37. package/session/credentials-accessor.d.ts.map +1 -0
  38. package/session/credentials-accessor.impl.d.ts +46 -0
  39. package/session/credentials-accessor.impl.d.ts.map +1 -0
  40. package/session/index.d.ts +32 -2
  41. package/session/index.d.ts.map +1 -1
  42. package/session/orchestrated-token.crypto.d.ts +33 -0
  43. package/session/orchestrated-token.crypto.d.ts.map +1 -0
  44. package/session/secure-store-accessor.d.ts +77 -0
  45. package/session/secure-store-accessor.d.ts.map +1 -0
  46. package/session/secure-store-accessor.impl.d.ts +61 -0
  47. package/session/secure-store-accessor.impl.d.ts.map +1 -0
  48. package/session/secure-store-backends.d.ts +75 -0
  49. package/session/secure-store-backends.d.ts.map +1 -0
  50. package/session/secure-store.d.ts +89 -0
  51. package/session/secure-store.d.ts.map +1 -0
  52. package/session/secure-store.factory.d.ts +62 -0
  53. package/session/secure-store.factory.d.ts.map +1 -0
  54. package/session/session-credential-vault.d.ts +153 -0
  55. package/session/session-credential-vault.d.ts.map +1 -0
  56. package/session/session-credential-vault.factory.d.ts +50 -0
  57. package/session/session-credential-vault.factory.d.ts.map +1 -0
  58. package/session/storage-authorization.store.d.ts +51 -0
  59. package/session/storage-authorization.store.d.ts.map +1 -0
  60. package/session/storage-consent.store.d.ts +33 -0
  61. package/session/storage-consent.store.d.ts.map +1 -0
  62. package/session/storage-federated-auth.session.d.ts +36 -0
  63. package/session/storage-federated-auth.session.d.ts.map +1 -0
  64. package/session/storage-orchestrated-token.store.d.ts +72 -0
  65. package/session/storage-orchestrated-token.store.d.ts.map +1 -0
  66. package/session/token-storage.factory.d.ts +46 -0
  67. package/session/token-storage.factory.d.ts.map +1 -0
  68. package/ui/index.d.ts +3 -2
  69. package/ui/index.d.ts.map +1 -1
  70. package/ui/local-login.helper.d.ts +31 -0
  71. package/ui/local-login.helper.d.ts.map +1 -0
  72. package/ui/templates.d.ts +118 -2
  73. package/ui/templates.d.ts.map +1 -1
  74. package/vault/auth-providers.types.d.ts +3 -3
package/index.js CHANGED
@@ -244,6 +244,7 @@ __export(index_exports, {
244
244
  AuthorityDeniedError: () => AuthorityDeniedError,
245
245
  AuthorizationBase: () => AuthorizationBase,
246
246
  CDN: () => CDN,
247
+ CREDENTIALS_ACCESSOR: () => CREDENTIALS_ACCESSOR,
247
248
  CimdClientIdMismatchError: () => CimdClientIdMismatchError,
248
249
  CimdDisabledError: () => CimdDisabledError,
249
250
  CimdError: () => CimdError,
@@ -255,12 +256,16 @@ __export(index_exports, {
255
256
  CredentialCache: () => CredentialCache,
256
257
  CredentialProviderAlreadyRegisteredError: () => CredentialProviderAlreadyRegisteredError,
257
258
  CredentialStorageError: () => CredentialStorageError,
259
+ CredentialsAccessorImpl: () => CredentialsAccessorImpl,
260
+ DEFAULT_RESUME_TTL_MS: () => DEFAULT_RESUME_TTL_MS,
258
261
  DEFAULT_THEME: () => DEFAULT_THEME,
262
+ DcrClientRegistry: () => DcrClientRegistry,
259
263
  EagerCredentialLoader: () => EagerCredentialLoader,
260
264
  ElicitationSecretRequiredError: () => ElicitationSecretRequiredError,
261
265
  EncryptedRedisVault: () => EncryptedRedisVault,
262
- EncryptedStorageError: () => import_utils16.EncryptedStorageError,
263
- EncryptedTypedStorage: () => import_utils16.EncryptedTypedStorage,
266
+ EncryptedStorageError: () => import_utils22.EncryptedStorageError,
267
+ EncryptedStorageSecureStoreBackend: () => EncryptedStorageSecureStoreBackend,
268
+ EncryptedTypedStorage: () => import_utils22.EncryptedTypedStorage,
264
269
  EncryptionContextNotSetError: () => EncryptionContextNotSetError,
265
270
  EncryptionKeyNotConfiguredError: () => EncryptionKeyNotConfiguredError,
266
271
  FetchCredentialMiddleware: () => FetchCredentialMiddleware,
@@ -268,6 +273,7 @@ __export(index_exports, {
268
273
  InMemoryAuthorizationStore: () => InMemoryAuthorizationStore,
269
274
  InMemoryAuthorizationVault: () => InMemoryAuthorizationVault,
270
275
  InMemoryCimdCache: () => InMemoryCimdCache,
276
+ InMemoryConsentStore: () => InMemoryConsentStore,
271
277
  InMemoryFederatedAuthSessionStore: () => InMemoryFederatedAuthSessionStore,
272
278
  InMemoryOrchestratedTokenStore: () => InMemoryOrchestratedTokenStore,
273
279
  InMemoryStoreRequiredError: () => InMemoryStoreRequiredError,
@@ -285,11 +291,18 @@ __export(index_exports, {
285
291
  RedirectUriMismatchError: () => RedirectUriMismatchError,
286
292
  RedisAuthorizationStore: () => RedisAuthorizationStore,
287
293
  RedisSessionStore: () => RedisSessionStore,
294
+ SECURE_STORE_ACCESSOR: () => SECURE_STORE_ACCESSOR,
288
295
  ScopeDeniedError: () => ScopeDeniedError,
296
+ SecureStoreAccessorImpl: () => SecureStoreAccessorImpl,
297
+ SessionCredentialVault: () => SessionCredentialVault,
289
298
  SessionIdEmptyError: () => SessionIdEmptyError,
290
299
  SessionRateLimiter: () => SessionRateLimiter,
291
300
  SessionSecretRequiredError: () => SessionSecretRequiredError,
301
+ StorageAuthorizationStore: () => StorageAuthorizationStore,
292
302
  StorageAuthorizationVault: () => StorageAuthorizationVault,
303
+ StorageConsentStore: () => StorageConsentStore,
304
+ StorageFederatedAuthSessionStore: () => StorageFederatedAuthSessionStore,
305
+ StorageOrchestratedTokenStore: () => StorageOrchestratedTokenStore,
293
306
  StorageTokenStore: () => StorageTokenStore,
294
307
  TinyTtlCache: () => TinyTtlCache,
295
308
  TokenLeakDetectedError: () => TokenLeakDetectedError,
@@ -298,7 +311,7 @@ __export(index_exports, {
298
311
  TokenVault: () => TokenVault,
299
312
  TransparentAuthorization: () => TransparentAuthorization,
300
313
  TransportIdGenerator: () => TransportIdGenerator,
301
- TypedStorage: () => import_utils15.TypedStorage,
314
+ TypedStorage: () => import_utils21.TypedStorage,
302
315
  VaultEncryption: () => VaultEncryption,
303
316
  VaultLoadError: () => VaultLoadError,
304
317
  VaultNotFoundError: () => VaultNotFoundError,
@@ -321,6 +334,7 @@ __export(index_exports, {
321
334
  authProviderMappingSchema: () => authProviderMappingSchema,
322
335
  authProvidersVaultOptionsSchema: () => authProvidersVaultOptionsSchema,
323
336
  authUserSchema: () => authUserSchema,
337
+ authenticateFnSchema: () => authenticateFnSchema,
324
338
  authoritiesClaimsMappingSchema: () => authoritiesClaimsMappingSchema,
325
339
  authoritiesConfigSchema: () => authoritiesConfigSchema,
326
340
  authoritiesMetadataSchema: () => authoritiesMetadataSchema,
@@ -335,7 +349,11 @@ __export(index_exports, {
335
349
  bearerApplier: () => bearerApplier,
336
350
  bearerCredentialSchema: () => bearerCredentialSchema,
337
351
  buildAuthContext: () => buildAuthContext,
352
+ buildCodeRecord: () => buildCodeRecord,
353
+ buildConnectPage: () => buildConnectPage,
354
+ buildConnectSuccessPage: () => buildConnectSuccessPage,
338
355
  buildConsentPage: () => buildConsentPage,
356
+ buildCredentialResumeUrl: () => buildCredentialResumeUrl,
339
357
  buildErrorPage: () => buildErrorPage,
340
358
  buildFederatedLoginPage: () => buildFederatedLoginPage,
341
359
  buildIncrementalAuthPage: () => buildIncrementalAuthPage,
@@ -343,7 +361,9 @@ __export(index_exports, {
343
361
  buildInvalidRequestHeader: () => buildInvalidRequestHeader,
344
362
  buildInvalidTokenHeader: () => buildInvalidTokenHeader,
345
363
  buildLoginPage: () => buildLoginPage,
364
+ buildPendingRecord: () => buildPendingRecord,
346
365
  buildPrmUrl: () => buildPrmUrl,
366
+ buildRefreshTokenRecord: () => buildRefreshTokenRecord,
347
367
  buildToolConsentPage: () => buildToolConsentPage,
348
368
  buildUnauthorizedHeader: () => buildUnauthorizedHeader,
349
369
  buildWwwAuthenticate: () => buildWwwAuthenticate,
@@ -356,6 +376,7 @@ __export(index_exports, {
356
376
  clientMetadataDocumentSchema: () => clientMetadataDocumentSchema,
357
377
  completeCurrentProvider: () => completeCurrentProvider,
358
378
  consentConfigSchema: () => consentConfigSchema,
379
+ consentRecordKey: () => consentRecordKey,
359
380
  consentSelectionSchema: () => consentSelectionSchema,
360
381
  consentStateSchema: () => consentStateSchema,
361
382
  consentToolItemSchema: () => consentToolItemSchema,
@@ -363,15 +384,18 @@ __export(index_exports, {
363
384
  createEncryptedVault: () => createEncryptedVault,
364
385
  createFederatedAuthSession: () => createFederatedAuthSession,
365
386
  createLayout: () => createLayout,
387
+ createSecureStore: () => createSecureStore,
388
+ createSessionCredentialVault: () => createSessionCredentialVault,
389
+ createTokenStorageAdapter: () => createTokenStorageAdapter,
366
390
  credentialProviderConfigSchema: () => credentialProviderConfigSchema,
367
391
  credentialSchema: () => credentialSchema,
368
392
  credentialScopeSchema: () => credentialScopeSchema,
369
393
  credentialTypeSchema: () => credentialTypeSchema,
370
394
  customCredentialSchema: () => customCredentialSchema,
371
395
  decodeJwtPayloadSafe: () => decodeJwtPayloadSafe,
372
- decryptAesGcm: () => import_utils23.decryptAesGcm,
396
+ decryptAesGcm: () => import_utils32.decryptAesGcm,
373
397
  decryptSessionJson: () => decryptSessionJson,
374
- decryptValue: () => import_utils23.decryptValue,
398
+ decryptValue: () => import_utils32.decryptValue,
375
399
  defaultSessionRateLimiter: () => defaultSessionRateLimiter,
376
400
  deriveAuthorizationId: () => deriveAuthorizationId,
377
401
  deriveExpectedAudience: () => deriveExpectedAudience,
@@ -379,9 +403,9 @@ __export(index_exports, {
379
403
  deriveTypedUser: () => deriveTypedUser,
380
404
  detectAuthProviders: () => detectAuthProviders,
381
405
  detectedAuthProviderSchema: () => detectedAuthProviderSchema,
382
- encryptAesGcm: () => import_utils23.encryptAesGcm,
406
+ encryptAesGcm: () => import_utils32.encryptAesGcm,
383
407
  encryptJson: () => encryptJson,
384
- encryptValue: () => import_utils23.encryptValue,
408
+ encryptValue: () => import_utils32.encryptValue,
385
409
  encryptedBlobSchema: () => encryptedBlobSchema,
386
410
  encryptedDataSchema: () => encryptedDataSchema,
387
411
  encryptedVaultEntrySchema: () => encryptedVaultEntrySchema,
@@ -396,7 +420,9 @@ __export(index_exports, {
396
420
  federatedSelectionSchema: () => federatedSelectionSchema,
397
421
  flatRemoteProviderFields: () => flatRemoteProviderFields,
398
422
  fromSessionRecord: () => fromSessionRecord,
423
+ generateAuthorizationCode: () => generateAuthorizationCode,
399
424
  generatePkceChallenge: () => generatePkceChallenge,
425
+ generateRefreshTokenValue: () => generateRefreshTokenValue,
400
426
  getCredentialOptionsSchema: () => getCredentialOptionsSchema,
401
427
  getKey: () => getKey,
402
428
  getNextProvider: () => getNextProvider,
@@ -405,7 +431,7 @@ __export(index_exports, {
405
431
  getTokenSignatureFingerprint: () => getTokenSignatureFingerprint,
406
432
  hasOnlyLocalhostRedirectUris: () => hasOnlyLocalhostRedirectUris,
407
433
  headerApplier: () => headerApplier,
408
- hkdfSha256: () => import_utils23.hkdfSha256,
434
+ hkdfSha256: () => import_utils32.hkdfSha256,
409
435
  incrementalAuthConfigSchema: () => incrementalAuthConfigSchema,
410
436
  isCimdClientId: () => isCimdClientId,
411
437
  isFrontMcpCredentials: () => isFrontMcpCredentials,
@@ -414,12 +440,15 @@ __export(index_exports, {
414
440
  isOrchestratedLocal: () => isOrchestratedLocal,
415
441
  isOrchestratedMode: () => isOrchestratedMode,
416
442
  isOrchestratedRemote: () => isOrchestratedRemote,
443
+ isPersistentTokenStorage: () => isPersistentTokenStorage,
417
444
  isPublicMode: () => isPublicMode,
445
+ isRedisTokenStorage: () => isRedisTokenStorage,
418
446
  isRemoteMode: () => isRemoteMode,
419
447
  isSessionComplete: () => isSessionComplete,
420
- isSignedSession: () => import_utils8.isSignedData,
448
+ isSignedSession: () => import_utils14.isSignedData,
421
449
  isSoonExpiring: () => isSoonExpiring,
422
450
  isSoonExpiringProvider: () => isSoonExpiringProvider,
451
+ isSqliteTokenStorage: () => isSqliteTokenStorage,
423
452
  isTransparentMode: () => isTransparentMode,
424
453
  jsonWebKeySetSchema: () => jsonWebKeySetSchema,
425
454
  jwkParametersSchema: () => jwkParametersSchema,
@@ -428,7 +457,10 @@ __export(index_exports, {
428
457
  llmSafeAuthContextSchema: () => llmSafeAuthContextSchema,
429
458
  loadingStrategySchema: () => loadingStrategySchema,
430
459
  localAuthSchema: () => localAuthSchema,
460
+ localDcrClientSchema: () => localDcrClientSchema,
461
+ localDcrConfigSchema: () => localDcrConfigSchema,
431
462
  localSigningConfigSchema: () => localSigningConfigSchema,
463
+ loginConfigSchema: () => loginConfigSchema,
432
464
  mtlsCredentialSchema: () => mtlsCredentialSchema,
433
465
  noopLogger: () => noopLogger,
434
466
  normalizeIssuer: () => normalizeIssuer,
@@ -454,14 +486,20 @@ __export(index_exports, {
454
486
  redisVaultEntrySchema: () => redisVaultEntrySchema,
455
487
  remoteAuthSchema: () => remoteAuthSchema,
456
488
  remoteProviderConfigSchema: () => remoteProviderConfigSchema,
489
+ renderLocalLoginPage: () => renderLocalLoginPage,
457
490
  renderToHtml: () => renderToHtml,
458
491
  resetCachedKey: () => resetCachedKey,
459
492
  resolveRequiredScopes: () => resolveRequiredScopes,
460
493
  resourceIdRefSchema: () => resourceIdRefSchema,
461
494
  safeDecrypt: () => safeDecrypt,
495
+ secureStoreConfigSchema: () => secureStoreConfigSchema,
496
+ secureStoreCustomBackendSchema: () => secureStoreCustomBackendSchema,
497
+ secureStoreEncryptionSchema: () => secureStoreEncryptionSchema,
498
+ secureStoreScopeSchema: () => secureStoreScopeSchema,
462
499
  serviceAccountCredentialSchema: () => serviceAccountCredentialSchema,
463
500
  sessionIdPayloadSchema: () => sessionIdPayloadSchema,
464
501
  sessionJwtPayloadSchema: () => sessionJwtPayloadSchema,
502
+ signCredentialResumeToken: () => signCredentialResumeToken,
465
503
  signSession: () => signSession,
466
504
  skippedAppBehaviorSchema: () => skippedAppBehaviorSchema,
467
505
  sseTransportStateSchema: () => sseTransportStateSchema,
@@ -472,20 +510,24 @@ __export(index_exports, {
472
510
  storedSessionSchema: () => storedSessionSchema,
473
511
  streamableHttpTransportStateSchema: () => streamableHttpTransportStateSchema,
474
512
  toEpochSeconds: () => toEpochSeconds,
513
+ toLoginExtraFields: () => toLoginExtraFields,
475
514
  toSessionRecord: () => toSessionRecord,
476
515
  tokenRefreshConfigSchema: () => tokenRefreshConfigSchema,
477
516
  tokenStorageConfigSchema: () => tokenStorageConfigSchema,
517
+ tokenStorageSqliteSchema: () => tokenStorageSqliteSchema,
478
518
  transparentAuthOptionsSchema: () => transparentAuthOptionsSchema,
479
519
  transportProtocolSchema: () => transportProtocolSchema,
480
520
  transportSessionSchema: () => transportSessionSchema,
481
521
  transportStateSchema: () => transportStateSchema,
482
522
  trimSlash: () => trimSlash,
483
523
  tryJwtExp: () => tryJwtExp,
524
+ upstreamProviderSchema: () => upstreamProviderSchema,
484
525
  userClaimSchema: () => userClaimSchema,
485
526
  validateAudience: () => validateAudience,
486
527
  validateClientIdUrl: () => validateClientIdUrl,
487
528
  vaultConsentRecordSchema: () => vaultConsentRecordSchema,
488
529
  vaultFederatedRecordSchema: () => vaultFederatedRecordSchema,
530
+ verifyCredentialResumeToken: () => verifyCredentialResumeToken,
489
531
  verifyOrParseSession: () => verifyOrParseSession,
490
532
  verifyPkce: () => verifyPkce,
491
533
  verifySession: () => verifySession,
@@ -497,6 +539,18 @@ module.exports = __toCommonJS(index_exports);
497
539
  var import_jose = require("jose");
498
540
  var import_utils = require("@frontmcp/utils");
499
541
 
542
+ // libs/auth/src/common/auth-logger.interface.ts
543
+ var noopAuthLogger = {
544
+ info() {
545
+ },
546
+ warn() {
547
+ },
548
+ error() {
549
+ },
550
+ debug() {
551
+ }
552
+ };
553
+
500
554
  // libs/auth/src/jwks/jwks.utils.ts
501
555
  function trimSlash(s) {
502
556
  const str = s ?? "";
@@ -526,18 +580,6 @@ function decodeJwtPayloadSafe(token) {
526
580
  }
527
581
  }
528
582
 
529
- // libs/auth/src/common/auth-logger.interface.ts
530
- var noopAuthLogger = {
531
- info() {
532
- },
533
- warn() {
534
- },
535
- error() {
536
- },
537
- debug() {
538
- }
539
- };
540
-
541
583
  // libs/auth/src/jwks/jwks.service.ts
542
584
  var WEAK_KEY_WARNING = `
543
585
  \u26A0\uFE0F SECURITY WARNING: OAuth provider is using an RSA key smaller than 2048 bits.
@@ -596,28 +638,6 @@ var JwksService = class {
596
638
  // ===========================================================================
597
639
  // Scope-aware verification API
598
640
  // ===========================================================================
599
- /** Verify a token issued by the gateway itself (orchestrated mode). */
600
- async verifyGatewayToken(token, expectedIssuer) {
601
- try {
602
- const payload = decodeJwtPayloadSafe(token);
603
- if (!payload) {
604
- return {
605
- ok: false,
606
- error: "invalid bearer token"
607
- };
608
- }
609
- return {
610
- ok: true,
611
- issuer: expectedIssuer,
612
- sub: payload["sub"],
613
- payload,
614
- header: (0, import_jose.decodeProtectedHeader)(token)
615
- };
616
- } catch (err) {
617
- const message = err instanceof Error ? err.message : "verification_failed";
618
- return { ok: false, error: message };
619
- }
620
- }
621
641
  /**
622
642
  * Verify a token against candidate transparent providers.
623
643
  * Ensures JWKS are available (cached/TTL/AS discovery) per provider.
@@ -1262,32 +1282,52 @@ function buildFederatedLoginPage(params) {
1262
1282
  return wideLayout(content, { title: "Select Providers" });
1263
1283
  }
1264
1284
  function buildToolConsentPage(params) {
1265
- const { tools, clientName, pendingAuthId, csrfToken, callbackPath, userName, userEmail } = params;
1266
- const toolsByApp = {};
1267
- for (const tool of tools) {
1268
- if (!toolsByApp[tool.appId]) {
1269
- toolsByApp[tool.appId] = { appName: tool.appName, tools: [] };
1270
- }
1271
- toolsByApp[tool.appId].tools.push(tool);
1272
- }
1273
- const userInfo = userName || userEmail ? `<div class="p-3 bg-gray-50 rounded-lg mb-6 text-sm">
1274
- <span class="text-gray-500">Signed in as: </span>
1275
- <span class="font-medium text-gray-900">${escapeHtml3(userName || userEmail || "")}</span>
1276
- </div>` : "";
1277
- const appGroups = Object.entries(toolsByApp).map(([appId, { appName, tools: appTools }]) => {
1278
- const toolItems = appTools.map((tool) => {
1279
- const desc = tool.description ? `<p class="text-sm text-gray-500 mt-0.5">${escapeHtml3(tool.description)}</p>` : "";
1280
- return `<label class="flex items-start gap-3 p-3 bg-white rounded-lg cursor-pointer hover:bg-gray-50">
1281
- <input type="checkbox" name="tools" value="${escapeHtml3(
1282
- tool.toolId
1283
- )}" class="mt-0.5 w-5 h-5 rounded border-gray-300" checked>
1285
+ const {
1286
+ tools,
1287
+ clientName,
1288
+ pendingAuthId,
1289
+ csrfToken,
1290
+ callbackPath,
1291
+ userName,
1292
+ userEmail,
1293
+ groupByApp = true,
1294
+ showDescriptions = true,
1295
+ customMessage,
1296
+ allowSelectAll = true,
1297
+ requireSelection = true,
1298
+ defaultSelectedTools,
1299
+ error,
1300
+ hiddenFields = []
1301
+ } = params;
1302
+ const defaultSet = defaultSelectedTools ? new Set(defaultSelectedTools) : void 0;
1303
+ const isChecked = (toolId) => defaultSet ? defaultSet.has(toolId) : true;
1304
+ const checkedCount = tools.filter((t) => isChecked(t.toolId)).length;
1305
+ const renderTool = (tool) => {
1306
+ const desc = showDescriptions && tool.description ? `<p class="text-sm text-gray-500 mt-0.5">${escapeHtml3(tool.description)}</p>` : "";
1307
+ return `<label class="flex items-start gap-3 p-3 bg-white rounded-lg cursor-pointer hover:bg-gray-50">
1308
+ <input type="checkbox" name="tools" value="${escapeHtml3(tool.toolId)}" class="mt-0.5 w-5 h-5 rounded border-gray-300"${isChecked(tool.toolId) ? " checked" : ""}>
1284
1309
  <div>
1285
1310
  <span class="font-medium text-gray-900">${escapeHtml3(tool.toolName)}</span>
1286
1311
  ${desc}
1287
1312
  </div>
1288
1313
  </label>`;
1289
- }).join("\n");
1290
- return `<div class="bg-gray-50 rounded-xl overflow-hidden">
1314
+ };
1315
+ let toolSection;
1316
+ if (groupByApp) {
1317
+ const toolsByApp = {};
1318
+ for (const tool of tools) {
1319
+ if (!toolsByApp[tool.appId]) {
1320
+ toolsByApp[tool.appId] = { appName: tool.appName, tools: [] };
1321
+ }
1322
+ toolsByApp[tool.appId].tools.push(tool);
1323
+ }
1324
+ const toggleAllButton = allowSelectAll ? `<button type="button" class="text-sm text-blue-600 hover:text-blue-800"
1325
+ onclick="const container = this.closest('.bg-gray-50').querySelector('[data-app]'); const cbs = container.querySelectorAll('input[name=tools]'); const allChecked = [...cbs].every(cb => cb.checked); cbs.forEach(cb => cb.checked = !allChecked); updateCount();">
1326
+ Toggle All
1327
+ </button>` : "";
1328
+ toolSection = Object.entries(toolsByApp).map(([appId, { appName, tools: appTools }]) => {
1329
+ const toolItems = appTools.map(renderTool).join("\n");
1330
+ return `<div class="bg-gray-50 rounded-xl overflow-hidden">
1291
1331
  <div class="flex items-center justify-between px-4 py-3 bg-gray-100">
1292
1332
  <div class="flex items-center gap-3">
1293
1333
  <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm">
@@ -1295,51 +1335,70 @@ function buildToolConsentPage(params) {
1295
1335
  </div>
1296
1336
  <span class="font-semibold text-gray-900">${escapeHtml3(appName)}</span>
1297
1337
  </div>
1298
- <button type="button" class="text-sm text-blue-600 hover:text-blue-800"
1299
- onclick="const container = this.closest('.bg-gray-50').querySelector('[data-app]'); const cbs = container.querySelectorAll('input[name=tools]'); const allChecked = [...cbs].every(cb => cb.checked); cbs.forEach(cb => cb.checked = !allChecked); updateCount();">
1300
- Toggle All
1301
- </button>
1338
+ ${toggleAllButton}
1302
1339
  </div>
1303
1340
  <div class="p-4 space-y-2" data-app="${escapeHtml3(appId)}">
1304
1341
  ${toolItems}
1305
1342
  </div>
1306
1343
  </div>`;
1307
- }).join("\n");
1344
+ }).join("\n");
1345
+ } else {
1346
+ toolSection = `<div class="bg-gray-50 rounded-xl p-4 space-y-2" data-app="__all__">
1347
+ ${tools.map(renderTool).join("\n")}
1348
+ </div>`;
1349
+ }
1350
+ const userInfo = userName || userEmail ? `<div class="p-3 bg-gray-50 rounded-lg mb-6 text-sm">
1351
+ <span class="text-gray-500">Signed in as: </span>
1352
+ <span class="font-medium text-gray-900">${escapeHtml3(userName || userEmail || "")}</span>
1353
+ </div>` : "";
1354
+ const errorBanner = error ? `<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-sm text-red-800">${escapeHtml3(error)}</div>` : "";
1355
+ const hiddenInputs = hiddenFields.map((f) => `<input type="hidden" name="${escapeHtml3(f.name)}" value="${escapeHtml3(f.value)}">`).join("\n ");
1356
+ const selectAllControl = allowSelectAll ? `<label class="flex items-center gap-3 cursor-pointer">
1357
+ <input type="checkbox" id="select-all" class="w-5 h-5 rounded border-gray-300"${checkedCount === tools.length && tools.length > 0 ? " checked" : ""}
1358
+ onchange="document.querySelectorAll('input[name=tools]').forEach(cb => cb.checked = this.checked); updateCount();">
1359
+ <span class="text-gray-700">Select all tools</span>
1360
+ </label>` : "<span></span>";
1361
+ const requireSelectionScript = requireSelection ? `
1362
+ const submitBtn = document.getElementById('consent-submit');
1363
+ if (submitBtn) submitBtn.disabled = checked.length === 0;` : "";
1308
1364
  const updateCountScript = `
1309
1365
  <script>
1310
1366
  function updateCount() {
1311
1367
  const all = document.querySelectorAll('input[name="tools"]');
1312
1368
  const checked = document.querySelectorAll('input[name="tools"]:checked');
1313
- document.getElementById('selection-count').textContent = checked.length + ' of ' + all.length + ' selected';
1314
- document.getElementById('select-all').checked = all.length > 0 && all.length === checked.length;
1369
+ const countEl = document.getElementById('selection-count');
1370
+ if (countEl) countEl.textContent = checked.length + ' of ' + all.length + ' selected';
1371
+ const selectAll = document.getElementById('select-all');
1372
+ if (selectAll) selectAll.checked = all.length > 0 && all.length === checked.length;${requireSelectionScript}
1315
1373
  }
1316
1374
  document.querySelectorAll('input[name="tools"]').forEach(cb => cb.addEventListener('change', updateCount));
1375
+ updateCount();
1317
1376
  </script>`;
1377
+ const subtitle = customMessage ? escapeHtml3(customMessage) : `Choose which tools ${escapeHtml3(clientName)} can access. You can change this later.`;
1318
1378
  const content = `
1319
1379
  <h1 class="text-3xl font-bold text-gray-900 mb-4">Select Tools to Enable</h1>
1320
1380
  <p class="text-gray-600 mb-6">
1321
- Choose which tools ${escapeHtml3(clientName)} can access. You can change this later.
1381
+ ${subtitle}
1322
1382
  </p>
1323
1383
 
1384
+ ${errorBanner}
1324
1385
  ${userInfo}
1325
1386
 
1326
- <form method="POST" action="${escapeHtml3(callbackPath)}" id="consent-form">
1387
+ <form method="GET" action="${escapeHtml3(callbackPath)}" id="consent-form">
1327
1388
  <input type="hidden" name="csrf" value="${escapeHtml3(csrfToken)}">
1328
1389
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1390
+ <input type="hidden" name="consent_submitted" value="1">
1391
+ ${hiddenInputs}
1329
1392
 
1330
1393
  <!-- Select all toggle -->
1331
1394
  <div class="flex items-center justify-between mb-6">
1332
- <label class="flex items-center gap-3 cursor-pointer">
1333
- <input type="checkbox" id="select-all" class="w-5 h-5 rounded border-gray-300" checked
1334
- onchange="document.querySelectorAll('input[name=tools]').forEach(cb => cb.checked = this.checked); updateCount();">
1335
- <span class="text-gray-700">Select all tools</span>
1336
- </label>
1337
- <span id="selection-count" class="text-sm text-gray-500">${tools.length} of ${tools.length} selected</span>
1395
+ ${selectAllControl}
1396
+ <span id="selection-count" class="text-sm text-gray-500">${checkedCount} of ${tools.length} selected</span>
1338
1397
  </div>
1339
1398
 
1340
- <!-- Tool groups by app -->
1399
+ <!-- Tool groups -->
1341
1400
  <div class="space-y-6 mb-8">
1342
- ${appGroups}
1401
+ ${toolSection}
1343
1402
  </div>
1344
1403
 
1345
1404
  <!-- Buttons -->
@@ -1348,8 +1407,8 @@ function buildToolConsentPage(params) {
1348
1407
  class="flex-1 px-6 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 font-medium rounded-lg transition-colors">
1349
1408
  Cancel
1350
1409
  </button>
1351
- <button type="submit"
1352
- class="flex-1 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
1410
+ <button type="submit" id="consent-submit"
1411
+ class="flex-1 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
1353
1412
  Confirm Selection
1354
1413
  </button>
1355
1414
  </div>
@@ -1357,22 +1416,42 @@ function buildToolConsentPage(params) {
1357
1416
  ${updateCountScript}`;
1358
1417
  return extraWideLayout(content, { title: "Select Tools" });
1359
1418
  }
1419
+ function buildLoginFieldHtml(field) {
1420
+ const id = `field-${field.name}`;
1421
+ const safeName = escapeHtml3(field.name);
1422
+ const requiredAttr = field.required ? " required" : "";
1423
+ const labelText = escapeHtml3(field.label ?? field.name);
1424
+ const safeValue = field.value !== void 0 ? escapeHtml3(field.value) : "";
1425
+ if (field.type === "hidden") {
1426
+ return `<input type="hidden" name="${safeName}" value="${safeValue}">`;
1427
+ }
1428
+ const inputClass = "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1429
+ let control;
1430
+ if (field.type === "select") {
1431
+ const opts = (field.options ?? []).map(
1432
+ (o) => `<option value="${escapeHtml3(o.value)}"${o.value === field.value ? " selected" : ""}>${escapeHtml3(
1433
+ o.label
1434
+ )}</option>`
1435
+ ).join("");
1436
+ control = `<select id="${escapeHtml3(id)}" name="${safeName}"${requiredAttr} class="${inputClass}">${opts}</select>`;
1437
+ } else {
1438
+ const placeholder = field.placeholder ? ` placeholder="${escapeHtml3(field.placeholder)}"` : "";
1439
+ control = `<input type="${escapeHtml3(field.type)}" id="${escapeHtml3(
1440
+ id
1441
+ )}" name="${safeName}"${requiredAttr}${placeholder} value="${safeValue}" class="${inputClass}">`;
1442
+ }
1443
+ return `<div class="mb-4">
1444
+ <label for="${escapeHtml3(id)}" class="block text-sm font-medium text-gray-700 mb-2">${labelText}</label>
1445
+ ${control}
1446
+ </div>`;
1447
+ }
1360
1448
  function buildLoginPage(params) {
1361
- const { clientName, scope, pendingAuthId, callbackPath } = params;
1449
+ const { clientName, scope, pendingAuthId, callbackPath, title, subtitle, logoUri, error, extraFields } = params;
1362
1450
  const scopesHtml = scope ? `<div class="p-4 bg-gray-50 rounded-lg mb-6">
1363
1451
  <p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">Requested permissions</p>
1364
1452
  <p class="text-gray-700">${scope.split(" ").map((s) => escapeHtml3(s)).join(", ") || "Default access"}</p>
1365
1453
  </div>` : "";
1366
- const content = `
1367
- <div class="bg-white rounded-2xl shadow-xl p-8">
1368
- <h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">Sign In</h1>
1369
- <p class="text-gray-600 mb-8 text-center">Authorize access to ${escapeHtml3(clientName)}</p>
1370
-
1371
- ${scopesHtml}
1372
-
1373
- <form method="GET" action="${escapeHtml3(callbackPath)}">
1374
- <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1375
-
1454
+ const defaultFieldsHtml = `
1376
1455
  <!-- Email -->
1377
1456
  <div class="mb-4">
1378
1457
  <label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
@@ -1385,7 +1464,27 @@ function buildLoginPage(params) {
1385
1464
  <label for="name" class="block text-sm font-medium text-gray-700 mb-2">Name (optional)</label>
1386
1465
  <input type="text" id="name" name="name" placeholder="Your name"
1387
1466
  class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
1388
- </div>
1467
+ </div>`;
1468
+ const fieldsHtml = extraFields && extraFields.length > 0 ? extraFields.map((f) => buildLoginFieldHtml(f)).join("\n") : defaultFieldsHtml;
1469
+ const logoHtml = logoUri ? `<div class="flex justify-center mb-4"><img src="${escapeHtml3(
1470
+ logoUri
1471
+ )}" alt="${escapeHtml3(clientName)}" class="h-12 w-12 rounded-lg object-cover"></div>` : "";
1472
+ const errorHtml = error ? `<div class="p-3 mb-6 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">${escapeHtml3(error)}</div>` : "";
1473
+ const headingText = escapeHtml3(title ?? "Sign In");
1474
+ const subtitleText = subtitle ? escapeHtml3(subtitle) : `Authorize access to ${escapeHtml3(clientName)}`;
1475
+ const content = `
1476
+ <div class="bg-white rounded-2xl shadow-xl p-8">
1477
+ ${logoHtml}
1478
+ <h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">${headingText}</h1>
1479
+ <p class="text-gray-600 mb-8 text-center">${subtitleText}</p>
1480
+
1481
+ ${errorHtml}
1482
+
1483
+ ${scopesHtml}
1484
+
1485
+ <form method="GET" action="${escapeHtml3(callbackPath)}">
1486
+ <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1487
+ ${fieldsHtml}
1389
1488
 
1390
1489
  <!-- Submit -->
1391
1490
  <button type="submit"
@@ -1396,7 +1495,51 @@ function buildLoginPage(params) {
1396
1495
 
1397
1496
  <p class="text-center text-sm text-gray-500 mt-6">Client: ${escapeHtml3(clientName)}</p>
1398
1497
  </div>`;
1399
- return centeredCardLayout(content, { title: "Sign In" });
1498
+ return centeredCardLayout(content, { title: title ?? "Sign In" });
1499
+ }
1500
+ function buildConnectPage(params) {
1501
+ const { token, field, action, title, subtitle, logoUri, error } = params;
1502
+ const logoHtml = logoUri ? `<div class="flex justify-center mb-4"><img src="${escapeHtml3(
1503
+ logoUri
1504
+ )}" alt="Connect" class="h-12 w-12 rounded-lg object-cover"></div>` : "";
1505
+ const errorHtml = error ? `<div class="p-3 mb-6 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">${escapeHtml3(error)}</div>` : "";
1506
+ const headingText = escapeHtml3(title ?? "Connect credential");
1507
+ const subtitleText = subtitle ? escapeHtml3(subtitle) : `Connect "${escapeHtml3(field.name)}" to continue`;
1508
+ const content = `
1509
+ <div class="bg-white rounded-2xl shadow-xl p-8">
1510
+ ${logoHtml}
1511
+ <h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">${headingText}</h1>
1512
+ <p class="text-gray-600 mb-8 text-center">${subtitleText}</p>
1513
+
1514
+ ${errorHtml}
1515
+
1516
+ <form method="POST" action="${escapeHtml3(action)}">
1517
+ <input type="hidden" name="token" value="${escapeHtml3(token)}">
1518
+ ${buildLoginFieldHtml(field)}
1519
+
1520
+ <button type="submit"
1521
+ class="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
1522
+ Connect
1523
+ </button>
1524
+ </form>
1525
+ </div>`;
1526
+ return centeredCardLayout(content, { title: title ?? "Connect credential" });
1527
+ }
1528
+ function buildConnectSuccessPage(params) {
1529
+ const { key, message } = params;
1530
+ const content = `
1531
+ <div class="bg-white rounded-2xl shadow-xl p-8 text-center">
1532
+ <div class="flex justify-center mb-6">
1533
+ <div class="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
1534
+ <svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1535
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
1536
+ </svg>
1537
+ </div>
1538
+ </div>
1539
+ <h1 class="text-2xl font-bold text-gray-900 mb-4">Credential connected</h1>
1540
+ <p class="text-gray-600">${escapeHtml3(message ?? `"${key}" is now connected. You can return to your session.`)}</p>
1541
+ </div>`;
1542
+ return centeredCardLayout(content, { title: "Connected" });
1400
1543
  }
1401
1544
  function buildErrorPage(params) {
1402
1545
  const { error, description } = params;
@@ -1423,6 +1566,37 @@ function renderToHtml(html, _options) {
1423
1566
  return html;
1424
1567
  }
1425
1568
 
1569
+ // libs/auth/src/ui/local-login.helper.ts
1570
+ function toLoginExtraFields(fields, values) {
1571
+ if (!fields) return [];
1572
+ return Object.entries(fields).map(([name, def]) => ({
1573
+ name,
1574
+ type: def.type,
1575
+ label: def.label,
1576
+ required: def.required,
1577
+ placeholder: def.placeholder,
1578
+ options: def.options,
1579
+ value: values?.[name]
1580
+ }));
1581
+ }
1582
+ function renderLocalLoginPage(login, ctx, values) {
1583
+ if (login?.render) {
1584
+ return login.render({ ...ctx, values });
1585
+ }
1586
+ const extraFields = login?.fields ? toLoginExtraFields(login.fields, values) : void 0;
1587
+ return buildLoginPage({
1588
+ clientName: ctx.clientName,
1589
+ scope: ctx.scopes.join(" "),
1590
+ pendingAuthId: ctx.pendingAuthId,
1591
+ callbackPath: ctx.callbackPath,
1592
+ title: login?.title,
1593
+ subtitle: login?.subtitle,
1594
+ logoUri: login?.logoUri ?? ctx.logoUri,
1595
+ error: ctx.error,
1596
+ extraFields
1597
+ });
1598
+ }
1599
+
1426
1600
  // libs/auth/src/session/authorization.store.ts
1427
1601
  var import_lazy_zod = require("@frontmcp/lazy-zod");
1428
1602
  var import_utils4 = require("@frontmcp/utils");
@@ -1450,8 +1624,83 @@ var authorizationCodeRecordSchema = import_lazy_zod.z.object({
1450
1624
  skippedProviderIds: import_lazy_zod.z.array(import_lazy_zod.z.string()).optional(),
1451
1625
  consentEnabled: import_lazy_zod.z.boolean().optional(),
1452
1626
  federatedLoginUsed: import_lazy_zod.z.boolean().optional(),
1453
- pendingAuthId: import_lazy_zod.z.string().optional()
1627
+ pendingAuthId: import_lazy_zod.z.string().optional(),
1628
+ // Progressive/Incremental authorization: granted app-id set.
1629
+ authorizedAppIds: import_lazy_zod.z.array(import_lazy_zod.z.string()).optional(),
1630
+ // Custom claims from a local authenticate() verifier (Checkpoint 3a).
1631
+ customClaims: import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown()).optional()
1454
1632
  });
1633
+ var AUTH_CODE_TTL_MS = 60 * 1e3;
1634
+ var PENDING_AUTH_TTL_MS = 10 * 60 * 1e3;
1635
+ var REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
1636
+ function generateAuthorizationCode() {
1637
+ return (0, import_utils4.randomUUID)().replace(/-/g, "") + (0, import_utils4.randomUUID)().replace(/-/g, "");
1638
+ }
1639
+ function generateRefreshTokenValue() {
1640
+ return (0, import_utils4.randomUUID)() + "-" + (0, import_utils4.randomUUID)();
1641
+ }
1642
+ function buildCodeRecord(params) {
1643
+ const now = Date.now();
1644
+ return {
1645
+ code: generateAuthorizationCode(),
1646
+ clientId: params.clientId,
1647
+ redirectUri: params.redirectUri,
1648
+ scopes: params.scopes,
1649
+ pkce: params.pkce,
1650
+ userSub: params.userSub,
1651
+ userEmail: params.userEmail,
1652
+ userName: params.userName,
1653
+ state: params.state,
1654
+ resource: params.resource,
1655
+ createdAt: now,
1656
+ expiresAt: now + AUTH_CODE_TTL_MS,
1657
+ used: false,
1658
+ selectedToolIds: params.selectedToolIds,
1659
+ selectedProviderIds: params.selectedProviderIds,
1660
+ skippedProviderIds: params.skippedProviderIds,
1661
+ consentEnabled: params.consentEnabled,
1662
+ federatedLoginUsed: params.federatedLoginUsed,
1663
+ pendingAuthId: params.pendingAuthId,
1664
+ authorizedAppIds: params.authorizedAppIds,
1665
+ customClaims: params.customClaims
1666
+ };
1667
+ }
1668
+ function buildPendingRecord(params) {
1669
+ const now = Date.now();
1670
+ return {
1671
+ id: (0, import_utils4.randomUUID)(),
1672
+ clientId: params.clientId,
1673
+ redirectUri: params.redirectUri,
1674
+ scopes: params.scopes,
1675
+ pkce: params.pkce,
1676
+ state: params.state,
1677
+ resource: params.resource,
1678
+ createdAt: now,
1679
+ expiresAt: now + PENDING_AUTH_TTL_MS,
1680
+ isIncremental: params.isIncremental,
1681
+ targetAppId: params.targetAppId,
1682
+ targetToolId: params.targetToolId,
1683
+ existingSessionId: params.existingSessionId,
1684
+ existingAuthorizationId: params.existingAuthorizationId,
1685
+ priorAuthorizedAppIds: params.priorAuthorizedAppIds,
1686
+ federatedLogin: params.federatedLogin,
1687
+ consent: params.consent,
1688
+ authUiCsrf: params.authUiCsrf
1689
+ };
1690
+ }
1691
+ function buildRefreshTokenRecord(params) {
1692
+ const now = Date.now();
1693
+ return {
1694
+ token: generateRefreshTokenValue(),
1695
+ clientId: params.clientId,
1696
+ userSub: params.userSub,
1697
+ scopes: params.scopes,
1698
+ resource: params.resource,
1699
+ createdAt: now,
1700
+ expiresAt: now + REFRESH_TOKEN_TTL_MS,
1701
+ revoked: false
1702
+ };
1703
+ }
1455
1704
  function verifyPkce(codeVerifier, challenge) {
1456
1705
  if (challenge.method !== "S256") {
1457
1706
  return false;
@@ -1467,17 +1716,11 @@ var InMemoryAuthorizationStore = class {
1467
1716
  codes = /* @__PURE__ */ new Map();
1468
1717
  pending = /* @__PURE__ */ new Map();
1469
1718
  refreshTokens = /* @__PURE__ */ new Map();
1470
- /** Default TTL for authorization codes (60 seconds) */
1471
- codeTtlMs = 60 * 1e3;
1472
- /** Default TTL for pending authorizations (10 minutes) */
1473
- pendingTtlMs = 10 * 60 * 1e3;
1474
- /** Default TTL for refresh tokens (30 days) */
1475
- refreshTtlMs = 30 * 24 * 60 * 60 * 1e3;
1476
1719
  generateCode() {
1477
- return (0, import_utils4.randomUUID)().replace(/-/g, "") + (0, import_utils4.randomUUID)().replace(/-/g, "");
1720
+ return generateAuthorizationCode();
1478
1721
  }
1479
1722
  generateRefreshToken() {
1480
- return (0, import_utils4.randomUUID)() + "-" + (0, import_utils4.randomUUID)();
1723
+ return generateRefreshTokenValue();
1481
1724
  }
1482
1725
  async storeAuthorizationCode(record) {
1483
1726
  this.codes.set(record.code, record);
@@ -1556,76 +1799,22 @@ var InMemoryAuthorizationStore = class {
1556
1799
  }
1557
1800
  }
1558
1801
  /**
1559
- * Create an authorization code record with defaults
1802
+ * Create an authorization code record with defaults.
1560
1803
  */
1561
1804
  createCodeRecord(params) {
1562
- const now = Date.now();
1563
- return {
1564
- code: this.generateCode(),
1565
- clientId: params.clientId,
1566
- redirectUri: params.redirectUri,
1567
- scopes: params.scopes,
1568
- pkce: params.pkce,
1569
- userSub: params.userSub,
1570
- userEmail: params.userEmail,
1571
- userName: params.userName,
1572
- state: params.state,
1573
- resource: params.resource,
1574
- createdAt: now,
1575
- expiresAt: now + this.codeTtlMs,
1576
- used: false,
1577
- // Consent and Federated Login Data
1578
- selectedToolIds: params.selectedToolIds,
1579
- selectedProviderIds: params.selectedProviderIds,
1580
- skippedProviderIds: params.skippedProviderIds,
1581
- consentEnabled: params.consentEnabled,
1582
- federatedLoginUsed: params.federatedLoginUsed,
1583
- // Token migration ID (for federated auth)
1584
- pendingAuthId: params.pendingAuthId
1585
- };
1805
+ return buildCodeRecord(params);
1586
1806
  }
1587
1807
  /**
1588
- * Create a pending authorization record with defaults
1808
+ * Create a pending authorization record with defaults.
1589
1809
  */
1590
1810
  createPendingRecord(params) {
1591
- const now = Date.now();
1592
- return {
1593
- id: (0, import_utils4.randomUUID)(),
1594
- clientId: params.clientId,
1595
- redirectUri: params.redirectUri,
1596
- scopes: params.scopes,
1597
- pkce: params.pkce,
1598
- state: params.state,
1599
- resource: params.resource,
1600
- createdAt: now,
1601
- expiresAt: now + this.pendingTtlMs,
1602
- // Progressive/Incremental Authorization Fields
1603
- isIncremental: params.isIncremental,
1604
- targetAppId: params.targetAppId,
1605
- targetToolId: params.targetToolId,
1606
- existingSessionId: params.existingSessionId,
1607
- existingAuthorizationId: params.existingAuthorizationId,
1608
- // Federated Login State
1609
- federatedLogin: params.federatedLogin,
1610
- // Consent State
1611
- consent: params.consent
1612
- };
1811
+ return buildPendingRecord(params);
1613
1812
  }
1614
1813
  /**
1615
- * Create a refresh token record with defaults
1814
+ * Create a refresh token record with defaults.
1616
1815
  */
1617
1816
  createRefreshTokenRecord(params) {
1618
- const now = Date.now();
1619
- return {
1620
- token: this.generateRefreshToken(),
1621
- clientId: params.clientId,
1622
- userSub: params.userSub,
1623
- scopes: params.scopes,
1624
- resource: params.resource,
1625
- createdAt: now,
1626
- expiresAt: now + this.refreshTtlMs,
1627
- revoked: false
1628
- };
1817
+ return buildRefreshTokenRecord(params);
1629
1818
  }
1630
1819
  };
1631
1820
  var RedisAuthorizationStore = class {
@@ -1637,10 +1826,19 @@ var RedisAuthorizationStore = class {
1637
1826
  return `${this.namespace}${type}:${id}`;
1638
1827
  }
1639
1828
  generateCode() {
1640
- return (0, import_utils4.randomUUID)().replace(/-/g, "") + (0, import_utils4.randomUUID)().replace(/-/g, "");
1829
+ return generateAuthorizationCode();
1641
1830
  }
1642
1831
  generateRefreshToken() {
1643
- return (0, import_utils4.randomUUID)() + "-" + (0, import_utils4.randomUUID)();
1832
+ return generateRefreshTokenValue();
1833
+ }
1834
+ createCodeRecord(params) {
1835
+ return buildCodeRecord(params);
1836
+ }
1837
+ createPendingRecord(params) {
1838
+ return buildPendingRecord(params);
1839
+ }
1840
+ createRefreshTokenRecord(params) {
1841
+ return buildRefreshTokenRecord(params);
1644
1842
  }
1645
1843
  async storeAuthorizationCode(record) {
1646
1844
  const ttl = Math.max(Math.ceil((record.expiresAt - Date.now()) / 1e3), 1);
@@ -1702,6 +1900,130 @@ var RedisAuthorizationStore = class {
1702
1900
  }
1703
1901
  };
1704
1902
 
1903
+ // libs/auth/src/session/storage-authorization.store.ts
1904
+ var import_utils5 = require("@frontmcp/utils");
1905
+ var StorageAuthorizationStore = class {
1906
+ codeStore;
1907
+ pendingStore;
1908
+ refreshStore;
1909
+ namespace;
1910
+ storageIsNamespaced;
1911
+ constructor(storage, options = {}) {
1912
+ this.namespace = options.namespace ?? "oauth";
1913
+ this.storageIsNamespaced = this.isNamespacedStorage(storage);
1914
+ const base = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
1915
+ this.codeStore = new import_utils5.TypedStorage(base);
1916
+ this.pendingStore = new import_utils5.TypedStorage(base);
1917
+ this.refreshStore = new import_utils5.TypedStorage(base);
1918
+ }
1919
+ // ============================================
1920
+ // Record builders (pure; shared with in-memory store)
1921
+ // ============================================
1922
+ generateCode() {
1923
+ return generateAuthorizationCode();
1924
+ }
1925
+ generateRefreshToken() {
1926
+ return generateRefreshTokenValue();
1927
+ }
1928
+ createCodeRecord(params) {
1929
+ return buildCodeRecord(params);
1930
+ }
1931
+ createPendingRecord(params) {
1932
+ return buildPendingRecord(params);
1933
+ }
1934
+ createRefreshTokenRecord(params) {
1935
+ return buildRefreshTokenRecord(params);
1936
+ }
1937
+ // ============================================
1938
+ // Authorization code operations
1939
+ // ============================================
1940
+ async storeAuthorizationCode(record) {
1941
+ await this.codeStore.set(this.key("code", record.code), record, this.ttl(record.expiresAt));
1942
+ }
1943
+ async getAuthorizationCode(code) {
1944
+ const record = await this.codeStore.get(this.key("code", code));
1945
+ if (!record) return null;
1946
+ if (Date.now() > record.expiresAt) {
1947
+ await this.codeStore.delete(this.key("code", code));
1948
+ return null;
1949
+ }
1950
+ return record;
1951
+ }
1952
+ async markCodeUsed(code) {
1953
+ const record = await this.getAuthorizationCode(code);
1954
+ if (record) {
1955
+ record.used = true;
1956
+ await this.codeStore.set(this.key("code", code), record, this.ttl(record.expiresAt));
1957
+ }
1958
+ }
1959
+ async deleteAuthorizationCode(code) {
1960
+ await this.codeStore.delete(this.key("code", code));
1961
+ }
1962
+ // ============================================
1963
+ // Pending authorization operations
1964
+ // ============================================
1965
+ async storePendingAuthorization(record) {
1966
+ await this.pendingStore.set(this.key("pending", record.id), record, this.ttl(record.expiresAt));
1967
+ }
1968
+ async getPendingAuthorization(id) {
1969
+ const record = await this.pendingStore.get(this.key("pending", id));
1970
+ if (!record) return null;
1971
+ if (Date.now() > record.expiresAt) {
1972
+ await this.pendingStore.delete(this.key("pending", id));
1973
+ return null;
1974
+ }
1975
+ return record;
1976
+ }
1977
+ async deletePendingAuthorization(id) {
1978
+ await this.pendingStore.delete(this.key("pending", id));
1979
+ }
1980
+ // ============================================
1981
+ // Refresh token operations
1982
+ // ============================================
1983
+ async storeRefreshToken(record) {
1984
+ await this.refreshStore.set(this.key("refresh", record.token), record, this.ttl(record.expiresAt));
1985
+ }
1986
+ async getRefreshToken(token) {
1987
+ const record = await this.refreshStore.get(this.key("refresh", token));
1988
+ if (!record) return null;
1989
+ if (Date.now() > record.expiresAt || record.revoked) {
1990
+ return null;
1991
+ }
1992
+ return record;
1993
+ }
1994
+ async revokeRefreshToken(token) {
1995
+ const record = await this.refreshStore.get(this.key("refresh", token));
1996
+ if (record) {
1997
+ record.revoked = true;
1998
+ await this.refreshStore.set(this.key("refresh", token), record, this.ttl(record.expiresAt));
1999
+ }
2000
+ }
2001
+ async rotateRefreshToken(oldToken, newRecord) {
2002
+ await this.revokeRefreshToken(oldToken);
2003
+ newRecord.previousToken = oldToken;
2004
+ await this.storeRefreshToken(newRecord);
2005
+ }
2006
+ // ============================================
2007
+ // Utility
2008
+ // ============================================
2009
+ async cleanup() {
2010
+ }
2011
+ // ============================================
2012
+ // Internals
2013
+ // ============================================
2014
+ ttl(expiresAtMs) {
2015
+ const ttl = Math.ceil((expiresAtMs - Date.now()) / 1e3);
2016
+ return { ttlSeconds: ttl > 0 ? ttl : 1 };
2017
+ }
2018
+ key(type, id) {
2019
+ const composite = `${type}:${id}`;
2020
+ return this.storageIsNamespaced ? composite : `${this.namespace}:${composite}`;
2021
+ }
2022
+ isNamespacedStorage(storage) {
2023
+ return "namespace" in storage && typeof storage.namespace === "function";
2024
+ }
2025
+ };
2026
+
1705
2027
  // libs/auth/src/session/authorization-vault.ts
1706
2028
  var import_lazy_zod2 = require("@frontmcp/lazy-zod");
1707
2029
  var credentialTypeSchema = import_lazy_zod2.z.enum([
@@ -1963,9 +2285,12 @@ var authorizationVaultEntrySchema = import_lazy_zod2.z.object({
1963
2285
  skippedAppIds: import_lazy_zod2.z.array(import_lazy_zod2.z.string())
1964
2286
  });
1965
2287
 
2288
+ // libs/auth/src/session/session-credential-vault.ts
2289
+ var import_utils7 = require("@frontmcp/utils");
2290
+
1966
2291
  // libs/auth/src/session/vault-encryption.ts
1967
2292
  var import_lazy_zod3 = require("@frontmcp/lazy-zod");
1968
- var import_utils5 = require("@frontmcp/utils");
2293
+ var import_utils6 = require("@frontmcp/utils");
1969
2294
  var encryptedDataSchema = import_lazy_zod3.z.object({
1970
2295
  /** Version for future algorithm changes */
1971
2296
  v: import_lazy_zod3.z.literal(1),
@@ -2005,7 +2330,7 @@ var VaultEncryption = class {
2005
2330
  ];
2006
2331
  const ikm = new TextEncoder().encode(ikmParts.join(""));
2007
2332
  const infoBytes = new TextEncoder().encode(this.hkdfInfo);
2008
- const key = await (0, import_utils5.hkdfSha256)(ikm, new Uint8Array(0), infoBytes, 32);
2333
+ const key = await (0, import_utils6.hkdfSha256)(ikm, new Uint8Array(0), infoBytes, 32);
2009
2334
  return key;
2010
2335
  }
2011
2336
  /**
@@ -2032,7 +2357,7 @@ var VaultEncryption = class {
2032
2357
  ];
2033
2358
  const ikm = new TextEncoder().encode(ikmParts.join(""));
2034
2359
  const infoBytes = new TextEncoder().encode(this.hkdfInfo);
2035
- const key = await (0, import_utils5.hkdfSha256)(ikm, new Uint8Array(0), infoBytes, 32);
2360
+ const key = await (0, import_utils6.hkdfSha256)(ikm, new Uint8Array(0), infoBytes, 32);
2036
2361
  return key;
2037
2362
  }
2038
2363
  /**
@@ -2046,14 +2371,14 @@ var VaultEncryption = class {
2046
2371
  if (key.length !== 32) {
2047
2372
  throw new Error("Encryption key must be 32 bytes");
2048
2373
  }
2049
- const iv = (0, import_utils5.randomBytes)(12);
2050
- const { ciphertext, tag } = await (0, import_utils5.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
2374
+ const iv = (0, import_utils6.randomBytes)(12);
2375
+ const { ciphertext, tag } = await (0, import_utils6.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
2051
2376
  return {
2052
2377
  v: 1,
2053
2378
  alg: "aes-256-gcm",
2054
- iv: (0, import_utils5.base64urlEncode)(iv),
2055
- ct: (0, import_utils5.base64urlEncode)(ciphertext),
2056
- tag: (0, import_utils5.base64urlEncode)(tag)
2379
+ iv: (0, import_utils6.base64urlEncode)(iv),
2380
+ ct: (0, import_utils6.base64urlEncode)(ciphertext),
2381
+ tag: (0, import_utils6.base64urlEncode)(tag)
2057
2382
  };
2058
2383
  }
2059
2384
  /**
@@ -2073,74 +2398,707 @@ var VaultEncryption = class {
2073
2398
  throw new Error("Invalid encrypted data format");
2074
2399
  }
2075
2400
  const { iv, ct, tag } = parsed.data;
2076
- const ivBuffer = (0, import_utils5.base64urlDecode)(iv);
2077
- const ciphertext = (0, import_utils5.base64urlDecode)(ct);
2078
- const tagBuffer = (0, import_utils5.base64urlDecode)(tag);
2401
+ const ivBuffer = (0, import_utils6.base64urlDecode)(iv);
2402
+ const ciphertext = (0, import_utils6.base64urlDecode)(ct);
2403
+ const tagBuffer = (0, import_utils6.base64urlDecode)(tag);
2079
2404
  try {
2080
- const plaintext = await (0, import_utils5.decryptAesGcm)(key, ciphertext, ivBuffer, tagBuffer);
2405
+ const plaintext = await (0, import_utils6.decryptAesGcm)(key, ciphertext, ivBuffer, tagBuffer);
2081
2406
  return new TextDecoder().decode(plaintext);
2082
2407
  } catch (_error) {
2083
2408
  throw new Error("Decryption failed: invalid key or corrupted data");
2084
2409
  }
2085
2410
  }
2086
- /**
2087
- * Encrypt a JavaScript object (serializes to JSON first)
2088
- *
2089
- * @param data - Object to encrypt
2090
- * @param key - Encryption key
2091
- * @returns Encrypted data
2092
- */
2093
- async encryptObject(data, key) {
2094
- return this.encrypt(JSON.stringify(data), key);
2411
+ /**
2412
+ * Encrypt a JavaScript object (serializes to JSON first)
2413
+ *
2414
+ * @param data - Object to encrypt
2415
+ * @param key - Encryption key
2416
+ * @returns Encrypted data
2417
+ */
2418
+ async encryptObject(data, key) {
2419
+ return this.encrypt(JSON.stringify(data), key);
2420
+ }
2421
+ /**
2422
+ * Decrypt and parse a JavaScript object
2423
+ *
2424
+ * @param encrypted - Encrypted data
2425
+ * @param key - Encryption key
2426
+ * @returns Decrypted and parsed object
2427
+ */
2428
+ async decryptObject(encrypted, key) {
2429
+ const plaintext = await this.decrypt(encrypted, key);
2430
+ return JSON.parse(plaintext);
2431
+ }
2432
+ /**
2433
+ * Check if data is in encrypted format
2434
+ *
2435
+ * @param data - Data to check
2436
+ * @returns True if data appears to be encrypted
2437
+ */
2438
+ isEncrypted(data) {
2439
+ return encryptedDataSchema.safeParse(data).success;
2440
+ }
2441
+ };
2442
+ var encryptedVaultEntrySchema = import_lazy_zod3.z.object({
2443
+ /** Vault ID (maps to JWT jti claim) */
2444
+ id: import_lazy_zod3.z.string(),
2445
+ /** User subject identifier */
2446
+ userSub: import_lazy_zod3.z.string(),
2447
+ /** User email (unencrypted for display) */
2448
+ userEmail: import_lazy_zod3.z.string().optional(),
2449
+ /** User name (unencrypted for display) */
2450
+ userName: import_lazy_zod3.z.string().optional(),
2451
+ /** Client ID that created this session */
2452
+ clientId: import_lazy_zod3.z.string(),
2453
+ /** Creation timestamp */
2454
+ createdAt: import_lazy_zod3.z.number(),
2455
+ /** Last access timestamp */
2456
+ lastAccessAt: import_lazy_zod3.z.number(),
2457
+ /** Encrypted sensitive data (provider tokens, credentials, consent) */
2458
+ encryptedData: encryptedDataSchema,
2459
+ /** Apps that are fully authorized (unencrypted for quick lookup) */
2460
+ authorizedAppIds: import_lazy_zod3.z.array(import_lazy_zod3.z.string()),
2461
+ /** Apps that were skipped (unencrypted for quick lookup) */
2462
+ skippedAppIds: import_lazy_zod3.z.array(import_lazy_zod3.z.string()),
2463
+ /** Pending auth IDs (unencrypted for lookup, actual URLs encrypted) */
2464
+ pendingAuthIds: import_lazy_zod3.z.array(import_lazy_zod3.z.string()).default([])
2465
+ });
2466
+
2467
+ // libs/auth/src/session/session-credential-vault.ts
2468
+ var DEFAULT_KEY_PREFIX = "mcp:cred:";
2469
+ var DEFAULT_PENDING_TTL_MS = 6e5;
2470
+ var SessionCredentialVault = class {
2471
+ storage;
2472
+ encryption;
2473
+ keyPrefix;
2474
+ ttlMs;
2475
+ pendingTtlMs;
2476
+ logger;
2477
+ /** Cache of derived AES keys, keyed by `${sub}:${vaultId}`. */
2478
+ keyCache = /* @__PURE__ */ new Map();
2479
+ constructor(options) {
2480
+ this.storage = options.storage;
2481
+ this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;
2482
+ this.ttlMs = options.ttlMs;
2483
+ this.pendingTtlMs = options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
2484
+ this.logger = options.logger ?? noopAuthLogger;
2485
+ const pepper = options.pepper ?? resolveVaultPepper(this.logger);
2486
+ this.encryption = new VaultEncryption({ pepper, hkdfInfo: "frontmcp-session-credential-vault-v1" });
2487
+ }
2488
+ // ============================================
2489
+ // Storage-key helpers
2490
+ // ============================================
2491
+ /** `cred:idx:<sub>` → current vaultId (the sub → vaultId indirection). */
2492
+ indexKey(sub) {
2493
+ return `${this.keyPrefix}idx:${sub}`;
2494
+ }
2495
+ /** `cred:keys:<sub>:<vaultId>` → JSON array of credential key names (the SET). */
2496
+ keysKey(sub, vaultId) {
2497
+ return `${this.keyPrefix}keys:${sub}:${vaultId}`;
2498
+ }
2499
+ /** `cred:data:<sub>:<vaultId>:<key>` → encrypted credential record. */
2500
+ dataKey(sub, vaultId, key) {
2501
+ return `${this.keyPrefix}data:${sub}:${vaultId}:${key}`;
2502
+ }
2503
+ /** `cred:pending:<pendingId>` → encrypted pending-authorize accumulator. */
2504
+ pendingKey(pendingId) {
2505
+ return `${this.keyPrefix}pending:${pendingId}`;
2506
+ }
2507
+ /** Convert the configured `ttlMs` into `SetOptions` (storage uses `ttlSeconds`). */
2508
+ get credentialSetOpts() {
2509
+ return this.ttlMs ? { ttlSeconds: Math.ceil(this.ttlMs / 1e3) } : void 0;
2510
+ }
2511
+ // ============================================
2512
+ // Key derivation
2513
+ // ============================================
2514
+ /**
2515
+ * Derive (and cache) the AES-256 key for a `(sub, vaultId)` pair. The vaultId
2516
+ * acts as the HKDF salt, so rotating it makes prior ciphertext undecryptable.
2517
+ */
2518
+ deriveKey(sub, vaultId) {
2519
+ const cacheKey = `${sub}:${vaultId}`;
2520
+ const cached = this.keyCache.get(cacheKey);
2521
+ if (cached) return cached;
2522
+ const promise = this.encryption.deriveKey({ jti: vaultId, vaultKey: vaultId, sub, iat: 0 });
2523
+ this.keyCache.set(cacheKey, promise);
2524
+ return promise;
2525
+ }
2526
+ // ============================================
2527
+ // vaultId indirection
2528
+ // ============================================
2529
+ /**
2530
+ * Read the current vaultId for a subject, or undefined when the subject has no
2531
+ * live vault (never authorized, or rotated away).
2532
+ */
2533
+ async getVaultId(sub) {
2534
+ const raw = await this.storage.get(this.indexKey(sub));
2535
+ return raw ?? void 0;
2536
+ }
2537
+ /**
2538
+ * Mint a FRESH vaultId for a subject and point the indirection at it. Called
2539
+ * at the start of each authorize so reconnect = empty vault. Returns the new
2540
+ * vaultId. The old vaultId's ciphertext is left in storage but is
2541
+ * unreachable (no index pointer) and undecryptable (different derived key);
2542
+ * it expires via TTL when configured.
2543
+ */
2544
+ async rotateVault(sub) {
2545
+ const vaultId = (0, import_utils7.randomUUID)();
2546
+ await this.storage.set(this.indexKey(sub), vaultId, this.credentialSetOpts);
2547
+ return vaultId;
2548
+ }
2549
+ // ============================================
2550
+ // Credential CRUD (committed vault)
2551
+ // ============================================
2552
+ /**
2553
+ * Encrypt + store a single credential into the vault identified by
2554
+ * `(sub, vaultId)` and add its key to the key set. Used both to commit
2555
+ * authorize-time credentials and to additively add a credential mid-session.
2556
+ */
2557
+ async store(sub, vaultId, key, value) {
2558
+ const aesKey = await this.deriveKey(sub, vaultId);
2559
+ const enc = await this.encryption.encryptObject(this.normalize(value), aesKey);
2560
+ const record = { enc };
2561
+ await this.storage.set(this.dataKey(sub, vaultId, key), JSON.stringify(record), this.credentialSetOpts);
2562
+ await this.addKeyToSet(sub, vaultId, key);
2563
+ }
2564
+ /**
2565
+ * Decrypt and return a credential by key for a subject's CURRENT vault.
2566
+ * Returns undefined when there is no live vault or no such key, or when
2567
+ * decryption fails (e.g. a stale pointer to rotated-away ciphertext).
2568
+ */
2569
+ async get(sub, key) {
2570
+ const vaultId = await this.getVaultId(sub);
2571
+ if (!vaultId) return void 0;
2572
+ const raw = await this.storage.get(this.dataKey(sub, vaultId, key));
2573
+ if (!raw) return void 0;
2574
+ try {
2575
+ const record = JSON.parse(raw);
2576
+ const aesKey = await this.deriveKey(sub, vaultId);
2577
+ return await this.encryption.decryptObject(record.enc, aesKey);
2578
+ } catch (err) {
2579
+ this.logger.warn(`SessionCredentialVault: failed to read credential "${key}": ${errMsg(err)}`);
2580
+ return void 0;
2581
+ }
2582
+ }
2583
+ /**
2584
+ * List the credential keys available in a subject's current vault. Returns an
2585
+ * empty array when there is no live vault (e.g. after rotation on reconnect).
2586
+ */
2587
+ async list(sub) {
2588
+ const vaultId = await this.getVaultId(sub);
2589
+ if (!vaultId) return [];
2590
+ return this.readKeySet(sub, vaultId);
2591
+ }
2592
+ /**
2593
+ * Remove a credential by key from a subject's current vault (deletes the
2594
+ * ciphertext and drops the key from the set). No-op when absent.
2595
+ */
2596
+ async remove(sub, key) {
2597
+ const vaultId = await this.getVaultId(sub);
2598
+ if (!vaultId) return;
2599
+ await this.storage.delete(this.dataKey(sub, vaultId, key));
2600
+ const keys = await this.readKeySet(sub, vaultId);
2601
+ const next = keys.filter((k) => k !== key);
2602
+ if (next.length !== keys.length) {
2603
+ await this.writeKeySet(sub, vaultId, next);
2604
+ }
2605
+ }
2606
+ // ============================================
2607
+ // Authorize-time accumulation + commit
2608
+ // ============================================
2609
+ /**
2610
+ * Stage credentials returned by `authenticate()` under a pending id, encrypted
2611
+ * against a freshly-rotated vaultId for `sub`. They are NOT visible to tools
2612
+ * until {@link commit} flushes them — this lets the authorize flow accumulate
2613
+ * credentials before the subject's identity is final.
2614
+ *
2615
+ * @returns the vaultId the credentials were staged against.
2616
+ */
2617
+ async stage(pendingId, sub, credentials) {
2618
+ const vaultId = await this.rotateVault(sub);
2619
+ const creds = {};
2620
+ for (const cred of credentials) {
2621
+ const aesKey = await this.deriveKey(sub, vaultId);
2622
+ const { key, ...value } = cred;
2623
+ creds[key] = { enc: await this.encryption.encryptObject(this.normalize(value), aesKey) };
2624
+ }
2625
+ const batch = { sub, vaultId, creds };
2626
+ await this.storage.set(this.pendingKey(pendingId), JSON.stringify(batch), {
2627
+ ttlSeconds: Math.ceil(this.pendingTtlMs / 1e3)
2628
+ });
2629
+ return vaultId;
2630
+ }
2631
+ /**
2632
+ * Flush a staged batch into the live vault, binding it to `sub`. The staged
2633
+ * batch's `sub` must equal the supplied `sub` (the one baked into the minted
2634
+ * token) — a mismatch is refused so credentials can never be committed under a
2635
+ * different identity than the one that will read them.
2636
+ *
2637
+ * @returns true when a batch was committed, false when none was staged.
2638
+ */
2639
+ async commit(pendingId, sub) {
2640
+ const raw = await this.storage.get(this.pendingKey(pendingId));
2641
+ if (!raw) return false;
2642
+ let batch;
2643
+ try {
2644
+ batch = JSON.parse(raw);
2645
+ } catch {
2646
+ await this.storage.delete(this.pendingKey(pendingId));
2647
+ return false;
2648
+ }
2649
+ if (batch.sub !== sub) {
2650
+ this.logger.warn("SessionCredentialVault: pending batch subject mismatch on commit; discarding");
2651
+ await this.storage.delete(this.pendingKey(pendingId));
2652
+ return false;
2653
+ }
2654
+ await this.storage.set(this.indexKey(sub), batch.vaultId, this.credentialSetOpts);
2655
+ const keys = [];
2656
+ for (const [key, record] of Object.entries(batch.creds)) {
2657
+ await this.storage.set(this.dataKey(sub, batch.vaultId, key), JSON.stringify(record), this.credentialSetOpts);
2658
+ keys.push(key);
2659
+ }
2660
+ await this.writeKeySet(sub, batch.vaultId, keys);
2661
+ await this.storage.delete(this.pendingKey(pendingId));
2662
+ return true;
2663
+ }
2664
+ // ============================================
2665
+ // Key-set helpers
2666
+ // ============================================
2667
+ async readKeySet(sub, vaultId) {
2668
+ const raw = await this.storage.get(this.keysKey(sub, vaultId));
2669
+ if (!raw) return [];
2670
+ try {
2671
+ const parsed = JSON.parse(raw);
2672
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
2673
+ } catch {
2674
+ return [];
2675
+ }
2676
+ }
2677
+ async writeKeySet(sub, vaultId, keys) {
2678
+ const unique = [...new Set(keys)];
2679
+ await this.storage.set(this.keysKey(sub, vaultId), JSON.stringify(unique), this.credentialSetOpts);
2680
+ }
2681
+ async addKeyToSet(sub, vaultId, key) {
2682
+ const keys = await this.readKeySet(sub, vaultId);
2683
+ if (!keys.includes(key)) {
2684
+ keys.push(key);
2685
+ await this.writeKeySet(sub, vaultId, keys);
2686
+ }
2687
+ }
2688
+ /** Strip undefined metadata so the encrypted JSON stays minimal. */
2689
+ normalize(value) {
2690
+ return value.metadata === void 0 ? { secret: value.secret } : { secret: value.secret, metadata: value.metadata };
2691
+ }
2692
+ };
2693
+ function resolveVaultPepper(logger) {
2694
+ const fromEnv = (0, import_utils7.getEnv)("VAULT_SECRET") ?? (0, import_utils7.getEnv)("JWT_SECRET");
2695
+ if (fromEnv && fromEnv.trim().length > 0) return fromEnv;
2696
+ logger.warn(
2697
+ "SessionCredentialVault: neither VAULT_SECRET nor JWT_SECRET is set; using a random per-process pepper. Stored credentials will NOT survive a restart and cannot be shared across instances. Set VAULT_SECRET (or JWT_SECRET) for persistent, multi-instance credential storage."
2698
+ );
2699
+ return (0, import_utils7.randomUUID)() + (0, import_utils7.randomUUID)();
2700
+ }
2701
+ function errMsg(err) {
2702
+ return err instanceof Error ? err.message : String(err);
2703
+ }
2704
+
2705
+ // libs/auth/src/session/token-storage.factory.ts
2706
+ var import_utils8 = require("@frontmcp/utils");
2707
+ function isRedisTokenStorage(config) {
2708
+ return typeof config === "object" && config !== null && "redis" in config && !!config.redis;
2709
+ }
2710
+ function isSqliteTokenStorage(config) {
2711
+ return typeof config === "object" && config !== null && "sqlite" in config && !!config.sqlite;
2712
+ }
2713
+ function isPersistentTokenStorage(config) {
2714
+ return config !== void 0 && config !== "memory";
2715
+ }
2716
+ async function createTokenStorageAdapter(config) {
2717
+ if (config === void 0 || config === "memory") {
2718
+ const adapter = new import_utils8.MemoryStorageAdapter();
2719
+ await adapter.connect();
2720
+ return adapter;
2721
+ }
2722
+ if (isSqliteTokenStorage(config)) {
2723
+ const { SqliteStorageAdapter } = require("@frontmcp/storage-sqlite");
2724
+ const adapter = new SqliteStorageAdapter({
2725
+ path: config.sqlite.path,
2726
+ encryption: config.sqlite.encryption,
2727
+ ttlCleanupIntervalMs: config.sqlite.ttlCleanupIntervalMs ?? 6e4,
2728
+ walMode: config.sqlite.walMode ?? true
2729
+ });
2730
+ await adapter.connect();
2731
+ return adapter;
2732
+ }
2733
+ if (isRedisTokenStorage(config)) {
2734
+ const { host, port, password, db, tls, keyPrefix } = config.redis;
2735
+ const adapter = new import_utils8.RedisStorageAdapter({
2736
+ config: { host, port, password, db, tls },
2737
+ keyPrefix: keyPrefix ?? ""
2738
+ });
2739
+ await adapter.connect();
2740
+ return adapter;
2741
+ }
2742
+ const fallback = new import_utils8.MemoryStorageAdapter();
2743
+ await fallback.connect();
2744
+ return fallback;
2745
+ }
2746
+
2747
+ // libs/auth/src/session/session-credential-vault.factory.ts
2748
+ async function createSessionCredentialVault(options) {
2749
+ const storage = options.storage ?? await createTokenStorageAdapter(options.tokenStorage);
2750
+ const vault = new SessionCredentialVault({
2751
+ storage,
2752
+ pepper: options.pepper,
2753
+ keyPrefix: options.keyPrefix,
2754
+ ttlMs: options.ttlMs,
2755
+ logger: options.logger
2756
+ });
2757
+ const type = options.tokenStorage === void 0 || options.tokenStorage === "memory" ? "memory" : "sqlite" in options.tokenStorage ? "sqlite" : "redis";
2758
+ return { vault, type };
2759
+ }
2760
+
2761
+ // libs/auth/src/session/credential-resume-link.ts
2762
+ var import_utils9 = require("@frontmcp/utils");
2763
+ var DEFAULT_RESUME_TTL_MS = 6e5;
2764
+ function signCredentialResumeToken(input, secret) {
2765
+ const payload = {
2766
+ sub: input.sub,
2767
+ key: input.key,
2768
+ ...input.context !== void 0 ? { context: input.context } : {},
2769
+ exp: Date.now() + (input.ttlMs ?? DEFAULT_RESUME_TTL_MS)
2770
+ };
2771
+ const signedJson = (0, import_utils9.signData)(payload, { secret });
2772
+ return Buffer.from(signedJson, "utf8").toString("base64url");
2773
+ }
2774
+ function verifyCredentialResumeToken(token, secret, now = Date.now()) {
2775
+ let signedJson;
2776
+ try {
2777
+ signedJson = Buffer.from(token, "base64url").toString("utf8");
2778
+ } catch {
2779
+ return null;
2780
+ }
2781
+ const payload = (0, import_utils9.verifyData)(signedJson, { secret });
2782
+ if (!payload) return null;
2783
+ if (typeof payload.sub !== "string" || typeof payload.key !== "string" || !Number.isFinite(payload.exp)) {
2784
+ return null;
2785
+ }
2786
+ if (payload.context !== void 0 && typeof payload.context !== "string") {
2787
+ return null;
2788
+ }
2789
+ if (now >= payload.exp) {
2790
+ return null;
2791
+ }
2792
+ return payload;
2793
+ }
2794
+ function buildCredentialResumeUrl(basePath, token) {
2795
+ const trimmed = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
2796
+ return `${trimmed}/oauth/connect?token=${encodeURIComponent(token)}`;
2797
+ }
2798
+
2799
+ // libs/auth/src/session/credentials-accessor.ts
2800
+ var CREDENTIALS_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:CREDENTIALS_ACCESSOR");
2801
+
2802
+ // libs/auth/src/session/credentials-accessor.impl.ts
2803
+ var CredentialsAccessorImpl = class {
2804
+ vault;
2805
+ resolveSub;
2806
+ signingSecret;
2807
+ basePath;
2808
+ resumeTtlMs;
2809
+ logger;
2810
+ constructor(deps) {
2811
+ this.vault = deps.vault;
2812
+ this.resolveSub = deps.resolveSub;
2813
+ this.signingSecret = deps.signingSecret;
2814
+ this.basePath = deps.basePath;
2815
+ this.resumeTtlMs = deps.resumeTtlMs;
2816
+ this.logger = deps.logger ?? noopAuthLogger;
2817
+ }
2818
+ async get(key) {
2819
+ const sub = this.resolveSub();
2820
+ if (!sub) return void 0;
2821
+ return this.vault.get(sub, key);
2822
+ }
2823
+ async list() {
2824
+ const sub = this.resolveSub();
2825
+ if (!sub) return [];
2826
+ return this.vault.list(sub);
2827
+ }
2828
+ async requireConnect(options) {
2829
+ const { key, context } = options;
2830
+ const sub = this.resolveSub();
2831
+ if (sub) {
2832
+ const existing = await this.vault.get(sub, key);
2833
+ if (existing) {
2834
+ return { connected: true, key, credential: existing };
2835
+ }
2836
+ }
2837
+ const token = signCredentialResumeToken(
2838
+ { sub: sub ?? "", key, context, ttlMs: this.resumeTtlMs },
2839
+ this.signingSecret
2840
+ );
2841
+ const resumeUrl = buildCredentialResumeUrl(this.basePath, token);
2842
+ this.logger.debug(`CredentialsAccessor: credential "${key}" not connected; issued resume link`);
2843
+ return {
2844
+ connected: false,
2845
+ key,
2846
+ resumeUrl,
2847
+ message: `Credential "${key}" is not connected. Open the resume URL to connect it.`
2848
+ };
2849
+ }
2850
+ };
2851
+
2852
+ // libs/auth/src/session/secure-store-backends.ts
2853
+ var DEFAULT_KEY_PREFIX2 = "mcp:secret:";
2854
+ var EncryptedStorageSecureStoreBackend = class {
2855
+ storage;
2856
+ encryption;
2857
+ keyPrefix;
2858
+ logger;
2859
+ /** Cache of derived AES keys, keyed by namespace. */
2860
+ keyCache = /* @__PURE__ */ new Map();
2861
+ constructor(options) {
2862
+ this.storage = options.storage;
2863
+ this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX2;
2864
+ this.logger = options.logger ?? noopAuthLogger;
2865
+ this.encryption = new VaultEncryption({
2866
+ pepper: options.pepper,
2867
+ hkdfInfo: "frontmcp-secure-store-v1"
2868
+ });
2869
+ }
2870
+ // ============================================
2871
+ // Storage-key helpers
2872
+ // ============================================
2873
+ /** `secret:keys:<namespace>` → JSON array of key names. */
2874
+ keysKey(namespace) {
2875
+ return `${this.keyPrefix}keys:${namespace}`;
2876
+ }
2877
+ /** `secret:data:<namespace>:<key>` → encrypted record. */
2878
+ dataKey(namespace, key) {
2879
+ return `${this.keyPrefix}data:${namespace}:${key}`;
2880
+ }
2881
+ /**
2882
+ * Derive (and cache) the AES-256 key for a namespace. The namespace acts as
2883
+ * the HKDF salt material, so values are cryptographically isolated per scope.
2884
+ */
2885
+ deriveKey(namespace) {
2886
+ const cached = this.keyCache.get(namespace);
2887
+ if (cached) return cached;
2888
+ const promise = this.encryption.deriveKey({ jti: namespace, vaultKey: namespace, sub: namespace, iat: 0 });
2889
+ this.keyCache.set(namespace, promise);
2890
+ return promise;
2891
+ }
2892
+ setOpts(ttlMs) {
2893
+ return ttlMs && ttlMs > 0 ? { ttlSeconds: Math.ceil(ttlMs / 1e3) } : void 0;
2894
+ }
2895
+ // ============================================
2896
+ // SecureStoreBackend
2897
+ // ============================================
2898
+ async get(namespace, key) {
2899
+ const raw = await this.storage.get(this.dataKey(namespace, key));
2900
+ if (!raw) return null;
2901
+ try {
2902
+ const record = JSON.parse(raw);
2903
+ const aesKey = await this.deriveKey(namespace);
2904
+ return await this.encryption.decrypt(record.enc, aesKey);
2905
+ } catch (err) {
2906
+ this.logger.warn(`SecureStore: failed to read secret "${key}": ${errMsg2(err)}`);
2907
+ return null;
2908
+ }
2909
+ }
2910
+ async set(namespace, key, value, ttlMs) {
2911
+ const aesKey = await this.deriveKey(namespace);
2912
+ const enc = await this.encryption.encrypt(value, aesKey);
2913
+ const record = { enc };
2914
+ const opts = this.setOpts(ttlMs);
2915
+ await this.storage.set(this.dataKey(namespace, key), JSON.stringify(record), opts);
2916
+ await this.addKeyToSet(namespace, key, opts);
2917
+ }
2918
+ async delete(namespace, key) {
2919
+ const existed = await this.storage.delete(this.dataKey(namespace, key));
2920
+ const keys = await this.readKeySet(namespace);
2921
+ const next = keys.filter((k) => k !== key);
2922
+ if (next.length !== keys.length) {
2923
+ await this.writeKeySet(namespace, next);
2924
+ }
2925
+ return existed;
2926
+ }
2927
+ async list(namespace) {
2928
+ return this.readKeySet(namespace);
2929
+ }
2930
+ // ============================================
2931
+ // Key-set helpers (the namespace's key SET)
2932
+ // ============================================
2933
+ async readKeySet(namespace) {
2934
+ const raw = await this.storage.get(this.keysKey(namespace));
2935
+ if (!raw) return [];
2936
+ try {
2937
+ const parsed = JSON.parse(raw);
2938
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
2939
+ } catch {
2940
+ return [];
2941
+ }
2942
+ }
2943
+ async writeKeySet(namespace, keys, opts) {
2944
+ const unique = [...new Set(keys)];
2945
+ await this.storage.set(this.keysKey(namespace), JSON.stringify(unique), opts);
2946
+ }
2947
+ async addKeyToSet(namespace, key, opts) {
2948
+ const keys = await this.readKeySet(namespace);
2949
+ if (!keys.includes(key)) {
2950
+ keys.push(key);
2951
+ await this.writeKeySet(namespace, keys, opts);
2952
+ }
2953
+ }
2954
+ };
2955
+ function errMsg2(err) {
2956
+ return err instanceof Error ? err.message : String(err);
2957
+ }
2958
+
2959
+ // libs/auth/src/session/secure-store-accessor.ts
2960
+ var SECURE_STORE_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:SECURE_STORE_ACCESSOR");
2961
+
2962
+ // libs/auth/src/session/secure-store-accessor.impl.ts
2963
+ var import_utils10 = require("@frontmcp/utils");
2964
+ var GLOBAL_NAMESPACE = "global";
2965
+ var SecureStoreAccessorImpl = class {
2966
+ backend;
2967
+ scope;
2968
+ resolveSub;
2969
+ resolveSessionId;
2970
+ ttlMs;
2971
+ logger;
2972
+ constructor(deps) {
2973
+ this.backend = deps.backend;
2974
+ this.scope = deps.scope ?? "user";
2975
+ this.resolveSub = deps.resolveSub;
2976
+ this.resolveSessionId = deps.resolveSessionId;
2977
+ this.ttlMs = deps.ttlMs;
2978
+ this.logger = deps.logger ?? noopAuthLogger;
2979
+ }
2980
+ /**
2981
+ * Resolve the namespace for the current request from the configured scope.
2982
+ * Returns undefined when the scope's identity is unavailable (anonymous user
2983
+ * under `user` scope, or no session under `session` scope), in which case
2984
+ * reads return empty and writes are skipped.
2985
+ */
2986
+ resolveNamespace() {
2987
+ switch (this.scope) {
2988
+ case "global":
2989
+ return GLOBAL_NAMESPACE;
2990
+ case "session": {
2991
+ const sid = this.resolveSessionId();
2992
+ return sid ? `s:${(0, import_utils10.sha256Hex)(sid).slice(0, 32)}` : void 0;
2993
+ }
2994
+ case "user":
2995
+ default: {
2996
+ const sub = this.resolveSub();
2997
+ return sub ? `u:${(0, import_utils10.sha256Hex)(sub).slice(0, 32)}` : void 0;
2998
+ }
2999
+ }
3000
+ }
3001
+ async get(key) {
3002
+ const namespace = this.resolveNamespace();
3003
+ if (!namespace) return void 0;
3004
+ const raw = await this.backend.get(namespace, key);
3005
+ if (raw === null) return void 0;
3006
+ try {
3007
+ return JSON.parse(raw);
3008
+ } catch (err) {
3009
+ this.logger.warn(`SecureStore: failed to parse secret "${key}": ${errMsg3(err)}`);
3010
+ return void 0;
3011
+ }
3012
+ }
3013
+ async set(key, value, options) {
3014
+ const namespace = this.resolveNamespace();
3015
+ if (!namespace) {
3016
+ this.logger.debug(`SecureStore: no ${this.scope} scope for request; skipping set("${key}")`);
3017
+ return;
3018
+ }
3019
+ const ttlMs = options?.ttlMs ?? this.ttlMs;
3020
+ await this.backend.set(namespace, key, JSON.stringify(value), ttlMs);
2095
3021
  }
2096
- /**
2097
- * Decrypt and parse a JavaScript object
2098
- *
2099
- * @param encrypted - Encrypted data
2100
- * @param key - Encryption key
2101
- * @returns Decrypted and parsed object
2102
- */
2103
- async decryptObject(encrypted, key) {
2104
- const plaintext = await this.decrypt(encrypted, key);
2105
- return JSON.parse(plaintext);
3022
+ async delete(key) {
3023
+ const namespace = this.resolveNamespace();
3024
+ if (!namespace) return false;
3025
+ return this.backend.delete(namespace, key);
2106
3026
  }
2107
- /**
2108
- * Check if data is in encrypted format
2109
- *
2110
- * @param data - Data to check
2111
- * @returns True if data appears to be encrypted
2112
- */
2113
- isEncrypted(data) {
2114
- return encryptedDataSchema.safeParse(data).success;
3027
+ async list() {
3028
+ const namespace = this.resolveNamespace();
3029
+ if (!namespace) return [];
3030
+ return this.backend.list(namespace);
2115
3031
  }
2116
3032
  };
2117
- var encryptedVaultEntrySchema = import_lazy_zod3.z.object({
2118
- /** Vault ID (maps to JWT jti claim) */
2119
- id: import_lazy_zod3.z.string(),
2120
- /** User subject identifier */
2121
- userSub: import_lazy_zod3.z.string(),
2122
- /** User email (unencrypted for display) */
2123
- userEmail: import_lazy_zod3.z.string().optional(),
2124
- /** User name (unencrypted for display) */
2125
- userName: import_lazy_zod3.z.string().optional(),
2126
- /** Client ID that created this session */
2127
- clientId: import_lazy_zod3.z.string(),
2128
- /** Creation timestamp */
2129
- createdAt: import_lazy_zod3.z.number(),
2130
- /** Last access timestamp */
2131
- lastAccessAt: import_lazy_zod3.z.number(),
2132
- /** Encrypted sensitive data (provider tokens, credentials, consent) */
2133
- encryptedData: encryptedDataSchema,
2134
- /** Apps that are fully authorized (unencrypted for quick lookup) */
2135
- authorizedAppIds: import_lazy_zod3.z.array(import_lazy_zod3.z.string()),
2136
- /** Apps that were skipped (unencrypted for quick lookup) */
2137
- skippedAppIds: import_lazy_zod3.z.array(import_lazy_zod3.z.string()),
2138
- /** Pending auth IDs (unencrypted for lookup, actual URLs encrypted) */
2139
- pendingAuthIds: import_lazy_zod3.z.array(import_lazy_zod3.z.string()).default([])
2140
- });
3033
+ function errMsg3(err) {
3034
+ return err instanceof Error ? err.message : String(err);
3035
+ }
3036
+
3037
+ // libs/auth/src/session/secure-store.factory.ts
3038
+ var import_utils11 = require("@frontmcp/utils");
3039
+ function isCustomBackendConfig(config) {
3040
+ return typeof config === "object" && config !== null && "backend" in config && !!config.backend;
3041
+ }
3042
+ function isSqliteConfig(config) {
3043
+ return typeof config === "object" && config !== null && "sqlite" in config && !!config.sqlite;
3044
+ }
3045
+ function isRedisConfig(config) {
3046
+ return typeof config === "object" && config !== null && "redis" in config && !!config.redis;
3047
+ }
3048
+ function readScope(config) {
3049
+ if (typeof config === "object" && config !== null && "scope" in config && config.scope) {
3050
+ return config.scope;
3051
+ }
3052
+ return "user";
3053
+ }
3054
+ function readTtlMs(config) {
3055
+ if (typeof config === "object" && config !== null && "ttlMs" in config) {
3056
+ return config.ttlMs;
3057
+ }
3058
+ return void 0;
3059
+ }
3060
+ function readPepper(config) {
3061
+ if (typeof config === "object" && config !== null && "encryption" in config && config.encryption) {
3062
+ return config.encryption.pepper;
3063
+ }
3064
+ return void 0;
3065
+ }
3066
+ function toTokenStorageConfig(config) {
3067
+ if (isSqliteConfig(config)) {
3068
+ return { sqlite: config.sqlite };
3069
+ }
3070
+ if (isRedisConfig(config)) {
3071
+ return { redis: config.redis };
3072
+ }
3073
+ return "memory";
3074
+ }
3075
+ async function createSecureStore(options) {
3076
+ const { config, pepper, storage, logger } = options;
3077
+ const scope = readScope(config);
3078
+ const ttlMs = readTtlMs(config);
3079
+ if (isCustomBackendConfig(config)) {
3080
+ return { backend: config.backend, scope, ttlMs, kind: "custom" };
3081
+ }
3082
+ const kind = isSqliteConfig(config) ? "sqlite" : isRedisConfig(config) ? "redis" : "memory";
3083
+ let adapter;
3084
+ if (storage) {
3085
+ adapter = storage;
3086
+ } else if (kind === "memory") {
3087
+ adapter = new import_utils11.MemoryStorageAdapter();
3088
+ await adapter.connect();
3089
+ } else {
3090
+ adapter = await createTokenStorageAdapter(toTokenStorageConfig(config));
3091
+ }
3092
+ const backend = new EncryptedStorageSecureStoreBackend({
3093
+ storage: adapter,
3094
+ pepper: readPepper(config) ?? pepper,
3095
+ logger
3096
+ });
3097
+ return { backend, scope, ttlMs, kind };
3098
+ }
2141
3099
 
2142
3100
  // libs/auth/src/session/token.vault.ts
2143
- var import_utils6 = require("@frontmcp/utils");
3101
+ var import_utils12 = require("@frontmcp/utils");
2144
3102
  var TokenVault = class {
2145
3103
  /** Active key used for new encryptions */
2146
3104
  active;
@@ -2169,14 +3127,14 @@ var TokenVault = class {
2169
3127
  this.keys.set(k.kid, k.key);
2170
3128
  }
2171
3129
  async encrypt(plaintext, opts) {
2172
- const iv = (0, import_utils6.randomBytes)(12);
2173
- const { ciphertext, tag } = await (0, import_utils6.encryptAesGcm)(this.active.key, new TextEncoder().encode(plaintext), iv);
3130
+ const iv = (0, import_utils12.randomBytes)(12);
3131
+ const { ciphertext, tag } = await (0, import_utils12.encryptAesGcm)(this.active.key, new TextEncoder().encode(plaintext), iv);
2174
3132
  return {
2175
3133
  alg: "A256GCM",
2176
3134
  kid: this.active.kid,
2177
- iv: (0, import_utils6.base64urlEncode)(iv),
2178
- tag: (0, import_utils6.base64urlEncode)(tag),
2179
- data: (0, import_utils6.base64urlEncode)(ciphertext),
3135
+ iv: (0, import_utils12.base64urlEncode)(iv),
3136
+ tag: (0, import_utils12.base64urlEncode)(tag),
3137
+ data: (0, import_utils12.base64urlEncode)(ciphertext),
2180
3138
  exp: opts?.exp,
2181
3139
  meta: opts?.meta
2182
3140
  };
@@ -2190,16 +3148,16 @@ var TokenVault = class {
2190
3148
  }
2191
3149
  const key = this.keys.get(blob.kid);
2192
3150
  if (!key) throw new Error(`vault_unknown_kid:${blob.kid}`);
2193
- const iv = (0, import_utils6.base64urlDecode)(blob.iv);
2194
- const tag = (0, import_utils6.base64urlDecode)(blob.tag);
2195
- const data = (0, import_utils6.base64urlDecode)(blob.data);
2196
- const plaintext = await (0, import_utils6.decryptAesGcm)(key, data, iv, tag);
3151
+ const iv = (0, import_utils12.base64urlDecode)(blob.iv);
3152
+ const tag = (0, import_utils12.base64urlDecode)(blob.tag);
3153
+ const data = (0, import_utils12.base64urlDecode)(blob.data);
3154
+ const plaintext = await (0, import_utils12.decryptAesGcm)(key, data, iv, tag);
2197
3155
  return new TextDecoder().decode(plaintext);
2198
3156
  }
2199
3157
  };
2200
3158
 
2201
3159
  // libs/auth/src/session/index.ts
2202
- var import_utils23 = require("@frontmcp/utils");
3160
+ var import_utils32 = require("@frontmcp/utils");
2203
3161
 
2204
3162
  // libs/auth/src/session/transport-session.types.ts
2205
3163
  var import_lazy_zod4 = require("@frontmcp/lazy-zod");
@@ -2295,10 +3253,10 @@ var redisConfigSchema = import_lazy_zod4.z.object({
2295
3253
  });
2296
3254
 
2297
3255
  // libs/auth/src/session/session-crypto.ts
2298
- var import_utils8 = require("@frontmcp/utils");
3256
+ var import_utils14 = require("@frontmcp/utils");
2299
3257
 
2300
3258
  // libs/auth/src/errors/auth-internal.error.ts
2301
- var import_utils7 = require("@frontmcp/utils");
3259
+ var import_utils13 = require("@frontmcp/utils");
2302
3260
  var AuthInternalError = class extends Error {
2303
3261
  /**
2304
3262
  * Unique error ID for tracking in logs.
@@ -2320,7 +3278,7 @@ var AuthInternalError = class extends Error {
2320
3278
  super(message);
2321
3279
  this.name = this.constructor.name;
2322
3280
  this.code = code;
2323
- this.errorId = `err_${(0, import_utils7.bytesToHex)((0, import_utils7.randomBytes)(8))}`;
3281
+ this.errorId = `err_${(0, import_utils13.bytesToHex)((0, import_utils13.randomBytes)(8))}`;
2324
3282
  Error.captureStackTrace(this, this.constructor);
2325
3283
  }
2326
3284
  /**
@@ -2443,9 +3401,9 @@ var AuthFlowError = class extends AuthInternalError {
2443
3401
 
2444
3402
  // libs/auth/src/session/session-crypto.ts
2445
3403
  function getSigningSecret(config) {
2446
- const secret = config?.secret || (0, import_utils8.getEnv)("MCP_SESSION_SECRET");
3404
+ const secret = config?.secret || (0, import_utils14.getEnv)("MCP_SESSION_SECRET");
2447
3405
  if (!secret) {
2448
- if ((0, import_utils8.isProduction)()) {
3406
+ if ((0, import_utils14.isProduction)()) {
2449
3407
  throw new SessionSecretRequiredError("session signing");
2450
3408
  }
2451
3409
  console.warn("[SessionCrypto] MCP_SESSION_SECRET not set. Using insecure default for development only.");
@@ -2457,13 +3415,13 @@ function resolveConfig(config) {
2457
3415
  return { secret: getSigningSecret(config) };
2458
3416
  }
2459
3417
  function signSession(session, config) {
2460
- return (0, import_utils8.signData)(session, resolveConfig(config));
3418
+ return (0, import_utils14.signData)(session, resolveConfig(config));
2461
3419
  }
2462
3420
  function verifySession(signedData, config) {
2463
- return (0, import_utils8.verifyData)(signedData, resolveConfig(config));
3421
+ return (0, import_utils14.verifyData)(signedData, resolveConfig(config));
2464
3422
  }
2465
3423
  function verifyOrParseSession(data, config) {
2466
- return (0, import_utils8.verifyOrParseData)(data, resolveConfig(config));
3424
+ return (0, import_utils14.verifyOrParseData)(data, resolveConfig(config));
2467
3425
  }
2468
3426
 
2469
3427
  // libs/auth/src/session/session-rate-limiter.ts
@@ -2582,7 +3540,7 @@ var SessionRateLimiter = class {
2582
3540
  var defaultSessionRateLimiter = new SessionRateLimiter();
2583
3541
 
2584
3542
  // libs/auth/src/session/session.transport.ts
2585
- var import_utils9 = require("@frontmcp/utils");
3543
+ var import_utils15 = require("@frontmcp/utils");
2586
3544
  var TransportIdGenerator = class {
2587
3545
  /**
2588
3546
  * Create a transport session ID.
@@ -2591,7 +3549,7 @@ var TransportIdGenerator = class {
2591
3549
  * @returns A transport session ID (UUID without dashes)
2592
3550
  */
2593
3551
  static createId() {
2594
- return (0, import_utils9.randomUUID)().replace(/-/g, "");
3552
+ return (0, import_utils15.randomUUID)().replace(/-/g, "");
2595
3553
  }
2596
3554
  };
2597
3555
 
@@ -2639,7 +3597,7 @@ var TinyTtlCache = class {
2639
3597
  };
2640
3598
 
2641
3599
  // libs/auth/src/session/utils/auth-token.utils.ts
2642
- var import_utils10 = require("@frontmcp/utils");
3600
+ var import_utils16 = require("@frontmcp/utils");
2643
3601
  function isJwt(token) {
2644
3602
  if (!token) return false;
2645
3603
  return token.split(".").length === 3;
@@ -2649,7 +3607,7 @@ function getTokenSignatureFingerprint(token) {
2649
3607
  const sig = token.split(".")[2];
2650
3608
  if (sig) return sig;
2651
3609
  }
2652
- return (0, import_utils10.sha256Base64url)(token);
3610
+ return (0, import_utils16.sha256Base64url)(token);
2653
3611
  }
2654
3612
  function extractClaimValue(claims, key, validator) {
2655
3613
  const value = claims[key];
@@ -2681,26 +3639,26 @@ function extractBearerToken(header) {
2681
3639
  }
2682
3640
 
2683
3641
  // libs/auth/src/session/utils/session-crypto.utils.ts
2684
- var import_utils11 = require("@frontmcp/utils");
3642
+ var import_utils17 = require("@frontmcp/utils");
2685
3643
  var cachedKey = null;
2686
3644
  function getKey() {
2687
3645
  if (cachedKey) return cachedKey;
2688
- const secret = (0, import_utils11.getEnv)("MCP_SESSION_SECRET");
3646
+ const secret = (0, import_utils17.getEnv)("MCP_SESSION_SECRET");
2689
3647
  if (!secret) {
2690
- if ((0, import_utils11.isProduction)()) {
3648
+ if ((0, import_utils17.isProduction)()) {
2691
3649
  throw new SessionSecretRequiredError("session ID encryption");
2692
3650
  }
2693
3651
  console.warn(
2694
3652
  "[SessionIdUtils] Using machine ID as session encryption secret - NOT SECURE FOR PRODUCTION. Set MCP_SESSION_SECRET environment variable for secure session encryption."
2695
3653
  );
2696
3654
  }
2697
- const base = secret || (0, import_utils11.getMachineId)();
2698
- cachedKey = (0, import_utils11.sha256)(new TextEncoder().encode(base));
3655
+ const base = secret || (0, import_utils17.getMachineId)();
3656
+ cachedKey = (0, import_utils17.sha256)(new TextEncoder().encode(base));
2699
3657
  return cachedKey;
2700
3658
  }
2701
3659
  function encryptJson(obj) {
2702
3660
  const key = getKey();
2703
- const encrypted = (0, import_utils11.encryptValue)(obj, key);
3661
+ const encrypted = (0, import_utils17.encryptValue)(obj, key);
2704
3662
  return `${encrypted.iv}.${encrypted.tag}.${encrypted.data}`;
2705
3663
  }
2706
3664
  function decryptSessionJson(sessionId) {
@@ -2709,7 +3667,7 @@ function decryptSessionJson(sessionId) {
2709
3667
  const [ivB64, tagB64, ctB64] = parts;
2710
3668
  if (!ivB64 || !tagB64 || !ctB64) return null;
2711
3669
  const key = getKey();
2712
- return (0, import_utils11.decryptValue)({ alg: "A256GCM", iv: ivB64, tag: tagB64, data: ctB64 }, key);
3670
+ return (0, import_utils17.decryptValue)({ alg: "A256GCM", iv: ivB64, tag: tagB64, data: ctB64 }, key);
2713
3671
  }
2714
3672
  function safeDecrypt(sessionId) {
2715
3673
  try {
@@ -2723,11 +3681,11 @@ function resetCachedKey() {
2723
3681
  }
2724
3682
 
2725
3683
  // libs/auth/src/session/storage/index.ts
2726
- var import_utils15 = require("@frontmcp/utils");
2727
- var import_utils16 = require("@frontmcp/utils");
3684
+ var import_utils21 = require("@frontmcp/utils");
3685
+ var import_utils22 = require("@frontmcp/utils");
2728
3686
 
2729
3687
  // libs/auth/src/session/storage/storage-token-store.ts
2730
- var import_utils12 = require("@frontmcp/utils");
3688
+ var import_utils18 = require("@frontmcp/utils");
2731
3689
  var StorageTokenStore = class {
2732
3690
  storage;
2733
3691
  namespace;
@@ -2739,13 +3697,13 @@ var StorageTokenStore = class {
2739
3697
  this.defaultTtlSeconds = options.defaultTtlSeconds;
2740
3698
  this.storageIsNamespaced = "namespace" in storage && typeof storage.namespace === "function";
2741
3699
  const namespacedStorage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
2742
- this.storage = new import_utils12.TypedStorage(namespacedStorage);
3700
+ this.storage = new import_utils18.TypedStorage(namespacedStorage);
2743
3701
  }
2744
3702
  /**
2745
3703
  * Allocate a new unique ID for a token record.
2746
3704
  */
2747
3705
  allocId() {
2748
- return (0, import_utils12.randomUUID)();
3706
+ return (0, import_utils18.randomUUID)();
2749
3707
  }
2750
3708
  /**
2751
3709
  * Store an encrypted token blob.
@@ -2808,7 +3766,7 @@ var StorageTokenStore = class {
2808
3766
  };
2809
3767
 
2810
3768
  // libs/auth/src/session/storage/storage-authorization-vault.ts
2811
- var import_utils13 = require("@frontmcp/utils");
3769
+ var import_utils19 = require("@frontmcp/utils");
2812
3770
  var StorageAuthorizationVault = class {
2813
3771
  storage;
2814
3772
  namespace;
@@ -2817,7 +3775,7 @@ var StorageAuthorizationVault = class {
2817
3775
  this.namespace = options.namespace ?? "vault";
2818
3776
  this.pendingAuthTtlMs = options.pendingAuthTtlMs ?? 10 * 60 * 1e3;
2819
3777
  const namespacedStorage = this.isNamespacedStorage(storage) ? storage.namespace(this.namespace) : storage;
2820
- this.storage = new import_utils13.TypedStorage(namespacedStorage, {
3778
+ this.storage = new import_utils19.TypedStorage(namespacedStorage, {
2821
3779
  schema: options.validateOnRead ? authorizationVaultEntrySchema : void 0,
2822
3780
  throwOnInvalid: false
2823
3781
  });
@@ -2828,7 +3786,7 @@ var StorageAuthorizationVault = class {
2828
3786
  async create(params) {
2829
3787
  const now = Date.now();
2830
3788
  const entry = {
2831
- id: (0, import_utils13.randomUUID)(),
3789
+ id: (0, import_utils19.randomUUID)(),
2832
3790
  userSub: params.userSub,
2833
3791
  userEmail: params.userEmail,
2834
3792
  userName: params.userName,
@@ -2902,7 +3860,7 @@ var StorageAuthorizationVault = class {
2902
3860
  }
2903
3861
  const now = Date.now();
2904
3862
  const pendingAuth = {
2905
- id: (0, import_utils13.randomUUID)(),
3863
+ id: (0, import_utils19.randomUUID)(),
2906
3864
  appId: params.appId,
2907
3865
  toolId: params.toolId,
2908
3866
  authUrl: params.authUrl,
@@ -3106,11 +4064,11 @@ var StorageAuthorizationVault = class {
3106
4064
  };
3107
4065
 
3108
4066
  // libs/auth/src/session/storage/in-memory-authorization-vault.ts
3109
- var import_utils14 = require("@frontmcp/utils");
4067
+ var import_utils20 = require("@frontmcp/utils");
3110
4068
  var InMemoryAuthorizationVault = class extends StorageAuthorizationVault {
3111
4069
  memoryAdapter;
3112
4070
  constructor(options = {}) {
3113
- const memoryAdapter = new import_utils14.MemoryStorageAdapter();
4071
+ const memoryAdapter = new import_utils20.MemoryStorageAdapter();
3114
4072
  super(memoryAdapter, {
3115
4073
  namespace: options.namespace ?? "vault",
3116
4074
  pendingAuthTtlMs: options.pendingAuthTtlMs
@@ -3131,7 +4089,7 @@ var InMemoryAuthorizationVault = class extends StorageAuthorizationVault {
3131
4089
  };
3132
4090
 
3133
4091
  // libs/auth/src/session/redis-session.store.ts
3134
- var import_utils17 = require("@frontmcp/utils");
4092
+ var import_utils23 = require("@frontmcp/utils");
3135
4093
  var RedisSessionStore = class {
3136
4094
  storage;
3137
4095
  keyPrefix;
@@ -3153,14 +4111,14 @@ var RedisSessionStore = class {
3153
4111
  });
3154
4112
  }
3155
4113
  if ("redis" in config && config.redis) {
3156
- this.storage = new import_utils17.RedisStorageAdapter({
4114
+ this.storage = new import_utils23.RedisStorageAdapter({
3157
4115
  client: config.redis,
3158
4116
  keyPrefix: this.keyPrefix
3159
4117
  });
3160
4118
  this.externalInstance = true;
3161
4119
  } else {
3162
4120
  const redisConfig = config;
3163
- this.storage = new import_utils17.RedisStorageAdapter({
4121
+ this.storage = new import_utils23.RedisStorageAdapter({
3164
4122
  config: {
3165
4123
  host: redisConfig.host,
3166
4124
  port: redisConfig.port ?? 6379,
@@ -3342,7 +4300,7 @@ var RedisSessionStore = class {
3342
4300
  * Allocate a new session ID
3343
4301
  */
3344
4302
  allocId() {
3345
- return (0, import_utils17.randomUUID)();
4303
+ return (0, import_utils23.randomUUID)();
3346
4304
  }
3347
4305
  /**
3348
4306
  * Disconnect from Redis (only if we created the connection)
@@ -3378,7 +4336,7 @@ var RedisSessionStore = class {
3378
4336
  };
3379
4337
 
3380
4338
  // libs/auth/src/session/vercel-kv-session.store.ts
3381
- var import_utils18 = require("@frontmcp/utils");
4339
+ var import_utils24 = require("@frontmcp/utils");
3382
4340
  var VercelKvSessionStore = class {
3383
4341
  storage;
3384
4342
  keyPrefix;
@@ -3398,7 +4356,7 @@ var VercelKvSessionStore = class {
3398
4356
  maxRequests: this.security.rateLimiting?.maxRequests
3399
4357
  });
3400
4358
  }
3401
- this.storage = new import_utils18.VercelKvStorageAdapter({
4359
+ this.storage = new import_utils24.VercelKvStorageAdapter({
3402
4360
  url: config.url,
3403
4361
  token: config.token,
3404
4362
  keyPrefix: this.keyPrefix
@@ -3572,7 +4530,7 @@ var VercelKvSessionStore = class {
3572
4530
  * Allocate a new session ID
3573
4531
  */
3574
4532
  allocId() {
3575
- return (0, import_utils18.randomUUID)();
4533
+ return (0, import_utils24.randomUUID)();
3576
4534
  }
3577
4535
  /**
3578
4536
  * Disconnect from Vercel KV
@@ -3601,7 +4559,7 @@ var VercelKvSessionStore = class {
3601
4559
  };
3602
4560
 
3603
4561
  // libs/auth/src/session/orchestrated-token.store.ts
3604
- var import_utils19 = require("@frontmcp/utils");
4562
+ var import_utils25 = require("@frontmcp/utils");
3605
4563
  var InMemoryOrchestratedTokenStore = class {
3606
4564
  /** Token storage: Map<compositeKey, ProviderTokenRecord> */
3607
4565
  tokens = /* @__PURE__ */ new Map();
@@ -3643,7 +4601,7 @@ var InMemoryOrchestratedTokenStore = class {
3643
4601
  }
3644
4602
  const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
3645
4603
  const salt = new TextEncoder().encode("frontmcp-token-store");
3646
- const derivedKey = (0, import_utils19.hkdfSha256)(this.encryptionKey, salt, info, 32);
4604
+ const derivedKey = (0, import_utils25.hkdfSha256)(this.encryptionKey, salt, info, 32);
3647
4605
  this.derivedKeys.set(compositeKey, derivedKey);
3648
4606
  return derivedKey;
3649
4607
  }
@@ -3653,8 +4611,8 @@ var InMemoryOrchestratedTokenStore = class {
3653
4611
  async encryptRecord(compositeKey, record) {
3654
4612
  const key = await this.deriveKeyForRecord(compositeKey);
3655
4613
  const plaintext = JSON.stringify(record);
3656
- const iv = (0, import_utils19.randomBytes)(12);
3657
- const { ciphertext, tag } = (0, import_utils19.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
4614
+ const iv = (0, import_utils25.randomBytes)(12);
4615
+ const { ciphertext, tag } = (0, import_utils25.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
3658
4616
  return JSON.stringify({
3659
4617
  iv: Buffer.from(iv).toString("base64url"),
3660
4618
  tag: Buffer.from(tag).toString("base64url"),
@@ -3670,7 +4628,7 @@ var InMemoryOrchestratedTokenStore = class {
3670
4628
  const ivBytes = Buffer.from(iv, "base64url");
3671
4629
  const tagBytes = Buffer.from(tag, "base64url");
3672
4630
  const ciphertextBytes = Buffer.from(data, "base64url");
3673
- const plaintext = (0, import_utils19.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
4631
+ const plaintext = (0, import_utils25.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
3674
4632
  return JSON.parse(new TextDecoder().decode(plaintext));
3675
4633
  }
3676
4634
  /**
@@ -3778,101 +4736,307 @@ var InMemoryOrchestratedTokenStore = class {
3778
4736
  }
3779
4737
  }
3780
4738
  }
3781
- return providerIds;
4739
+ return providerIds;
4740
+ }
4741
+ /**
4742
+ * Clean up expired tokens
4743
+ */
4744
+ async cleanup() {
4745
+ const now = Date.now();
4746
+ const keysToDelete = [];
4747
+ for (const [key, stored] of this.tokens.entries()) {
4748
+ let record = null;
4749
+ if (this.encryptionKey) {
4750
+ if (typeof stored === "string") {
4751
+ try {
4752
+ record = await this.decryptRecord(key, stored);
4753
+ } catch {
4754
+ keysToDelete.push(key);
4755
+ continue;
4756
+ }
4757
+ }
4758
+ } else {
4759
+ record = stored;
4760
+ }
4761
+ if (record?.expiresAt && record.expiresAt < now) {
4762
+ keysToDelete.push(key);
4763
+ }
4764
+ }
4765
+ for (const key of keysToDelete) {
4766
+ this.tokens.delete(key);
4767
+ this.derivedKeys.delete(key);
4768
+ }
4769
+ }
4770
+ /**
4771
+ * Stop the cleanup timer
4772
+ */
4773
+ dispose() {
4774
+ if (this.cleanupTimer) {
4775
+ clearInterval(this.cleanupTimer);
4776
+ this.cleanupTimer = void 0;
4777
+ }
4778
+ }
4779
+ /**
4780
+ * Get total number of stored token records (for testing/monitoring)
4781
+ */
4782
+ get size() {
4783
+ return this.tokens.size;
4784
+ }
4785
+ /**
4786
+ * Clear all tokens (for testing)
4787
+ */
4788
+ clear() {
4789
+ this.tokens.clear();
4790
+ this.derivedKeys.clear();
4791
+ }
4792
+ /**
4793
+ * Migrate tokens from one authorization ID to another.
4794
+ * Used when tokens are stored with a pending ID during federated auth
4795
+ * and need to be accessible under the real authorization ID.
4796
+ *
4797
+ * @param fromAuthId - Source authorization ID (e.g., "pending:abc123")
4798
+ * @param toAuthId - Target authorization ID (e.g., "def456")
4799
+ */
4800
+ async migrateTokens(fromAuthId, toAuthId) {
4801
+ const prefix = `${fromAuthId}:`;
4802
+ const keysToMigrate = [];
4803
+ for (const key of this.tokens.keys()) {
4804
+ if (key.startsWith(prefix)) {
4805
+ keysToMigrate.push(key);
4806
+ }
4807
+ }
4808
+ for (const oldKey of keysToMigrate) {
4809
+ const providerId = oldKey.slice(prefix.length);
4810
+ const newKey = this.buildKey(toAuthId, providerId);
4811
+ const stored = this.tokens.get(oldKey);
4812
+ if (!stored) {
4813
+ continue;
4814
+ }
4815
+ if (this.encryptionKey) {
4816
+ try {
4817
+ const record = await this.decryptRecord(oldKey, stored);
4818
+ const encrypted = await this.encryptRecord(newKey, record);
4819
+ this.tokens.set(newKey, encrypted);
4820
+ } catch {
4821
+ continue;
4822
+ }
4823
+ } else {
4824
+ this.tokens.set(newKey, stored);
4825
+ }
4826
+ this.tokens.delete(oldKey);
4827
+ this.derivedKeys.delete(oldKey);
4828
+ }
4829
+ }
4830
+ };
4831
+
4832
+ // libs/auth/src/session/orchestrated-token.crypto.ts
4833
+ var import_utils26 = require("@frontmcp/utils");
4834
+ var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
4835
+ function deriveKeyForRecord(masterKey, compositeKey, cache) {
4836
+ const cached = cache.get(compositeKey);
4837
+ if (cached) {
4838
+ return cached;
4839
+ }
4840
+ const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
4841
+ const derived = (0, import_utils26.hkdfSha256)(masterKey, HKDF_SALT, info, 32);
4842
+ cache.set(compositeKey, derived);
4843
+ return derived;
4844
+ }
4845
+ function encryptRecord(key, record) {
4846
+ const plaintext = JSON.stringify(record);
4847
+ const iv = (0, import_utils26.randomBytes)(12);
4848
+ const { ciphertext, tag } = (0, import_utils26.encryptAesGcm)(key, new TextEncoder().encode(plaintext), iv);
4849
+ return JSON.stringify({
4850
+ iv: Buffer.from(iv).toString("base64url"),
4851
+ tag: Buffer.from(tag).toString("base64url"),
4852
+ data: Buffer.from(ciphertext).toString("base64url")
4853
+ });
4854
+ }
4855
+ function decryptRecord(key, encrypted) {
4856
+ const { iv, tag, data } = JSON.parse(encrypted);
4857
+ const ivBytes = Buffer.from(iv, "base64url");
4858
+ const tagBytes = Buffer.from(tag, "base64url");
4859
+ const ciphertextBytes = Buffer.from(data, "base64url");
4860
+ const plaintext = (0, import_utils26.decryptAesGcm)(key, ciphertextBytes, ivBytes, tagBytes);
4861
+ return JSON.parse(new TextDecoder().decode(plaintext));
4862
+ }
4863
+
4864
+ // libs/auth/src/session/storage-orchestrated-token.store.ts
4865
+ var StorageOrchestratedTokenStore = class {
4866
+ storage;
4867
+ encryptionKey;
4868
+ defaultTtlMs;
4869
+ namespace;
4870
+ storageIsNamespaced;
4871
+ /** HKDF-derived subkey cache, keyed by composite key. */
4872
+ derivedKeys = /* @__PURE__ */ new Map();
4873
+ constructor(storage, options = {}) {
4874
+ this.encryptionKey = options.encryptionKey;
4875
+ this.defaultTtlMs = options.defaultTtlMs;
4876
+ this.namespace = options.namespace ?? "otok";
4877
+ this.storageIsNamespaced = this.isNamespacedStorage(storage);
4878
+ this.storage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
4879
+ }
4880
+ // ============================================
4881
+ // TokenStore interface
4882
+ // ============================================
4883
+ async getAccessToken(authorizationId, providerId) {
4884
+ const record = await this.getRecord(authorizationId, providerId);
4885
+ return record?.accessToken ?? null;
4886
+ }
4887
+ async getRefreshToken(authorizationId, providerId) {
4888
+ const record = await this.getRecord(authorizationId, providerId);
4889
+ return record?.refreshToken ?? null;
4890
+ }
4891
+ async storeTokens(authorizationId, providerId, tokens) {
4892
+ const compositeKey = this.compositeKey(authorizationId, providerId);
4893
+ const now = Date.now();
4894
+ const record = {
4895
+ accessToken: tokens.accessToken,
4896
+ refreshToken: tokens.refreshToken,
4897
+ expiresAt: tokens.expiresAt ?? (this.defaultTtlMs ? now + this.defaultTtlMs : void 0),
4898
+ createdAt: now,
4899
+ updatedAt: now
4900
+ };
4901
+ const value = this.encryptionKey ? encryptRecord(this.keyFor(compositeKey), record) : JSON.stringify(record);
4902
+ await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(record.expiresAt));
4903
+ }
4904
+ async deleteTokens(authorizationId, providerId) {
4905
+ const compositeKey = this.compositeKey(authorizationId, providerId);
4906
+ await this.storage.delete(this.storageKey(compositeKey));
4907
+ this.derivedKeys.delete(compositeKey);
4908
+ }
4909
+ async hasTokens(authorizationId, providerId) {
4910
+ const record = await this.getRecord(authorizationId, providerId);
4911
+ return record !== null;
4912
+ }
4913
+ async getProviderIds(authorizationId) {
4914
+ const prefix = `${authorizationId}:`;
4915
+ const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
4916
+ const providerIds = [];
4917
+ for (const compositeKey of compositeKeys) {
4918
+ if (!compositeKey.startsWith(prefix)) continue;
4919
+ const providerId = compositeKey.slice(prefix.length);
4920
+ const record = await this.getRecord(authorizationId, providerId);
4921
+ if (record) {
4922
+ providerIds.push(providerId);
4923
+ }
4924
+ }
4925
+ return providerIds;
4926
+ }
4927
+ async migrateTokens(fromAuthId, toAuthId) {
4928
+ const prefix = `${fromAuthId}:`;
4929
+ const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
4930
+ for (const oldComposite of compositeKeys) {
4931
+ if (!oldComposite.startsWith(prefix)) continue;
4932
+ const providerId = oldComposite.slice(prefix.length);
4933
+ const newComposite = this.compositeKey(toAuthId, providerId);
4934
+ const stored = await this.storage.get(this.storageKey(oldComposite));
4935
+ if (stored === null) continue;
4936
+ if (this.encryptionKey) {
4937
+ let record;
4938
+ try {
4939
+ record = decryptRecord(this.keyFor(oldComposite), stored);
4940
+ } catch {
4941
+ continue;
4942
+ }
4943
+ const value = encryptRecord(this.keyFor(newComposite), record);
4944
+ await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(record.expiresAt));
4945
+ } else {
4946
+ let expiresAt;
4947
+ try {
4948
+ expiresAt = JSON.parse(stored).expiresAt;
4949
+ } catch {
4950
+ expiresAt = void 0;
4951
+ }
4952
+ await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(expiresAt));
4953
+ }
4954
+ await this.storage.delete(this.storageKey(oldComposite));
4955
+ this.derivedKeys.delete(oldComposite);
4956
+ }
3782
4957
  }
3783
4958
  /**
3784
- * Clean up expired tokens
4959
+ * Delete all tokens for an authorization id.
3785
4960
  */
3786
- async cleanup() {
3787
- const now = Date.now();
3788
- const keysToDelete = [];
3789
- for (const [key, stored] of this.tokens.entries()) {
3790
- let record = null;
3791
- if (this.encryptionKey) {
3792
- if (typeof stored === "string") {
3793
- try {
3794
- record = await this.decryptRecord(key, stored);
3795
- } catch {
3796
- keysToDelete.push(key);
3797
- continue;
3798
- }
3799
- }
3800
- } else {
3801
- record = stored;
4961
+ async deleteAllForAuthorization(authorizationId) {
4962
+ const prefix = `${authorizationId}:`;
4963
+ const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
4964
+ for (const compositeKey of compositeKeys) {
4965
+ if (!compositeKey.startsWith(prefix)) continue;
4966
+ await this.storage.delete(this.storageKey(compositeKey));
4967
+ this.derivedKeys.delete(compositeKey);
4968
+ }
4969
+ }
4970
+ // ============================================
4971
+ // Internals
4972
+ // ============================================
4973
+ async getRecord(authorizationId, providerId) {
4974
+ const compositeKey = this.compositeKey(authorizationId, providerId);
4975
+ const stored = await this.storage.get(this.storageKey(compositeKey));
4976
+ if (stored === null) {
4977
+ return null;
4978
+ }
4979
+ let record;
4980
+ if (this.encryptionKey) {
4981
+ try {
4982
+ record = decryptRecord(this.keyFor(compositeKey), stored);
4983
+ } catch {
4984
+ await this.storage.delete(this.storageKey(compositeKey));
4985
+ return null;
3802
4986
  }
3803
- if (record?.expiresAt && record.expiresAt < now) {
3804
- keysToDelete.push(key);
4987
+ } else {
4988
+ try {
4989
+ record = JSON.parse(stored);
4990
+ } catch {
4991
+ await this.storage.delete(this.storageKey(compositeKey));
4992
+ return null;
3805
4993
  }
3806
4994
  }
3807
- for (const key of keysToDelete) {
3808
- this.tokens.delete(key);
3809
- this.derivedKeys.delete(key);
4995
+ if (record.expiresAt && record.expiresAt < Date.now()) {
4996
+ await this.storage.delete(this.storageKey(compositeKey));
4997
+ return null;
3810
4998
  }
4999
+ return record;
3811
5000
  }
3812
- /**
3813
- * Stop the cleanup timer
3814
- */
3815
- dispose() {
3816
- if (this.cleanupTimer) {
3817
- clearInterval(this.cleanupTimer);
3818
- this.cleanupTimer = void 0;
5001
+ keyFor(compositeKey) {
5002
+ if (!this.encryptionKey) {
5003
+ throw new EncryptionKeyNotConfiguredError();
3819
5004
  }
5005
+ return deriveKeyForRecord(this.encryptionKey, compositeKey, this.derivedKeys);
3820
5006
  }
3821
- /**
3822
- * Get total number of stored token records (for testing/monitoring)
3823
- */
3824
- get size() {
3825
- return this.tokens.size;
5007
+ compositeKey(authorizationId, providerId) {
5008
+ return `${authorizationId}:${providerId}`;
3826
5009
  }
3827
- /**
3828
- * Clear all tokens (for testing)
3829
- */
3830
- clear() {
3831
- this.tokens.clear();
3832
- this.derivedKeys.clear();
5010
+ storageKey(compositeKey) {
5011
+ return this.storageIsNamespaced ? compositeKey : `${this.namespace}:${compositeKey}`;
3833
5012
  }
3834
5013
  /**
3835
- * Migrate tokens from one authorization ID to another.
3836
- * Used when tokens are stored with a pending ID during federated auth
3837
- * and need to be accessible under the real authorization ID.
3838
- *
3839
- * @param fromAuthId - Source authorization ID (e.g., "pending:abc123")
3840
- * @param toAuthId - Target authorization ID (e.g., "def456")
5014
+ * List composite keys (authorizationId:providerId) matching a glob over the
5015
+ * composite-key space, stripping any non-namespaced prefix we added.
3841
5016
  */
3842
- async migrateTokens(fromAuthId, toAuthId) {
3843
- const prefix = `${fromAuthId}:`;
3844
- const keysToMigrate = [];
3845
- for (const key of this.tokens.keys()) {
3846
- if (key.startsWith(prefix)) {
3847
- keysToMigrate.push(key);
3848
- }
5017
+ async listCompositeKeys(compositeGlob) {
5018
+ const pattern = this.storageIsNamespaced ? compositeGlob : `${this.namespace}:${compositeGlob}`;
5019
+ const raw = await this.storage.keys(pattern);
5020
+ if (this.storageIsNamespaced) {
5021
+ return raw;
3849
5022
  }
3850
- for (const oldKey of keysToMigrate) {
3851
- const providerId = oldKey.slice(prefix.length);
3852
- const newKey = this.buildKey(toAuthId, providerId);
3853
- const stored = this.tokens.get(oldKey);
3854
- if (!stored) {
3855
- continue;
3856
- }
3857
- if (this.encryptionKey) {
3858
- try {
3859
- const record = await this.decryptRecord(oldKey, stored);
3860
- const encrypted = await this.encryptRecord(newKey, record);
3861
- this.tokens.set(newKey, encrypted);
3862
- } catch {
3863
- continue;
3864
- }
3865
- } else {
3866
- this.tokens.set(newKey, stored);
3867
- }
3868
- this.tokens.delete(oldKey);
3869
- this.derivedKeys.delete(oldKey);
5023
+ const stripPrefix = `${this.namespace}:`;
5024
+ return raw.map((k) => k.startsWith(stripPrefix) ? k.slice(stripPrefix.length) : k);
5025
+ }
5026
+ ttlOptions(expiresAtMs) {
5027
+ if (!expiresAtMs) {
5028
+ return void 0;
3870
5029
  }
5030
+ const ttl = Math.ceil((expiresAtMs - Date.now()) / 1e3);
5031
+ return { ttlSeconds: ttl > 0 ? ttl : 1 };
5032
+ }
5033
+ isNamespacedStorage(storage) {
5034
+ return "namespace" in storage && typeof storage.namespace === "function";
3871
5035
  }
3872
5036
  };
3873
5037
 
3874
5038
  // libs/auth/src/session/federated-auth.session.ts
3875
- var import_utils20 = require("@frontmcp/utils");
5039
+ var import_utils27 = require("@frontmcp/utils");
3876
5040
  function toSessionRecord(session) {
3877
5041
  return {
3878
5042
  ...session,
@@ -3947,7 +5111,7 @@ var InMemoryFederatedAuthSessionStore = class {
3947
5111
  createSession(params) {
3948
5112
  const now = Date.now();
3949
5113
  return {
3950
- id: (0, import_utils20.randomUUID)(),
5114
+ id: (0, import_utils27.randomUUID)(),
3951
5115
  pendingAuthId: params.pendingAuthId,
3952
5116
  clientId: params.clientId,
3953
5117
  redirectUri: params.redirectUri,
@@ -3979,7 +5143,7 @@ var InMemoryFederatedAuthSessionStore = class {
3979
5143
  function createFederatedAuthSession(params, ttlMs = 15 * 60 * 1e3) {
3980
5144
  const now = Date.now();
3981
5145
  return {
3982
- id: (0, import_utils20.randomUUID)(),
5146
+ id: (0, import_utils27.randomUUID)(),
3983
5147
  pendingAuthId: params.pendingAuthId,
3984
5148
  clientId: params.clientId,
3985
5149
  redirectUri: params.redirectUri,
@@ -4035,9 +5199,119 @@ function startNextProvider(session, pkce, state) {
4035
5199
  return providerId;
4036
5200
  }
4037
5201
 
5202
+ // libs/auth/src/session/storage-federated-auth.session.ts
5203
+ var import_utils28 = require("@frontmcp/utils");
5204
+ var StorageFederatedAuthSessionStore = class {
5205
+ storage;
5206
+ namespace;
5207
+ storageIsNamespaced;
5208
+ constructor(storage, options = {}) {
5209
+ this.namespace = options.namespace ?? "fed";
5210
+ this.storageIsNamespaced = this.isNamespacedStorage(storage);
5211
+ const namespacedStorage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
5212
+ this.storage = new import_utils28.TypedStorage(namespacedStorage);
5213
+ }
5214
+ async store(session) {
5215
+ await this.put(session);
5216
+ }
5217
+ async update(session) {
5218
+ await this.put(session);
5219
+ }
5220
+ async get(id) {
5221
+ const record = await this.storage.get(this.key(id));
5222
+ if (!record) {
5223
+ return null;
5224
+ }
5225
+ if (Date.now() > record.expiresAt) {
5226
+ await this.storage.delete(this.key(id));
5227
+ return null;
5228
+ }
5229
+ return fromSessionRecord(record);
5230
+ }
5231
+ async delete(id) {
5232
+ await this.storage.delete(this.key(id));
5233
+ }
5234
+ async put(session) {
5235
+ const record = toSessionRecord(session);
5236
+ const ttlSeconds = this.calculateTtl(record.expiresAt);
5237
+ await this.storage.set(this.key(session.id), record, ttlSeconds ? { ttlSeconds } : void 0);
5238
+ }
5239
+ calculateTtl(expiresAtMs) {
5240
+ const ttl = Math.ceil((expiresAtMs - Date.now()) / 1e3);
5241
+ return ttl > 0 ? ttl : 1;
5242
+ }
5243
+ key(id) {
5244
+ return this.storageIsNamespaced ? id : `${this.namespace}:${id}`;
5245
+ }
5246
+ isNamespacedStorage(storage) {
5247
+ return "namespace" in storage && typeof storage.namespace === "function";
5248
+ }
5249
+ };
5250
+
5251
+ // libs/auth/src/session/consent.store.ts
5252
+ function consentRecordKey(userSub, clientId) {
5253
+ return `consent:${userSub}:${clientId}`;
5254
+ }
5255
+ var InMemoryConsentStore = class {
5256
+ records = /* @__PURE__ */ new Map();
5257
+ async get(userSub, clientId) {
5258
+ const record = this.records.get(consentRecordKey(userSub, clientId));
5259
+ if (!record) return null;
5260
+ return { ...record, selectedToolIds: [...record.selectedToolIds], seenToolIds: [...record.seenToolIds] };
5261
+ }
5262
+ async set(record) {
5263
+ this.records.set(consentRecordKey(record.userSub, record.clientId), {
5264
+ ...record,
5265
+ selectedToolIds: [...record.selectedToolIds],
5266
+ seenToolIds: [...record.seenToolIds]
5267
+ });
5268
+ }
5269
+ async delete(userSub, clientId) {
5270
+ this.records.delete(consentRecordKey(userSub, clientId));
5271
+ }
5272
+ /** Number of remembered records (for testing/monitoring). */
5273
+ get size() {
5274
+ return this.records.size;
5275
+ }
5276
+ /** Clear all remembered records (for testing). */
5277
+ clear() {
5278
+ this.records.clear();
5279
+ }
5280
+ };
5281
+
5282
+ // libs/auth/src/session/storage-consent.store.ts
5283
+ var import_utils29 = require("@frontmcp/utils");
5284
+ var StorageConsentStore = class {
5285
+ storage;
5286
+ namespace;
5287
+ storageIsNamespaced;
5288
+ constructor(storage, options = {}) {
5289
+ this.namespace = options.namespace ?? "remembered-consent";
5290
+ this.storageIsNamespaced = this.isNamespacedStorage(storage);
5291
+ const namespacedStorage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
5292
+ this.storage = new import_utils29.TypedStorage(namespacedStorage);
5293
+ }
5294
+ async get(userSub, clientId) {
5295
+ return this.storage.get(this.key(userSub, clientId));
5296
+ }
5297
+ async set(record) {
5298
+ await this.storage.set(this.key(record.userSub, record.clientId), record);
5299
+ }
5300
+ async delete(userSub, clientId) {
5301
+ await this.storage.delete(this.key(userSub, clientId));
5302
+ }
5303
+ key(userSub, clientId) {
5304
+ const base = consentRecordKey(userSub, clientId);
5305
+ return this.storageIsNamespaced ? base : `${this.namespace}:${base}`;
5306
+ }
5307
+ isNamespacedStorage(storage) {
5308
+ return "namespace" in storage && typeof storage.namespace === "function";
5309
+ }
5310
+ };
5311
+
4038
5312
  // libs/auth/src/session/encrypted-authorization-vault.ts
4039
5313
  var import_lazy_zod5 = require("@frontmcp/lazy-zod");
4040
- var import_utils21 = require("@frontmcp/utils");
5314
+ var import_utils30 = require("@frontmcp/utils");
4041
5315
  var redisVaultEntrySchema = import_lazy_zod5.z.object({
4042
5316
  /** Vault ID */
4043
5317
  id: import_lazy_zod5.z.string(),
@@ -4062,7 +5336,7 @@ var redisVaultEntrySchema = import_lazy_zod5.z.object({
4062
5336
  /** Encrypted sensitive data blob */
4063
5337
  encrypted: encryptedDataSchema
4064
5338
  });
4065
- var encryptionContextStorage = new import_utils21.AsyncLocalStorage();
5339
+ var encryptionContextStorage = new import_utils30.AsyncLocalStorage();
4066
5340
  var EncryptedRedisVault = class {
4067
5341
  constructor(redis, encryption, namespace = "vault:") {
4068
5342
  this.redis = redis;
@@ -4195,7 +5469,7 @@ var EncryptedRedisVault = class {
4195
5469
  async create(params) {
4196
5470
  const now = Date.now();
4197
5471
  const entry = {
4198
- id: (0, import_utils21.randomUUID)(),
5472
+ id: (0, import_utils30.randomUUID)(),
4199
5473
  userSub: params.userSub,
4200
5474
  userEmail: params.userEmail,
4201
5475
  userName: params.userName,
@@ -4254,7 +5528,7 @@ var EncryptedRedisVault = class {
4254
5528
  }
4255
5529
  const now = Date.now();
4256
5530
  const pendingAuth = {
4257
- id: (0, import_utils21.randomUUID)(),
5531
+ id: (0, import_utils30.randomUUID)(),
4258
5532
  appId: params.appId,
4259
5533
  toolId: params.toolId,
4260
5534
  authUrl: params.authUrl,
@@ -4432,7 +5706,7 @@ function createEncryptedVault(redis, config = {}) {
4432
5706
  }
4433
5707
 
4434
5708
  // libs/auth/src/session/token.refresh.ts
4435
- var import_utils22 = require("@frontmcp/utils");
5709
+ var import_utils31 = require("@frontmcp/utils");
4436
5710
  function toEpochSeconds(exp) {
4437
5711
  if (exp == null) return void 0;
4438
5712
  if (exp instanceof Date) return Math.floor(exp.getTime() / 1e3);
@@ -4454,7 +5728,7 @@ function tryJwtExp(token) {
4454
5728
  const parts = token.split(".");
4455
5729
  if (parts.length < 2) return void 0;
4456
5730
  try {
4457
- const json = JSON.parse(new TextDecoder().decode((0, import_utils22.base64urlDecode)(parts[1])));
5731
+ const json = JSON.parse(new TextDecoder().decode((0, import_utils31.base64urlDecode)(parts[1])));
4458
5732
  const e = json?.exp;
4459
5733
  return typeof e === "number" ? toEpochSeconds(e) : void 0;
4460
5734
  } catch {
@@ -4517,7 +5791,7 @@ var progressiveAuthStateSchema = import_lazy_zod6.z.object({
4517
5791
  });
4518
5792
 
4519
5793
  // libs/auth/src/authorization/authorization.class.ts
4520
- var import_utils25 = require("@frontmcp/utils");
5794
+ var import_utils34 = require("@frontmcp/utils");
4521
5795
  var AuthorizationBase = class _AuthorizationBase {
4522
5796
  id;
4523
5797
  isAnonymous;
@@ -4565,14 +5839,14 @@ var AuthorizationBase = class _AuthorizationBase {
4565
5839
  * @param fingerprint - Optional client fingerprint for tracking
4566
5840
  */
4567
5841
  createTransportSession(protocol, fingerprint) {
4568
- const sessionId = (0, import_utils25.randomUUID)();
5842
+ const sessionId = (0, import_utils34.randomUUID)();
4569
5843
  const session = {
4570
5844
  id: sessionId,
4571
5845
  authorizationId: this.id,
4572
5846
  protocol,
4573
5847
  createdAt: Date.now(),
4574
5848
  expiresAt: this.expiresAt,
4575
- nodeId: (0, import_utils25.getMachineId)(),
5849
+ nodeId: (0, import_utils34.getMachineId)(),
4576
5850
  clientFingerprint: fingerprint
4577
5851
  };
4578
5852
  this.#sessions.set(session.id, session);
@@ -4702,7 +5976,7 @@ var AuthorizationBase = class _AuthorizationBase {
4702
5976
  */
4703
5977
  static isJwtHeader(segment) {
4704
5978
  try {
4705
- const bytes = (0, import_utils25.base64urlDecode)(segment);
5979
+ const bytes = (0, import_utils34.base64urlDecode)(segment);
4706
5980
  const text = new TextDecoder().decode(bytes);
4707
5981
  const parsed = JSON.parse(text);
4708
5982
  return typeof parsed === "object" && parsed !== null && typeof parsed.alg === "string";
@@ -4754,7 +6028,7 @@ var AuthorizationBase = class _AuthorizationBase {
4754
6028
  };
4755
6029
 
4756
6030
  // libs/auth/src/authorization/public.authorization.ts
4757
- var import_utils26 = require("@frontmcp/utils");
6031
+ var import_utils35 = require("@frontmcp/utils");
4758
6032
  var PublicAuthorization = class _PublicAuthorization extends AuthorizationBase {
4759
6033
  mode = "public";
4760
6034
  /**
@@ -4790,7 +6064,7 @@ var PublicAuthorization = class _PublicAuthorization extends AuthorizationBase {
4790
6064
  allowedTools = "all",
4791
6065
  allowedPrompts = "all"
4792
6066
  } = ctx;
4793
- const uuid = (0, import_utils26.randomUUID)();
6067
+ const uuid = (0, import_utils35.randomUUID)();
4794
6068
  const sub = `${prefix}:${uuid}`;
4795
6069
  const user = {
4796
6070
  sub,
@@ -4874,7 +6148,7 @@ var PublicAuthorization = class _PublicAuthorization extends AuthorizationBase {
4874
6148
  };
4875
6149
 
4876
6150
  // libs/auth/src/authorization/transparent.authorization.ts
4877
- var import_utils27 = require("@frontmcp/utils");
6151
+ var import_utils36 = require("@frontmcp/utils");
4878
6152
  var TransparentAuthorization = class _TransparentAuthorization extends AuthorizationBase {
4879
6153
  mode = "transparent";
4880
6154
  /**
@@ -4974,7 +6248,7 @@ var TransparentAuthorization = class _TransparentAuthorization extends Authoriza
4974
6248
  static generateAuthorizationId(token) {
4975
6249
  const parts = token.split(".");
4976
6250
  const signature = parts[2] || token;
4977
- return (0, import_utils27.sha256Hex)(signature).substring(0, 16);
6251
+ return (0, import_utils36.sha256Hex)(signature).substring(0, 16);
4978
6252
  }
4979
6253
  /**
4980
6254
  * Get the issuer from the token claims
@@ -5000,11 +6274,11 @@ var TransparentAuthorization = class _TransparentAuthorization extends Authoriza
5000
6274
  };
5001
6275
 
5002
6276
  // libs/auth/src/utils/authorization-id.utils.ts
5003
- var import_utils28 = require("@frontmcp/utils");
6277
+ var import_utils37 = require("@frontmcp/utils");
5004
6278
  function deriveAuthorizationId(token) {
5005
6279
  const parts = token.split(".");
5006
6280
  const signature = parts[2] || token;
5007
- return (0, import_utils28.sha256Hex)(signature).substring(0, 16);
6281
+ return (0, import_utils37.sha256Hex)(signature).substring(0, 16);
5008
6282
  }
5009
6283
 
5010
6284
  // libs/auth/src/authorization/orchestrated.authorization.ts
@@ -5371,6 +6645,103 @@ var OrchestratedAuthAccessorAdapter = class {
5371
6645
  }
5372
6646
  };
5373
6647
 
6648
+ // libs/auth/src/authorization/dcr-client.registry.ts
6649
+ var import_utils38 = require("@frontmcp/utils");
6650
+ function globToRegExp(pattern) {
6651
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, (ch) => ch === "*" ? "\0" : `\\${ch}`);
6652
+ const withWildcards = escaped.split("\0").join(".*");
6653
+ return new RegExp(`^${withWildcards}$`);
6654
+ }
6655
+ var DcrClientRegistry = class {
6656
+ constructor(config = {}) {
6657
+ this.config = config;
6658
+ this.redirectMatchers = config.allowedRedirectUris?.map(globToRegExp);
6659
+ this.seedPreRegisteredClients();
6660
+ }
6661
+ clients = /* @__PURE__ */ new Map();
6662
+ redirectMatchers;
6663
+ /** Seed `dcr.clients` so the authorize/token flows accept them without DCR. */
6664
+ seedPreRegisteredClients() {
6665
+ const clients = this.config.clients ?? [];
6666
+ const now = Math.floor(Date.now() / 1e3);
6667
+ for (const c of clients) {
6668
+ this.clients.set(c.clientId, {
6669
+ client_id: c.clientId,
6670
+ client_secret: c.clientSecret,
6671
+ token_endpoint_auth_method: c.tokenEndpointAuthMethod ?? "none",
6672
+ grant_types: c.grantTypes ?? ["authorization_code"],
6673
+ response_types: c.responseTypes ?? ["code"],
6674
+ redirect_uris: c.redirectUris,
6675
+ client_name: c.clientName,
6676
+ scope: c.scope,
6677
+ created_at: now,
6678
+ dev: false,
6679
+ preRegistered: true
6680
+ });
6681
+ }
6682
+ }
6683
+ /** Look up a client by id (pre-registered or dynamically registered). */
6684
+ get(clientId) {
6685
+ return this.clients.get(clientId);
6686
+ }
6687
+ /** Whether a client id is known to the registry. */
6688
+ has(clientId) {
6689
+ return this.clients.has(clientId);
6690
+ }
6691
+ /** Add a dynamically-registered client (called by the register flow). */
6692
+ register(client) {
6693
+ this.clients.set(client.client_id, client);
6694
+ }
6695
+ /** Whether a redirect_uri allowlist is configured. */
6696
+ hasRedirectAllowlist() {
6697
+ return !!this.redirectMatchers && this.redirectMatchers.length > 0;
6698
+ }
6699
+ /**
6700
+ * Whether `redirectUri` is permitted by the configured allowlist. Always
6701
+ * `true` when no allowlist is configured (preserves default behavior).
6702
+ */
6703
+ isRedirectUriAllowed(redirectUri) {
6704
+ if (!this.redirectMatchers || this.redirectMatchers.length === 0) {
6705
+ return true;
6706
+ }
6707
+ return this.redirectMatchers.some((re) => re.test(redirectUri));
6708
+ }
6709
+ /** Whether a client-id allowlist is configured. */
6710
+ hasClientIdAllowlist() {
6711
+ return !!this.config.allowedClientIds && this.config.allowedClientIds.length > 0;
6712
+ }
6713
+ /**
6714
+ * Whether `clientId` is permitted by the configured client-id allowlist.
6715
+ * Always `true` when no allowlist is configured (preserves default behavior).
6716
+ */
6717
+ isClientIdAllowed(clientId) {
6718
+ const allow = this.config.allowedClientIds;
6719
+ if (!allow || allow.length === 0) {
6720
+ return true;
6721
+ }
6722
+ return allow.includes(clientId);
6723
+ }
6724
+ /** Whether an initial access token is required for DCR registration. */
6725
+ requiresInitialAccessToken() {
6726
+ return typeof this.config.initialAccessToken === "string" && this.config.initialAccessToken.length > 0;
6727
+ }
6728
+ /**
6729
+ * Constant-time check of a presented bearer token against the configured
6730
+ * initial access token. Returns `false` when none is configured (callers
6731
+ * should gate on {@link requiresInitialAccessToken} first).
6732
+ */
6733
+ verifyInitialAccessToken(presented) {
6734
+ const expected = this.config.initialAccessToken;
6735
+ if (!expected) {
6736
+ return false;
6737
+ }
6738
+ if (typeof presented !== "string" || presented.length === 0) {
6739
+ return false;
6740
+ }
6741
+ return (0, import_utils38.timingSafeEqual)((0, import_utils38.sha256)(presented), (0, import_utils38.sha256)(expected));
6742
+ }
6743
+ };
6744
+
5374
6745
  // libs/auth/src/common/jwt.types.ts
5375
6746
  var import_lazy_zod7 = require("@frontmcp/lazy-zod");
5376
6747
  var jwkParametersSchema = import_lazy_zod7.z.object({
@@ -6318,10 +7689,15 @@ var CimdService = class {
6318
7689
  }
6319
7690
  }
6320
7691
  };
7692
+ var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
7693
+ function isLoopbackHostname(hostname) {
7694
+ return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
7695
+ }
6321
7696
  function normalizeRedirectUri(uri) {
6322
7697
  try {
6323
7698
  const url = new URL(uri);
6324
- let normalized = `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}`;
7699
+ const host = isLoopbackHostname(url.hostname) ? url.hostname.toLowerCase() : url.host.toLowerCase();
7700
+ let normalized = `${url.protocol.toLowerCase()}//${host}`;
6325
7701
  normalized += url.pathname.replace(/\/+$/, "") || "/";
6326
7702
  if (url.search) normalized += url.search;
6327
7703
  return normalized;
@@ -6452,6 +7828,105 @@ var remoteProviderConfigSchema = import_lazy_zod10.z.object({
6452
7828
  */
6453
7829
  userInfoEndpoint: import_lazy_zod10.z.string().url().optional()
6454
7830
  });
7831
+ var upstreamProviderSchema = import_lazy_zod10.z.object({
7832
+ /** Stable provider id (e.g. `'github'`). Used in `this.orchestration.getToken(id)`. */
7833
+ id: import_lazy_zod10.z.string().min(1),
7834
+ /** Human-readable display name. Defaults to `id` when omitted. */
7835
+ name: import_lazy_zod10.z.string().optional(),
7836
+ /** Authorization endpoint (canonical name). */
7837
+ authorizationEndpoint: import_lazy_zod10.z.string().url().optional(),
7838
+ /** Authorization endpoint (alias for {@link authorizationEndpoint}). */
7839
+ authorizeUrl: import_lazy_zod10.z.string().url().optional(),
7840
+ /** Token endpoint (canonical name). */
7841
+ tokenEndpoint: import_lazy_zod10.z.string().url().optional(),
7842
+ /** Token endpoint (alias for {@link tokenEndpoint}). */
7843
+ tokenUrl: import_lazy_zod10.z.string().url().optional(),
7844
+ /** OAuth client id issued by the upstream provider. */
7845
+ clientId: import_lazy_zod10.z.string().min(1),
7846
+ /** OAuth client secret (confidential clients). */
7847
+ clientSecret: import_lazy_zod10.z.string().optional(),
7848
+ /** Scopes to request from the upstream provider. */
7849
+ scopes: import_lazy_zod10.z.array(import_lazy_zod10.z.string()).optional(),
7850
+ /** User info endpoint (optional, used to enrich the session identity). */
7851
+ userInfoEndpoint: import_lazy_zod10.z.string().url().optional(),
7852
+ /** JWKS URI for upstream id_token validation (optional). */
7853
+ jwksUri: import_lazy_zod10.z.string().url().optional()
7854
+ }).refine((p) => !!(p.authorizationEndpoint ?? p.authorizeUrl), {
7855
+ message: "authorizationEndpoint (or authorizeUrl) is required",
7856
+ path: ["authorizationEndpoint"]
7857
+ }).refine((p) => !!(p.tokenEndpoint ?? p.tokenUrl), {
7858
+ message: "tokenEndpoint (or tokenUrl) is required",
7859
+ path: ["tokenEndpoint"]
7860
+ }).refine((p) => !(p.authorizationEndpoint && p.authorizeUrl), {
7861
+ message: "provide only one of authorizationEndpoint or authorizeUrl",
7862
+ path: ["authorizationEndpoint"]
7863
+ }).refine((p) => !(p.tokenEndpoint && p.tokenUrl), {
7864
+ message: "provide only one of tokenEndpoint or tokenUrl",
7865
+ path: ["tokenEndpoint"]
7866
+ });
7867
+ var localDcrClientSchema = import_lazy_zod10.z.object({
7868
+ /** Stable, pre-assigned client identifier the client authenticates with. */
7869
+ clientId: import_lazy_zod10.z.string().min(1),
7870
+ /**
7871
+ * Confidential-client secret. Omit for public clients
7872
+ * (`token_endpoint_auth_method: 'none'`, the default).
7873
+ */
7874
+ clientSecret: import_lazy_zod10.z.string().min(1).optional(),
7875
+ /** Exact redirect URIs this client is allowed to use (at least one). */
7876
+ redirectUris: import_lazy_zod10.z.array(import_lazy_zod10.z.string().url()).min(1, "At least one redirect_uri is required"),
7877
+ /** Human-readable name shown on consent/login screens. */
7878
+ clientName: import_lazy_zod10.z.string().optional(),
7879
+ /**
7880
+ * Token-endpoint auth method. Defaults to `'none'` (public PKCE client).
7881
+ */
7882
+ tokenEndpointAuthMethod: import_lazy_zod10.z.enum(["none", "client_secret_basic", "client_secret_post"]).default("none"),
7883
+ /** Allowed grant types. Defaults to `['authorization_code']`. */
7884
+ grantTypes: import_lazy_zod10.z.array(import_lazy_zod10.z.enum(["authorization_code", "refresh_token"])).default(["authorization_code"]),
7885
+ /** Allowed response types. Defaults to `['code']`. */
7886
+ responseTypes: import_lazy_zod10.z.array(import_lazy_zod10.z.enum(["code"])).default(["code"]),
7887
+ /** Optional space-delimited default scope string. */
7888
+ scope: import_lazy_zod10.z.string().optional()
7889
+ });
7890
+ var localDcrConfigSchema = import_lazy_zod10.z.object({
7891
+ /**
7892
+ * Whether `POST /oauth/register` is mounted/active and whether
7893
+ * `registration_endpoint` is advertised in AS metadata.
7894
+ *
7895
+ * When unset (default), the historical guard applies: enabled in
7896
+ * development, disabled in production. Set explicitly to force it on or off
7897
+ * regardless of `NODE_ENV`.
7898
+ */
7899
+ enabled: import_lazy_zod10.z.boolean().optional(),
7900
+ /**
7901
+ * Allowlist of redirect URIs. Each entry is matched EXACTLY or as a simple
7902
+ * glob (`*` matches any run of characters). When set, both DCR registrations
7903
+ * and `/oauth/authorize` requests are rejected unless their `redirect_uri`
7904
+ * matches an entry. When unset, no redirect_uri allowlist is enforced.
7905
+ */
7906
+ allowedRedirectUris: import_lazy_zod10.z.array(import_lazy_zod10.z.string().min(1)).optional(),
7907
+ /**
7908
+ * Allowlist of client ids. When set, only these `client_id`s may register
7909
+ * (DCR registrations are forced to one of these ids) or be used at
7910
+ * `/oauth/authorize`. When unset, any client id is accepted (subject to the
7911
+ * other checks). CIMD URL client ids are validated by the CIMD layer and are
7912
+ * exempt from this allowlist.
7913
+ */
7914
+ allowedClientIds: import_lazy_zod10.z.array(import_lazy_zod10.z.string().min(1)).optional(),
7915
+ /**
7916
+ * Initial Access Token (RFC 7591 §3). When set, `POST /oauth/register`
7917
+ * requires an `Authorization: Bearer <token>` header that matches this value
7918
+ * (constant-time compared); requests without it (or with a wrong value) are
7919
+ * rejected `401 invalid_token`. When unset, registration is unauthenticated
7920
+ * (the historical behavior).
7921
+ */
7922
+ initialAccessToken: import_lazy_zod10.z.string().min(1).optional(),
7923
+ /**
7924
+ * Pre-registered trusted clients seeded into the registry at startup. These
7925
+ * are accepted by the authorize/token flows WITHOUT a DCR round-trip, which
7926
+ * lets you disable DCR entirely while still shipping known clients.
7927
+ */
7928
+ clients: import_lazy_zod10.z.array(localDcrClientSchema).optional()
7929
+ });
6455
7930
  var flatRemoteProviderFields = {
6456
7931
  /**
6457
7932
  * OAuth provider base URL (required)
@@ -6467,7 +7942,64 @@ var flatRemoteProviderFields = {
6467
7942
  /** Advanced provider configuration */
6468
7943
  providerConfig: providerConfigSchema.optional()
6469
7944
  };
6470
- var tokenStorageConfigSchema = import_lazy_zod10.z.union([import_lazy_zod10.z.literal("memory"), import_lazy_zod10.z.object({ redis: redisConfigSchema })]);
7945
+ var tokenStorageSqliteSchema = import_lazy_zod10.z.object({
7946
+ /** Path to the `.sqlite` database file used for auth persistence. */
7947
+ path: import_lazy_zod10.z.string().min(1),
7948
+ /**
7949
+ * Encryption configuration for at-rest encryption of stored values.
7950
+ * Independent of the AES-GCM blob encryption already applied to upstream
7951
+ * provider tokens by the orchestrated-token store.
7952
+ */
7953
+ encryption: import_lazy_zod10.z.object({
7954
+ secret: import_lazy_zod10.z.string().min(1)
7955
+ }).optional(),
7956
+ /**
7957
+ * Interval in milliseconds for purging expired keys.
7958
+ * @default 60000
7959
+ */
7960
+ ttlCleanupIntervalMs: import_lazy_zod10.z.number().int().nonnegative().optional(),
7961
+ /**
7962
+ * Enable WAL mode for better read concurrency.
7963
+ * @default true
7964
+ */
7965
+ walMode: import_lazy_zod10.z.boolean().optional()
7966
+ });
7967
+ var tokenStorageConfigSchema = import_lazy_zod10.z.union([
7968
+ import_lazy_zod10.z.literal("memory"),
7969
+ import_lazy_zod10.z.object({ redis: redisConfigSchema }),
7970
+ import_lazy_zod10.z.object({ sqlite: tokenStorageSqliteSchema })
7971
+ ]);
7972
+ var secureStoreScopeSchema = import_lazy_zod10.z.enum(["user", "session", "global"]);
7973
+ var secureStoreEncryptionSchema = import_lazy_zod10.z.object({
7974
+ /**
7975
+ * Server pepper mixed into HKDF key derivation (defense-in-depth). Defaults to
7976
+ * `VAULT_SECRET ?? JWT_SECRET` at runtime when omitted.
7977
+ */
7978
+ pepper: import_lazy_zod10.z.string().min(1).optional()
7979
+ });
7980
+ var secureStoreCustomBackendSchema = import_lazy_zod10.z.custom(
7981
+ (v) => typeof v === "object" && v !== null && typeof v.get === "function" && typeof v.set === "function" && typeof v.delete === "function" && typeof v.list === "function",
7982
+ { message: "secureStore.backend must implement get/set/delete/list (SecureStoreBackend)" }
7983
+ );
7984
+ var secureStoreCommonFields = {
7985
+ /** Namespace scope. @default 'user' */
7986
+ scope: secureStoreScopeSchema.optional(),
7987
+ /** Default TTL (ms) for stored secrets. @default undefined (no expiry) */
7988
+ ttlMs: import_lazy_zod10.z.number().int().positive().optional(),
7989
+ /** At-rest encryption settings (built-in backings only). */
7990
+ encryption: secureStoreEncryptionSchema.optional()
7991
+ };
7992
+ var secureStoreConfigSchema = import_lazy_zod10.z.union([
7993
+ import_lazy_zod10.z.literal("memory"),
7994
+ // Backing-bearing variants MUST precede the bare-object variant: Zod object
7995
+ // schemas are non-strict (extra keys allowed), so a bare `z.object({ scope })`
7996
+ // would otherwise match `{ sqlite, scope }` first and silently strip the
7997
+ // backing. Ordering most-specific-first keeps sqlite/redis/custom intact.
7998
+ import_lazy_zod10.z.object({ sqlite: tokenStorageSqliteSchema, ...secureStoreCommonFields }),
7999
+ import_lazy_zod10.z.object({ redis: redisConfigSchema, ...secureStoreCommonFields }),
8000
+ import_lazy_zod10.z.object({ backend: secureStoreCustomBackendSchema, ...secureStoreCommonFields }),
8001
+ import_lazy_zod10.z.object({ ...secureStoreCommonFields })
8002
+ ]);
6471
8003
  var tokenRefreshConfigSchema = import_lazy_zod10.z.object({
6472
8004
  /**
6473
8005
  * Enable automatic token refresh
@@ -6534,7 +8066,22 @@ var federatedAuthConfigSchema = import_lazy_zod10.z.object({
6534
8066
  * - 'format': validate only "federated:{sessionId}:{nonce}" format
6535
8067
  * @default 'strict'
6536
8068
  */
6537
- stateValidation: import_lazy_zod10.z.enum(["strict", "format"]).default("strict")
8069
+ stateValidation: import_lazy_zod10.z.enum(["strict", "format"]).default("strict"),
8070
+ /**
8071
+ * Minimum number of upstream providers a user must link before a FrontMCP JWT
8072
+ * is minted. The federated callback refuses to issue a token until at least
8073
+ * this many providers have been authorized. Defaults to `1` at runtime when
8074
+ * `providers` are configured (i.e. "no JWT until ≥1 linked"). Left optional
8075
+ * here so existing configs without `providers` are completely unaffected.
8076
+ */
8077
+ minProviders: import_lazy_zod10.z.number().int().positive().optional(),
8078
+ /**
8079
+ * Provider ids that MUST be among the linked providers before a JWT is minted.
8080
+ * The federated callback rejects the login unless every id listed here is in
8081
+ * the user's selected providers. Optional — omit to allow any combination that
8082
+ * satisfies {@link minProviders}.
8083
+ */
8084
+ requiredProviders: import_lazy_zod10.z.array(import_lazy_zod10.z.string()).optional()
6538
8085
  });
6539
8086
  var incrementalAuthConfigSchema = import_lazy_zod10.z.object({
6540
8087
  /**
@@ -6635,9 +8182,19 @@ var transparentAuthOptionsSchema = import_lazy_zod12.z.object({
6635
8182
 
6636
8183
  // libs/auth/src/options/orchestrated.schema.ts
6637
8184
  var import_lazy_zod13 = require("@frontmcp/lazy-zod");
8185
+ var authSlotSchema = import_lazy_zod13.z.enum(["login", "consent", "incremental", "federated", "error"]);
8186
+ var authUiMapSchema = import_lazy_zod13.z.partialRecord(authSlotSchema, import_lazy_zod13.z.string().min(1));
8187
+ var authExtraHandlerSchema = import_lazy_zod13.z.custom((v) => typeof v === "function", {
8188
+ message: "auth.extras entries must be handler functions (input, ctx) => AuthExtraResult."
8189
+ });
8190
+ var authExtrasMapSchema = import_lazy_zod13.z.record(import_lazy_zod13.z.string().min(1), authExtraHandlerSchema);
6638
8191
  var sharedAuthFields = {
6639
8192
  local: localSigningConfigSchema.optional(),
6640
8193
  tokenStorage: tokenStorageConfigSchema.default("memory"),
8194
+ // #470 — backing for the general session secure-secret store (`this.secureStore`).
8195
+ // Optional (no default) so an unset value resolves to the in-memory encrypted
8196
+ // default at runtime; existing configs are unaffected.
8197
+ secureStore: secureStoreConfigSchema.optional(),
6641
8198
  allowDefaultPublic: import_lazy_zod13.z.boolean().default(false),
6642
8199
  anonymousScopes: import_lazy_zod13.z.array(import_lazy_zod13.z.string()).default(["anonymous"]),
6643
8200
  publicAccess: publicAccessConfigSchema.optional(),
@@ -6646,11 +8203,59 @@ var sharedAuthFields = {
6646
8203
  refresh: tokenRefreshConfigSchema.optional(),
6647
8204
  expectedAudience: import_lazy_zod13.z.union([import_lazy_zod13.z.string(), import_lazy_zod13.z.array(import_lazy_zod13.z.string())]).optional(),
6648
8205
  incrementalAuth: incrementalAuthConfigSchema.optional(),
6649
- cimd: cimdConfigSchema.optional()
8206
+ cimd: cimdConfigSchema.optional(),
8207
+ // #469 — custom auth UI as a slot→file map + extras name→handler map (per-app
8208
+ // under splitByApp). Both optional; omitting them serves the built-in pages.
8209
+ ui: authUiMapSchema.optional(),
8210
+ extras: authExtrasMapSchema.optional()
6650
8211
  };
8212
+ var loginFieldSchema = import_lazy_zod13.z.object({
8213
+ type: import_lazy_zod13.z.enum(["text", "password", "email", "select", "hidden"]),
8214
+ label: import_lazy_zod13.z.string().optional(),
8215
+ required: import_lazy_zod13.z.boolean().optional(),
8216
+ placeholder: import_lazy_zod13.z.string().optional(),
8217
+ options: import_lazy_zod13.z.array(import_lazy_zod13.z.object({ value: import_lazy_zod13.z.string(), label: import_lazy_zod13.z.string() })).optional()
8218
+ });
8219
+ var loginConfigSchema = import_lazy_zod13.z.object({
8220
+ title: import_lazy_zod13.z.string().optional(),
8221
+ subtitle: import_lazy_zod13.z.string().optional(),
8222
+ logoUri: import_lazy_zod13.z.string().optional(),
8223
+ fields: import_lazy_zod13.z.record(import_lazy_zod13.z.string(), loginFieldSchema).optional(),
8224
+ // Full HTML override — a function, validated structurally like other callbacks
8225
+ // in the codebase (see http/cors handlers using z.custom<…>).
8226
+ render: import_lazy_zod13.z.custom((v) => typeof v === "function", {
8227
+ message: "login.render must be a function"
8228
+ }).optional(),
8229
+ subject: import_lazy_zod13.z.object({
8230
+ fromField: import_lazy_zod13.z.string().optional(),
8231
+ strategy: import_lazy_zod13.z.enum(["per-session", "per-account"]).optional()
8232
+ }).optional()
8233
+ });
8234
+ var authenticateFnSchema = import_lazy_zod13.z.custom((v) => typeof v === "function", {
8235
+ message: "authenticate must be a function"
8236
+ });
6651
8237
  var localAuthSchema = import_lazy_zod13.z.object({
6652
8238
  mode: import_lazy_zod13.z.literal("local"),
6653
- ...sharedAuthFields
8239
+ ...sharedAuthFields,
8240
+ // #468 — opt-out of the email requirement at /oauth/callback for
8241
+ // single-operator local setups. Defaults preserve the historical behavior
8242
+ // (email required) so existing configs are unaffected.
8243
+ requireEmail: import_lazy_zod13.z.boolean().default(true),
8244
+ anonymousSubject: import_lazy_zod13.z.string().min(1).default("local-operator"),
8245
+ // Checkpoint 3a — declarative login customization + custom verification.
8246
+ // Both optional; omitting them preserves the default login path exactly.
8247
+ login: loginConfigSchema.optional(),
8248
+ authenticate: authenticateFnSchema.optional(),
8249
+ // Multi-OAuth-provider orchestration — declare upstream providers (GitHub,
8250
+ // Slack, Jira, …) to federate at /oauth/authorize. Optional; omitting it
8251
+ // preserves the default single-operator local login exactly.
8252
+ providers: import_lazy_zod13.z.array(upstreamProviderSchema).optional(),
8253
+ // Local-AS Dynamic Client Registration control surface (#462) — gate
8254
+ // `POST /oauth/register`, allowlist redirect_uris/client_ids, require an
8255
+ // initial access token, and seed pre-registered trusted clients. Optional;
8256
+ // omitting it preserves today's behavior exactly (DCR on in dev, off in prod;
8257
+ // no allowlist; no initial access token).
8258
+ dcr: localDcrConfigSchema.optional()
6654
8259
  });
6655
8260
  var remoteAuthSchema = import_lazy_zod13.z.object({
6656
8261
  mode: import_lazy_zod13.z.literal("remote"),
@@ -7447,7 +9052,7 @@ var CredentialCache = class {
7447
9052
  var AUTH_PROVIDERS_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:AUTH_PROVIDERS_ACCESSOR");
7448
9053
 
7449
9054
  // libs/auth/src/vault/auth-providers.accessor.impl.ts
7450
- var import_utils31 = require("@frontmcp/utils");
9055
+ var import_utils41 = require("@frontmcp/utils");
7451
9056
  var AuthProvidersAccessorImpl = class {
7452
9057
  constructor(registry, vault, cache, loader, context, logger) {
7453
9058
  this.registry = registry;
@@ -7656,7 +9261,7 @@ var AuthProvidersAccessorImpl = class {
7656
9261
  return { [credential.headerName]: credential.key };
7657
9262
  case "basic": {
7658
9263
  const value = `${credential.username}:${credential.password}`;
7659
- const encoded = credential.encodedValue ?? (0, import_utils31.base64Encode)(new TextEncoder().encode(value));
9264
+ const encoded = credential.encodedValue ?? (0, import_utils41.base64Encode)(new TextEncoder().encode(value));
7660
9265
  return { Authorization: `Basic ${encoded}` };
7661
9266
  }
7662
9267
  case "custom":
@@ -9192,6 +10797,7 @@ var FetchCredentialMiddleware = class {
9192
10797
  AuthorityDeniedError,
9193
10798
  AuthorizationBase,
9194
10799
  CDN,
10800
+ CREDENTIALS_ACCESSOR,
9195
10801
  CimdClientIdMismatchError,
9196
10802
  CimdDisabledError,
9197
10803
  CimdError,
@@ -9203,11 +10809,15 @@ var FetchCredentialMiddleware = class {
9203
10809
  CredentialCache,
9204
10810
  CredentialProviderAlreadyRegisteredError,
9205
10811
  CredentialStorageError,
10812
+ CredentialsAccessorImpl,
10813
+ DEFAULT_RESUME_TTL_MS,
9206
10814
  DEFAULT_THEME,
10815
+ DcrClientRegistry,
9207
10816
  EagerCredentialLoader,
9208
10817
  ElicitationSecretRequiredError,
9209
10818
  EncryptedRedisVault,
9210
10819
  EncryptedStorageError,
10820
+ EncryptedStorageSecureStoreBackend,
9211
10821
  EncryptedTypedStorage,
9212
10822
  EncryptionContextNotSetError,
9213
10823
  EncryptionKeyNotConfiguredError,
@@ -9216,6 +10826,7 @@ var FetchCredentialMiddleware = class {
9216
10826
  InMemoryAuthorizationStore,
9217
10827
  InMemoryAuthorizationVault,
9218
10828
  InMemoryCimdCache,
10829
+ InMemoryConsentStore,
9219
10830
  InMemoryFederatedAuthSessionStore,
9220
10831
  InMemoryOrchestratedTokenStore,
9221
10832
  InMemoryStoreRequiredError,
@@ -9233,11 +10844,18 @@ var FetchCredentialMiddleware = class {
9233
10844
  RedirectUriMismatchError,
9234
10845
  RedisAuthorizationStore,
9235
10846
  RedisSessionStore,
10847
+ SECURE_STORE_ACCESSOR,
9236
10848
  ScopeDeniedError,
10849
+ SecureStoreAccessorImpl,
10850
+ SessionCredentialVault,
9237
10851
  SessionIdEmptyError,
9238
10852
  SessionRateLimiter,
9239
10853
  SessionSecretRequiredError,
10854
+ StorageAuthorizationStore,
9240
10855
  StorageAuthorizationVault,
10856
+ StorageConsentStore,
10857
+ StorageFederatedAuthSessionStore,
10858
+ StorageOrchestratedTokenStore,
9241
10859
  StorageTokenStore,
9242
10860
  TinyTtlCache,
9243
10861
  TokenLeakDetectedError,
@@ -9269,6 +10887,7 @@ var FetchCredentialMiddleware = class {
9269
10887
  authProviderMappingSchema,
9270
10888
  authProvidersVaultOptionsSchema,
9271
10889
  authUserSchema,
10890
+ authenticateFnSchema,
9272
10891
  authoritiesClaimsMappingSchema,
9273
10892
  authoritiesConfigSchema,
9274
10893
  authoritiesMetadataSchema,
@@ -9283,7 +10902,11 @@ var FetchCredentialMiddleware = class {
9283
10902
  bearerApplier,
9284
10903
  bearerCredentialSchema,
9285
10904
  buildAuthContext,
10905
+ buildCodeRecord,
10906
+ buildConnectPage,
10907
+ buildConnectSuccessPage,
9286
10908
  buildConsentPage,
10909
+ buildCredentialResumeUrl,
9287
10910
  buildErrorPage,
9288
10911
  buildFederatedLoginPage,
9289
10912
  buildIncrementalAuthPage,
@@ -9291,7 +10914,9 @@ var FetchCredentialMiddleware = class {
9291
10914
  buildInvalidRequestHeader,
9292
10915
  buildInvalidTokenHeader,
9293
10916
  buildLoginPage,
10917
+ buildPendingRecord,
9294
10918
  buildPrmUrl,
10919
+ buildRefreshTokenRecord,
9295
10920
  buildToolConsentPage,
9296
10921
  buildUnauthorizedHeader,
9297
10922
  buildWwwAuthenticate,
@@ -9304,6 +10929,7 @@ var FetchCredentialMiddleware = class {
9304
10929
  clientMetadataDocumentSchema,
9305
10930
  completeCurrentProvider,
9306
10931
  consentConfigSchema,
10932
+ consentRecordKey,
9307
10933
  consentSelectionSchema,
9308
10934
  consentStateSchema,
9309
10935
  consentToolItemSchema,
@@ -9311,6 +10937,9 @@ var FetchCredentialMiddleware = class {
9311
10937
  createEncryptedVault,
9312
10938
  createFederatedAuthSession,
9313
10939
  createLayout,
10940
+ createSecureStore,
10941
+ createSessionCredentialVault,
10942
+ createTokenStorageAdapter,
9314
10943
  credentialProviderConfigSchema,
9315
10944
  credentialSchema,
9316
10945
  credentialScopeSchema,
@@ -9344,7 +10973,9 @@ var FetchCredentialMiddleware = class {
9344
10973
  federatedSelectionSchema,
9345
10974
  flatRemoteProviderFields,
9346
10975
  fromSessionRecord,
10976
+ generateAuthorizationCode,
9347
10977
  generatePkceChallenge,
10978
+ generateRefreshTokenValue,
9348
10979
  getCredentialOptionsSchema,
9349
10980
  getKey,
9350
10981
  getNextProvider,
@@ -9362,12 +10993,15 @@ var FetchCredentialMiddleware = class {
9362
10993
  isOrchestratedLocal,
9363
10994
  isOrchestratedMode,
9364
10995
  isOrchestratedRemote,
10996
+ isPersistentTokenStorage,
9365
10997
  isPublicMode,
10998
+ isRedisTokenStorage,
9366
10999
  isRemoteMode,
9367
11000
  isSessionComplete,
9368
11001
  isSignedSession,
9369
11002
  isSoonExpiring,
9370
11003
  isSoonExpiringProvider,
11004
+ isSqliteTokenStorage,
9371
11005
  isTransparentMode,
9372
11006
  jsonWebKeySetSchema,
9373
11007
  jwkParametersSchema,
@@ -9376,7 +11010,10 @@ var FetchCredentialMiddleware = class {
9376
11010
  llmSafeAuthContextSchema,
9377
11011
  loadingStrategySchema,
9378
11012
  localAuthSchema,
11013
+ localDcrClientSchema,
11014
+ localDcrConfigSchema,
9379
11015
  localSigningConfigSchema,
11016
+ loginConfigSchema,
9380
11017
  mtlsCredentialSchema,
9381
11018
  noopLogger,
9382
11019
  normalizeIssuer,
@@ -9402,14 +11039,20 @@ var FetchCredentialMiddleware = class {
9402
11039
  redisVaultEntrySchema,
9403
11040
  remoteAuthSchema,
9404
11041
  remoteProviderConfigSchema,
11042
+ renderLocalLoginPage,
9405
11043
  renderToHtml,
9406
11044
  resetCachedKey,
9407
11045
  resolveRequiredScopes,
9408
11046
  resourceIdRefSchema,
9409
11047
  safeDecrypt,
11048
+ secureStoreConfigSchema,
11049
+ secureStoreCustomBackendSchema,
11050
+ secureStoreEncryptionSchema,
11051
+ secureStoreScopeSchema,
9410
11052
  serviceAccountCredentialSchema,
9411
11053
  sessionIdPayloadSchema,
9412
11054
  sessionJwtPayloadSchema,
11055
+ signCredentialResumeToken,
9413
11056
  signSession,
9414
11057
  skippedAppBehaviorSchema,
9415
11058
  sseTransportStateSchema,
@@ -9420,20 +11063,24 @@ var FetchCredentialMiddleware = class {
9420
11063
  storedSessionSchema,
9421
11064
  streamableHttpTransportStateSchema,
9422
11065
  toEpochSeconds,
11066
+ toLoginExtraFields,
9423
11067
  toSessionRecord,
9424
11068
  tokenRefreshConfigSchema,
9425
11069
  tokenStorageConfigSchema,
11070
+ tokenStorageSqliteSchema,
9426
11071
  transparentAuthOptionsSchema,
9427
11072
  transportProtocolSchema,
9428
11073
  transportSessionSchema,
9429
11074
  transportStateSchema,
9430
11075
  trimSlash,
9431
11076
  tryJwtExp,
11077
+ upstreamProviderSchema,
9432
11078
  userClaimSchema,
9433
11079
  validateAudience,
9434
11080
  validateClientIdUrl,
9435
11081
  vaultConsentRecordSchema,
9436
11082
  vaultFederatedRecordSchema,
11083
+ verifyCredentialResumeToken,
9437
11084
  verifyOrParseSession,
9438
11085
  verifyPkce,
9439
11086
  verifySession,