@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.
- package/authorities/authorities.profiles.d.ts +14 -1
- package/authorities/authorities.profiles.d.ts.map +1 -1
- package/authorization/dcr-client.registry.d.ts +83 -0
- package/authorization/dcr-client.registry.d.ts.map +1 -0
- package/authorization/index.d.ts +2 -0
- package/authorization/index.d.ts.map +1 -1
- package/cimd/cimd.service.d.ts +2 -9
- package/cimd/cimd.service.d.ts.map +1 -1
- package/common/session.types.d.ts +4 -4
- package/esm/index.mjs +1821 -214
- package/esm/package.json +5 -5
- package/index.d.ts +5 -5
- package/index.d.ts.map +1 -1
- package/index.js +2030 -383
- package/jwks/jwks.service.d.ts +2 -4
- package/jwks/jwks.service.d.ts.map +1 -1
- package/options/app-auth.schema.d.ts +226 -0
- package/options/app-auth.schema.d.ts.map +1 -1
- package/options/index.d.ts +4 -4
- package/options/index.d.ts.map +1 -1
- package/options/interfaces.d.ts +580 -4
- package/options/interfaces.d.ts.map +1 -1
- package/options/orchestrated.schema.d.ts +492 -1
- package/options/orchestrated.schema.d.ts.map +1 -1
- package/options/schema.d.ts +226 -0
- package/options/schema.d.ts.map +1 -1
- package/options/shared.schemas.d.ts +237 -2
- package/options/shared.schemas.d.ts.map +1 -1
- package/package.json +5 -5
- package/session/authorization.store.d.ts +117 -47
- package/session/authorization.store.d.ts.map +1 -1
- package/session/consent.store.d.ts +86 -0
- package/session/consent.store.d.ts.map +1 -0
- package/session/credential-resume-link.d.ts +64 -0
- package/session/credential-resume-link.d.ts.map +1 -0
- package/session/credentials-accessor.d.ts +102 -0
- package/session/credentials-accessor.d.ts.map +1 -0
- package/session/credentials-accessor.impl.d.ts +46 -0
- package/session/credentials-accessor.impl.d.ts.map +1 -0
- package/session/index.d.ts +32 -2
- package/session/index.d.ts.map +1 -1
- package/session/orchestrated-token.crypto.d.ts +33 -0
- package/session/orchestrated-token.crypto.d.ts.map +1 -0
- package/session/secure-store-accessor.d.ts +77 -0
- package/session/secure-store-accessor.d.ts.map +1 -0
- package/session/secure-store-accessor.impl.d.ts +61 -0
- package/session/secure-store-accessor.impl.d.ts.map +1 -0
- package/session/secure-store-backends.d.ts +75 -0
- package/session/secure-store-backends.d.ts.map +1 -0
- package/session/secure-store.d.ts +89 -0
- package/session/secure-store.d.ts.map +1 -0
- package/session/secure-store.factory.d.ts +62 -0
- package/session/secure-store.factory.d.ts.map +1 -0
- package/session/session-credential-vault.d.ts +153 -0
- package/session/session-credential-vault.d.ts.map +1 -0
- package/session/session-credential-vault.factory.d.ts +50 -0
- package/session/session-credential-vault.factory.d.ts.map +1 -0
- package/session/storage-authorization.store.d.ts +51 -0
- package/session/storage-authorization.store.d.ts.map +1 -0
- package/session/storage-consent.store.d.ts +33 -0
- package/session/storage-consent.store.d.ts.map +1 -0
- package/session/storage-federated-auth.session.d.ts +36 -0
- package/session/storage-federated-auth.session.d.ts.map +1 -0
- package/session/storage-orchestrated-token.store.d.ts +72 -0
- package/session/storage-orchestrated-token.store.d.ts.map +1 -0
- package/session/token-storage.factory.d.ts +46 -0
- package/session/token-storage.factory.d.ts.map +1 -0
- package/ui/index.d.ts +3 -2
- package/ui/index.d.ts.map +1 -1
- package/ui/local-login.helper.d.ts +31 -0
- package/ui/local-login.helper.d.ts.map +1 -0
- package/ui/templates.d.ts +118 -2
- package/ui/templates.d.ts.map +1 -1
- 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 {
|
|
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 {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')
|
|
1039
|
-
|
|
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
|
-
|
|
1066
|
+
${subtitle}
|
|
1047
1067
|
</p>
|
|
1048
1068
|
|
|
1069
|
+
${errorBanner}
|
|
1049
1070
|
${userInfo}
|
|
1050
1071
|
|
|
1051
|
-
<form method="
|
|
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
|
-
|
|
1058
|
-
|
|
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
|
|
1084
|
+
<!-- Tool groups -->
|
|
1066
1085
|
<div class="space-y-6 mb-8">
|
|
1067
|
-
${
|
|
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
|
|
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
|
|
1405
|
+
return generateAuthorizationCode();
|
|
1203
1406
|
}
|
|
1204
1407
|
generateRefreshToken() {
|
|
1205
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1514
|
+
return generateAuthorizationCode();
|
|
1366
1515
|
}
|
|
1367
1516
|
generateRefreshToken() {
|
|
1368
|
-
return
|
|
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
|
|
2853
|
+
hkdfSha256 as hkdfSha2564,
|
|
1936
2854
|
encryptValue as encryptValue2,
|
|
1937
2855
|
decryptValue as decryptValue2,
|
|
1938
|
-
encryptAesGcm as
|
|
1939
|
-
decryptAesGcm as
|
|
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 ||
|
|
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
|
|
3123
|
+
return signData2(session, resolveConfig(config));
|
|
2206
3124
|
}
|
|
2207
3125
|
function verifySession(signedData, config) {
|
|
2208
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|