@frontmcp/sdk 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/agent.scope.d.ts +12 -0
- package/agent/agent.scope.d.ts.map +1 -1
- package/auth/auth.registry.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/flows/session.verify.flow.d.ts +19 -1
- package/auth/flows/session.verify.flow.d.ts.map +1 -1
- package/auth/instances/instance.local-primary-auth.d.ts +37 -2
- package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
- package/auth/session/utils/session-id.utils.d.ts +11 -4
- package/auth/session/utils/session-id.utils.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/app.metadata.d.ts +24 -0
- package/common/metadata/app.metadata.d.ts.map +1 -1
- package/common/metadata/front-mcp.metadata.d.ts +131 -14
- package/common/metadata/front-mcp.metadata.d.ts.map +1 -1
- package/common/tokens/server.tokens.d.ts +1 -0
- package/common/tokens/server.tokens.d.ts.map +1 -1
- package/common/types/options/auth/index.d.ts +4 -2
- package/common/types/options/auth/index.d.ts.map +1 -1
- package/common/types/options/transport/schema.d.ts +2 -0
- 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/path.utils.d.ts +29 -0
- package/common/utils/path.utils.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/context/frontmcp-context-storage.d.ts +3 -1
- package/context/frontmcp-context-storage.d.ts.map +1 -1
- package/context/metadata.utils.d.ts +29 -6
- package/context/metadata.utils.d.ts.map +1 -1
- package/direct/llm-platform.d.ts +2 -2
- package/direct/llm-platform.d.ts.map +1 -1
- package/errors/auth-internal.errors.d.ts +1 -1
- package/errors/auth-internal.errors.d.ts.map +1 -1
- package/errors/index.d.ts +2 -1
- 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 +1359 -347
- package/flows/flow.instance.d.ts.map +1 -1
- package/front-mcp/front-mcp.d.ts.map +1 -1
- package/front-mcp/front-mcp.providers.d.ts +56 -4
- package/front-mcp/front-mcp.providers.d.ts.map +1 -1
- package/index.js +1767 -752
- 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/scope/flows/http.request.flow.d.ts.map +1 -1
- package/scope/scope.instance.d.ts.map +1 -1
- package/scope/scope.utils.d.ts.map +1 -1
- package/server/adapters/express.host.adapter.d.ts +32 -0
- 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/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 +17 -1
- package/tool/flows/call-tool.flow.d.ts.map +1 -1
- package/tool/ui/ui-shared.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 +22 -1
- package/transport/web-fetch-handler.d.ts.map +1 -1
- package/workflow/engine/workflow-step.executor.d.ts +12 -4
- package/workflow/engine/workflow-step.executor.d.ts.map +1 -1
- package/workflow/engine/workflow.engine.d.ts +2 -4
- package/workflow/engine/workflow.engine.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/esm/index.mjs
CHANGED
|
@@ -679,6 +679,7 @@ var init_server_tokens = __esm({
|
|
|
679
679
|
reinitialize: tokenFactory.meta("reinitialize"),
|
|
680
680
|
webRequest: tokenFactory.meta("webRequest"),
|
|
681
681
|
webCtx: tokenFactory.meta("webCtx"),
|
|
682
|
+
webEnv: tokenFactory.meta("webEnv"),
|
|
682
683
|
webTransport: tokenFactory.meta("webTransport")
|
|
683
684
|
};
|
|
684
685
|
}
|
|
@@ -1938,6 +1939,7 @@ import {
|
|
|
1938
1939
|
localDcrConfigSchema
|
|
1939
1940
|
} from "@frontmcp/auth";
|
|
1940
1941
|
import { publicAuthOptionsSchema } from "@frontmcp/auth";
|
|
1942
|
+
import { staticAuthOptionsSchema } from "@frontmcp/auth";
|
|
1941
1943
|
import { transparentAuthOptionsSchema } from "@frontmcp/auth";
|
|
1942
1944
|
import {
|
|
1943
1945
|
localAuthSchema,
|
|
@@ -1952,6 +1954,7 @@ import { appAuthOptionsSchema } from "@frontmcp/auth";
|
|
|
1952
1954
|
import {
|
|
1953
1955
|
parseAuthOptions,
|
|
1954
1956
|
isPublicMode,
|
|
1957
|
+
isStaticMode,
|
|
1955
1958
|
isTransparentMode,
|
|
1956
1959
|
isLocalMode,
|
|
1957
1960
|
isRemoteMode,
|
|
@@ -2345,7 +2348,10 @@ var init_schema7 = __esm({
|
|
|
2345
2348
|
});
|
|
2346
2349
|
eventStoreConfigSchema = z12.object({
|
|
2347
2350
|
enabled: z12.boolean().default(false),
|
|
2348
|
-
|
|
2351
|
+
// 'sqlite' is supported by createEventStore and the TS interface but was
|
|
2352
|
+
// missing from this enum, so a valid `provider: 'sqlite'` config failed
|
|
2353
|
+
// validation.
|
|
2354
|
+
provider: z12.enum(["memory", "redis", "sqlite"]).optional().default("memory"),
|
|
2349
2355
|
maxEvents: z12.number().int().positive().optional().default(1e4),
|
|
2350
2356
|
ttlMs: z12.number().int().positive().optional().default(3e5),
|
|
2351
2357
|
redis: redisOptionsSchema.optional()
|
|
@@ -3149,6 +3155,10 @@ var init_front_mcp_metadata = __esm({
|
|
|
3149
3155
|
output: outputPolicySchema.optional(),
|
|
3150
3156
|
jobs: z24.object({
|
|
3151
3157
|
enabled: z24.boolean(),
|
|
3158
|
+
// Opt-in for `register_job` / `register_workflow` (GHSA-58v2-gpcc-jmqv).
|
|
3159
|
+
// Omitting it here would let Zod strip the flag before the scope reads
|
|
3160
|
+
// it, leaving the documented escape hatch permanently off.
|
|
3161
|
+
allowDynamicRegistration: z24.boolean().optional(),
|
|
3152
3162
|
store: z24.object({
|
|
3153
3163
|
redis: redisOptionsSchema.optional(),
|
|
3154
3164
|
keyPrefix: z24.string().optional()
|
|
@@ -4081,7 +4091,7 @@ function formatToolsForPlatform(tools, platform) {
|
|
|
4081
4091
|
})
|
|
4082
4092
|
);
|
|
4083
4093
|
case "vercel-ai": {
|
|
4084
|
-
const result =
|
|
4094
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
4085
4095
|
for (const tool of tools) {
|
|
4086
4096
|
result[tool.name] = {
|
|
4087
4097
|
description: tool.description ?? `Execute ${tool.name}`,
|
|
@@ -5348,6 +5358,8 @@ import {
|
|
|
5348
5358
|
TokenStoreRequiredError,
|
|
5349
5359
|
NoProviderIdError,
|
|
5350
5360
|
TokenLeakDetectedError,
|
|
5361
|
+
JwtSecretRequiredError,
|
|
5362
|
+
JwtSecretWeakError,
|
|
5351
5363
|
SessionSecretRequiredError,
|
|
5352
5364
|
CredentialProviderAlreadyRegisteredError,
|
|
5353
5365
|
AuthProvidersNotConfiguredError,
|
|
@@ -5636,6 +5648,32 @@ var init_sdk_errors = __esm({
|
|
|
5636
5648
|
}
|
|
5637
5649
|
});
|
|
5638
5650
|
|
|
5651
|
+
// libs/sdk/src/errors/job.errors.ts
|
|
5652
|
+
var JobNotAuthorizedError, DynamicJobRegistrationDisabledError;
|
|
5653
|
+
var init_job_errors = __esm({
|
|
5654
|
+
"libs/sdk/src/errors/job.errors.ts"() {
|
|
5655
|
+
"use strict";
|
|
5656
|
+
init_mcp_error();
|
|
5657
|
+
JobNotAuthorizedError = class extends PublicMcpError {
|
|
5658
|
+
/** The requested name, for internal logs only — never surfaced publicly. */
|
|
5659
|
+
requestedName;
|
|
5660
|
+
constructor(requestedName) {
|
|
5661
|
+
super(`Job or workflow "${requestedName}" not found or not permitted`, "JOB_NOT_AUTHORIZED", 403);
|
|
5662
|
+
this.requestedName = requestedName;
|
|
5663
|
+
}
|
|
5664
|
+
};
|
|
5665
|
+
DynamicJobRegistrationDisabledError = class extends PublicMcpError {
|
|
5666
|
+
constructor(kind = "job") {
|
|
5667
|
+
super(
|
|
5668
|
+
`Dynamic ${kind} registration is disabled. Enable it with jobs: { allowDynamicRegistration: true }.`,
|
|
5669
|
+
"DYNAMIC_REGISTRATION_DISABLED",
|
|
5670
|
+
403
|
|
5671
|
+
);
|
|
5672
|
+
}
|
|
5673
|
+
};
|
|
5674
|
+
}
|
|
5675
|
+
});
|
|
5676
|
+
|
|
5639
5677
|
// libs/sdk/src/errors/index.ts
|
|
5640
5678
|
var errors_exports = {};
|
|
5641
5679
|
__export(errors_exports, {
|
|
@@ -5661,6 +5699,7 @@ __export(errors_exports, {
|
|
|
5661
5699
|
DependencyNotFoundError: () => DependencyNotFoundError,
|
|
5662
5700
|
DynamicAdapterNameError: () => DynamicAdapterNameError,
|
|
5663
5701
|
DynamicJobDirectExecutionError: () => DynamicJobDirectExecutionError,
|
|
5702
|
+
DynamicJobRegistrationDisabledError: () => DynamicJobRegistrationDisabledError,
|
|
5664
5703
|
ElicitationDisabledError: () => ElicitationDisabledError,
|
|
5665
5704
|
ElicitationEncryptionError: () => ElicitationEncryptionError,
|
|
5666
5705
|
ElicitationFallbackRequired: () => ElicitationFallbackRequired,
|
|
@@ -5709,6 +5748,9 @@ __export(errors_exports, {
|
|
|
5709
5748
|
InvalidUseFactoryError: () => InvalidUseFactoryError,
|
|
5710
5749
|
InvalidUseValueError: () => InvalidUseValueError,
|
|
5711
5750
|
InvokeStateMissingKeyError: () => InvokeStateMissingKeyError,
|
|
5751
|
+
JobNotAuthorizedError: () => JobNotAuthorizedError,
|
|
5752
|
+
JwtSecretRequiredError: () => JwtSecretRequiredError,
|
|
5753
|
+
JwtSecretWeakError: () => JwtSecretWeakError,
|
|
5712
5754
|
MCP_ERROR_CODES: () => MCP_ERROR_CODES,
|
|
5713
5755
|
McpError: () => McpError,
|
|
5714
5756
|
MethodNotImplementedError: () => MethodNotImplementedError,
|
|
@@ -5831,6 +5873,7 @@ var init_errors = __esm({
|
|
|
5831
5873
|
init_workflow_errors();
|
|
5832
5874
|
init_esm_errors();
|
|
5833
5875
|
init_sdk_errors();
|
|
5876
|
+
init_job_errors();
|
|
5834
5877
|
}
|
|
5835
5878
|
});
|
|
5836
5879
|
|
|
@@ -7158,6 +7201,10 @@ var require_dist = __commonJS({
|
|
|
7158
7201
|
}
|
|
7159
7202
|
return id;
|
|
7160
7203
|
}
|
|
7204
|
+
async getStreamIdForEventId(eventId) {
|
|
7205
|
+
const row = this.prepared().getStreamId.get(eventId);
|
|
7206
|
+
return row?.stream_id;
|
|
7207
|
+
}
|
|
7161
7208
|
async replayEventsAfter(lastEventId, { send }) {
|
|
7162
7209
|
const stmts = this.prepared();
|
|
7163
7210
|
const streamRow = stmts.getStreamId.get(lastEventId);
|
|
@@ -9313,22 +9360,24 @@ function hasValidSessionStructure(dec) {
|
|
|
9313
9360
|
function isValidSessionPayload(dec, sig) {
|
|
9314
9361
|
return hasValidSessionStructure(dec) && dec.authSig === sig;
|
|
9315
9362
|
}
|
|
9316
|
-
function isValidPublicSessionPayload(dec) {
|
|
9363
|
+
function isValidPublicSessionPayload(dec, expectedAuthSig = "public") {
|
|
9317
9364
|
if (typeof dec !== "object" || dec === null) return false;
|
|
9318
9365
|
const d = dec;
|
|
9319
|
-
return typeof d["nodeId"] === "string" && d["authSig"] ===
|
|
9366
|
+
return typeof d["nodeId"] === "string" && d["authSig"] === expectedAuthSig && typeof d["uuid"] === "string" && typeof d["iat"] === "number" && // `isPublic` is set only for public mode, so it must track the signature —
|
|
9367
|
+
// a static-mode session must never decrypt as a public one.
|
|
9368
|
+
d["isPublic"] === (expectedAuthSig === "public");
|
|
9320
9369
|
}
|
|
9321
9370
|
function decryptSessionId(sessionId, sig) {
|
|
9322
9371
|
const dec = safeDecrypt(sessionId);
|
|
9323
9372
|
return isValidSessionPayload(dec, sig) ? dec : null;
|
|
9324
9373
|
}
|
|
9325
|
-
function decryptPublicSession(sessionId) {
|
|
9374
|
+
function decryptPublicSession(sessionId, expectedAuthSig = "public") {
|
|
9326
9375
|
const cached = cache.get(sessionId);
|
|
9327
|
-
if (cached && isValidPublicSessionPayload(cached)) {
|
|
9376
|
+
if (cached && isValidPublicSessionPayload(cached, expectedAuthSig)) {
|
|
9328
9377
|
return cached;
|
|
9329
9378
|
}
|
|
9330
9379
|
const dec = safeDecrypt(sessionId);
|
|
9331
|
-
if (isValidPublicSessionPayload(dec)) {
|
|
9380
|
+
if (isValidPublicSessionPayload(dec, expectedAuthSig)) {
|
|
9332
9381
|
cache.set(sessionId, dec);
|
|
9333
9382
|
return dec;
|
|
9334
9383
|
}
|
|
@@ -14073,26 +14122,44 @@ async function loadYamlConfig(basePath, configPath) {
|
|
|
14073
14122
|
}
|
|
14074
14123
|
return {};
|
|
14075
14124
|
}
|
|
14125
|
+
function stripUnsafeKeys(value) {
|
|
14126
|
+
if (Array.isArray(value)) return value.map(stripUnsafeKeys);
|
|
14127
|
+
if (!isPlainObject(value)) return value;
|
|
14128
|
+
const clean = {};
|
|
14129
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
14130
|
+
if (UNSAFE_MERGE_KEYS.has(key)) continue;
|
|
14131
|
+
clean[key] = stripUnsafeKeys(nested);
|
|
14132
|
+
}
|
|
14133
|
+
return clean;
|
|
14134
|
+
}
|
|
14135
|
+
function isPlainObject(value) {
|
|
14136
|
+
if (!value || typeof value !== "object") return false;
|
|
14137
|
+
const proto = Object.getPrototypeOf(value);
|
|
14138
|
+
return proto === Object.prototype || proto === null;
|
|
14139
|
+
}
|
|
14076
14140
|
function deepMerge(target, source) {
|
|
14077
14141
|
const result = { ...target };
|
|
14078
14142
|
for (const key in source) {
|
|
14143
|
+
if (UNSAFE_MERGE_KEYS.has(key)) continue;
|
|
14079
14144
|
const sourceVal = source[key];
|
|
14080
14145
|
const targetVal = result[key];
|
|
14081
14146
|
if (sourceVal !== void 0 && sourceVal !== null) {
|
|
14082
14147
|
if (typeof sourceVal === "object" && !Array.isArray(sourceVal) && typeof targetVal === "object" && !Array.isArray(targetVal) && targetVal !== null) {
|
|
14083
14148
|
result[key] = deepMerge(targetVal, sourceVal);
|
|
14084
14149
|
} else {
|
|
14085
|
-
result[key] = sourceVal;
|
|
14150
|
+
result[key] = stripUnsafeKeys(sourceVal);
|
|
14086
14151
|
}
|
|
14087
14152
|
}
|
|
14088
14153
|
}
|
|
14089
14154
|
return result;
|
|
14090
14155
|
}
|
|
14156
|
+
var UNSAFE_MERGE_KEYS;
|
|
14091
14157
|
var init_config_loader = __esm({
|
|
14092
14158
|
"libs/sdk/src/builtin/config/providers/config-loader.ts"() {
|
|
14093
14159
|
"use strict";
|
|
14094
14160
|
init_config_service();
|
|
14095
14161
|
init_env_loader();
|
|
14162
|
+
UNSAFE_MERGE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
14096
14163
|
}
|
|
14097
14164
|
});
|
|
14098
14165
|
|
|
@@ -18128,7 +18195,7 @@ function isUIResourceUri(uri) {
|
|
|
18128
18195
|
return uri.startsWith("ui://");
|
|
18129
18196
|
}
|
|
18130
18197
|
function parseWidgetUri(uri) {
|
|
18131
|
-
const match = uri.match(
|
|
18198
|
+
const match = uri.match(WIDGET_URI_PATTERN);
|
|
18132
18199
|
if (!match) return null;
|
|
18133
18200
|
return { toolName: match[1], extension: match[2] };
|
|
18134
18201
|
}
|
|
@@ -18137,7 +18204,7 @@ function getUIResourceMimeType(_platformOrUri) {
|
|
|
18137
18204
|
if (_platformOrUri?.endsWith(".css")) return "text/css";
|
|
18138
18205
|
return MCP_APPS_MIME_TYPE;
|
|
18139
18206
|
}
|
|
18140
|
-
var ToolUIRegistry;
|
|
18207
|
+
var ToolUIRegistry, WIDGET_URI_PATTERN;
|
|
18141
18208
|
var init_ui_shared = __esm({
|
|
18142
18209
|
"libs/sdk/src/tool/ui/ui-shared.ts"() {
|
|
18143
18210
|
"use strict";
|
|
@@ -18262,6 +18329,7 @@ var init_ui_shared = __esm({
|
|
|
18262
18329
|
return { meta: result.meta };
|
|
18263
18330
|
}
|
|
18264
18331
|
};
|
|
18332
|
+
WIDGET_URI_PATTERN = /^ui:\/\/widget\/([A-Za-z0-9_:@-]+)\.(html|js|css)$/;
|
|
18265
18333
|
}
|
|
18266
18334
|
});
|
|
18267
18335
|
|
|
@@ -20746,6 +20814,7 @@ var init_tool_credentials_gate = __esm({
|
|
|
20746
20814
|
});
|
|
20747
20815
|
|
|
20748
20816
|
// libs/sdk/src/tool/flows/call-tool.flow.ts
|
|
20817
|
+
import { signIncrementalAuthTicket } from "@frontmcp/auth";
|
|
20749
20818
|
import { ConcurrencyLimitError, ExecutionTimeoutError, withTimeout } from "@frontmcp/guard";
|
|
20750
20819
|
import { z as z56 } from "@frontmcp/lazy-zod";
|
|
20751
20820
|
import { CallToolRequestSchema as CallToolRequestSchema2, CallToolResultSchema as CallToolResultSchema2 } from "@frontmcp/protocol";
|
|
@@ -21134,7 +21203,7 @@ var init_call_tool_flow = __esm({
|
|
|
21134
21203
|
this.logger.verbose(`checkToolAuthorization: using anonymous for ${appId}`);
|
|
21135
21204
|
return;
|
|
21136
21205
|
}
|
|
21137
|
-
const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "");
|
|
21206
|
+
const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "", authInfo, claimAuthorizedApps);
|
|
21138
21207
|
this.logger.info(`checkToolAuthorization: authorization required for app "${appId}"`);
|
|
21139
21208
|
throw new AuthorizationRequiredError({
|
|
21140
21209
|
appId,
|
|
@@ -21146,11 +21215,51 @@ var init_call_tool_flow = __esm({
|
|
|
21146
21215
|
this.logger.verbose("checkToolAuthorization:done");
|
|
21147
21216
|
}
|
|
21148
21217
|
/**
|
|
21149
|
-
* Build the URL for progressive/incremental authorization
|
|
21218
|
+
* Build the URL for progressive/incremental authorization.
|
|
21219
|
+
*
|
|
21220
|
+
* This is the ONE point in the request lifecycle where the caller's identity
|
|
21221
|
+
* is both known and verified, so it is where the incremental-authorization
|
|
21222
|
+
* ticket is minted (GHSA-2c4g-9c8x-6m8g). The ticket is what later authorizes
|
|
21223
|
+
* `/oauth/authorize` to skip the login step; `app`/`tool` are carried purely
|
|
21224
|
+
* for display and are re-read from the signed payload server-side.
|
|
21225
|
+
*
|
|
21226
|
+
* When no verified subject or signing secret is available the URL is emitted
|
|
21227
|
+
* WITHOUT a ticket — the user then completes an ordinary login, which is the
|
|
21228
|
+
* correct fail-closed outcome rather than a credential-free shortcut.
|
|
21150
21229
|
*/
|
|
21151
|
-
buildProgressiveAuthUrl(appId, toolId) {
|
|
21230
|
+
buildProgressiveAuthUrl(appId, toolId, authInfo, priorAppIds) {
|
|
21152
21231
|
const baseUrl = this.scope.fullPath || "";
|
|
21153
|
-
|
|
21232
|
+
const url = new URL(`${baseUrl}/oauth/authorize`, "http://placeholder.invalid");
|
|
21233
|
+
url.searchParams.set("app", appId);
|
|
21234
|
+
url.searchParams.set("tool", toolId);
|
|
21235
|
+
const ticket = this.mintIncrementalTicket(appId, toolId, authInfo, priorAppIds);
|
|
21236
|
+
if (ticket) {
|
|
21237
|
+
url.searchParams.set("ticket", ticket);
|
|
21238
|
+
} else {
|
|
21239
|
+
this.logger.verbose("buildProgressiveAuthUrl: no incremental ticket minted \u2014 link requires a full login");
|
|
21240
|
+
}
|
|
21241
|
+
return baseUrl.startsWith("http") ? url.toString() : `${url.pathname}${url.search}`;
|
|
21242
|
+
}
|
|
21243
|
+
/**
|
|
21244
|
+
* Mint a single-use, short-lived incremental-authorization ticket for the
|
|
21245
|
+
* verified caller. Returns undefined when the scope has no local signing
|
|
21246
|
+
* secret or the request has no verified subject.
|
|
21247
|
+
*/
|
|
21248
|
+
mintIncrementalTicket(appId, toolId, authInfo, priorAppIds) {
|
|
21249
|
+
const extraUser = authInfo?.extra?.user;
|
|
21250
|
+
const sub = extraUser?.sub ?? authInfo?.user?.sub;
|
|
21251
|
+
if (!sub) return void 0;
|
|
21252
|
+
const secret = this.scope.auth?.signingSecret;
|
|
21253
|
+
if (!secret) return void 0;
|
|
21254
|
+
return signIncrementalAuthTicket(
|
|
21255
|
+
{
|
|
21256
|
+
sub,
|
|
21257
|
+
appId,
|
|
21258
|
+
toolId,
|
|
21259
|
+
...priorAppIds && priorAppIds.size > 0 ? { priorAppIds: [...priorAppIds] } : {}
|
|
21260
|
+
},
|
|
21261
|
+
secret
|
|
21262
|
+
);
|
|
21154
21263
|
}
|
|
21155
21264
|
async checkEntryAuthorities() {
|
|
21156
21265
|
this.logger.verbose("checkEntryAuthorities:start");
|
|
@@ -23107,8 +23216,180 @@ var init_adapter_registry = __esm({
|
|
|
23107
23216
|
}
|
|
23108
23217
|
});
|
|
23109
23218
|
|
|
23219
|
+
// libs/sdk/src/common/utils/path.utils.ts
|
|
23220
|
+
import { joinPath, trimSlashes } from "@frontmcp/utils";
|
|
23221
|
+
import { trimSlashes as trimSlashes2, joinPath as joinPath2 } from "@frontmcp/utils";
|
|
23222
|
+
function normalizeEntryPrefix(entryPath) {
|
|
23223
|
+
const t = trimSlashes(entryPath ?? "");
|
|
23224
|
+
return t ? `/${t}` : "";
|
|
23225
|
+
}
|
|
23226
|
+
function resolveEntryPath(entryPath) {
|
|
23227
|
+
if (entryPath !== void 0) return entryPath;
|
|
23228
|
+
return process.env["FRONTMCP_HTTP_ENTRY_PATH"] ?? "";
|
|
23229
|
+
}
|
|
23230
|
+
function normalizeScopeBase(scopeBase) {
|
|
23231
|
+
const t = trimSlashes(scopeBase ?? "");
|
|
23232
|
+
return t ? `/${t}` : "";
|
|
23233
|
+
}
|
|
23234
|
+
function readPublicUrlPin() {
|
|
23235
|
+
try {
|
|
23236
|
+
if (typeof process !== "undefined" && process.env) {
|
|
23237
|
+
const v = process.env["FRONTMCP_PUBLIC_URL"];
|
|
23238
|
+
if (typeof v === "string" && v.trim()) {
|
|
23239
|
+
return v.trim().replace(/\/+$/, "");
|
|
23240
|
+
}
|
|
23241
|
+
}
|
|
23242
|
+
} catch {
|
|
23243
|
+
}
|
|
23244
|
+
return void 0;
|
|
23245
|
+
}
|
|
23246
|
+
function isPublicUrlPinned() {
|
|
23247
|
+
return readPublicUrlPin() !== void 0;
|
|
23248
|
+
}
|
|
23249
|
+
function isProxyTrusted() {
|
|
23250
|
+
try {
|
|
23251
|
+
if (typeof process !== "undefined" && process.env) {
|
|
23252
|
+
const v = process.env["FRONTMCP_TRUST_PROXY"];
|
|
23253
|
+
return typeof v === "string" && /^(1|true|yes|on)$/i.test(v.trim());
|
|
23254
|
+
}
|
|
23255
|
+
} catch {
|
|
23256
|
+
}
|
|
23257
|
+
return false;
|
|
23258
|
+
}
|
|
23259
|
+
function getRequestBaseUrl(req, entryPath) {
|
|
23260
|
+
const pin = readPublicUrlPin();
|
|
23261
|
+
if (pin) {
|
|
23262
|
+
return `${pin}${entryPath ?? ""}`;
|
|
23263
|
+
}
|
|
23264
|
+
const trustProxy = isProxyTrusted();
|
|
23265
|
+
const fwdProto = trustProxy ? req.headers["x-forwarded-proto"] : void 0;
|
|
23266
|
+
const fwdHost = trustProxy ? req.headers["x-forwarded-host"] : void 0;
|
|
23267
|
+
const proto = fwdProto || req.protocol || "http";
|
|
23268
|
+
const host = fwdHost || req.headers["host"];
|
|
23269
|
+
return `${proto}://${host}${entryPath ?? ""}`;
|
|
23270
|
+
}
|
|
23271
|
+
function computeIssuer(req, entryPath, scopeBase) {
|
|
23272
|
+
const entryPrefix = normalizeEntryPrefix(entryPath);
|
|
23273
|
+
const scope = normalizeScopeBase(scopeBase);
|
|
23274
|
+
return `${getRequestBaseUrl(req)}${entryPrefix}${scope}`;
|
|
23275
|
+
}
|
|
23276
|
+
function computeResource(req, entryPath, scopeBase) {
|
|
23277
|
+
const entryPrefix = normalizeEntryPrefix(entryPath);
|
|
23278
|
+
const scope = normalizeScopeBase(scopeBase);
|
|
23279
|
+
return `${getRequestBaseUrl(req)}${entryPrefix}${scope}`;
|
|
23280
|
+
}
|
|
23281
|
+
function normalizeResourceUri(uri) {
|
|
23282
|
+
try {
|
|
23283
|
+
const url = new URL(uri);
|
|
23284
|
+
let normalized = `${url.protocol}//${url.hostname}`;
|
|
23285
|
+
const isDefaultPort = url.protocol === "https:" && url.port === "443" || url.protocol === "http:" && url.port === "80";
|
|
23286
|
+
if (url.port && !isDefaultPort) {
|
|
23287
|
+
normalized += `:${url.port}`;
|
|
23288
|
+
}
|
|
23289
|
+
let path3 = url.pathname;
|
|
23290
|
+
if (path3.length > 1 && path3.endsWith("/")) {
|
|
23291
|
+
path3 = path3.slice(0, -1);
|
|
23292
|
+
}
|
|
23293
|
+
normalized += path3;
|
|
23294
|
+
return normalized;
|
|
23295
|
+
} catch {
|
|
23296
|
+
return uri;
|
|
23297
|
+
}
|
|
23298
|
+
}
|
|
23299
|
+
function resourceUriMatches(provided, canonical) {
|
|
23300
|
+
return normalizeResourceUri(provided) === normalizeResourceUri(canonical);
|
|
23301
|
+
}
|
|
23302
|
+
function urlToSafeId(url) {
|
|
23303
|
+
const u = new URL(url);
|
|
23304
|
+
const raw = (u.host + (u.pathname && u.pathname !== "/" ? u.pathname : "")).replace(/\/+$/, "");
|
|
23305
|
+
return trimSlashes(raw).replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
23306
|
+
}
|
|
23307
|
+
function makeWellKnownPaths(name42, entryPrefix, scopeBase = "") {
|
|
23308
|
+
const prefix = normalizeEntryPrefix(entryPrefix);
|
|
23309
|
+
const scope = normalizeScopeBase(scopeBase);
|
|
23310
|
+
const reversed = joinPath(".well-known", name42) + `${prefix}${scope}`;
|
|
23311
|
+
const inPrefixRoot = `${prefix}${joinPath(".well-known", name42)}${scope}`;
|
|
23312
|
+
const inPrefixScope = `${prefix}${scope}${joinPath(".well-known", name42)}`;
|
|
23313
|
+
return /* @__PURE__ */ new Set([reversed, inPrefixRoot, inPrefixScope]);
|
|
23314
|
+
}
|
|
23315
|
+
var init_path_utils = __esm({
|
|
23316
|
+
"libs/sdk/src/common/utils/path.utils.ts"() {
|
|
23317
|
+
"use strict";
|
|
23318
|
+
}
|
|
23319
|
+
});
|
|
23320
|
+
|
|
23110
23321
|
// libs/sdk/src/context/metadata.utils.ts
|
|
23111
|
-
function
|
|
23322
|
+
function isIpv4(value) {
|
|
23323
|
+
const match = IPV4_PATTERN.exec(value);
|
|
23324
|
+
if (!match) return false;
|
|
23325
|
+
return match.slice(1).every((octet) => {
|
|
23326
|
+
if (octet.length > 1 && octet.startsWith("0")) return false;
|
|
23327
|
+
return Number(octet) <= 255;
|
|
23328
|
+
});
|
|
23329
|
+
}
|
|
23330
|
+
function isIpv6(value) {
|
|
23331
|
+
const text = value.includes("%") ? value.slice(0, value.indexOf("%")) : value;
|
|
23332
|
+
if (text.length === 0) return false;
|
|
23333
|
+
let head = text;
|
|
23334
|
+
let embedded = 0;
|
|
23335
|
+
const lastColon = head.lastIndexOf(":");
|
|
23336
|
+
const tail = lastColon === -1 ? "" : head.slice(lastColon + 1);
|
|
23337
|
+
if (tail.includes(".")) {
|
|
23338
|
+
if (!isIpv4(tail)) return false;
|
|
23339
|
+
head = head.slice(0, lastColon + 1);
|
|
23340
|
+
embedded = 2;
|
|
23341
|
+
}
|
|
23342
|
+
const halves = head.split("::");
|
|
23343
|
+
if (halves.length > 2) return false;
|
|
23344
|
+
const countGroups = (part) => {
|
|
23345
|
+
if (part === "") return 0;
|
|
23346
|
+
const groups = part.split(":").filter((group, index, all) => !(group === "" && index === all.length - 1));
|
|
23347
|
+
for (const group of groups) {
|
|
23348
|
+
if (!/^[0-9A-Fa-f]{1,4}$/.test(group)) return null;
|
|
23349
|
+
}
|
|
23350
|
+
return groups.length;
|
|
23351
|
+
};
|
|
23352
|
+
const headCount = countGroups(halves[0]);
|
|
23353
|
+
if (headCount === null) return false;
|
|
23354
|
+
if (halves.length === 1) {
|
|
23355
|
+
return headCount + embedded === 8;
|
|
23356
|
+
}
|
|
23357
|
+
const tailCount = countGroups(halves[1]);
|
|
23358
|
+
if (tailCount === null) return false;
|
|
23359
|
+
return headCount + tailCount + embedded <= 7;
|
|
23360
|
+
}
|
|
23361
|
+
function isIpAddress(value) {
|
|
23362
|
+
const candidate = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
|
|
23363
|
+
return candidate.includes(":") ? isIpv6(candidate) : isIpv4(candidate);
|
|
23364
|
+
}
|
|
23365
|
+
function normalizeIp(value) {
|
|
23366
|
+
if (typeof value !== "string") return void 0;
|
|
23367
|
+
const trimmed = value.trim();
|
|
23368
|
+
if (!trimmed) return void 0;
|
|
23369
|
+
return isIpAddress(trimmed) ? trimmed : void 0;
|
|
23370
|
+
}
|
|
23371
|
+
function readTrustedProxyDepth() {
|
|
23372
|
+
try {
|
|
23373
|
+
if (typeof process !== "undefined" && process.env) {
|
|
23374
|
+
const parsed = Number(process.env["FRONTMCP_TRUSTED_PROXY_DEPTH"]);
|
|
23375
|
+
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
23376
|
+
}
|
|
23377
|
+
} catch {
|
|
23378
|
+
}
|
|
23379
|
+
return 1;
|
|
23380
|
+
}
|
|
23381
|
+
function normalizeDepth(depth) {
|
|
23382
|
+
if (depth === void 0) return readTrustedProxyDepth();
|
|
23383
|
+
return Number.isInteger(depth) && depth > 0 ? depth : readTrustedProxyDepth();
|
|
23384
|
+
}
|
|
23385
|
+
function headerValues(header) {
|
|
23386
|
+
if (typeof header === "string") return header.split(",");
|
|
23387
|
+
if (Array.isArray(header)) {
|
|
23388
|
+
return header.filter((entry) => typeof entry === "string").flatMap((entry) => entry.split(","));
|
|
23389
|
+
}
|
|
23390
|
+
return [];
|
|
23391
|
+
}
|
|
23392
|
+
function extractMetadata(headers, options) {
|
|
23112
23393
|
const customHeaders = {};
|
|
23113
23394
|
for (const [key, value] of Object.entries(headers)) {
|
|
23114
23395
|
if (key.toLowerCase().startsWith("x-frontmcp-") && typeof value === "string") {
|
|
@@ -23119,30 +23400,34 @@ function extractMetadata(headers) {
|
|
|
23119
23400
|
userAgent: typeof headers["user-agent"] === "string" ? headers["user-agent"] : void 0,
|
|
23120
23401
|
contentType: typeof headers["content-type"] === "string" ? headers["content-type"] : void 0,
|
|
23121
23402
|
accept: typeof headers["accept"] === "string" ? headers["accept"] : void 0,
|
|
23122
|
-
clientIp: extractClientIp(headers),
|
|
23403
|
+
clientIp: extractClientIp(headers, options),
|
|
23123
23404
|
customHeaders
|
|
23124
23405
|
};
|
|
23125
23406
|
}
|
|
23126
|
-
function extractClientIp(headers) {
|
|
23127
|
-
const
|
|
23128
|
-
|
|
23129
|
-
|
|
23130
|
-
|
|
23131
|
-
if (Array.isArray(xff) && typeof xff[0] === "string") {
|
|
23132
|
-
return xff[0].split(",")[0]?.trim();
|
|
23407
|
+
function extractClientIp(headers, options) {
|
|
23408
|
+
const peerIp = normalizeIp(options?.peerAddress);
|
|
23409
|
+
const trustProxy = options?.trustProxy ?? isProxyTrusted();
|
|
23410
|
+
if (!trustProxy) {
|
|
23411
|
+
return peerIp;
|
|
23133
23412
|
}
|
|
23134
|
-
const
|
|
23135
|
-
|
|
23136
|
-
|
|
23413
|
+
const depth = normalizeDepth(options?.trustedProxyDepth);
|
|
23414
|
+
const forwarded = headerValues(headers["x-forwarded-for"]).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
23415
|
+
if (forwarded.length > 0) {
|
|
23416
|
+
if (forwarded.length < depth) return peerIp;
|
|
23417
|
+
return normalizeIp(forwarded[forwarded.length - depth]) ?? peerIp;
|
|
23137
23418
|
}
|
|
23138
|
-
if (
|
|
23139
|
-
|
|
23419
|
+
if (depth === 1) {
|
|
23420
|
+
const realIp = normalizeIp(headerValues(headers["x-real-ip"])[0]);
|
|
23421
|
+
if (realIp) return realIp;
|
|
23140
23422
|
}
|
|
23141
|
-
return
|
|
23423
|
+
return peerIp;
|
|
23142
23424
|
}
|
|
23425
|
+
var IPV4_PATTERN;
|
|
23143
23426
|
var init_metadata_utils = __esm({
|
|
23144
23427
|
"libs/sdk/src/context/metadata.utils.ts"() {
|
|
23145
23428
|
"use strict";
|
|
23429
|
+
init_path_utils();
|
|
23430
|
+
IPV4_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
23146
23431
|
}
|
|
23147
23432
|
});
|
|
23148
23433
|
|
|
@@ -23187,10 +23472,11 @@ var init_frontmcp_context_storage = __esm({
|
|
|
23187
23472
|
* @returns Result of the callback
|
|
23188
23473
|
*/
|
|
23189
23474
|
runFromHeaders(headers, args, fn) {
|
|
23475
|
+
const { peerAddress, ...contextArgs } = args;
|
|
23190
23476
|
const traceContext = parseTraceContext(headers);
|
|
23191
|
-
const metadata = extractMetadata(headers);
|
|
23477
|
+
const metadata = extractMetadata(headers, { peerAddress });
|
|
23192
23478
|
const context = new FrontMcpContext({
|
|
23193
|
-
...
|
|
23479
|
+
...contextArgs,
|
|
23194
23480
|
traceContext,
|
|
23195
23481
|
metadata
|
|
23196
23482
|
});
|
|
@@ -24370,11 +24656,13 @@ var init_flow_instance = __esm({
|
|
|
24370
24656
|
const headerSessionId = typeof headers["mcp-session-id"] === "string" ? headers["mcp-session-id"].trim() : "";
|
|
24371
24657
|
const sessionId = headerSessionId.length > 0 ? headerSessionId : `anon:${randomUUID13()}`;
|
|
24372
24658
|
const scope = this.globalProviders.getActiveScope();
|
|
24659
|
+
const peerAddress = request.socket?.remoteAddress;
|
|
24373
24660
|
return storage.runFromHeaders(
|
|
24374
24661
|
headers,
|
|
24375
24662
|
{
|
|
24376
24663
|
sessionId,
|
|
24377
|
-
scopeId: scope.id
|
|
24664
|
+
scopeId: scope.id,
|
|
24665
|
+
peerAddress
|
|
24378
24666
|
},
|
|
24379
24667
|
async () => {
|
|
24380
24668
|
return this.run(input, deps);
|
|
@@ -34318,13 +34606,13 @@ var init_esm_module_loader = __esm({
|
|
|
34318
34606
|
URL.revokeObjectURL(url);
|
|
34319
34607
|
}
|
|
34320
34608
|
}
|
|
34321
|
-
const { ensureDir, fileExists: fileExists9, sha256Hex:
|
|
34609
|
+
const { ensureDir, fileExists: fileExists9, sha256Hex: sha256Hex12, writeFile } = await import("@frontmcp/utils");
|
|
34322
34610
|
const nodePath = await import("node:path");
|
|
34323
34611
|
const nodeOs = await import("node:os");
|
|
34324
34612
|
const { pathToFileURL } = await import("node:url");
|
|
34325
34613
|
const dir = nodePath.join(nodeOs.tmpdir(), "frontmcp-esm-modules");
|
|
34326
34614
|
await ensureDir(dir);
|
|
34327
|
-
const tempPath = nodePath.join(dir, `${
|
|
34615
|
+
const tempPath = nodePath.join(dir, `${sha256Hex12(bundleContent)}.mjs`);
|
|
34328
34616
|
if (!await fileExists9(tempPath)) {
|
|
34329
34617
|
await writeFile(tempPath, bundleContent);
|
|
34330
34618
|
}
|
|
@@ -35896,7 +36184,8 @@ import {
|
|
|
35896
36184
|
createFederatedAuthSession,
|
|
35897
36185
|
escapeHtml as escapeHtml2,
|
|
35898
36186
|
renderLocalLoginPage,
|
|
35899
|
-
startNextProvider
|
|
36187
|
+
startNextProvider,
|
|
36188
|
+
verifyIncrementalAuthTicket
|
|
35900
36189
|
} from "@frontmcp/auth";
|
|
35901
36190
|
import { z as z64 } from "@frontmcp/lazy-zod";
|
|
35902
36191
|
import { generateCodeVerifier, randomUUID as randomUUID15, sha256Base64url } from "@frontmcp/utils";
|
|
@@ -35971,6 +36260,13 @@ var init_oauth_authorize_flow = __esm({
|
|
|
35971
36260
|
targetAppId: z64.string().optional().describe("Target app ID for incremental authorization"),
|
|
35972
36261
|
targetToolId: z64.string().optional().describe("Target tool ID that triggered the incremental auth"),
|
|
35973
36262
|
existingSessionId: z64.string().optional().describe("Existing session ID for incremental auth"),
|
|
36263
|
+
/**
|
|
36264
|
+
* The VERIFIED incremental-authorization ticket (GHSA-2c4g-9c8x-6m8g). Set
|
|
36265
|
+
* only when `?ticket=` carried a valid, unexpired server signature. Its `sub`
|
|
36266
|
+
* is the proven identity the expanded grant is minted for; `isIncrementalAuth`
|
|
36267
|
+
* is derived from its presence, never from a raw query parameter.
|
|
36268
|
+
*/
|
|
36269
|
+
incrementalTicket: z64.custom().optional(),
|
|
35974
36270
|
/**
|
|
35975
36271
|
* Apps the client already holds a grant for (the prior `authorized_apps`
|
|
35976
36272
|
* claim), carried forward on an incremental authorize so the newly-minted
|
|
@@ -36023,18 +36319,21 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36023
36319
|
const { request } = this.rawInput;
|
|
36024
36320
|
const rawRedirectUri = request.query["redirect_uri"];
|
|
36025
36321
|
const rawState = request.query["state"];
|
|
36026
|
-
const
|
|
36027
|
-
const
|
|
36028
|
-
const
|
|
36029
|
-
const
|
|
36030
|
-
const isIncrementalAuth = mode === "incremental" || !!targetAppId;
|
|
36031
|
-
const appsParam = request.query["apps"];
|
|
36322
|
+
const incrementalTicket = this.verifyIncrementalTicket(request.query["ticket"]);
|
|
36323
|
+
const isIncrementalAuth = incrementalTicket !== void 0;
|
|
36324
|
+
const targetAppId = incrementalTicket?.appId;
|
|
36325
|
+
const targetToolId = incrementalTicket?.toolId;
|
|
36032
36326
|
let priorAuthorizedAppIds;
|
|
36033
|
-
if (
|
|
36034
|
-
|
|
36035
|
-
|
|
36036
|
-
|
|
36327
|
+
if (incrementalTicket) {
|
|
36328
|
+
priorAuthorizedAppIds = incrementalTicket.priorAppIds && incrementalTicket.priorAppIds.length > 0 ? Array.from(new Set(incrementalTicket.priorAppIds)) : void 0;
|
|
36329
|
+
} else {
|
|
36330
|
+
const appsParam = request.query["apps"];
|
|
36331
|
+
if (appsParam) {
|
|
36332
|
+
const ids = (Array.isArray(appsParam) ? appsParam : [appsParam]).flatMap((v) => String(v).split(",")).map((v) => v.trim()).filter(Boolean);
|
|
36333
|
+
priorAuthorizedAppIds = ids.length > 0 ? Array.from(new Set(ids)) : void 0;
|
|
36334
|
+
}
|
|
36037
36335
|
}
|
|
36336
|
+
const existingSessionId = request.query["session_id"];
|
|
36038
36337
|
const isDefaultAuthProvider = !metadata.auth;
|
|
36039
36338
|
let requiresFederatedLogin = false;
|
|
36040
36339
|
if (metadata.auth && isOrchestratedMode(metadata.auth)) {
|
|
@@ -36060,6 +36359,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36060
36359
|
targetToolId,
|
|
36061
36360
|
existingSessionId,
|
|
36062
36361
|
priorAuthorizedAppIds,
|
|
36362
|
+
incrementalTicket,
|
|
36063
36363
|
// Federated Login
|
|
36064
36364
|
requiresFederatedLogin,
|
|
36065
36365
|
// Consent Flow
|
|
@@ -36185,6 +36485,28 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36185
36485
|
}
|
|
36186
36486
|
}
|
|
36187
36487
|
}
|
|
36488
|
+
/**
|
|
36489
|
+
* Verify a client-supplied incremental-authorization ticket.
|
|
36490
|
+
*
|
|
36491
|
+
* Returns the verified payload, or `undefined` when the parameter is absent,
|
|
36492
|
+
* malformed, expired, or not signed by this server. Verification is
|
|
36493
|
+
* side-effect free — the ticket is CLAIMED (single use) later, in
|
|
36494
|
+
* {@link prepareAuthorizationRequest}, so a request that fails OAuth
|
|
36495
|
+
* validation does not burn the user's ticket.
|
|
36496
|
+
*/
|
|
36497
|
+
verifyIncrementalTicket(raw) {
|
|
36498
|
+
const token = typeof raw === "string" ? raw : Array.isArray(raw) ? raw[0] : void 0;
|
|
36499
|
+
if (typeof token !== "string" || token.length === 0) return void 0;
|
|
36500
|
+
const localAuth = this.scope.auth;
|
|
36501
|
+
const secret = localAuth?.signingSecret;
|
|
36502
|
+
if (!secret) return void 0;
|
|
36503
|
+
const payload = verifyIncrementalAuthTicket(token, secret);
|
|
36504
|
+
if (!payload) {
|
|
36505
|
+
this.logger.warn("Incremental auth ticket failed verification \u2014 treating as an ordinary login");
|
|
36506
|
+
return void 0;
|
|
36507
|
+
}
|
|
36508
|
+
return payload;
|
|
36509
|
+
}
|
|
36188
36510
|
async checkIfAuthorized() {
|
|
36189
36511
|
}
|
|
36190
36512
|
async prepareAuthorizationRequest() {
|
|
@@ -36196,7 +36518,8 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36196
36518
|
existingSessionId,
|
|
36197
36519
|
priorAuthorizedAppIds,
|
|
36198
36520
|
requiresFederatedLogin,
|
|
36199
|
-
requiresConsent
|
|
36521
|
+
requiresConsent,
|
|
36522
|
+
incrementalTicket
|
|
36200
36523
|
} = this.state;
|
|
36201
36524
|
const { metadata } = this.scope;
|
|
36202
36525
|
if (!validatedRequest) {
|
|
@@ -36209,6 +36532,25 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36209
36532
|
}
|
|
36210
36533
|
const localAuth = auth;
|
|
36211
36534
|
const store = localAuth.authorizationStore;
|
|
36535
|
+
let effectiveIncremental = isIncrementalAuth;
|
|
36536
|
+
let effectivePriorAppIds = priorAuthorizedAppIds;
|
|
36537
|
+
if (incrementalTicket) {
|
|
36538
|
+
const claimed = await localAuth.claimIncrementalTicket(
|
|
36539
|
+
incrementalTicket.jti,
|
|
36540
|
+
Math.max(0, incrementalTicket.exp - Date.now())
|
|
36541
|
+
);
|
|
36542
|
+
if (!claimed) {
|
|
36543
|
+
this.logger.warn("Incremental auth ticket already used \u2014 falling back to an ordinary login");
|
|
36544
|
+
effectiveIncremental = false;
|
|
36545
|
+
effectivePriorAppIds = void 0;
|
|
36546
|
+
this.state.set({
|
|
36547
|
+
isIncrementalAuth: false,
|
|
36548
|
+
targetAppId: void 0,
|
|
36549
|
+
targetToolId: void 0,
|
|
36550
|
+
priorAuthorizedAppIds: void 0
|
|
36551
|
+
});
|
|
36552
|
+
}
|
|
36553
|
+
}
|
|
36212
36554
|
let federatedLogin;
|
|
36213
36555
|
if (requiresFederatedLogin) {
|
|
36214
36556
|
const apps = this.scope.apps.getApps();
|
|
@@ -36256,12 +36598,16 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36256
36598
|
},
|
|
36257
36599
|
state: validatedRequest.state,
|
|
36258
36600
|
resource: validatedRequest.resource,
|
|
36259
|
-
// Progressive/Incremental Authorization Fields
|
|
36260
|
-
|
|
36261
|
-
|
|
36262
|
-
|
|
36601
|
+
// Progressive/Incremental Authorization Fields. `isIncremental` reflects a
|
|
36602
|
+
// CLAIMED, server-signed ticket; `incrementalSub` is the identity that
|
|
36603
|
+
// ticket proved, and is the only subject the callback may mint for when
|
|
36604
|
+
// it skips the credential gate.
|
|
36605
|
+
isIncremental: effectiveIncremental,
|
|
36606
|
+
incrementalSub: effectiveIncremental ? incrementalTicket?.sub : void 0,
|
|
36607
|
+
targetAppId: effectiveIncremental ? targetAppId : void 0,
|
|
36608
|
+
targetToolId: effectiveIncremental ? targetToolId : void 0,
|
|
36263
36609
|
existingSessionId,
|
|
36264
|
-
priorAuthorizedAppIds,
|
|
36610
|
+
priorAuthorizedAppIds: effectivePriorAppIds,
|
|
36265
36611
|
// Federated Login State
|
|
36266
36612
|
federatedLogin,
|
|
36267
36613
|
// Consent State
|
|
@@ -36269,7 +36615,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36269
36615
|
});
|
|
36270
36616
|
await localAuth.authorizationStore.storePendingAuthorization(pendingRecord);
|
|
36271
36617
|
this.logger.info(
|
|
36272
|
-
`Pending authorization created: ${pendingRecord.id}${
|
|
36618
|
+
`Pending authorization created: ${pendingRecord.id}${effectiveIncremental ? ` (incremental for app: ${targetAppId})` : ""}${requiresFederatedLogin ? " (federated)" : ""}${requiresConsent ? " (consent enabled)" : ""}`
|
|
36273
36619
|
);
|
|
36274
36620
|
this.state.set("pendingAuthId", pendingRecord.id);
|
|
36275
36621
|
}
|
|
@@ -36836,8 +37182,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36836
37182
|
const email = request.query["email"];
|
|
36837
37183
|
const name42 = request.query["name"];
|
|
36838
37184
|
const loginFields = this.collectLoginFields(request);
|
|
36839
|
-
const isIncremental = request.query["incremental"] === "true";
|
|
36840
|
-
const targetAppId = request.query["app_id"];
|
|
36841
37185
|
const isFederated = request.query["federated"] === "true";
|
|
36842
37186
|
const providersParam = request.query["providers"];
|
|
36843
37187
|
let selectedProviders;
|
|
@@ -36856,8 +37200,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36856
37200
|
pendingAuthId,
|
|
36857
37201
|
email,
|
|
36858
37202
|
name: name42,
|
|
36859
|
-
isIncremental,
|
|
36860
|
-
targetAppId,
|
|
36861
37203
|
isFederated,
|
|
36862
37204
|
selectedProviders,
|
|
36863
37205
|
selectedTools,
|
|
@@ -36865,9 +37207,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36865
37207
|
loginFields,
|
|
36866
37208
|
csrf
|
|
36867
37209
|
});
|
|
36868
|
-
if (isIncremental) {
|
|
36869
|
-
this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
|
|
36870
|
-
}
|
|
36871
37210
|
if (isFederated) {
|
|
36872
37211
|
this.logger.info(`Federated login callback with ${selectedProviders?.length || 0} selected providers`);
|
|
36873
37212
|
}
|
|
@@ -36876,31 +37215,13 @@ var init_oauth_callback_flow = __esm({
|
|
|
36876
37215
|
}
|
|
36877
37216
|
}
|
|
36878
37217
|
async validatePendingAuth() {
|
|
36879
|
-
const {
|
|
36880
|
-
pendingAuthId,
|
|
36881
|
-
email,
|
|
36882
|
-
name: name42,
|
|
36883
|
-
isIncremental,
|
|
36884
|
-
isFederated,
|
|
36885
|
-
selectedProviders,
|
|
36886
|
-
selectedTools,
|
|
36887
|
-
consentSubmitted,
|
|
36888
|
-
loginFields
|
|
36889
|
-
} = this.state;
|
|
37218
|
+
const { pendingAuthId, email, name: name42, isFederated, selectedProviders, selectedTools, consentSubmitted, loginFields } = this.state;
|
|
36890
37219
|
if (!pendingAuthId) {
|
|
36891
37220
|
this.logger.warn("Missing pending_auth_id in callback");
|
|
36892
37221
|
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Missing pending_auth_id parameter"), 400));
|
|
36893
37222
|
return;
|
|
36894
37223
|
}
|
|
36895
37224
|
const localAuth = this.scope.auth;
|
|
36896
|
-
const localOptions = localAuth.options;
|
|
36897
|
-
const authenticateFn = typeof localOptions.authenticate === "function" ? localOptions.authenticate : void 0;
|
|
36898
|
-
const requireEmail = localOptions.requireEmail ?? true;
|
|
36899
|
-
if (!authenticateFn && !isIncremental && !email && requireEmail) {
|
|
36900
|
-
this.logger.warn("Missing email in callback");
|
|
36901
|
-
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Email is required"), 400));
|
|
36902
|
-
return;
|
|
36903
|
-
}
|
|
36904
37225
|
const pendingAuth = await localAuth.authorizationStore.getPendingAuthorization(pendingAuthId);
|
|
36905
37226
|
if (!pendingAuth) {
|
|
36906
37227
|
this.logger.warn(`Pending authorization not found or expired: ${pendingAuthId}`);
|
|
@@ -36912,6 +37233,20 @@ var init_oauth_callback_flow = __esm({
|
|
|
36912
37233
|
);
|
|
36913
37234
|
return;
|
|
36914
37235
|
}
|
|
37236
|
+
const isIncremental = pendingAuth.isIncremental === true && typeof pendingAuth.incrementalSub === "string";
|
|
37237
|
+
const targetAppId = isIncremental ? pendingAuth.targetAppId : void 0;
|
|
37238
|
+
this.state.set({ isIncremental, targetAppId });
|
|
37239
|
+
if (isIncremental) {
|
|
37240
|
+
this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
|
|
37241
|
+
}
|
|
37242
|
+
const localOptions = localAuth.options;
|
|
37243
|
+
const authenticateFn = typeof localOptions.authenticate === "function" ? localOptions.authenticate : void 0;
|
|
37244
|
+
const requireEmail = localOptions.requireEmail ?? true;
|
|
37245
|
+
if (!authenticateFn && !isIncremental && !email && requireEmail) {
|
|
37246
|
+
this.logger.warn("Missing email in callback");
|
|
37247
|
+
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Email is required"), 400));
|
|
37248
|
+
return;
|
|
37249
|
+
}
|
|
36915
37250
|
if (pendingAuth.authUiCsrf) {
|
|
36916
37251
|
const submitted = this.state.csrf;
|
|
36917
37252
|
if (!submitted || !timingSafeEqualStr3(pendingAuth.authUiCsrf, submitted)) {
|
|
@@ -36926,9 +37261,11 @@ var init_oauth_callback_flow = __esm({
|
|
|
36926
37261
|
return;
|
|
36927
37262
|
}
|
|
36928
37263
|
let userSub;
|
|
36929
|
-
if (
|
|
37264
|
+
if (isIncremental) {
|
|
37265
|
+
userSub = pendingAuth.incrementalSub;
|
|
37266
|
+
} else if (email) {
|
|
36930
37267
|
userSub = this.generateUserSub(email);
|
|
36931
|
-
} else if (!requireEmail
|
|
37268
|
+
} else if (!requireEmail) {
|
|
36932
37269
|
const anonymousSubject = localOptions.anonymousSubject ?? "local-operator";
|
|
36933
37270
|
userSub = this.generateUserSub(anonymousSubject);
|
|
36934
37271
|
} else {
|
|
@@ -37097,10 +37434,13 @@ var init_oauth_callback_flow = __esm({
|
|
|
37097
37434
|
originalState: pendingAuth.state,
|
|
37098
37435
|
resource: pendingAuth.resource,
|
|
37099
37436
|
userSub,
|
|
37100
|
-
// Progressive/Incremental Authorization from pending record
|
|
37101
|
-
|
|
37102
|
-
|
|
37103
|
-
|
|
37437
|
+
// Progressive/Incremental Authorization — from the pending record ONLY.
|
|
37438
|
+
// `isIncremental` was derived above from the record plus its proven
|
|
37439
|
+
// subject; OR-ing a client-supplied flag in here is what let a caller
|
|
37440
|
+
// override the server's `false` (GHSA-2c4g-9c8x-6m8g).
|
|
37441
|
+
isIncremental,
|
|
37442
|
+
targetAppId,
|
|
37443
|
+
targetToolId: isIncremental ? pendingAuth.targetToolId : void 0,
|
|
37104
37444
|
existingSessionId: pendingAuth.existingSessionId,
|
|
37105
37445
|
existingAuthorizationId: pendingAuth.existingAuthorizationId,
|
|
37106
37446
|
priorAuthorizedAppIds: pendingAuth.priorAuthorizedAppIds,
|
|
@@ -37704,6 +38044,7 @@ var init_oauth_callback_flow = __esm({
|
|
|
37704
38044
|
"pending_auth_id",
|
|
37705
38045
|
"incremental",
|
|
37706
38046
|
"app_id",
|
|
38047
|
+
"ticket",
|
|
37707
38048
|
"federated",
|
|
37708
38049
|
"providers",
|
|
37709
38050
|
"tools",
|
|
@@ -39444,7 +39785,23 @@ import {
|
|
|
39444
39785
|
validateAudience
|
|
39445
39786
|
} from "@frontmcp/auth";
|
|
39446
39787
|
import { z as z71 } from "@frontmcp/lazy-zod";
|
|
39447
|
-
import { getMachineId as getMachineId2, randomUUID as randomUUID20 } from "@frontmcp/utils";
|
|
39788
|
+
import { getMachineId as getMachineId2, randomUUID as randomUUID20, sha256, sha256Hex as sha256Hex9, timingSafeEqual as timingSafeEqual2 } from "@frontmcp/utils";
|
|
39789
|
+
function constantTimeEquals(a, b) {
|
|
39790
|
+
const encoder5 = new TextEncoder();
|
|
39791
|
+
return timingSafeEqual2(sha256(encoder5.encode(a)), sha256(encoder5.encode(b)));
|
|
39792
|
+
}
|
|
39793
|
+
function quoteHeaderParam(value) {
|
|
39794
|
+
return value.replace(/[^\x20-\x21\x23-\x7e]/g, "");
|
|
39795
|
+
}
|
|
39796
|
+
function buildStaticChallenge(options, description) {
|
|
39797
|
+
const scheme = options.scheme.trim();
|
|
39798
|
+
if (!scheme) return "";
|
|
39799
|
+
const parts = [`${quoteHeaderParam(scheme)} realm="${quoteHeaderParam(options.realm)}"`];
|
|
39800
|
+
if (description) {
|
|
39801
|
+
parts.push('error="invalid_token"', `error_description="${quoteHeaderParam(description)}"`);
|
|
39802
|
+
}
|
|
39803
|
+
return parts.join(", ");
|
|
39804
|
+
}
|
|
39448
39805
|
var inputSchema23, stateSchema22, UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema, sessionVerifyOutputSchema, plan22, name22, Stage22, warnedUnpinnedAudience, SessionVerifyFlow;
|
|
39449
39806
|
var init_session_verify_flow = __esm({
|
|
39450
39807
|
"libs/sdk/src/auth/flows/session.verify.flow.ts"() {
|
|
@@ -39485,7 +39842,14 @@ var init_session_verify_flow = __esm({
|
|
|
39485
39842
|
}).describe("403 Forbidden \u2014 token valid but insufficient scope");
|
|
39486
39843
|
sessionVerifyOutputSchema = z71.union([UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema]);
|
|
39487
39844
|
plan22 = {
|
|
39488
|
-
pre: [
|
|
39845
|
+
pre: [
|
|
39846
|
+
"parseInput",
|
|
39847
|
+
"handleStaticToken",
|
|
39848
|
+
"handlePublicMode",
|
|
39849
|
+
"handleAnonymousFallback",
|
|
39850
|
+
"requireAuthorizationHeader",
|
|
39851
|
+
"verifyIfJwt"
|
|
39852
|
+
],
|
|
39489
39853
|
execute: ["deriveUser", "parseSessionHeader", "buildAuthorizedOutput"]
|
|
39490
39854
|
};
|
|
39491
39855
|
name22 = "session:verify";
|
|
@@ -39503,14 +39867,20 @@ var init_session_verify_flow = __esm({
|
|
|
39503
39867
|
* Encapsulates the shared logic for session creation, payload encryption, and user derivation.
|
|
39504
39868
|
*/
|
|
39505
39869
|
createAnonymousSession(options) {
|
|
39506
|
-
const { authMode, issuer, scopes = ["anonymous"], sessionIdHeader } = options;
|
|
39870
|
+
const { authMode, issuer, scopes = ["anonymous"], subject, sessionIdHeader } = options;
|
|
39507
39871
|
this.logger.verbose("createAnonymousSession", { authMode, hasExistingSession: !!sessionIdHeader });
|
|
39508
39872
|
const machineId = getMachineId2();
|
|
39873
|
+
const authSignature = subject ? `${authMode}:${subject}` : authMode;
|
|
39509
39874
|
if (sessionIdHeader) {
|
|
39510
|
-
const existingPayload = decryptPublicSession(sessionIdHeader);
|
|
39875
|
+
const existingPayload = decryptPublicSession(sessionIdHeader, authSignature);
|
|
39511
39876
|
if (existingPayload && existingPayload.nodeId === machineId) {
|
|
39512
39877
|
const anonId = existingPayload.uuid ?? `${existingPayload.iat * 1e3}`;
|
|
39513
|
-
const user2 = {
|
|
39878
|
+
const user2 = {
|
|
39879
|
+
sub: subject ?? `anon:${anonId}`,
|
|
39880
|
+
iss: issuer,
|
|
39881
|
+
name: subject ? "Static token" : "Anonymous",
|
|
39882
|
+
scope: scopes.join(" ")
|
|
39883
|
+
};
|
|
39514
39884
|
this.respond({
|
|
39515
39885
|
kind: "authorized",
|
|
39516
39886
|
authorization: {
|
|
@@ -39525,9 +39895,9 @@ var init_session_verify_flow = __esm({
|
|
|
39525
39895
|
}
|
|
39526
39896
|
const now = Date.now();
|
|
39527
39897
|
const user = {
|
|
39528
|
-
sub: `anon:${randomUUID20()}`,
|
|
39898
|
+
sub: subject ?? `anon:${randomUUID20()}`,
|
|
39529
39899
|
iss: issuer,
|
|
39530
|
-
name: "Anonymous",
|
|
39900
|
+
name: subject ? "Static token" : "Anonymous",
|
|
39531
39901
|
scope: scopes.join(" ")
|
|
39532
39902
|
};
|
|
39533
39903
|
const uuid = randomUUID20();
|
|
@@ -39536,7 +39906,7 @@ var init_session_verify_flow = __esm({
|
|
|
39536
39906
|
const payload = {
|
|
39537
39907
|
uuid,
|
|
39538
39908
|
nodeId: machineId,
|
|
39539
|
-
authSig:
|
|
39909
|
+
authSig: authSignature,
|
|
39540
39910
|
iat: Math.floor(now / 1e3),
|
|
39541
39911
|
isPublic: authMode === "public",
|
|
39542
39912
|
authMode,
|
|
@@ -39586,14 +39956,68 @@ var init_session_verify_flow = __esm({
|
|
|
39586
39956
|
prmUrl
|
|
39587
39957
|
});
|
|
39588
39958
|
}
|
|
39959
|
+
async handleStaticToken() {
|
|
39960
|
+
const authOptions = this.scope.auth?.options;
|
|
39961
|
+
if (!authOptions || !isStaticMode(authOptions)) return;
|
|
39962
|
+
const options = authOptions;
|
|
39963
|
+
const presented = this.readStaticCredential(options);
|
|
39964
|
+
if (!presented) {
|
|
39965
|
+
this.logger.warn("handleStaticToken: no credential presented, returning 401");
|
|
39966
|
+
this.respond({
|
|
39967
|
+
kind: "unauthorized",
|
|
39968
|
+
prmMetadataHeader: buildStaticChallenge(options)
|
|
39969
|
+
});
|
|
39970
|
+
return;
|
|
39971
|
+
}
|
|
39972
|
+
const matched = options.tokens.reduce(
|
|
39973
|
+
(found, candidate) => constantTimeEquals(candidate, presented) ? candidate : found,
|
|
39974
|
+
void 0
|
|
39975
|
+
);
|
|
39976
|
+
if (!matched) {
|
|
39977
|
+
this.logger.warn("handleStaticToken: credential did not match, returning 401");
|
|
39978
|
+
this.respond({
|
|
39979
|
+
kind: "unauthorized",
|
|
39980
|
+
prmMetadataHeader: buildStaticChallenge(options, "The access token is invalid")
|
|
39981
|
+
});
|
|
39982
|
+
return;
|
|
39983
|
+
}
|
|
39984
|
+
this.logger.verbose("handleStaticToken: credential accepted");
|
|
39985
|
+
this.createAnonymousSession({
|
|
39986
|
+
authMode: "static",
|
|
39987
|
+
issuer: "static",
|
|
39988
|
+
scopes: options.scopes,
|
|
39989
|
+
subject: `static:${sha256Hex9(matched).slice(0, 12)}`,
|
|
39990
|
+
sessionIdHeader: this.state.sessionIdHeader
|
|
39991
|
+
});
|
|
39992
|
+
}
|
|
39993
|
+
/**
|
|
39994
|
+
* Pull the credential out of the configured header, stripping the scheme
|
|
39995
|
+
* prefix when one is configured (case-insensitively, per RFC 7235).
|
|
39996
|
+
*/
|
|
39997
|
+
readStaticCredential(options) {
|
|
39998
|
+
const headers = this.rawInput.request.headers ?? {};
|
|
39999
|
+
const raw = headers[options.header.toLowerCase()];
|
|
40000
|
+
const value = (Array.isArray(raw) ? raw[0] : raw)?.trim();
|
|
40001
|
+
if (!value) return void 0;
|
|
40002
|
+
const { scheme } = options;
|
|
40003
|
+
if (!scheme) return value;
|
|
40004
|
+
if (value.slice(0, scheme.length).toLowerCase() !== scheme.toLowerCase()) return void 0;
|
|
40005
|
+
if (value[scheme.length] !== " ") return void 0;
|
|
40006
|
+
return value.slice(scheme.length + 1).trim() || void 0;
|
|
40007
|
+
}
|
|
39589
40008
|
async handlePublicMode() {
|
|
39590
40009
|
const authOptions = this.scope.auth?.options;
|
|
39591
40010
|
if (!authOptions || !isPublicMode(authOptions)) {
|
|
39592
40011
|
return;
|
|
39593
40012
|
}
|
|
39594
|
-
if (this.state.token) {
|
|
40013
|
+
if (this.state.token && isJwt(this.state.token)) {
|
|
39595
40014
|
return;
|
|
39596
40015
|
}
|
|
40016
|
+
if (this.state.token) {
|
|
40017
|
+
this.logger.verbose(
|
|
40018
|
+
"handlePublicMode: ignoring non-JWT bearer token (public mode has nothing to verify it against)"
|
|
40019
|
+
);
|
|
40020
|
+
}
|
|
39597
40021
|
this.logger.info("handlePublicMode: allowing anonymous access (public mode)");
|
|
39598
40022
|
this.createAnonymousSession({
|
|
39599
40023
|
authMode: "public",
|
|
@@ -39767,6 +40191,14 @@ var init_session_verify_flow = __esm({
|
|
|
39767
40191
|
__decorateClass([
|
|
39768
40192
|
Stage22("parseInput")
|
|
39769
40193
|
], SessionVerifyFlow.prototype, "parseInput", 1);
|
|
40194
|
+
__decorateClass([
|
|
40195
|
+
Stage22("handleStaticToken", {
|
|
40196
|
+
filter: ({ scope }) => {
|
|
40197
|
+
const authOptions = scope.auth?.options;
|
|
40198
|
+
return !!authOptions && isStaticMode(authOptions);
|
|
40199
|
+
}
|
|
40200
|
+
})
|
|
40201
|
+
], SessionVerifyFlow.prototype, "handleStaticToken", 1);
|
|
39770
40202
|
__decorateClass([
|
|
39771
40203
|
Stage22("handlePublicMode")
|
|
39772
40204
|
], SessionVerifyFlow.prototype, "handlePublicMode", 1);
|
|
@@ -40261,7 +40693,8 @@ import {
|
|
|
40261
40693
|
MemoryStorageAdapter,
|
|
40262
40694
|
randomBytes as randomBytes6,
|
|
40263
40695
|
randomUUID as randomUUID21,
|
|
40264
|
-
sha256Hex as
|
|
40696
|
+
sha256Hex as sha256Hex10,
|
|
40697
|
+
StorageNotSupportedError
|
|
40265
40698
|
} from "@frontmcp/utils";
|
|
40266
40699
|
function getDefaultNoAuthSecret() {
|
|
40267
40700
|
if (!defaultNoAuthSecret) {
|
|
@@ -40283,13 +40716,17 @@ function validateAuthorizationIssuer(received, expected) {
|
|
|
40283
40716
|
}
|
|
40284
40717
|
return { ok: true };
|
|
40285
40718
|
}
|
|
40286
|
-
|
|
40719
|
+
function isUnsupportedOperation(error) {
|
|
40720
|
+
return error instanceof StorageNotSupportedError;
|
|
40721
|
+
}
|
|
40722
|
+
var MIN_HS256_SECRET_BYTES, defaultNoAuthSecret, RESERVED_JWT_CLAIMS, LocalPrimaryAuth;
|
|
40287
40723
|
var init_instance_local_primary_auth = __esm({
|
|
40288
40724
|
"libs/sdk/src/auth/instances/instance.local-primary-auth.ts"() {
|
|
40289
40725
|
"use strict";
|
|
40290
40726
|
init_common();
|
|
40291
40727
|
init_auth2();
|
|
40292
40728
|
init_context_extension();
|
|
40729
|
+
init_errors();
|
|
40293
40730
|
init_cimd();
|
|
40294
40731
|
init_credentials();
|
|
40295
40732
|
init_credentials_context_extension();
|
|
@@ -40307,6 +40744,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40307
40744
|
init_well_known_prm_flow();
|
|
40308
40745
|
init_secure_store();
|
|
40309
40746
|
init_secure_store_context_extension();
|
|
40747
|
+
MIN_HS256_SECRET_BYTES = 32;
|
|
40310
40748
|
RESERVED_JWT_CLAIMS = /* @__PURE__ */ new Set([
|
|
40311
40749
|
"sub",
|
|
40312
40750
|
"iss",
|
|
@@ -40334,10 +40772,22 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40334
40772
|
this.host = getEnv4("FRONTMCP_PUBLIC_HOST")?.trim() || "localhost";
|
|
40335
40773
|
this.issuer = this.deriveIssuer(options);
|
|
40336
40774
|
const jwtSecret = getEnv4("JWT_SECRET");
|
|
40337
|
-
|
|
40338
|
-
|
|
40775
|
+
const hasJwtSecret = !!jwtSecret?.trim();
|
|
40776
|
+
if (hasJwtSecret && jwtSecret) {
|
|
40777
|
+
const encoded = new TextEncoder().encode(jwtSecret);
|
|
40778
|
+
if (encoded.length < MIN_HS256_SECRET_BYTES) {
|
|
40779
|
+
if (isProduction4()) throw new JwtSecretWeakError(encoded.length);
|
|
40780
|
+
this.logger.warn(
|
|
40781
|
+
`JWT_SECRET is ${encoded.length} bytes; HS256 wants at least ${MIN_HS256_SECRET_BYTES} (RFC 7518). This is refused in production \u2014 generate one with \`openssl rand -hex 32\`.`
|
|
40782
|
+
);
|
|
40783
|
+
}
|
|
40784
|
+
this.secret = encoded;
|
|
40785
|
+
} else if (isProduction4() && isOrchestratedMode(options)) {
|
|
40786
|
+
throw new JwtSecretRequiredError(options.mode);
|
|
40339
40787
|
} else {
|
|
40340
|
-
this.logger.warn(
|
|
40788
|
+
this.logger.warn(
|
|
40789
|
+
"JWT_SECRET is not set; signing with a random per-process secret. Tokens will not survive a restart and will not verify across instances. Set JWT_SECRET for any deployment that mints tokens."
|
|
40790
|
+
);
|
|
40341
40791
|
this.secret = getDefaultNoAuthSecret();
|
|
40342
40792
|
}
|
|
40343
40793
|
this.tokenStorage = this.readTokenStorage(options);
|
|
@@ -40394,6 +40844,17 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40394
40844
|
dcrClientRegistryImpl;
|
|
40395
40845
|
/** Storage adapter backing the persistent stores (kept for disposal). */
|
|
40396
40846
|
storageAdapter;
|
|
40847
|
+
/**
|
|
40848
|
+
* Replay guard for incremental-authorization tickets (GHSA-2c4g-9c8x-6m8g).
|
|
40849
|
+
* Shares the persistent adapter when one is configured so the guard holds
|
|
40850
|
+
* across a distributed deployment; falls back to a dedicated in-memory
|
|
40851
|
+
* adapter, matching how the credential vault selects its backing store.
|
|
40852
|
+
*
|
|
40853
|
+
* Holds the in-flight PROMISE, not the resolved adapter: two concurrent
|
|
40854
|
+
* claims for the same ticket must not each build their own store, or each
|
|
40855
|
+
* would write and read back its own nonce and both would win.
|
|
40856
|
+
*/
|
|
40857
|
+
ticketReplayStorage;
|
|
40397
40858
|
/**
|
|
40398
40859
|
* Per-session encrypted credential vault (Checkpoint 3b). Backs
|
|
40399
40860
|
* `this.credentials` in tools and persists `authenticate()` credentials.
|
|
@@ -40561,6 +41022,81 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40561
41022
|
* (`this.secret`), so resume URLs are framework-signed with the same trust
|
|
40562
41023
|
* root as the access tokens.
|
|
40563
41024
|
*/
|
|
41025
|
+
/**
|
|
41026
|
+
* The server HMAC signing key, as the string form the `signData`/`verifyData`
|
|
41027
|
+
* helpers take. These are the exact bytes the access tokens are signed with,
|
|
41028
|
+
* so framework-signed URLs share the access tokens' trust root.
|
|
41029
|
+
*/
|
|
41030
|
+
get signingSecret() {
|
|
41031
|
+
return new TextDecoder().decode(this.secret);
|
|
41032
|
+
}
|
|
41033
|
+
/**
|
|
41034
|
+
* Claim an incremental-authorization ticket, exactly once.
|
|
41035
|
+
*
|
|
41036
|
+
* A signature check proves a ticket was minted by this server for a verified
|
|
41037
|
+
* subject; it cannot prove the ticket has not already been used. The `auth_url`
|
|
41038
|
+
* carrying the ticket travels through agent transcripts and server logs, so a
|
|
41039
|
+
* leaked ticket must not stay usable for the rest of its TTL.
|
|
41040
|
+
*
|
|
41041
|
+
* The claim is race-free without a transaction: each attempt writes its OWN
|
|
41042
|
+
* nonce under `ifNotExists` and reads the key back. Only the attempt whose
|
|
41043
|
+
* nonce survives won, so concurrent replays all lose.
|
|
41044
|
+
*
|
|
41045
|
+
* @returns true when this caller claimed the ticket, false when it was already used.
|
|
41046
|
+
*/
|
|
41047
|
+
async claimIncrementalTicket(jti, ttlMs) {
|
|
41048
|
+
const key = `incremental-ticket:${jti}`;
|
|
41049
|
+
const nonce = randomUUID21();
|
|
41050
|
+
const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1e3));
|
|
41051
|
+
let storage = await this.getTicketReplayStorage();
|
|
41052
|
+
try {
|
|
41053
|
+
await storage.set(key, nonce, { ifNotExists: true, ttlSeconds });
|
|
41054
|
+
} catch (error) {
|
|
41055
|
+
if (!isUnsupportedOperation(error)) {
|
|
41056
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(error)}`);
|
|
41057
|
+
return false;
|
|
41058
|
+
}
|
|
41059
|
+
this.logger.warn(
|
|
41060
|
+
"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)."
|
|
41061
|
+
);
|
|
41062
|
+
storage = await this.useInMemoryTicketReplayStorage();
|
|
41063
|
+
try {
|
|
41064
|
+
await storage.set(key, nonce, { ifNotExists: true, ttlSeconds });
|
|
41065
|
+
} catch (fallbackError) {
|
|
41066
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(fallbackError)}`);
|
|
41067
|
+
return false;
|
|
41068
|
+
}
|
|
41069
|
+
}
|
|
41070
|
+
try {
|
|
41071
|
+
return await storage.get(key) === nonce;
|
|
41072
|
+
} catch (error) {
|
|
41073
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(error)}`);
|
|
41074
|
+
return false;
|
|
41075
|
+
}
|
|
41076
|
+
}
|
|
41077
|
+
getTicketReplayStorage() {
|
|
41078
|
+
if (!this.ticketReplayStorage) {
|
|
41079
|
+
if (this.storageAdapter) {
|
|
41080
|
+
this.ticketReplayStorage = Promise.resolve(this.storageAdapter);
|
|
41081
|
+
} else {
|
|
41082
|
+
this.logger.debug(
|
|
41083
|
+
"Incremental ticket replay guard is in-memory (no persistent tokenStorage configured); single use holds within this instance only."
|
|
41084
|
+
);
|
|
41085
|
+
this.ticketReplayStorage = this.useInMemoryTicketReplayStorage();
|
|
41086
|
+
}
|
|
41087
|
+
}
|
|
41088
|
+
return this.ticketReplayStorage;
|
|
41089
|
+
}
|
|
41090
|
+
/** Replace the replay guard with a fresh in-memory adapter, memoized. */
|
|
41091
|
+
useInMemoryTicketReplayStorage() {
|
|
41092
|
+
const pending = (async () => {
|
|
41093
|
+
const memory = new MemoryStorageAdapter();
|
|
41094
|
+
await memory.connect();
|
|
41095
|
+
return memory;
|
|
41096
|
+
})();
|
|
41097
|
+
this.ticketReplayStorage = pending;
|
|
41098
|
+
return pending;
|
|
41099
|
+
}
|
|
40564
41100
|
async initializeCredentialVault() {
|
|
40565
41101
|
if (isPublicMode(this.options)) {
|
|
40566
41102
|
return;
|
|
@@ -40579,7 +41115,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40579
41115
|
pepper,
|
|
40580
41116
|
logger: this.logger.child("SessionCredentialVault")
|
|
40581
41117
|
});
|
|
40582
|
-
const signingSecret =
|
|
41118
|
+
const signingSecret = this.signingSecret;
|
|
40583
41119
|
const basePath = this.issuer;
|
|
40584
41120
|
await this.providers.addDynamicProviders(
|
|
40585
41121
|
createCredentialsProviders({
|
|
@@ -40811,7 +41347,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40811
41347
|
const pendingAuthId = `pending:${codeRecord.pendingAuthId}`;
|
|
40812
41348
|
const parts = accessToken.split(".");
|
|
40813
41349
|
const signature = parts[2] || accessToken;
|
|
40814
|
-
const newAuthId =
|
|
41350
|
+
const newAuthId = sha256Hex10(signature).substring(0, 16);
|
|
40815
41351
|
await this.orchestratedTokenStore.migrateTokens(pendingAuthId, newAuthId);
|
|
40816
41352
|
this.logger.info(`Migrated tokens from ${pendingAuthId} to ${newAuthId}`);
|
|
40817
41353
|
} catch (err) {
|
|
@@ -41523,6 +42059,9 @@ var init_auth_registry = __esm({
|
|
|
41523
42059
|
if (isPublicMode(options)) {
|
|
41524
42060
|
return new LocalPrimaryAuth(scope, providers, options);
|
|
41525
42061
|
}
|
|
42062
|
+
if (isStaticMode(options)) {
|
|
42063
|
+
return new LocalPrimaryAuth(scope, providers, options);
|
|
42064
|
+
}
|
|
41526
42065
|
if (isTransparentMode(options)) {
|
|
41527
42066
|
return new RemotePrimaryAuth(scope, providers, options);
|
|
41528
42067
|
}
|
|
@@ -44884,6 +45423,7 @@ var init_execute_workflow_tool = __esm({
|
|
|
44884
45423
|
"libs/sdk/src/workflow/tools/execute-workflow.tool.ts"() {
|
|
44885
45424
|
"use strict";
|
|
44886
45425
|
init_common();
|
|
45426
|
+
init_errors();
|
|
44887
45427
|
ExecuteWorkflowTool = class extends ToolContext {
|
|
44888
45428
|
async execute(input) {
|
|
44889
45429
|
const scope = this.scope;
|
|
@@ -44895,12 +45435,13 @@ var init_execute_workflow_tool = __esm({
|
|
|
44895
45435
|
}
|
|
44896
45436
|
const workflow = workflowRegistry.findByName(input.name);
|
|
44897
45437
|
if (!workflow) {
|
|
44898
|
-
return this.fail(new
|
|
45438
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
44899
45439
|
}
|
|
44900
45440
|
const result = await executionManager.executeWorkflow(workflow, jobRegistry, {
|
|
44901
45441
|
background: input.background,
|
|
44902
45442
|
sessionId: this.authInfo.sessionId,
|
|
44903
45443
|
authInfo: this.authInfo,
|
|
45444
|
+
authoritiesContextBuilder: this.scope.authoritiesContextBuilder,
|
|
44904
45445
|
workflowInput: input.input
|
|
44905
45446
|
});
|
|
44906
45447
|
return result;
|
|
@@ -44925,6 +45466,55 @@ var init_execute_workflow_tool = __esm({
|
|
|
44925
45466
|
}
|
|
44926
45467
|
});
|
|
44927
45468
|
|
|
45469
|
+
// libs/sdk/src/common/utils/principal.utils.ts
|
|
45470
|
+
function toStringArray(value) {
|
|
45471
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
45472
|
+
if (typeof value === "string" && value.length > 0) return value.split(" ").filter(Boolean);
|
|
45473
|
+
return [];
|
|
45474
|
+
}
|
|
45475
|
+
function asRecord(value) {
|
|
45476
|
+
return value && typeof value === "object" ? value : void 0;
|
|
45477
|
+
}
|
|
45478
|
+
function firstPresent(sources, convert) {
|
|
45479
|
+
for (const source of sources) {
|
|
45480
|
+
if (source !== void 0 && source !== null) return convert(source);
|
|
45481
|
+
}
|
|
45482
|
+
return [];
|
|
45483
|
+
}
|
|
45484
|
+
function resolvePrincipal2(authInfo, contextBuilder) {
|
|
45485
|
+
const info = authInfo ?? {};
|
|
45486
|
+
const user = asRecord(info["user"]) ?? {};
|
|
45487
|
+
const extraAuthorization = asRecord(asRecord(info["extra"])?.["authorization"]);
|
|
45488
|
+
const authInfoClaims = asRecord(info["claims"]) ?? {};
|
|
45489
|
+
const authorizationClaims = asRecord(extraAuthorization?.["claims"]) ?? {};
|
|
45490
|
+
const claims = { ...authorizationClaims, ...authInfoClaims, ...user };
|
|
45491
|
+
const scopes = toStringArray(info["scopes"]).length ? toStringArray(info["scopes"]) : firstPresent([extraAuthorization?.["scopes"], claims["scope"], claims["scopes"]], toStringArray);
|
|
45492
|
+
if (contextBuilder) {
|
|
45493
|
+
const ctx = contextBuilder.build(info);
|
|
45494
|
+
return {
|
|
45495
|
+
sub: ctx.user.sub,
|
|
45496
|
+
roles: ctx.user.roles,
|
|
45497
|
+
permissions: ctx.user.permissions,
|
|
45498
|
+
scopes,
|
|
45499
|
+
claims: ctx.user.claims
|
|
45500
|
+
};
|
|
45501
|
+
}
|
|
45502
|
+
const roles = firstPresent([user["roles"], claims["roles"], extraAuthorization?.["scopes"]], toStringArray);
|
|
45503
|
+
const permissions = firstPresent([user["permissions"], claims["permissions"]], toStringArray);
|
|
45504
|
+
return {
|
|
45505
|
+
sub: typeof user["sub"] === "string" ? user["sub"] : "",
|
|
45506
|
+
roles,
|
|
45507
|
+
permissions,
|
|
45508
|
+
scopes,
|
|
45509
|
+
claims
|
|
45510
|
+
};
|
|
45511
|
+
}
|
|
45512
|
+
var init_principal_utils = __esm({
|
|
45513
|
+
"libs/sdk/src/common/utils/principal.utils.ts"() {
|
|
45514
|
+
"use strict";
|
|
45515
|
+
}
|
|
45516
|
+
});
|
|
45517
|
+
|
|
44928
45518
|
// libs/sdk/src/workflow/tools/get-workflow-status.tool.ts
|
|
44929
45519
|
import { z as z82 } from "@frontmcp/lazy-zod";
|
|
44930
45520
|
var GetWorkflowStatusTool;
|
|
@@ -44932,6 +45522,7 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44932
45522
|
"libs/sdk/src/workflow/tools/get-workflow-status.tool.ts"() {
|
|
44933
45523
|
"use strict";
|
|
44934
45524
|
init_common();
|
|
45525
|
+
init_principal_utils();
|
|
44935
45526
|
GetWorkflowStatusTool = class extends ToolContext {
|
|
44936
45527
|
async execute(input) {
|
|
44937
45528
|
const scope = this.scope;
|
|
@@ -44940,7 +45531,7 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44940
45531
|
return this.fail(new Error("Jobs system is not enabled"));
|
|
44941
45532
|
}
|
|
44942
45533
|
const record = await executionManager.getStatus(input.runId);
|
|
44943
|
-
if (!record) {
|
|
45534
|
+
if (!record || !this.ownsRun(record)) {
|
|
44944
45535
|
return this.fail(new Error(`Run "${input.runId}" not found`));
|
|
44945
45536
|
}
|
|
44946
45537
|
const workflowRecord = record;
|
|
@@ -44953,6 +45544,23 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44953
45544
|
completedAt: record.completedAt
|
|
44954
45545
|
};
|
|
44955
45546
|
}
|
|
45547
|
+
/**
|
|
45548
|
+
* Match on the owning subject, falling back to the session for runs started
|
|
45549
|
+
* before an owner was recorded (and for anonymous/public servers, where every
|
|
45550
|
+
* caller has an empty subject and the session is the only identity there is).
|
|
45551
|
+
*
|
|
45552
|
+
* A record carrying NEITHER identity is refused. Such a run cannot be
|
|
45553
|
+
* attributed to anyone, and its inputs and results are exactly what this check
|
|
45554
|
+
* exists to protect — anyone holding the id would otherwise be able to read
|
|
45555
|
+
* it. Internal callers that must read unattributed runs should go through the
|
|
45556
|
+
* execution manager directly rather than this tool.
|
|
45557
|
+
*/
|
|
45558
|
+
ownsRun(record) {
|
|
45559
|
+
const callerSub = resolvePrincipal2(this.authInfo, this.scope.authoritiesContextBuilder).sub;
|
|
45560
|
+
if (record.ownerSub) return record.ownerSub === callerSub;
|
|
45561
|
+
if (record.sessionId) return record.sessionId === this.authInfo.sessionId;
|
|
45562
|
+
return false;
|
|
45563
|
+
}
|
|
44956
45564
|
};
|
|
44957
45565
|
GetWorkflowStatusTool = __decorateClass([
|
|
44958
45566
|
FrontMcpTool({
|
|
@@ -44980,6 +45588,53 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44980
45588
|
}
|
|
44981
45589
|
});
|
|
44982
45590
|
|
|
45591
|
+
// libs/sdk/src/job/job-permission.guard.ts
|
|
45592
|
+
var JobPermissionGuard;
|
|
45593
|
+
var init_job_permission_guard = __esm({
|
|
45594
|
+
"libs/sdk/src/job/job-permission.guard.ts"() {
|
|
45595
|
+
"use strict";
|
|
45596
|
+
init_principal_utils();
|
|
45597
|
+
JobPermissionGuard = class _JobPermissionGuard {
|
|
45598
|
+
/**
|
|
45599
|
+
* Check whether the caller may perform `action`.
|
|
45600
|
+
*
|
|
45601
|
+
* @param permissions - the entry's declared rules (undefined = unrestricted).
|
|
45602
|
+
* @param action - the action being attempted.
|
|
45603
|
+
* @param authInfo - the request's AuthInfo.
|
|
45604
|
+
* @param contextBuilder - the scope's authorities context builder, when configured.
|
|
45605
|
+
*/
|
|
45606
|
+
static async check(permissions, action, authInfo, contextBuilder) {
|
|
45607
|
+
if (!permissions || permissions.length === 0) {
|
|
45608
|
+
return true;
|
|
45609
|
+
}
|
|
45610
|
+
const relevant = permissions.filter((p) => p.action === action);
|
|
45611
|
+
if (relevant.length === 0) {
|
|
45612
|
+
return true;
|
|
45613
|
+
}
|
|
45614
|
+
const principal = resolvePrincipal2(authInfo, contextBuilder);
|
|
45615
|
+
for (const perm of relevant) {
|
|
45616
|
+
const passed = await _JobPermissionGuard.checkSingle(perm, principal, authInfo);
|
|
45617
|
+
if (!passed) return false;
|
|
45618
|
+
}
|
|
45619
|
+
return true;
|
|
45620
|
+
}
|
|
45621
|
+
static async checkSingle(perm, principal, authInfo) {
|
|
45622
|
+
if (perm.roles && perm.roles.length > 0) {
|
|
45623
|
+
if (!perm.roles.some((role) => principal.roles.includes(role))) return false;
|
|
45624
|
+
}
|
|
45625
|
+
if (perm.scopes && perm.scopes.length > 0) {
|
|
45626
|
+
if (!perm.scopes.some((scope) => principal.scopes.includes(scope))) return false;
|
|
45627
|
+
}
|
|
45628
|
+
if (perm.custom) {
|
|
45629
|
+
const result = await perm.custom(authInfo ?? {});
|
|
45630
|
+
if (!result) return false;
|
|
45631
|
+
}
|
|
45632
|
+
return true;
|
|
45633
|
+
}
|
|
45634
|
+
};
|
|
45635
|
+
}
|
|
45636
|
+
});
|
|
45637
|
+
|
|
44983
45638
|
// libs/sdk/src/workflow/tools/list-workflows.tool.ts
|
|
44984
45639
|
import { z as z83 } from "@frontmcp/lazy-zod";
|
|
44985
45640
|
var ListWorkflowsTool;
|
|
@@ -44987,6 +45642,7 @@ var init_list_workflows_tool = __esm({
|
|
|
44987
45642
|
"libs/sdk/src/workflow/tools/list-workflows.tool.ts"() {
|
|
44988
45643
|
"use strict";
|
|
44989
45644
|
init_common();
|
|
45645
|
+
init_job_permission_guard();
|
|
44990
45646
|
ListWorkflowsTool = class extends ToolContext {
|
|
44991
45647
|
async execute(input) {
|
|
44992
45648
|
const scope = this.scope;
|
|
@@ -44998,7 +45654,11 @@ var init_list_workflows_tool = __esm({
|
|
|
44998
45654
|
tags: input.tags,
|
|
44999
45655
|
labels: input.labels
|
|
45000
45656
|
});
|
|
45001
|
-
const
|
|
45657
|
+
const visible = [];
|
|
45658
|
+
for (const workflow of workflows) {
|
|
45659
|
+
if (await this.maySee(workflow)) visible.push(workflow);
|
|
45660
|
+
}
|
|
45661
|
+
const mapped = visible.map((w) => ({
|
|
45002
45662
|
name: w.name,
|
|
45003
45663
|
description: w.metadata.description,
|
|
45004
45664
|
trigger: w.getTrigger(),
|
|
@@ -45010,6 +45670,12 @@ var init_list_workflows_tool = __esm({
|
|
|
45010
45670
|
count: mapped.length
|
|
45011
45671
|
};
|
|
45012
45672
|
}
|
|
45673
|
+
async maySee(workflow) {
|
|
45674
|
+
const permissions = workflow.metadata.permissions;
|
|
45675
|
+
const builder = this.scope.authoritiesContextBuilder;
|
|
45676
|
+
if (!await JobPermissionGuard.check(permissions, "list", this.authInfo, builder)) return false;
|
|
45677
|
+
return JobPermissionGuard.check(permissions, "execute", this.authInfo, builder);
|
|
45678
|
+
}
|
|
45013
45679
|
};
|
|
45014
45680
|
ListWorkflowsTool = __decorateClass([
|
|
45015
45681
|
FrontMcpTool({
|
|
@@ -45045,6 +45711,7 @@ var init_register_workflow_tool = __esm({
|
|
|
45045
45711
|
"libs/sdk/src/workflow/tools/register-workflow.tool.ts"() {
|
|
45046
45712
|
"use strict";
|
|
45047
45713
|
init_common();
|
|
45714
|
+
init_errors();
|
|
45048
45715
|
RegisterWorkflowTool = class extends ToolContext {
|
|
45049
45716
|
async execute(input) {
|
|
45050
45717
|
const scope = this.scope;
|
|
@@ -45052,6 +45719,9 @@ var init_register_workflow_tool = __esm({
|
|
|
45052
45719
|
if (!workflowRegistry) {
|
|
45053
45720
|
return this.fail(new Error("Workflows system is not enabled"));
|
|
45054
45721
|
}
|
|
45722
|
+
if (!this.dynamicRegistrationEnabled()) {
|
|
45723
|
+
return this.fail(new DynamicJobRegistrationDisabledError("workflow"));
|
|
45724
|
+
}
|
|
45055
45725
|
const existing = workflowRegistry.findByName(input.name);
|
|
45056
45726
|
if (existing) {
|
|
45057
45727
|
return this.fail(new Error(`Workflow "${input.name}" already exists`));
|
|
@@ -45073,6 +45743,10 @@ var init_register_workflow_tool = __esm({
|
|
|
45073
45743
|
});
|
|
45074
45744
|
return { success: true, workflowId };
|
|
45075
45745
|
}
|
|
45746
|
+
dynamicRegistrationEnabled() {
|
|
45747
|
+
const jobs = this.scope.metadata?.jobs;
|
|
45748
|
+
return jobs?.allowDynamicRegistration === true;
|
|
45749
|
+
}
|
|
45076
45750
|
};
|
|
45077
45751
|
RegisterWorkflowTool = __decorateClass([
|
|
45078
45752
|
FrontMcpTool({
|
|
@@ -45111,6 +45785,8 @@ var init_remove_workflow_tool = __esm({
|
|
|
45111
45785
|
"libs/sdk/src/workflow/tools/remove-workflow.tool.ts"() {
|
|
45112
45786
|
"use strict";
|
|
45113
45787
|
init_common();
|
|
45788
|
+
init_errors();
|
|
45789
|
+
init_job_permission_guard();
|
|
45114
45790
|
RemoveWorkflowTool = class extends ToolContext {
|
|
45115
45791
|
async execute(input) {
|
|
45116
45792
|
const scope = this.scope;
|
|
@@ -45120,7 +45796,16 @@ var init_remove_workflow_tool = __esm({
|
|
|
45120
45796
|
}
|
|
45121
45797
|
const workflow = workflowRegistry.findByName(input.name);
|
|
45122
45798
|
if (!workflow) {
|
|
45123
|
-
return this.fail(new
|
|
45799
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
45800
|
+
}
|
|
45801
|
+
const allowed = await JobPermissionGuard.check(
|
|
45802
|
+
workflow.metadata.permissions,
|
|
45803
|
+
"delete",
|
|
45804
|
+
this.authInfo,
|
|
45805
|
+
this.scope.authoritiesContextBuilder
|
|
45806
|
+
);
|
|
45807
|
+
if (!allowed) {
|
|
45808
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
45124
45809
|
}
|
|
45125
45810
|
if (!workflow.isDynamic()) {
|
|
45126
45811
|
return this.fail(new Error(`Workflow "${input.name}" is not dynamic and cannot be removed`));
|
|
@@ -45485,7 +46170,9 @@ var init_workflow_step_executor = __esm({
|
|
|
45485
46170
|
"libs/sdk/src/workflow/engine/workflow-step.executor.ts"() {
|
|
45486
46171
|
"use strict";
|
|
45487
46172
|
init_errors();
|
|
46173
|
+
init_job_errors();
|
|
45488
46174
|
init_workflow_errors();
|
|
46175
|
+
init_job_permission_guard();
|
|
45489
46176
|
WorkflowStepExecutor = class {
|
|
45490
46177
|
jobRegistry;
|
|
45491
46178
|
logger;
|
|
@@ -45500,6 +46187,16 @@ var init_workflow_step_executor = __esm({
|
|
|
45500
46187
|
if (!job) {
|
|
45501
46188
|
throw new InvalidEntityError("job", step.jobName, `a registered job (referenced by step "${step.id}")`);
|
|
45502
46189
|
}
|
|
46190
|
+
const allowed = await JobPermissionGuard.check(
|
|
46191
|
+
job.metadata.permissions,
|
|
46192
|
+
"execute",
|
|
46193
|
+
this.extra.authInfo,
|
|
46194
|
+
this.extra.authoritiesContextBuilder
|
|
46195
|
+
);
|
|
46196
|
+
if (!allowed) {
|
|
46197
|
+
this.logger.warn(`Step "${step.id}" denied: caller does not satisfy the execute permissions of "${job.name}"`);
|
|
46198
|
+
throw new JobNotAuthorizedError(job.name);
|
|
46199
|
+
}
|
|
45503
46200
|
const retryConfig = step.retry ?? job.metadata.retry ?? {};
|
|
45504
46201
|
const maxAttempts = retryConfig.maxAttempts ?? 3;
|
|
45505
46202
|
const backoffMs = retryConfig.backoffMs ?? 1e3;
|
|
@@ -45729,7 +46426,10 @@ var JobExecutionManager;
|
|
|
45729
46426
|
var init_job_execution_manager = __esm({
|
|
45730
46427
|
"libs/sdk/src/job/execution/job-execution.manager.ts"() {
|
|
45731
46428
|
"use strict";
|
|
46429
|
+
init_principal_utils();
|
|
46430
|
+
init_errors();
|
|
45732
46431
|
init_workflow_engine();
|
|
46432
|
+
init_job_permission_guard();
|
|
45733
46433
|
JobExecutionManager = class {
|
|
45734
46434
|
stateStore;
|
|
45735
46435
|
logger;
|
|
@@ -45743,6 +46443,7 @@ var init_job_execution_manager = __esm({
|
|
|
45743
46443
|
* Execute a job inline (synchronous) or in background.
|
|
45744
46444
|
*/
|
|
45745
46445
|
async executeJob(job, input, opts = {}) {
|
|
46446
|
+
await this.assertMayExecute(job.metadata.permissions, job.name, opts);
|
|
45746
46447
|
const runId = randomUUID23();
|
|
45747
46448
|
const retryConfig = job.metadata.retry ?? {};
|
|
45748
46449
|
const maxAttempts = retryConfig.maxAttempts ?? 1;
|
|
@@ -45751,6 +46452,7 @@ var init_job_execution_manager = __esm({
|
|
|
45751
46452
|
jobId: job.metadata.id ?? job.name,
|
|
45752
46453
|
jobName: job.name,
|
|
45753
46454
|
sessionId: opts.sessionId,
|
|
46455
|
+
ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
|
|
45754
46456
|
state: "pending",
|
|
45755
46457
|
input,
|
|
45756
46458
|
startedAt: Date.now(),
|
|
@@ -45781,6 +46483,7 @@ var init_job_execution_manager = __esm({
|
|
|
45781
46483
|
* Execute a workflow inline or in background.
|
|
45782
46484
|
*/
|
|
45783
46485
|
async executeWorkflow(workflow, jobRegistry, opts = {}) {
|
|
46486
|
+
await this.assertMayExecute(workflow.metadata.permissions, workflow.name, opts);
|
|
45784
46487
|
const runId = randomUUID23();
|
|
45785
46488
|
const runRecord = {
|
|
45786
46489
|
runId,
|
|
@@ -45788,6 +46491,7 @@ var init_job_execution_manager = __esm({
|
|
|
45788
46491
|
jobName: workflow.name,
|
|
45789
46492
|
workflowName: workflow.name,
|
|
45790
46493
|
sessionId: opts.sessionId,
|
|
46494
|
+
ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
|
|
45791
46495
|
state: "pending",
|
|
45792
46496
|
input: opts.workflowInput,
|
|
45793
46497
|
startedAt: Date.now(),
|
|
@@ -45815,6 +46519,25 @@ var init_job_execution_manager = __esm({
|
|
|
45815
46519
|
}
|
|
45816
46520
|
return this.executeWorkflowInline(workflow, jobRegistry, runId, opts);
|
|
45817
46521
|
}
|
|
46522
|
+
/**
|
|
46523
|
+
* Throw unless the caller may execute the entry.
|
|
46524
|
+
*
|
|
46525
|
+
* Uses the same error for "not authorized" as the tools use for "not found",
|
|
46526
|
+
* so a caller cannot enumerate which restricted jobs exist by comparing
|
|
46527
|
+
* responses.
|
|
46528
|
+
*/
|
|
46529
|
+
async assertMayExecute(permissions, name42, opts) {
|
|
46530
|
+
const allowed = await JobPermissionGuard.check(
|
|
46531
|
+
permissions,
|
|
46532
|
+
"execute",
|
|
46533
|
+
opts.authInfo,
|
|
46534
|
+
opts.authoritiesContextBuilder
|
|
46535
|
+
);
|
|
46536
|
+
if (!allowed) {
|
|
46537
|
+
this.logger.warn(`Execution of "${name42}" denied: caller does not satisfy its execute permissions`);
|
|
46538
|
+
throw new JobNotAuthorizedError(name42);
|
|
46539
|
+
}
|
|
46540
|
+
}
|
|
45818
46541
|
/**
|
|
45819
46542
|
* Get execution status.
|
|
45820
46543
|
*/
|
|
@@ -45887,7 +46610,8 @@ var init_job_execution_manager = __esm({
|
|
|
45887
46610
|
try {
|
|
45888
46611
|
const engine = new WorkflowEngine(workflow.metadata, jobRegistry, this.logger, {
|
|
45889
46612
|
authInfo: opts.authInfo ?? {},
|
|
45890
|
-
contextProviders: opts.contextProviders
|
|
46613
|
+
contextProviders: opts.contextProviders,
|
|
46614
|
+
authoritiesContextBuilder: opts.authoritiesContextBuilder
|
|
45891
46615
|
});
|
|
45892
46616
|
const result = await engine.execute(opts.workflowInput);
|
|
45893
46617
|
await this.stateStore.updateRun(runId, {
|
|
@@ -46839,6 +47563,7 @@ var init_execute_job_tool = __esm({
|
|
|
46839
47563
|
"libs/sdk/src/job/tools/execute-job.tool.ts"() {
|
|
46840
47564
|
"use strict";
|
|
46841
47565
|
init_common();
|
|
47566
|
+
init_errors();
|
|
46842
47567
|
ExecuteJobTool = class extends ToolContext {
|
|
46843
47568
|
async execute(input) {
|
|
46844
47569
|
const scope = this.scope;
|
|
@@ -46849,12 +47574,13 @@ var init_execute_job_tool = __esm({
|
|
|
46849
47574
|
}
|
|
46850
47575
|
const job = jobRegistry.findByName(input.name);
|
|
46851
47576
|
if (!job) {
|
|
46852
|
-
return this.fail(new
|
|
47577
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
46853
47578
|
}
|
|
46854
47579
|
const result = await executionManager.executeJob(job, input.input ?? {}, {
|
|
46855
47580
|
background: input.background,
|
|
46856
47581
|
sessionId: this.authInfo.sessionId,
|
|
46857
|
-
authInfo: this.authInfo
|
|
47582
|
+
authInfo: this.authInfo,
|
|
47583
|
+
authoritiesContextBuilder: this.scope.authoritiesContextBuilder
|
|
46858
47584
|
});
|
|
46859
47585
|
return result;
|
|
46860
47586
|
}
|
|
@@ -46886,6 +47612,7 @@ var init_get_job_status_tool = __esm({
|
|
|
46886
47612
|
"libs/sdk/src/job/tools/get-job-status.tool.ts"() {
|
|
46887
47613
|
"use strict";
|
|
46888
47614
|
init_common();
|
|
47615
|
+
init_principal_utils();
|
|
46889
47616
|
init_errors();
|
|
46890
47617
|
GetJobStatusTool = class extends ToolContext {
|
|
46891
47618
|
async execute(input) {
|
|
@@ -46895,7 +47622,7 @@ var init_get_job_status_tool = __esm({
|
|
|
46895
47622
|
return this.fail(new GenericServerError("Jobs system is not enabled"));
|
|
46896
47623
|
}
|
|
46897
47624
|
const record = await executionManager.getStatus(input.runId);
|
|
46898
|
-
if (!record) {
|
|
47625
|
+
if (!record || !this.ownsRun(record)) {
|
|
46899
47626
|
return this.fail(new InvalidInputError(`Run "${input.runId}" not found`));
|
|
46900
47627
|
}
|
|
46901
47628
|
return {
|
|
@@ -46910,6 +47637,23 @@ var init_get_job_status_tool = __esm({
|
|
|
46910
47637
|
logs: record.logs
|
|
46911
47638
|
};
|
|
46912
47639
|
}
|
|
47640
|
+
/**
|
|
47641
|
+
* Match on the owning subject, falling back to the session for runs started
|
|
47642
|
+
* before an owner was recorded (and for anonymous/public servers, where every
|
|
47643
|
+
* caller has an empty subject and the session is the only identity there is).
|
|
47644
|
+
*
|
|
47645
|
+
* A record carrying NEITHER identity is refused. Such a run cannot be
|
|
47646
|
+
* attributed to anyone, and its inputs and results are exactly what this check
|
|
47647
|
+
* exists to protect — anyone holding the id would otherwise be able to read
|
|
47648
|
+
* it. Internal callers that must read unattributed runs should go through the
|
|
47649
|
+
* execution manager directly rather than this tool.
|
|
47650
|
+
*/
|
|
47651
|
+
ownsRun(record) {
|
|
47652
|
+
const callerSub = resolvePrincipal2(this.authInfo, this.scope.authoritiesContextBuilder).sub;
|
|
47653
|
+
if (record.ownerSub) return record.ownerSub === callerSub;
|
|
47654
|
+
if (record.sessionId) return record.sessionId === this.authInfo.sessionId;
|
|
47655
|
+
return false;
|
|
47656
|
+
}
|
|
46913
47657
|
};
|
|
46914
47658
|
GetJobStatusTool = __decorateClass([
|
|
46915
47659
|
FrontMcpTool({
|
|
@@ -46944,6 +47688,7 @@ var init_list_jobs_tool = __esm({
|
|
|
46944
47688
|
"libs/sdk/src/job/tools/list-jobs.tool.ts"() {
|
|
46945
47689
|
"use strict";
|
|
46946
47690
|
init_common();
|
|
47691
|
+
init_job_permission_guard();
|
|
46947
47692
|
ListJobsTool = class extends ToolContext {
|
|
46948
47693
|
async execute(input) {
|
|
46949
47694
|
const jobRegistry = this.scope.jobs;
|
|
@@ -46954,7 +47699,11 @@ var init_list_jobs_tool = __esm({
|
|
|
46954
47699
|
tags: input.tags,
|
|
46955
47700
|
labels: input.labels
|
|
46956
47701
|
});
|
|
46957
|
-
const
|
|
47702
|
+
const visible = [];
|
|
47703
|
+
for (const job of jobs) {
|
|
47704
|
+
if (await this.maySee(job)) visible.push(job);
|
|
47705
|
+
}
|
|
47706
|
+
const mapped = visible.map((j) => ({
|
|
46958
47707
|
name: j.name,
|
|
46959
47708
|
description: j.metadata.description,
|
|
46960
47709
|
tags: j.getTags(),
|
|
@@ -46966,6 +47715,17 @@ var init_list_jobs_tool = __esm({
|
|
|
46966
47715
|
count: mapped.length
|
|
46967
47716
|
};
|
|
46968
47717
|
}
|
|
47718
|
+
/**
|
|
47719
|
+
* A job is listable when the caller passes its `list` rules and is not
|
|
47720
|
+
* blocked from executing it. Checking `execute` too keeps the listing honest:
|
|
47721
|
+
* showing a job the caller can never run is the disclosure this closes.
|
|
47722
|
+
*/
|
|
47723
|
+
async maySee(job) {
|
|
47724
|
+
const permissions = job.metadata.permissions;
|
|
47725
|
+
const builder = this.scope.authoritiesContextBuilder;
|
|
47726
|
+
if (!await JobPermissionGuard.check(permissions, "list", this.authInfo, builder)) return false;
|
|
47727
|
+
return JobPermissionGuard.check(permissions, "execute", this.authInfo, builder);
|
|
47728
|
+
}
|
|
46969
47729
|
};
|
|
46970
47730
|
ListJobsTool = __decorateClass([
|
|
46971
47731
|
FrontMcpTool({
|
|
@@ -47001,6 +47761,7 @@ var init_register_job_tool = __esm({
|
|
|
47001
47761
|
"libs/sdk/src/job/tools/register-job.tool.ts"() {
|
|
47002
47762
|
"use strict";
|
|
47003
47763
|
init_common();
|
|
47764
|
+
init_errors();
|
|
47004
47765
|
RegisterJobTool = class extends ToolContext {
|
|
47005
47766
|
async execute(input) {
|
|
47006
47767
|
const scope = this.scope;
|
|
@@ -47008,6 +47769,9 @@ var init_register_job_tool = __esm({
|
|
|
47008
47769
|
if (!jobRegistry) {
|
|
47009
47770
|
return this.fail(new Error("Jobs system is not enabled"));
|
|
47010
47771
|
}
|
|
47772
|
+
if (!this.dynamicRegistrationEnabled()) {
|
|
47773
|
+
return this.fail(new DynamicJobRegistrationDisabledError("job"));
|
|
47774
|
+
}
|
|
47011
47775
|
const existing = jobRegistry.findByName(input.name);
|
|
47012
47776
|
if (existing) {
|
|
47013
47777
|
return this.fail(new Error(`Job "${input.name}" already exists`));
|
|
@@ -47030,6 +47794,10 @@ var init_register_job_tool = __esm({
|
|
|
47030
47794
|
});
|
|
47031
47795
|
return { success: true, jobId };
|
|
47032
47796
|
}
|
|
47797
|
+
dynamicRegistrationEnabled() {
|
|
47798
|
+
const jobs = this.scope.metadata?.jobs;
|
|
47799
|
+
return jobs?.allowDynamicRegistration === true;
|
|
47800
|
+
}
|
|
47033
47801
|
};
|
|
47034
47802
|
RegisterJobTool = __decorateClass([
|
|
47035
47803
|
FrontMcpTool({
|
|
@@ -47060,6 +47828,8 @@ var init_remove_job_tool = __esm({
|
|
|
47060
47828
|
"libs/sdk/src/job/tools/remove-job.tool.ts"() {
|
|
47061
47829
|
"use strict";
|
|
47062
47830
|
init_common();
|
|
47831
|
+
init_errors();
|
|
47832
|
+
init_job_permission_guard();
|
|
47063
47833
|
RemoveJobTool = class extends ToolContext {
|
|
47064
47834
|
async execute(input) {
|
|
47065
47835
|
const scope = this.scope;
|
|
@@ -47069,7 +47839,16 @@ var init_remove_job_tool = __esm({
|
|
|
47069
47839
|
}
|
|
47070
47840
|
const job = jobRegistry.findByName(input.name);
|
|
47071
47841
|
if (!job) {
|
|
47072
|
-
return this.fail(new
|
|
47842
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
47843
|
+
}
|
|
47844
|
+
const allowed = await JobPermissionGuard.check(
|
|
47845
|
+
job.metadata.permissions,
|
|
47846
|
+
"delete",
|
|
47847
|
+
this.authInfo,
|
|
47848
|
+
this.scope.authoritiesContextBuilder
|
|
47849
|
+
);
|
|
47850
|
+
if (!allowed) {
|
|
47851
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
47073
47852
|
}
|
|
47074
47853
|
if (!job.isDynamic()) {
|
|
47075
47854
|
return this.fail(new Error(`Job "${input.name}" is not a dynamic job and cannot be removed`));
|
|
@@ -47130,14 +47909,18 @@ async function registerJobCapabilities(args) {
|
|
|
47130
47909
|
ListJobsTool,
|
|
47131
47910
|
ExecuteJobTool,
|
|
47132
47911
|
GetJobStatusTool,
|
|
47133
|
-
RegisterJobTool,
|
|
47134
47912
|
RemoveJobTool,
|
|
47135
47913
|
ListWorkflowsTool,
|
|
47136
47914
|
ExecuteWorkflowTool,
|
|
47137
47915
|
GetWorkflowStatusTool,
|
|
47138
|
-
RegisterWorkflowTool,
|
|
47139
47916
|
RemoveWorkflowTool
|
|
47140
47917
|
];
|
|
47918
|
+
if (jobsConfig.allowDynamicRegistration === true) {
|
|
47919
|
+
managementTools.push(RegisterJobTool, RegisterWorkflowTool);
|
|
47920
|
+
logger.warn(
|
|
47921
|
+
"Dynamic job registration is ENABLED: register_job/register_workflow accept a raw script and register it as an executable job."
|
|
47922
|
+
);
|
|
47923
|
+
}
|
|
47141
47924
|
logger.info(
|
|
47142
47925
|
`Jobs initialized: ${jobRegistry.getJobs().length} jobs, ${workflowRegistry.getWorkflows().length} workflows`
|
|
47143
47926
|
);
|
|
@@ -48742,7 +49525,7 @@ var init_load_skill_flow = __esm({
|
|
|
48742
49525
|
});
|
|
48743
49526
|
|
|
48744
49527
|
// libs/sdk/src/skill/auth/skill-http-auth.ts
|
|
48745
|
-
import { timingSafeEqual as
|
|
49528
|
+
import { timingSafeEqual as timingSafeEqual3 } from "@frontmcp/utils";
|
|
48746
49529
|
function createSkillHttpAuthValidator(skillsConfig, logger) {
|
|
48747
49530
|
if (!skillsConfig?.auth || skillsConfig.auth === "public" || skillsConfig.auth === "inherit") {
|
|
48748
49531
|
return null;
|
|
@@ -48829,7 +49612,7 @@ var init_skill_http_auth = __esm({
|
|
|
48829
49612
|
const keyBytes = encoder5.encode(key);
|
|
48830
49613
|
if (keyBytes.length === candidateBytes.length) {
|
|
48831
49614
|
try {
|
|
48832
|
-
if (
|
|
49615
|
+
if (timingSafeEqual3(keyBytes, candidateBytes)) {
|
|
48833
49616
|
found = true;
|
|
48834
49617
|
}
|
|
48835
49618
|
} catch {
|
|
@@ -51795,104 +52578,6 @@ var init_decide_request_intent_utils = __esm({
|
|
|
51795
52578
|
}
|
|
51796
52579
|
});
|
|
51797
52580
|
|
|
51798
|
-
// libs/sdk/src/common/utils/path.utils.ts
|
|
51799
|
-
import { joinPath, trimSlashes } from "@frontmcp/utils";
|
|
51800
|
-
import { trimSlashes as trimSlashes2, joinPath as joinPath2 } from "@frontmcp/utils";
|
|
51801
|
-
function normalizeEntryPrefix(entryPath) {
|
|
51802
|
-
const t = trimSlashes(entryPath ?? "");
|
|
51803
|
-
return t ? `/${t}` : "";
|
|
51804
|
-
}
|
|
51805
|
-
function normalizeScopeBase(scopeBase) {
|
|
51806
|
-
const t = trimSlashes(scopeBase ?? "");
|
|
51807
|
-
return t ? `/${t}` : "";
|
|
51808
|
-
}
|
|
51809
|
-
function readPublicUrlPin() {
|
|
51810
|
-
try {
|
|
51811
|
-
if (typeof process !== "undefined" && process.env) {
|
|
51812
|
-
const v = process.env["FRONTMCP_PUBLIC_URL"];
|
|
51813
|
-
if (typeof v === "string" && v.trim()) {
|
|
51814
|
-
return v.trim().replace(/\/+$/, "");
|
|
51815
|
-
}
|
|
51816
|
-
}
|
|
51817
|
-
} catch {
|
|
51818
|
-
}
|
|
51819
|
-
return void 0;
|
|
51820
|
-
}
|
|
51821
|
-
function isPublicUrlPinned() {
|
|
51822
|
-
return readPublicUrlPin() !== void 0;
|
|
51823
|
-
}
|
|
51824
|
-
function isProxyTrusted() {
|
|
51825
|
-
try {
|
|
51826
|
-
if (typeof process !== "undefined" && process.env) {
|
|
51827
|
-
const v = process.env["FRONTMCP_TRUST_PROXY"];
|
|
51828
|
-
return typeof v === "string" && /^(1|true|yes|on)$/i.test(v.trim());
|
|
51829
|
-
}
|
|
51830
|
-
} catch {
|
|
51831
|
-
}
|
|
51832
|
-
return false;
|
|
51833
|
-
}
|
|
51834
|
-
function getRequestBaseUrl(req, entryPath) {
|
|
51835
|
-
const pin = readPublicUrlPin();
|
|
51836
|
-
if (pin) {
|
|
51837
|
-
return `${pin}${entryPath ?? ""}`;
|
|
51838
|
-
}
|
|
51839
|
-
const trustProxy = isProxyTrusted();
|
|
51840
|
-
const fwdProto = trustProxy ? req.headers["x-forwarded-proto"] : void 0;
|
|
51841
|
-
const fwdHost = trustProxy ? req.headers["x-forwarded-host"] : void 0;
|
|
51842
|
-
const proto = fwdProto || req.protocol || "http";
|
|
51843
|
-
const host = fwdHost || req.headers["host"];
|
|
51844
|
-
return `${proto}://${host}${entryPath ?? ""}`;
|
|
51845
|
-
}
|
|
51846
|
-
function computeIssuer(req, entryPath, scopeBase) {
|
|
51847
|
-
const entryPrefix = normalizeEntryPrefix(entryPath);
|
|
51848
|
-
const scope = normalizeScopeBase(scopeBase);
|
|
51849
|
-
return `${getRequestBaseUrl(req)}${entryPrefix}${scope}`;
|
|
51850
|
-
}
|
|
51851
|
-
function computeResource(req, entryPath, scopeBase) {
|
|
51852
|
-
const entryPrefix = normalizeEntryPrefix(entryPath);
|
|
51853
|
-
const scope = normalizeScopeBase(scopeBase);
|
|
51854
|
-
return `${getRequestBaseUrl(req)}${entryPrefix}${scope}`;
|
|
51855
|
-
}
|
|
51856
|
-
function normalizeResourceUri(uri) {
|
|
51857
|
-
try {
|
|
51858
|
-
const url = new URL(uri);
|
|
51859
|
-
let normalized = `${url.protocol}//${url.hostname}`;
|
|
51860
|
-
const isDefaultPort = url.protocol === "https:" && url.port === "443" || url.protocol === "http:" && url.port === "80";
|
|
51861
|
-
if (url.port && !isDefaultPort) {
|
|
51862
|
-
normalized += `:${url.port}`;
|
|
51863
|
-
}
|
|
51864
|
-
let path3 = url.pathname;
|
|
51865
|
-
if (path3.length > 1 && path3.endsWith("/")) {
|
|
51866
|
-
path3 = path3.slice(0, -1);
|
|
51867
|
-
}
|
|
51868
|
-
normalized += path3;
|
|
51869
|
-
return normalized;
|
|
51870
|
-
} catch {
|
|
51871
|
-
return uri;
|
|
51872
|
-
}
|
|
51873
|
-
}
|
|
51874
|
-
function resourceUriMatches(provided, canonical) {
|
|
51875
|
-
return normalizeResourceUri(provided) === normalizeResourceUri(canonical);
|
|
51876
|
-
}
|
|
51877
|
-
function urlToSafeId(url) {
|
|
51878
|
-
const u = new URL(url);
|
|
51879
|
-
const raw = (u.host + (u.pathname && u.pathname !== "/" ? u.pathname : "")).replace(/\/+$/, "");
|
|
51880
|
-
return trimSlashes(raw).replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
51881
|
-
}
|
|
51882
|
-
function makeWellKnownPaths(name42, entryPrefix, scopeBase = "") {
|
|
51883
|
-
const prefix = normalizeEntryPrefix(entryPrefix);
|
|
51884
|
-
const scope = normalizeScopeBase(scopeBase);
|
|
51885
|
-
const reversed = joinPath(".well-known", name42) + `${prefix}${scope}`;
|
|
51886
|
-
const inPrefixRoot = `${prefix}${joinPath(".well-known", name42)}${scope}`;
|
|
51887
|
-
const inPrefixScope = `${prefix}${scope}${joinPath(".well-known", name42)}`;
|
|
51888
|
-
return /* @__PURE__ */ new Set([reversed, inPrefixRoot, inPrefixScope]);
|
|
51889
|
-
}
|
|
51890
|
-
var init_path_utils = __esm({
|
|
51891
|
-
"libs/sdk/src/common/utils/path.utils.ts"() {
|
|
51892
|
-
"use strict";
|
|
51893
|
-
}
|
|
51894
|
-
});
|
|
51895
|
-
|
|
51896
52581
|
// libs/sdk/src/common/utils/global-config.utils.ts
|
|
51897
52582
|
function getGlobalStoreConfig(pluginName, config) {
|
|
51898
52583
|
if (!config.redis) {
|
|
@@ -51968,6 +52653,7 @@ var init_utils2 = __esm({
|
|
|
51968
52653
|
init_caller_dir_utils();
|
|
51969
52654
|
init_decide_request_intent_utils();
|
|
51970
52655
|
init_path_utils();
|
|
52656
|
+
init_principal_utils();
|
|
51971
52657
|
init_global_config_utils();
|
|
51972
52658
|
init_primitive_filter();
|
|
51973
52659
|
init_validate_remote_url();
|
|
@@ -52226,7 +52912,7 @@ async function tryResolvedSubFile(instance, kind, tail) {
|
|
|
52226
52912
|
return parseSkillMdFrontmatter(inline).body;
|
|
52227
52913
|
}
|
|
52228
52914
|
const absolutePath = entry.path;
|
|
52229
|
-
const resolvedPath =
|
|
52915
|
+
const resolvedPath = resolveEntryPath2(instance, kind, absolutePath, filename);
|
|
52230
52916
|
if (!resolvedPath) continue;
|
|
52231
52917
|
try {
|
|
52232
52918
|
const buf = await readFileBuffer(resolvedPath);
|
|
@@ -52241,7 +52927,7 @@ async function tryResolvedSubFile(instance, kind, tail) {
|
|
|
52241
52927
|
}
|
|
52242
52928
|
return void 0;
|
|
52243
52929
|
}
|
|
52244
|
-
function
|
|
52930
|
+
function resolveEntryPath2(instance, kind, absolutePath, filename) {
|
|
52245
52931
|
if (absolutePath && isAbsolute3(absolutePath)) return absolutePath;
|
|
52246
52932
|
if (!filename) return void 0;
|
|
52247
52933
|
const baseDir = instance.getBaseDir();
|
|
@@ -52913,9 +53599,24 @@ var init_redis_event_store = __esm({
|
|
|
52913
53599
|
await client.expire(streamKey, this.ttlSeconds);
|
|
52914
53600
|
return `${streamId}:${redisId}`;
|
|
52915
53601
|
}
|
|
53602
|
+
/**
|
|
53603
|
+
* Resolve the stream an event belongs to.
|
|
53604
|
+
*
|
|
53605
|
+
* Optional in the upstream `EventStore` contract, and the transport's
|
|
53606
|
+
* 409-conflict guard (refusing a second live stream for the same id) is dead
|
|
53607
|
+
* code while no store provides it (GHSA-84j6-jc92-77jm).
|
|
53608
|
+
*/
|
|
53609
|
+
async getStreamIdForEventId(eventId) {
|
|
53610
|
+
const parsed = this.parseEventId(eventId);
|
|
53611
|
+
return parsed ? parsed[0] : void 0;
|
|
53612
|
+
}
|
|
52916
53613
|
async replayEventsAfter(lastEventId, { send }) {
|
|
53614
|
+
const parsed = this.parseEventId(lastEventId);
|
|
53615
|
+
if (!parsed) {
|
|
53616
|
+
return "default-stream";
|
|
53617
|
+
}
|
|
52917
53618
|
const client = await this.getClient();
|
|
52918
|
-
const [streamId, redisId] =
|
|
53619
|
+
const [streamId, redisId] = parsed;
|
|
52919
53620
|
const streamKey = `${this.keyPrefix}${streamId}`;
|
|
52920
53621
|
const entries = await client.xrange(streamKey, `(${redisId}`, "+");
|
|
52921
53622
|
for (const [id, fields] of entries) {
|
|
@@ -52934,16 +53635,32 @@ var init_redis_event_store = __esm({
|
|
|
52934
53635
|
* Event IDs have format: `{streamId}:{redisStreamEntryId}`
|
|
52935
53636
|
* Redis stream entry IDs have format: `{timestamp}-{sequence}`
|
|
52936
53637
|
*
|
|
53638
|
+
* The entry-id half is validated because it is interpolated into the `xrange`
|
|
53639
|
+
* range argument (`(${redisId}`), where a malformed value is a protocol error
|
|
53640
|
+
* rather than data.
|
|
53641
|
+
*
|
|
53642
|
+
* The stream-id half is NOT character-filtered. `xadd`/`xrange` take the key
|
|
53643
|
+
* as an exact argument — Redis applies no glob matching there — and ioredis
|
|
53644
|
+
* length-prefixes it, so no character is special. Filtering here would also
|
|
53645
|
+
* make `storeEvent`, which accepts any `StreamId`, able to mint ids this
|
|
53646
|
+
* function then refuses, silently breaking replay for that stream. Ownership
|
|
53647
|
+
* is enforced by the session-scoped facade (GHSA-84j6-jc92-77jm), not by
|
|
53648
|
+
* guessing at the id's characters.
|
|
53649
|
+
*
|
|
53650
|
+
* @returns the pair, or `undefined` when the id is malformed.
|
|
53651
|
+
*
|
|
52937
53652
|
* @example
|
|
52938
53653
|
* 'my-stream:1234567890123-0' -> ['my-stream', '1234567890123-0']
|
|
52939
53654
|
*/
|
|
52940
53655
|
parseEventId(eventId) {
|
|
52941
|
-
const
|
|
52942
|
-
|
|
52943
|
-
|
|
52944
|
-
|
|
52945
|
-
|
|
52946
|
-
|
|
53656
|
+
const value = String(eventId);
|
|
53657
|
+
const lastDash = value.lastIndexOf("-");
|
|
53658
|
+
const separator = lastDash === -1 ? value.lastIndexOf(":") : value.lastIndexOf(":", lastDash);
|
|
53659
|
+
if (separator <= 0 || separator === value.length - 1) return void 0;
|
|
53660
|
+
const streamId = value.slice(0, separator);
|
|
53661
|
+
const redisId = value.slice(separator + 1);
|
|
53662
|
+
if (!/^\d+(-\d+)?$/.test(redisId)) return void 0;
|
|
53663
|
+
return [streamId, redisId];
|
|
52947
53664
|
}
|
|
52948
53665
|
/**
|
|
52949
53666
|
* Close the Redis client connection.
|
|
@@ -52989,6 +53706,16 @@ var init_memory_event_store = __esm({
|
|
|
52989
53706
|
}
|
|
52990
53707
|
return id;
|
|
52991
53708
|
}
|
|
53709
|
+
/**
|
|
53710
|
+
* Resolve the stream an event belongs to.
|
|
53711
|
+
*
|
|
53712
|
+
* Optional in the upstream `EventStore` contract, and the transport's
|
|
53713
|
+
* 409-conflict guard (refusing a second live stream for the same id) is dead
|
|
53714
|
+
* code while no store provides it (GHSA-84j6-jc92-77jm).
|
|
53715
|
+
*/
|
|
53716
|
+
async getStreamIdForEventId(eventId) {
|
|
53717
|
+
return this.index.get(eventId)?.streamId;
|
|
53718
|
+
}
|
|
52992
53719
|
async replayEventsAfter(lastEventId, { send }) {
|
|
52993
53720
|
const meta = this.index.get(lastEventId);
|
|
52994
53721
|
if (!meta) {
|
|
@@ -53140,6 +53867,50 @@ var init_event_store_factory = __esm({
|
|
|
53140
53867
|
}
|
|
53141
53868
|
});
|
|
53142
53869
|
|
|
53870
|
+
// libs/sdk/src/transport/event-stores/session-scoped.event-store.ts
|
|
53871
|
+
function createSessionScopedEventStore(store, sessionId) {
|
|
53872
|
+
const prefix = `${sessionId}${SCOPE_SEPARATOR}`;
|
|
53873
|
+
const scope = (streamId) => `${prefix}${streamId}`;
|
|
53874
|
+
const unscope = (streamId) => {
|
|
53875
|
+
const value = String(streamId);
|
|
53876
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
53877
|
+
};
|
|
53878
|
+
const resolveOwnedStream = async (eventId) => {
|
|
53879
|
+
const lookup = store.getStreamIdForEventId;
|
|
53880
|
+
if (lookup) {
|
|
53881
|
+
const resolved = await lookup.call(store, eventId);
|
|
53882
|
+
return resolved !== void 0 && String(resolved).startsWith(prefix) ? resolved : void 0;
|
|
53883
|
+
}
|
|
53884
|
+
if (!String(eventId).startsWith(prefix)) return void 0;
|
|
53885
|
+
const value = String(eventId);
|
|
53886
|
+
const lastColon = value.lastIndexOf(":");
|
|
53887
|
+
return lastColon === -1 ? void 0 : value.slice(0, lastColon);
|
|
53888
|
+
};
|
|
53889
|
+
return {
|
|
53890
|
+
async storeEvent(streamId, message) {
|
|
53891
|
+
return store.storeEvent(scope(streamId), message);
|
|
53892
|
+
},
|
|
53893
|
+
async getStreamIdForEventId(eventId) {
|
|
53894
|
+
const owned = await resolveOwnedStream(eventId);
|
|
53895
|
+
return owned === void 0 ? void 0 : unscope(owned);
|
|
53896
|
+
},
|
|
53897
|
+
async replayEventsAfter(lastEventId, options) {
|
|
53898
|
+
const owned = await resolveOwnedStream(lastEventId);
|
|
53899
|
+
if (owned === void 0) {
|
|
53900
|
+
return "default-stream";
|
|
53901
|
+
}
|
|
53902
|
+
return unscope(await store.replayEventsAfter(lastEventId, options));
|
|
53903
|
+
}
|
|
53904
|
+
};
|
|
53905
|
+
}
|
|
53906
|
+
var SCOPE_SEPARATOR;
|
|
53907
|
+
var init_session_scoped_event_store = __esm({
|
|
53908
|
+
"libs/sdk/src/transport/event-stores/session-scoped.event-store.ts"() {
|
|
53909
|
+
"use strict";
|
|
53910
|
+
SCOPE_SEPARATOR = " ";
|
|
53911
|
+
}
|
|
53912
|
+
});
|
|
53913
|
+
|
|
53143
53914
|
// libs/sdk/src/transport/event-stores/index.ts
|
|
53144
53915
|
var init_event_stores = __esm({
|
|
53145
53916
|
"libs/sdk/src/transport/event-stores/index.ts"() {
|
|
@@ -54809,6 +55580,61 @@ var init_transport_error = __esm({
|
|
|
54809
55580
|
}
|
|
54810
55581
|
});
|
|
54811
55582
|
|
|
55583
|
+
// libs/sdk/src/server/security/host-validation.ts
|
|
55584
|
+
function normalizeHost(value) {
|
|
55585
|
+
const host = value.trim().toLowerCase();
|
|
55586
|
+
if (host.length === 0) return host;
|
|
55587
|
+
const portSeparator = host.startsWith("[") ? host.indexOf(":", host.indexOf("]")) : host.lastIndexOf(":");
|
|
55588
|
+
if (portSeparator === -1) return host;
|
|
55589
|
+
const name42 = host.slice(0, portSeparator);
|
|
55590
|
+
const port = host.slice(portSeparator + 1);
|
|
55591
|
+
if (port.length === 0 || !/^\d+$/.test(port)) return host;
|
|
55592
|
+
return DEFAULT_PORTS.has(port) ? name42 : `${name42}:${port}`;
|
|
55593
|
+
}
|
|
55594
|
+
function normalizeOrigin(value) {
|
|
55595
|
+
const origin = value.trim().toLowerCase();
|
|
55596
|
+
try {
|
|
55597
|
+
return new URL(origin).origin.toLowerCase();
|
|
55598
|
+
} catch {
|
|
55599
|
+
return origin;
|
|
55600
|
+
}
|
|
55601
|
+
}
|
|
55602
|
+
function compileHostValidation(rules) {
|
|
55603
|
+
return {
|
|
55604
|
+
hosts: rules.allowedHosts ? new Set(rules.allowedHosts.map(normalizeHost)) : void 0,
|
|
55605
|
+
origins: rules.allowedOrigins ? new Set(rules.allowedOrigins.map(normalizeOrigin)) : void 0
|
|
55606
|
+
};
|
|
55607
|
+
}
|
|
55608
|
+
function validateHostHeaders(headers, compiled) {
|
|
55609
|
+
if (compiled.hosts) {
|
|
55610
|
+
const host = headers.host;
|
|
55611
|
+
if (!host || !compiled.hosts.has(normalizeHost(host))) {
|
|
55612
|
+
return { status: 403, error: "Forbidden", message: "Invalid Host header" };
|
|
55613
|
+
}
|
|
55614
|
+
const forwardedHost = headers.forwardedHost;
|
|
55615
|
+
if (forwardedHost) {
|
|
55616
|
+
const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
55617
|
+
if (candidates.some((h2) => !compiled.hosts?.has(normalizeHost(h2)))) {
|
|
55618
|
+
return { status: 403, error: "Forbidden", message: "Invalid X-Forwarded-Host header" };
|
|
55619
|
+
}
|
|
55620
|
+
}
|
|
55621
|
+
}
|
|
55622
|
+
if (compiled.origins) {
|
|
55623
|
+
const origin = headers.origin;
|
|
55624
|
+
if (origin && !compiled.origins.has(normalizeOrigin(origin))) {
|
|
55625
|
+
return { status: 403, error: "Forbidden", message: "Invalid Origin header" };
|
|
55626
|
+
}
|
|
55627
|
+
}
|
|
55628
|
+
return void 0;
|
|
55629
|
+
}
|
|
55630
|
+
var DEFAULT_PORTS;
|
|
55631
|
+
var init_host_validation = __esm({
|
|
55632
|
+
"libs/sdk/src/server/security/host-validation.ts"() {
|
|
55633
|
+
"use strict";
|
|
55634
|
+
DEFAULT_PORTS = /* @__PURE__ */ new Set(["80", "443"]);
|
|
55635
|
+
}
|
|
55636
|
+
});
|
|
55637
|
+
|
|
54812
55638
|
// libs/sdk/src/transport/adapters/base-sse-transport.ts
|
|
54813
55639
|
import contentType from "content-type";
|
|
54814
55640
|
import getRawBody from "raw-body";
|
|
@@ -54821,6 +55647,7 @@ var init_base_sse_transport = __esm({
|
|
|
54821
55647
|
"libs/sdk/src/transport/adapters/base-sse-transport.ts"() {
|
|
54822
55648
|
"use strict";
|
|
54823
55649
|
init_transport_errors();
|
|
55650
|
+
init_host_validation();
|
|
54824
55651
|
MAXIMUM_MESSAGE_SIZE = "4mb";
|
|
54825
55652
|
SSEServerTransport = class {
|
|
54826
55653
|
/**
|
|
@@ -54844,25 +55671,31 @@ var init_base_sse_transport = __esm({
|
|
|
54844
55671
|
onmessage;
|
|
54845
55672
|
/**
|
|
54846
55673
|
* Validates request headers for DNS rebinding protection.
|
|
55674
|
+
*
|
|
55675
|
+
* Since v1.7.2 the primary defence is the adapter-level Host validation, which
|
|
55676
|
+
* runs before routing and therefore already covers `/sse` and `/message`
|
|
55677
|
+
* (GHSA-mc9g-v2cp-vfff). These per-transport options remain for a host that
|
|
55678
|
+
* constructs the transport directly; they delegate to the same rules so the
|
|
55679
|
+
* two can never disagree about what a valid Host looks like.
|
|
55680
|
+
*
|
|
54847
55681
|
* @returns Error message if validation fails, undefined if validation passes.
|
|
54848
55682
|
*/
|
|
54849
55683
|
validateRequestHeaders(req) {
|
|
54850
55684
|
if (!this._options.enableDnsRebindingProtection) {
|
|
54851
55685
|
return void 0;
|
|
54852
55686
|
}
|
|
54853
|
-
|
|
54854
|
-
|
|
54855
|
-
|
|
54856
|
-
|
|
54857
|
-
|
|
54858
|
-
|
|
54859
|
-
|
|
54860
|
-
|
|
54861
|
-
|
|
54862
|
-
|
|
54863
|
-
|
|
54864
|
-
|
|
54865
|
-
return void 0;
|
|
55687
|
+
const rejection = validateHostHeaders(
|
|
55688
|
+
{
|
|
55689
|
+
host: req.headers.host,
|
|
55690
|
+
forwardedHost: req.headers["x-forwarded-host"],
|
|
55691
|
+
origin: req.headers.origin
|
|
55692
|
+
},
|
|
55693
|
+
compileHostValidation({
|
|
55694
|
+
allowedHosts: this._options.allowedHosts?.length ? this._options.allowedHosts : void 0,
|
|
55695
|
+
allowedOrigins: this._options.allowedOrigins?.length ? this._options.allowedOrigins : void 0
|
|
55696
|
+
})
|
|
55697
|
+
);
|
|
55698
|
+
return rejection?.message;
|
|
54866
55699
|
}
|
|
54867
55700
|
/**
|
|
54868
55701
|
* Handles the initial SSE connection request.
|
|
@@ -55364,7 +56197,7 @@ var init_transport_local_adapter = __esm({
|
|
|
55364
56197
|
}
|
|
55365
56198
|
}
|
|
55366
56199
|
ensureAuthInfo(req, transport) {
|
|
55367
|
-
const { token, user, session } = req[ServerRequestTokens.auth];
|
|
56200
|
+
const { token, user, session, scopes, claims } = req[ServerRequestTokens.auth];
|
|
55368
56201
|
if (!session?.id) {
|
|
55369
56202
|
throw new UnauthorizedError(
|
|
55370
56203
|
"Session could not be reconstructed for the provided token. Re-authenticate to start a new session.",
|
|
@@ -55393,7 +56226,16 @@ var init_transport_local_adapter = __esm({
|
|
|
55393
56226
|
user,
|
|
55394
56227
|
sessionId,
|
|
55395
56228
|
sessionIdPayload: sessionPayload,
|
|
55396
|
-
scopes
|
|
56229
|
+
// Carry the scopes and claims the verified Authorization actually holds.
|
|
56230
|
+
// These used to be dropped (`scopes: []`, no claims), which left every
|
|
56231
|
+
// scope- or claim-based check downstream inert.
|
|
56232
|
+
//
|
|
56233
|
+
// Both are COPIED, not aliased. Tool code receives this object, and a
|
|
56234
|
+
// nested mutation (`authInfo.claims.roles.push('admin')`) on a shared
|
|
56235
|
+
// reference would rewrite the request's own verified authorization — which
|
|
56236
|
+
// the job/workflow permission guard then reads.
|
|
56237
|
+
scopes: Array.isArray(scopes) ? [...scopes] : [],
|
|
56238
|
+
...claims ? { claims: structuredClone(claims) } : {},
|
|
55397
56239
|
clientId: user.sub ?? "",
|
|
55398
56240
|
transport
|
|
55399
56241
|
};
|
|
@@ -55874,6 +56716,7 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
55874
56716
|
"libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts"() {
|
|
55875
56717
|
"use strict";
|
|
55876
56718
|
init_errors();
|
|
56719
|
+
init_session_scoped_event_store();
|
|
55877
56720
|
init_transport_error();
|
|
55878
56721
|
init_streamable_http_transport();
|
|
55879
56722
|
init_transport_local_adapter();
|
|
@@ -55883,7 +56726,8 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
55883
56726
|
TransportStreamableHttpAdapter = class extends LocalTransportAdapter {
|
|
55884
56727
|
createTransport(sessionId, response) {
|
|
55885
56728
|
const sessionIdGenerator = resolveSessionIdGenerator(this.key.type, sessionId);
|
|
55886
|
-
const
|
|
56729
|
+
const sharedEventStore = this.scope.eventStore;
|
|
56730
|
+
const eventStore = sharedEventStore ? createSessionScopedEventStore(sharedEventStore, sessionId) : void 0;
|
|
55887
56731
|
const isStateless = this.key.type === "stateless-http";
|
|
55888
56732
|
return new RecreateableStreamableHTTPServerTransport({
|
|
55889
56733
|
sessionIdGenerator,
|
|
@@ -56224,7 +57068,7 @@ var init_transport_remote = __esm({
|
|
|
56224
57068
|
});
|
|
56225
57069
|
|
|
56226
57070
|
// libs/sdk/src/transport/transport.registry.ts
|
|
56227
|
-
import { getMachineId as getMachineId5, sha256Hex as
|
|
57071
|
+
import { getMachineId as getMachineId5, sha256Hex as sha256Hex11 } from "@frontmcp/utils";
|
|
56228
57072
|
var TransportService;
|
|
56229
57073
|
var init_transport_registry = __esm({
|
|
56230
57074
|
"libs/sdk/src/transport/transport.registry.ts"() {
|
|
@@ -56760,7 +57604,7 @@ var init_transport_registry = __esm({
|
|
|
56760
57604
|
}
|
|
56761
57605
|
/* --------------------------------- internals -------------------------------- */
|
|
56762
57606
|
sha256(value) {
|
|
56763
|
-
return
|
|
57607
|
+
return sha256Hex11(value);
|
|
56764
57608
|
}
|
|
56765
57609
|
/**
|
|
56766
57610
|
* Create a history key from components.
|
|
@@ -57053,13 +57897,28 @@ var init_http_request_flow = __esm({
|
|
|
57053
57897
|
}
|
|
57054
57898
|
async acquireQuota() {
|
|
57055
57899
|
const manager = this.scope.rateLimitManager;
|
|
57056
|
-
if (!manager
|
|
57900
|
+
if (!manager) return;
|
|
57057
57901
|
const context = this.tryGetContext();
|
|
57058
57902
|
const partitionCtx = context ? {
|
|
57059
57903
|
sessionId: context.sessionId,
|
|
57060
57904
|
clientIp: context.metadata?.clientIp,
|
|
57061
57905
|
userId: context.authInfo?.clientId
|
|
57062
57906
|
} : void 0;
|
|
57907
|
+
const ipResult = manager.checkIpFilter(partitionCtx?.clientIp);
|
|
57908
|
+
if (ipResult && !ipResult.allowed) {
|
|
57909
|
+
this.logger.warn(`[${this.requestId}] request rejected by ipFilter`, { reason: ipResult.reason });
|
|
57910
|
+
this.respond(
|
|
57911
|
+
httpRespond.json(
|
|
57912
|
+
{
|
|
57913
|
+
jsonrpc: "2.0",
|
|
57914
|
+
error: { code: -32001, message: "Forbidden: client IP rejected by ipFilter" }
|
|
57915
|
+
},
|
|
57916
|
+
{ status: 403 }
|
|
57917
|
+
)
|
|
57918
|
+
);
|
|
57919
|
+
return;
|
|
57920
|
+
}
|
|
57921
|
+
if (!manager.config?.global) return;
|
|
57063
57922
|
const result = await manager.checkGlobalRateLimit(partitionCtx);
|
|
57064
57923
|
if (!result.allowed) {
|
|
57065
57924
|
const retryAfter = Math.ceil((result.retryAfterMs ?? 6e4) / 1e3);
|
|
@@ -57680,6 +58539,7 @@ var init_scope_instance = __esm({
|
|
|
57680
58539
|
init_channel_scope_helper();
|
|
57681
58540
|
init_common();
|
|
57682
58541
|
init_sqlite();
|
|
58542
|
+
init_path_utils();
|
|
57683
58543
|
init_complete_flow();
|
|
57684
58544
|
init_context();
|
|
57685
58545
|
init_elicitation2();
|
|
@@ -57780,7 +58640,7 @@ var init_scope_instance = __esm({
|
|
|
57780
58640
|
this.logger = globalProviders.get(FrontMcpLogger).child("FrontMcp.MultiAppScope");
|
|
57781
58641
|
this.globalProviders = globalProviders;
|
|
57782
58642
|
this.server = this.globalProviders.get(FrontMcpServer);
|
|
57783
|
-
this.entryPath = rec.metadata.http?.entryPath
|
|
58643
|
+
this.entryPath = resolveEntryPath(rec.metadata.http?.entryPath);
|
|
57784
58644
|
if (rec.kind === "SPLIT_BY_APP") {
|
|
57785
58645
|
this.routeBase = `/${rec.metadata.id}`;
|
|
57786
58646
|
} else {
|
|
@@ -57884,16 +58744,25 @@ var init_scope_instance = __esm({
|
|
|
57884
58744
|
})() : void 0;
|
|
57885
58745
|
const tasksConfig = this.metadata.tasks;
|
|
57886
58746
|
const tasksExplicitlyDisabled = tasksConfig?.enabled === false;
|
|
58747
|
+
const tasksExplicitlyEnabled = tasksConfig?.enabled === true;
|
|
57887
58748
|
const isTaskWorker = !!this.metadata["__taskWorkerMode"];
|
|
57888
58749
|
const hasPersistentBackend = Boolean(
|
|
57889
58750
|
tasksConfig?.sqlite || tasksConfig?.redis || this.metadata.redis || this.metadata.sqlite
|
|
57890
58751
|
);
|
|
57891
58752
|
const tasksEnabledForCli = this.cliMode && hasPersistentBackend;
|
|
57892
|
-
const
|
|
58753
|
+
const hasDistributedTaskBackend = Boolean(tasksConfig?.redis ?? this.metadata.redis);
|
|
58754
|
+
const onEdgeRuntime = isEdgeRuntime();
|
|
58755
|
+
const tasksUnavailableOnEdge = onEdgeRuntime && !hasDistributedTaskBackend && !tasksExplicitlyEnabled;
|
|
58756
|
+
if (tasksUnavailableOnEdge) {
|
|
58757
|
+
this.logger.info(
|
|
58758
|
+
"[tasks] Background tasks are unavailable on this edge/serverless runtime: the in-process runner cannot continue past the HTTP response, and an in-memory store is not shared between isolates. Serving without tasks. Configure `tasks.redis` (Upstash Redis over HTTP works on Workers) to enable them."
|
|
58759
|
+
);
|
|
58760
|
+
}
|
|
58761
|
+
const shouldInitTasks = !tasksExplicitlyDisabled && !tasksUnavailableOnEdge && (!this.cliMode || tasksEnabledForCli || isTaskWorker);
|
|
57893
58762
|
const tasksPromise = shouldInitTasks ? (async () => {
|
|
57894
58763
|
const tasksRedis = tasksConfig?.redis ?? this.metadata.redis;
|
|
57895
58764
|
const tasksSqlite = tasksConfig?.sqlite ?? (tasksRedis ? void 0 : resolvedTopLevelSqlite);
|
|
57896
|
-
const onEdge =
|
|
58765
|
+
const onEdge = onEdgeRuntime;
|
|
57897
58766
|
if (onEdge) {
|
|
57898
58767
|
const msg = "[tasks] Background tasks are enabled on an edge/serverless runtime. The in-process runner cannot continue past the HTTP response on this platform \u2014 task bodies will not execute. Run on a long-lived Node process, or set `tasks.enabled: false`.";
|
|
57899
58768
|
if (tasksConfig?.strict) {
|
|
@@ -59775,7 +60644,13 @@ function normalizeAppScope(appItem, metadata) {
|
|
|
59775
60644
|
...metadata,
|
|
59776
60645
|
id: scopeId,
|
|
59777
60646
|
apps: [appItem],
|
|
59778
|
-
auth
|
|
60647
|
+
// An app that declares no `auth` INHERITS the server's policy. Writing
|
|
60648
|
+
// `appMetadata.auth` unconditionally set the key to `undefined`, which
|
|
60649
|
+
// overrode the spread above and dropped the server's auth entirely — so a
|
|
60650
|
+
// standalone app silently became public (GHSA-rgxj-434m-vxh3, where the
|
|
60651
|
+
// dashboard's own scope was the case in point). Declaring
|
|
60652
|
+
// `auth: { mode: 'public' }` explicitly is still honoured.
|
|
60653
|
+
auth: appMetadata.auth ?? metadata.auth,
|
|
59779
60654
|
// For a splitByApp scope, auth (and any auth.ui paths) come from `@App`, so
|
|
59780
60655
|
// anchor on the App's captured source dir. Fall back to the server's.
|
|
59781
60656
|
// `__sourceDir` lives on LocalAppMetadata only; read it defensively.
|
|
@@ -60033,10 +60908,15 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
60033
60908
|
while (end > 1 && withSlash.charCodeAt(end - 1) === 47) end--;
|
|
60034
60909
|
return withSlash.slice(0, end);
|
|
60035
60910
|
};
|
|
60036
|
-
const rawEntry = options.entryPath ?? (normalizeEntryPrefix(httpConfig?.entryPath) || "/");
|
|
60911
|
+
const rawEntry = options.entryPath ?? (normalizeEntryPrefix(resolveEntryPath(httpConfig?.entryPath)) || "/");
|
|
60037
60912
|
const entryPaths = new Set((Array.isArray(rawEntry) ? rawEntry : [rawEntry]).map(normalizePath));
|
|
60038
60913
|
const cors2 = options.cors ?? mapHttpCors(httpConfig?.cors, scope);
|
|
60039
60914
|
const corsEnabled = cors2?.origin !== void 0 && cors2.origin !== false;
|
|
60915
|
+
const rebinding = httpConfig?.security?.dnsRebindingProtection;
|
|
60916
|
+
const hostValidation = rebinding?.enabled === false || !rebinding?.allowedHosts?.length && !rebinding?.allowedOrigins?.length ? void 0 : compileHostValidation({
|
|
60917
|
+
allowedHosts: rebinding.allowedHosts,
|
|
60918
|
+
allowedOrigins: rebinding.allowedOrigins
|
|
60919
|
+
});
|
|
60040
60920
|
const corsHeadersFor = (request) => {
|
|
60041
60921
|
if (!corsEnabled) return {};
|
|
60042
60922
|
const reqOrigin = request.headers.get("origin") ?? "";
|
|
@@ -60067,6 +60947,19 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
60067
60947
|
};
|
|
60068
60948
|
return async function handle(request, ctx, env) {
|
|
60069
60949
|
const url = new URL(request.url);
|
|
60950
|
+
if (hostValidation) {
|
|
60951
|
+
const rejection = validateHostHeaders(
|
|
60952
|
+
{
|
|
60953
|
+
host: request.headers.get("host") ?? url.host,
|
|
60954
|
+
forwardedHost: request.headers.get("x-forwarded-host") ?? void 0,
|
|
60955
|
+
origin: request.headers.get("origin") ?? void 0
|
|
60956
|
+
},
|
|
60957
|
+
hostValidation
|
|
60958
|
+
);
|
|
60959
|
+
if (rejection) {
|
|
60960
|
+
return Response.json({ error: rejection.error, message: rejection.message }, { status: rejection.status });
|
|
60961
|
+
}
|
|
60962
|
+
}
|
|
60070
60963
|
if (corsEnabled && request.method === "OPTIONS") {
|
|
60071
60964
|
return new Response(null, { status: 204, headers: corsHeadersFor(request) });
|
|
60072
60965
|
}
|
|
@@ -60080,7 +60973,7 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
60080
60973
|
);
|
|
60081
60974
|
}
|
|
60082
60975
|
if (!entryPaths.has(normalizePath(url.pathname))) {
|
|
60083
|
-
const authResponse = await runMatchingHttpFlowWeb(scope, request);
|
|
60976
|
+
const authResponse = await runMatchingHttpFlowWeb(scope, request, { ctx, env });
|
|
60084
60977
|
if (authResponse) return withCors(authResponse, request);
|
|
60085
60978
|
return withCors(Response.json({ error: "Not Found", entryPaths: [...entryPaths] }, { status: 404 }), request);
|
|
60086
60979
|
}
|
|
@@ -60088,13 +60981,13 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
60088
60981
|
const routed = await options.sessionRouter(request, env, ctx);
|
|
60089
60982
|
if (routed) return withCors(routed, request);
|
|
60090
60983
|
}
|
|
60091
|
-
const rendered = await runHttpRequestFlowWeb(scope, request, { ctx });
|
|
60984
|
+
const rendered = await runHttpRequestFlowWeb(scope, request, { ctx, env });
|
|
60092
60985
|
return withCors(rendered ?? Response.json({ error: "Not Found" }, { status: 404 }), request);
|
|
60093
60986
|
};
|
|
60094
60987
|
}
|
|
60095
60988
|
async function runHttpRequestFlowWeb(scope, request, opts = {}) {
|
|
60096
60989
|
const url = new URL(request.url);
|
|
60097
|
-
const serverRequest = await toServerRequest(request, url, opts.ctx, opts.persistent);
|
|
60990
|
+
const serverRequest = await toServerRequest(request, url, opts.ctx, opts.persistent, opts.env);
|
|
60098
60991
|
let output;
|
|
60099
60992
|
try {
|
|
60100
60993
|
output = await scope.runFlow("http:request", {
|
|
@@ -60106,9 +60999,9 @@ async function runHttpRequestFlowWeb(scope, request, opts = {}) {
|
|
|
60106
60999
|
}
|
|
60107
61000
|
return output ? renderHttpOutputToWebResponse(output) : void 0;
|
|
60108
61001
|
}
|
|
60109
|
-
async function runMatchingHttpFlowWeb(scope, request) {
|
|
61002
|
+
async function runMatchingHttpFlowWeb(scope, request, opts = {}) {
|
|
60110
61003
|
const url = new URL(request.url);
|
|
60111
|
-
const serverRequest = await toServerRequest(request, url);
|
|
61004
|
+
const serverRequest = await toServerRequest(request, url, opts.ctx, void 0, opts.env);
|
|
60112
61005
|
const flowName = await scope.findHttpFlowName(serverRequest);
|
|
60113
61006
|
if (!flowName) return void 0;
|
|
60114
61007
|
let output;
|
|
@@ -60122,7 +61015,7 @@ async function runMatchingHttpFlowWeb(scope, request) {
|
|
|
60122
61015
|
}
|
|
60123
61016
|
return output ? renderHttpOutputToWebResponse(output) : void 0;
|
|
60124
61017
|
}
|
|
60125
|
-
async function toServerRequest(request, url, ctx, persistent) {
|
|
61018
|
+
async function toServerRequest(request, url, ctx, persistent, env) {
|
|
60126
61019
|
const headers = {};
|
|
60127
61020
|
request.headers.forEach((v, k) => {
|
|
60128
61021
|
headers[k] = v;
|
|
@@ -60166,10 +61059,39 @@ async function toServerRequest(request, url, ctx, persistent) {
|
|
|
60166
61059
|
const tokenized = serverRequest;
|
|
60167
61060
|
tokenized[ServerRequestTokens.webRequest] = webRequest;
|
|
60168
61061
|
tokenized[ServerRequestTokens.webCtx] = ctx;
|
|
61062
|
+
tokenized[ServerRequestTokens.webEnv] = env;
|
|
60169
61063
|
if (persistent) tokenized[ServerRequestTokens.webTransport] = persistent;
|
|
60170
61064
|
return serverRequest;
|
|
60171
61065
|
}
|
|
61066
|
+
function misconfigurationResponse(error) {
|
|
61067
|
+
const misconfiguration = findMisconfiguration(error);
|
|
61068
|
+
if (!misconfiguration) return void 0;
|
|
61069
|
+
return Response.json(
|
|
61070
|
+
{ error: "server_misconfigured", code: misconfiguration.code, message: misconfiguration.remedy },
|
|
61071
|
+
{ status: 500 }
|
|
61072
|
+
);
|
|
61073
|
+
}
|
|
61074
|
+
function findMisconfiguration(error) {
|
|
61075
|
+
let current = error;
|
|
61076
|
+
for (let depth = 0; current instanceof Error && depth < 5; depth++) {
|
|
61077
|
+
const code = current.code;
|
|
61078
|
+
if (typeof code === "string" && MISCONFIGURATION_REMEDIES[code]) {
|
|
61079
|
+
return { code, remedy: MISCONFIGURATION_REMEDIES[code] };
|
|
61080
|
+
}
|
|
61081
|
+
current = current.cause;
|
|
61082
|
+
}
|
|
61083
|
+
return void 0;
|
|
61084
|
+
}
|
|
60172
61085
|
function flowErrorToHttpOutput(error) {
|
|
61086
|
+
const misconfiguration = findMisconfiguration(error);
|
|
61087
|
+
if (misconfiguration) {
|
|
61088
|
+
return {
|
|
61089
|
+
kind: "json",
|
|
61090
|
+
status: 500,
|
|
61091
|
+
contentType: "application/json; charset=utf-8",
|
|
61092
|
+
body: { error: "server_misconfigured", code: misconfiguration.code, message: misconfiguration.remedy }
|
|
61093
|
+
};
|
|
61094
|
+
}
|
|
60173
61095
|
if (error instanceof FlowControl) {
|
|
60174
61096
|
switch (error.type) {
|
|
60175
61097
|
case "respond":
|
|
@@ -60193,6 +61115,7 @@ function flowErrorToHttpOutput(error) {
|
|
|
60193
61115
|
}
|
|
60194
61116
|
return { kind: "text", status: 500, body: "Internal Server Error", contentType: "text/plain; charset=utf-8" };
|
|
60195
61117
|
}
|
|
61118
|
+
var MISCONFIGURATION_REMEDIES;
|
|
60196
61119
|
var init_web_fetch_handler = __esm({
|
|
60197
61120
|
"libs/sdk/src/transport/web-fetch-handler.ts"() {
|
|
60198
61121
|
"use strict";
|
|
@@ -60200,7 +61123,13 @@ var init_web_fetch_handler = __esm({
|
|
|
60200
61123
|
init_server_tokens();
|
|
60201
61124
|
init_path_utils();
|
|
60202
61125
|
init_errors();
|
|
61126
|
+
init_host_validation();
|
|
60203
61127
|
init_web_response_renderer();
|
|
61128
|
+
MISCONFIGURATION_REMEDIES = {
|
|
61129
|
+
SESSION_SECRET_REQUIRED: "Set MCP_SESSION_SECRET in the deployment environment (e.g. `wrangler secret put MCP_SESSION_SECRET`). Session IDs are encrypted with it, and production refuses the development machine-id fallback.",
|
|
61130
|
+
JWT_SECRET_INVALID: "JWT_SECRET is present but too weak: HS256 requires at least 32 bytes (RFC 7518). Replace it with `openssl rand -hex 32`.",
|
|
61131
|
+
JWT_SECRET_REQUIRED: "Set JWT_SECRET in the deployment environment (e.g. `wrangler secret put JWT_SECRET`). Tokens are signed with it; production refuses the random per-process fallback because tokens would not survive a restart or verify across instances."
|
|
61132
|
+
};
|
|
60204
61133
|
}
|
|
60205
61134
|
});
|
|
60206
61135
|
|
|
@@ -60352,39 +61281,22 @@ function createHostValidationMiddleware(options) {
|
|
|
60352
61281
|
if (!options.enabled) {
|
|
60353
61282
|
return (_req, _res, next) => next();
|
|
60354
61283
|
}
|
|
60355
|
-
const
|
|
60356
|
-
|
|
61284
|
+
const compiled = compileHostValidation({
|
|
61285
|
+
allowedHosts: options.allowedHosts,
|
|
61286
|
+
allowedOrigins: options.allowedOrigins
|
|
61287
|
+
});
|
|
60357
61288
|
return (req, res, next) => {
|
|
60358
|
-
|
|
60359
|
-
|
|
60360
|
-
|
|
60361
|
-
|
|
60362
|
-
|
|
60363
|
-
|
|
60364
|
-
|
|
60365
|
-
|
|
60366
|
-
|
|
60367
|
-
|
|
60368
|
-
|
|
60369
|
-
const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
60370
|
-
if (candidates.some((h2) => !allowedHostsSet.has(h2))) {
|
|
60371
|
-
res.status(403).json({
|
|
60372
|
-
error: "Forbidden",
|
|
60373
|
-
message: "Invalid X-Forwarded-Host header"
|
|
60374
|
-
});
|
|
60375
|
-
return;
|
|
60376
|
-
}
|
|
60377
|
-
}
|
|
60378
|
-
}
|
|
60379
|
-
if (allowedOriginsSet) {
|
|
60380
|
-
const origin = req.headers?.["origin"];
|
|
60381
|
-
if (origin && !allowedOriginsSet.has(origin)) {
|
|
60382
|
-
res.status(403).json({
|
|
60383
|
-
error: "Forbidden",
|
|
60384
|
-
message: "Invalid Origin header"
|
|
60385
|
-
});
|
|
60386
|
-
return;
|
|
60387
|
-
}
|
|
61289
|
+
const rejection = validateHostHeaders(
|
|
61290
|
+
{
|
|
61291
|
+
host: req.headers?.["host"],
|
|
61292
|
+
forwardedHost: req.headers?.["x-forwarded-host"],
|
|
61293
|
+
origin: req.headers?.["origin"]
|
|
61294
|
+
},
|
|
61295
|
+
compiled
|
|
61296
|
+
);
|
|
61297
|
+
if (rejection) {
|
|
61298
|
+
res.status(rejection.status).json({ error: rejection.error, message: rejection.message });
|
|
61299
|
+
return;
|
|
60388
61300
|
}
|
|
60389
61301
|
next();
|
|
60390
61302
|
};
|
|
@@ -60392,6 +61304,82 @@ function createHostValidationMiddleware(options) {
|
|
|
60392
61304
|
var init_host_validation_middleware = __esm({
|
|
60393
61305
|
"libs/sdk/src/server/middleware/host-validation.middleware.ts"() {
|
|
60394
61306
|
"use strict";
|
|
61307
|
+
init_host_validation();
|
|
61308
|
+
}
|
|
61309
|
+
});
|
|
61310
|
+
|
|
61311
|
+
// libs/sdk/src/server/security/resolve-allowed-hosts.ts
|
|
61312
|
+
function canonicalizeAddress(address) {
|
|
61313
|
+
const bare = address.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
61314
|
+
if (!bare.includes(":")) return bare;
|
|
61315
|
+
try {
|
|
61316
|
+
return new URL(`http://[${bare}]`).hostname.replace(/^\[|\]$/g, "");
|
|
61317
|
+
} catch {
|
|
61318
|
+
return bare;
|
|
61319
|
+
}
|
|
61320
|
+
}
|
|
61321
|
+
function isLoopbackAddress(address) {
|
|
61322
|
+
if (!address) return false;
|
|
61323
|
+
const normalized = canonicalizeAddress(address);
|
|
61324
|
+
return normalized === "::1" || normalized === "localhost" || normalized.startsWith("127.") || // IPv4-mapped IPv6 loopback, which `URL` renders as `::ffff:7f00:1`.
|
|
61325
|
+
normalized === "::ffff:7f00:1" || normalized.startsWith("::ffff:127.");
|
|
61326
|
+
}
|
|
61327
|
+
function withAndWithoutPort(host, port) {
|
|
61328
|
+
return port ? [host, `${host}:${port}`] : [host];
|
|
61329
|
+
}
|
|
61330
|
+
function hostFromUrl(url) {
|
|
61331
|
+
if (!url) return void 0;
|
|
61332
|
+
try {
|
|
61333
|
+
return new URL(url).host || void 0;
|
|
61334
|
+
} catch {
|
|
61335
|
+
return void 0;
|
|
61336
|
+
}
|
|
61337
|
+
}
|
|
61338
|
+
function isRoutableBind(address) {
|
|
61339
|
+
if (!address) return false;
|
|
61340
|
+
return !isLoopbackAddress(address);
|
|
61341
|
+
}
|
|
61342
|
+
function deriveAllowedHosts(input) {
|
|
61343
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
61344
|
+
if (input.socketPath) return [];
|
|
61345
|
+
for (const name42 of LOOPBACK_NAMES) {
|
|
61346
|
+
for (const value of withAndWithoutPort(name42, input.port)) hosts.add(value);
|
|
61347
|
+
}
|
|
61348
|
+
const bindAddress = input.bindAddress?.trim();
|
|
61349
|
+
if (bindAddress && !isLoopbackAddress(bindAddress) && bindAddress !== "0.0.0.0" && bindAddress !== "::") {
|
|
61350
|
+
const literal = bindAddress.includes(":") && !bindAddress.startsWith("[") ? `[${bindAddress}]` : bindAddress;
|
|
61351
|
+
for (const value of withAndWithoutPort(literal, input.port)) hosts.add(value);
|
|
61352
|
+
}
|
|
61353
|
+
const issuerHost = hostFromUrl(input.issuer);
|
|
61354
|
+
if (issuerHost) {
|
|
61355
|
+
hosts.add(issuerHost);
|
|
61356
|
+
const [name42] = issuerHost.split(":");
|
|
61357
|
+
if (!issuerHost.includes(":") && input.port) hosts.add(`${name42}:${input.port}`);
|
|
61358
|
+
}
|
|
61359
|
+
for (const extra of input.extraHosts ?? []) {
|
|
61360
|
+
const trimmed = extra.trim();
|
|
61361
|
+
if (trimmed) hosts.add(trimmed);
|
|
61362
|
+
}
|
|
61363
|
+
return [...hosts];
|
|
61364
|
+
}
|
|
61365
|
+
function shouldEnforceDerivedHosts(input) {
|
|
61366
|
+
if (input.socketPath) return false;
|
|
61367
|
+
const hasExplicitName = hostFromUrl(input.issuer) !== void 0 || (input.extraHosts ?? []).some((h2) => h2.trim());
|
|
61368
|
+
if (hasExplicitName) return true;
|
|
61369
|
+
if (input.port === void 0) return false;
|
|
61370
|
+
return !isRoutableBind(input.bindAddress);
|
|
61371
|
+
}
|
|
61372
|
+
function allowedHostsFromEnv(env = process.env) {
|
|
61373
|
+
const raw = env["FRONTMCP_ALLOWED_HOSTS"]?.trim();
|
|
61374
|
+
if (!raw) return void 0;
|
|
61375
|
+
const hosts = raw.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
61376
|
+
return hosts.length > 0 ? hosts : void 0;
|
|
61377
|
+
}
|
|
61378
|
+
var LOOPBACK_NAMES;
|
|
61379
|
+
var init_resolve_allowed_hosts = __esm({
|
|
61380
|
+
"libs/sdk/src/server/security/resolve-allowed-hosts.ts"() {
|
|
61381
|
+
"use strict";
|
|
61382
|
+
LOOPBACK_NAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
60395
61383
|
}
|
|
60396
61384
|
});
|
|
60397
61385
|
|
|
@@ -60417,12 +61405,17 @@ var init_express_host_adapter = __esm({
|
|
|
60417
61405
|
"use strict";
|
|
60418
61406
|
init_mcp_error();
|
|
60419
61407
|
init_host_validation_middleware();
|
|
61408
|
+
init_resolve_allowed_hosts();
|
|
60420
61409
|
init_base_host_adapter();
|
|
60421
61410
|
DEFAULT_EXPRESS_BODY_LIMIT = "4mb";
|
|
60422
61411
|
ExpressHostAdapter = class extends HostServerAdapter {
|
|
60423
61412
|
app = express();
|
|
60424
61413
|
router = express.Router();
|
|
60425
61414
|
prepared = false;
|
|
61415
|
+
/** Active host-validation middleware, or undefined while nothing is enforced. */
|
|
61416
|
+
hostValidation;
|
|
61417
|
+
/** Builds the derived allow-list, once this process commits to listening. */
|
|
61418
|
+
deriveHostValidation;
|
|
60426
61419
|
constructor(options) {
|
|
60427
61420
|
super();
|
|
60428
61421
|
const corsOptions = options?.cors;
|
|
@@ -60436,6 +61429,7 @@ var init_express_host_adapter = __esm({
|
|
|
60436
61429
|
})
|
|
60437
61430
|
);
|
|
60438
61431
|
}
|
|
61432
|
+
this.installHostValidation(options);
|
|
60439
61433
|
const jsonLimit = options?.bodyLimit ?? DEFAULT_EXPRESS_BODY_LIMIT;
|
|
60440
61434
|
const formLimit = options?.urlencodedLimit ?? jsonLimit;
|
|
60441
61435
|
const headerContains = (req, needle) => (req.headers["content-type"] ?? "").toLowerCase().includes(needle);
|
|
@@ -60456,23 +61450,6 @@ var init_express_host_adapter = __esm({
|
|
|
60456
61450
|
}
|
|
60457
61451
|
next(err);
|
|
60458
61452
|
});
|
|
60459
|
-
const securityOpts = options?.security;
|
|
60460
|
-
if (securityOpts?.dnsRebindingProtection?.enabled || securityOpts?.strict) {
|
|
60461
|
-
const allowedHosts = securityOpts.dnsRebindingProtection?.allowedHosts;
|
|
60462
|
-
const allowedOrigins = securityOpts.dnsRebindingProtection?.allowedOrigins;
|
|
60463
|
-
const effectiveHosts = allowedHosts ?? (securityOpts.strict ? ["localhost", "127.0.0.1"] : void 0);
|
|
60464
|
-
if (!effectiveHosts?.length && !allowedOrigins?.length) {
|
|
60465
|
-
throw new Error(
|
|
60466
|
-
"security.dnsRebindingProtection is enabled but no allowedHosts or allowedOrigins are configured. Provide at least one allowedHosts entry or disable dnsRebindingProtection."
|
|
60467
|
-
);
|
|
60468
|
-
}
|
|
60469
|
-
const hostValidation = createHostValidationMiddleware({
|
|
60470
|
-
enabled: true,
|
|
60471
|
-
allowedHosts: effectiveHosts,
|
|
60472
|
-
allowedOrigins
|
|
60473
|
-
});
|
|
60474
|
-
this.app.use(hostValidation);
|
|
60475
|
-
}
|
|
60476
61453
|
this.app.use((req, res, next) => {
|
|
60477
61454
|
if (corsEnabled) {
|
|
60478
61455
|
res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate, Mcp-Session-Id");
|
|
@@ -60482,6 +61459,63 @@ var init_express_host_adapter = __esm({
|
|
|
60482
61459
|
next();
|
|
60483
61460
|
});
|
|
60484
61461
|
}
|
|
61462
|
+
/**
|
|
61463
|
+
* Install DNS-rebinding protection.
|
|
61464
|
+
*
|
|
61465
|
+
* ON by default since v1.7.2 (BC-035). `allowedHosts` falls back to the names
|
|
61466
|
+
* this process actually answers to — the loopback aliases with and without
|
|
61467
|
+
* the bound port, the bound NIC address, and the configured issuer host.
|
|
61468
|
+
*
|
|
61469
|
+
* Escape hatches, in precedence order: explicit
|
|
61470
|
+
* `security.dnsRebindingProtection.allowedHosts`, the
|
|
61471
|
+
* `FRONTMCP_ALLOWED_HOSTS` env var, or `enabled: false` to turn it off.
|
|
61472
|
+
*/
|
|
61473
|
+
installHostValidation(options) {
|
|
61474
|
+
const protection = options?.security?.dnsRebindingProtection;
|
|
61475
|
+
if (protection?.enabled === false) return;
|
|
61476
|
+
const allowedOrigins = protection?.allowedOrigins;
|
|
61477
|
+
const explicitHosts = protection?.allowedHosts ?? allowedHostsFromEnv();
|
|
61478
|
+
if (explicitHosts?.length || allowedOrigins?.length) {
|
|
61479
|
+
this.hostValidation = createHostValidationMiddleware({
|
|
61480
|
+
enabled: true,
|
|
61481
|
+
allowedHosts: explicitHosts?.length ? explicitHosts : void 0,
|
|
61482
|
+
allowedOrigins
|
|
61483
|
+
});
|
|
61484
|
+
} else {
|
|
61485
|
+
this.deriveHostValidation = (portOrSocketPath, bindAddress) => {
|
|
61486
|
+
if (typeof portOrSocketPath === "string") return void 0;
|
|
61487
|
+
const derivation = {
|
|
61488
|
+
// `start()` binds `bindAddress ?? '0.0.0.0'`; mirror that exactly.
|
|
61489
|
+
bindAddress: bindAddress ?? "0.0.0.0",
|
|
61490
|
+
port: portOrSocketPath,
|
|
61491
|
+
issuer: options?.listen?.issuer
|
|
61492
|
+
};
|
|
61493
|
+
if (!shouldEnforceDerivedHosts(derivation)) {
|
|
61494
|
+
console.warn(
|
|
61495
|
+
`[frontmcp] DNS-rebinding protection is not enforcing a Host allow-list: the server binds a routable address (${derivation.bindAddress}) and no allowed hosts are configured. Set security.dnsRebindingProtection.allowedHosts (or FRONTMCP_ALLOWED_HOSTS) to your public hostname(s).`
|
|
61496
|
+
);
|
|
61497
|
+
return void 0;
|
|
61498
|
+
}
|
|
61499
|
+
const allowedHosts = deriveAllowedHosts(derivation);
|
|
61500
|
+
if (!allowedHosts.length) return void 0;
|
|
61501
|
+
return createHostValidationMiddleware({ enabled: true, allowedHosts });
|
|
61502
|
+
};
|
|
61503
|
+
}
|
|
61504
|
+
this.app.use(((req, res, next) => {
|
|
61505
|
+
const validate = this.hostValidation;
|
|
61506
|
+
if (!validate) return next();
|
|
61507
|
+
return validate(req, res, next);
|
|
61508
|
+
}));
|
|
61509
|
+
}
|
|
61510
|
+
/**
|
|
61511
|
+
* Enable the DERIVED host allow-list. Called from `start()` only — see
|
|
61512
|
+
* `installHostValidation`.
|
|
61513
|
+
*/
|
|
61514
|
+
enableDerivedHostValidation(portOrSocketPath, bindAddress) {
|
|
61515
|
+
if (this.hostValidation || !this.deriveHostValidation) return;
|
|
61516
|
+
this.hostValidation = this.deriveHostValidation(portOrSocketPath, bindAddress);
|
|
61517
|
+
this.deriveHostValidation = void 0;
|
|
61518
|
+
}
|
|
60485
61519
|
registerRoute(method, path3, handler) {
|
|
60486
61520
|
this.router[method.toLowerCase()](path3, this.enhancedHandler(handler));
|
|
60487
61521
|
}
|
|
@@ -60521,6 +61555,7 @@ var init_express_host_adapter = __esm({
|
|
|
60521
61555
|
server.keepAliveTimeout = 75e3;
|
|
60522
61556
|
if (typeof portOrSocketPath === "string") {
|
|
60523
61557
|
await this.cleanupStaleSocket(portOrSocketPath);
|
|
61558
|
+
this.enableDerivedHostValidation(portOrSocketPath);
|
|
60524
61559
|
await new Promise((resolve, reject) => {
|
|
60525
61560
|
server.on("error", reject);
|
|
60526
61561
|
server.listen(portOrSocketPath, () => {
|
|
@@ -60538,7 +61573,11 @@ var init_express_host_adapter = __esm({
|
|
|
60538
61573
|
await new Promise((resolve, reject) => {
|
|
60539
61574
|
server.on("error", reject);
|
|
60540
61575
|
server.listen(portOrSocketPath, host, () => {
|
|
60541
|
-
|
|
61576
|
+
const bound = server.address();
|
|
61577
|
+
const boundPort = typeof bound === "object" && bound ? bound.port : portOrSocketPath;
|
|
61578
|
+
const boundAddress = typeof bound === "object" && bound ? bound.address : host;
|
|
61579
|
+
this.enableDerivedHostValidation(boundPort, boundAddress);
|
|
61580
|
+
console.log(`MCP HTTP (Express) on ${host}:${boundPort}`);
|
|
60542
61581
|
resolve();
|
|
60543
61582
|
});
|
|
60544
61583
|
});
|
|
@@ -60940,7 +61979,7 @@ var init_front_mcp = __esm({
|
|
|
60940
61979
|
frontMcp.log?.info("FrontMCP fetch handler created (web-standard transport)");
|
|
60941
61980
|
return createWebFetchHandler(scope);
|
|
60942
61981
|
};
|
|
60943
|
-
return async (request) => {
|
|
61982
|
+
return async (request, ctx, env) => {
|
|
60944
61983
|
if (!inner) {
|
|
60945
61984
|
if (!building) {
|
|
60946
61985
|
building = build().catch((err) => {
|
|
@@ -60948,9 +61987,15 @@ var init_front_mcp = __esm({
|
|
|
60948
61987
|
throw err;
|
|
60949
61988
|
});
|
|
60950
61989
|
}
|
|
60951
|
-
|
|
61990
|
+
try {
|
|
61991
|
+
inner = await building;
|
|
61992
|
+
} catch (err) {
|
|
61993
|
+
const misconfigured = misconfigurationResponse(err);
|
|
61994
|
+
if (misconfigured) return misconfigured;
|
|
61995
|
+
throw err;
|
|
61996
|
+
}
|
|
60952
61997
|
}
|
|
60953
|
-
return inner(request);
|
|
61998
|
+
return inner(request, ctx, env);
|
|
60954
61999
|
};
|
|
60955
62000
|
}
|
|
60956
62001
|
/**
|
|
@@ -63372,49 +64417,7 @@ init_job_registry();
|
|
|
63372
64417
|
init_job_instance();
|
|
63373
64418
|
init_job_events();
|
|
63374
64419
|
init_job_utils();
|
|
63375
|
-
|
|
63376
|
-
// libs/sdk/src/job/job-permission.guard.ts
|
|
63377
|
-
var JobPermissionGuard = class _JobPermissionGuard {
|
|
63378
|
-
/**
|
|
63379
|
-
* Check if the given auth info is allowed to perform the action.
|
|
63380
|
-
* Returns true if no permissions are defined (permissive by default).
|
|
63381
|
-
*/
|
|
63382
|
-
static async check(permissions, action, authInfo) {
|
|
63383
|
-
if (!permissions || permissions.length === 0) {
|
|
63384
|
-
return true;
|
|
63385
|
-
}
|
|
63386
|
-
const relevant = permissions.filter((p) => p.action === action);
|
|
63387
|
-
if (relevant.length === 0) {
|
|
63388
|
-
return true;
|
|
63389
|
-
}
|
|
63390
|
-
for (const perm of relevant) {
|
|
63391
|
-
const passed = await _JobPermissionGuard.checkSingle(perm, authInfo);
|
|
63392
|
-
if (!passed) return false;
|
|
63393
|
-
}
|
|
63394
|
-
return true;
|
|
63395
|
-
}
|
|
63396
|
-
static async checkSingle(perm, authInfo) {
|
|
63397
|
-
if (perm.roles && perm.roles.length > 0) {
|
|
63398
|
-
const userRoles = authInfo["roles"];
|
|
63399
|
-
if (!Array.isArray(userRoles)) return false;
|
|
63400
|
-
const hasRole = perm.roles.some((r) => userRoles.includes(r));
|
|
63401
|
-
if (!hasRole) return false;
|
|
63402
|
-
}
|
|
63403
|
-
if (perm.scopes && perm.scopes.length > 0) {
|
|
63404
|
-
const userScopes = authInfo["scopes"] ?? authInfo["scope"];
|
|
63405
|
-
const scopeArr = typeof userScopes === "string" ? userScopes.split(" ") : Array.isArray(userScopes) ? userScopes : [];
|
|
63406
|
-
const hasScope = perm.scopes.some((s) => scopeArr.includes(s));
|
|
63407
|
-
if (!hasScope) return false;
|
|
63408
|
-
}
|
|
63409
|
-
if (perm.custom) {
|
|
63410
|
-
const result = await perm.custom(authInfo);
|
|
63411
|
-
if (!result) return false;
|
|
63412
|
-
}
|
|
63413
|
-
return true;
|
|
63414
|
-
}
|
|
63415
|
-
};
|
|
63416
|
-
|
|
63417
|
-
// libs/sdk/src/job/index.ts
|
|
64420
|
+
init_job_permission_guard();
|
|
63418
64421
|
init_job_scope_helper();
|
|
63419
64422
|
|
|
63420
64423
|
// libs/sdk/src/job/execution/index.ts
|
|
@@ -63816,6 +64819,7 @@ export {
|
|
|
63816
64819
|
DynamicAdapter,
|
|
63817
64820
|
DynamicAdapterNameError,
|
|
63818
64821
|
DynamicJobDirectExecutionError,
|
|
64822
|
+
DynamicJobRegistrationDisabledError,
|
|
63819
64823
|
DynamicPlugin,
|
|
63820
64824
|
ELICITATION_FALLBACK_JSON_SCHEMA,
|
|
63821
64825
|
ESM_SH_BASE_URL,
|
|
@@ -63970,8 +64974,11 @@ export {
|
|
|
63970
64974
|
JobExecutionManager,
|
|
63971
64975
|
JobInstance,
|
|
63972
64976
|
JobKind,
|
|
64977
|
+
JobNotAuthorizedError,
|
|
63973
64978
|
JobPermissionGuard,
|
|
63974
64979
|
JobRegistry,
|
|
64980
|
+
JwtSecretRequiredError,
|
|
64981
|
+
JwtSecretWeakError,
|
|
63975
64982
|
LLM_ADAPTER,
|
|
63976
64983
|
LazyZodSchema,
|
|
63977
64984
|
ListJobsTool,
|
|
@@ -64399,6 +65406,7 @@ export {
|
|
|
64399
65406
|
isOrchestratedMode,
|
|
64400
65407
|
isOrchestratedRemote,
|
|
64401
65408
|
isPackageSpecifier,
|
|
65409
|
+
isProxyTrusted,
|
|
64402
65410
|
isPublicError,
|
|
64403
65411
|
isPublicMode,
|
|
64404
65412
|
isPublicUrlPinned,
|
|
@@ -64407,6 +65415,7 @@ export {
|
|
|
64407
65415
|
isRemoteMode,
|
|
64408
65416
|
isSkillDecorated,
|
|
64409
65417
|
isSkillRecord,
|
|
65418
|
+
isStaticMode,
|
|
64410
65419
|
isTaskWorkerProcess,
|
|
64411
65420
|
isTerminal,
|
|
64412
65421
|
isToolRefWithClass,
|
|
@@ -64484,9 +65493,11 @@ export {
|
|
|
64484
65493
|
remoteAuthSchema,
|
|
64485
65494
|
remoteProviderConfigSchema,
|
|
64486
65495
|
resolveDefaultSqlitePath,
|
|
65496
|
+
resolveEntryPath,
|
|
64487
65497
|
resolveNameConflicts,
|
|
64488
65498
|
resolveOutputSchemaMode,
|
|
64489
65499
|
resolvePendingTC39HooksForClass,
|
|
65500
|
+
resolvePrincipal2 as resolvePrincipal,
|
|
64490
65501
|
resolveRegistryToken,
|
|
64491
65502
|
resolveSchemaDescriptionFormat,
|
|
64492
65503
|
resolveToolVisibility,
|
|
@@ -64525,6 +65536,7 @@ export {
|
|
|
64525
65536
|
skillsConfigOptionsSchema,
|
|
64526
65537
|
skippedAppBehaviorSchema,
|
|
64527
65538
|
sqliteOptionsSchema,
|
|
65539
|
+
staticAuthOptionsSchema,
|
|
64528
65540
|
storageProviderSchema,
|
|
64529
65541
|
supportsStreaming,
|
|
64530
65542
|
toJSONSchema9 as toJSONSchema,
|