@frontmcp/auth 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +5 -5
  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 +5 -5
  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/esm/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { createRequire as __frontmcpCreateRequire } from 'module';
2
+ const require = __frontmcpCreateRequire(import.meta.url);
1
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
2
4
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
5
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -213,15 +215,27 @@ var init_cimd_cache = __esm({
213
215
  });
214
216
 
215
217
  // libs/auth/src/jwks/jwks.service.ts
216
- import { jwtVerify, createLocalJWKSet, decodeProtectedHeader } from "jose";
218
+ import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
217
219
  import {
218
220
  bytesToHex,
219
- randomBytes,
220
- rsaVerify,
221
221
  createKeyPersistence,
222
- isProduction
222
+ isProduction,
223
+ randomBytes,
224
+ rsaVerify
223
225
  } from "@frontmcp/utils";
224
226
 
227
+ // libs/auth/src/common/auth-logger.interface.ts
228
+ var noopAuthLogger = {
229
+ info() {
230
+ },
231
+ warn() {
232
+ },
233
+ error() {
234
+ },
235
+ debug() {
236
+ }
237
+ };
238
+
225
239
  // libs/auth/src/jwks/jwks.utils.ts
226
240
  function trimSlash(s) {
227
241
  const str = s ?? "";
@@ -251,18 +265,6 @@ function decodeJwtPayloadSafe(token) {
251
265
  }
252
266
  }
253
267
 
254
- // libs/auth/src/common/auth-logger.interface.ts
255
- var noopAuthLogger = {
256
- info() {
257
- },
258
- warn() {
259
- },
260
- error() {
261
- },
262
- debug() {
263
- }
264
- };
265
-
266
268
  // libs/auth/src/jwks/jwks.service.ts
267
269
  var WEAK_KEY_WARNING = `
268
270
  \u26A0\uFE0F SECURITY WARNING: OAuth provider is using an RSA key smaller than 2048 bits.
@@ -321,28 +323,6 @@ var JwksService = class {
321
323
  // ===========================================================================
322
324
  // Scope-aware verification API
323
325
  // ===========================================================================
324
- /** Verify a token issued by the gateway itself (orchestrated mode). */
325
- async verifyGatewayToken(token, expectedIssuer) {
326
- try {
327
- const payload = decodeJwtPayloadSafe(token);
328
- if (!payload) {
329
- return {
330
- ok: false,
331
- error: "invalid bearer token"
332
- };
333
- }
334
- return {
335
- ok: true,
336
- issuer: expectedIssuer,
337
- sub: payload["sub"],
338
- payload,
339
- header: decodeProtectedHeader(token)
340
- };
341
- } catch (err) {
342
- const message = err instanceof Error ? err.message : "verification_failed";
343
- return { ok: false, error: message };
344
- }
345
- }
346
326
  /**
347
327
  * Verify a token against candidate transparent providers.
348
328
  * Ensures JWKS are available (cached/TTL/AS discovery) per provider.
@@ -987,32 +967,52 @@ function buildFederatedLoginPage(params) {
987
967
  return wideLayout(content, { title: "Select Providers" });
988
968
  }
989
969
  function buildToolConsentPage(params) {
990
- const { tools, clientName, pendingAuthId, csrfToken, callbackPath, userName, userEmail } = params;
991
- const toolsByApp = {};
992
- for (const tool of tools) {
993
- if (!toolsByApp[tool.appId]) {
994
- toolsByApp[tool.appId] = { appName: tool.appName, tools: [] };
995
- }
996
- toolsByApp[tool.appId].tools.push(tool);
997
- }
998
- const userInfo = userName || userEmail ? `<div class="p-3 bg-gray-50 rounded-lg mb-6 text-sm">
999
- <span class="text-gray-500">Signed in as: </span>
1000
- <span class="font-medium text-gray-900">${escapeHtml3(userName || userEmail || "")}</span>
1001
- </div>` : "";
1002
- const appGroups = Object.entries(toolsByApp).map(([appId, { appName, tools: appTools }]) => {
1003
- const toolItems = appTools.map((tool) => {
1004
- const desc = tool.description ? `<p class="text-sm text-gray-500 mt-0.5">${escapeHtml3(tool.description)}</p>` : "";
1005
- return `<label class="flex items-start gap-3 p-3 bg-white rounded-lg cursor-pointer hover:bg-gray-50">
1006
- <input type="checkbox" name="tools" value="${escapeHtml3(
1007
- tool.toolId
1008
- )}" class="mt-0.5 w-5 h-5 rounded border-gray-300" checked>
970
+ const {
971
+ tools,
972
+ clientName,
973
+ pendingAuthId,
974
+ csrfToken,
975
+ callbackPath,
976
+ userName,
977
+ userEmail,
978
+ groupByApp = true,
979
+ showDescriptions = true,
980
+ customMessage,
981
+ allowSelectAll = true,
982
+ requireSelection = true,
983
+ defaultSelectedTools,
984
+ error,
985
+ hiddenFields = []
986
+ } = params;
987
+ const defaultSet = defaultSelectedTools ? new Set(defaultSelectedTools) : void 0;
988
+ const isChecked = (toolId) => defaultSet ? defaultSet.has(toolId) : true;
989
+ const checkedCount = tools.filter((t) => isChecked(t.toolId)).length;
990
+ const renderTool = (tool) => {
991
+ const desc = showDescriptions && tool.description ? `<p class="text-sm text-gray-500 mt-0.5">${escapeHtml3(tool.description)}</p>` : "";
992
+ return `<label class="flex items-start gap-3 p-3 bg-white rounded-lg cursor-pointer hover:bg-gray-50">
993
+ <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" : ""}>
1009
994
  <div>
1010
995
  <span class="font-medium text-gray-900">${escapeHtml3(tool.toolName)}</span>
1011
996
  ${desc}
1012
997
  </div>
1013
998
  </label>`;
1014
- }).join("\n");
1015
- return `<div class="bg-gray-50 rounded-xl overflow-hidden">
999
+ };
1000
+ let toolSection;
1001
+ if (groupByApp) {
1002
+ const toolsByApp = {};
1003
+ for (const tool of tools) {
1004
+ if (!toolsByApp[tool.appId]) {
1005
+ toolsByApp[tool.appId] = { appName: tool.appName, tools: [] };
1006
+ }
1007
+ toolsByApp[tool.appId].tools.push(tool);
1008
+ }
1009
+ const toggleAllButton = allowSelectAll ? `<button type="button" class="text-sm text-blue-600 hover:text-blue-800"
1010
+ 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();">
1011
+ Toggle All
1012
+ </button>` : "";
1013
+ toolSection = Object.entries(toolsByApp).map(([appId, { appName, tools: appTools }]) => {
1014
+ const toolItems = appTools.map(renderTool).join("\n");
1015
+ return `<div class="bg-gray-50 rounded-xl overflow-hidden">
1016
1016
  <div class="flex items-center justify-between px-4 py-3 bg-gray-100">
1017
1017
  <div class="flex items-center gap-3">
1018
1018
  <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">
@@ -1020,51 +1020,70 @@ function buildToolConsentPage(params) {
1020
1020
  </div>
1021
1021
  <span class="font-semibold text-gray-900">${escapeHtml3(appName)}</span>
1022
1022
  </div>
1023
- <button type="button" class="text-sm text-blue-600 hover:text-blue-800"
1024
- 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();">
1025
- Toggle All
1026
- </button>
1023
+ ${toggleAllButton}
1027
1024
  </div>
1028
1025
  <div class="p-4 space-y-2" data-app="${escapeHtml3(appId)}">
1029
1026
  ${toolItems}
1030
1027
  </div>
1031
1028
  </div>`;
1032
- }).join("\n");
1029
+ }).join("\n");
1030
+ } else {
1031
+ toolSection = `<div class="bg-gray-50 rounded-xl p-4 space-y-2" data-app="__all__">
1032
+ ${tools.map(renderTool).join("\n")}
1033
+ </div>`;
1034
+ }
1035
+ const userInfo = userName || userEmail ? `<div class="p-3 bg-gray-50 rounded-lg mb-6 text-sm">
1036
+ <span class="text-gray-500">Signed in as: </span>
1037
+ <span class="font-medium text-gray-900">${escapeHtml3(userName || userEmail || "")}</span>
1038
+ </div>` : "";
1039
+ 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>` : "";
1040
+ const hiddenInputs = hiddenFields.map((f) => `<input type="hidden" name="${escapeHtml3(f.name)}" value="${escapeHtml3(f.value)}">`).join("\n ");
1041
+ const selectAllControl = allowSelectAll ? `<label class="flex items-center gap-3 cursor-pointer">
1042
+ <input type="checkbox" id="select-all" class="w-5 h-5 rounded border-gray-300"${checkedCount === tools.length && tools.length > 0 ? " checked" : ""}
1043
+ onchange="document.querySelectorAll('input[name=tools]').forEach(cb => cb.checked = this.checked); updateCount();">
1044
+ <span class="text-gray-700">Select all tools</span>
1045
+ </label>` : "<span></span>";
1046
+ const requireSelectionScript = requireSelection ? `
1047
+ const submitBtn = document.getElementById('consent-submit');
1048
+ if (submitBtn) submitBtn.disabled = checked.length === 0;` : "";
1033
1049
  const updateCountScript = `
1034
1050
  <script>
1035
1051
  function updateCount() {
1036
1052
  const all = document.querySelectorAll('input[name="tools"]');
1037
1053
  const checked = document.querySelectorAll('input[name="tools"]:checked');
1038
- document.getElementById('selection-count').textContent = checked.length + ' of ' + all.length + ' selected';
1039
- document.getElementById('select-all').checked = all.length > 0 && all.length === checked.length;
1054
+ const countEl = document.getElementById('selection-count');
1055
+ if (countEl) countEl.textContent = checked.length + ' of ' + all.length + ' selected';
1056
+ const selectAll = document.getElementById('select-all');
1057
+ if (selectAll) selectAll.checked = all.length > 0 && all.length === checked.length;${requireSelectionScript}
1040
1058
  }
1041
1059
  document.querySelectorAll('input[name="tools"]').forEach(cb => cb.addEventListener('change', updateCount));
1060
+ updateCount();
1042
1061
  </script>`;
1062
+ const subtitle = customMessage ? escapeHtml3(customMessage) : `Choose which tools ${escapeHtml3(clientName)} can access. You can change this later.`;
1043
1063
  const content = `
1044
1064
  <h1 class="text-3xl font-bold text-gray-900 mb-4">Select Tools to Enable</h1>
1045
1065
  <p class="text-gray-600 mb-6">
1046
- Choose which tools ${escapeHtml3(clientName)} can access. You can change this later.
1066
+ ${subtitle}
1047
1067
  </p>
1048
1068
 
1069
+ ${errorBanner}
1049
1070
  ${userInfo}
1050
1071
 
1051
- <form method="POST" action="${escapeHtml3(callbackPath)}" id="consent-form">
1072
+ <form method="GET" action="${escapeHtml3(callbackPath)}" id="consent-form">
1052
1073
  <input type="hidden" name="csrf" value="${escapeHtml3(csrfToken)}">
1053
1074
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1075
+ <input type="hidden" name="consent_submitted" value="1">
1076
+ ${hiddenInputs}
1054
1077
 
1055
1078
  <!-- Select all toggle -->
1056
1079
  <div class="flex items-center justify-between mb-6">
1057
- <label class="flex items-center gap-3 cursor-pointer">
1058
- <input type="checkbox" id="select-all" class="w-5 h-5 rounded border-gray-300" checked
1059
- onchange="document.querySelectorAll('input[name=tools]').forEach(cb => cb.checked = this.checked); updateCount();">
1060
- <span class="text-gray-700">Select all tools</span>
1061
- </label>
1062
- <span id="selection-count" class="text-sm text-gray-500">${tools.length} of ${tools.length} selected</span>
1080
+ ${selectAllControl}
1081
+ <span id="selection-count" class="text-sm text-gray-500">${checkedCount} of ${tools.length} selected</span>
1063
1082
  </div>
1064
1083
 
1065
- <!-- Tool groups by app -->
1084
+ <!-- Tool groups -->
1066
1085
  <div class="space-y-6 mb-8">
1067
- ${appGroups}
1086
+ ${toolSection}
1068
1087
  </div>
1069
1088
 
1070
1089
  <!-- Buttons -->
@@ -1073,8 +1092,8 @@ function buildToolConsentPage(params) {
1073
1092
  class="flex-1 px-6 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 font-medium rounded-lg transition-colors">
1074
1093
  Cancel
1075
1094
  </button>
1076
- <button type="submit"
1077
- class="flex-1 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
1095
+ <button type="submit" id="consent-submit"
1096
+ 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">
1078
1097
  Confirm Selection
1079
1098
  </button>
1080
1099
  </div>
@@ -1082,22 +1101,42 @@ function buildToolConsentPage(params) {
1082
1101
  ${updateCountScript}`;
1083
1102
  return extraWideLayout(content, { title: "Select Tools" });
1084
1103
  }
1104
+ function buildLoginFieldHtml(field) {
1105
+ const id = `field-${field.name}`;
1106
+ const safeName = escapeHtml3(field.name);
1107
+ const requiredAttr = field.required ? " required" : "";
1108
+ const labelText = escapeHtml3(field.label ?? field.name);
1109
+ const safeValue = field.value !== void 0 ? escapeHtml3(field.value) : "";
1110
+ if (field.type === "hidden") {
1111
+ return `<input type="hidden" name="${safeName}" value="${safeValue}">`;
1112
+ }
1113
+ 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";
1114
+ let control;
1115
+ if (field.type === "select") {
1116
+ const opts = (field.options ?? []).map(
1117
+ (o) => `<option value="${escapeHtml3(o.value)}"${o.value === field.value ? " selected" : ""}>${escapeHtml3(
1118
+ o.label
1119
+ )}</option>`
1120
+ ).join("");
1121
+ control = `<select id="${escapeHtml3(id)}" name="${safeName}"${requiredAttr} class="${inputClass}">${opts}</select>`;
1122
+ } else {
1123
+ const placeholder = field.placeholder ? ` placeholder="${escapeHtml3(field.placeholder)}"` : "";
1124
+ control = `<input type="${escapeHtml3(field.type)}" id="${escapeHtml3(
1125
+ id
1126
+ )}" name="${safeName}"${requiredAttr}${placeholder} value="${safeValue}" class="${inputClass}">`;
1127
+ }
1128
+ return `<div class="mb-4">
1129
+ <label for="${escapeHtml3(id)}" class="block text-sm font-medium text-gray-700 mb-2">${labelText}</label>
1130
+ ${control}
1131
+ </div>`;
1132
+ }
1085
1133
  function buildLoginPage(params) {
1086
- const { clientName, scope, pendingAuthId, callbackPath } = params;
1134
+ const { clientName, scope, pendingAuthId, callbackPath, title, subtitle, logoUri, error, extraFields } = params;
1087
1135
  const scopesHtml = scope ? `<div class="p-4 bg-gray-50 rounded-lg mb-6">
1088
1136
  <p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">Requested permissions</p>
1089
1137
  <p class="text-gray-700">${scope.split(" ").map((s) => escapeHtml3(s)).join(", ") || "Default access"}</p>
1090
1138
  </div>` : "";
1091
- const content = `
1092
- <div class="bg-white rounded-2xl shadow-xl p-8">
1093
- <h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">Sign In</h1>
1094
- <p class="text-gray-600 mb-8 text-center">Authorize access to ${escapeHtml3(clientName)}</p>
1095
-
1096
- ${scopesHtml}
1097
-
1098
- <form method="GET" action="${escapeHtml3(callbackPath)}">
1099
- <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1100
-
1139
+ const defaultFieldsHtml = `
1101
1140
  <!-- Email -->
1102
1141
  <div class="mb-4">
1103
1142
  <label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
@@ -1110,7 +1149,27 @@ function buildLoginPage(params) {
1110
1149
  <label for="name" class="block text-sm font-medium text-gray-700 mb-2">Name (optional)</label>
1111
1150
  <input type="text" id="name" name="name" placeholder="Your name"
1112
1151
  class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
1113
- </div>
1152
+ </div>`;
1153
+ const fieldsHtml = extraFields && extraFields.length > 0 ? extraFields.map((f) => buildLoginFieldHtml(f)).join("\n") : defaultFieldsHtml;
1154
+ const logoHtml = logoUri ? `<div class="flex justify-center mb-4"><img src="${escapeHtml3(
1155
+ logoUri
1156
+ )}" alt="${escapeHtml3(clientName)}" class="h-12 w-12 rounded-lg object-cover"></div>` : "";
1157
+ 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>` : "";
1158
+ const headingText = escapeHtml3(title ?? "Sign In");
1159
+ const subtitleText = subtitle ? escapeHtml3(subtitle) : `Authorize access to ${escapeHtml3(clientName)}`;
1160
+ const content = `
1161
+ <div class="bg-white rounded-2xl shadow-xl p-8">
1162
+ ${logoHtml}
1163
+ <h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">${headingText}</h1>
1164
+ <p class="text-gray-600 mb-8 text-center">${subtitleText}</p>
1165
+
1166
+ ${errorHtml}
1167
+
1168
+ ${scopesHtml}
1169
+
1170
+ <form method="GET" action="${escapeHtml3(callbackPath)}">
1171
+ <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1172
+ ${fieldsHtml}
1114
1173
 
