@frontmcp/sdk 1.7.0 → 1.7.2
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/agent/flows/call-agent.flow.d.ts +2 -6
- package/agent/flows/call-agent.flow.d.ts.map +1 -1
- package/auth/flows/oauth.authorize.flow.d.ts +30 -0
- package/auth/flows/oauth.authorize.flow.d.ts.map +1 -1
- package/auth/flows/oauth.callback.flow.d.ts.map +1 -1
- package/auth/instances/instance.local-primary-auth.d.ts +35 -0
- package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
- package/builtin/config/providers/config-loader.d.ts +5 -0
- package/builtin/config/providers/config-loader.d.ts.map +1 -1
- package/common/metadata/agent.metadata.d.ts +2 -2
- package/common/metadata/app.metadata.d.ts +6 -6
- package/common/metadata/flow.metadata.d.ts +4 -4
- package/common/metadata/front-mcp.metadata.d.ts +43 -31
- package/common/metadata/front-mcp.metadata.d.ts.map +1 -1
- package/common/metadata/job.metadata.d.ts +2 -2
- package/common/metadata/tool.metadata.d.ts +3 -3
- package/common/metadata/workflow.metadata.d.ts +2 -2
- package/common/schemas/http-output.schema.d.ts +4 -4
- package/common/types/options/http/interfaces.d.ts +26 -10
- package/common/types/options/http/interfaces.d.ts.map +1 -1
- package/common/types/options/http/schema.d.ts.map +1 -1
- package/common/types/options/session/schema.d.ts +3 -3
- package/common/types/options/transport/schema.d.ts +3 -1
- package/common/types/options/transport/schema.d.ts.map +1 -1
- package/common/utils/index.d.ts +1 -0
- package/common/utils/index.d.ts.map +1 -1
- package/common/utils/principal.utils.d.ts +29 -0
- package/common/utils/principal.utils.d.ts.map +1 -0
- package/direct/llm-platform.d.ts +2 -2
- package/direct/llm-platform.d.ts.map +1 -1
- package/errors/index.d.ts +1 -0
- package/errors/index.d.ts.map +1 -1
- package/errors/job.errors.d.ts +26 -0
- package/errors/job.errors.d.ts.map +1 -0
- package/esm/index.mjs +928 -207
- package/front-mcp/front-mcp.providers.d.ts +4 -4
- package/index.js +1041 -320
- package/job/execution/job-execution.manager.d.ts +15 -0
- package/job/execution/job-execution.manager.d.ts.map +1 -1
- package/job/job-permission.guard.d.ts +23 -4
- package/job/job-permission.guard.d.ts.map +1 -1
- package/job/job-scope.helper.d.ts +12 -0
- package/job/job-scope.helper.d.ts.map +1 -1
- package/job/store/job-state.interface.d.ts +6 -0
- package/job/store/job-state.interface.d.ts.map +1 -1
- package/job/tools/execute-job.tool.d.ts.map +1 -1
- package/job/tools/get-job-status.tool.d.ts +12 -0
- package/job/tools/get-job-status.tool.d.ts.map +1 -1
- package/job/tools/list-jobs.tool.d.ts +6 -0
- package/job/tools/list-jobs.tool.d.ts.map +1 -1
- package/job/tools/register-job.tool.d.ts +1 -0
- package/job/tools/register-job.tool.d.ts.map +1 -1
- package/job/tools/remove-job.tool.d.ts.map +1 -1
- package/package.json +10 -10
- package/resource/flows/resource-templates-list.flow.d.ts +2 -2
- package/resource/flows/resources-list.flow.d.ts +2 -2
- package/scope/scope.utils.d.ts.map +1 -1
- package/server/adapters/express.host.adapter.d.ts +34 -2
- package/server/adapters/express.host.adapter.d.ts.map +1 -1
- package/server/middleware/host-validation.middleware.d.ts +8 -4
- package/server/middleware/host-validation.middleware.d.ts.map +1 -1
- package/server/security/host-validation.d.ts +66 -0
- package/server/security/host-validation.d.ts.map +1 -0
- package/server/security/resolve-allowed-hosts.d.ts +63 -0
- package/server/security/resolve-allowed-hosts.d.ts.map +1 -0
- package/server/security/security-audit.d.ts +7 -4
- package/server/security/security-audit.d.ts.map +1 -1
- package/server/server.instance.d.ts.map +1 -1
- package/server/server.types.d.ts +11 -6
- package/server/server.types.d.ts.map +1 -1
- package/tool/flows/call-tool.flow.d.ts +19 -7
- package/tool/flows/call-tool.flow.d.ts.map +1 -1
- package/tool/flows/tools-list.flow.d.ts +2 -6
- package/tool/flows/tools-list.flow.d.ts.map +1 -1
- package/transport/adapters/base-sse-transport.d.ts +7 -0
- package/transport/adapters/base-sse-transport.d.ts.map +1 -1
- package/transport/adapters/transport.local.adapter.d.ts.map +1 -1
- package/transport/adapters/transport.streamable-http.adapter.d.ts.map +1 -1
- package/transport/event-stores/index.d.ts +2 -0
- package/transport/event-stores/index.d.ts.map +1 -1
- package/transport/event-stores/memory.event-store.d.ts +9 -2
- package/transport/event-stores/memory.event-store.d.ts.map +1 -1
- package/transport/event-stores/redis.event-store.d.ts +24 -3
- package/transport/event-stores/redis.event-store.d.ts.map +1 -1
- package/transport/event-stores/session-scoped.event-store.d.ts +17 -0
- package/transport/event-stores/session-scoped.event-store.d.ts.map +1 -0
- package/transport/web-fetch-handler.d.ts.map +1 -1
- package/workflow/tools/execute-workflow.tool.d.ts.map +1 -1
- package/workflow/tools/get-workflow-status.tool.d.ts +12 -0
- package/workflow/tools/get-workflow-status.tool.d.ts.map +1 -1
- package/workflow/tools/list-workflows.tool.d.ts +1 -0
- package/workflow/tools/list-workflows.tool.d.ts.map +1 -1
- package/workflow/tools/register-workflow.tool.d.ts +1 -0
- package/workflow/tools/register-workflow.tool.d.ts.map +1 -1
- package/workflow/tools/remove-workflow.tool.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -1863,14 +1863,19 @@ var init_schema3 = __esm({
|
|
|
1863
1863
|
socketPath: import_lazy_zod8.z.string().optional(),
|
|
1864
1864
|
/**
|
|
1865
1865
|
* CORS configuration.
|
|
1866
|
-
* - undefined (default):
|
|
1867
|
-
*
|
|
1866
|
+
* - undefined (default): NO CORS headers — same-origin only. A browser will not let another
|
|
1867
|
+
* origin read the response. Set `{ origin: true }` for the old permissive behaviour.
|
|
1868
|
+
* - false: CORS disabled (same effect as the default; kept for explicitness)
|
|
1868
1869
|
* - CorsOptions object: custom CORS config
|
|
1869
1870
|
*/
|
|
1870
1871
|
cors: import_lazy_zod8.z.union([import_lazy_zod8.z.literal(false), corsOptionsSchema]).optional(),
|
|
1871
1872
|
/**
|
|
1872
1873
|
* Security configuration for transport hardening.
|
|
1873
|
-
*
|
|
1874
|
+
*
|
|
1875
|
+
* The defaults are the SAFE choice: the server binds loopback and sends no CORS headers. Reaching
|
|
1876
|
+
* it from another host or another origin is something you opt into — `bindAddress: 'all'`, the
|
|
1877
|
+
* `FRONTMCP_BIND_ADDRESS=all` env var (no rebuild; the right fit for a Dockerfile), or a
|
|
1878
|
+
* distributed build — and an explicit `cors` config.
|
|
1874
1879
|
*/
|
|
1875
1880
|
security: import_lazy_zod8.z.object({
|
|
1876
1881
|
strict: import_lazy_zod8.z.boolean().optional(),
|
|
@@ -2304,7 +2309,10 @@ var init_schema7 = __esm({
|
|
|
2304
2309
|
});
|
|
2305
2310
|
eventStoreConfigSchema = import_lazy_zod12.z.object({
|
|
2306
2311
|
enabled: import_lazy_zod12.z.boolean().default(false),
|
|
2307
|
-
|
|
2312
|
+
// 'sqlite' is supported by createEventStore and the TS interface but was
|
|
2313
|
+
// missing from this enum, so a valid `provider: 'sqlite'` config failed
|
|
2314
|
+
// validation.
|
|
2315
|
+
provider: import_lazy_zod12.z.enum(["memory", "redis", "sqlite"]).optional().default("memory"),
|
|
2308
2316
|
maxEvents: import_lazy_zod12.z.number().int().positive().optional().default(1e4),
|
|
2309
2317
|
ttlMs: import_lazy_zod12.z.number().int().positive().optional().default(3e5),
|
|
2310
2318
|
redis: redisOptionsSchema.optional()
|
|
@@ -4033,7 +4041,7 @@ function formatToolsForPlatform(tools, platform) {
|
|
|
4033
4041
|
})
|
|
4034
4042
|
);
|
|
4035
4043
|
case "vercel-ai": {
|
|
4036
|
-
const result =
|
|
4044
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
4037
4045
|
for (const tool of tools) {
|
|
4038
4046
|
result[tool.name] = {
|
|
4039
4047
|
description: tool.description ?? `Execute ${tool.name}`,
|
|
@@ -5571,6 +5579,32 @@ var init_sdk_errors = __esm({
|
|
|
5571
5579
|
}
|
|
5572
5580
|
});
|
|
5573
5581
|
|
|
5582
|
+
// libs/sdk/src/errors/job.errors.ts
|
|
5583
|
+
var JobNotAuthorizedError, DynamicJobRegistrationDisabledError;
|
|
5584
|
+
var init_job_errors = __esm({
|
|
5585
|
+
"libs/sdk/src/errors/job.errors.ts"() {
|
|
5586
|
+
"use strict";
|
|
5587
|
+
init_mcp_error();
|
|
5588
|
+
JobNotAuthorizedError = class extends PublicMcpError {
|
|
5589
|
+
/** The requested name, for internal logs only — never surfaced publicly. */
|
|
5590
|
+
requestedName;
|
|
5591
|
+
constructor(requestedName) {
|
|
5592
|
+
super(`Job or workflow "${requestedName}" not found or not permitted`, "JOB_NOT_AUTHORIZED", 403);
|
|
5593
|
+
this.requestedName = requestedName;
|
|
5594
|
+
}
|
|
5595
|
+
};
|
|
5596
|
+
DynamicJobRegistrationDisabledError = class extends PublicMcpError {
|
|
5597
|
+
constructor(kind = "job") {
|
|
5598
|
+
super(
|
|
5599
|
+
`Dynamic ${kind} registration is disabled. Enable it with jobs: { allowDynamicRegistration: true }.`,
|
|
5600
|
+
"DYNAMIC_REGISTRATION_DISABLED",
|
|
5601
|
+
403
|
|
5602
|
+
);
|
|
5603
|
+
}
|
|
5604
|
+
};
|
|
5605
|
+
}
|
|
5606
|
+
});
|
|
5607
|
+
|
|
5574
5608
|
// libs/sdk/src/errors/index.ts
|
|
5575
5609
|
var errors_exports = {};
|
|
5576
5610
|
__export(errors_exports, {
|
|
@@ -5596,6 +5630,7 @@ __export(errors_exports, {
|
|
|
5596
5630
|
DependencyNotFoundError: () => DependencyNotFoundError,
|
|
5597
5631
|
DynamicAdapterNameError: () => DynamicAdapterNameError,
|
|
5598
5632
|
DynamicJobDirectExecutionError: () => DynamicJobDirectExecutionError,
|
|
5633
|
+
DynamicJobRegistrationDisabledError: () => DynamicJobRegistrationDisabledError,
|
|
5599
5634
|
ElicitationDisabledError: () => ElicitationDisabledError,
|
|
5600
5635
|
ElicitationEncryptionError: () => ElicitationEncryptionError,
|
|
5601
5636
|
ElicitationFallbackRequired: () => ElicitationFallbackRequired,
|
|
@@ -5644,6 +5679,7 @@ __export(errors_exports, {
|
|
|
5644
5679
|
InvalidUseFactoryError: () => InvalidUseFactoryError,
|
|
5645
5680
|
InvalidUseValueError: () => InvalidUseValueError,
|
|
5646
5681
|
InvokeStateMissingKeyError: () => InvokeStateMissingKeyError,
|
|
5682
|
+
JobNotAuthorizedError: () => JobNotAuthorizedError,
|
|
5647
5683
|
MCP_ERROR_CODES: () => MCP_ERROR_CODES,
|
|
5648
5684
|
McpError: () => McpError,
|
|
5649
5685
|
MethodNotImplementedError: () => MethodNotImplementedError,
|
|
@@ -5766,6 +5802,7 @@ var init_errors = __esm({
|
|
|
5766
5802
|
init_workflow_errors();
|
|
5767
5803
|
init_esm_errors();
|
|
5768
5804
|
init_sdk_errors();
|
|
5805
|
+
init_job_errors();
|
|
5769
5806
|
}
|
|
5770
5807
|
});
|
|
5771
5808
|
|
|
@@ -7088,6 +7125,10 @@ var require_dist = __commonJS({
|
|
|
7088
7125
|
}
|
|
7089
7126
|
return id;
|
|
7090
7127
|
}
|
|
7128
|
+
async getStreamIdForEventId(eventId) {
|
|
7129
|
+
const row = this.prepared().getStreamId.get(eventId);
|
|
7130
|
+
return row?.stream_id;
|
|
7131
|
+
}
|
|
7091
7132
|
async replayEventsAfter(lastEventId, { send }) {
|
|
7092
7133
|
const stmts = this.prepared();
|
|
7093
7134
|
const streamRow = stmts.getStreamId.get(lastEventId);
|
|
@@ -14010,22 +14051,38 @@ async function loadYamlConfig(basePath, configPath) {
|
|
|
14010
14051
|
}
|
|
14011
14052
|
return {};
|
|
14012
14053
|
}
|
|
14054
|
+
function stripUnsafeKeys(value) {
|
|
14055
|
+
if (Array.isArray(value)) return value.map(stripUnsafeKeys);
|
|
14056
|
+
if (!isPlainObject(value)) return value;
|
|
14057
|
+
const clean = {};
|
|
14058
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
14059
|
+
if (UNSAFE_MERGE_KEYS.has(key)) continue;
|
|
14060
|
+
clean[key] = stripUnsafeKeys(nested);
|
|
14061
|
+
}
|
|
14062
|
+
return clean;
|
|
14063
|
+
}
|
|
14064
|
+
function isPlainObject(value) {
|
|
14065
|
+
if (!value || typeof value !== "object") return false;
|
|
14066
|
+
const proto = Object.getPrototypeOf(value);
|
|
14067
|
+
return proto === Object.prototype || proto === null;
|
|
14068
|
+
}
|
|
14013
14069
|
function deepMerge(target, source) {
|
|
14014
14070
|
const result = { ...target };
|
|
14015
14071
|
for (const key in source) {
|
|
14072
|
+
if (UNSAFE_MERGE_KEYS.has(key)) continue;
|
|
14016
14073
|
const sourceVal = source[key];
|
|
14017
14074
|
const targetVal = result[key];
|
|
14018
14075
|
if (sourceVal !== void 0 && sourceVal !== null) {
|
|
14019
14076
|
if (typeof sourceVal === "object" && !Array.isArray(sourceVal) && typeof targetVal === "object" && !Array.isArray(targetVal) && targetVal !== null) {
|
|
14020
14077
|
result[key] = deepMerge(targetVal, sourceVal);
|
|
14021
14078
|
} else {
|
|
14022
|
-
result[key] = sourceVal;
|
|
14079
|
+
result[key] = stripUnsafeKeys(sourceVal);
|
|
14023
14080
|
}
|
|
14024
14081
|
}
|
|
14025
14082
|
}
|
|
14026
14083
|
return result;
|
|
14027
14084
|
}
|
|
14028
|
-
var yaml2, import_utils32;
|
|
14085
|
+
var yaml2, import_utils32, UNSAFE_MERGE_KEYS;
|
|
14029
14086
|
var init_config_loader = __esm({
|
|
14030
14087
|
"libs/sdk/src/builtin/config/providers/config-loader.ts"() {
|
|
14031
14088
|
"use strict";
|
|
@@ -14033,6 +14090,7 @@ var init_config_loader = __esm({
|
|
|
14033
14090
|
import_utils32 = require("@frontmcp/utils");
|
|
14034
14091
|
init_config_service();
|
|
14035
14092
|
init_env_loader();
|
|
14093
|
+
UNSAFE_MERGE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
14036
14094
|
}
|
|
14037
14095
|
});
|
|
14038
14096
|
|
|
@@ -20699,10 +20757,11 @@ var init_tool_credentials_gate = __esm({
|
|
|
20699
20757
|
});
|
|
20700
20758
|
|
|
20701
20759
|
// libs/sdk/src/tool/flows/call-tool.flow.ts
|
|
20702
|
-
var import_guard4, import_lazy_zod56, import_protocol38, import_adapters4, import_utils45, inputSchema13, outputSchema12, stateSchema12, plan12, name12, Stage12, CallToolFlow;
|
|
20760
|
+
var import_auth17, import_guard4, import_lazy_zod56, import_protocol38, import_adapters4, import_utils45, inputSchema13, outputSchema12, stateSchema12, plan12, name12, Stage12, CallToolFlow;
|
|
20703
20761
|
var init_call_tool_flow = __esm({
|
|
20704
20762
|
"libs/sdk/src/tool/flows/call-tool.flow.ts"() {
|
|
20705
20763
|
"use strict";
|
|
20764
|
+
import_auth17 = require("@frontmcp/auth");
|
|
20706
20765
|
import_guard4 = require("@frontmcp/guard");
|
|
20707
20766
|
import_lazy_zod56 = require("@frontmcp/lazy-zod");
|
|
20708
20767
|
import_protocol38 = require("@frontmcp/protocol");
|
|
@@ -21075,7 +21134,7 @@ var init_call_tool_flow = __esm({
|
|
|
21075
21134
|
this.logger.verbose(`checkToolAuthorization: using anonymous for ${appId}`);
|
|
21076
21135
|
return;
|
|
21077
21136
|
}
|
|
21078
|
-
const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "");
|
|
21137
|
+
const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "", authInfo, claimAuthorizedApps);
|
|
21079
21138
|
this.logger.info(`checkToolAuthorization: authorization required for app "${appId}"`);
|
|
21080
21139
|
throw new AuthorizationRequiredError({
|
|
21081
21140
|
appId,
|
|
@@ -21087,11 +21146,51 @@ var init_call_tool_flow = __esm({
|
|
|
21087
21146
|
this.logger.verbose("checkToolAuthorization:done");
|
|
21088
21147
|
}
|
|
21089
21148
|
/**
|
|
21090
|
-
* Build the URL for progressive/incremental authorization
|
|
21149
|
+
* Build the URL for progressive/incremental authorization.
|
|
21150
|
+
*
|
|
21151
|
+
* This is the ONE point in the request lifecycle where the caller's identity
|
|
21152
|
+
* is both known and verified, so it is where the incremental-authorization
|
|
21153
|
+
* ticket is minted (GHSA-2c4g-9c8x-6m8g). The ticket is what later authorizes
|
|
21154
|
+
* `/oauth/authorize` to skip the login step; `app`/`tool` are carried purely
|
|
21155
|
+
* for display and are re-read from the signed payload server-side.
|
|
21156
|
+
*
|
|
21157
|
+
* When no verified subject or signing secret is available the URL is emitted
|
|
21158
|
+
* WITHOUT a ticket — the user then completes an ordinary login, which is the
|
|
21159
|
+
* correct fail-closed outcome rather than a credential-free shortcut.
|
|
21091
21160
|
*/
|
|
21092
|
-
buildProgressiveAuthUrl(appId, toolId) {
|
|
21161
|
+
buildProgressiveAuthUrl(appId, toolId, authInfo, priorAppIds) {
|
|
21093
21162
|
const baseUrl = this.scope.fullPath || "";
|
|
21094
|
-
|
|
21163
|
+
const url = new URL(`${baseUrl}/oauth/authorize`, "http://placeholder.invalid");
|
|
21164
|
+
url.searchParams.set("app", appId);
|
|
21165
|
+
url.searchParams.set("tool", toolId);
|
|
21166
|
+
const ticket = this.mintIncrementalTicket(appId, toolId, authInfo, priorAppIds);
|
|
21167
|
+
if (ticket) {
|
|
21168
|
+
url.searchParams.set("ticket", ticket);
|
|
21169
|
+
} else {
|
|
21170
|
+
this.logger.verbose("buildProgressiveAuthUrl: no incremental ticket minted \u2014 link requires a full login");
|
|
21171
|
+
}
|
|
21172
|
+
return baseUrl.startsWith("http") ? url.toString() : `${url.pathname}${url.search}`;
|
|
21173
|
+
}
|
|
21174
|
+
/**
|
|
21175
|
+
* Mint a single-use, short-lived incremental-authorization ticket for the
|
|
21176
|
+
* verified caller. Returns undefined when the scope has no local signing
|
|
21177
|
+
* secret or the request has no verified subject.
|
|
21178
|
+
*/
|
|
21179
|
+
mintIncrementalTicket(appId, toolId, authInfo, priorAppIds) {
|
|
21180
|
+
const extraUser = authInfo?.extra?.user;
|
|
21181
|
+
const sub = extraUser?.sub ?? authInfo?.user?.sub;
|
|
21182
|
+
if (!sub) return void 0;
|
|
21183
|
+
const secret = this.scope.auth?.signingSecret;
|
|
21184
|
+
if (!secret) return void 0;
|
|
21185
|
+
return (0, import_auth17.signIncrementalAuthTicket)(
|
|
21186
|
+
{
|
|
21187
|
+
sub,
|
|
21188
|
+
appId,
|
|
21189
|
+
toolId,
|
|
21190
|
+
...priorAppIds && priorAppIds.size > 0 ? { priorAppIds: [...priorAppIds] } : {}
|
|
21191
|
+
},
|
|
21192
|
+
secret
|
|
21193
|
+
);
|
|
21095
21194
|
}
|
|
21096
21195
|
async checkEntryAuthorities() {
|
|
21097
21196
|
this.logger.verbose("checkEntryAuthorities:start");
|
|
@@ -35572,25 +35671,25 @@ var init_auth_utils = __esm({
|
|
|
35572
35671
|
});
|
|
35573
35672
|
|
|
35574
35673
|
// libs/sdk/src/auth/authorization/orchestrated.context-extension.ts
|
|
35575
|
-
var
|
|
35674
|
+
var import_auth18, orchestratedAuthContextExtension;
|
|
35576
35675
|
var init_orchestrated_context_extension = __esm({
|
|
35577
35676
|
"libs/sdk/src/auth/authorization/orchestrated.context-extension.ts"() {
|
|
35578
35677
|
"use strict";
|
|
35579
|
-
|
|
35678
|
+
import_auth18 = require("@frontmcp/auth");
|
|
35580
35679
|
orchestratedAuthContextExtension = {
|
|
35581
35680
|
property: "orchestration",
|
|
35582
|
-
token:
|
|
35681
|
+
token: import_auth18.ORCHESTRATED_AUTH_ACCESSOR,
|
|
35583
35682
|
errorMessage: "Orchestrated authorization not available. Ensure your server is configured with orchestrated mode and the user has completed authentication."
|
|
35584
35683
|
};
|
|
35585
35684
|
}
|
|
35586
35685
|
});
|
|
35587
35686
|
|
|
35588
35687
|
// libs/sdk/src/auth/cimd/index.ts
|
|
35589
|
-
var
|
|
35688
|
+
var import_auth19;
|
|
35590
35689
|
var init_cimd = __esm({
|
|
35591
35690
|
"libs/sdk/src/auth/cimd/index.ts"() {
|
|
35592
35691
|
"use strict";
|
|
35593
|
-
|
|
35692
|
+
import_auth19 = require("@frontmcp/auth");
|
|
35594
35693
|
}
|
|
35595
35694
|
});
|
|
35596
35695
|
|
|
@@ -35612,9 +35711,9 @@ function createCredentialsProviders(opts) {
|
|
|
35612
35711
|
name: "SessionCredentialVault"
|
|
35613
35712
|
};
|
|
35614
35713
|
const accessorProvider = {
|
|
35615
|
-
provide:
|
|
35714
|
+
provide: import_auth20.CREDENTIALS_ACCESSOR,
|
|
35616
35715
|
inject: () => [FRONTMCP_CONTEXT, FrontMcpLogger],
|
|
35617
|
-
useFactory: (ctx, logger) => new
|
|
35716
|
+
useFactory: (ctx, logger) => new import_auth20.CredentialsAccessorImpl({
|
|
35618
35717
|
vault,
|
|
35619
35718
|
resolveSub: () => resolveRequestSub(ctx),
|
|
35620
35719
|
signingSecret,
|
|
@@ -35627,11 +35726,11 @@ function createCredentialsProviders(opts) {
|
|
|
35627
35726
|
};
|
|
35628
35727
|
return [vaultProvider, accessorProvider];
|
|
35629
35728
|
}
|
|
35630
|
-
var
|
|
35729
|
+
var import_auth20, import_di31, SESSION_CREDENTIAL_VAULT;
|
|
35631
35730
|
var init_credentials_providers = __esm({
|
|
35632
35731
|
"libs/sdk/src/auth/credentials/credentials.providers.ts"() {
|
|
35633
35732
|
"use strict";
|
|
35634
|
-
|
|
35733
|
+
import_auth20 = require("@frontmcp/auth");
|
|
35635
35734
|
import_di31 = require("@frontmcp/di");
|
|
35636
35735
|
init_common();
|
|
35637
35736
|
init_frontmcp_context_provider();
|
|
@@ -35642,14 +35741,14 @@ var init_credentials_providers = __esm({
|
|
|
35642
35741
|
});
|
|
35643
35742
|
|
|
35644
35743
|
// libs/sdk/src/auth/credentials/credentials.context-extension.ts
|
|
35645
|
-
var
|
|
35744
|
+
var import_auth21, credentialsContextExtension;
|
|
35646
35745
|
var init_credentials_context_extension = __esm({
|
|
35647
35746
|
"libs/sdk/src/auth/credentials/credentials.context-extension.ts"() {
|
|
35648
35747
|
"use strict";
|
|
35649
|
-
|
|
35748
|
+
import_auth21 = require("@frontmcp/auth");
|
|
35650
35749
|
credentialsContextExtension = {
|
|
35651
35750
|
property: "credentials",
|
|
35652
|
-
token:
|
|
35751
|
+
token: import_auth21.CREDENTIALS_ACCESSOR,
|
|
35653
35752
|
errorMessage: "Per-session credentials are not available. Ensure the server uses auth mode 'local' (or 'remote'); credentials are persisted by a local authenticate() verifier."
|
|
35654
35753
|
};
|
|
35655
35754
|
}
|
|
@@ -35818,11 +35917,11 @@ function getConfiguredProviders(auth) {
|
|
|
35818
35917
|
function getConfiguredProviderIds(auth) {
|
|
35819
35918
|
return getConfiguredProviders(auth).map((p) => p.id);
|
|
35820
35919
|
}
|
|
35821
|
-
var
|
|
35920
|
+
var import_auth22, import_lazy_zod64, import_utils66, pkceChallengeSchema, codeChallengeMethodSchema, responseTypeSchema, safeRedirectUriSchema, oauthAuthorizeRequestSchema, anonymousAuthorizeRequestSchema, inputSchema16, stateSchema15, outputSchema15, plan15, name15, Stage15, OauthAuthorizeFlow;
|
|
35822
35921
|
var init_oauth_authorize_flow = __esm({
|
|
35823
35922
|
"libs/sdk/src/auth/flows/oauth.authorize.flow.ts"() {
|
|
35824
35923
|
"use strict";
|
|
35825
|
-
|
|
35924
|
+
import_auth22 = require("@frontmcp/auth");
|
|
35826
35925
|
import_lazy_zod64 = require("@frontmcp/lazy-zod");
|
|
35827
35926
|
import_utils66 = require("@frontmcp/utils");
|
|
35828
35927
|
init_common();
|
|
@@ -35880,6 +35979,13 @@ var init_oauth_authorize_flow = __esm({
|
|
|
35880
35979
|
targetAppId: import_lazy_zod64.z.string().optional().describe("Target app ID for incremental authorization"),
|
|
35881
35980
|
targetToolId: import_lazy_zod64.z.string().optional().describe("Target tool ID that triggered the incremental auth"),
|
|
35882
35981
|
existingSessionId: import_lazy_zod64.z.string().optional().describe("Existing session ID for incremental auth"),
|
|
35982
|
+
/**
|
|
35983
|
+
* The VERIFIED incremental-authorization ticket (GHSA-2c4g-9c8x-6m8g). Set
|
|
35984
|
+
* only when `?ticket=` carried a valid, unexpired server signature. Its `sub`
|
|
35985
|
+
* is the proven identity the expanded grant is minted for; `isIncrementalAuth`
|
|
35986
|
+
* is derived from its presence, never from a raw query parameter.
|
|
35987
|
+
*/
|
|
35988
|
+
incrementalTicket: import_lazy_zod64.z.custom().optional(),
|
|
35883
35989
|
/**
|
|
35884
35990
|
* Apps the client already holds a grant for (the prior `authorized_apps`
|
|
35885
35991
|
* claim), carried forward on an incremental authorize so the newly-minted
|
|
@@ -35893,7 +35999,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
35893
35999
|
requiresConsent: import_lazy_zod64.z.boolean().default(false).describe("Whether consent flow is enabled"),
|
|
35894
36000
|
// CIMD (Client ID Metadata Documents)
|
|
35895
36001
|
isCimdClient: import_lazy_zod64.z.boolean().default(false).describe("Whether client_id is a CIMD URL"),
|
|
35896
|
-
cimdMetadata:
|
|
36002
|
+
cimdMetadata: import_auth19.clientMetadataDocumentSchema.optional().describe("CIMD metadata document for the client")
|
|
35897
36003
|
});
|
|
35898
36004
|
outputSchema15 = import_lazy_zod64.z.union([
|
|
35899
36005
|
HttpRedirectSchema,
|
|
@@ -35932,18 +36038,21 @@ var init_oauth_authorize_flow = __esm({
|
|
|
35932
36038
|
const { request } = this.rawInput;
|
|
35933
36039
|
const rawRedirectUri = request.query["redirect_uri"];
|
|
35934
36040
|
const rawState = request.query["state"];
|
|
35935
|
-
const
|
|
35936
|
-
const
|
|
35937
|
-
const
|
|
35938
|
-
const
|
|
35939
|
-
const isIncrementalAuth = mode === "incremental" || !!targetAppId;
|
|
35940
|
-
const appsParam = request.query["apps"];
|
|
36041
|
+
const incrementalTicket = this.verifyIncrementalTicket(request.query["ticket"]);
|
|
36042
|
+
const isIncrementalAuth = incrementalTicket !== void 0;
|
|
36043
|
+
const targetAppId = incrementalTicket?.appId;
|
|
36044
|
+
const targetToolId = incrementalTicket?.toolId;
|
|
35941
36045
|
let priorAuthorizedAppIds;
|
|
35942
|
-
if (
|
|
35943
|
-
|
|
35944
|
-
|
|
35945
|
-
|
|
36046
|
+
if (incrementalTicket) {
|
|
36047
|
+
priorAuthorizedAppIds = incrementalTicket.priorAppIds && incrementalTicket.priorAppIds.length > 0 ? Array.from(new Set(incrementalTicket.priorAppIds)) : void 0;
|
|
36048
|
+
} else {
|
|
36049
|
+
const appsParam = request.query["apps"];
|
|
36050
|
+
if (appsParam) {
|
|
36051
|
+
const ids = (Array.isArray(appsParam) ? appsParam : [appsParam]).flatMap((v) => String(v).split(",")).map((v) => v.trim()).filter(Boolean);
|
|
36052
|
+
priorAuthorizedAppIds = ids.length > 0 ? Array.from(new Set(ids)) : void 0;
|
|
36053
|
+
}
|
|
35946
36054
|
}
|
|
36055
|
+
const existingSessionId = request.query["session_id"];
|
|
35947
36056
|
const isDefaultAuthProvider = !metadata.auth;
|
|
35948
36057
|
let requiresFederatedLogin = false;
|
|
35949
36058
|
if (metadata.auth && (0, import_auth10.isOrchestratedMode)(metadata.auth)) {
|
|
@@ -35969,6 +36078,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
35969
36078
|
targetToolId,
|
|
35970
36079
|
existingSessionId,
|
|
35971
36080
|
priorAuthorizedAppIds,
|
|
36081
|
+
incrementalTicket,
|
|
35972
36082
|
// Federated Login
|
|
35973
36083
|
requiresFederatedLogin,
|
|
35974
36084
|
// Consent Flow
|
|
@@ -36040,7 +36150,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36040
36150
|
}
|
|
36041
36151
|
}
|
|
36042
36152
|
const { client_id, redirect_uri } = result.data;
|
|
36043
|
-
const cimdService = this.get(
|
|
36153
|
+
const cimdService = this.get(import_auth19.CimdService);
|
|
36044
36154
|
const isCimdClientId2 = !!cimdService?.enabled && cimdService.isCimdClientId(client_id);
|
|
36045
36155
|
const dcrError = this.checkDcrAllowlist(client_id, redirect_uri, isCimdClientId2);
|
|
36046
36156
|
if (dcrError) {
|
|
@@ -36094,6 +36204,28 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36094
36204
|
}
|
|
36095
36205
|
}
|
|
36096
36206
|
}
|
|
36207
|
+
/**
|
|
36208
|
+
* Verify a client-supplied incremental-authorization ticket.
|
|
36209
|
+
*
|
|
36210
|
+
* Returns the verified payload, or `undefined` when the parameter is absent,
|
|
36211
|
+
* malformed, expired, or not signed by this server. Verification is
|
|
36212
|
+
* side-effect free — the ticket is CLAIMED (single use) later, in
|
|
36213
|
+
* {@link prepareAuthorizationRequest}, so a request that fails OAuth
|
|
36214
|
+
* validation does not burn the user's ticket.
|
|
36215
|
+
*/
|
|
36216
|
+
verifyIncrementalTicket(raw) {
|
|
36217
|
+
const token = typeof raw === "string" ? raw : Array.isArray(raw) ? raw[0] : void 0;
|
|
36218
|
+
if (typeof token !== "string" || token.length === 0) return void 0;
|
|
36219
|
+
const localAuth = this.scope.auth;
|
|
36220
|
+
const secret = localAuth?.signingSecret;
|
|
36221
|
+
if (!secret) return void 0;
|
|
36222
|
+
const payload = (0, import_auth22.verifyIncrementalAuthTicket)(token, secret);
|
|
36223
|
+
if (!payload) {
|
|
36224
|
+
this.logger.warn("Incremental auth ticket failed verification \u2014 treating as an ordinary login");
|
|
36225
|
+
return void 0;
|
|
36226
|
+
}
|
|
36227
|
+
return payload;
|
|
36228
|
+
}
|
|
36097
36229
|
async checkIfAuthorized() {
|
|
36098
36230
|
}
|
|
36099
36231
|
async prepareAuthorizationRequest() {
|
|
@@ -36105,7 +36237,8 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36105
36237
|
existingSessionId,
|
|
36106
36238
|
priorAuthorizedAppIds,
|
|
36107
36239
|
requiresFederatedLogin,
|
|
36108
|
-
requiresConsent
|
|
36240
|
+
requiresConsent,
|
|
36241
|
+
incrementalTicket
|
|
36109
36242
|
} = this.state;
|
|
36110
36243
|
const { metadata } = this.scope;
|
|
36111
36244
|
if (!validatedRequest) {
|
|
@@ -36118,6 +36251,18 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36118
36251
|
}
|
|
36119
36252
|
const localAuth = auth;
|
|
36120
36253
|
const store = localAuth.authorizationStore;
|
|
36254
|
+
let effectiveIncremental = isIncrementalAuth;
|
|
36255
|
+
if (incrementalTicket) {
|
|
36256
|
+
const claimed = await localAuth.claimIncrementalTicket(
|
|
36257
|
+
incrementalTicket.jti,
|
|
36258
|
+
Math.max(0, incrementalTicket.exp - Date.now())
|
|
36259
|
+
);
|
|
36260
|
+
if (!claimed) {
|
|
36261
|
+
this.logger.warn("Incremental auth ticket already used \u2014 falling back to an ordinary login");
|
|
36262
|
+
effectiveIncremental = false;
|
|
36263
|
+
this.state.set({ isIncrementalAuth: false, targetAppId: void 0, targetToolId: void 0 });
|
|
36264
|
+
}
|
|
36265
|
+
}
|
|
36121
36266
|
let federatedLogin;
|
|
36122
36267
|
if (requiresFederatedLogin) {
|
|
36123
36268
|
const apps = this.scope.apps.getApps();
|
|
@@ -36165,10 +36310,14 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36165
36310
|
},
|
|
36166
36311
|
state: validatedRequest.state,
|
|
36167
36312
|
resource: validatedRequest.resource,
|
|
36168
|
-
// Progressive/Incremental Authorization Fields
|
|
36169
|
-
|
|
36170
|
-
|
|
36171
|
-
|
|
36313
|
+
// Progressive/Incremental Authorization Fields. `isIncremental` reflects a
|
|
36314
|
+
// CLAIMED, server-signed ticket; `incrementalSub` is the identity that
|
|
36315
|
+
// ticket proved, and is the only subject the callback may mint for when
|
|
36316
|
+
// it skips the credential gate.
|
|
36317
|
+
isIncremental: effectiveIncremental,
|
|
36318
|
+
incrementalSub: effectiveIncremental ? incrementalTicket?.sub : void 0,
|
|
36319
|
+
targetAppId: effectiveIncremental ? targetAppId : void 0,
|
|
36320
|
+
targetToolId: effectiveIncremental ? targetToolId : void 0,
|
|
36172
36321
|
existingSessionId,
|
|
36173
36322
|
priorAuthorizedAppIds,
|
|
36174
36323
|
// Federated Login State
|
|
@@ -36178,7 +36327,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36178
36327
|
});
|
|
36179
36328
|
await localAuth.authorizationStore.storePendingAuthorization(pendingRecord);
|
|
36180
36329
|
this.logger.info(
|
|
36181
|
-
`Pending authorization created: ${pendingRecord.id}${
|
|
36330
|
+
`Pending authorization created: ${pendingRecord.id}${effectiveIncremental ? ` (incremental for app: ${targetAppId})` : ""}${requiresFederatedLogin ? " (federated)" : ""}${requiresConsent ? " (consent enabled)" : ""}`
|
|
36182
36331
|
);
|
|
36183
36332
|
this.state.set("pendingAuthId", pendingRecord.id);
|
|
36184
36333
|
}
|
|
@@ -36399,7 +36548,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36399
36548
|
);
|
|
36400
36549
|
return;
|
|
36401
36550
|
}
|
|
36402
|
-
const session = (0,
|
|
36551
|
+
const session = (0, import_auth22.createFederatedAuthSession)({
|
|
36403
36552
|
pendingAuthId,
|
|
36404
36553
|
clientId: validatedRequest.client_id,
|
|
36405
36554
|
redirectUri: validatedRequest.redirect_uri,
|
|
@@ -36414,7 +36563,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36414
36563
|
const challenge = (0, import_utils66.sha256Base64url)(verifier);
|
|
36415
36564
|
const pkce = { verifier, challenge, method: "S256" };
|
|
36416
36565
|
const providerState = `federated:${session.id}:${(0, import_utils66.randomUUID)()}`;
|
|
36417
|
-
(0,
|
|
36566
|
+
(0, import_auth22.startNextProvider)(session, pkce, providerState);
|
|
36418
36567
|
await localAuth.federatedSessionStore.store(session);
|
|
36419
36568
|
const redirectUrl = await localAuth.buildProviderAuthorizeUrl(providerId, {
|
|
36420
36569
|
state: providerState,
|
|
@@ -36515,7 +36664,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36515
36664
|
callbackPath,
|
|
36516
36665
|
fields: login?.fields ?? {}
|
|
36517
36666
|
};
|
|
36518
|
-
return (0,
|
|
36667
|
+
return (0, import_auth22.renderLocalLoginPage)(login, ctx);
|
|
36519
36668
|
}
|
|
36520
36669
|
/**
|
|
36521
36670
|
* Render incremental authorization page for a single app using HTMX templates
|
|
@@ -36528,7 +36677,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36528
36677
|
appName,
|
|
36529
36678
|
description: appDescription
|
|
36530
36679
|
};
|
|
36531
|
-
return (0,
|
|
36680
|
+
return (0, import_auth22.buildIncrementalAuthPage)({
|
|
36532
36681
|
app,
|
|
36533
36682
|
toolId: toolId || "unknown tool",
|
|
36534
36683
|
sessionHint: pendingAuthId,
|
|
@@ -36549,7 +36698,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36549
36698
|
appIds: provider.appIds.filter((id) => id !== "__parent__"),
|
|
36550
36699
|
isPrimary: provider.isParentProvider
|
|
36551
36700
|
}));
|
|
36552
|
-
return (0,
|
|
36701
|
+
return (0, import_auth22.buildFederatedLoginPage)({
|
|
36553
36702
|
providers,
|
|
36554
36703
|
clientName: clientId,
|
|
36555
36704
|
pendingAuthId,
|
|
@@ -36594,8 +36743,8 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36594
36743
|
<div class="error-container">
|
|
36595
36744
|
<div class="error-icon">\u26A0\uFE0F</div>
|
|
36596
36745
|
<h1>Authorization Error</h1>
|
|
36597
|
-
<p><span class="error-code">${(0,
|
|
36598
|
-
<p>${(0,
|
|
36746
|
+
<p><span class="error-code">${(0, import_auth22.escapeHtml)(error)}</span></p>
|
|
36747
|
+
<p>${(0, import_auth22.escapeHtml)(description)}</p>
|
|
36599
36748
|
</div>
|
|
36600
36749
|
</body>
|
|
36601
36750
|
</html>`;
|
|
@@ -36644,11 +36793,11 @@ function timingSafeEqualStr3(a, b) {
|
|
|
36644
36793
|
}
|
|
36645
36794
|
return diff === 0;
|
|
36646
36795
|
}
|
|
36647
|
-
var
|
|
36796
|
+
var import_auth23, import_lazy_zod65, import_utils67, inputSchema17, stateSchema16, outputSchema16, plan16, name16, Stage16, OauthCallbackFlow;
|
|
36648
36797
|
var init_oauth_callback_flow = __esm({
|
|
36649
36798
|
"libs/sdk/src/auth/flows/oauth.callback.flow.ts"() {
|
|
36650
36799
|
"use strict";
|
|
36651
|
-
|
|
36800
|
+
import_auth23 = require("@frontmcp/auth");
|
|
36652
36801
|
import_lazy_zod65 = require("@frontmcp/lazy-zod");
|
|
36653
36802
|
import_utils67 = require("@frontmcp/utils");
|
|
36654
36803
|
init_common();
|
|
@@ -36739,8 +36888,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36739
36888
|
const email = request.query["email"];
|
|
36740
36889
|
const name42 = request.query["name"];
|
|
36741
36890
|
const loginFields = this.collectLoginFields(request);
|
|
36742
|
-
const isIncremental = request.query["incremental"] === "true";
|
|
36743
|
-
const targetAppId = request.query["app_id"];
|
|
36744
36891
|
const isFederated = request.query["federated"] === "true";
|
|
36745
36892
|
const providersParam = request.query["providers"];
|
|
36746
36893
|
let selectedProviders;
|
|
@@ -36759,8 +36906,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36759
36906
|
pendingAuthId,
|
|
36760
36907
|
email,
|
|
36761
36908
|
name: name42,
|
|
36762
|
-
isIncremental,
|
|
36763
|
-
targetAppId,
|
|
36764
36909
|
isFederated,
|
|
36765
36910
|
selectedProviders,
|
|
36766
36911
|
selectedTools,
|
|
@@ -36768,9 +36913,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36768
36913
|
loginFields,
|
|
36769
36914
|
csrf
|
|
36770
36915
|
});
|
|
36771
|
-
if (isIncremental) {
|
|
36772
|
-
this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
|
|
36773
|
-
}
|
|
36774
36916
|
if (isFederated) {
|
|
36775
36917
|
this.logger.info(`Federated login callback with ${selectedProviders?.length || 0} selected providers`);
|
|
36776
36918
|
}
|
|
@@ -36779,31 +36921,13 @@ var init_oauth_callback_flow = __esm({
|
|
|
36779
36921
|
}
|
|
36780
36922
|
}
|
|
36781
36923
|
async validatePendingAuth() {
|
|
36782
|
-
const {
|
|
36783
|
-
pendingAuthId,
|
|
36784
|
-
email,
|
|
36785
|
-
name: name42,
|
|
36786
|
-
isIncremental,
|
|
36787
|
-
isFederated,
|
|
36788
|
-
selectedProviders,
|
|
36789
|
-
selectedTools,
|
|
36790
|
-
consentSubmitted,
|
|
36791
|
-
loginFields
|
|
36792
|
-
} = this.state;
|
|
36924
|
+
const { pendingAuthId, email, name: name42, isFederated, selectedProviders, selectedTools, consentSubmitted, loginFields } = this.state;
|
|
36793
36925
|
if (!pendingAuthId) {
|
|
36794
36926
|
this.logger.warn("Missing pending_auth_id in callback");
|
|
36795
36927
|
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Missing pending_auth_id parameter"), 400));
|
|
36796
36928
|
return;
|
|
36797
36929
|
}
|
|
36798
36930
|
const localAuth = this.scope.auth;
|
|
36799
|
-
const localOptions = localAuth.options;
|
|
36800
|
-
const authenticateFn = typeof localOptions.authenticate === "function" ? localOptions.authenticate : void 0;
|
|
36801
|
-
const requireEmail = localOptions.requireEmail ?? true;
|
|
36802
|
-
if (!authenticateFn && !isIncremental && !email && requireEmail) {
|
|
36803
|
-
this.logger.warn("Missing email in callback");
|
|
36804
|
-
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Email is required"), 400));
|
|
36805
|
-
return;
|
|
36806
|
-
}
|
|
36807
36931
|
const pendingAuth = await localAuth.authorizationStore.getPendingAuthorization(pendingAuthId);
|
|
36808
36932
|
if (!pendingAuth) {
|
|
36809
36933
|
this.logger.warn(`Pending authorization not found or expired: ${pendingAuthId}`);
|
|
@@ -36815,6 +36939,20 @@ var init_oauth_callback_flow = __esm({
|
|
|
36815
36939
|
);
|
|
36816
36940
|
return;
|
|
36817
36941
|
}
|
|
36942
|
+
const isIncremental = pendingAuth.isIncremental === true && typeof pendingAuth.incrementalSub === "string";
|
|
36943
|
+
const targetAppId = isIncremental ? pendingAuth.targetAppId : void 0;
|
|
36944
|
+
this.state.set({ isIncremental, targetAppId });
|
|
36945
|
+
if (isIncremental) {
|
|
36946
|
+
this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
|
|
36947
|
+
}
|
|
36948
|
+
const localOptions = localAuth.options;
|
|
36949
|
+
const authenticateFn = typeof localOptions.authenticate === "function" ? localOptions.authenticate : void 0;
|
|
36950
|
+
const requireEmail = localOptions.requireEmail ?? true;
|
|
36951
|
+
if (!authenticateFn && !isIncremental && !email && requireEmail) {
|
|
36952
|
+
this.logger.warn("Missing email in callback");
|
|
36953
|
+
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Email is required"), 400));
|
|
36954
|
+
return;
|
|
36955
|
+
}
|
|
36818
36956
|
if (pendingAuth.authUiCsrf) {
|
|
36819
36957
|
const submitted = this.state.csrf;
|
|
36820
36958
|
if (!submitted || !timingSafeEqualStr3(pendingAuth.authUiCsrf, submitted)) {
|
|
@@ -36829,9 +36967,11 @@ var init_oauth_callback_flow = __esm({
|
|
|
36829
36967
|
return;
|
|
36830
36968
|
}
|
|
36831
36969
|
let userSub;
|
|
36832
|
-
if (
|
|
36970
|
+
if (isIncremental) {
|
|
36971
|
+
userSub = pendingAuth.incrementalSub;
|
|
36972
|
+
} else if (email) {
|
|
36833
36973
|
userSub = this.generateUserSub(email);
|
|
36834
|
-
} else if (!requireEmail
|
|
36974
|
+
} else if (!requireEmail) {
|
|
36835
36975
|
const anonymousSubject = localOptions.anonymousSubject ?? "local-operator";
|
|
36836
36976
|
userSub = this.generateUserSub(anonymousSubject);
|
|
36837
36977
|
} else {
|
|
@@ -37000,10 +37140,13 @@ var init_oauth_callback_flow = __esm({
|
|
|
37000
37140
|
originalState: pendingAuth.state,
|
|
37001
37141
|
resource: pendingAuth.resource,
|
|
37002
37142
|
userSub,
|
|
37003
|
-
// Progressive/Incremental Authorization from pending record
|
|
37004
|
-
|
|
37005
|
-
|
|
37006
|
-
|
|
37143
|
+
// Progressive/Incremental Authorization — from the pending record ONLY.
|
|
37144
|
+
// `isIncremental` was derived above from the record plus its proven
|
|
37145
|
+
// subject; OR-ing a client-supplied flag in here is what let a caller
|
|
37146
|
+
// override the server's `false` (GHSA-2c4g-9c8x-6m8g).
|
|
37147
|
+
isIncremental,
|
|
37148
|
+
targetAppId,
|
|
37149
|
+
targetToolId: isIncremental ? pendingAuth.targetToolId : void 0,
|
|
37007
37150
|
existingSessionId: pendingAuth.existingSessionId,
|
|
37008
37151
|
existingAuthorizationId: pendingAuth.existingAuthorizationId,
|
|
37009
37152
|
priorAuthorizedAppIds: pendingAuth.priorAuthorizedAppIds,
|
|
@@ -37086,7 +37229,7 @@ var init_oauth_callback_flow = __esm({
|
|
|
37086
37229
|
this.respond(httpRespond.html(this.renderErrorPage("server_error", "Authorization request incomplete"), 500));
|
|
37087
37230
|
return;
|
|
37088
37231
|
}
|
|
37089
|
-
const federatedSession = (0,
|
|
37232
|
+
const federatedSession = (0, import_auth23.createFederatedAuthSession)({
|
|
37090
37233
|
pendingAuthId: this.state.required.pendingAuthId || (0, import_utils67.randomUUID)(),
|
|
37091
37234
|
clientId,
|
|
37092
37235
|
redirectUri,
|
|
@@ -37115,7 +37258,7 @@ var init_oauth_callback_flow = __esm({
|
|
|
37115
37258
|
method: "S256"
|
|
37116
37259
|
};
|
|
37117
37260
|
const providerState = `federated:${federatedSession.id}:${(0, import_utils67.randomUUID)()}`;
|
|
37118
|
-
(0,
|
|
37261
|
+
(0, import_auth23.startNextProvider)(federatedSession, pkce, providerState);
|
|
37119
37262
|
await sessionStore.update(federatedSession);
|
|
37120
37263
|
const providerConfig = localAuth.getProviderConfig(firstProviderId);
|
|
37121
37264
|
if (!providerConfig) {
|
|
@@ -37419,7 +37562,7 @@ var init_oauth_callback_flow = __esm({
|
|
|
37419
37562
|
fields: login?.fields ?? {},
|
|
37420
37563
|
error: failure.message
|
|
37421
37564
|
};
|
|
37422
|
-
return (0,
|
|
37565
|
+
return (0, import_auth23.renderLocalLoginPage)(login, ctx, fields);
|
|
37423
37566
|
}
|
|
37424
37567
|
/**
|
|
37425
37568
|
* Render a custom `@AuthUi({ slot: 'consent' })` page when one is registered.
|
|
@@ -37515,7 +37658,7 @@ var init_oauth_callback_flow = __esm({
|
|
|
37515
37658
|
if (pendingAuth.authUiCsrf) {
|
|
37516
37659
|
hiddenFields.push({ name: "csrf", value: pendingAuth.authUiCsrf });
|
|
37517
37660
|
}
|
|
37518
|
-
return (0,
|
|
37661
|
+
return (0, import_auth23.buildToolConsentPage)({
|
|
37519
37662
|
tools: toolCards,
|
|
37520
37663
|
clientName: pendingAuth.clientId,
|
|
37521
37664
|
pendingAuthId: pendingAuth.id,
|
|
@@ -37548,8 +37691,8 @@ var init_oauth_callback_flow = __esm({
|
|
|
37548
37691
|
* Render an error page
|
|
37549
37692
|
*/
|
|
37550
37693
|
renderErrorPage(error, description) {
|
|
37551
|
-
const safeError = (0,
|
|
37552
|
-
const safeDescription = (0,
|
|
37694
|
+
const safeError = (0, import_auth23.escapeHtml)(error);
|
|
37695
|
+
const safeDescription = (0, import_auth23.escapeHtml)(description);
|
|
37553
37696
|
return `<!DOCTYPE html>
|
|
37554
37697
|
<html lang="en">
|
|
37555
37698
|
<head>
|
|
@@ -37607,6 +37750,7 @@ var init_oauth_callback_flow = __esm({
|
|
|
37607
37750
|
"pending_auth_id",
|
|
37608
37751
|
"incremental",
|
|
37609
37752
|
"app_id",
|
|
37753
|
+
"ticket",
|
|
37610
37754
|
"federated",
|
|
37611
37755
|
"providers",
|
|
37612
37756
|
"tools",
|
|
@@ -37659,11 +37803,11 @@ var init_oauth_callback_flow = __esm({
|
|
|
37659
37803
|
});
|
|
37660
37804
|
|
|
37661
37805
|
// libs/sdk/src/auth/flows/oauth.connect.flow.ts
|
|
37662
|
-
var
|
|
37806
|
+
var import_auth24, import_lazy_zod66, inputSchema18, stateSchema17, outputSchema17, plan17, name17, Stage17, RESERVED_CONNECT_PARAMS, OauthConnectFlow;
|
|
37663
37807
|
var init_oauth_connect_flow = __esm({
|
|
37664
37808
|
"libs/sdk/src/auth/flows/oauth.connect.flow.ts"() {
|
|
37665
37809
|
"use strict";
|
|
37666
|
-
|
|
37810
|
+
import_auth24 = require("@frontmcp/auth");
|
|
37667
37811
|
import_lazy_zod66 = require("@frontmcp/lazy-zod");
|
|
37668
37812
|
init_common();
|
|
37669
37813
|
inputSchema18 = httpInputSchema;
|
|
@@ -37773,7 +37917,7 @@ var init_oauth_connect_flow = __esm({
|
|
|
37773
37917
|
}
|
|
37774
37918
|
this.logger.info(`Connected ${stored} credential(s) mid-session for key "${payload.key}"`);
|
|
37775
37919
|
this.notifyResourcesUpdated(payload.key);
|
|
37776
|
-
this.respond(httpRespond.html((0,
|
|
37920
|
+
this.respond(httpRespond.html((0, import_auth24.buildConnectSuccessPage)({ key: payload.key })));
|
|
37777
37921
|
}
|
|
37778
37922
|
// ============================================
|
|
37779
37923
|
// Helpers
|
|
@@ -37781,7 +37925,7 @@ var init_oauth_connect_flow = __esm({
|
|
|
37781
37925
|
/** Verify the signed resume token against the server secret (constant-time + expiry). */
|
|
37782
37926
|
verifyToken(token, localAuth) {
|
|
37783
37927
|
const secret = new TextDecoder().decode(localAuth.secret);
|
|
37784
|
-
return (0,
|
|
37928
|
+
return (0, import_auth24.verifyCredentialResumeToken)(token, secret);
|
|
37785
37929
|
}
|
|
37786
37930
|
/** Resolve the single credential field to render from `login.fields` (first field), or a default. */
|
|
37787
37931
|
resolveConnectField(login, key) {
|
|
@@ -37797,8 +37941,8 @@ var init_oauth_connect_flow = __esm({
|
|
|
37797
37941
|
renderConnectPage(payload, login, token, error) {
|
|
37798
37942
|
const { name: name42, def } = this.resolveConnectField(login, payload.key);
|
|
37799
37943
|
const action = `${this.scope.fullPath}/oauth/connect`;
|
|
37800
|
-
const [field] = (0,
|
|
37801
|
-
return (0,
|
|
37944
|
+
const [field] = (0, import_auth24.toLoginExtraFields)({ [name42]: def });
|
|
37945
|
+
return (0, import_auth24.buildConnectPage)({
|
|
37802
37946
|
token,
|
|
37803
37947
|
field,
|
|
37804
37948
|
action,
|
|
@@ -37897,8 +38041,8 @@ var init_oauth_connect_flow = __esm({
|
|
|
37897
38041
|
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Connect Error</title></head>
|
|
37898
38042
|
<body style="font-family:-apple-system,sans-serif;padding:40px;text-align:center">
|
|
37899
38043
|
<h1 style="color:#e53e3e">Connect Error</h1>
|
|
37900
|
-
<p><code>${(0,
|
|
37901
|
-
<p>${(0,
|
|
38044
|
+
<p><code>${(0, import_auth24.escapeHtml)(error)}</code></p>
|
|
38045
|
+
<p>${(0, import_auth24.escapeHtml)(description)}</p>
|
|
37902
38046
|
</body></html>`;
|
|
37903
38047
|
}
|
|
37904
38048
|
};
|
|
@@ -37925,11 +38069,11 @@ var init_oauth_connect_flow = __esm({
|
|
|
37925
38069
|
});
|
|
37926
38070
|
|
|
37927
38071
|
// libs/sdk/src/auth/flows/oauth.provider-callback.flow.ts
|
|
37928
|
-
var
|
|
38072
|
+
var import_auth25, import_lazy_zod67, import_utils68, inputSchema19, stateSchema18, outputSchema18, plan18, name18, Stage18, OauthProviderCallbackFlow;
|
|
37929
38073
|
var init_oauth_provider_callback_flow = __esm({
|
|
37930
38074
|
"libs/sdk/src/auth/flows/oauth.provider-callback.flow.ts"() {
|
|
37931
38075
|
"use strict";
|
|
37932
|
-
|
|
38076
|
+
import_auth25 = require("@frontmcp/auth");
|
|
37933
38077
|
import_lazy_zod67 = require("@frontmcp/lazy-zod");
|
|
37934
38078
|
import_utils68 = require("@frontmcp/utils");
|
|
37935
38079
|
init_common();
|
|
@@ -38032,7 +38176,7 @@ var init_oauth_provider_callback_flow = __esm({
|
|
|
38032
38176
|
);
|
|
38033
38177
|
return;
|
|
38034
38178
|
}
|
|
38035
|
-
if (!(0,
|
|
38179
|
+
if (!(0, import_auth25.isSessionComplete)(session)) {
|
|
38036
38180
|
this.respond(
|
|
38037
38181
|
httpRespond.html(this.renderErrorPage("invalid_request", "Authentication is still in progress."), 400)
|
|
38038
38182
|
);
|
|
@@ -38231,7 +38375,7 @@ var init_oauth_provider_callback_flow = __esm({
|
|
|
38231
38375
|
this.logger.info(`Stored tokens for provider ${providerId} (temp auth: ${tempAuthId})`);
|
|
38232
38376
|
}
|
|
38233
38377
|
if (tokens) {
|
|
38234
|
-
(0,
|
|
38378
|
+
(0, import_auth25.completeCurrentProvider)(session, tokens, userInfo);
|
|
38235
38379
|
if (userInfo) {
|
|
38236
38380
|
if (!session.userInfo.sub && userInfo.sub) {
|
|
38237
38381
|
session.userInfo.sub = userInfo.sub;
|
|
@@ -38264,11 +38408,11 @@ var init_oauth_provider_callback_flow = __esm({
|
|
|
38264
38408
|
return;
|
|
38265
38409
|
}
|
|
38266
38410
|
const localAuth = this.getLocalAuth();
|
|
38267
|
-
if ((0,
|
|
38411
|
+
if ((0, import_auth25.isSessionComplete)(session)) {
|
|
38268
38412
|
this.logger.info("All providers authenticated, completing federated auth flow");
|
|
38269
38413
|
return this.completeFederatedAuth(session);
|
|
38270
38414
|
}
|
|
38271
|
-
const nextProviderId = (0,
|
|
38415
|
+
const nextProviderId = (0, import_auth25.getNextProvider)(session);
|
|
38272
38416
|
if (!nextProviderId) {
|
|
38273
38417
|
return this.completeFederatedAuth(session);
|
|
38274
38418
|
}
|
|
@@ -38280,7 +38424,7 @@ var init_oauth_provider_callback_flow = __esm({
|
|
|
38280
38424
|
method: "S256"
|
|
38281
38425
|
};
|
|
38282
38426
|
const providerState = `federated:${session.id}:${(0, import_utils68.randomUUID)()}`;
|
|
38283
|
-
(0,
|
|
38427
|
+
(0, import_auth25.startNextProvider)(session, pkce, providerState);
|
|
38284
38428
|
const sessionStore = localAuth.federatedSessionStore;
|
|
38285
38429
|
if (sessionStore) {
|
|
38286
38430
|
await sessionStore.update(session);
|
|
@@ -38443,7 +38587,7 @@ var init_oauth_provider_callback_flow = __esm({
|
|
|
38443
38587
|
renderConsentScreen(session, consentConfig, error, preSelectedTools) {
|
|
38444
38588
|
const callbackPath = `${this.scope.fullPath}/oauth/provider/_consent/callback`;
|
|
38445
38589
|
const { toolCards } = projectConsentTools(this.scope, consentConfig.excludedTools);
|
|
38446
|
-
return (0,
|
|
38590
|
+
return (0, import_auth25.buildToolConsentPage)({
|
|
38447
38591
|
tools: toolCards,
|
|
38448
38592
|
clientName: session.clientId,
|
|
38449
38593
|
// The pending_auth_id input is unused on the federated round-trip (we key
|
|
@@ -38480,8 +38624,8 @@ var init_oauth_provider_callback_flow = __esm({
|
|
|
38480
38624
|
* Render an error page
|
|
38481
38625
|
*/
|
|
38482
38626
|
renderErrorPage(error, description) {
|
|
38483
|
-
const safeError = (0,
|
|
38484
|
-
const safeDescription = (0,
|
|
38627
|
+
const safeError = (0, import_auth25.escapeHtml)(error);
|
|
38628
|
+
const safeDescription = (0, import_auth25.escapeHtml)(description);
|
|
38485
38629
|
return `<!DOCTYPE html>
|
|
38486
38630
|
<html lang="en">
|
|
38487
38631
|
<head>
|
|
@@ -39224,12 +39368,12 @@ var init_oauth_token_flow = __esm({
|
|
|
39224
39368
|
});
|
|
39225
39369
|
|
|
39226
39370
|
// libs/sdk/src/auth/flows/oauth.userinfo.flow.ts
|
|
39227
|
-
var import_reflect_metadata16,
|
|
39371
|
+
var import_reflect_metadata16, import_auth26, import_lazy_zod70, inputSchema22, stateSchema21, outputSchema21, plan21, name21, Stage21, OauthUserInfoFlow;
|
|
39228
39372
|
var init_oauth_userinfo_flow = __esm({
|
|
39229
39373
|
"libs/sdk/src/auth/flows/oauth.userinfo.flow.ts"() {
|
|
39230
39374
|
"use strict";
|
|
39231
39375
|
import_reflect_metadata16 = require("reflect-metadata");
|
|
39232
|
-
|
|
39376
|
+
import_auth26 = require("@frontmcp/auth");
|
|
39233
39377
|
import_lazy_zod70 = require("@frontmcp/lazy-zod");
|
|
39234
39378
|
init_common();
|
|
39235
39379
|
inputSchema22 = httpInputSchema;
|
|
@@ -39249,7 +39393,7 @@ var init_oauth_userinfo_flow = __esm({
|
|
|
39249
39393
|
async parseInput() {
|
|
39250
39394
|
const { request } = this.rawInput;
|
|
39251
39395
|
const authorizationHeader = request.headers?.["authorization"] ?? void 0;
|
|
39252
|
-
const token = (0,
|
|
39396
|
+
const token = (0, import_auth26.extractBearerToken)(authorizationHeader);
|
|
39253
39397
|
const baseUrl = getRequestBaseUrl(request, this.scope.entryPath);
|
|
39254
39398
|
this.state.set({ token, baseUrl });
|
|
39255
39399
|
}
|
|
@@ -39320,13 +39464,13 @@ var init_oauth_userinfo_flow = __esm({
|
|
|
39320
39464
|
});
|
|
39321
39465
|
|
|
39322
39466
|
// libs/sdk/src/auth/flows/session.verify.flow.ts
|
|
39323
|
-
var import_reflect_metadata17,
|
|
39467
|
+
var import_reflect_metadata17, import_auth27, import_lazy_zod71, import_utils71, inputSchema23, stateSchema22, UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema, sessionVerifyOutputSchema, plan22, name22, Stage22, warnedUnpinnedAudience, SessionVerifyFlow;
|
|
39324
39468
|
var init_session_verify_flow = __esm({
|
|
39325
39469
|
"libs/sdk/src/auth/flows/session.verify.flow.ts"() {
|
|
39326
39470
|
"use strict";
|
|
39327
39471
|
init_common();
|
|
39328
39472
|
import_reflect_metadata17 = require("reflect-metadata");
|
|
39329
|
-
|
|
39473
|
+
import_auth27 = require("@frontmcp/auth");
|
|
39330
39474
|
import_lazy_zod71 = require("@frontmcp/lazy-zod");
|
|
39331
39475
|
import_utils71 = require("@frontmcp/utils");
|
|
39332
39476
|
init_notification_service();
|
|
@@ -39421,7 +39565,7 @@ var init_session_verify_flow = __esm({
|
|
|
39421
39565
|
authMode,
|
|
39422
39566
|
...platformType !== "unknown" && { platformType }
|
|
39423
39567
|
};
|
|
39424
|
-
const sessionId = (0,
|
|
39568
|
+
const sessionId = (0, import_auth27.encryptJson)(payload);
|
|
39425
39569
|
this.respond({
|
|
39426
39570
|
kind: "authorized",
|
|
39427
39571
|
authorization: {
|
|
@@ -39442,11 +39586,11 @@ var init_session_verify_flow = __esm({
|
|
|
39442
39586
|
const sessionIdQuery = request.query["sessionId"];
|
|
39443
39587
|
const sessionIdHeader = sessionIdRawHeader ?? sessionIdQuery ?? void 0;
|
|
39444
39588
|
const sessionProtocol = httpTransportHeader ? "http" : sessionIdRawHeader ? "streamable-http" : sessionIdQuery ? "sse" : void 0;
|
|
39445
|
-
const token = (0,
|
|
39589
|
+
const token = (0, import_auth27.extractBearerToken)(authorizationHeader);
|
|
39446
39590
|
const userAgent = request.headers?.["user-agent"] ?? void 0;
|
|
39447
39591
|
const prmMetadataPath = `/.well-known/oauth-protected-resource${entryPath}${routeBase}`;
|
|
39448
39592
|
const prmUrl = `${getRequestBaseUrl(request)}${prmMetadataPath}`;
|
|
39449
|
-
const prmMetadataHeader = (0,
|
|
39593
|
+
const prmMetadataHeader = (0, import_auth27.buildUnauthorizedHeader)(prmUrl);
|
|
39450
39594
|
this.logger.verbose("parseInput", {
|
|
39451
39595
|
hasAuthHeader: !!authorizationHeader,
|
|
39452
39596
|
hasToken: !!token,
|
|
@@ -39500,7 +39644,7 @@ var init_session_verify_flow = __esm({
|
|
|
39500
39644
|
});
|
|
39501
39645
|
}
|
|
39502
39646
|
async verifyIfJwt() {
|
|
39503
|
-
const jwks = this.get(
|
|
39647
|
+
const jwks = this.get(import_auth27.JwksService);
|
|
39504
39648
|
const token = this.state.token;
|
|
39505
39649
|
if (!token) {
|
|
39506
39650
|
this.logger.warn("verifyIfJwt: missing or empty bearer token, returning 401");
|
|
@@ -39510,11 +39654,11 @@ var init_session_verify_flow = __esm({
|
|
|
39510
39654
|
});
|
|
39511
39655
|
return;
|
|
39512
39656
|
}
|
|
39513
|
-
if (!(0,
|
|
39657
|
+
if (!(0, import_auth27.isJwt)(token)) {
|
|
39514
39658
|
this.logger.warn("verifyIfJwt: token is not a JWT, returning 401");
|
|
39515
39659
|
this.respond({
|
|
39516
39660
|
kind: "unauthorized",
|
|
39517
|
-
prmMetadataHeader: (0,
|
|
39661
|
+
prmMetadataHeader: (0, import_auth27.buildInvalidTokenHeader)(this.state.required.prmUrl, "Token is not a valid JWT")
|
|
39518
39662
|
});
|
|
39519
39663
|
return;
|
|
39520
39664
|
}
|
|
@@ -39559,8 +39703,8 @@ var init_session_verify_flow = __esm({
|
|
|
39559
39703
|
"transparent audience validation is deriving the expected audience from the request Host: set auth.expectedAudience or FRONTMCP_PUBLIC_URL to bind tokens to a fixed resource. X-Forwarded-Host is ignored unless FRONTMCP_TRUST_PROXY is enabled."
|
|
39560
39704
|
);
|
|
39561
39705
|
}
|
|
39562
|
-
const expectedAudiences = configuredAudience ? Array.isArray(configuredAudience) ? configuredAudience : [configuredAudience] : (0,
|
|
39563
|
-
const audResult = (0,
|
|
39706
|
+
const expectedAudiences = configuredAudience ? Array.isArray(configuredAudience) ? configuredAudience : [configuredAudience] : (0, import_auth27.deriveExpectedAudience)(this.state.required.baseUrl);
|
|
39707
|
+
const audResult = (0, import_auth27.validateAudience)(result.payload?.["aud"], {
|
|
39564
39708
|
expectedAudiences,
|
|
39565
39709
|
// By default accept tokens with no `aud` claim: many IdPs omit it, and
|
|
39566
39710
|
// rejecting them here would break existing transparent deployments.
|
|
@@ -39574,7 +39718,7 @@ var init_session_verify_flow = __esm({
|
|
|
39574
39718
|
this.logger.warn("verifyIfJwt: audience validation failed", { error: audResult.error });
|
|
39575
39719
|
this.respond({
|
|
39576
39720
|
kind: "unauthorized",
|
|
39577
|
-
prmMetadataHeader: (0,
|
|
39721
|
+
prmMetadataHeader: (0, import_auth27.buildInvalidTokenHeader)(
|
|
39578
39722
|
this.state.required.prmUrl,
|
|
39579
39723
|
audResult.error ?? "Token audience is not valid for this resource"
|
|
39580
39724
|
)
|
|
@@ -39594,7 +39738,7 @@ var init_session_verify_flow = __esm({
|
|
|
39594
39738
|
});
|
|
39595
39739
|
this.respond({
|
|
39596
39740
|
kind: "forbidden",
|
|
39597
|
-
prmMetadataHeader: (0,
|
|
39741
|
+
prmMetadataHeader: (0, import_auth27.buildInsufficientScopeHeader)(this.state.required.prmUrl, requiredScopes)
|
|
39598
39742
|
});
|
|
39599
39743
|
return;
|
|
39600
39744
|
}
|
|
@@ -39604,14 +39748,14 @@ var init_session_verify_flow = __esm({
|
|
|
39604
39748
|
this.logger.warn("verifyIfJwt: JWT verification failed", { error: result.error });
|
|
39605
39749
|
this.respond({
|
|
39606
39750
|
kind: "unauthorized",
|
|
39607
|
-
prmMetadataHeader: (0,
|
|
39751
|
+
prmMetadataHeader: (0, import_auth27.buildInvalidTokenHeader)(
|
|
39608
39752
|
this.state.required.prmUrl,
|
|
39609
39753
|
result.error ?? "Token verification failed"
|
|
39610
39754
|
)
|
|
39611
39755
|
});
|
|
39612
39756
|
}
|
|
39613
39757
|
async deriveUser() {
|
|
39614
|
-
this.state.set("user", (0,
|
|
39758
|
+
this.state.set("user", (0, import_auth27.deriveTypedUser)(this.state.required.jwtPayload ?? {}));
|
|
39615
39759
|
}
|
|
39616
39760
|
async parseSessionHeader() {
|
|
39617
39761
|
const {
|
|
@@ -39697,13 +39841,13 @@ var init_session_verify_flow = __esm({
|
|
|
39697
39841
|
});
|
|
39698
39842
|
|
|
39699
39843
|
// libs/sdk/src/auth/flows/well-known.jwks.flow.ts
|
|
39700
|
-
var import_reflect_metadata18,
|
|
39844
|
+
var import_reflect_metadata18, import_auth28, import_lazy_zod72, inputSchema24, stateSchema23, outputSchema22, plan23, name23, Stage23, WellKnownJwksFlow;
|
|
39701
39845
|
var init_well_known_jwks_flow = __esm({
|
|
39702
39846
|
"libs/sdk/src/auth/flows/well-known.jwks.flow.ts"() {
|
|
39703
39847
|
"use strict";
|
|
39704
39848
|
init_common();
|
|
39705
39849
|
import_reflect_metadata18 = require("reflect-metadata");
|
|
39706
|
-
|
|
39850
|
+
import_auth28 = require("@frontmcp/auth");
|
|
39707
39851
|
import_lazy_zod72 = require("@frontmcp/lazy-zod");
|
|
39708
39852
|
init_auth_internal_errors();
|
|
39709
39853
|
inputSchema24 = httpInputSchema;
|
|
@@ -39729,7 +39873,7 @@ var init_well_known_jwks_flow = __esm({
|
|
|
39729
39873
|
}
|
|
39730
39874
|
async collectData() {
|
|
39731
39875
|
const { isOrchestrated } = this.state.required;
|
|
39732
|
-
const jwksSvc = this.get(
|
|
39876
|
+
const jwksSvc = this.get(import_auth28.JwksService);
|
|
39733
39877
|
if (isOrchestrated) {
|
|
39734
39878
|
const keysDoc = await jwksSvc.getPublicJwks();
|
|
39735
39879
|
if (!keysDoc?.keys || !Array.isArray(keysDoc.keys)) {
|
|
@@ -40059,9 +40203,9 @@ function createSecureStoreProviders(opts) {
|
|
|
40059
40203
|
name: "SecureStoreBackend"
|
|
40060
40204
|
};
|
|
40061
40205
|
const accessorProvider = {
|
|
40062
|
-
provide:
|
|
40206
|
+
provide: import_auth29.SECURE_STORE_ACCESSOR,
|
|
40063
40207
|
inject: () => [FRONTMCP_CONTEXT, FrontMcpLogger],
|
|
40064
|
-
useFactory: (ctx, logger) => new
|
|
40208
|
+
useFactory: (ctx, logger) => new import_auth29.SecureStoreAccessorImpl({
|
|
40065
40209
|
backend,
|
|
40066
40210
|
scope,
|
|
40067
40211
|
resolveSub: () => resolveRequestSub(ctx),
|
|
@@ -40074,11 +40218,11 @@ function createSecureStoreProviders(opts) {
|
|
|
40074
40218
|
};
|
|
40075
40219
|
return [backendProvider, accessorProvider];
|
|
40076
40220
|
}
|
|
40077
|
-
var
|
|
40221
|
+
var import_auth29, import_di32, SECURE_STORE_BACKEND;
|
|
40078
40222
|
var init_secure_store_providers = __esm({
|
|
40079
40223
|
"libs/sdk/src/auth/secure-store/secure-store.providers.ts"() {
|
|
40080
40224
|
"use strict";
|
|
40081
|
-
|
|
40225
|
+
import_auth29 = require("@frontmcp/auth");
|
|
40082
40226
|
import_di32 = require("@frontmcp/di");
|
|
40083
40227
|
init_common();
|
|
40084
40228
|
init_frontmcp_context_provider();
|
|
@@ -40088,14 +40232,14 @@ var init_secure_store_providers = __esm({
|
|
|
40088
40232
|
});
|
|
40089
40233
|
|
|
40090
40234
|
// libs/sdk/src/auth/secure-store/secure-store.context-extension.ts
|
|
40091
|
-
var
|
|
40235
|
+
var import_auth30, secureStoreContextExtension;
|
|
40092
40236
|
var init_secure_store_context_extension = __esm({
|
|
40093
40237
|
"libs/sdk/src/auth/secure-store/secure-store.context-extension.ts"() {
|
|
40094
40238
|
"use strict";
|
|
40095
|
-
|
|
40239
|
+
import_auth30 = require("@frontmcp/auth");
|
|
40096
40240
|
secureStoreContextExtension = {
|
|
40097
40241
|
property: "secureStore",
|
|
40098
|
-
token:
|
|
40242
|
+
token: import_auth30.SECURE_STORE_ACCESSOR,
|
|
40099
40243
|
errorMessage: "The secure store is not available. Ensure the server uses auth mode 'local' (or 'remote'); the secure store is enabled automatically there."
|
|
40100
40244
|
};
|
|
40101
40245
|
}
|
|
@@ -40130,12 +40274,15 @@ function validateAuthorizationIssuer(received, expected) {
|
|
|
40130
40274
|
}
|
|
40131
40275
|
return { ok: true };
|
|
40132
40276
|
}
|
|
40133
|
-
|
|
40277
|
+
function isUnsupportedOperation(error) {
|
|
40278
|
+
return error instanceof import_utils73.StorageNotSupportedError;
|
|
40279
|
+
}
|
|
40280
|
+
var import_jose, import_auth31, import_utils73, defaultNoAuthSecret, RESERVED_JWT_CLAIMS, LocalPrimaryAuth;
|
|
40134
40281
|
var init_instance_local_primary_auth = __esm({
|
|
40135
40282
|
"libs/sdk/src/auth/instances/instance.local-primary-auth.ts"() {
|
|
40136
40283
|
"use strict";
|
|
40137
40284
|
import_jose = require("jose");
|
|
40138
|
-
|
|
40285
|
+
import_auth31 = require("@frontmcp/auth");
|
|
40139
40286
|
import_utils73 = require("@frontmcp/utils");
|
|
40140
40287
|
init_common();
|
|
40141
40288
|
init_auth2();
|
|
@@ -40191,17 +40338,17 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40191
40338
|
this.secret = getDefaultNoAuthSecret();
|
|
40192
40339
|
}
|
|
40193
40340
|
this.tokenStorage = this.readTokenStorage(options);
|
|
40194
|
-
this.authorizationStoreImpl = new
|
|
40195
|
-
this.federatedSessionStoreImpl = new
|
|
40196
|
-
this.orchestratedTokenStoreImpl = new
|
|
40341
|
+
this.authorizationStoreImpl = new import_auth31.InMemoryAuthorizationStore();
|
|
40342
|
+
this.federatedSessionStoreImpl = new import_auth31.InMemoryFederatedAuthSessionStore();
|
|
40343
|
+
this.orchestratedTokenStoreImpl = new import_auth31.InMemoryOrchestratedTokenStore({
|
|
40197
40344
|
encryptionKey: this.secret
|
|
40198
40345
|
// Reuse JWT secret for token encryption
|
|
40199
40346
|
});
|
|
40200
|
-
this.consentStoreImpl = new
|
|
40201
|
-
this.dcrClientRegistryImpl = new
|
|
40347
|
+
this.consentStoreImpl = new import_auth31.InMemoryConsentStore();
|
|
40348
|
+
this.dcrClientRegistryImpl = new import_auth31.DcrClientRegistry(this.getDcrConfig() ?? {});
|
|
40202
40349
|
if ((0, import_auth10.isOrchestratedMode)(options)) {
|
|
40203
40350
|
const cimdConfig = options.cimd;
|
|
40204
|
-
this.cimdService = new
|
|
40351
|
+
this.cimdService = new import_auth19.CimdService(this.logger, cimdConfig);
|
|
40205
40352
|
}
|
|
40206
40353
|
this.ready = this.initialize();
|
|
40207
40354
|
}
|
|
@@ -40213,7 +40360,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40213
40360
|
keys = [];
|
|
40214
40361
|
secret;
|
|
40215
40362
|
logger;
|
|
40216
|
-
jwks = new
|
|
40363
|
+
jwks = new import_auth31.JwksService();
|
|
40217
40364
|
cimdService;
|
|
40218
40365
|
/**
|
|
40219
40366
|
* Token storage backend selected from `options.tokenStorage`.
|
|
@@ -40244,6 +40391,17 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40244
40391
|
dcrClientRegistryImpl;
|
|
40245
40392
|
/** Storage adapter backing the persistent stores (kept for disposal). */
|
|
40246
40393
|
storageAdapter;
|
|
40394
|
+
/**
|
|
40395
|
+
* Replay guard for incremental-authorization tickets (GHSA-2c4g-9c8x-6m8g).
|
|
40396
|
+
* Shares the persistent adapter when one is configured so the guard holds
|
|
40397
|
+
* across a distributed deployment; falls back to a dedicated in-memory
|
|
40398
|
+
* adapter, matching how the credential vault selects its backing store.
|
|
40399
|
+
*
|
|
40400
|
+
* Holds the in-flight PROMISE, not the resolved adapter: two concurrent
|
|
40401
|
+
* claims for the same ticket must not each build their own store, or each
|
|
40402
|
+
* would write and read back its own nonce and both would win.
|
|
40403
|
+
*/
|
|
40404
|
+
ticketReplayStorage;
|
|
40247
40405
|
/**
|
|
40248
40406
|
* Per-session encrypted credential vault (Checkpoint 3b). Backs
|
|
40249
40407
|
* `this.credentials` in tools and persists `authenticate()` credentials.
|
|
@@ -40381,19 +40539,19 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40381
40539
|
* key, so upstream provider tokens stay encrypted at rest in every backend.
|
|
40382
40540
|
*/
|
|
40383
40541
|
async initializeStores() {
|
|
40384
|
-
if (!(0,
|
|
40542
|
+
if (!(0, import_auth31.isPersistentTokenStorage)(this.tokenStorage)) {
|
|
40385
40543
|
return;
|
|
40386
40544
|
}
|
|
40387
40545
|
try {
|
|
40388
|
-
const adapter = await (0,
|
|
40546
|
+
const adapter = await (0, import_auth31.createTokenStorageAdapter)(this.tokenStorage);
|
|
40389
40547
|
this.storageAdapter = adapter;
|
|
40390
|
-
this.authorizationStoreImpl = new
|
|
40391
|
-
this.federatedSessionStoreImpl = new
|
|
40392
|
-
this.orchestratedTokenStoreImpl = new
|
|
40548
|
+
this.authorizationStoreImpl = new import_auth31.StorageAuthorizationStore(adapter);
|
|
40549
|
+
this.federatedSessionStoreImpl = new import_auth31.StorageFederatedAuthSessionStore(adapter);
|
|
40550
|
+
this.orchestratedTokenStoreImpl = new import_auth31.StorageOrchestratedTokenStore(adapter, {
|
|
40393
40551
|
encryptionKey: this.secret
|
|
40394
40552
|
});
|
|
40395
|
-
this.consentStoreImpl = new
|
|
40396
|
-
const backend = (0,
|
|
40553
|
+
this.consentStoreImpl = new import_auth31.StorageConsentStore(adapter);
|
|
40554
|
+
const backend = (0, import_auth31.isSqliteTokenStorage)(this.tokenStorage) ? "sqlite" : (0, import_auth31.isRedisTokenStorage)(this.tokenStorage) ? "redis" : "unknown";
|
|
40397
40555
|
this.logger.info(`Token storage initialized with persistent backend: ${backend}`);
|
|
40398
40556
|
} catch (err) {
|
|
40399
40557
|
this.logger.error("Failed to initialize persistent token storage", err);
|
|
@@ -40411,6 +40569,81 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40411
40569
|
* (`this.secret`), so resume URLs are framework-signed with the same trust
|
|
40412
40570
|
* root as the access tokens.
|
|
40413
40571
|
*/
|
|
40572
|
+
/**
|
|
40573
|
+
* The server HMAC signing key, as the string form the `signData`/`verifyData`
|
|
40574
|
+
* helpers take. These are the exact bytes the access tokens are signed with,
|
|
40575
|
+
* so framework-signed URLs share the access tokens' trust root.
|
|
40576
|
+
*/
|
|
40577
|
+
get signingSecret() {
|
|
40578
|
+
return new TextDecoder().decode(this.secret);
|
|
40579
|
+
}
|
|
40580
|
+
/**
|
|
40581
|
+
* Claim an incremental-authorization ticket, exactly once.
|
|
40582
|
+
*
|
|
40583
|
+
* A signature check proves a ticket was minted by this server for a verified
|
|
40584
|
+
* subject; it cannot prove the ticket has not already been used. The `auth_url`
|
|
40585
|
+
* carrying the ticket travels through agent transcripts and server logs, so a
|
|
40586
|
+
* leaked ticket must not stay usable for the rest of its TTL.
|
|
40587
|
+
*
|
|
40588
|
+
* The claim is race-free without a transaction: each attempt writes its OWN
|
|
40589
|
+
* nonce under `ifNotExists` and reads the key back. Only the attempt whose
|
|
40590
|
+
* nonce survives won, so concurrent replays all lose.
|
|
40591
|
+
*
|
|
40592
|
+
* @returns true when this caller claimed the ticket, false when it was already used.
|
|
40593
|
+
*/
|
|
40594
|
+
async claimIncrementalTicket(jti, ttlMs) {
|
|
40595
|
+
const key = `incremental-ticket:${jti}`;
|
|
40596
|
+
const nonce = (0, import_utils73.randomUUID)();
|
|
40597
|
+
const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1e3));
|
|
40598
|
+
let storage = await this.getTicketReplayStorage();
|
|
40599
|
+
try {
|
|
40600
|
+
await storage.set(key, nonce, { ifNotExists: true, ttlSeconds });
|
|
40601
|
+
} catch (error) {
|
|
40602
|
+
if (!isUnsupportedOperation(error)) {
|
|
40603
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(error)}`);
|
|
40604
|
+
return false;
|
|
40605
|
+
}
|
|
40606
|
+
this.logger.warn(
|
|
40607
|
+
"Incremental ticket replay guard: the configured storage has no conditional write; falling back to an in-memory guard (single use holds within this instance only)."
|
|
40608
|
+
);
|
|
40609
|
+
storage = await this.useInMemoryTicketReplayStorage();
|
|
40610
|
+
try {
|
|
40611
|
+
await storage.set(key, nonce, { ifNotExists: true, ttlSeconds });
|
|
40612
|
+
} catch (fallbackError) {
|
|
40613
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(fallbackError)}`);
|
|
40614
|
+
return false;
|
|
40615
|
+
}
|
|
40616
|
+
}
|
|
40617
|
+
try {
|
|
40618
|
+
return await storage.get(key) === nonce;
|
|
40619
|
+
} catch (error) {
|
|
40620
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(error)}`);
|
|
40621
|
+
return false;
|
|
40622
|
+
}
|
|
40623
|
+
}
|
|
40624
|
+
getTicketReplayStorage() {
|
|
40625
|
+
if (!this.ticketReplayStorage) {
|
|
40626
|
+
if (this.storageAdapter) {
|
|
40627
|
+
this.ticketReplayStorage = Promise.resolve(this.storageAdapter);
|
|
40628
|
+
} else {
|
|
40629
|
+
this.logger.debug(
|
|
40630
|
+
"Incremental ticket replay guard is in-memory (no persistent tokenStorage configured); single use holds within this instance only."
|
|
40631
|
+
);
|
|
40632
|
+
this.ticketReplayStorage = this.useInMemoryTicketReplayStorage();
|
|
40633
|
+
}
|
|
40634
|
+
}
|
|
40635
|
+
return this.ticketReplayStorage;
|
|
40636
|
+
}
|
|
40637
|
+
/** Replace the replay guard with a fresh in-memory adapter, memoized. */
|
|
40638
|
+
useInMemoryTicketReplayStorage() {
|
|
40639
|
+
const pending = (async () => {
|
|
40640
|
+
const memory = new import_utils73.MemoryStorageAdapter();
|
|
40641
|
+
await memory.connect();
|
|
40642
|
+
return memory;
|
|
40643
|
+
})();
|
|
40644
|
+
this.ticketReplayStorage = pending;
|
|
40645
|
+
return pending;
|
|
40646
|
+
}
|
|
40414
40647
|
async initializeCredentialVault() {
|
|
40415
40648
|
if ((0, import_auth10.isPublicMode)(this.options)) {
|
|
40416
40649
|
return;
|
|
@@ -40424,12 +40657,12 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40424
40657
|
storage = memory;
|
|
40425
40658
|
}
|
|
40426
40659
|
const pepper = (0, import_utils73.getEnv)("VAULT_SECRET") ?? (0, import_utils73.getEnv)("JWT_SECRET") ?? void 0;
|
|
40427
|
-
this.credentialVaultImpl = new
|
|
40660
|
+
this.credentialVaultImpl = new import_auth31.SessionCredentialVault({
|
|
40428
40661
|
storage,
|
|
40429
40662
|
pepper,
|
|
40430
40663
|
logger: this.logger.child("SessionCredentialVault")
|
|
40431
40664
|
});
|
|
40432
|
-
const signingSecret =
|
|
40665
|
+
const signingSecret = this.signingSecret;
|
|
40433
40666
|
const basePath = this.issuer;
|
|
40434
40667
|
await this.providers.addDynamicProviders(
|
|
40435
40668
|
createCredentialsProviders({
|
|
@@ -40475,7 +40708,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40475
40708
|
const config = this.readSecureStoreConfig();
|
|
40476
40709
|
const reuseAdapter = this.storageAdapter && this.secureStoreSharesTokenStorage(config) ? this.storageAdapter : void 0;
|
|
40477
40710
|
const pepper = (0, import_utils73.getEnv)("VAULT_SECRET") ?? (0, import_utils73.getEnv)("JWT_SECRET") ?? new TextDecoder().decode(this.secret);
|
|
40478
|
-
const resolved = await (0,
|
|
40711
|
+
const resolved = await (0, import_auth31.createSecureStore)({
|
|
40479
40712
|
config,
|
|
40480
40713
|
pepper,
|
|
40481
40714
|
storage: reuseAdapter,
|
|
@@ -40502,8 +40735,8 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40502
40735
|
*/
|
|
40503
40736
|
secureStoreSharesTokenStorage(config) {
|
|
40504
40737
|
if (typeof config !== "object" || config === null) return false;
|
|
40505
|
-
if ("sqlite" in config && config.sqlite) return (0,
|
|
40506
|
-
if ("redis" in config && config.redis) return (0,
|
|
40738
|
+
if ("sqlite" in config && config.sqlite) return (0, import_auth31.isSqliteTokenStorage)(this.tokenStorage);
|
|
40739
|
+
if ("redis" in config && config.redis) return (0, import_auth31.isRedisTokenStorage)(this.tokenStorage);
|
|
40507
40740
|
return false;
|
|
40508
40741
|
}
|
|
40509
40742
|
async signAnonymousJwt() {
|
|
@@ -40631,7 +40864,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40631
40864
|
error_description: "Redirect URI does not match"
|
|
40632
40865
|
};
|
|
40633
40866
|
}
|
|
40634
|
-
if (!(0,
|
|
40867
|
+
if (!(0, import_auth31.verifyPkce)(codeVerifier, codeRecord.pkce)) {
|
|
40635
40868
|
this.logger.warn(`PKCE verification failed`);
|
|
40636
40869
|
return {
|
|
40637
40870
|
error: "invalid_grant",
|
|
@@ -40802,7 +41035,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40802
41035
|
scope: import_di4.ProviderScope.GLOBAL,
|
|
40803
41036
|
name: "auth:jwk-service"
|
|
40804
41037
|
},
|
|
40805
|
-
provide:
|
|
41038
|
+
provide: import_auth31.JwksService
|
|
40806
41039
|
});
|
|
40807
41040
|
if (this.cimdService) {
|
|
40808
41041
|
this.providers.injectProvider({
|
|
@@ -40811,7 +41044,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40811
41044
|
scope: import_di4.ProviderScope.GLOBAL,
|
|
40812
41045
|
name: "auth:cimd-service"
|
|
40813
41046
|
},
|
|
40814
|
-
provide:
|
|
41047
|
+
provide: import_auth19.CimdService
|
|
40815
41048
|
});
|
|
40816
41049
|
this.logger.debug("CIMD service registered");
|
|
40817
41050
|
}
|
|
@@ -41177,11 +41410,11 @@ var init_instance_local_primary_auth = __esm({
|
|
|
41177
41410
|
});
|
|
41178
41411
|
|
|
41179
41412
|
// libs/sdk/src/auth/instances/instance.remote-primary-auth.ts
|
|
41180
|
-
var
|
|
41413
|
+
var import_auth33, RemotePrimaryAuth;
|
|
41181
41414
|
var init_instance_remote_primary_auth = __esm({
|
|
41182
41415
|
"libs/sdk/src/auth/instances/instance.remote-primary-auth.ts"() {
|
|
41183
41416
|
"use strict";
|
|
41184
|
-
|
|
41417
|
+
import_auth33 = require("@frontmcp/auth");
|
|
41185
41418
|
init_common();
|
|
41186
41419
|
init_session_verify_flow();
|
|
41187
41420
|
init_well_known_jwks_flow();
|
|
@@ -41197,7 +41430,7 @@ var init_instance_remote_primary_auth = __esm({
|
|
|
41197
41430
|
scope;
|
|
41198
41431
|
providers;
|
|
41199
41432
|
ready;
|
|
41200
|
-
jwks = new
|
|
41433
|
+
jwks = new import_auth33.JwksService();
|
|
41201
41434
|
fetch(input, init) {
|
|
41202
41435
|
return fetch(input, init);
|
|
41203
41436
|
}
|
|
@@ -41215,7 +41448,7 @@ var init_instance_remote_primary_auth = __esm({
|
|
|
41215
41448
|
scope: import_di4.ProviderScope.GLOBAL,
|
|
41216
41449
|
name: "auth:jwk-service"
|
|
41217
41450
|
},
|
|
41218
|
-
provide:
|
|
41451
|
+
provide: import_auth33.JwksService
|
|
41219
41452
|
});
|
|
41220
41453
|
await this.registerAuthFlows(scope);
|
|
41221
41454
|
return Promise.resolve();
|
|
@@ -41233,12 +41466,12 @@ var init_instance_remote_primary_auth = __esm({
|
|
|
41233
41466
|
});
|
|
41234
41467
|
|
|
41235
41468
|
// libs/sdk/src/auth/auth.registry.ts
|
|
41236
|
-
var import_reflect_metadata21,
|
|
41469
|
+
var import_reflect_metadata21, import_auth34, import_di33, DEFAULT_AUTH_OPTIONS, AuthRegistry;
|
|
41237
41470
|
var init_auth_registry = __esm({
|
|
41238
41471
|
"libs/sdk/src/auth/auth.registry.ts"() {
|
|
41239
41472
|
"use strict";
|
|
41240
41473
|
import_reflect_metadata21 = require("reflect-metadata");
|
|
41241
|
-
|
|
41474
|
+
import_auth34 = require("@frontmcp/auth");
|
|
41242
41475
|
import_di33 = require("@frontmcp/di");
|
|
41243
41476
|
init_common();
|
|
41244
41477
|
init_context_extension();
|
|
@@ -41270,7 +41503,7 @@ var init_auth_registry = __esm({
|
|
|
41270
41503
|
this.logger = providers.get(FrontMcpLogger).child("AuthRegistry");
|
|
41271
41504
|
this.parsedOptions = (0, import_auth10.parseAuthOptions)(primaryInput ?? DEFAULT_AUTH_OPTIONS);
|
|
41272
41505
|
const appAuthInfos = this.extractAppAuthInfo(scope);
|
|
41273
|
-
this.detection = (0,
|
|
41506
|
+
this.detection = (0, import_auth34.detectAuthProviders)(this.parsedOptions, appAuthInfos);
|
|
41274
41507
|
this.requiresOrchestration = this.detection.requiresOrchestration;
|
|
41275
41508
|
this.logDetectionResults();
|
|
41276
41509
|
this.validateConfiguration();
|
|
@@ -44728,6 +44961,7 @@ var init_execute_workflow_tool = __esm({
|
|
|
44728
44961
|
"use strict";
|
|
44729
44962
|
import_lazy_zod81 = require("@frontmcp/lazy-zod");
|
|
44730
44963
|
init_common();
|
|
44964
|
+
init_errors();
|
|
44731
44965
|
ExecuteWorkflowTool = class extends ToolContext {
|
|
44732
44966
|
async execute(input) {
|
|
44733
44967
|
const scope = this.scope;
|
|
@@ -44739,12 +44973,13 @@ var init_execute_workflow_tool = __esm({
|
|
|
44739
44973
|
}
|
|
44740
44974
|
const workflow = workflowRegistry.findByName(input.name);
|
|
44741
44975
|
if (!workflow) {
|
|
44742
|
-
return this.fail(new
|
|
44976
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
44743
44977
|
}
|
|
44744
44978
|
const result = await executionManager.executeWorkflow(workflow, jobRegistry, {
|
|
44745
44979
|
background: input.background,
|
|
44746
44980
|
sessionId: this.authInfo.sessionId,
|
|
44747
44981
|
authInfo: this.authInfo,
|
|
44982
|
+
authoritiesContextBuilder: this.scope.authoritiesContextBuilder,
|
|
44748
44983
|
workflowInput: input.input
|
|
44749
44984
|
});
|
|
44750
44985
|
return result;
|
|
@@ -44769,6 +45004,55 @@ var init_execute_workflow_tool = __esm({
|
|
|
44769
45004
|
}
|
|
44770
45005
|
});
|
|
44771
45006
|
|
|
45007
|
+
// libs/sdk/src/common/utils/principal.utils.ts
|
|
45008
|
+
function toStringArray(value) {
|
|
45009
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
45010
|
+
if (typeof value === "string" && value.length > 0) return value.split(" ").filter(Boolean);
|
|
45011
|
+
return [];
|
|
45012
|
+
}
|
|
45013
|
+
function asRecord(value) {
|
|
45014
|
+
return value && typeof value === "object" ? value : void 0;
|
|
45015
|
+
}
|
|
45016
|
+
function firstPresent(sources, convert) {
|
|
45017
|
+
for (const source of sources) {
|
|
45018
|
+
if (source !== void 0 && source !== null) return convert(source);
|
|
45019
|
+
}
|
|
45020
|
+
return [];
|
|
45021
|
+
}
|
|
45022
|
+
function resolvePrincipal2(authInfo, contextBuilder) {
|
|
45023
|
+
const info = authInfo ?? {};
|
|
45024
|
+
const user = asRecord(info["user"]) ?? {};
|
|
45025
|
+
const extraAuthorization = asRecord(asRecord(info["extra"])?.["authorization"]);
|
|
45026
|
+
const authInfoClaims = asRecord(info["claims"]) ?? {};
|
|
45027
|
+
const authorizationClaims = asRecord(extraAuthorization?.["claims"]) ?? {};
|
|
45028
|
+
const claims = { ...authorizationClaims, ...authInfoClaims, ...user };
|
|
45029
|
+
const scopes = toStringArray(info["scopes"]).length ? toStringArray(info["scopes"]) : firstPresent([extraAuthorization?.["scopes"], claims["scope"], claims["scopes"]], toStringArray);
|
|
45030
|
+
if (contextBuilder) {
|
|
45031
|
+
const ctx = contextBuilder.build(info);
|
|
45032
|
+
return {
|
|
45033
|
+
sub: ctx.user.sub,
|
|
45034
|
+
roles: ctx.user.roles,
|
|
45035
|
+
permissions: ctx.user.permissions,
|
|
45036
|
+
scopes,
|
|
45037
|
+
claims: ctx.user.claims
|
|
45038
|
+
};
|
|
45039
|
+
}
|
|
45040
|
+
const roles = firstPresent([user["roles"], claims["roles"], extraAuthorization?.["scopes"]], toStringArray);
|
|
45041
|
+
const permissions = firstPresent([user["permissions"], claims["permissions"]], toStringArray);
|
|
45042
|
+
return {
|
|
45043
|
+
sub: typeof user["sub"] === "string" ? user["sub"] : "",
|
|
45044
|
+
roles,
|
|
45045
|
+
permissions,
|
|
45046
|
+
scopes,
|
|
45047
|
+
claims
|
|
45048
|
+
};
|
|
45049
|
+
}
|
|
45050
|
+
var init_principal_utils = __esm({
|
|
45051
|
+
"libs/sdk/src/common/utils/principal.utils.ts"() {
|
|
45052
|
+
"use strict";
|
|
45053
|
+
}
|
|
45054
|
+
});
|
|
45055
|
+
|
|
44772
45056
|
// libs/sdk/src/workflow/tools/get-workflow-status.tool.ts
|
|
44773
45057
|
var import_lazy_zod82, GetWorkflowStatusTool;
|
|
44774
45058
|
var init_get_workflow_status_tool = __esm({
|
|
@@ -44776,6 +45060,7 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44776
45060
|
"use strict";
|
|
44777
45061
|
import_lazy_zod82 = require("@frontmcp/lazy-zod");
|
|
44778
45062
|
init_common();
|
|
45063
|
+
init_principal_utils();
|
|
44779
45064
|
GetWorkflowStatusTool = class extends ToolContext {
|
|
44780
45065
|
async execute(input) {
|
|
44781
45066
|
const scope = this.scope;
|
|
@@ -44784,7 +45069,7 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44784
45069
|
return this.fail(new Error("Jobs system is not enabled"));
|
|
44785
45070
|
}
|
|
44786
45071
|
const record = await executionManager.getStatus(input.runId);
|
|
44787
|
-
if (!record) {
|
|
45072
|
+
if (!record || !this.ownsRun(record)) {
|
|
44788
45073
|
return this.fail(new Error(`Run "${input.runId}" not found`));
|
|
44789
45074
|
}
|
|
44790
45075
|
const workflowRecord = record;
|
|
@@ -44797,6 +45082,23 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44797
45082
|
completedAt: record.completedAt
|
|
44798
45083
|
};
|
|
44799
45084
|
}
|
|
45085
|
+
/**
|
|
45086
|
+
* Match on the owning subject, falling back to the session for runs started
|
|
45087
|
+
* before an owner was recorded (and for anonymous/public servers, where every
|
|
45088
|
+
* caller has an empty subject and the session is the only identity there is).
|
|
45089
|
+
*
|
|
45090
|
+
* A record carrying NEITHER identity is refused. Such a run cannot be
|
|
45091
|
+
* attributed to anyone, and its inputs and results are exactly what this check
|
|
45092
|
+
* exists to protect — anyone holding the id would otherwise be able to read
|
|
45093
|
+
* it. Internal callers that must read unattributed runs should go through the
|
|
45094
|
+
* execution manager directly rather than this tool.
|
|
45095
|
+
*/
|
|
45096
|
+
ownsRun(record) {
|
|
45097
|
+
const callerSub = resolvePrincipal2(this.authInfo, this.scope.authoritiesContextBuilder).sub;
|
|
45098
|
+
if (record.ownerSub) return record.ownerSub === callerSub;
|
|
45099
|
+
if (record.sessionId) return record.sessionId === this.authInfo.sessionId;
|
|
45100
|
+
return false;
|
|
45101
|
+
}
|
|
44800
45102
|
};
|
|
44801
45103
|
GetWorkflowStatusTool = __decorateClass([
|
|
44802
45104
|
FrontMcpTool({
|
|
@@ -44824,6 +45126,53 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44824
45126
|
}
|
|
44825
45127
|
});
|
|
44826
45128
|
|
|
45129
|
+
// libs/sdk/src/job/job-permission.guard.ts
|
|
45130
|
+
var JobPermissionGuard;
|
|
45131
|
+
var init_job_permission_guard = __esm({
|
|
45132
|
+
"libs/sdk/src/job/job-permission.guard.ts"() {
|
|
45133
|
+
"use strict";
|
|
45134
|
+
init_principal_utils();
|
|
45135
|
+
JobPermissionGuard = class _JobPermissionGuard {
|
|
45136
|
+
/**
|
|
45137
|
+
* Check whether the caller may perform `action`.
|
|
45138
|
+
*
|
|
45139
|
+
* @param permissions - the entry's declared rules (undefined = unrestricted).
|
|
45140
|
+
* @param action - the action being attempted.
|
|
45141
|
+
* @param authInfo - the request's AuthInfo.
|
|
45142
|
+
* @param contextBuilder - the scope's authorities context builder, when configured.
|
|
45143
|
+
*/
|
|
45144
|
+
static async check(permissions, action, authInfo, contextBuilder) {
|
|
45145
|
+
if (!permissions || permissions.length === 0) {
|
|
45146
|
+
return true;
|
|
45147
|
+
}
|
|
45148
|
+
const relevant = permissions.filter((p) => p.action === action);
|
|
45149
|
+
if (relevant.length === 0) {
|
|
45150
|
+
return true;
|
|
45151
|
+
}
|
|
45152
|
+
const principal = resolvePrincipal2(authInfo, contextBuilder);
|
|
45153
|
+
for (const perm of relevant) {
|
|
45154
|
+
const passed = await _JobPermissionGuard.checkSingle(perm, principal, authInfo);
|
|
45155
|
+
if (!passed) return false;
|
|
45156
|
+
}
|
|
45157
|
+
return true;
|
|
45158
|
+
}
|
|
45159
|
+
static async checkSingle(perm, principal, authInfo) {
|
|
45160
|
+
if (perm.roles && perm.roles.length > 0) {
|
|
45161
|
+
if (!perm.roles.some((role) => principal.roles.includes(role))) return false;
|
|
45162
|
+
}
|
|
45163
|
+
if (perm.scopes && perm.scopes.length > 0) {
|
|
45164
|
+
if (!perm.scopes.some((scope) => principal.scopes.includes(scope))) return false;
|
|
45165
|
+
}
|
|
45166
|
+
if (perm.custom) {
|
|
45167
|
+
const result = await perm.custom(authInfo ?? {});
|
|
45168
|
+
if (!result) return false;
|
|
45169
|
+
}
|
|
45170
|
+
return true;
|
|
45171
|
+
}
|
|
45172
|
+
};
|
|
45173
|
+
}
|
|
45174
|
+
});
|
|
45175
|
+
|
|
44827
45176
|
// libs/sdk/src/workflow/tools/list-workflows.tool.ts
|
|
44828
45177
|
var import_lazy_zod83, ListWorkflowsTool;
|
|
44829
45178
|
var init_list_workflows_tool = __esm({
|
|
@@ -44831,6 +45180,7 @@ var init_list_workflows_tool = __esm({
|
|
|
44831
45180
|
"use strict";
|
|
44832
45181
|
import_lazy_zod83 = require("@frontmcp/lazy-zod");
|
|
44833
45182
|
init_common();
|
|
45183
|
+
init_job_permission_guard();
|
|
44834
45184
|
ListWorkflowsTool = class extends ToolContext {
|
|
44835
45185
|
async execute(input) {
|
|
44836
45186
|
const scope = this.scope;
|
|
@@ -44842,7 +45192,11 @@ var init_list_workflows_tool = __esm({
|
|
|
44842
45192
|
tags: input.tags,
|
|
44843
45193
|
labels: input.labels
|
|
44844
45194
|
});
|
|
44845
|
-
const
|
|
45195
|
+
const visible = [];
|
|
45196
|
+
for (const workflow of workflows) {
|
|
45197
|
+
if (await this.maySee(workflow)) visible.push(workflow);
|
|
45198
|
+
}
|
|
45199
|
+
const mapped = visible.map((w) => ({
|
|
44846
45200
|
name: w.name,
|
|
44847
45201
|
description: w.metadata.description,
|
|
44848
45202
|
trigger: w.getTrigger(),
|
|
@@ -44854,6 +45208,12 @@ var init_list_workflows_tool = __esm({
|
|
|
44854
45208
|
count: mapped.length
|
|
44855
45209
|
};
|
|
44856
45210
|
}
|
|
45211
|
+
async maySee(workflow) {
|
|
45212
|
+
const permissions = workflow.metadata.permissions;
|
|
45213
|
+
const builder = this.scope.authoritiesContextBuilder;
|
|
45214
|
+
if (!await JobPermissionGuard.check(permissions, "list", this.authInfo, builder)) return false;
|
|
45215
|
+
return JobPermissionGuard.check(permissions, "execute", this.authInfo, builder);
|
|
45216
|
+
}
|
|
44857
45217
|
};
|
|
44858
45218
|
ListWorkflowsTool = __decorateClass([
|
|
44859
45219
|
FrontMcpTool({
|
|
@@ -44889,6 +45249,7 @@ var init_register_workflow_tool = __esm({
|
|
|
44889
45249
|
"use strict";
|
|
44890
45250
|
import_lazy_zod84 = require("@frontmcp/lazy-zod");
|
|
44891
45251
|
init_common();
|
|
45252
|
+
init_errors();
|
|
44892
45253
|
RegisterWorkflowTool = class extends ToolContext {
|
|
44893
45254
|
async execute(input) {
|
|
44894
45255
|
const scope = this.scope;
|
|
@@ -44896,6 +45257,9 @@ var init_register_workflow_tool = __esm({
|
|
|
44896
45257
|
if (!workflowRegistry) {
|
|
44897
45258
|
return this.fail(new Error("Workflows system is not enabled"));
|
|
44898
45259
|
}
|
|
45260
|
+
if (!this.dynamicRegistrationEnabled()) {
|
|
45261
|
+
return this.fail(new DynamicJobRegistrationDisabledError("workflow"));
|
|
45262
|
+
}
|
|
44899
45263
|
const existing = workflowRegistry.findByName(input.name);
|
|
44900
45264
|
if (existing) {
|
|
44901
45265
|
return this.fail(new Error(`Workflow "${input.name}" already exists`));
|
|
@@ -44917,6 +45281,10 @@ var init_register_workflow_tool = __esm({
|
|
|
44917
45281
|
});
|
|
44918
45282
|
return { success: true, workflowId };
|
|
44919
45283
|
}
|
|
45284
|
+
dynamicRegistrationEnabled() {
|
|
45285
|
+
const jobs = this.scope.metadata?.jobs;
|
|
45286
|
+
return jobs?.allowDynamicRegistration === true;
|
|
45287
|
+
}
|
|
44920
45288
|
};
|
|
44921
45289
|
RegisterWorkflowTool = __decorateClass([
|
|
44922
45290
|
FrontMcpTool({
|
|
@@ -44955,6 +45323,8 @@ var init_remove_workflow_tool = __esm({
|
|
|
44955
45323
|
"use strict";
|
|
44956
45324
|
import_lazy_zod85 = require("@frontmcp/lazy-zod");
|
|
44957
45325
|
init_common();
|
|
45326
|
+
init_errors();
|
|
45327
|
+
init_job_permission_guard();
|
|
44958
45328
|
RemoveWorkflowTool = class extends ToolContext {
|
|
44959
45329
|
async execute(input) {
|
|
44960
45330
|
const scope = this.scope;
|
|
@@ -44964,7 +45334,16 @@ var init_remove_workflow_tool = __esm({
|
|
|
44964
45334
|
}
|
|
44965
45335
|
const workflow = workflowRegistry.findByName(input.name);
|
|
44966
45336
|
if (!workflow) {
|
|
44967
|
-
return this.fail(new
|
|
45337
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
45338
|
+
}
|
|
45339
|
+
const allowed = await JobPermissionGuard.check(
|
|
45340
|
+
workflow.metadata.permissions,
|
|
45341
|
+
"delete",
|
|
45342
|
+
this.authInfo,
|
|
45343
|
+
this.scope.authoritiesContextBuilder
|
|
45344
|
+
);
|
|
45345
|
+
if (!allowed) {
|
|
45346
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
44968
45347
|
}
|
|
44969
45348
|
if (!workflow.isDynamic()) {
|
|
44970
45349
|
return this.fail(new Error(`Workflow "${input.name}" is not dynamic and cannot be removed`));
|
|
@@ -45574,7 +45953,10 @@ var init_job_execution_manager = __esm({
|
|
|
45574
45953
|
"libs/sdk/src/job/execution/job-execution.manager.ts"() {
|
|
45575
45954
|
"use strict";
|
|
45576
45955
|
import_utils79 = require("@frontmcp/utils");
|
|
45956
|
+
init_principal_utils();
|
|
45957
|
+
init_errors();
|
|
45577
45958
|
init_workflow_engine();
|
|
45959
|
+
init_job_permission_guard();
|
|
45578
45960
|
JobExecutionManager = class {
|
|
45579
45961
|
stateStore;
|
|
45580
45962
|
logger;
|
|
@@ -45588,6 +45970,7 @@ var init_job_execution_manager = __esm({
|
|
|
45588
45970
|
* Execute a job inline (synchronous) or in background.
|
|
45589
45971
|
*/
|
|
45590
45972
|
async executeJob(job, input, opts = {}) {
|
|
45973
|
+
await this.assertMayExecute(job.metadata.permissions, job.name, opts);
|
|
45591
45974
|
const runId = (0, import_utils79.randomUUID)();
|
|
45592
45975
|
const retryConfig = job.metadata.retry ?? {};
|
|
45593
45976
|
const maxAttempts = retryConfig.maxAttempts ?? 1;
|
|
@@ -45596,6 +45979,7 @@ var init_job_execution_manager = __esm({
|
|
|
45596
45979
|
jobId: job.metadata.id ?? job.name,
|
|
45597
45980
|
jobName: job.name,
|
|
45598
45981
|
sessionId: opts.sessionId,
|
|
45982
|
+
ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
|
|
45599
45983
|
state: "pending",
|
|
45600
45984
|
input,
|
|
45601
45985
|
startedAt: Date.now(),
|
|
@@ -45626,6 +46010,7 @@ var init_job_execution_manager = __esm({
|
|
|
45626
46010
|
* Execute a workflow inline or in background.
|
|
45627
46011
|
*/
|
|
45628
46012
|
async executeWorkflow(workflow, jobRegistry, opts = {}) {
|
|
46013
|
+
await this.assertMayExecute(workflow.metadata.permissions, workflow.name, opts);
|
|
45629
46014
|
const runId = (0, import_utils79.randomUUID)();
|
|
45630
46015
|
const runRecord = {
|
|
45631
46016
|
runId,
|
|
@@ -45633,6 +46018,7 @@ var init_job_execution_manager = __esm({
|
|
|
45633
46018
|
jobName: workflow.name,
|
|
45634
46019
|
workflowName: workflow.name,
|
|
45635
46020
|
sessionId: opts.sessionId,
|
|
46021
|
+
ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
|
|
45636
46022
|
state: "pending",
|
|
45637
46023
|
input: opts.workflowInput,
|
|
45638
46024
|
startedAt: Date.now(),
|
|
@@ -45660,6 +46046,25 @@ var init_job_execution_manager = __esm({
|
|
|
45660
46046
|
}
|
|
45661
46047
|
return this.executeWorkflowInline(workflow, jobRegistry, runId, opts);
|
|
45662
46048
|
}
|
|
46049
|
+
/**
|
|
46050
|
+
* Throw unless the caller may execute the entry.
|
|
46051
|
+
*
|
|
46052
|
+
* Uses the same error for "not authorized" as the tools use for "not found",
|
|
46053
|
+
* so a caller cannot enumerate which restricted jobs exist by comparing
|
|
46054
|
+
* responses.
|
|
46055
|
+
*/
|
|
46056
|
+
async assertMayExecute(permissions, name42, opts) {
|
|
46057
|
+
const allowed = await JobPermissionGuard.check(
|
|
46058
|
+
permissions,
|
|
46059
|
+
"execute",
|
|
46060
|
+
opts.authInfo,
|
|
46061
|
+
opts.authoritiesContextBuilder
|
|
46062
|
+
);
|
|
46063
|
+
if (!allowed) {
|
|
46064
|
+
this.logger.warn(`Execution of "${name42}" denied: caller does not satisfy its execute permissions`);
|
|
46065
|
+
throw new JobNotAuthorizedError(name42);
|
|
46066
|
+
}
|
|
46067
|
+
}
|
|
45663
46068
|
/**
|
|
45664
46069
|
* Get execution status.
|
|
45665
46070
|
*/
|
|
@@ -46685,6 +47090,7 @@ var init_execute_job_tool = __esm({
|
|
|
46685
47090
|
"use strict";
|
|
46686
47091
|
import_lazy_zod88 = require("@frontmcp/lazy-zod");
|
|
46687
47092
|
init_common();
|
|
47093
|
+
init_errors();
|
|
46688
47094
|
ExecuteJobTool = class extends ToolContext {
|
|
46689
47095
|
async execute(input) {
|
|
46690
47096
|
const scope = this.scope;
|
|
@@ -46695,12 +47101,13 @@ var init_execute_job_tool = __esm({
|
|
|
46695
47101
|
}
|
|
46696
47102
|
const job = jobRegistry.findByName(input.name);
|
|
46697
47103
|
if (!job) {
|
|
46698
|
-
return this.fail(new
|
|
47104
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
46699
47105
|
}
|
|
46700
47106
|
const result = await executionManager.executeJob(job, input.input ?? {}, {
|
|
46701
47107
|
background: input.background,
|
|
46702
47108
|
sessionId: this.authInfo.sessionId,
|
|
46703
|
-
authInfo: this.authInfo
|
|
47109
|
+
authInfo: this.authInfo,
|
|
47110
|
+
authoritiesContextBuilder: this.scope.authoritiesContextBuilder
|
|
46704
47111
|
});
|
|
46705
47112
|
return result;
|
|
46706
47113
|
}
|
|
@@ -46732,6 +47139,7 @@ var init_get_job_status_tool = __esm({
|
|
|
46732
47139
|
"use strict";
|
|
46733
47140
|
import_lazy_zod89 = require("@frontmcp/lazy-zod");
|
|
46734
47141
|
init_common();
|
|
47142
|
+
init_principal_utils();
|
|
46735
47143
|
init_errors();
|
|
46736
47144
|
GetJobStatusTool = class extends ToolContext {
|
|
46737
47145
|
async execute(input) {
|
|
@@ -46741,7 +47149,7 @@ var init_get_job_status_tool = __esm({
|
|
|
46741
47149
|
return this.fail(new GenericServerError("Jobs system is not enabled"));
|
|
46742
47150
|
}
|
|
46743
47151
|
const record = await executionManager.getStatus(input.runId);
|
|
46744
|
-
if (!record) {
|
|
47152
|
+
if (!record || !this.ownsRun(record)) {
|
|
46745
47153
|
return this.fail(new InvalidInputError(`Run "${input.runId}" not found`));
|
|
46746
47154
|
}
|
|
46747
47155
|
return {
|
|
@@ -46756,6 +47164,23 @@ var init_get_job_status_tool = __esm({
|
|
|
46756
47164
|
logs: record.logs
|
|
46757
47165
|
};
|
|
46758
47166
|
}
|
|
47167
|
+
/**
|
|
47168
|
+
* Match on the owning subject, falling back to the session for runs started
|
|
47169
|
+
* before an owner was recorded (and for anonymous/public servers, where every
|
|
47170
|
+
* caller has an empty subject and the session is the only identity there is).
|
|
47171
|
+
*
|
|
47172
|
+
* A record carrying NEITHER identity is refused. Such a run cannot be
|
|
47173
|
+
* attributed to anyone, and its inputs and results are exactly what this check
|
|
47174
|
+
* exists to protect — anyone holding the id would otherwise be able to read
|
|
47175
|
+
* it. Internal callers that must read unattributed runs should go through the
|
|
47176
|
+
* execution manager directly rather than this tool.
|
|
47177
|
+
*/
|
|
47178
|
+
ownsRun(record) {
|
|
47179
|
+
const callerSub = resolvePrincipal2(this.authInfo, this.scope.authoritiesContextBuilder).sub;
|
|
47180
|
+
if (record.ownerSub) return record.ownerSub === callerSub;
|
|
47181
|
+
if (record.sessionId) return record.sessionId === this.authInfo.sessionId;
|
|
47182
|
+
return false;
|
|
47183
|
+
}
|
|
46759
47184
|
};
|
|
46760
47185
|
GetJobStatusTool = __decorateClass([
|
|
46761
47186
|
FrontMcpTool({
|
|
@@ -46790,6 +47215,7 @@ var init_list_jobs_tool = __esm({
|
|
|
46790
47215
|
"use strict";
|
|
46791
47216
|
import_lazy_zod90 = require("@frontmcp/lazy-zod");
|
|
46792
47217
|
init_common();
|
|
47218
|
+
init_job_permission_guard();
|
|
46793
47219
|
ListJobsTool = class extends ToolContext {
|
|
46794
47220
|
async execute(input) {
|
|
46795
47221
|
const jobRegistry = this.scope.jobs;
|
|
@@ -46800,7 +47226,11 @@ var init_list_jobs_tool = __esm({
|
|
|
46800
47226
|
tags: input.tags,
|
|
46801
47227
|
labels: input.labels
|
|
46802
47228
|
});
|
|
46803
|
-
const
|
|
47229
|
+
const visible = [];
|
|
47230
|
+
for (const job of jobs) {
|
|
47231
|
+
if (await this.maySee(job)) visible.push(job);
|
|
47232
|
+
}
|
|
47233
|
+
const mapped = visible.map((j) => ({
|
|
46804
47234
|
name: j.name,
|
|
46805
47235
|
description: j.metadata.description,
|
|
46806
47236
|
tags: j.getTags(),
|
|
@@ -46812,6 +47242,17 @@ var init_list_jobs_tool = __esm({
|
|
|
46812
47242
|
count: mapped.length
|
|
46813
47243
|
};
|
|
46814
47244
|
}
|
|
47245
|
+
/**
|
|
47246
|
+
* A job is listable when the caller passes its `list` rules and is not
|
|
47247
|
+
* blocked from executing it. Checking `execute` too keeps the listing honest:
|
|
47248
|
+
* showing a job the caller can never run is the disclosure this closes.
|
|
47249
|
+
*/
|
|
47250
|
+
async maySee(job) {
|
|
47251
|
+
const permissions = job.metadata.permissions;
|
|
47252
|
+
const builder = this.scope.authoritiesContextBuilder;
|
|
47253
|
+
if (!await JobPermissionGuard.check(permissions, "list", this.authInfo, builder)) return false;
|
|
47254
|
+
return JobPermissionGuard.check(permissions, "execute", this.authInfo, builder);
|
|
47255
|
+
}
|
|
46815
47256
|
};
|
|
46816
47257
|
ListJobsTool = __decorateClass([
|
|
46817
47258
|
FrontMcpTool({
|
|
@@ -46847,6 +47288,7 @@ var init_register_job_tool = __esm({
|
|
|
46847
47288
|
"use strict";
|
|
46848
47289
|
import_lazy_zod91 = require("@frontmcp/lazy-zod");
|
|
46849
47290
|
init_common();
|
|
47291
|
+
init_errors();
|
|
46850
47292
|
RegisterJobTool = class extends ToolContext {
|
|
46851
47293
|
async execute(input) {
|
|
46852
47294
|
const scope = this.scope;
|
|
@@ -46854,6 +47296,9 @@ var init_register_job_tool = __esm({
|
|
|
46854
47296
|
if (!jobRegistry) {
|
|
46855
47297
|
return this.fail(new Error("Jobs system is not enabled"));
|
|
46856
47298
|
}
|
|
47299
|
+
if (!this.dynamicRegistrationEnabled()) {
|
|
47300
|
+
return this.fail(new DynamicJobRegistrationDisabledError("job"));
|
|
47301
|
+
}
|
|
46857
47302
|
const existing = jobRegistry.findByName(input.name);
|
|
46858
47303
|
if (existing) {
|
|
46859
47304
|
return this.fail(new Error(`Job "${input.name}" already exists`));
|
|
@@ -46876,6 +47321,10 @@ var init_register_job_tool = __esm({
|
|
|
46876
47321
|
});
|
|
46877
47322
|
return { success: true, jobId };
|
|
46878
47323
|
}
|
|
47324
|
+
dynamicRegistrationEnabled() {
|
|
47325
|
+
const jobs = this.scope.metadata?.jobs;
|
|
47326
|
+
return jobs?.allowDynamicRegistration === true;
|
|
47327
|
+
}
|
|
46879
47328
|
};
|
|
46880
47329
|
RegisterJobTool = __decorateClass([
|
|
46881
47330
|
FrontMcpTool({
|
|
@@ -46906,6 +47355,8 @@ var init_remove_job_tool = __esm({
|
|
|
46906
47355
|
"use strict";
|
|
46907
47356
|
import_lazy_zod92 = require("@frontmcp/lazy-zod");
|
|
46908
47357
|
init_common();
|
|
47358
|
+
init_errors();
|
|
47359
|
+
init_job_permission_guard();
|
|
46909
47360
|
RemoveJobTool = class extends ToolContext {
|
|
46910
47361
|
async execute(input) {
|
|
46911
47362
|
const scope = this.scope;
|
|
@@ -46915,7 +47366,16 @@ var init_remove_job_tool = __esm({
|
|
|
46915
47366
|
}
|
|
46916
47367
|
const job = jobRegistry.findByName(input.name);
|
|
46917
47368
|
if (!job) {
|
|
46918
|
-
return this.fail(new
|
|
47369
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
47370
|
+
}
|
|
47371
|
+
const allowed = await JobPermissionGuard.check(
|
|
47372
|
+
job.metadata.permissions,
|
|
47373
|
+
"delete",
|
|
47374
|
+
this.authInfo,
|
|
47375
|
+
this.scope.authoritiesContextBuilder
|
|
47376
|
+
);
|
|
47377
|
+
if (!allowed) {
|
|
47378
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
46919
47379
|
}
|
|
46920
47380
|
if (!job.isDynamic()) {
|
|
46921
47381
|
return this.fail(new Error(`Job "${input.name}" is not a dynamic job and cannot be removed`));
|
|
@@ -46976,14 +47436,18 @@ async function registerJobCapabilities(args) {
|
|
|
46976
47436
|
ListJobsTool,
|
|
46977
47437
|
ExecuteJobTool,
|
|
46978
47438
|
GetJobStatusTool,
|
|
46979
|
-
RegisterJobTool,
|
|
46980
47439
|
RemoveJobTool,
|
|
46981
47440
|
ListWorkflowsTool,
|
|
46982
47441
|
ExecuteWorkflowTool,
|
|
46983
47442
|
GetWorkflowStatusTool,
|
|
46984
|
-
RegisterWorkflowTool,
|
|
46985
47443
|
RemoveWorkflowTool
|
|
46986
47444
|
];
|
|
47445
|
+
if (jobsConfig.allowDynamicRegistration === true) {
|
|
47446
|
+
managementTools.push(RegisterJobTool, RegisterWorkflowTool);
|
|
47447
|
+
logger.warn(
|
|
47448
|
+
"Dynamic job registration is ENABLED: register_job/register_workflow accept a raw script and register it as an executable job."
|
|
47449
|
+
);
|
|
47450
|
+
}
|
|
46987
47451
|
logger.info(
|
|
46988
47452
|
`Jobs initialized: ${jobRegistry.getJobs().length} jobs, ${workflowRegistry.getWorkflows().length} workflows`
|
|
46989
47453
|
);
|
|
@@ -51815,6 +52279,7 @@ var init_utils2 = __esm({
|
|
|
51815
52279
|
init_caller_dir_utils();
|
|
51816
52280
|
init_decide_request_intent_utils();
|
|
51817
52281
|
init_path_utils();
|
|
52282
|
+
init_principal_utils();
|
|
51818
52283
|
init_global_config_utils();
|
|
51819
52284
|
init_primitive_filter();
|
|
51820
52285
|
init_validate_remote_url();
|
|
@@ -52762,9 +53227,24 @@ var init_redis_event_store = __esm({
|
|
|
52762
53227
|
await client.expire(streamKey, this.ttlSeconds);
|
|
52763
53228
|
return `${streamId}:${redisId}`;
|
|
52764
53229
|
}
|
|
53230
|
+
/**
|
|
53231
|
+
* Resolve the stream an event belongs to.
|
|
53232
|
+
*
|
|
53233
|
+
* Optional in the upstream `EventStore` contract, and the transport's
|
|
53234
|
+
* 409-conflict guard (refusing a second live stream for the same id) is dead
|
|
53235
|
+
* code while no store provides it (GHSA-84j6-jc92-77jm).
|
|
53236
|
+
*/
|
|
53237
|
+
async getStreamIdForEventId(eventId) {
|
|
53238
|
+
const parsed = this.parseEventId(eventId);
|
|
53239
|
+
return parsed ? parsed[0] : void 0;
|
|
53240
|
+
}
|
|
52765
53241
|
async replayEventsAfter(lastEventId, { send }) {
|
|
53242
|
+
const parsed = this.parseEventId(lastEventId);
|
|
53243
|
+
if (!parsed) {
|
|
53244
|
+
return "default-stream";
|
|
53245
|
+
}
|
|
52766
53246
|
const client = await this.getClient();
|
|
52767
|
-
const [streamId, redisId] =
|
|
53247
|
+
const [streamId, redisId] = parsed;
|
|
52768
53248
|
const streamKey = `${this.keyPrefix}${streamId}`;
|
|
52769
53249
|
const entries = await client.xrange(streamKey, `(${redisId}`, "+");
|
|
52770
53250
|
for (const [id, fields] of entries) {
|
|
@@ -52783,16 +53263,32 @@ var init_redis_event_store = __esm({
|
|
|
52783
53263
|
* Event IDs have format: `{streamId}:{redisStreamEntryId}`
|
|
52784
53264
|
* Redis stream entry IDs have format: `{timestamp}-{sequence}`
|
|
52785
53265
|
*
|
|
53266
|
+
* The entry-id half is validated because it is interpolated into the `xrange`
|
|
53267
|
+
* range argument (`(${redisId}`), where a malformed value is a protocol error
|
|
53268
|
+
* rather than data.
|
|
53269
|
+
*
|
|
53270
|
+
* The stream-id half is NOT character-filtered. `xadd`/`xrange` take the key
|
|
53271
|
+
* as an exact argument — Redis applies no glob matching there — and ioredis
|
|
53272
|
+
* length-prefixes it, so no character is special. Filtering here would also
|
|
53273
|
+
* make `storeEvent`, which accepts any `StreamId`, able to mint ids this
|
|
53274
|
+
* function then refuses, silently breaking replay for that stream. Ownership
|
|
53275
|
+
* is enforced by the session-scoped facade (GHSA-84j6-jc92-77jm), not by
|
|
53276
|
+
* guessing at the id's characters.
|
|
53277
|
+
*
|
|
53278
|
+
* @returns the pair, or `undefined` when the id is malformed.
|
|
53279
|
+
*
|
|
52786
53280
|
* @example
|
|
52787
53281
|
* 'my-stream:1234567890123-0' -> ['my-stream', '1234567890123-0']
|
|
52788
53282
|
*/
|
|
52789
53283
|
parseEventId(eventId) {
|
|
52790
|
-
const
|
|
52791
|
-
|
|
52792
|
-
|
|
52793
|
-
|
|
52794
|
-
|
|
52795
|
-
|
|
53284
|
+
const value = String(eventId);
|
|
53285
|
+
const lastDash = value.lastIndexOf("-");
|
|
53286
|
+
const separator = lastDash === -1 ? value.lastIndexOf(":") : value.lastIndexOf(":", lastDash);
|
|
53287
|
+
if (separator <= 0 || separator === value.length - 1) return void 0;
|
|
53288
|
+
const streamId = value.slice(0, separator);
|
|
53289
|
+
const redisId = value.slice(separator + 1);
|
|
53290
|
+
if (!/^\d+(-\d+)?$/.test(redisId)) return void 0;
|
|
53291
|
+
return [streamId, redisId];
|
|
52796
53292
|
}
|
|
52797
53293
|
/**
|
|
52798
53294
|
* Close the Redis client connection.
|
|
@@ -52838,6 +53334,16 @@ var init_memory_event_store = __esm({
|
|
|
52838
53334
|
}
|
|
52839
53335
|
return id;
|
|
52840
53336
|
}
|
|
53337
|
+
/**
|
|
53338
|
+
* Resolve the stream an event belongs to.
|
|
53339
|
+
*
|
|
53340
|
+
* Optional in the upstream `EventStore` contract, and the transport's
|
|
53341
|
+
* 409-conflict guard (refusing a second live stream for the same id) is dead
|
|
53342
|
+
* code while no store provides it (GHSA-84j6-jc92-77jm).
|
|
53343
|
+
*/
|
|
53344
|
+
async getStreamIdForEventId(eventId) {
|
|
53345
|
+
return this.index.get(eventId)?.streamId;
|
|
53346
|
+
}
|
|
52841
53347
|
async replayEventsAfter(lastEventId, { send }) {
|
|
52842
53348
|
const meta = this.index.get(lastEventId);
|
|
52843
53349
|
if (!meta) {
|
|
@@ -52989,6 +53495,50 @@ var init_event_store_factory = __esm({
|
|
|
52989
53495
|
}
|
|
52990
53496
|
});
|
|
52991
53497
|
|
|
53498
|
+
// libs/sdk/src/transport/event-stores/session-scoped.event-store.ts
|
|
53499
|
+
function createSessionScopedEventStore(store, sessionId) {
|
|
53500
|
+
const prefix = `${sessionId}${SCOPE_SEPARATOR}`;
|
|
53501
|
+
const scope = (streamId) => `${prefix}${streamId}`;
|
|
53502
|
+
const unscope = (streamId) => {
|
|
53503
|
+
const value = String(streamId);
|
|
53504
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
53505
|
+
};
|
|
53506
|
+
const resolveOwnedStream = async (eventId) => {
|
|
53507
|
+
const lookup = store.getStreamIdForEventId;
|
|
53508
|
+
if (lookup) {
|
|
53509
|
+
const resolved = await lookup.call(store, eventId);
|
|
53510
|
+
return resolved !== void 0 && String(resolved).startsWith(prefix) ? resolved : void 0;
|
|
53511
|
+
}
|
|
53512
|
+
if (!String(eventId).startsWith(prefix)) return void 0;
|
|
53513
|
+
const value = String(eventId);
|
|
53514
|
+
const lastColon = value.lastIndexOf(":");
|
|
53515
|
+
return lastColon === -1 ? void 0 : value.slice(0, lastColon);
|
|
53516
|
+
};
|
|
53517
|
+
return {
|
|
53518
|
+
async storeEvent(streamId, message) {
|
|
53519
|
+
return store.storeEvent(scope(streamId), message);
|
|
53520
|
+
},
|
|
53521
|
+
async getStreamIdForEventId(eventId) {
|
|
53522
|
+
const owned = await resolveOwnedStream(eventId);
|
|
53523
|
+
return owned === void 0 ? void 0 : unscope(owned);
|
|
53524
|
+
},
|
|
53525
|
+
async replayEventsAfter(lastEventId, options) {
|
|
53526
|
+
const owned = await resolveOwnedStream(lastEventId);
|
|
53527
|
+
if (owned === void 0) {
|
|
53528
|
+
return "default-stream";
|
|
53529
|
+
}
|
|
53530
|
+
return unscope(await store.replayEventsAfter(lastEventId, options));
|
|
53531
|
+
}
|
|
53532
|
+
};
|
|
53533
|
+
}
|
|
53534
|
+
var SCOPE_SEPARATOR;
|
|
53535
|
+
var init_session_scoped_event_store = __esm({
|
|
53536
|
+
"libs/sdk/src/transport/event-stores/session-scoped.event-store.ts"() {
|
|
53537
|
+
"use strict";
|
|
53538
|
+
SCOPE_SEPARATOR = " ";
|
|
53539
|
+
}
|
|
53540
|
+
});
|
|
53541
|
+
|
|
52992
53542
|
// libs/sdk/src/transport/event-stores/index.ts
|
|
52993
53543
|
var init_event_stores = __esm({
|
|
52994
53544
|
"libs/sdk/src/transport/event-stores/index.ts"() {
|
|
@@ -54659,6 +55209,61 @@ var init_transport_error = __esm({
|
|
|
54659
55209
|
}
|
|
54660
55210
|
});
|
|
54661
55211
|
|
|
55212
|
+
// libs/sdk/src/server/security/host-validation.ts
|
|
55213
|
+
function normalizeHost(value) {
|
|
55214
|
+
const host = value.trim().toLowerCase();
|
|
55215
|
+
if (host.length === 0) return host;
|
|
55216
|
+
const portSeparator = host.startsWith("[") ? host.indexOf(":", host.indexOf("]")) : host.lastIndexOf(":");
|
|
55217
|
+
if (portSeparator === -1) return host;
|
|
55218
|
+
const name42 = host.slice(0, portSeparator);
|
|
55219
|
+
const port = host.slice(portSeparator + 1);
|
|
55220
|
+
if (port.length === 0 || !/^\d+$/.test(port)) return host;
|
|
55221
|
+
return DEFAULT_PORTS.has(port) ? name42 : `${name42}:${port}`;
|
|
55222
|
+
}
|
|
55223
|
+
function normalizeOrigin(value) {
|
|
55224
|
+
const origin = value.trim().toLowerCase();
|
|
55225
|
+
try {
|
|
55226
|
+
return new URL(origin).origin.toLowerCase();
|
|
55227
|
+
} catch {
|
|
55228
|
+
return origin;
|
|
55229
|
+
}
|
|
55230
|
+
}
|
|
55231
|
+
function compileHostValidation(rules) {
|
|
55232
|
+
return {
|
|
55233
|
+
hosts: rules.allowedHosts ? new Set(rules.allowedHosts.map(normalizeHost)) : void 0,
|
|
55234
|
+
origins: rules.allowedOrigins ? new Set(rules.allowedOrigins.map(normalizeOrigin)) : void 0
|
|
55235
|
+
};
|
|
55236
|
+
}
|
|
55237
|
+
function validateHostHeaders(headers, compiled) {
|
|
55238
|
+
if (compiled.hosts) {
|
|
55239
|
+
const host = headers.host;
|
|
55240
|
+
if (!host || !compiled.hosts.has(normalizeHost(host))) {
|
|
55241
|
+
return { status: 403, error: "Forbidden", message: "Invalid Host header" };
|
|
55242
|
+
}
|
|
55243
|
+
const forwardedHost = headers.forwardedHost;
|
|
55244
|
+
if (forwardedHost) {
|
|
55245
|
+
const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
55246
|
+
if (candidates.some((h2) => !compiled.hosts?.has(normalizeHost(h2)))) {
|
|
55247
|
+
return { status: 403, error: "Forbidden", message: "Invalid X-Forwarded-Host header" };
|
|
55248
|
+
}
|
|
55249
|
+
}
|
|
55250
|
+
}
|
|
55251
|
+
if (compiled.origins) {
|
|
55252
|
+
const origin = headers.origin;
|
|
55253
|
+
if (origin && !compiled.origins.has(normalizeOrigin(origin))) {
|
|
55254
|
+
return { status: 403, error: "Forbidden", message: "Invalid Origin header" };
|
|
55255
|
+
}
|
|
55256
|
+
}
|
|
55257
|
+
return void 0;
|
|
55258
|
+
}
|
|
55259
|
+
var DEFAULT_PORTS;
|
|
55260
|
+
var init_host_validation = __esm({
|
|
55261
|
+
"libs/sdk/src/server/security/host-validation.ts"() {
|
|
55262
|
+
"use strict";
|
|
55263
|
+
DEFAULT_PORTS = /* @__PURE__ */ new Set(["80", "443"]);
|
|
55264
|
+
}
|
|
55265
|
+
});
|
|
55266
|
+
|
|
54662
55267
|
// libs/sdk/src/transport/adapters/base-sse-transport.ts
|
|
54663
55268
|
var import_content_type, import_raw_body, import_protocol54, import_utils93, MAXIMUM_MESSAGE_SIZE, SSEServerTransport;
|
|
54664
55269
|
var init_base_sse_transport = __esm({
|
|
@@ -54669,6 +55274,7 @@ var init_base_sse_transport = __esm({
|
|
|
54669
55274
|
import_protocol54 = require("@frontmcp/protocol");
|
|
54670
55275
|
import_utils93 = require("@frontmcp/utils");
|
|
54671
55276
|
init_transport_errors();
|
|
55277
|
+
init_host_validation();
|
|
54672
55278
|
MAXIMUM_MESSAGE_SIZE = "4mb";
|
|
54673
55279
|
SSEServerTransport = class {
|
|
54674
55280
|
/**
|
|
@@ -54692,25 +55298,31 @@ var init_base_sse_transport = __esm({
|
|
|
54692
55298
|
onmessage;
|
|
54693
55299
|
/**
|
|
54694
55300
|
* Validates request headers for DNS rebinding protection.
|
|
55301
|
+
*
|
|
55302
|
+
* Since v1.7.2 the primary defence is the adapter-level Host validation, which
|
|
55303
|
+
* runs before routing and therefore already covers `/sse` and `/message`
|
|
55304
|
+
* (GHSA-mc9g-v2cp-vfff). These per-transport options remain for a host that
|
|
55305
|
+
* constructs the transport directly; they delegate to the same rules so the
|
|
55306
|
+
* two can never disagree about what a valid Host looks like.
|
|
55307
|
+
*
|
|
54695
55308
|
* @returns Error message if validation fails, undefined if validation passes.
|
|
54696
55309
|
*/
|
|
54697
55310
|
validateRequestHeaders(req) {
|
|
54698
55311
|
if (!this._options.enableDnsRebindingProtection) {
|
|
54699
55312
|
return void 0;
|
|
54700
55313
|
}
|
|
54701
|
-
|
|
54702
|
-
|
|
54703
|
-
|
|
54704
|
-
|
|
54705
|
-
|
|
54706
|
-
|
|
54707
|
-
|
|
54708
|
-
|
|
54709
|
-
|
|
54710
|
-
|
|
54711
|
-
|
|
54712
|
-
|
|
54713
|
-
return void 0;
|
|
55314
|
+
const rejection = validateHostHeaders(
|
|
55315
|
+
{
|
|
55316
|
+
host: req.headers.host,
|
|
55317
|
+
forwardedHost: req.headers["x-forwarded-host"],
|
|
55318
|
+
origin: req.headers.origin
|
|
55319
|
+
},
|
|
55320
|
+
compileHostValidation({
|
|
55321
|
+
allowedHosts: this._options.allowedHosts?.length ? this._options.allowedHosts : void 0,
|
|
55322
|
+
allowedOrigins: this._options.allowedOrigins?.length ? this._options.allowedOrigins : void 0
|
|
55323
|
+
})
|
|
55324
|
+
);
|
|
55325
|
+
return rejection?.message;
|
|
54714
55326
|
}
|
|
54715
55327
|
/**
|
|
54716
55328
|
* Handles the initial SSE connection request.
|
|
@@ -55208,7 +55820,7 @@ var init_transport_local_adapter = __esm({
|
|
|
55208
55820
|
}
|
|
55209
55821
|
}
|
|
55210
55822
|
ensureAuthInfo(req, transport) {
|
|
55211
|
-
const { token, user, session } = req[ServerRequestTokens.auth];
|
|
55823
|
+
const { token, user, session, scopes, claims } = req[ServerRequestTokens.auth];
|
|
55212
55824
|
if (!session?.id) {
|
|
55213
55825
|
throw new UnauthorizedError(
|
|
55214
55826
|
"Session could not be reconstructed for the provided token. Re-authenticate to start a new session.",
|
|
@@ -55237,7 +55849,16 @@ var init_transport_local_adapter = __esm({
|
|
|
55237
55849
|
user,
|
|
55238
55850
|
sessionId,
|
|
55239
55851
|
sessionIdPayload: sessionPayload,
|
|
55240
|
-
scopes
|
|
55852
|
+
// Carry the scopes and claims the verified Authorization actually holds.
|
|
55853
|
+
// These used to be dropped (`scopes: []`, no claims), which left every
|
|
55854
|
+
// scope- or claim-based check downstream inert.
|
|
55855
|
+
//
|
|
55856
|
+
// Both are COPIED, not aliased. Tool code receives this object, and a
|
|
55857
|
+
// nested mutation (`authInfo.claims.roles.push('admin')`) on a shared
|
|
55858
|
+
// reference would rewrite the request's own verified authorization — which
|
|
55859
|
+
// the job/workflow permission guard then reads.
|
|
55860
|
+
scopes: Array.isArray(scopes) ? [...scopes] : [],
|
|
55861
|
+
...claims ? { claims: structuredClone(claims) } : {},
|
|
55241
55862
|
clientId: user.sub ?? "",
|
|
55242
55863
|
transport
|
|
55243
55864
|
};
|
|
@@ -55718,6 +56339,7 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
55718
56339
|
"use strict";
|
|
55719
56340
|
import_lazy_zod105 = require("@frontmcp/lazy-zod");
|
|
55720
56341
|
init_errors();
|
|
56342
|
+
init_session_scoped_event_store();
|
|
55721
56343
|
init_transport_error();
|
|
55722
56344
|
init_streamable_http_transport();
|
|
55723
56345
|
init_transport_local_adapter();
|
|
@@ -55727,7 +56349,8 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
55727
56349
|
TransportStreamableHttpAdapter = class extends LocalTransportAdapter {
|
|
55728
56350
|
createTransport(sessionId, response) {
|
|
55729
56351
|
const sessionIdGenerator = resolveSessionIdGenerator(this.key.type, sessionId);
|
|
55730
|
-
const
|
|
56352
|
+
const sharedEventStore = this.scope.eventStore;
|
|
56353
|
+
const eventStore = sharedEventStore ? createSessionScopedEventStore(sharedEventStore, sessionId) : void 0;
|
|
55731
56354
|
const isStateless = this.key.type === "stateless-http";
|
|
55732
56355
|
return new RecreateableStreamableHTTPServerTransport({
|
|
55733
56356
|
sessionIdGenerator,
|
|
@@ -56784,11 +57407,11 @@ function parseUserScopes(user) {
|
|
|
56784
57407
|
const s = user?.scope;
|
|
56785
57408
|
return typeof s === "string" ? s.split(/\s+/).filter(Boolean) : [];
|
|
56786
57409
|
}
|
|
56787
|
-
var
|
|
57410
|
+
var import_auth39, import_lazy_zod106, import_utils96, plan40, httpRequestStateSchema, name41, Stage41, HttpRequestFlow;
|
|
56788
57411
|
var init_http_request_flow = __esm({
|
|
56789
57412
|
"libs/sdk/src/scope/flows/http.request.flow.ts"() {
|
|
56790
57413
|
"use strict";
|
|
56791
|
-
|
|
57414
|
+
import_auth39 = require("@frontmcp/auth");
|
|
56792
57415
|
import_lazy_zod106 = require("@frontmcp/lazy-zod");
|
|
56793
57416
|
import_utils96 = require("@frontmcp/utils");
|
|
56794
57417
|
init_session_verify_flow();
|
|
@@ -56982,20 +57605,20 @@ var init_http_request_flow = __esm({
|
|
|
56982
57605
|
const tokenStore = auth.orchestratedTokenStore;
|
|
56983
57606
|
let providers;
|
|
56984
57607
|
try {
|
|
56985
|
-
const authorizationId = (0,
|
|
57608
|
+
const authorizationId = (0, import_auth39.deriveAuthorizationId)(token);
|
|
56986
57609
|
const providerIds = await tokenStore.getProviderIds(authorizationId);
|
|
56987
57610
|
providers = Object.fromEntries(providerIds.map((id) => [id, { id }]));
|
|
56988
57611
|
} catch (err) {
|
|
56989
57612
|
this.logger.warn(`[${this.requestId}] failed to load orchestrated providers: ${String(err)}`);
|
|
56990
57613
|
}
|
|
56991
|
-
const authorization =
|
|
57614
|
+
const authorization = import_auth39.OrchestratedAuthorization.create({
|
|
56992
57615
|
token,
|
|
56993
57616
|
user: { sub: user.sub ?? "unknown", name: user.name, email: user.email, picture: user.picture },
|
|
56994
57617
|
expiresAt: user.exp ? user.exp * 1e3 : void 0,
|
|
56995
57618
|
tokenStore,
|
|
56996
57619
|
providers
|
|
56997
57620
|
});
|
|
56998
|
-
ctx.setContextToken(
|
|
57621
|
+
ctx.setContextToken(import_auth39.ORCHESTRATED_AUTH_ACCESSOR, new import_auth39.OrchestratedAuthAccessorAdapter(authorization));
|
|
56999
57622
|
}
|
|
57000
57623
|
async router() {
|
|
57001
57624
|
try {
|
|
@@ -59606,7 +60229,13 @@ function normalizeAppScope(appItem, metadata) {
|
|
|
59606
60229
|
...metadata,
|
|
59607
60230
|
id: scopeId,
|
|
59608
60231
|
apps: [appItem],
|
|
59609
|
-
auth
|
|
60232
|
+
// An app that declares no `auth` INHERITS the server's policy. Writing
|
|
60233
|
+
// `appMetadata.auth` unconditionally set the key to `undefined`, which
|
|
60234
|
+
// overrode the spread above and dropped the server's auth entirely — so a
|
|
60235
|
+
// standalone app silently became public (GHSA-rgxj-434m-vxh3, where the
|
|
60236
|
+
// dashboard's own scope was the case in point). Declaring
|
|
60237
|
+
// `auth: { mode: 'public' }` explicitly is still honoured.
|
|
60238
|
+
auth: appMetadata.auth ?? metadata.auth,
|
|
59610
60239
|
// For a splitByApp scope, auth (and any auth.ui paths) come from `@App`, so
|
|
59611
60240
|
// anchor on the App's captured source dir. Fall back to the server's.
|
|
59612
60241
|
// `__sourceDir` lives on LocalAppMetadata only; read it defensively.
|
|
@@ -59870,6 +60499,11 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
59870
60499
|
const entryPaths = new Set((Array.isArray(rawEntry) ? rawEntry : [rawEntry]).map(normalizePath));
|
|
59871
60500
|
const cors2 = options.cors ?? mapHttpCors(httpConfig?.cors, scope);
|
|
59872
60501
|
const corsEnabled = cors2?.origin !== void 0 && cors2.origin !== false;
|
|
60502
|
+
const rebinding = httpConfig?.security?.dnsRebindingProtection;
|
|
60503
|
+
const hostValidation = rebinding?.enabled === false || !rebinding?.allowedHosts?.length && !rebinding?.allowedOrigins?.length ? void 0 : compileHostValidation({
|
|
60504
|
+
allowedHosts: rebinding.allowedHosts,
|
|
60505
|
+
allowedOrigins: rebinding.allowedOrigins
|
|
60506
|
+
});
|
|
59873
60507
|
const corsHeadersFor = (request) => {
|
|
59874
60508
|
if (!corsEnabled) return {};
|
|
59875
60509
|
const reqOrigin = request.headers.get("origin") ?? "";
|
|
@@ -59900,6 +60534,19 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
59900
60534
|
};
|
|
59901
60535
|
return async function handle(request, ctx, env) {
|
|
59902
60536
|
const url = new URL(request.url);
|
|
60537
|
+
if (hostValidation) {
|
|
60538
|
+
const rejection = validateHostHeaders(
|
|
60539
|
+
{
|
|
60540
|
+
host: request.headers.get("host") ?? url.host,
|
|
60541
|
+
forwardedHost: request.headers.get("x-forwarded-host") ?? void 0,
|
|
60542
|
+
origin: request.headers.get("origin") ?? void 0
|
|
60543
|
+
},
|
|
60544
|
+
hostValidation
|
|
60545
|
+
);
|
|
60546
|
+
if (rejection) {
|
|
60547
|
+
return Response.json({ error: rejection.error, message: rejection.message }, { status: rejection.status });
|
|
60548
|
+
}
|
|
60549
|
+
}
|
|
59903
60550
|
if (corsEnabled && request.method === "OPTIONS") {
|
|
59904
60551
|
return new Response(null, { status: 204, headers: corsHeadersFor(request) });
|
|
59905
60552
|
}
|
|
@@ -60033,6 +60680,7 @@ var init_web_fetch_handler = __esm({
|
|
|
60033
60680
|
init_server_tokens();
|
|
60034
60681
|
init_path_utils();
|
|
60035
60682
|
init_errors();
|
|
60683
|
+
init_host_validation();
|
|
60036
60684
|
init_web_response_renderer();
|
|
60037
60685
|
}
|
|
60038
60686
|
});
|
|
@@ -60069,26 +60717,20 @@ function auditSecurityDefaults(config, isProduction5) {
|
|
|
60069
60717
|
return findings;
|
|
60070
60718
|
}
|
|
60071
60719
|
const strict = config.security?.strict === true;
|
|
60072
|
-
|
|
60720
|
+
const corsOrigin = config.cors === false ? void 0 : config.cors?.origin;
|
|
60721
|
+
if (corsOrigin === void 0 || corsOrigin === false) {
|
|
60073
60722
|
findings.push({
|
|
60074
|
-
level: "
|
|
60075
|
-
code: "
|
|
60076
|
-
message: "CORS
|
|
60077
|
-
recommendation: "Set explicit cors.origin to restrict allowed origins in production."
|
|
60723
|
+
level: "info",
|
|
60724
|
+
code: "CORS_DISABLED",
|
|
60725
|
+
message: "No CORS headers are sent. Cross-origin requests still reach the server \u2014 a browser just will not let the calling page read the response. CORS is not server-side access control."
|
|
60078
60726
|
});
|
|
60079
|
-
} else if (
|
|
60727
|
+
} else if (corsOrigin === true) {
|
|
60080
60728
|
findings.push({
|
|
60081
60729
|
level: "warn",
|
|
60082
60730
|
code: "CORS_ORIGIN_TRUE",
|
|
60083
60731
|
message: "CORS origin=true allows all origins to make cross-origin requests.",
|
|
60084
60732
|
recommendation: "Set cors.origin to specific allowed origins."
|
|
60085
60733
|
});
|
|
60086
|
-
} else if (config.cors === false) {
|
|
60087
|
-
findings.push({
|
|
60088
|
-
level: "info",
|
|
60089
|
-
code: "CORS_DISABLED",
|
|
60090
|
-
message: "CORS is disabled. Cross-origin requests will be blocked by browsers."
|
|
60091
|
-
});
|
|
60092
60734
|
} else {
|
|
60093
60735
|
findings.push({
|
|
60094
60736
|
level: "info",
|
|
@@ -60096,7 +60738,7 @@ function auditSecurityDefaults(config, isProduction5) {
|
|
|
60096
60738
|
message: "CORS is explicitly configured."
|
|
60097
60739
|
});
|
|
60098
60740
|
}
|
|
60099
|
-
const bindAddress = config.resolvedBindAddress ?? "
|
|
60741
|
+
const bindAddress = config.resolvedBindAddress ?? "127.0.0.1";
|
|
60100
60742
|
if (bindAddress === "0.0.0.0" || bindAddress === "::") {
|
|
60101
60743
|
if (config.deploymentMode !== "distributed") {
|
|
60102
60744
|
findings.push({
|
|
@@ -60138,7 +60780,7 @@ function auditSecurityDefaults(config, isProduction5) {
|
|
|
60138
60780
|
findings.push({
|
|
60139
60781
|
level: "info",
|
|
60140
60782
|
code: "STRICT_MODE_ENABLED",
|
|
60141
|
-
message: "Strict security mode is enabled: loopback binding
|
|
60783
|
+
message: config.deploymentMode === "distributed" ? "Strict security mode is enabled: DNS rebinding protection. Binding is unchanged for a distributed deployment \u2014 it still listens on all interfaces so peers can reach it." : "Strict security mode is enabled: loopback binding and DNS rebinding protection."
|
|
60142
60784
|
});
|
|
60143
60785
|
} else {
|
|
60144
60786
|
findings.push({
|
|
@@ -60161,16 +60803,24 @@ function logSecurityFindings(findings, logger) {
|
|
|
60161
60803
|
}
|
|
60162
60804
|
}
|
|
60163
60805
|
}
|
|
60806
|
+
function toBindAddress(value) {
|
|
60807
|
+
if (value === "loopback") return "127.0.0.1";
|
|
60808
|
+
if (value === "all") return "0.0.0.0";
|
|
60809
|
+
return value;
|
|
60810
|
+
}
|
|
60164
60811
|
function resolveBindAddress(security, deploymentMode) {
|
|
60165
60812
|
if (security?.bindAddress) {
|
|
60166
|
-
|
|
60167
|
-
|
|
60168
|
-
|
|
60813
|
+
return toBindAddress(security.bindAddress);
|
|
60814
|
+
}
|
|
60815
|
+
const envBindAddress = process.env["FRONTMCP_BIND_ADDRESS"]?.trim();
|
|
60816
|
+
if (envBindAddress) {
|
|
60817
|
+
return toBindAddress(envBindAddress);
|
|
60169
60818
|
}
|
|
60170
60819
|
if (security?.strict) {
|
|
60171
60820
|
return deploymentMode === "distributed" ? "0.0.0.0" : "127.0.0.1";
|
|
60172
60821
|
}
|
|
60173
|
-
return "0.0.0.0";
|
|
60822
|
+
if (deploymentMode === "distributed") return "0.0.0.0";
|
|
60823
|
+
return "127.0.0.1";
|
|
60174
60824
|
}
|
|
60175
60825
|
var init_security_audit = __esm({
|
|
60176
60826
|
"libs/sdk/src/server/security/security-audit.ts"() {
|
|
@@ -60183,39 +60833,22 @@ function createHostValidationMiddleware(options) {
|
|
|
60183
60833
|
if (!options.enabled) {
|
|
60184
60834
|
return (_req, _res, next) => next();
|
|
60185
60835
|
}
|
|
60186
|
-
const
|
|
60187
|
-
|
|
60836
|
+
const compiled = compileHostValidation({
|
|
60837
|
+
allowedHosts: options.allowedHosts,
|
|
60838
|
+
allowedOrigins: options.allowedOrigins
|
|
60839
|
+
});
|
|
60188
60840
|
return (req, res, next) => {
|
|
60189
|
-
|
|
60190
|
-
|
|
60191
|
-
|
|
60192
|
-
|
|
60193
|
-
|
|
60194
|
-
|
|
60195
|
-
|
|
60196
|
-
|
|
60197
|
-
|
|
60198
|
-
|
|
60199
|
-
|
|
60200
|
-
const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
60201
|
-
if (candidates.some((h2) => !allowedHostsSet.has(h2))) {
|
|
60202
|
-
res.status(403).json({
|
|
60203
|
-
error: "Forbidden",
|
|
60204
|
-
message: "Invalid X-Forwarded-Host header"
|
|
60205
|
-
});
|
|
60206
|
-
return;
|
|
60207
|
-
}
|
|
60208
|
-
}
|
|
60209
|
-
}
|
|
60210
|
-
if (allowedOriginsSet) {
|
|
60211
|
-
const origin = req.headers?.["origin"];
|
|
60212
|
-
if (origin && !allowedOriginsSet.has(origin)) {
|
|
60213
|
-
res.status(403).json({
|
|
60214
|
-
error: "Forbidden",
|
|
60215
|
-
message: "Invalid Origin header"
|
|
60216
|
-
});
|
|
60217
|
-
return;
|
|
60218
|
-
}
|
|
60841
|
+
const rejection = validateHostHeaders(
|
|
60842
|
+
{
|
|
60843
|
+
host: req.headers?.["host"],
|
|
60844
|
+
forwardedHost: req.headers?.["x-forwarded-host"],
|
|
60845
|
+
origin: req.headers?.["origin"]
|
|
60846
|
+
},
|
|
60847
|
+
compiled
|
|
60848
|
+
);
|
|
60849
|
+
if (rejection) {
|
|
60850
|
+
res.status(rejection.status).json({ error: rejection.error, message: rejection.message });
|
|
60851
|
+
return;
|
|
60219
60852
|
}
|
|
60220
60853
|
next();
|
|
60221
60854
|
};
|
|
@@ -60223,6 +60856,82 @@ function createHostValidationMiddleware(options) {
|
|
|
60223
60856
|
var init_host_validation_middleware = __esm({
|
|
60224
60857
|
"libs/sdk/src/server/middleware/host-validation.middleware.ts"() {
|
|
60225
60858
|
"use strict";
|
|
60859
|
+
init_host_validation();
|
|
60860
|
+
}
|
|
60861
|
+
});
|
|
60862
|
+
|
|
60863
|
+
// libs/sdk/src/server/security/resolve-allowed-hosts.ts
|
|
60864
|
+
function canonicalizeAddress(address) {
|
|
60865
|
+
const bare = address.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
60866
|
+
if (!bare.includes(":")) return bare;
|
|
60867
|
+
try {
|
|
60868
|
+
return new URL(`http://[${bare}]`).hostname.replace(/^\[|\]$/g, "");
|
|
60869
|
+
} catch {
|
|
60870
|
+
return bare;
|
|
60871
|
+
}
|
|
60872
|
+
}
|
|
60873
|
+
function isLoopbackAddress(address) {
|
|
60874
|
+
if (!address) return false;
|
|
60875
|
+
const normalized = canonicalizeAddress(address);
|
|
60876
|
+
return normalized === "::1" || normalized === "localhost" || normalized.startsWith("127.") || // IPv4-mapped IPv6 loopback, which `URL` renders as `::ffff:7f00:1`.
|
|
60877
|
+
normalized === "::ffff:7f00:1" || normalized.startsWith("::ffff:127.");
|
|
60878
|
+
}
|
|
60879
|
+
function withAndWithoutPort(host, port) {
|
|
60880
|
+
return port ? [host, `${host}:${port}`] : [host];
|
|
60881
|
+
}
|
|
60882
|
+
function hostFromUrl(url) {
|
|
60883
|
+
if (!url) return void 0;
|
|
60884
|
+
try {
|
|
60885
|
+
return new URL(url).host || void 0;
|
|
60886
|
+
} catch {
|
|
60887
|
+
return void 0;
|
|
60888
|
+
}
|
|
60889
|
+
}
|
|
60890
|
+
function isRoutableBind(address) {
|
|
60891
|
+
if (!address) return false;
|
|
60892
|
+
return !isLoopbackAddress(address);
|
|
60893
|
+
}
|
|
60894
|
+
function deriveAllowedHosts(input) {
|
|
60895
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
60896
|
+
if (input.socketPath) return [];
|
|
60897
|
+
for (const name42 of LOOPBACK_NAMES) {
|
|
60898
|
+
for (const value of withAndWithoutPort(name42, input.port)) hosts.add(value);
|
|
60899
|
+
}
|
|
60900
|
+
const bindAddress = input.bindAddress?.trim();
|
|
60901
|
+
if (bindAddress && !isLoopbackAddress(bindAddress) && bindAddress !== "0.0.0.0" && bindAddress !== "::") {
|
|
60902
|
+
const literal = bindAddress.includes(":") && !bindAddress.startsWith("[") ? `[${bindAddress}]` : bindAddress;
|
|
60903
|
+
for (const value of withAndWithoutPort(literal, input.port)) hosts.add(value);
|
|
60904
|
+
}
|
|
60905
|
+
const issuerHost = hostFromUrl(input.issuer);
|
|
60906
|
+
if (issuerHost) {
|
|
60907
|
+
hosts.add(issuerHost);
|
|
60908
|
+
const [name42] = issuerHost.split(":");
|
|
60909
|
+
if (!issuerHost.includes(":") && input.port) hosts.add(`${name42}:${input.port}`);
|
|
60910
|
+
}
|
|
60911
|
+
for (const extra of input.extraHosts ?? []) {
|
|
60912
|
+
const trimmed = extra.trim();
|
|
60913
|
+
if (trimmed) hosts.add(trimmed);
|
|
60914
|
+
}
|
|
60915
|
+
return [...hosts];
|
|
60916
|
+
}
|
|
60917
|
+
function shouldEnforceDerivedHosts(input) {
|
|
60918
|
+
if (input.socketPath) return false;
|
|
60919
|
+
const hasExplicitName = hostFromUrl(input.issuer) !== void 0 || (input.extraHosts ?? []).some((h2) => h2.trim());
|
|
60920
|
+
if (hasExplicitName) return true;
|
|
60921
|
+
if (input.port === void 0) return false;
|
|
60922
|
+
return !isRoutableBind(input.bindAddress);
|
|
60923
|
+
}
|
|
60924
|
+
function allowedHostsFromEnv(env = process.env) {
|
|
60925
|
+
const raw = env["FRONTMCP_ALLOWED_HOSTS"]?.trim();
|
|
60926
|
+
if (!raw) return void 0;
|
|
60927
|
+
const hosts = raw.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
60928
|
+
return hosts.length > 0 ? hosts : void 0;
|
|
60929
|
+
}
|
|
60930
|
+
var LOOPBACK_NAMES;
|
|
60931
|
+
var init_resolve_allowed_hosts = __esm({
|
|
60932
|
+
"libs/sdk/src/server/security/resolve-allowed-hosts.ts"() {
|
|
60933
|
+
"use strict";
|
|
60934
|
+
LOOPBACK_NAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
60226
60935
|
}
|
|
60227
60936
|
});
|
|
60228
60937
|
|
|
@@ -60248,12 +60957,17 @@ var init_express_host_adapter = __esm({
|
|
|
60248
60957
|
import_utils98 = require("@frontmcp/utils");
|
|
60249
60958
|
init_mcp_error();
|
|
60250
60959
|
init_host_validation_middleware();
|
|
60960
|
+
init_resolve_allowed_hosts();
|
|
60251
60961
|
init_base_host_adapter();
|
|
60252
60962
|
DEFAULT_EXPRESS_BODY_LIMIT = "4mb";
|
|
60253
60963
|
ExpressHostAdapter = class extends HostServerAdapter {
|
|
60254
60964
|
app = (0, import_express.default)();
|
|
60255
60965
|
router = import_express.default.Router();
|
|
60256
60966
|
prepared = false;
|
|
60967
|
+
/** Active host-validation middleware, or undefined while nothing is enforced. */
|
|
60968
|
+
hostValidation;
|
|
60969
|
+
/** Builds the derived allow-list, once this process commits to listening. */
|
|
60970
|
+
deriveHostValidation;
|
|
60257
60971
|
constructor(options) {
|
|
60258
60972
|
super();
|
|
60259
60973
|
const corsOptions = options?.cors;
|
|
@@ -60267,6 +60981,7 @@ var init_express_host_adapter = __esm({
|
|
|
60267
60981
|
})
|
|
60268
60982
|
);
|
|
60269
60983
|
}
|
|
60984
|
+
this.installHostValidation(options);
|
|
60270
60985
|
const jsonLimit = options?.bodyLimit ?? DEFAULT_EXPRESS_BODY_LIMIT;
|
|
60271
60986
|
const formLimit = options?.urlencodedLimit ?? jsonLimit;
|
|
60272
60987
|
const headerContains = (req, needle) => (req.headers["content-type"] ?? "").toLowerCase().includes(needle);
|
|
@@ -60287,23 +61002,6 @@ var init_express_host_adapter = __esm({
|
|
|
60287
61002
|
}
|
|
60288
61003
|
next(err);
|
|
60289
61004
|
});
|
|
60290
|
-
const securityOpts = options?.security;
|
|
60291
|
-
if (securityOpts?.dnsRebindingProtection?.enabled || securityOpts?.strict) {
|
|
60292
|
-
const allowedHosts = securityOpts.dnsRebindingProtection?.allowedHosts;
|
|
60293
|
-
const allowedOrigins = securityOpts.dnsRebindingProtection?.allowedOrigins;
|
|
60294
|
-
const effectiveHosts = allowedHosts ?? (securityOpts.strict ? ["localhost", "127.0.0.1"] : void 0);
|
|
60295
|
-
if (!effectiveHosts?.length && !allowedOrigins?.length) {
|
|
60296
|
-
throw new Error(
|
|
60297
|
-
"security.dnsRebindingProtection is enabled but no allowedHosts or allowedOrigins are configured. Provide at least one allowedHosts entry or disable dnsRebindingProtection."
|
|
60298
|
-
);
|
|
60299
|
-
}
|
|
60300
|
-
const hostValidation = createHostValidationMiddleware({
|
|
60301
|
-
enabled: true,
|
|
60302
|
-
allowedHosts: effectiveHosts,
|
|
60303
|
-
allowedOrigins
|
|
60304
|
-
});
|
|
60305
|
-
this.app.use(hostValidation);
|
|
60306
|
-
}
|
|
60307
61005
|
this.app.use((req, res, next) => {
|
|
60308
61006
|
if (corsEnabled) {
|
|
60309
61007
|
res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate, Mcp-Session-Id");
|
|
@@ -60313,6 +61011,64 @@ var init_express_host_adapter = __esm({
|
|
|
60313
61011
|
next();
|
|
60314
61012
|
});
|
|
60315
61013
|
}
|
|
61014
|
+
/**
|
|
61015
|
+
* Install DNS-rebinding protection.
|
|
61016
|
+
*
|
|
61017
|
+
* ON by default since v1.7.2 (BC-035). `allowedHosts` falls back to the names
|
|
61018
|
+
* this process actually answers to — the loopback aliases with and without
|
|
61019
|
+
* the bound port, the bound NIC address, and the configured issuer host.
|
|
61020
|
+
*
|
|
61021
|
+
* Escape hatches, in precedence order: explicit
|
|
61022
|
+
* `security.dnsRebindingProtection.allowedHosts`, the
|
|
61023
|
+
* `FRONTMCP_ALLOWED_HOSTS` env var, or `enabled: false` to turn it off.
|
|
61024
|
+
*/
|
|
61025
|
+
installHostValidation(options) {
|
|
61026
|
+
const protection = options?.security?.dnsRebindingProtection;
|
|
61027
|
+
if (protection?.enabled === false) return;
|
|
61028
|
+
const allowedOrigins = protection?.allowedOrigins;
|
|
61029
|
+
const explicitHosts = protection?.allowedHosts ?? allowedHostsFromEnv();
|
|
61030
|
+
if (explicitHosts?.length || allowedOrigins?.length) {
|
|
61031
|
+
this.hostValidation = createHostValidationMiddleware({
|
|
61032
|
+
enabled: true,
|
|
61033
|
+
allowedHosts: explicitHosts?.length ? explicitHosts : void 0,
|
|
61034
|
+
allowedOrigins
|
|
61035
|
+
});
|
|
61036
|
+
} else {
|
|
61037
|
+
this.deriveHostValidation = (portOrSocketPath, bindAddress) => {
|
|
61038
|
+
const isSocket = typeof portOrSocketPath === "string";
|
|
61039
|
+
const derivation = {
|
|
61040
|
+
// `start()` binds `bindAddress ?? '0.0.0.0'`; mirror that exactly.
|
|
61041
|
+
bindAddress: isSocket ? void 0 : bindAddress ?? "0.0.0.0",
|
|
61042
|
+
port: isSocket ? void 0 : portOrSocketPath,
|
|
61043
|
+
socketPath: isSocket ? portOrSocketPath : void 0,
|
|
61044
|
+
issuer: options?.listen?.issuer
|
|
61045
|
+
};
|
|
61046
|
+
if (!shouldEnforceDerivedHosts(derivation)) {
|
|
61047
|
+
console.warn(
|
|
61048
|
+
`[frontmcp] DNS-rebinding protection is not enforcing a Host allow-list: the server binds a routable address (${derivation.bindAddress ?? "unknown"}) and no allowed hosts are configured. Set security.dnsRebindingProtection.allowedHosts (or FRONTMCP_ALLOWED_HOSTS) to your public hostname(s).`
|
|
61049
|
+
);
|
|
61050
|
+
return void 0;
|
|
61051
|
+
}
|
|
61052
|
+
const allowedHosts = deriveAllowedHosts(derivation);
|
|
61053
|
+
if (!allowedHosts.length) return void 0;
|
|
61054
|
+
return createHostValidationMiddleware({ enabled: true, allowedHosts });
|
|
61055
|
+
};
|
|
61056
|
+
}
|
|
61057
|
+
this.app.use(((req, res, next) => {
|
|
61058
|
+
const validate = this.hostValidation;
|
|
61059
|
+
if (!validate) return next();
|
|
61060
|
+
return validate(req, res, next);
|
|
61061
|
+
}));
|
|
61062
|
+
}
|
|
61063
|
+
/**
|
|
61064
|
+
* Enable the DERIVED host allow-list. Called from `start()` only — see
|
|
61065
|
+
* `installHostValidation`.
|
|
61066
|
+
*/
|
|
61067
|
+
enableDerivedHostValidation(portOrSocketPath, bindAddress) {
|
|
61068
|
+
if (this.hostValidation || !this.deriveHostValidation) return;
|
|
61069
|
+
this.hostValidation = this.deriveHostValidation(portOrSocketPath, bindAddress);
|
|
61070
|
+
this.deriveHostValidation = void 0;
|
|
61071
|
+
}
|
|
60316
61072
|
registerRoute(method, path3, handler) {
|
|
60317
61073
|
this.router[method.toLowerCase()](path3, this.enhancedHandler(handler));
|
|
60318
61074
|
}
|
|
@@ -60345,6 +61101,7 @@ var init_express_host_adapter = __esm({
|
|
|
60345
61101
|
return this.app;
|
|
60346
61102
|
}
|
|
60347
61103
|
async start(portOrSocketPath, bindAddress) {
|
|
61104
|
+
this.enableDerivedHostValidation(portOrSocketPath, bindAddress);
|
|
60348
61105
|
this.prepare();
|
|
60349
61106
|
const server = http.createServer(this.app);
|
|
60350
61107
|
server.requestTimeout = 0;
|
|
@@ -60410,7 +61167,7 @@ var init_server_instance = __esm({
|
|
|
60410
61167
|
init_health2();
|
|
60411
61168
|
init_metrics2();
|
|
60412
61169
|
init_security_audit();
|
|
60413
|
-
DEFAULT_CORS =
|
|
61170
|
+
DEFAULT_CORS = void 0;
|
|
60414
61171
|
FrontMcpServerInstance = class extends FrontMcpServer {
|
|
60415
61172
|
config;
|
|
60416
61173
|
host;
|
|
@@ -63234,6 +63991,7 @@ __export(index_exports, {
|
|
|
63234
63991
|
DynamicAdapter: () => DynamicAdapter,
|
|
63235
63992
|
DynamicAdapterNameError: () => DynamicAdapterNameError,
|
|
63236
63993
|
DynamicJobDirectExecutionError: () => DynamicJobDirectExecutionError,
|
|
63994
|
+
DynamicJobRegistrationDisabledError: () => DynamicJobRegistrationDisabledError,
|
|
63237
63995
|
DynamicPlugin: () => DynamicPlugin,
|
|
63238
63996
|
ELICITATION_FALLBACK_JSON_SCHEMA: () => ELICITATION_FALLBACK_JSON_SCHEMA,
|
|
63239
63997
|
ESM_SH_BASE_URL: () => ESM_SH_BASE_URL,
|
|
@@ -63357,7 +64115,7 @@ __export(index_exports, {
|
|
|
63357
64115
|
HttpTextSchema: () => HttpTextSchema,
|
|
63358
64116
|
HttpWebResponseSchema: () => HttpWebResponseSchema,
|
|
63359
64117
|
ImageOutputSchema: () => ImageOutputSchema,
|
|
63360
|
-
InMemoryOrchestratedTokenStore: () =>
|
|
64118
|
+
InMemoryOrchestratedTokenStore: () => import_auth41.InMemoryOrchestratedTokenStore,
|
|
63361
64119
|
InMemoryStoreRequiredError: () => import_auth11.InMemoryStoreRequiredError,
|
|
63362
64120
|
InProcessTaskRunner: () => InProcessTaskRunner,
|
|
63363
64121
|
InputRequiredSignal: () => InputRequiredSignal,
|
|
@@ -63388,6 +64146,7 @@ __export(index_exports, {
|
|
|
63388
64146
|
JobExecutionManager: () => JobExecutionManager,
|
|
63389
64147
|
JobInstance: () => JobInstance,
|
|
63390
64148
|
JobKind: () => JobKind,
|
|
64149
|
+
JobNotAuthorizedError: () => JobNotAuthorizedError,
|
|
63391
64150
|
JobPermissionGuard: () => JobPermissionGuard,
|
|
63392
64151
|
JobRegistry: () => JobRegistry,
|
|
63393
64152
|
LLM_ADAPTER: () => LLM_ADAPTER,
|
|
@@ -63905,6 +64664,7 @@ __export(index_exports, {
|
|
|
63905
64664
|
resolveNameConflicts: () => resolveNameConflicts,
|
|
63906
64665
|
resolveOutputSchemaMode: () => resolveOutputSchemaMode,
|
|
63907
64666
|
resolvePendingTC39HooksForClass: () => resolvePendingTC39HooksForClass,
|
|
64667
|
+
resolvePrincipal: () => resolvePrincipal2,
|
|
63908
64668
|
resolveRegistryToken: () => resolveRegistryToken,
|
|
63909
64669
|
resolveSchemaDescriptionFormat: () => resolveSchemaDescriptionFormat,
|
|
63910
64670
|
resolveToolVisibility: () => resolveToolVisibility,
|
|
@@ -63997,49 +64757,7 @@ init_job_registry();
|
|
|
63997
64757
|
init_job_instance();
|
|
63998
64758
|
init_job_events();
|
|
63999
64759
|
init_job_utils();
|
|
64000
|
-
|
|
64001
|
-
// libs/sdk/src/job/job-permission.guard.ts
|
|
64002
|
-
var JobPermissionGuard = class _JobPermissionGuard {
|
|
64003
|
-
/**
|
|
64004
|
-
* Check if the given auth info is allowed to perform the action.
|
|
64005
|
-
* Returns true if no permissions are defined (permissive by default).
|
|
64006
|
-
*/
|
|
64007
|
-
static async check(permissions, action, authInfo) {
|
|
64008
|
-
if (!permissions || permissions.length === 0) {
|
|
64009
|
-
return true;
|
|
64010
|
-
}
|
|
64011
|
-
const relevant = permissions.filter((p) => p.action === action);
|
|
64012
|
-
if (relevant.length === 0) {
|
|
64013
|
-
return true;
|
|
64014
|
-
}
|
|
64015
|
-
for (const perm of relevant) {
|
|
64016
|
-
const passed = await _JobPermissionGuard.checkSingle(perm, authInfo);
|
|
64017
|
-
if (!passed) return false;
|
|
64018
|
-
}
|
|
64019
|
-
return true;
|
|
64020
|
-
}
|
|
64021
|
-
static async checkSingle(perm, authInfo) {
|
|
64022
|
-
if (perm.roles && perm.roles.length > 0) {
|
|
64023
|
-
const userRoles = authInfo["roles"];
|
|
64024
|
-
if (!Array.isArray(userRoles)) return false;
|
|
64025
|
-
const hasRole = perm.roles.some((r) => userRoles.includes(r));
|
|
64026
|
-
if (!hasRole) return false;
|
|
64027
|
-
}
|
|
64028
|
-
if (perm.scopes && perm.scopes.length > 0) {
|
|
64029
|
-
const userScopes = authInfo["scopes"] ?? authInfo["scope"];
|
|
64030
|
-
const scopeArr = typeof userScopes === "string" ? userScopes.split(" ") : Array.isArray(userScopes) ? userScopes : [];
|
|
64031
|
-
const hasScope = perm.scopes.some((s) => scopeArr.includes(s));
|
|
64032
|
-
if (!hasScope) return false;
|
|
64033
|
-
}
|
|
64034
|
-
if (perm.custom) {
|
|
64035
|
-
const result = await perm.custom(authInfo);
|
|
64036
|
-
if (!result) return false;
|
|
64037
|
-
}
|
|
64038
|
-
return true;
|
|
64039
|
-
}
|
|
64040
|
-
};
|
|
64041
|
-
|
|
64042
|
-
// libs/sdk/src/job/index.ts
|
|
64760
|
+
init_job_permission_guard();
|
|
64043
64761
|
init_job_scope_helper();
|
|
64044
64762
|
|
|
64045
64763
|
// libs/sdk/src/job/execution/index.ts
|
|
@@ -64335,7 +65053,7 @@ init_mcp_20260728();
|
|
|
64335
65053
|
init_mcp_stateless_client_adapter();
|
|
64336
65054
|
var import_protocol57 = require("@frontmcp/protocol");
|
|
64337
65055
|
init_ext_apps2();
|
|
64338
|
-
var
|
|
65056
|
+
var import_auth41 = require("@frontmcp/auth");
|
|
64339
65057
|
(function suppressZodExpressWarning() {
|
|
64340
65058
|
if (typeof process === "undefined" || typeof process.emitWarning !== "function") return;
|
|
64341
65059
|
const originalEmitWarning = process.emitWarning.bind(process);
|
|
@@ -64442,6 +65160,7 @@ var ChannelListHook = FlowHooksOf("channels:list");
|
|
|
64442
65160
|
DynamicAdapter,
|
|
64443
65161
|
DynamicAdapterNameError,
|
|
64444
65162
|
DynamicJobDirectExecutionError,
|
|
65163
|
+
DynamicJobRegistrationDisabledError,
|
|
64445
65164
|
DynamicPlugin,
|
|
64446
65165
|
ELICITATION_FALLBACK_JSON_SCHEMA,
|
|
64447
65166
|
ESM_SH_BASE_URL,
|
|
@@ -64596,6 +65315,7 @@ var ChannelListHook = FlowHooksOf("channels:list");
|
|
|
64596
65315
|
JobExecutionManager,
|
|
64597
65316
|
JobInstance,
|
|
64598
65317
|
JobKind,
|
|
65318
|
+
JobNotAuthorizedError,
|
|
64599
65319
|
JobPermissionGuard,
|
|
64600
65320
|
JobRegistry,
|
|
64601
65321
|
LLM_ADAPTER,
|
|
@@ -65112,6 +65832,7 @@ var ChannelListHook = FlowHooksOf("channels:list");
|
|
|
65112
65832
|
resolveNameConflicts,
|
|
65113
65833
|
resolveOutputSchemaMode,
|
|
65114
65834
|
resolvePendingTC39HooksForClass,
|
|
65835
|
+
resolvePrincipal,
|
|
65115
65836
|
resolveRegistryToken,
|
|
65116
65837
|
resolveSchemaDescriptionFormat,
|
|
65117
65838
|
resolveToolVisibility,
|