1115
1174
  <!-- Submit -->
1116
1175
  <button type="submit"
@@ -1121,7 +1180,51 @@ function buildLoginPage(params) {
1121
1180
 
1122
1181
  <p class="text-center text-sm text-gray-500 mt-6">Client: ${escapeHtml3(clientName)}</p>
1123
1182
  </div>`;
1124
- return centeredCardLayout(content, { title: "Sign In" });
1183
+ return centeredCardLayout(content, { title: title ?? "Sign In" });
1184
+ }
1185
+ function buildConnectPage(params) {
1186
+ const { token, field, action, title, subtitle, logoUri, error } = params;
1187
+ const logoHtml = logoUri ? `<div class="flex justify-center mb-4"><img src="${escapeHtml3(
1188
+ logoUri
1189
+ )}" alt="Connect" class="h-12 w-12 rounded-lg object-cover"></div>` : "";
1190
+ 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>` : "";
1191
+ const headingText = escapeHtml3(title ?? "Connect credential");
1192
+ const subtitleText = subtitle ? escapeHtml3(subtitle) : `Connect "${escapeHtml3(field.name)}" to continue`;
1193
+ const content = `
1194
+ <div class="bg-white rounded-2xl shadow-xl p-8">
1195
+ ${logoHtml}
1196
+ <h1 class="text-3xl font-bold text-gray-900 mb-2 text-center">${headingText}</h1>
1197
+ <p class="text-gray-600 mb-8 text-center">${subtitleText}</p>
1198
+
1199
+ ${errorHtml}
1200
+
1201
+ <form method="POST" action="${escapeHtml3(action)}">
1202
+ <input type="hidden" name="token" value="${escapeHtml3(token)}">
1203
+ ${buildLoginFieldHtml(field)}
1204
+
1205
+ <button type="submit"
1206
+ class="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
1207
+ Connect
1208
+ </button>
1209
+ </form>
1210
+ </div>`;
1211
+ return centeredCardLayout(content, { title: title ?? "Connect credential" });
1212
+ }
1213
+ function buildConnectSuccessPage(params) {
1214
+ const { key, message } = params;
1215
+ const content = `
1216
+ <div class="bg-white rounded-2xl shadow-xl p-8 text-center">
1217
+ <div class="flex justify-center mb-6">
1218
+ <div class="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
1219
+ <svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1220
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
1221
+ </svg>
1222
+ </div>
1223
+ </div>
1224
+ <h1 class="text-2xl font-bold text-gray-900 mb-4">Credential connected</h1>
1225
+ <p class="text-gray-600">${escapeHtml3(message ?? `"${key}" is now connected. You can return to your session.`)}</p>
1226
+ </div>`;
1227
+ return centeredCardLayout(content, { title: "Connected" });
1125
1228
  }
1126
1229
  function buildErrorPage(params) {
1127
1230
  const { error, description } = params;
@@ -1148,6 +1251,37 @@ function renderToHtml(html, _options) {
1148
1251
  return html;
1149
1252
  }
1150
1253
 
1254
+ // libs/auth/src/ui/local-login.helper.ts
1255
+ function toLoginExtraFields(fields, values) {
1256
+ if (!fields) return [];
1257
+ return Object.entries(fields).map(([name, def]) => ({
1258
+ name,
1259
+ type: def.type,
1260
+ label: def.label,
1261
+ required: def.required,
1262
+ placeholder: def.placeholder,
1263
+ options: def.options,
1264
+ value: values?.[name]
1265
+ }));
1266
+ }
1267
+ function renderLocalLoginPage(login, ctx, values) {
1268
+ if (login?.render) {
1269
+ return login.render({ ...ctx, values });
1270
+ }
1271
+ const extraFields = login?.fields ? toLoginExtraFields(login.fields, values) : void 0;
1272
+ return buildLoginPage({
1273
+ clientName: ctx.clientName,
1274
+ scope: ctx.scopes.join(" "),
1275
+ pendingAuthId: ctx.pendingAuthId,
1276
+ callbackPath: ctx.callbackPath,
1277
+ title: login?.title,
1278
+ subtitle: login?.subtitle,
1279
+ logoUri: login?.logoUri ?? ctx.logoUri,
1280
+ error: ctx.error,
1281
+ extraFields
1282
+ });
1283
+ }
1284
+
1151
1285
  // libs/auth/src/session/authorization.store.ts
1152
1286
  import { z } from "@frontmcp/lazy-zod";
1153
1287
  import { randomUUID, sha256Base64url } from "@frontmcp/utils";
@@ -1175,8 +1309,83 @@ var authorizationCodeRecordSchema = z.object({
1175
1309
  skippedProviderIds: z.array(z.string()).optional(),
1176
1310
  consentEnabled: z.boolean().optional(),
1177
1311
  federatedLoginUsed: z.boolean().optional(),
1178
- pendingAuthId: z.string().optional()
1312
+ pendingAuthId: z.string().optional(),
1313
+ // Progressive/Incremental authorization: granted app-id set.
1314
+ authorizedAppIds: z.array(z.string()).optional(),
1315
+ // Custom claims from a local authenticate() verifier (Checkpoint 3a).
1316
+ customClaims: z.record(z.string(), z.unknown()).optional()
1179
1317
  });
1318
+ var AUTH_CODE_TTL_MS = 60 * 1e3;
1319
+ var PENDING_AUTH_TTL_MS = 10 * 60 * 1e3;
1320
+ var REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
1321
+ function generateAuthorizationCode() {
1322
+ return randomUUID().replace(/-/g, "") + randomUUID().replace(/-/g, "");
1323
+ }
1324
+ function generateRefreshTokenValue() {
1325
+ return randomUUID() + "-" + randomUUID();
1326
+ }
1327
+ function buildCodeRecord(params) {
1328
+ const now = Date.now();
1329
+ return {
1330
+ code: generateAuthorizationCode(),
1331
+ clientId: params.clientId,
1332
+ redirectUri: params.redirectUri,
1333
+ scopes: params.scopes,
1334
+ pkce: params.pkce,
1335
+ userSub: params.userSub,
1336
+ userEmail: params.userEmail,
1337
+ userName: params.userName,
1338
+ state: params.state,
1339
+ resource: params.resource,
1340
+ createdAt: now,
1341
+ expiresAt: now + AUTH_CODE_TTL_MS,
1342
+ used: false,
1343
+ selectedToolIds: params.selectedToolIds,
1344
+ selectedProviderIds: params.selectedProviderIds,
1345
+ skippedProviderIds: params.skippedProviderIds,
1346
+ consentEnabled: params.consentEnabled,
1347
+ federatedLoginUsed: params.federatedLoginUsed,
1348
+ pendingAuthId: params.pendingAuthId,
1349
+ authorizedAppIds: params.authorizedAppIds,
1350
+ customClaims: params.customClaims
1351
+ };
1352
+ }
1353
+ function buildPendingRecord(params) {
1354
+ const now = Date.now();
1355
+ return {
1356
+ id: randomUUID(),
1357
+ clientId: params.clientId,
1358
+ redirectUri: params.redirectUri,
1359
+ scopes: params.scopes,
1360
+ pkce: params.pkce,
1361
+ state: params.state,
1362
+ resource: params.resource,
1363
+ createdAt: now,
1364
+ expiresAt: now + PENDING_AUTH_TTL_MS,
1365
+ isIncremental: params.isIncremental,
1366
+ targetAppId: params.targetAppId,
1367
+ targetToolId: params.targetToolId,
1368
+ existingSessionId: params.existingSessionId,
1369
+ existingAuthorizationId: params.existingAuthorizationId,
1370
+ priorAuthorizedAppIds: params.priorAuthorizedAppIds,
1371
+ federatedLogin: params.federatedLogin,
1372
+ consent: params.consent,
1373
+ authUiCsrf: params.authUiCsrf
1374
+ };
1375
+ }
1376
+ function buildRefreshTokenRecord(params) {
1377
+ const now = Date.now();
1378
+ return {
1379
+ token: generateRefreshTokenValue(),
1380
+ clientId: params.clientId,
1381
+ userSub: params.userSub,
1382
+ scopes: params.scopes,
1383
+ resource: params.resource,
1384
+ createdAt: now,
1385
+ expiresAt: now + REFRESH_TOKEN_TTL_MS,
1386
+ revoked: false
1387
+ };
1388
+ }
1180
1389
  function verifyPkce(codeVerifier, challenge) {
1181
1390
  if (challenge.method !== "S256") {
1182
1391
  return false;
@@ -1192,17 +1401,11 @@ var InMemoryAuthorizationStore = class {
1192
1401
  codes = /* @__PURE__ */ new Map();
1193
1402
  pending = /* @__PURE__ */ new Map();
1194
1403
  refreshTokens = /* @__PURE__ */ new Map();
1195
- /** Default TTL for authorization codes (60 seconds) */
1196
- codeTtlMs = 60 * 1e3;
1197
- /** Default TTL for pending authorizations (10 minutes) */
1198
- pendingTtlMs = 10 * 60 * 1e3;
1199
- /** Default TTL for refresh tokens (30 days) */
1200
- refreshTtlMs = 30 * 24 * 60 * 60 * 1e3;
1201
1404
  generateCode() {
1202
- return randomUUID().replace(/-/g, "") + randomUUID().replace(/-/g, "");
1405
+ return generateAuthorizationCode();
1203
1406
  }
1204
1407
  generateRefreshToken() {
1205
- return randomUUID() + "-" + randomUUID();
1408
+ return generateRefreshTokenValue();
1206
1409
  }
1207
1410
  async storeAuthorizationCode(record) {
1208
1411
  this.codes.set(record.code, record);
@@ -1281,76 +1484,22 @@ var InMemoryAuthorizationStore = class {
1281
1484
  }
1282
1485
  }
1283
1486
  /**
1284
- * Create an authorization code record with defaults
1487
+ * Create an authorization code record with defaults.
1285
1488
  */
1286
1489
  createCodeRecord(params) {
1287
- const now = Date.now();
1288
- return {
1289
- code: this.generateCode(),
1290
- clientId: params.clientId,
1291
- redirectUri: params.redirectUri,
1292
- scopes: params.scopes,
1293
- pkce: params.pkce,
1294
- userSub: params.userSub,
1295
- userEmail: params.userEmail,
1296
- userName: params.userName,
1297
- state: params.state,
1298
- resource: params.resource,
1299
- createdAt: now,
1300
- expiresAt: now + this.codeTtlMs,
1301
- used: false,
1302
- // Consent and Federated Login Data
1303
- selectedToolIds: params.selectedToolIds,
1304
- selectedProviderIds: params.selectedProviderIds,
1305
- skippedProviderIds: params.skippedProviderIds,
1306
- consentEnabled: params.consentEnabled,
1307
- federatedLoginUsed: params.federatedLoginUsed,
1308
- // Token migration ID (for federated auth)
1309
- pendingAuthId: params.pendingAuthId
1310
- };
1490
+ return buildCodeRecord(params);
1311
1491
  }
1312
1492
  /**
1313
- * Create a pending authorization record with defaults
1493
+ * Create a pending authorization record with defaults.
1314
1494
  */
1315
1495
  createPendingRecord(params) {
1316
- const now = Date.now();
1317
- return {
1318
- id: randomUUID(),
1319
- clientId: params.clientId,
1320
- redirectUri: params.redirectUri,
1321
- scopes: params.scopes,
1322
- pkce: params.pkce,
1323
- state: params.state,
1324
- resource: params.resource,
1325
- createdAt: now,
1326
- expiresAt: now + this.pendingTtlMs,
1327
- // Progressive/Incremental Authorization Fields
1328
- isIncremental: params.isIncremental,
1329
- targetAppId: params.targetAppId,
1330
- targetToolId: params.targetToolId,
1331
- existingSessionId: params.existingSessionId,
1332
- existingAuthorizationId: params.existingAuthorizationId,
1333
- // Federated Login State
1334
- federatedLogin: params.federatedLogin,
1335
- // Consent State
1336
- consent: params.consent
1337
- };
1496
+ return buildPendingRecord(params);
1338
1497
  }
1339
1498
  /**
1340
- * Create a refresh token record with defaults
1499
+ * Create a refresh token record with defaults.
1341
1500
  */
1342
1501
  createRefreshTokenRecord(params) {
1343
- const now = Date.now();
1344
- return {
1345
- token: this.generateRefreshToken(),
1346
- clientId: params.clientId,
1347
- userSub: params.userSub,
1348
- scopes: params.scopes,
1349
- resource: params.resource,
1350
- createdAt: now,
1351
- expiresAt: now + this.refreshTtlMs,
1352
- revoked: false
1353
- };
1502
+ return buildRefreshTokenRecord(params);
1354
1503
  }
1355
1504
  };
1356
1505
  var RedisAuthorizationStore = class {
@@ -1362,10 +1511,19 @@ var RedisAuthorizationStore = class {
1362
1511
  return `${this.namespace}${type}:${id}`;
1363
1512
  }
1364
1513
  generateCode() {
1365
- return randomUUID().replace(/-/g, "") + randomUUID().replace(/-/g, "");
1514
+ return generateAuthorizationCode();
1366
1515
  }
1367
1516
  generateRefreshToken() {
1368
- return randomUUID() + "-" + randomUUID();
1517
+ return generateRefreshTokenValue();
1518
+ }
1519
+ createCodeRecord(params) {
1520
+ return buildCodeRecord(params);
1521
+ }
1522
+ createPendingRecord(params) {
1523
+ return buildPendingRecord(params);
1524
+ }
1525
+ createRefreshTokenRecord(params) {
1526
+ return buildRefreshTokenRecord(params);
1369
1527
  }
1370
1528
  async storeAuthorizationCode(record) {
1371
1529
  const ttl = Math.max(Math.ceil((record.expiresAt - Date.now()) / 1e3), 1);
@@ -1427,6 +1585,130 @@ var RedisAuthorizationStore = class {
1427
1585
  }
1428
1586
  };
1429
1587
 
1588
+ // libs/auth/src/session/storage-authorization.store.ts
1589
+ import { TypedStorage } from "@frontmcp/utils";
1590
+ var StorageAuthorizationStore = class {
1591
+ codeStore;
1592
+ pendingStore;
1593
+ refreshStore;
1594
+ namespace;
1595
+ storageIsNamespaced;
1596
+ constructor(storage, options = {}) {
1597
+ this.namespace = options.namespace ?? "oauth";
1598
+ this.storageIsNamespaced = this.isNamespacedStorage(storage);
1599
+ const base = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
1600
+ this.codeStore = new TypedStorage(base);
1601
+ this.pendingStore = new TypedStorage(base);
1602
+ this.refreshStore = new TypedStorage(base);
1603
+ }
1604
+ // ============================================
1605
+ // Record builders (pure; shared with in-memory store)
1606
+ // ============================================
1607
+ generateCode() {
1608
+ return generateAuthorizationCode();
1609
+ }
1610
+ generateRefreshToken() {
1611
+ return generateRefreshTokenValue();
1612
+ }
1613
+ createCodeRecord(params) {
1614
+ return buildCodeRecord(params);
1615
+ }
1616
+ createPendingRecord(params) {
1617
+ return buildPendingRecord(params);
1618
+ }
1619
+ createRefreshTokenRecord(params) {
1620
+ return buildRefreshTokenRecord(params);
1621
+ }
1622
+ // ============================================
1623
+ // Authorization code operations
1624
+ // ============================================
1625
+ async storeAuthorizationCode(record) {
1626
+ await this.codeStore.set(this.key("code", record.code), record, this.ttl(record.expiresAt));
1627
+ }
1628
+ async getAuthorizationCode(code) {
1629
+ const record = await this.codeStore.get(this.key("code", code));
1630
+ if (!record) return null;
1631
+ if (Date.now() > record.expiresAt) {
1632
+ await this.codeStore.delete(this.key("code", code));
1633
+ return null;
1634
+ }
1635
+ return record;
1636
+ }
1637
+ async markCodeUsed(code) {
1638
+ const record = await this.getAuthorizationCode(code);
1639
+ if (record) {
1640
+ record.used = true;
1641
+ await this.codeStore.set(this.key("code", code), record, this.ttl(record.expiresAt));
1642
+ }
1643
+ }
1644
+ async deleteAuthorizationCode(code) {
1645
+ await this.codeStore.delete(this.key("code", code));
1646
+ }
1647
+ // ============================================
1648
+ // Pending authorization operations
1649
+ // ============================================
1650
+ async storePendingAuthorization(record) {
1651
+ await this.pendingStore.set(this.key("pending", record.id), record, this.ttl(record.expiresAt));
1652
+ }
1653
+ async getPendingAuthorization(id) {
1654
+ const record = await this.pendingStore.get(this.key("pending", id));
1655
+ if (!record) return null;
1656
+ if (Date.now() > record.expiresAt) {
1657
+ await this.pendingStore.delete(this.key("pending", id));
1658
+ return null;
1659
+ }
1660
+ return record;
1661
+ }
1662
+ async deletePendingAuthorization(id) {
1663
+ await this.pendingStore.delete(this.key("pending", id));
1664
+ }
1665
+ // ============================================
1666
+ // Refresh token operations
1667
+ // ============================================
1668
+ async storeRefreshToken(record) {
1669
+ await this.refreshStore.set(this.key("refresh", record.token), record, this.ttl(record.expiresAt));
1670
+ }
1671
+ async getRefreshToken(token) {
1672
+ const record = await this.refreshStore.get(this.key("refresh", token));
1673
+ if (!record) return null;
1674
+ if (Date.now() > record.expiresAt || record.revoked) {
1675
+ return null;
1676
+ }
1677
+ return record;
1678
+ }
1679
+ async revokeRefreshToken(token) {
1680
+ const record = await this.refreshStore.get(this.key("refresh", token));
1681
+ if (record) {
1682
+ record.revoked = true;
1683
+ await this.refreshStore.set(this.key("refresh", token), record, this.ttl(record.expiresAt));
1684
+ }
1685
+ }
1686
+ async rotateRefreshToken(oldToken, newRecord) {
1687
+ await this.revokeRefreshToken(oldToken);
1688
+ newRecord.previousToken = oldToken;
1689
+ await this.storeRefreshToken(newRecord);
1690
+ }
1691
+ // ============================================
1692
+ // Utility
1693
+ // ============================================
1694
+ async cleanup() {
1695
+ }
1696
+ // ============================================
1697
+ // Internals
1698
+ // ============================================
1699
+ ttl(expiresAtMs) {
1700
+ const ttl = Math.ceil((expiresAtMs - Date.now()) / 1e3);
1701
+ return { ttlSeconds: ttl > 0 ? ttl : 1 };
1702
+ }
1703
+ key(type, id) {
1704
+ const composite = `${type}:${id}`;
1705
+ return this.storageIsNamespaced ? composite : `${this.namespace}:${composite}`;
1706
+ }
1707
+ isNamespacedStorage(storage) {
1708
+ return "namespace" in storage && typeof storage.namespace === "function";
1709
+ }
1710
+ };
1711
+
1430
1712
  // libs/auth/src/session/authorization-vault.ts
1431
1713
  import { z as z2 } from "@frontmcp/lazy-zod";
1432
1714
  var credentialTypeSchema = z2.enum([
@@ -1688,6 +1970,9 @@ var authorizationVaultEntrySchema = z2.object({
1688
1970
  skippedAppIds: z2.array(z2.string())
1689
1971
  });
1690
1972
 
1973
+ // libs/auth/src/session/session-credential-vault.ts
1974
+ import { getEnv, randomUUID as randomUUID2 } from "@frontmcp/utils";
1975
+
1691
1976
  // libs/auth/src/session/vault-encryption.ts
1692
1977
  import { z as z3 } from "@frontmcp/lazy-zod";
1693
1978
  import {
@@ -1871,6 +2156,639 @@ var encryptedVaultEntrySchema = z3.object({
1871
2156
  pendingAuthIds: z3.array(z3.string()).default([])
1872
2157
  });
1873
2158
 
2159
+ // libs/auth/src/session/session-credential-vault.ts
2160
+ var DEFAULT_KEY_PREFIX = "mcp:cred:";
2161
+ var DEFAULT_PENDING_TTL_MS = 6e5;
2162
+ var SessionCredentialVault = class {
2163
+ storage;
2164
+ encryption;
2165
+ keyPrefix;
2166
+ ttlMs;
2167
+ pendingTtlMs;
2168
+ logger;
2169
+ /** Cache of derived AES keys, keyed by `${sub}:${vaultId}`. */
2170
+ keyCache = /* @__PURE__ */ new Map();
2171
+ constructor(options) {
2172
+ this.storage = options.storage;
2173
+ this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;
2174
+ this.ttlMs = options.ttlMs;
2175
+ this.pendingTtlMs = options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
2176
+ this.logger = options.logger ?? noopAuthLogger;
2177
+ const pepper = options.pepper ?? resolveVaultPepper(this.logger);
2178
+ this.encryption = new VaultEncryption({ pepper, hkdfInfo: "frontmcp-session-credential-vault-v1" });
2179
+ }
2180
+ // ============================================
2181
+ // Storage-key helpers
2182
+ // ============================================
2183
+ /** `cred:idx:<sub>` → current vaultId (the sub → vaultId indirection). */
2184
+ indexKey(sub) {
2185
+ return `${this.keyPrefix}idx:${sub}`;
2186
+ }
2187
+ /** `cred:keys:<sub>:<vaultId>` → JSON array of credential key names (the SET). */
2188
+ keysKey(sub, vaultId) {
2189
+ return `${this.keyPrefix}keys:${sub}:${vaultId}`;
2190
+ }
2191
+ /** `cred:data:<sub>:<vaultId>:<key>` → encrypted credential record. */
2192
+ dataKey(sub, vaultId, key) {
2193
+ return `${this.keyPrefix}data:${sub}:${vaultId}:${key}`;
2194
+ }
2195
+ /** `cred:pending:<pendingId>` → encrypted pending-authorize accumulator. */
2196
+ pendingKey(pendingId) {
2197
+ return `${this.keyPrefix}pending:${pendingId}`;
2198
+ }
2199
+ /** Convert the configured `ttlMs` into `SetOptions` (storage uses `ttlSeconds`). */
2200
+ get credentialSetOpts() {
2201
+ return this.ttlMs ? { ttlSeconds: Math.ceil(this.ttlMs / 1e3) } : void 0;
2202
+ }
2203
+ // ============================================
2204
+ // Key derivation
2205
+ // ============================================
2206
+ /**
2207
+ * Derive (and cache) the AES-256 key for a `(sub, vaultId)` pair. The vaultId
2208
+ * acts as the HKDF salt, so rotating it makes prior ciphertext undecryptable.
2209
+ */
2210
+ deriveKey(sub, vaultId) {
2211
+ const cacheKey = `${sub}:${vaultId}`;
2212
+ const cached = this.keyCache.get(cacheKey);
2213
+ if (cached) return cached;
2214
+ const promise = this.encryption.deriveKey({ jti: vaultId, vaultKey: vaultId, sub, iat: 0 });
2215
+ this.keyCache.set(cacheKey, promise);
2216
+ return promise;
2217
+ }
2218
+ // ============================================
2219
+ // vaultId indirection
2220
+ // ============================================
2221
+ /**
2222
+ * Read the current vaultId for a subject, or undefined when the subject has no
2223
+ * live vault (never authorized, or rotated away).
2224
+ */
2225
+ async getVaultId(sub) {
2226
+ const raw = await this.storage.get(this.indexKey(sub));
2227
+ return raw ?? void 0;
2228
+ }
2229
+ /**
2230
+ * Mint a FRESH vaultId for a subject and point the indirection at it. Called
2231
+ * at the start of each authorize so reconnect = empty vault. Returns the new
2232
+ * vaultId. The old vaultId's ciphertext is left in storage but is
2233
+ * unreachable (no index pointer) and undecryptable (different derived key);
2234
+ * it expires via TTL when configured.
2235
+ */
2236
+ async rotateVault(sub) {
2237
+ const vaultId = randomUUID2();
2238
+ await this.storage.set(this.indexKey(sub), vaultId, this.credentialSetOpts);
2239
+ return vaultId;
2240
+ }
2241
+ // ============================================
2242
+ // Credential CRUD (committed vault)
2243
+ // ============================================
2244
+ /**
2245
+ * Encrypt + store a single credential into the vault identified by
2246
+ * `(sub, vaultId)` and add its key to the key set. Used both to commit
2247
+ * authorize-time credentials and to additively add a credential mid-session.
2248
+ */
2249
+ async store(sub, vaultId, key, value) {
2250
+ const aesKey = await this.deriveKey(sub, vaultId);
2251
+ const enc = await this.encryption.encryptObject(this.normalize(value), aesKey);
2252
+ const record = { enc };
2253
+ await this.storage.set(this.dataKey(sub, vaultId, key), JSON.stringify(record), this.credentialSetOpts);
2254
+ await this.addKeyToSet(sub, vaultId, key);
2255
+ }
2256
+ /**
2257
+ * Decrypt and return a credential by key for a subject's CURRENT vault.
2258
+ * Returns undefined when there is no live vault or no such key, or when
2259
+ * decryption fails (e.g. a stale pointer to rotated-away ciphertext).
2260
+ */
2261
+ async get(sub, key) {
2262
+ const vaultId = await this.getVaultId(sub);
2263
+ if (!vaultId) return void 0;
2264
+ const raw = await this.storage.get(this.dataKey(sub, vaultId, key));
2265
+ if (!raw) return void 0;
2266
+ try {
2267
+ const record = JSON.parse(raw);
2268
+ const aesKey = await this.deriveKey(sub, vaultId);
2269
+ return await this.encryption.decryptObject(record.enc, aesKey);
2270
+ } catch (err) {
2271
+ this.logger.warn(`SessionCredentialVault: failed to read credential "${key}": ${errMsg(err)}`);
2272
+ return void 0;
2273
+ }
2274
+ }
2275
+ /**
2276
+ * List the credential keys available in a subject's current vault. Returns an
2277
+ * empty array when there is no live vault (e.g. after rotation on reconnect).
2278
+ */
2279
+ async list(sub) {
2280
+ const vaultId = await this.getVaultId(sub);
2281
+ if (!vaultId) return [];
2282
+ return this.readKeySet(sub, vaultId);
2283
+ }
2284
+ /**
2285
+ * Remove a credential by key from a subject's current vault (deletes the
2286
+ * ciphertext and drops the key from the set). No-op when absent.
2287
+ */
2288
+ async remove(sub, key) {
2289
+ const vaultId = await this.getVaultId(sub);
2290
+ if (!vaultId) return;
2291
+ await this.storage.delete(this.dataKey(sub, vaultId, key));
2292
+ const keys = await this.readKeySet(sub, vaultId);
2293
+ const next = keys.filter((k) => k !== key);
2294
+ if (next.length !== keys.length) {
2295
+ await this.writeKeySet(sub, vaultId, next);
2296
+ }
2297
+ }
2298
+ // ============================================
2299
+ // Authorize-time accumulation + commit
2300
+ // ============================================
2301
+ /**
2302
+ * Stage credentials returned by `authenticate()` under a pending id, encrypted
2303
+ * against a freshly-rotated vaultId for `sub`. They are NOT visible to tools
2304
+ * until {@link commit} flushes them — this lets the authorize flow accumulate
2305
+ * credentials before the subject's identity is final.
2306
+ *
2307
+ * @returns the vaultId the credentials were staged against.
2308
+ */
2309
+ async stage(pendingId, sub, credentials) {
2310
+ const vaultId = await this.rotateVault(sub);
2311
+ const creds = {};
2312
+ for (const cred of credentials) {
2313
+ const aesKey = await this.deriveKey(sub, vaultId);
2314
+ const { key, ...value } = cred;
2315
+ creds[key] = { enc: await this.encryption.encryptObject(this.normalize(value), aesKey) };
2316
+ }
2317
+ const batch = { sub, vaultId, creds };
2318
+ await this.storage.set(this.pendingKey(pendingId), JSON.stringify(batch), {
2319
+ ttlSeconds: Math.ceil(this.pendingTtlMs / 1e3)
2320
+ });
2321
+ return vaultId;
2322
+ }
2323
+ /**
2324
+ * Flush a staged batch into the live vault, binding it to `sub`. The staged
2325
+ * batch's `sub` must equal the supplied `sub` (the one baked into the minted
2326
+ * token) — a mismatch is refused so credentials can never be committed under a
2327
+ * different identity than the one that will read them.
2328
+ *
2329
+ * @returns true when a batch was committed, false when none was staged.
2330
+ */
2331
+ async commit(pendingId, sub) {
2332
+ const raw = await this.storage.get(this.pendingKey(pendingId));
2333
+ if (!raw) return false;
2334
+ let batch;
2335
+ try {
2336
+ batch = JSON.parse(raw);
2337
+ } catch {
2338
+ await this.storage.delete(this.pendingKey(pendingId));
2339
+ return false;
2340
+ }
2341
+ if (batch.sub !== sub) {
2342
+ this.logger.warn("SessionCredentialVault: pending batch subject mismatch on commit; discarding");
2343
+ await this.storage.delete(this.pendingKey(pendingId));
2344
+ return false;
2345
+ }
2346
+ await this.storage.set(this.indexKey(sub), batch.vaultId, this.credentialSetOpts);
2347
+ const keys = [];
2348
+ for (const [key, record] of Object.entries(batch.creds)) {
2349
+ await this.storage.set(this.dataKey(sub, batch.vaultId, key), JSON.stringify(record), this.credentialSetOpts);
2350
+ keys.push(key);
2351
+ }
2352
+ await this.writeKeySet(sub, batch.vaultId, keys);
2353
+ await this.storage.delete(this.pendingKey(pendingId));
2354
+ return true;
2355
+ }
2356
+ // ============================================
2357
+ // Key-set helpers
2358
+ // ============================================
2359
+ async readKeySet(sub, vaultId) {
2360
+ const raw = await this.storage.get(this.keysKey(sub, vaultId));
2361
+ if (!raw) return [];
2362
+ try {
2363
+ const parsed = JSON.parse(raw);
2364
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
2365
+ } catch {
2366
+ return [];
2367
+ }
2368
+ }
2369
+ async writeKeySet(sub, vaultId, keys) {
2370
+ const unique = [...new Set(keys)];
2371
+ await this.storage.set(this.keysKey(sub, vaultId), JSON.stringify(unique), this.credentialSetOpts);
2372
+ }
2373
+ async addKeyToSet(sub, vaultId, key) {
2374
+ const keys = await this.readKeySet(sub, vaultId);
2375
+ if (!keys.includes(key)) {
2376
+ keys.push(key);
2377
+ await this.writeKeySet(sub, vaultId, keys);
2378
+ }
2379
+ }
2380
+ /** Strip undefined metadata so the encrypted JSON stays minimal. */
2381
+ normalize(value) {
2382
+ return value.metadata === void 0 ? { secret: value.secret } : { secret: value.secret, metadata: value.metadata };
2383
+ }
2384
+ };
2385
+ function resolveVaultPepper(logger) {
2386
+ const fromEnv = getEnv("VAULT_SECRET") ?? getEnv("JWT_SECRET");
2387
+ if (fromEnv && fromEnv.trim().length > 0) return fromEnv;
2388
+ logger.warn(
2389
+ "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."
2390
+ );
2391
+ return randomUUID2() + randomUUID2();
2392
+ }
2393
+ function errMsg(err) {
2394
+ return err instanceof Error ? err.message : String(err);
2395
+ }
2396
+
2397
+ // libs/auth/src/session/token-storage.factory.ts
2398
+ import { MemoryStorageAdapter, RedisStorageAdapter } from "@frontmcp/utils";
2399
+ function isRedisTokenStorage(config) {
2400
+ return typeof config === "object" && config !== null && "redis" in config && !!config.redis;
2401
+ }
2402
+ function isSqliteTokenStorage(config) {
2403
+ return typeof config === "object" && config !== null && "sqlite" in config && !!config.sqlite;
2404
+ }
2405
+ function isPersistentTokenStorage(config) {
2406
+ return config !== void 0 && config !== "memory";
2407
+ }
2408
+ async function createTokenStorageAdapter(config) {
2409
+ if (config === void 0 || config === "memory") {
2410
+ const adapter = new MemoryStorageAdapter();
2411
+ await adapter.connect();
2412
+ return adapter;
2413
+ }
2414
+ if (isSqliteTokenStorage(config)) {
2415
+ const { SqliteStorageAdapter } = __require("@frontmcp/storage-sqlite");
2416
+ const adapter = new SqliteStorageAdapter({
2417
+ path: config.sqlite.path,
2418
+ encryption: config.sqlite.encryption,
2419
+ ttlCleanupIntervalMs: config.sqlite.ttlCleanupIntervalMs ?? 6e4,
2420
+ walMode: config.sqlite.walMode ?? true
2421
+ });
2422
+ await adapter.connect();
2423
+ return adapter;
2424
+ }
2425
+ if (isRedisTokenStorage(config)) {
2426
+ const { host, port, password, db, tls, keyPrefix } = config.redis;
2427
+ const adapter = new RedisStorageAdapter({
2428
+ config: { host, port, password, db, tls },
2429
+ keyPrefix: keyPrefix ?? ""
2430
+ });
2431
+ await adapter.connect();
2432
+ return adapter;
2433
+ }
2434
+ const fallback = new MemoryStorageAdapter();
2435
+ await fallback.connect();
2436
+ return fallback;
2437
+ }
2438
+
2439
+ // libs/auth/src/session/session-credential-vault.factory.ts
2440
+ async function createSessionCredentialVault(options) {
2441
+ const storage = options.storage ?? await createTokenStorageAdapter(options.tokenStorage);
2442
+ const vault = new SessionCredentialVault({
2443
+ storage,
2444
+ pepper: options.pepper,
2445
+ keyPrefix: options.keyPrefix,
2446
+ ttlMs: options.ttlMs,
2447
+ logger: options.logger
2448
+ });
2449
+ const type = options.tokenStorage === void 0 || options.tokenStorage === "memory" ? "memory" : "sqlite" in options.tokenStorage ? "sqlite" : "redis";
2450
+ return { vault, type };
2451
+ }
2452
+
2453
+ // libs/auth/src/session/credential-resume-link.ts
2454
+ import { signData, verifyData } from "@frontmcp/utils";
2455
+ var DEFAULT_RESUME_TTL_MS = 6e5;
2456
+ function signCredentialResumeToken(input, secret) {
2457
+ const payload = {
2458
+ sub: input.sub,
2459
+ key: input.key,
2460
+ ...input.context !== void 0 ? { context: input.context } : {},
2461
+ exp: Date.now() + (input.ttlMs ?? DEFAULT_RESUME_TTL_MS)
2462
+ };
2463
+ const signedJson = signData(payload, { secret });
2464
+ return Buffer.from(signedJson, "utf8").toString("base64url");
2465
+ }
2466
+ function verifyCredentialResumeToken(token, secret, now = Date.now()) {
2467
+ let signedJson;
2468
+ try {
2469
+ signedJson = Buffer.from(token, "base64url").toString("utf8");
2470
+ } catch {
2471
+ return null;
2472
+ }
2473
+ const payload = verifyData(signedJson, { secret });
2474
+ if (!payload) return null;
2475
+ if (typeof payload.sub !== "string" || typeof payload.key !== "string" || !Number.isFinite(payload.exp)) {
2476
+ return null;
2477
+ }
2478
+ if (payload.context !== void 0 && typeof payload.context !== "string") {
2479
+ return null;
2480
+ }
2481
+ if (now >= payload.exp) {
2482
+ return null;
2483
+ }
2484
+ return payload;
2485
+ }
2486
+ function buildCredentialResumeUrl(basePath, token) {
2487
+ const trimmed = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
2488
+ return `${trimmed}/oauth/connect?token=${encodeURIComponent(token)}`;
2489
+ }
2490
+
2491
+ // libs/auth/src/session/credentials-accessor.ts
2492
+ var CREDENTIALS_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:CREDENTIALS_ACCESSOR");
2493
+
2494
+ // libs/auth/src/session/credentials-accessor.impl.ts
2495
+ var CredentialsAccessorImpl = class {
2496
+ vault;
2497
+ resolveSub;
2498
+ signingSecret;
2499
+ basePath;
2500
+ resumeTtlMs;
2501
+ logger;
2502
+ constructor(deps) {
2503
+ this.vault = deps.vault;
2504
+ this.resolveSub = deps.resolveSub;
2505
+ this.signingSecret = deps.signingSecret;
2506
+ this.basePath = deps.basePath;
2507
+ this.resumeTtlMs = deps.resumeTtlMs;
2508
+ this.logger = deps.logger ?? noopAuthLogger;
2509
+ }
2510
+ async get(key) {
2511
+ const sub = this.resolveSub();
2512
+ if (!sub) return void 0;
2513
+ return this.vault.get(sub, key);
2514
+ }
2515
+ async list() {
2516
+ const sub = this.resolveSub();
2517
+ if (!sub) return [];
2518
+ return this.vault.list(sub);
2519
+ }
2520
+ async requireConnect(options) {
2521
+ const { key, context } = options;
2522
+ const sub = this.resolveSub();
2523
+ if (sub) {
2524
+ const existing = await this.vault.get(sub, key);
2525
+ if (existing) {
2526
+ return { connected: true, key, credential: existing };
2527
+ }
2528
+ }
2529
+ const token = signCredentialResumeToken(
2530
+ { sub: sub ?? "", key, context, ttlMs: this.resumeTtlMs },
2531
+ this.signingSecret
2532
+ );
2533
+ const resumeUrl = buildCredentialResumeUrl(this.basePath, token);
2534
+ this.logger.debug(`CredentialsAccessor: credential "${key}" not connected; issued resume link`);
2535
+ return {
2536
+ connected: false,
2537
+ key,
2538
+ resumeUrl,
2539
+ message: `Credential "${key}" is not connected. Open the resume URL to connect it.`
2540
+ };
2541
+ }
2542
+ };
2543
+
2544
+ // libs/auth/src/session/secure-store-backends.ts
2545
+ var DEFAULT_KEY_PREFIX2 = "mcp:secret:";
2546
+ var EncryptedStorageSecureStoreBackend = class {
2547
+ storage;
2548
+ encryption;
2549
+ keyPrefix;
2550
+ logger;
2551
+ /** Cache of derived AES keys, keyed by namespace. */
2552
+ keyCache = /* @__PURE__ */ new Map();
2553
+ constructor(options) {
2554
+ this.storage = options.storage;
2555
+ this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX2;
2556
+ this.logger = options.logger ?? noopAuthLogger;
2557
+ this.encryption = new VaultEncryption({
2558
+ pepper: options.pepper,
2559
+ hkdfInfo: "frontmcp-secure-store-v1"
2560
+ });
2561
+ }
2562
+ // ============================================
2563
+ // Storage-key helpers
2564
+ // ============================================
2565
+ /** `secret:keys:<namespace>` → JSON array of key names. */
2566
+ keysKey(namespace) {
2567
+ return `${this.keyPrefix}keys:${namespace}`;
2568
+ }
2569
+ /** `secret:data:<namespace>:<key>` → encrypted record. */
2570
+ dataKey(namespace, key) {
2571
+ return `${this.keyPrefix}data:${namespace}:${key}`;
2572
+ }
2573
+ /**
2574
+ * Derive (and cache) the AES-256 key for a namespace. The namespace acts as
2575
+ * the HKDF salt material, so values are cryptographically isolated per scope.
2576
+ */
2577
+ deriveKey(namespace) {
2578
+ const cached = this.keyCache.get(namespace);
2579
+ if (cached) return cached;
2580
+ const promise = this.encryption.deriveKey({ jti: namespace, vaultKey: namespace, sub: namespace, iat: 0 });
2581
+ this.keyCache.set(namespace, promise);
2582
+ return promise;
2583
+ }
2584
+ setOpts(ttlMs) {
2585
+ return ttlMs && ttlMs > 0 ? { ttlSeconds: Math.ceil(ttlMs / 1e3) } : void 0;
2586
+ }
2587
+ // ============================================
2588
+ // SecureStoreBackend
2589
+ // ============================================
2590
+ async get(namespace, key) {
2591
+ const raw = await this.storage.get(this.dataKey(namespace, key));
2592
+ if (!raw) return null;
2593
+ try {
2594
+ const record = JSON.parse(raw);
2595
+ const aesKey = await this.deriveKey(namespace);
2596
+ return await this.encryption.decrypt(record.enc, aesKey);
2597
+ } catch (err) {
2598
+ this.logger.warn(`SecureStore: failed to read secret "${key}": ${errMsg2(err)}`);
2599
+ return null;
2600
+ }
2601
+ }
2602
+ async set(namespace, key, value, ttlMs) {
2603
+ const aesKey = await this.deriveKey(namespace);
2604
+ const enc = await this.encryption.encrypt(value, aesKey);
2605
+ const record = { enc };
2606
+ const opts = this.setOpts(ttlMs);
2607
+ await this.storage.set(this.dataKey(namespace, key), JSON.stringify(record), opts);
2608
+ await this.addKeyToSet(namespace, key, opts);
2609
+ }
2610
+ async delete(namespace, key) {
2611
+ const existed = await this.storage.delete(this.dataKey(namespace, key));
2612
+ const keys = await this.readKeySet(namespace);
2613
+ const next = keys.filter((k) => k !== key);
2614
+ if (next.length !== keys.length) {
2615
+ await this.writeKeySet(namespace, next);
2616
+ }
2617
+ return existed;
2618
+ }
2619
+ async list(namespace) {
2620
+ return this.readKeySet(namespace);
2621
+ }
2622
+ // ============================================
2623
+ // Key-set helpers (the namespace's key SET)
2624
+ // ============================================
2625
+ async readKeySet(namespace) {
2626
+ const raw = await this.storage.get(this.keysKey(namespace));
2627
+ if (!raw) return [];
2628
+ try {
2629
+ const parsed = JSON.parse(raw);
2630
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
2631
+ } catch {
2632
+ return [];
2633
+ }
2634
+ }
2635
+ async writeKeySet(namespace, keys, opts) {
2636
+ const unique = [...new Set(keys)];
2637
+ await this.storage.set(this.keysKey(namespace), JSON.stringify(unique), opts);
2638
+ }
2639
+ async addKeyToSet(namespace, key, opts) {
2640
+ const keys = await this.readKeySet(namespace);
2641
+ if (!keys.includes(key)) {
2642
+ keys.push(key);
2643
+ await this.writeKeySet(namespace, keys, opts);
2644
+ }
2645
+ }
2646
+ };
2647
+ function errMsg2(err) {
2648
+ return err instanceof Error ? err.message : String(err);
2649
+ }
2650
+
2651
+ // libs/auth/src/session/secure-store-accessor.ts
2652
+ var SECURE_STORE_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:SECURE_STORE_ACCESSOR");
2653
+
2654
+ // libs/auth/src/session/secure-store-accessor.impl.ts
2655
+ import { sha256Hex } from "@frontmcp/utils";
2656
+ var GLOBAL_NAMESPACE = "global";
2657
+ var SecureStoreAccessorImpl = class {
2658
+ backend;
2659
+ scope;
2660
+ resolveSub;
2661
+ resolveSessionId;
2662
+ ttlMs;
2663
+ logger;
2664
+ constructor(deps) {
2665
+ this.backend = deps.backend;
2666
+ this.scope = deps.scope ?? "user";
2667
+ this.resolveSub = deps.resolveSub;
2668
+ this.resolveSessionId = deps.resolveSessionId;
2669
+ this.ttlMs = deps.ttlMs;
2670
+ this.logger = deps.logger ?? noopAuthLogger;
2671
+ }
2672
+ /**
2673
+ * Resolve the namespace for the current request from the configured scope.
2674
+ * Returns undefined when the scope's identity is unavailable (anonymous user
2675
+ * under `user` scope, or no session under `session` scope), in which case
2676
+ * reads return empty and writes are skipped.
2677
+ */
2678
+ resolveNamespace() {
2679
+ switch (this.scope) {
2680
+ case "global":
2681
+ return GLOBAL_NAMESPACE;
2682
+ case "session": {
2683
+ const sid = this.resolveSessionId();
2684
+ return sid ? `s:${sha256Hex(sid).slice(0, 32)}` : void 0;
2685
+ }
2686
+ case "user":
2687
+ default: {
2688
+ const sub = this.resolveSub();
2689
+ return sub ? `u:${sha256Hex(sub).slice(0, 32)}` : void 0;
2690
+ }
2691
+ }
2692
+ }
2693
+ async get(key) {
2694
+ const namespace = this.resolveNamespace();
2695
+ if (!namespace) return void 0;
2696
+ const raw = await this.backend.get(namespace, key);
2697
+ if (raw === null) return void 0;
2698
+ try {
2699
+ return JSON.parse(raw);
2700
+ } catch (err) {
2701
+ this.logger.warn(`SecureStore: failed to parse secret "${key}": ${errMsg3(err)}`);
2702
+ return void 0;
2703
+ }
2704
+ }
2705
+ async set(key, value, options) {
2706
+ const namespace = this.resolveNamespace();
2707
+ if (!namespace) {
2708
+ this.logger.debug(`SecureStore: no ${this.scope} scope for request; skipping set("${key}")`);
2709
+ return;
2710
+ }
2711
+ const ttlMs = options?.ttlMs ?? this.ttlMs;
2712
+ await this.backend.set(namespace, key, JSON.stringify(value), ttlMs);
2713
+ }
2714
+ async delete(key) {
2715
+ const namespace = this.resolveNamespace();
2716
+ if (!namespace) return false;
2717
+ return this.backend.delete(namespace, key);
2718
+ }
2719
+ async list() {
2720
+ const namespace = this.resolveNamespace();
2721
+ if (!namespace) return [];
2722
+ return this.backend.list(namespace);
2723
+ }
2724
+ };
2725
+ function errMsg3(err) {
2726
+ return err instanceof Error ? err.message : String(err);
2727
+ }
2728
+
2729
+ // libs/auth/src/session/secure-store.factory.ts
2730
+ import { MemoryStorageAdapter as MemoryStorageAdapter2 } from "@frontmcp/utils";
2731
+ function isCustomBackendConfig(config) {
2732
+ return typeof config === "object" && config !== null && "backend" in config && !!config.backend;
2733
+ }
2734
+ function isSqliteConfig(config) {
2735
+ return typeof config === "object" && config !== null && "sqlite" in config && !!config.sqlite;
2736
+ }
2737
+ function isRedisConfig(config) {
2738
+ return typeof config === "object" && config !== null && "redis" in config && !!config.redis;
2739
+ }
2740
+ function readScope(config) {
2741
+ if (typeof config === "object" && config !== null && "scope" in config && config.scope) {
2742
+ return config.scope;
2743
+ }
2744
+ return "user";
2745
+ }
2746
+ function readTtlMs(config) {
2747
+ if (typeof config === "object" && config !== null && "ttlMs" in config) {
2748
+ return config.ttlMs;
2749
+ }
2750
+ return void 0;
2751
+ }
2752
+ function readPepper(config) {
2753
+ if (typeof config === "object" && config !== null && "encryption" in config && config.encryption) {
2754
+ return config.encryption.pepper;
2755
+ }
2756
+ return void 0;
2757
+ }
2758
+ function toTokenStorageConfig(config) {
2759
+ if (isSqliteConfig(config)) {
2760
+ return { sqlite: config.sqlite };
2761
+ }
2762
+ if (isRedisConfig(config)) {
2763
+ return { redis: config.redis };
2764
+ }
2765
+ return "memory";
2766
+ }
2767
+ async function createSecureStore(options) {
2768
+ const { config, pepper, storage, logger } = options;
2769
+ const scope = readScope(config);
2770
+ const ttlMs = readTtlMs(config);
2771
+ if (isCustomBackendConfig(config)) {
2772
+ return { backend: config.backend, scope, ttlMs, kind: "custom" };
2773
+ }
2774
+ const kind = isSqliteConfig(config) ? "sqlite" : isRedisConfig(config) ? "redis" : "memory";
2775
+ let adapter;
2776
+ if (storage) {
2777
+ adapter = storage;
2778
+ } else if (kind === "memory") {
2779
+ adapter = new MemoryStorageAdapter2();
2780
+ await adapter.connect();
2781
+ } else {
2782
+ adapter = await createTokenStorageAdapter(toTokenStorageConfig(config));
2783
+ }
2784
+ const backend = new EncryptedStorageSecureStoreBackend({
2785
+ storage: adapter,
2786
+ pepper: readPepper(config) ?? pepper,
2787
+ logger
2788
+ });
2789
+ return { backend, scope, ttlMs, kind };
2790
+ }
2791
+
1874
2792
  // libs/auth/src/session/token.vault.ts
1875
2793
  import { encryptAesGcm as encryptAesGcm2, decryptAesGcm as decryptAesGcm2, randomBytes as randomBytes3, base64urlEncode as base64urlEncode2, base64urlDecode as base64urlDecode2 } from "@frontmcp/utils";
1876
2794
  var TokenVault = class {
@@ -1932,11 +2850,11 @@ var TokenVault = class {
1932
2850
 
1933
2851
  // libs/auth/src/session/index.ts
1934
2852
  import {
1935
- hkdfSha256 as hkdfSha2563,
2853
+ hkdfSha256 as hkdfSha2564,
1936
2854
  encryptValue as encryptValue2,
1937
2855
  decryptValue as decryptValue2,
1938
- encryptAesGcm as encryptAesGcm4,
1939
- decryptAesGcm as decryptAesGcm4
2856
+ encryptAesGcm as encryptAesGcm5,
2857
+ decryptAesGcm as decryptAesGcm5
1940
2858
  } from "@frontmcp/utils";
1941
2859
 
1942
2860
  // libs/auth/src/session/transport-session.types.ts
@@ -2034,11 +2952,11 @@ var redisConfigSchema = z4.object({
2034
2952
 
2035
2953
  // libs/auth/src/session/session-crypto.ts
2036
2954
  import {
2037
- signData,
2038
- verifyData,
2955
+ signData as signData2,
2956
+ verifyData as verifyData2,
2039
2957
  isSignedData,
2040
2958
  verifyOrParseData,
2041
- getEnv,
2959
+ getEnv as getEnv2,
2042
2960
  isProduction as isProduction2
2043
2961
  } from "@frontmcp/utils";
2044
2962
 
@@ -2188,7 +3106,7 @@ var AuthFlowError = class extends AuthInternalError {
2188
3106
 
2189
3107
  // libs/auth/src/session/session-crypto.ts
2190
3108
  function getSigningSecret(config) {
2191
- const secret = config?.secret || getEnv("MCP_SESSION_SECRET");
3109
+ const secret = config?.secret || getEnv2("MCP_SESSION_SECRET");
2192
3110
  if (!secret) {
2193
3111
  if (isProduction2()) {
2194
3112
  throw new SessionSecretRequiredError("session signing");
@@ -2202,10 +3120,10 @@ function resolveConfig(config) {
2202
3120
  return { secret: getSigningSecret(config) };
2203
3121
  }
2204
3122
  function signSession(session, config) {
2205
- return signData(session, resolveConfig(config));
3123
+ return signData2(session, resolveConfig(config));
2206
3124
  }
2207
3125
  function verifySession(signedData, config) {
2208
- return verifyData(signedData, resolveConfig(config));
3126
+ return verifyData2(signedData, resolveConfig(config));
2209
3127
  }
2210
3128
  function verifyOrParseSession(data, config) {
2211
3129
  return verifyOrParseData(data, resolveConfig(config));
@@ -2327,7 +3245,7 @@ var SessionRateLimiter = class {
2327
3245
  var defaultSessionRateLimiter = new SessionRateLimiter();
2328
3246
 
2329
3247
  // libs/auth/src/session/session.transport.ts
2330
- import { randomUUID as randomUUID2 } from "@frontmcp/utils";
3248
+ import { randomUUID as randomUUID3 } from "@frontmcp/utils";
2331
3249
  var TransportIdGenerator = class {
2332
3250
  /**
2333
3251
  * Create a transport session ID.
@@ -2336,7 +3254,7 @@ var TransportIdGenerator = class {
2336
3254
  * @returns A transport session ID (UUID without dashes)
2337
3255
  */
2338
3256
  static createId() {
2339
- return randomUUID2().replace(/-/g, "");
3257
+ return randomUUID3().replace(/-/g, "");
2340
3258
  }
2341
3259
  };
2342
3260
 
@@ -2426,11 +3344,11 @@ function extractBearerToken(header) {
2426
3344
  }
2427
3345
 
2428
3346
  // libs/auth/src/session/utils/session-crypto.utils.ts
2429
- import { decryptValue, encryptValue, getEnv as getEnv2, getMachineId, isProduction as isProduction3, sha256 } from "@frontmcp/utils";
3347
+ import { decryptValue, encryptValue, getEnv as getEnv3, getMachineId, isProduction as isProduction3, sha256 } from "@frontmcp/utils";
2430
3348
  var cachedKey = null;
2431
3349
  function getKey() {
2432
3350
  if (cachedKey) return cachedKey;
2433
- const secret = getEnv2("MCP_SESSION_SECRET");
3351
+ const secret = getEnv3("MCP_SESSION_SECRET");
2434
3352
  if (!secret) {
2435
3353
  if (isProduction3()) {
2436
3354
  throw new SessionSecretRequiredError("session ID encryption");
@@ -2468,11 +3386,11 @@ function resetCachedKey() {
2468
3386
  }
2469
3387
 
2470
3388
  // libs/auth/src/session/storage/index.ts
2471
- import { TypedStorage as TypedStorage3 } from "@frontmcp/utils";
3389
+ import { TypedStorage as TypedStorage4 } from "@frontmcp/utils";
2472
3390
  import { EncryptedTypedStorage, EncryptedStorageError } from "@frontmcp/utils";
2473
3391
 
2474
3392
  // libs/auth/src/session/storage/storage-token-store.ts
2475
- import { randomUUID as randomUUID3, TypedStorage } from "@frontmcp/utils";
3393
+ import { randomUUID as randomUUID4, TypedStorage as TypedStorage2 } from "@frontmcp/utils";
2476
3394
  var StorageTokenStore = class {
2477
3395
  storage;
2478
3396
  namespace;
@@ -2484,13 +3402,13 @@ var StorageTokenStore = class {
2484
3402
  this.defaultTtlSeconds = options.defaultTtlSeconds;
2485
3403
  this.storageIsNamespaced = "namespace" in storage && typeof storage.namespace === "function";
2486
3404
  const namespacedStorage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
2487
- this.storage = new TypedStorage(namespacedStorage);
3405
+ this.storage = new TypedStorage2(namespacedStorage);
2488
3406
  }
2489
3407
  /**
2490
3408
  * Allocate a new unique ID for a token record.
2491
3409
  */
2492
3410
  allocId() {
2493
- return randomUUID3();
3411
+ return randomUUID4();
2494
3412
  }
2495
3413
  /**
2496
3414
  * Store an encrypted token blob.
@@ -2553,7 +3471,7 @@ var StorageTokenStore = class {
2553
3471
  };
2554
3472
 
2555
3473
  // libs/auth/src/session/storage/storage-authorization-vault.ts
2556
- import { randomUUID as randomUUID4, TypedStorage as TypedStorage2 } from "@frontmcp/utils";
3474
+ import { randomUUID as randomUUID5, TypedStorage as TypedStorage3 } from "@frontmcp/utils";
2557
3475
  var StorageAuthorizationVault = class {
2558
3476
  storage;
2559
3477
  namespace;
@@ -2562,7 +3480,7 @@ var StorageAuthorizationVault = class {
2562
3480
  this.namespace = options.namespace ?? "vault";
2563
3481
  this.pendingAuthTtlMs = options.pendingAuthTtlMs ?? 10 * 60 * 1e3;
2564
3482
  const namespacedStorage = this.isNamespacedStorage(storage) ? storage.namespace(this.namespace) : storage;
2565
- this.storage = new TypedStorage2(namespacedStorage, {
3483
+ this.storage = new TypedStorage3(namespacedStorage, {
2566
3484
  schema: options.validateOnRead ? authorizationVaultEntrySchema : void 0,
2567
3485
  throwOnInvalid: false
2568
3486
  });
@@ -2573,7 +3491,7 @@ var StorageAuthorizationVault = class {
2573
3491
  async create(params) {
2574
3492
  const now = Date.now();
2575
3493
  const entry = {
2576
- id: randomUUID4(),
3494
+ id: randomUUID5(),
2577
3495
  userSub: params.userSub,
2578
3496
  userEmail: params.userEmail,
2579
3497
  userName: params.userName,
@@ -2647,7 +3565,7 @@ var StorageAuthorizationVault = class {
2647
3565
  }
2648
3566
  const now = Date.now();
2649
3567
  const pendingAuth = {
2650
- id: randomUUID4(),
3568
+ id: randomUUID5(),
2651
3569
  appId: params.appId,
2652
3570
  toolId: params.toolId,
2653
3571
  authUrl: params.authUrl,
@@ -2851,11 +3769,11 @@ var StorageAuthorizationVault = class {
2851
3769
  };
2852
3770
 
2853
3771
  // libs/auth/src/session/storage/in-memory-authorization-vault.ts
2854
- import { MemoryStorageAdapter } from "@frontmcp/utils";
3772
+ import { MemoryStorageAdapter as MemoryStorageAdapter3 } from "@frontmcp/utils";
2855
3773
  var InMemoryAuthorizationVault = class extends StorageAuthorizationVault {
2856
3774
  memoryAdapter;
2857
3775
  constructor(options = {}) {
2858
- const memoryAdapter = new MemoryStorageAdapter();
3776
+ const memoryAdapter = new MemoryStorageAdapter3();
2859
3777
  super(memoryAdapter, {
2860
3778
  namespace: options.namespace ?? "vault",
2861
3779
  pendingAuthTtlMs: options.pendingAuthTtlMs
@@ -2876,7 +3794,7 @@ var InMemoryAuthorizationVault = class extends StorageAuthorizationVault {
2876
3794
  };
2877
3795
 
2878
3796
  // libs/auth/src/session/redis-session.store.ts
2879
- import { randomUUID as randomUUID5, RedisStorageAdapter } from "@frontmcp/utils";
3797
+ import { randomUUID as randomUUID6, RedisStorageAdapter as RedisStorageAdapter2 } from "@frontmcp/utils";
2880
3798
  var RedisSessionStore = class {
2881
3799
  storage;
2882
3800
  keyPrefix;
@@ -2898,14 +3816,14 @@ var RedisSessionStore = class {
2898
3816
  });
2899
3817
  }
2900
3818
  if ("redis" in config && config.redis) {
2901
- this.storage = new RedisStorageAdapter({
3819
+ this.storage = new RedisStorageAdapter2({
2902
3820
  client: config.redis,
2903
3821
  keyPrefix: this.keyPrefix
2904
3822
  });
2905
3823
  this.externalInstance = true;
2906
3824
  } else {
2907
3825
  const redisConfig = config;
2908
- this.storage = new RedisStorageAdapter({
3826
+ this.storage = new RedisStorageAdapter2({
2909
3827
  config: {
2910
3828
  host: redisConfig.host,
2911
3829
  port: redisConfig.port ?? 6379,
@@ -3087,7 +4005,7 @@ var RedisSessionStore = class {
3087
4005
  * Allocate a new session ID
3088
4006
  */
3089
4007
  allocId() {
3090
- return randomUUID5();
4008
+ return randomUUID6();
3091
4009
  }
3092
4010
  /**
3093
4011
  * Disconnect from Redis (only if we created the connection)
@@ -3123,7 +4041,7 @@ var RedisSessionStore = class {
3123
4041
  };
3124
4042
 
3125
4043
  // libs/auth/src/session/vercel-kv-session.store.ts
3126
- import { randomUUID as randomUUID6, VercelKvStorageAdapter } from "@frontmcp/utils";
4044
+ import { randomUUID as randomUUID7, VercelKvStorageAdapter } from "@frontmcp/utils";
3127
4045
  var VercelKvSessionStore = class {
3128
4046
  storage;
3129
4047
  keyPrefix;
@@ -3317,7 +4235,7 @@ var VercelKvSessionStore = class {
3317
4235
  * Allocate a new session ID
3318
4236
  */
3319
4237
  allocId() {
3320
- return randomUUID6();
4238
+ return randomUUID7();
3321
4239
  }
3322
4240
  /**
3323
4241
  * Disconnect from Vercel KV
@@ -3616,8 +4534,214 @@ var InMemoryOrchestratedTokenStore = class {
3616
4534
  }
3617
4535
  };
3618
4536
 
4537
+ // libs/auth/src/session/orchestrated-token.crypto.ts
4538
+ import { decryptAesGcm as decryptAesGcm4, encryptAesGcm as encryptAesGcm4, hkdfSha256 as hkdfSha2563, randomBytes as randomBytes6 } from "@frontmcp/utils";
4539
+ var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
4540
+ function deriveKeyForRecord(masterKey, compositeKey, cache) {
4541
+ const cached = cache.get(compositeKey);
4542
+ if (cached) {
4543
+ return cached;
4544
+ }
4545
+ const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
4546
+ const derived = hkdfSha2563(masterKey, HKDF_SALT, info, 32);
4547
+ cache.set(compositeKey, derived);
4548
+ return derived;
4549
+ }
4550
+ function encryptRecord(key, record) {
4551
+ const plaintext = JSON.stringify(record);
4552
+ const iv = randomBytes6(12);
4553
+ const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
4554
+ return JSON.stringify({
4555
+ iv: Buffer.from(iv).toString("base64url"),
4556
+ tag: Buffer.from(tag).toString("base64url"),
4557
+ data: Buffer.from(ciphertext).toString("base64url")
4558
+ });
4559
+ }
4560
+ function decryptRecord(key, encrypted) {
4561
+ const { iv, tag, data } = JSON.parse(encrypted);
4562
+ const ivBytes = Buffer.from(iv, "base64url");
4563
+ const tagBytes = Buffer.from(tag, "base64url");
4564
+ const ciphertextBytes = Buffer.from(data, "base64url");
4565
+ const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
4566
+ return JSON.parse(new TextDecoder().decode(plaintext));
4567
+ }
4568
+
4569
+ // libs/auth/src/session/storage-orchestrated-token.store.ts
4570
+ var StorageOrchestratedTokenStore = class {
4571
+ storage;
4572
+ encryptionKey;
4573
+ defaultTtlMs;
4574
+ namespace;
4575
+ storageIsNamespaced;
4576
+ /** HKDF-derived subkey cache, keyed by composite key. */
4577
+ derivedKeys = /* @__PURE__ */ new Map();
4578
+ constructor(storage, options = {}) {
4579
+ this.encryptionKey = options.encryptionKey;
4580
+ this.defaultTtlMs = options.defaultTtlMs;
4581
+ this.namespace = options.namespace ?? "otok";
4582
+ this.storageIsNamespaced = this.isNamespacedStorage(storage);
4583
+ this.storage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
4584
+ }
4585
+ // ============================================
4586
+ // TokenStore interface
4587
+ // ============================================
4588
+ async getAccessToken(authorizationId, providerId) {
4589
+ const record = await this.getRecord(authorizationId, providerId);
4590
+ return record?.accessToken ?? null;
4591
+ }
4592
+ async getRefreshToken(authorizationId, providerId) {
4593
+ const record = await this.getRecord(authorizationId, providerId);
4594
+ return record?.refreshToken ?? null;
4595
+ }
4596
+ async storeTokens(authorizationId, providerId, tokens) {
4597
+ const compositeKey = this.compositeKey(authorizationId, providerId);
4598
+ const now = Date.now();
4599
+ const record = {
4600
+ accessToken: tokens.accessToken,
4601
+ refreshToken: tokens.refreshToken,
4602
+ expiresAt: tokens.expiresAt ?? (this.defaultTtlMs ? now + this.defaultTtlMs : void 0),
4603
+ createdAt: now,
4604
+ updatedAt: now
4605
+ };
4606
+ const value = this.encryptionKey ? encryptRecord(this.keyFor(compositeKey), record) : JSON.stringify(record);
4607
+ await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(record.expiresAt));
4608
+ }
4609
+ async deleteTokens(authorizationId, providerId) {
4610
+ const compositeKey = this.compositeKey(authorizationId, providerId);
4611
+ await this.storage.delete(this.storageKey(compositeKey));
4612
+ this.derivedKeys.delete(compositeKey);
4613
+ }
4614
+ async hasTokens(authorizationId, providerId) {
4615
+ const record = await this.getRecord(authorizationId, providerId);
4616
+ return record !== null;
4617
+ }
4618
+ async getProviderIds(authorizationId) {
4619
+ const prefix = `${authorizationId}:`;
4620
+ const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
4621
+ const providerIds = [];
4622
+ for (const compositeKey of compositeKeys) {
4623
+ if (!compositeKey.startsWith(prefix)) continue;
4624
+ const providerId = compositeKey.slice(prefix.length);
4625
+ const record = await this.getRecord(authorizationId, providerId);
4626
+ if (record) {
4627
+ providerIds.push(providerId);
4628
+ }
4629
+ }
4630
+ return providerIds;
4631
+ }
4632
+ async migrateTokens(fromAuthId, toAuthId) {
4633
+ const prefix = `${fromAuthId}:`;
4634
+ const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
4635
+ for (const oldComposite of compositeKeys) {
4636
+ if (!oldComposite.startsWith(prefix)) continue;
4637
+ const providerId = oldComposite.slice(prefix.length);
4638
+ const newComposite = this.compositeKey(toAuthId, providerId);
4639
+ const stored = await this.storage.get(this.storageKey(oldComposite));
4640
+ if (stored === null) continue;
4641
+ if (this.encryptionKey) {
4642
+ let record;
4643
+ try {
4644
+ record = decryptRecord(this.keyFor(oldComposite), stored);
4645
+ } catch {
4646
+ continue;
4647
+ }
4648
+ const value = encryptRecord(this.keyFor(newComposite), record);
4649
+ await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(record.expiresAt));
4650
+ } else {
4651
+ let expiresAt;
4652
+ try {
4653
+ expiresAt = JSON.parse(stored).expiresAt;
4654
+ } catch {
4655
+ expiresAt = void 0;
4656
+ }
4657
+ await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(expiresAt));
4658
+ }
4659
+ await this.storage.delete(this.storageKey(oldComposite));
4660
+ this.derivedKeys.delete(oldComposite);
4661
+ }
4662
+ }
4663
+ /**
4664
+ * Delete all tokens for an authorization id.
4665
+ */
4666
+ async deleteAllForAuthorization(authorizationId) {
4667
+ const prefix = `${authorizationId}:`;
4668
+ const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
4669
+ for (const compositeKey of compositeKeys) {
4670
+ if (!compositeKey.startsWith(prefix)) continue;
4671
+ await this.storage.delete(this.storageKey(compositeKey));
4672
+ this.derivedKeys.delete(compositeKey);
4673
+ }
4674
+ }
4675
+ // ============================================
4676
+ // Internals
4677
+ // ============================================
4678
+ async getRecord(authorizationId, providerId) {
4679
+ const compositeKey = this.compositeKey(authorizationId, providerId);
4680
+ const stored = await this.storage.get(this.storageKey(compositeKey));
4681
+ if (stored === null) {
4682
+ return null;
4683
+ }
4684
+ let record;
4685
+ if (this.encryptionKey) {
4686
+ try {
4687
+ record = decryptRecord(this.keyFor(compositeKey), stored);
4688
+ } catch {
4689
+ await this.storage.delete(this.storageKey(compositeKey));
4690
+ return null;
4691
+ }
4692
+ } else {
4693
+ try {
4694
+ record = JSON.parse(stored);
4695
+ } catch {
4696
+ await this.storage.delete(this.storageKey(compositeKey));
4697
+ return null;
4698
+ }
4699
+ }
4700
+ if (record.expiresAt && record.expiresAt < Date.now()) {
4701
+ await this.storage.delete(this.storageKey(compositeKey));
4702
+ return null;
4703
+ }
4704
+ return record;
4705
+ }
4706
+ keyFor(compositeKey) {
4707
+ if (!this.encryptionKey) {
4708
+ throw new EncryptionKeyNotConfiguredError();
4709
+ }
4710
+ return deriveKeyForRecord(this.encryptionKey, compositeKey, this.derivedKeys);
4711
+ }
4712
+ compositeKey(authorizationId, providerId) {
4713
+ return `${authorizationId}:${providerId}`;
4714
+ }
4715
+ storageKey(compositeKey) {
4716
+ return this.storageIsNamespaced ? compositeKey : `${this.namespace}:${compositeKey}`;
4717
+ }
4718
+ /**
4719
+ * List composite keys (authorizationId:providerId) matching a glob over the
4720
+ * composite-key space, stripping any non-namespaced prefix we added.
4721
+ */
4722
+ async listCompositeKeys(compositeGlob) {
4723
+ const pattern = this.storageIsNamespaced ? compositeGlob : `${this.namespace}:${compositeGlob}`;
4724
+ const raw = await this.storage.keys(pattern);
4725
+ if (this.storageIsNamespaced) {
4726
+ return raw;
4727
+ }
4728
+ const stripPrefix = `${this.namespace}:`;
4729
+ return raw.map((k) => k.startsWith(stripPrefix) ? k.slice(stripPrefix.length) : k);
4730
+ }
4731
+ ttlOptions(expiresAtMs) {
4732
+ if (!expiresAtMs) {
4733
+ return void 0;
4734
+ }
4735
+ const ttl = Math.ceil((expiresAtMs - Date.now()) / 1e3);
4736
+ return { ttlSeconds: ttl > 0 ? ttl : 1 };
4737
+ }
4738
+ isNamespacedStorage(storage) {
4739
+ return "namespace" in storage && typeof storage.namespace === "function";
4740
+ }
4741
+ };
4742
+
3619
4743
  // libs/auth/src/session/federated-auth.session.ts
3620
- import { randomUUID as randomUUID7 } from "@frontmcp/utils";
4744
+ import { randomUUID as randomUUID8 } from "@frontmcp/utils";
3621
4745
  function toSessionRecord(session) {
3622
4746
  return {
3623
4747
  ...session,
@@ -3692,7 +4816,7 @@ var InMemoryFederatedAuthSessionStore = class {
3692
4816
  createSession(params) {
3693
4817
  const now = Date.now();
3694
4818
  return {
3695
- id: randomUUID7(),
4819
+ id: randomUUID8(),
3696
4820
  pendingAuthId: params.pendingAuthId,
3697
4821
  clientId: params.clientId,
3698
4822
  redirectUri: params.redirectUri,
@@ -3724,7 +4848,7 @@ var InMemoryFederatedAuthSessionStore = class {
3724
4848
  function createFederatedAuthSession(params, ttlMs = 15 * 60 * 1e3) {
3725
4849
  const now = Date.now();
3726
4850
  return {
3727
- id: randomUUID7(),
4851
+ id: randomUUID8(),
3728
4852
  pendingAuthId: params.pendingAuthId,
3729
4853
  clientId: params.clientId,
3730
4854
  redirectUri: params.redirectUri,
@@ -3780,9 +4904,119 @@ function startNextProvider(session, pkce, state) {
3780
4904
  return providerId;
3781
4905
  }
3782
4906
 
4907
+ // libs/auth/src/session/storage-federated-auth.session.ts
4908
+ import { TypedStorage as TypedStorage5 } from "@frontmcp/utils";
4909
+ var StorageFederatedAuthSessionStore = class {
4910
+ storage;
4911
+ namespace;
4912
+ storageIsNamespaced;
4913
+ constructor(storage, options = {}) {
4914
+ this.namespace = options.namespace ?? "fed";
4915
+ this.storageIsNamespaced = this.isNamespacedStorage(storage);
4916
+ const namespacedStorage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
4917
+ this.storage = new TypedStorage5(namespacedStorage);
4918
+ }
4919
+ async store(session) {
4920
+ await this.put(session);
4921
+ }
4922
+ async update(session) {
4923
+ await this.put(session);
4924
+ }
4925
+ async get(id) {
4926
+ const record = await this.storage.get(this.key(id));
4927
+ if (!record) {
4928
+ return null;
4929
+ }
4930
+ if (Date.now() > record.expiresAt) {
4931
+ await this.storage.delete(this.key(id));
4932
+ return null;
4933
+ }
4934
+ return fromSessionRecord(record);
4935
+ }
4936
+ async delete(id) {
4937
+ await this.storage.delete(this.key(id));
4938
+ }
4939
+ async put(session) {
4940
+ const record = toSessionRecord(session);
4941
+ const ttlSeconds = this.calculateTtl(record.expiresAt);
4942
+ await this.storage.set(this.key(session.id), record, ttlSeconds ? { ttlSeconds } : void 0);
4943
+ }
4944
+ calculateTtl(expiresAtMs) {
4945
+ const ttl = Math.ceil((expiresAtMs - Date.now()) / 1e3);
4946
+ return ttl > 0 ? ttl : 1;
4947
+ }
4948
+ key(id) {
4949
+ return this.storageIsNamespaced ? id : `${this.namespace}:${id}`;
4950
+ }
4951
+ isNamespacedStorage(storage) {
4952
+ return "namespace" in storage && typeof storage.namespace === "function";
4953
+ }
4954
+ };
4955
+
4956
+ // libs/auth/src/session/consent.store.ts
4957
+ function consentRecordKey(userSub, clientId) {
4958
+ return `consent:${userSub}:${clientId}`;
4959
+ }
4960
+ var InMemoryConsentStore = class {
4961
+ records = /* @__PURE__ */ new Map();
4962
+ async get(userSub, clientId) {
4963
+ const record = this.records.get(consentRecordKey(userSub, clientId));
4964
+ if (!record) return null;
4965
+ return { ...record, selectedToolIds: [...record.selectedToolIds], seenToolIds: [...record.seenToolIds] };
4966
+ }
4967
+ async set(record) {
4968
+ this.records.set(consentRecordKey(record.userSub, record.clientId), {
4969
+ ...record,
4970
+ selectedToolIds: [...record.selectedToolIds],
4971
+ seenToolIds: [...record.seenToolIds]
4972
+ });
4973
+ }
4974
+ async delete(userSub, clientId) {
4975
+ this.records.delete(consentRecordKey(userSub, clientId));
4976
+ }
4977
+ /** Number of remembered records (for testing/monitoring). */
4978
+ get size() {
4979
+ return this.records.size;
4980
+ }
4981
+ /** Clear all remembered records (for testing). */
4982
+ clear() {
4983
+ this.records.clear();
4984
+ }
4985
+ };
4986
+
4987
+ // libs/auth/src/session/storage-consent.store.ts
4988
+ import { TypedStorage as TypedStorage6 } from "@frontmcp/utils";
4989
+ var StorageConsentStore = class {
4990
+ storage;
4991
+ namespace;
4992
+ storageIsNamespaced;
4993
+ constructor(storage, options = {}) {
4994
+ this.namespace = options.namespace ?? "remembered-consent";
4995
+ this.storageIsNamespaced = this.isNamespacedStorage(storage);
4996
+ const namespacedStorage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
4997
+ this.storage = new TypedStorage6(namespacedStorage);
4998
+ }
4999
+ async get(userSub, clientId) {
5000
+ return this.storage.get(this.key(userSub, clientId));
5001
+ }
5002
+ async set(record) {
5003
+ await this.storage.set(this.key(record.userSub, record.clientId), record);
5004
+ }
5005
+ async delete(userSub, clientId) {
5006
+ await this.storage.delete(this.key(userSub, clientId));
5007
+ }
5008
+ key(userSub, clientId) {
5009
+ const base = consentRecordKey(userSub, clientId);
5010
+ return this.storageIsNamespaced ? base : `${this.namespace}:${base}`;
5011
+ }
5012
+ isNamespacedStorage(storage) {
5013
+ return "namespace" in storage && typeof storage.namespace === "function";
5014
+ }
5015
+ };
5016
+
3783
5017
  // libs/auth/src/session/encrypted-authorization-vault.ts
3784
5018
  import { z as z5 } from "@frontmcp/lazy-zod";
3785
- import { AsyncLocalStorage, randomUUID as randomUUID8 } from "@frontmcp/utils";
5019
+ import { AsyncLocalStorage, randomUUID as randomUUID9 } from "@frontmcp/utils";
3786
5020
  var redisVaultEntrySchema = z5.object({
3787
5021
  /** Vault ID */
3788
5022
  id: z5.string(),
@@ -3940,7 +5174,7 @@ var EncryptedRedisVault = class {
3940
5174
  async create(params) {
3941
5175
  const now = Date.now();
3942
5176
  const entry = {
3943
- id: randomUUID8(),
5177
+ id: randomUUID9(),
3944
5178
  userSub: params.userSub,
3945
5179
  userEmail: params.userEmail,
3946
5180
  userName: params.userName,
@@ -3999,7 +5233,7 @@ var EncryptedRedisVault = class {
3999
5233
  }
4000
5234
  const now = Date.now();
4001
5235
  const pendingAuth = {
4002
- id: randomUUID8(),
5236
+ id: randomUUID9(),
4003
5237
  appId: params.appId,
4004
5238
  toolId: params.toolId,
4005
5239
  authUrl: params.authUrl,
@@ -4262,7 +5496,7 @@ var progressiveAuthStateSchema = z6.object({
4262
5496
  });
4263
5497
 
4264
5498
  // libs/auth/src/authorization/authorization.class.ts
4265
- import { base64urlDecode as base64urlDecode4, getMachineId as getMachineId2, randomUUID as randomUUID9 } from "@frontmcp/utils";
5499
+ import { base64urlDecode as base64urlDecode4, getMachineId as getMachineId2, randomUUID as randomUUID10 } from "@frontmcp/utils";
4266
5500
  var AuthorizationBase = class _AuthorizationBase {
4267
5501
  id;
4268
5502
  isAnonymous;
@@ -4310,7 +5544,7 @@ var AuthorizationBase = class _AuthorizationBase {
4310
5544
  * @param fingerprint - Optional client fingerprint for tracking
4311
5545
  */
4312
5546
  createTransportSession(protocol, fingerprint) {
4313
- const sessionId = randomUUID9();
5547
+ const sessionId = randomUUID10();
4314
5548
  const session = {
4315
5549
  id: sessionId,
4316
5550
  authorizationId: this.id,
@@ -4499,7 +5733,7 @@ var AuthorizationBase = class _AuthorizationBase {
4499
5733
  };
4500
5734
 
4501
5735
  // libs/auth/src/authorization/public.authorization.ts
4502
- import { randomUUID as randomUUID10 } from "@frontmcp/utils";
5736
+ import { randomUUID as randomUUID11 } from "@frontmcp/utils";
4503
5737
  var PublicAuthorization = class _PublicAuthorization extends AuthorizationBase {
4504
5738
  mode = "public";
4505
5739
  /**
@@ -4535,7 +5769,7 @@ var PublicAuthorization = class _PublicAuthorization extends AuthorizationBase {
4535
5769
  allowedTools = "all",
4536
5770
  allowedPrompts = "all"
4537
5771
  } = ctx;
4538
- const uuid = randomUUID10();
5772
+ const uuid = randomUUID11();
4539
5773
  const sub = `${prefix}:${uuid}`;
4540
5774
  const user = {
4541
5775
  sub,
@@ -4619,7 +5853,7 @@ var PublicAuthorization = class _PublicAuthorization extends AuthorizationBase {
4619
5853
  };
4620
5854
 
4621
5855
  // libs/auth/src/authorization/transparent.authorization.ts
4622
- import { sha256Hex } from "@frontmcp/utils";
5856
+ import { sha256Hex as sha256Hex2 } from "@frontmcp/utils";
4623
5857
  var TransparentAuthorization = class _TransparentAuthorization extends AuthorizationBase {
4624
5858
  mode = "transparent";
4625
5859
  /**
@@ -4719,7 +5953,7 @@ var TransparentAuthorization = class _TransparentAuthorization extends Authoriza
4719
5953
  static generateAuthorizationId(token) {
4720
5954
  const parts = token.split(".");
4721
5955
  const signature = parts[2] || token;
4722
- return sha256Hex(signature).substring(0, 16);
5956
+ return sha256Hex2(signature).substring(0, 16);
4723
5957
  }
4724
5958
  /**
4725
5959
  * Get the issuer from the token claims
@@ -4745,11 +5979,11 @@ var TransparentAuthorization = class _TransparentAuthorization extends Authoriza
4745
5979
  };
4746
5980
 
4747
5981
  // libs/auth/src/utils/authorization-id.utils.ts
4748
- import { sha256Hex as sha256Hex2 } from "@frontmcp/utils";
5982
+ import { sha256Hex as sha256Hex3 } from "@frontmcp/utils";
4749
5983
  function deriveAuthorizationId(token) {
4750
5984
  const parts = token.split(".");
4751
5985
  const signature = parts[2] || token;
4752
- return sha256Hex2(signature).substring(0, 16);
5986
+ return sha256Hex3(signature).substring(0, 16);
4753
5987
  }
4754
5988
 
4755
5989
  // libs/auth/src/authorization/orchestrated.authorization.ts
@@ -5116,6 +6350,103 @@ var OrchestratedAuthAccessorAdapter = class {
5116
6350
  }
5117
6351
  };
5118
6352
 
6353
+ // libs/auth/src/authorization/dcr-client.registry.ts
6354
+ import { sha256 as sha2562, timingSafeEqual } from "@frontmcp/utils";
6355
+ function globToRegExp(pattern) {
6356
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, (ch) => ch === "*" ? "\0" : `\\${ch}`);
6357
+ const withWildcards = escaped.split("\0").join(".*");
6358
+ return new RegExp(`^${withWildcards}$`);
6359
+ }
6360
+ var DcrClientRegistry = class {
6361
+ constructor(config = {}) {
6362
+ this.config = config;
6363
+ this.redirectMatchers = config.allowedRedirectUris?.map(globToRegExp);
6364
+ this.seedPreRegisteredClients();
6365
+ }
6366
+ clients = /* @__PURE__ */ new Map();
6367
+ redirectMatchers;
6368
+ /** Seed `dcr.clients` so the authorize/token flows accept them without DCR. */
6369
+ seedPreRegisteredClients() {
6370
+ const clients = this.config.clients ?? [];
6371
+ const now = Math.floor(Date.now() / 1e3);
6372
+ for (const c of clients) {
6373
+ this.clients.set(c.clientId, {
6374
+ client_id: c.clientId,
6375
+ client_secret: c.clientSecret,
6376
+ token_endpoint_auth_method: c.tokenEndpointAuthMethod ?? "none",
6377
+ grant_types: c.grantTypes ?? ["authorization_code"],
6378
+ response_types: c.responseTypes ?? ["code"],
6379
+ redirect_uris: c.redirectUris,
6380
+ client_name: c.clientName,
6381
+ scope: c.scope,
6382
+ created_at: now,
6383
+ dev: false,
6384
+ preRegistered: true
6385
+ });
6386
+ }
6387
+ }
6388
+ /** Look up a client by id (pre-registered or dynamically registered). */
6389
+ get(clientId) {
6390
+ return this.clients.get(clientId);
6391
+ }
6392
+ /** Whether a client id is known to the registry. */
6393
+ has(clientId) {
6394
+ return this.clients.has(clientId);
6395
+ }
6396
+ /** Add a dynamically-registered client (called by the register flow). */
6397
+ register(client) {
6398
+ this.clients.set(client.client_id, client);
6399
+ }
6400
+ /** Whether a redirect_uri allowlist is configured. */
6401
+ hasRedirectAllowlist() {
6402
+ return !!this.redirectMatchers && this.redirectMatchers.length > 0;
6403
+ }
6404
+ /**
6405
+ * Whether `redirectUri` is permitted by the configured allowlist. Always
6406
+ * `true` when no allowlist is configured (preserves default behavior).
6407
+ */
6408
+ isRedirectUriAllowed(redirectUri) {
6409
+ if (!this.redirectMatchers || this.redirectMatchers.length === 0) {
6410
+ return true;
6411
+ }
6412
+ return this.redirectMatchers.some((re) => re.test(redirectUri));
6413
+ }
6414
+ /** Whether a client-id allowlist is configured. */
6415
+ hasClientIdAllowlist() {
6416
+ return !!this.config.allowedClientIds && this.config.allowedClientIds.length > 0;
6417
+ }
6418
+ /**
6419
+ * Whether `clientId` is permitted by the configured client-id allowlist.
6420
+ * Always `true` when no allowlist is configured (preserves default behavior).
6421
+ */
6422
+ isClientIdAllowed(clientId) {
6423
+ const allow = this.config.allowedClientIds;
6424
+ if (!allow || allow.length === 0) {
6425
+ return true;
6426
+ }
6427
+ return allow.includes(clientId);
6428
+ }
6429
+ /** Whether an initial access token is required for DCR registration. */
6430
+ requiresInitialAccessToken() {
6431
+ return typeof this.config.initialAccessToken === "string" && this.config.initialAccessToken.length > 0;
6432
+ }
6433
+ /**
6434
+ * Constant-time check of a presented bearer token against the configured
6435
+ * initial access token. Returns `false` when none is configured (callers
6436
+ * should gate on {@link requiresInitialAccessToken} first).
6437
+ */
6438
+ verifyInitialAccessToken(presented) {
6439
+ const expected = this.config.initialAccessToken;
6440
+ if (!expected) {
6441
+ return false;
6442
+ }
6443
+ if (typeof presented !== "string" || presented.length === 0) {
6444
+ return false;
6445
+ }
6446
+ return timingSafeEqual(sha2562(presented), sha2562(expected));
6447
+ }
6448
+ };
6449
+
5119
6450
  // libs/auth/src/common/jwt.types.ts
5120
6451
  import { z as z7 } from "@frontmcp/lazy-zod";
5121
6452
  var jwkParametersSchema = z7.object({
@@ -6063,10 +7394,15 @@ var CimdService = class {
6063
7394
  }
6064
7395
  }
6065
7396
  };
7397
+ var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
7398
+ function isLoopbackHostname(hostname) {
7399
+ return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
7400
+ }
6066
7401
  function normalizeRedirectUri(uri) {
6067
7402
  try {
6068
7403
  const url = new URL(uri);
6069
- let normalized = `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}`;
7404
+ const host = isLoopbackHostname(url.hostname) ? url.hostname.toLowerCase() : url.host.toLowerCase();
7405
+ let normalized = `${url.protocol.toLowerCase()}//${host}`;
6070
7406
  normalized += url.pathname.replace(/\/+$/, "") || "/";
6071
7407
  if (url.search) normalized += url.search;
6072
7408
  return normalized;
@@ -6197,6 +7533,105 @@ var remoteProviderConfigSchema = z10.object({
6197
7533
  */
6198
7534
  userInfoEndpoint: z10.string().url().optional()
6199
7535
  });
7536
+ var upstreamProviderSchema = z10.object({
7537
+ /** Stable provider id (e.g. `'github'`). Used in `this.orchestration.getToken(id)`. */
7538
+ id: z10.string().min(1),
7539
+ /** Human-readable display name. Defaults to `id` when omitted. */
7540
+ name: z10.string().optional(),
7541
+ /** Authorization endpoint (canonical name). */
7542
+ authorizationEndpoint: z10.string().url().optional(),
7543
+ /** Authorization endpoint (alias for {@link authorizationEndpoint}). */
7544
+ authorizeUrl: z10.string().url().optional(),
7545
+ /** Token endpoint (canonical name). */
7546
+ tokenEndpoint: z10.string().url().optional(),
7547
+ /** Token endpoint (alias for {@link tokenEndpoint}). */
7548
+ tokenUrl: z10.string().url().optional(),
7549
+ /** OAuth client id issued by the upstream provider. */
7550
+ clientId: z10.string().min(1),
7551
+ /** OAuth client secret (confidential clients). */
7552
+ clientSecret: z10.string().optional(),
7553
+ /** Scopes to request from the upstream provider. */
7554
+ scopes: z10.array(z10.string()).optional(),
7555
+ /** User info endpoint (optional, used to enrich the session identity). */
7556
+ userInfoEndpoint: z10.string().url().optional(),
7557
+ /** JWKS URI for upstream id_token validation (optional). */
7558
+ jwksUri: z10.string().url().optional()
7559
+ }).refine((p) => !!(p.authorizationEndpoint ?? p.authorizeUrl), {
7560
+ message: "authorizationEndpoint (or authorizeUrl) is required",
7561
+ path: ["authorizationEndpoint"]
7562
+ }).refine((p) => !!(p.tokenEndpoint ?? p.tokenUrl), {
7563
+ message: "tokenEndpoint (or tokenUrl) is required",
7564
+ path: ["tokenEndpoint"]
7565
+ }).refine((p) => !(p.authorizationEndpoint && p.authorizeUrl), {
7566
+ message: "provide only one of authorizationEndpoint or authorizeUrl",
7567
+ path: ["authorizationEndpoint"]
7568
+ }).refine((p) => !(p.tokenEndpoint && p.tokenUrl), {
7569
+ message: "provide only one of tokenEndpoint or tokenUrl",
7570
+ path: ["tokenEndpoint"]
7571
+ });
7572
+ var localDcrClientSchema = z10.object({
7573
+ /** Stable, pre-assigned client identifier the client authenticates with. */
7574
+ clientId: z10.string().min(1),
7575
+ /**
7576
+ * Confidential-client secret. Omit for public clients
7577
+ * (`token_endpoint_auth_method: 'none'`, the default).
7578
+ */
7579
+ clientSecret: z10.string().min(1).optional(),
7580
+ /** Exact redirect URIs this client is allowed to use (at least one). */
7581
+ redirectUris: z10.array(z10.string().url()).min(1, "At least one redirect_uri is required"),
7582
+ /** Human-readable name shown on consent/login screens. */
7583
+ clientName: z10.string().optional(),
7584
+ /**
7585
+ * Token-endpoint auth method. Defaults to `'none'` (public PKCE client).
7586
+ */
7587
+ tokenEndpointAuthMethod: z10.enum(["none", "client_secret_basic", "client_secret_post"]).default("none"),
7588
+ /** Allowed grant types. Defaults to `['authorization_code']`. */
7589
+ grantTypes: z10.array(z10.enum(["authorization_code", "refresh_token"])).default(["authorization_code"]),
7590
+ /** Allowed response types. Defaults to `['code']`. */
7591
+ responseTypes: z10.array(z10.enum(["code"])).default(["code"]),
7592
+ /** Optional space-delimited default scope string. */
7593
+ scope: z10.string().optional()
7594
+ });
7595
+ var localDcrConfigSchema = z10.object({
7596
+ /**
7597
+ * Whether `POST /oauth/register` is mounted/active and whether
7598
+ * `registration_endpoint` is advertised in AS metadata.
7599
+ *
7600
+ * When unset (default), the historical guard applies: enabled in
7601
+ * development, disabled in production. Set explicitly to force it on or off
7602
+ * regardless of `NODE_ENV`.
7603
+ */
7604
+ enabled: z10.boolean().optional(),
7605
+ /**
7606
+ * Allowlist of redirect URIs. Each entry is matched EXACTLY or as a simple
7607
+ * glob (`*` matches any run of characters). When set, both DCR registrations
7608
+ * and `/oauth/authorize` requests are rejected unless their `redirect_uri`
7609
+ * matches an entry. When unset, no redirect_uri allowlist is enforced.
7610
+ */
7611
+ allowedRedirectUris: z10.array(z10.string().min(1)).optional(),
7612
+ /**
7613
+ * Allowlist of client ids. When set, only these `client_id`s may register
7614
+ * (DCR registrations are forced to one of these ids) or be used at
7615
+ * `/oauth/authorize`. When unset, any client id is accepted (subject to the
7616
+ * other checks). CIMD URL client ids are validated by the CIMD layer and are
7617
+ * exempt from this allowlist.
7618
+ */
7619
+ allowedClientIds: z10.array(z10.string().min(1)).optional(),
7620
+ /**
7621
+ * Initial Access Token (RFC 7591 §3). When set, `POST /oauth/register`
7622
+ * requires an `Authorization: Bearer <token>` header that matches this value
7623
+ * (constant-time compared); requests without it (or with a wrong value) are
7624
+ * rejected `401 invalid_token`. When unset, registration is unauthenticated
7625
+ * (the historical behavior).
7626
+ */
7627
+ initialAccessToken: z10.string().min(1).optional(),
7628
+ /**
7629
+ * Pre-registered trusted clients seeded into the registry at startup. These
7630
+ * are accepted by the authorize/token flows WITHOUT a DCR round-trip, which
7631
+ * lets you disable DCR entirely while still shipping known clients.
7632
+ */
7633
+ clients: z10.array(localDcrClientSchema).optional()
7634
+ });
6200
7635
  var flatRemoteProviderFields = {
6201
7636
  /**
6202
7637
  * OAuth provider base URL (required)
@@ -6212,7 +7647,64 @@ var flatRemoteProviderFields = {
6212
7647
  /** Advanced provider configuration */
6213
7648
  providerConfig: providerConfigSchema.optional()
6214
7649
  };
6215
- var tokenStorageConfigSchema = z10.union([z10.literal("memory"), z10.object({ redis: redisConfigSchema })]);
7650
+ var tokenStorageSqliteSchema = z10.object({
7651
+ /** Path to the `.sqlite` database file used for auth persistence. */
7652
+ path: z10.string().min(1),
7653
+ /**
7654
+ * Encryption configuration for at-rest encryption of stored values.
7655
+ * Independent of the AES-GCM blob encryption already applied to upstream
7656
+ * provider tokens by the orchestrated-token store.
7657
+ */
7658
+ encryption: z10.object({
7659
+ secret: z10.string().min(1)
7660
+ }).optional(),
7661
+ /**
7662
+ * Interval in milliseconds for purging expired keys.
7663
+ * @default 60000
7664
+ */
7665
+ ttlCleanupIntervalMs: z10.number().int().nonnegative().optional(),
7666
+ /**
7667
+ * Enable WAL mode for better read concurrency.
7668
+ * @default true
7669
+ */
7670
+ walMode: z10.boolean().optional()
7671
+ });
7672
+ var tokenStorageConfigSchema = z10.union([
7673
+ z10.literal("memory"),
7674
+ z10.object({ redis: redisConfigSchema }),
7675
+ z10.object({ sqlite: tokenStorageSqliteSchema })
7676
+ ]);
7677
+ var secureStoreScopeSchema = z10.enum(["user", "session", "global"]);
7678
+ var secureStoreEncryptionSchema = z10.object({
7679
+ /**
7680
+ * Server pepper mixed into HKDF key derivation (defense-in-depth). Defaults to
7681
+ * `VAULT_SECRET ?? JWT_SECRET` at runtime when omitted.
7682
+ */
7683
+ pepper: z10.string().min(1).optional()
7684
+ });
7685
+ var secureStoreCustomBackendSchema = z10.custom(
7686
+ (v) => typeof v === "object" && v !== null && typeof v.get === "function" && typeof v.set === "function" && typeof v.delete === "function" && typeof v.list === "function",
7687
+ { message: "secureStore.backend must implement get/set/delete/list (SecureStoreBackend)" }
7688
+ );
7689
+ var secureStoreCommonFields = {
7690
+ /** Namespace scope. @default 'user' */
7691
+ scope: secureStoreScopeSchema.optional(),
7692
+ /** Default TTL (ms) for stored secrets. @default undefined (no expiry) */
7693
+ ttlMs: z10.number().int().positive().optional(),
7694
+ /** At-rest encryption settings (built-in backings only). */
7695
+ encryption: secureStoreEncryptionSchema.optional()
7696
+ };
7697
+ var secureStoreConfigSchema = z10.union([
7698
+ z10.literal("memory"),
7699
+ // Backing-bearing variants MUST precede the bare-object variant: Zod object
7700
+ // schemas are non-strict (extra keys allowed), so a bare `z.object({ scope })`
7701
+ // would otherwise match `{ sqlite, scope }` first and silently strip the
7702
+ // backing. Ordering most-specific-first keeps sqlite/redis/custom intact.
7703
+ z10.object({ sqlite: tokenStorageSqliteSchema, ...secureStoreCommonFields }),
7704
+ z10.object({ redis: redisConfigSchema, ...secureStoreCommonFields }),
7705
+ z10.object({ backend: secureStoreCustomBackendSchema, ...secureStoreCommonFields }),
7706
+ z10.object({ ...secureStoreCommonFields })
7707
+ ]);
6216
7708
  var tokenRefreshConfigSchema = z10.object({
6217
7709
  /**
6218
7710
  * Enable automatic token refresh
@@ -6279,7 +7771,22 @@ var federatedAuthConfigSchema = z10.object({
6279
7771
  * - 'format': validate only "federated:{sessionId}:{nonce}" format
6280
7772
  * @default 'strict'
6281
7773
  */
6282
- stateValidation: z10.enum(["strict", "format"]).default("strict")
7774
+ stateValidation: z10.enum(["strict", "format"]).default("strict"),
7775
+ /**
7776
+ * Minimum number of upstream providers a user must link before a FrontMCP JWT
7777
+ * is minted. The federated callback refuses to issue a token until at least
7778
+ * this many providers have been authorized. Defaults to `1` at runtime when
7779
+ * `providers` are configured (i.e. "no JWT until ≥1 linked"). Left optional
7780
+ * here so existing configs without `providers` are completely unaffected.
7781
+ */
7782
+ minProviders: z10.number().int().positive().optional(),
7783
+ /**
7784
+ * Provider ids that MUST be among the linked providers before a JWT is minted.
7785
+ * The federated callback rejects the login unless every id listed here is in
7786
+ * the user's selected providers. Optional — omit to allow any combination that
7787
+ * satisfies {@link minProviders}.
7788
+ */
7789
+ requiredProviders: z10.array(z10.string()).optional()
6283
7790
  });
6284
7791
  var incrementalAuthConfigSchema = z10.object({
6285
7792
  /**
@@ -6380,9 +7887,19 @@ var transparentAuthOptionsSchema = z12.object({
6380
7887
 
6381
7888
  // libs/auth/src/options/orchestrated.schema.ts
6382
7889
  import { z as z13 } from "@frontmcp/lazy-zod";
7890
+ var authSlotSchema = z13.enum(["login", "consent", "incremental", "federated", "error"]);
7891
+ var authUiMapSchema = z13.partialRecord(authSlotSchema, z13.string().min(1));
7892
+ var authExtraHandlerSchema = z13.custom((v) => typeof v === "function", {
7893
+ message: "auth.extras entries must be handler functions (input, ctx) => AuthExtraResult."
7894
+ });
7895
+ var authExtrasMapSchema = z13.record(z13.string().min(1), authExtraHandlerSchema);
6383
7896
  var sharedAuthFields = {
6384
7897
  local: localSigningConfigSchema.optional(),
6385
7898
  tokenStorage: tokenStorageConfigSchema.default("memory"),
7899
+ // #470 — backing for the general session secure-secret store (`this.secureStore`).
7900
+ // Optional (no default) so an unset value resolves to the in-memory encrypted
7901
+ // default at runtime; existing configs are unaffected.
7902
+ secureStore: secureStoreConfigSchema.optional(),
6386
7903
  allowDefaultPublic: z13.boolean().default(false),
6387
7904
  anonymousScopes: z13.array(z13.string()).default(["anonymous"]),
6388
7905
  publicAccess: publicAccessConfigSchema.optional(),
@@ -6391,11 +7908,59 @@ var sharedAuthFields = {
6391
7908
  refresh: tokenRefreshConfigSchema.optional(),
6392
7909
  expectedAudience: z13.union([z13.string(), z13.array(z13.string())]).optional(),
6393
7910
  incrementalAuth: incrementalAuthConfigSchema.optional(),
6394
- cimd: cimdConfigSchema.optional()
7911
+ cimd: cimdConfigSchema.optional(),
7912
+ // #469 — custom auth UI as a slot→file map + extras name→handler map (per-app
7913
+ // under splitByApp). Both optional; omitting them serves the built-in pages.
7914
+ ui: authUiMapSchema.optional(),
7915
+ extras: authExtrasMapSchema.optional()
6395
7916
  };
7917
+ var loginFieldSchema = z13.object({
7918
+ type: z13.enum(["text", "password", "email", "select", "hidden"]),
7919
+ label: z13.string().optional(),
7920
+ required: z13.boolean().optional(),
7921
+ placeholder: z13.string().optional(),
7922
+ options: z13.array(z13.object({ value: z13.string(), label: z13.string() })).optional()
7923
+ });
7924
+ var loginConfigSchema = z13.object({
7925
+ title: z13.string().optional(),
7926
+ subtitle: z13.string().optional(),
7927
+ logoUri: z13.string().optional(),
7928
+ fields: z13.record(z13.string(), loginFieldSchema).optional(),
7929
+ // Full HTML override — a function, validated structurally like other callbacks
7930
+ // in the codebase (see http/cors handlers using z.custom<…>).
7931
+ render: z13.custom((v) => typeof v === "function", {
7932
+ message: "login.render must be a function"
7933
+ }).optional(),
7934
+ subject: z13.object({
7935
+ fromField: z13.string().optional(),
7936
+ strategy: z13.enum(["per-session", "per-account"]).optional()
7937
+ }).optional()
7938
+ });
7939
+ var authenticateFnSchema = z13.custom((v) => typeof v === "function", {
7940
+ message: "authenticate must be a function"
7941
+ });
6396
7942
  var localAuthSchema = z13.object({
6397
7943
  mode: z13.literal("local"),
6398
- ...sharedAuthFields
7944
+ ...sharedAuthFields,
7945
+ // #468 — opt-out of the email requirement at /oauth/callback for
7946
+ // single-operator local setups. Defaults preserve the historical behavior
7947
+ // (email required) so existing configs are unaffected.
7948
+ requireEmail: z13.boolean().default(true),
7949
+ anonymousSubject: z13.string().min(1).default("local-operator"),
7950
+ // Checkpoint 3a — declarative login customization + custom verification.
7951
+ // Both optional; omitting them preserves the default login path exactly.
7952
+ login: loginConfigSchema.optional(),
7953
+ authenticate: authenticateFnSchema.optional(),
7954
+ // Multi-OAuth-provider orchestration — declare upstream providers (GitHub,
7955
+ // Slack, Jira, …) to federate at /oauth/authorize. Optional; omitting it
7956
+ // preserves the default single-operator local login exactly.
7957
+ providers: z13.array(upstreamProviderSchema).optional(),
7958
+ // Local-AS Dynamic Client Registration control surface (#462) — gate
7959
+ // `POST /oauth/register`, allowlist redirect_uris/client_ids, require an
7960
+ // initial access token, and seed pre-registered trusted clients. Optional;
7961
+ // omitting it preserves today's behavior exactly (DCR on in dev, off in prod;
7962
+ // no allowlist; no initial access token).
7963
+ dcr: localDcrConfigSchema.optional()
6399
7964
  });
6400
7965
  var remoteAuthSchema = z13.object({
6401
7966
  mode: z13.literal("remote"),
@@ -8936,6 +10501,7 @@ export {
8936
10501
  AuthorityDeniedError,
8937
10502
  AuthorizationBase,
8938
10503
  CDN,
10504
+ CREDENTIALS_ACCESSOR,
8939
10505
  CimdClientIdMismatchError,
8940
10506
  CimdDisabledError,
8941
10507
  CimdError,
@@ -8947,11 +10513,15 @@ export {
8947
10513
  CredentialCache,
8948
10514
  CredentialProviderAlreadyRegisteredError,
8949
10515
  CredentialStorageError,
10516
+ CredentialsAccessorImpl,
10517
+ DEFAULT_RESUME_TTL_MS,
8950
10518
  DEFAULT_THEME,
10519
+ DcrClientRegistry,
8951
10520
  EagerCredentialLoader,
8952
10521
  ElicitationSecretRequiredError,
8953
10522
  EncryptedRedisVault,
8954
10523
  EncryptedStorageError,
10524
+ EncryptedStorageSecureStoreBackend,
8955
10525
  EncryptedTypedStorage,
8956
10526
  EncryptionContextNotSetError,
8957
10527
  EncryptionKeyNotConfiguredError,
@@ -8960,6 +10530,7 @@ export {
8960
10530
  InMemoryAuthorizationStore,
8961
10531
  InMemoryAuthorizationVault,
8962
10532
  InMemoryCimdCache,
10533
+ InMemoryConsentStore,
8963
10534
  InMemoryFederatedAuthSessionStore,
8964
10535
  InMemoryOrchestratedTokenStore,
8965
10536
  InMemoryStoreRequiredError,
@@ -8977,11 +10548,18 @@ export {
8977
10548
  RedirectUriMismatchError,
8978
10549
  RedisAuthorizationStore,
8979
10550
  RedisSessionStore,
10551
+ SECURE_STORE_ACCESSOR,
8980
10552
  ScopeDeniedError,
10553
+ SecureStoreAccessorImpl,
10554
+ SessionCredentialVault,
8981
10555
  SessionIdEmptyError,
8982
10556
  SessionRateLimiter,
8983
10557
  SessionSecretRequiredError,
10558
+ StorageAuthorizationStore,
8984
10559
  StorageAuthorizationVault,
10560
+ StorageConsentStore,
10561
+ StorageFederatedAuthSessionStore,
10562
+ StorageOrchestratedTokenStore,
8985
10563
  StorageTokenStore,
8986
10564
  TinyTtlCache,
8987
10565
  TokenLeakDetectedError,
@@ -8990,7 +10568,7 @@ export {
8990
10568
  TokenVault,
8991
10569
  TransparentAuthorization,
8992
10570
  TransportIdGenerator,
8993
- TypedStorage3 as TypedStorage,
10571
+ TypedStorage4 as TypedStorage,
8994
10572
  VaultEncryption,
8995
10573
  VaultLoadError,
8996
10574
  VaultNotFoundError,
@@ -9013,6 +10591,7 @@ export {
9013
10591
  authProviderMappingSchema,
9014
10592
  authProvidersVaultOptionsSchema,
9015
10593
  authUserSchema,
10594
+ authenticateFnSchema,
9016
10595
  authoritiesClaimsMappingSchema,
9017
10596
  authoritiesConfigSchema,
9018
10597
  authoritiesMetadataSchema,
@@ -9027,7 +10606,11 @@ export {
9027
10606
  bearerApplier,
9028
10607
  bearerCredentialSchema,
9029
10608
  buildAuthContext,
10609
+ buildCodeRecord,
10610
+ buildConnectPage,
10611
+ buildConnectSuccessPage,
9030
10612
  buildConsentPage,
10613
+ buildCredentialResumeUrl,
9031
10614
  buildErrorPage,
9032
10615
  buildFederatedLoginPage,
9033
10616
  buildIncrementalAuthPage,
@@ -9035,7 +10618,9 @@ export {
9035
10618
  buildInvalidRequestHeader,
9036
10619
  buildInvalidTokenHeader,
9037
10620
  buildLoginPage,
10621
+ buildPendingRecord,
9038
10622
  buildPrmUrl,
10623
+ buildRefreshTokenRecord,
9039
10624
  buildToolConsentPage,
9040
10625
  buildUnauthorizedHeader,
9041
10626
  buildWwwAuthenticate,
@@ -9048,6 +10633,7 @@ export {
9048
10633
  clientMetadataDocumentSchema,
9049
10634
  completeCurrentProvider,
9050
10635
  consentConfigSchema,
10636
+ consentRecordKey,
9051
10637
  consentSelectionSchema,
9052
10638
  consentStateSchema,
9053
10639
  consentToolItemSchema,
@@ -9055,13 +10641,16 @@ export {
9055
10641
  createEncryptedVault,
9056
10642
  createFederatedAuthSession,
9057
10643
  createLayout,
10644
+ createSecureStore,
10645
+ createSessionCredentialVault,
10646
+ createTokenStorageAdapter,
9058
10647
  credentialProviderConfigSchema,
9059
10648
  credentialSchema,
9060
10649
  credentialScopeSchema,
9061
10650
  credentialTypeSchema,
9062
10651
  customCredentialSchema,
9063
10652
  decodeJwtPayloadSafe,
9064
- decryptAesGcm4 as decryptAesGcm,
10653
+ decryptAesGcm5 as decryptAesGcm,
9065
10654
  decryptSessionJson,
9066
10655
  decryptValue2 as decryptValue,
9067
10656
  defaultSessionRateLimiter,
@@ -9071,7 +10660,7 @@ export {
9071
10660
  deriveTypedUser,
9072
10661
  detectAuthProviders,
9073
10662
  detectedAuthProviderSchema,
9074
- encryptAesGcm4 as encryptAesGcm,
10663
+ encryptAesGcm5 as encryptAesGcm,
9075
10664
  encryptJson,
9076
10665
  encryptValue2 as encryptValue,
9077
10666
  encryptedBlobSchema,
@@ -9088,7 +10677,9 @@ export {
9088
10677
  federatedSelectionSchema,
9089
10678
  flatRemoteProviderFields,
9090
10679
  fromSessionRecord,
10680
+ generateAuthorizationCode,
9091
10681
  generatePkceChallenge,
10682
+ generateRefreshTokenValue,
9092
10683
  getCredentialOptionsSchema,
9093
10684
  getKey,
9094
10685
  getNextProvider,
@@ -9097,7 +10688,7 @@ export {
9097
10688
  getTokenSignatureFingerprint,
9098
10689
  hasOnlyLocalhostRedirectUris,
9099
10690
  headerApplier,
9100
- hkdfSha2563 as hkdfSha256,
10691
+ hkdfSha2564 as hkdfSha256,
9101
10692
  incrementalAuthConfigSchema,
9102
10693
  isCimdClientId,
9103
10694
  isFrontMcpCredentials,
@@ -9106,12 +10697,15 @@ export {
9106
10697
  isOrchestratedLocal,
9107
10698
  isOrchestratedMode,
9108
10699
  isOrchestratedRemote,
10700
+ isPersistentTokenStorage,
9109
10701
  isPublicMode,
10702
+ isRedisTokenStorage,
9110
10703
  isRemoteMode,
9111
10704
  isSessionComplete,
9112
10705
  isSignedData as isSignedSession,
9113
10706
  isSoonExpiring,
9114
10707
  isSoonExpiringProvider,
10708
+ isSqliteTokenStorage,
9115
10709
  isTransparentMode,
9116
10710
  jsonWebKeySetSchema,
9117
10711
  jwkParametersSchema,
@@ -9120,7 +10714,10 @@ export {
9120
10714
  llmSafeAuthContextSchema,
9121
10715
  loadingStrategySchema,
9122
10716
  localAuthSchema,
10717
+ localDcrClientSchema,
10718
+ localDcrConfigSchema,
9123
10719
  localSigningConfigSchema,
10720
+ loginConfigSchema,
9124
10721
  mtlsCredentialSchema,
9125
10722
  noopLogger,
9126
10723
  normalizeIssuer,
@@ -9146,14 +10743,20 @@ export {
9146
10743
  redisVaultEntrySchema,
9147
10744
  remoteAuthSchema,
9148
10745
  remoteProviderConfigSchema,
10746
+ renderLocalLoginPage,
9149
10747
  renderToHtml,
9150
10748
  resetCachedKey,
9151
10749
  resolveRequiredScopes,
9152
10750
  resourceIdRefSchema,
9153
10751
  safeDecrypt,
10752
+ secureStoreConfigSchema,
10753
+ secureStoreCustomBackendSchema,
10754
+ secureStoreEncryptionSchema,
10755
+ secureStoreScopeSchema,
9154
10756
  serviceAccountCredentialSchema,
9155
10757
  sessionIdPayloadSchema,
9156
10758
  sessionJwtPayloadSchema,
10759
+ signCredentialResumeToken,
9157
10760
  signSession,
9158
10761
  skippedAppBehaviorSchema,
9159
10762
  sseTransportStateSchema,
@@ -9164,20 +10767,24 @@ export {
9164
10767
  storedSessionSchema,
9165
10768
  streamableHttpTransportStateSchema,
9166
10769
  toEpochSeconds,
10770
+ toLoginExtraFields,
9167
10771
  toSessionRecord,
9168
10772
  tokenRefreshConfigSchema,
9169
10773
  tokenStorageConfigSchema,
10774
+ tokenStorageSqliteSchema,
9170
10775
  transparentAuthOptionsSchema,
9171
10776
  transportProtocolSchema,
9172
10777
  transportSessionSchema,
9173
10778
  transportStateSchema,
9174
10779
  trimSlash,
9175
10780
  tryJwtExp,
10781
+ upstreamProviderSchema,
9176
10782
  userClaimSchema,
9177
10783
  validateAudience,
9178
10784
  validateClientIdUrl,
9179
10785
  vaultConsentRecordSchema,
9180
10786
  vaultFederatedRecordSchema,
10787
+ verifyCredentialResumeToken,
9181
10788
  verifyOrParseSession,
9182
10789
  verifyPkce,
9183
10790
  verifySession,