@frontmcp/sdk 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auth/flows/oauth.authorize.flow.d.ts +30 -0
- package/auth/flows/oauth.authorize.flow.d.ts.map +1 -1
- package/auth/flows/oauth.callback.flow.d.ts.map +1 -1
- package/auth/instances/instance.local-primary-auth.d.ts +35 -0
- package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
- package/builtin/config/providers/config-loader.d.ts +5 -0
- package/builtin/config/providers/config-loader.d.ts.map +1 -1
- package/common/metadata/front-mcp.metadata.d.ts +26 -14
- package/common/metadata/front-mcp.metadata.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/principal.utils.d.ts +29 -0
- package/common/utils/principal.utils.d.ts.map +1 -0
- package/direct/llm-platform.d.ts +2 -2
- package/direct/llm-platform.d.ts.map +1 -1
- package/errors/index.d.ts +1 -0
- package/errors/index.d.ts.map +1 -1
- package/errors/job.errors.d.ts +26 -0
- package/errors/job.errors.d.ts.map +1 -0
- package/esm/index.mjs +899 -185
- package/front-mcp/front-mcp.providers.d.ts +4 -4
- package/index.js +1012 -298
- 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/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/transport/adapters/base-sse-transport.d.ts +7 -0
- package/transport/adapters/base-sse-transport.d.ts.map +1 -1
- package/transport/adapters/transport.local.adapter.d.ts.map +1 -1
- package/transport/adapters/transport.streamable-http.adapter.d.ts.map +1 -1
- package/transport/event-stores/index.d.ts +2 -0
- package/transport/event-stores/index.d.ts.map +1 -1
- package/transport/event-stores/memory.event-store.d.ts +9 -2
- package/transport/event-stores/memory.event-store.d.ts.map +1 -1
- package/transport/event-stores/redis.event-store.d.ts +24 -3
- package/transport/event-stores/redis.event-store.d.ts.map +1 -1
- package/transport/event-stores/session-scoped.event-store.d.ts +17 -0
- package/transport/event-stores/session-scoped.event-store.d.ts.map +1 -0
- package/transport/web-fetch-handler.d.ts.map +1 -1
- package/workflow/tools/execute-workflow.tool.d.ts.map +1 -1
- package/workflow/tools/get-workflow-status.tool.d.ts +12 -0
- package/workflow/tools/get-workflow-status.tool.d.ts.map +1 -1
- package/workflow/tools/list-workflows.tool.d.ts +1 -0
- package/workflow/tools/list-workflows.tool.d.ts.map +1 -1
- package/workflow/tools/register-workflow.tool.d.ts +1 -0
- package/workflow/tools/register-workflow.tool.d.ts.map +1 -1
- package/workflow/tools/remove-workflow.tool.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -2345,7 +2345,10 @@ var init_schema7 = __esm({
|
|
|
2345
2345
|
});
|
|
2346
2346
|
eventStoreConfigSchema = z12.object({
|
|
2347
2347
|
enabled: z12.boolean().default(false),
|
|
2348
|
-
|
|
2348
|
+
// 'sqlite' is supported by createEventStore and the TS interface but was
|
|
2349
|
+
// missing from this enum, so a valid `provider: 'sqlite'` config failed
|
|
2350
|
+
// validation.
|
|
2351
|
+
provider: z12.enum(["memory", "redis", "sqlite"]).optional().default("memory"),
|
|
2349
2352
|
maxEvents: z12.number().int().positive().optional().default(1e4),
|
|
2350
2353
|
ttlMs: z12.number().int().positive().optional().default(3e5),
|
|
2351
2354
|
redis: redisOptionsSchema.optional()
|
|
@@ -4081,7 +4084,7 @@ function formatToolsForPlatform(tools, platform) {
|
|
|
4081
4084
|
})
|
|
4082
4085
|
);
|
|
4083
4086
|
case "vercel-ai": {
|
|
4084
|
-
const result =
|
|
4087
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
4085
4088
|
for (const tool of tools) {
|
|
4086
4089
|
result[tool.name] = {
|
|
4087
4090
|
description: tool.description ?? `Execute ${tool.name}`,
|
|
@@ -5636,6 +5639,32 @@ var init_sdk_errors = __esm({
|
|
|
5636
5639
|
}
|
|
5637
5640
|
});
|
|
5638
5641
|
|
|
5642
|
+
// libs/sdk/src/errors/job.errors.ts
|
|
5643
|
+
var JobNotAuthorizedError, DynamicJobRegistrationDisabledError;
|
|
5644
|
+
var init_job_errors = __esm({
|
|
5645
|
+
"libs/sdk/src/errors/job.errors.ts"() {
|
|
5646
|
+
"use strict";
|
|
5647
|
+
init_mcp_error();
|
|
5648
|
+
JobNotAuthorizedError = class extends PublicMcpError {
|
|
5649
|
+
/** The requested name, for internal logs only — never surfaced publicly. */
|
|
5650
|
+
requestedName;
|
|
5651
|
+
constructor(requestedName) {
|
|
5652
|
+
super(`Job or workflow "${requestedName}" not found or not permitted`, "JOB_NOT_AUTHORIZED", 403);
|
|
5653
|
+
this.requestedName = requestedName;
|
|
5654
|
+
}
|
|
5655
|
+
};
|
|
5656
|
+
DynamicJobRegistrationDisabledError = class extends PublicMcpError {
|
|
5657
|
+
constructor(kind = "job") {
|
|
5658
|
+
super(
|
|
5659
|
+
`Dynamic ${kind} registration is disabled. Enable it with jobs: { allowDynamicRegistration: true }.`,
|
|
5660
|
+
"DYNAMIC_REGISTRATION_DISABLED",
|
|
5661
|
+
403
|
|
5662
|
+
);
|
|
5663
|
+
}
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
});
|
|
5667
|
+
|
|
5639
5668
|
// libs/sdk/src/errors/index.ts
|
|
5640
5669
|
var errors_exports = {};
|
|
5641
5670
|
__export(errors_exports, {
|
|
@@ -5661,6 +5690,7 @@ __export(errors_exports, {
|
|
|
5661
5690
|
DependencyNotFoundError: () => DependencyNotFoundError,
|
|
5662
5691
|
DynamicAdapterNameError: () => DynamicAdapterNameError,
|
|
5663
5692
|
DynamicJobDirectExecutionError: () => DynamicJobDirectExecutionError,
|
|
5693
|
+
DynamicJobRegistrationDisabledError: () => DynamicJobRegistrationDisabledError,
|
|
5664
5694
|
ElicitationDisabledError: () => ElicitationDisabledError,
|
|
5665
5695
|
ElicitationEncryptionError: () => ElicitationEncryptionError,
|
|
5666
5696
|
ElicitationFallbackRequired: () => ElicitationFallbackRequired,
|
|
@@ -5709,6 +5739,7 @@ __export(errors_exports, {
|
|
|
5709
5739
|
InvalidUseFactoryError: () => InvalidUseFactoryError,
|
|
5710
5740
|
InvalidUseValueError: () => InvalidUseValueError,
|
|
5711
5741
|
InvokeStateMissingKeyError: () => InvokeStateMissingKeyError,
|
|
5742
|
+
JobNotAuthorizedError: () => JobNotAuthorizedError,
|
|
5712
5743
|
MCP_ERROR_CODES: () => MCP_ERROR_CODES,
|
|
5713
5744
|
McpError: () => McpError,
|
|
5714
5745
|
MethodNotImplementedError: () => MethodNotImplementedError,
|
|
@@ -5831,6 +5862,7 @@ var init_errors = __esm({
|
|
|
5831
5862
|
init_workflow_errors();
|
|
5832
5863
|
init_esm_errors();
|
|
5833
5864
|
init_sdk_errors();
|
|
5865
|
+
init_job_errors();
|
|
5834
5866
|
}
|
|
5835
5867
|
});
|
|
5836
5868
|
|
|
@@ -7158,6 +7190,10 @@ var require_dist = __commonJS({
|
|
|
7158
7190
|
}
|
|
7159
7191
|
return id;
|
|
7160
7192
|
}
|
|
7193
|
+
async getStreamIdForEventId(eventId) {
|
|
7194
|
+
const row = this.prepared().getStreamId.get(eventId);
|
|
7195
|
+
return row?.stream_id;
|
|
7196
|
+
}
|
|
7161
7197
|
async replayEventsAfter(lastEventId, { send }) {
|
|
7162
7198
|
const stmts = this.prepared();
|
|
7163
7199
|
const streamRow = stmts.getStreamId.get(lastEventId);
|
|
@@ -14073,26 +14109,44 @@ async function loadYamlConfig(basePath, configPath) {
|
|
|
14073
14109
|
}
|
|
14074
14110
|
return {};
|
|
14075
14111
|
}
|
|
14112
|
+
function stripUnsafeKeys(value) {
|
|
14113
|
+
if (Array.isArray(value)) return value.map(stripUnsafeKeys);
|
|
14114
|
+
if (!isPlainObject(value)) return value;
|
|
14115
|
+
const clean = {};
|
|
14116
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
14117
|
+
if (UNSAFE_MERGE_KEYS.has(key)) continue;
|
|
14118
|
+
clean[key] = stripUnsafeKeys(nested);
|
|
14119
|
+
}
|
|
14120
|
+
return clean;
|
|
14121
|
+
}
|
|
14122
|
+
function isPlainObject(value) {
|
|
14123
|
+
if (!value || typeof value !== "object") return false;
|
|
14124
|
+
const proto = Object.getPrototypeOf(value);
|
|
14125
|
+
return proto === Object.prototype || proto === null;
|
|
14126
|
+
}
|
|
14076
14127
|
function deepMerge(target, source) {
|
|
14077
14128
|
const result = { ...target };
|
|
14078
14129
|
for (const key in source) {
|
|
14130
|
+
if (UNSAFE_MERGE_KEYS.has(key)) continue;
|
|
14079
14131
|
const sourceVal = source[key];
|
|
14080
14132
|
const targetVal = result[key];
|
|
14081
14133
|
if (sourceVal !== void 0 && sourceVal !== null) {
|
|
14082
14134
|
if (typeof sourceVal === "object" && !Array.isArray(sourceVal) && typeof targetVal === "object" && !Array.isArray(targetVal) && targetVal !== null) {
|
|
14083
14135
|
result[key] = deepMerge(targetVal, sourceVal);
|
|
14084
14136
|
} else {
|
|
14085
|
-
result[key] = sourceVal;
|
|
14137
|
+
result[key] = stripUnsafeKeys(sourceVal);
|
|
14086
14138
|
}
|
|
14087
14139
|
}
|
|
14088
14140
|
}
|
|
14089
14141
|
return result;
|
|
14090
14142
|
}
|
|
14143
|
+
var UNSAFE_MERGE_KEYS;
|
|
14091
14144
|
var init_config_loader = __esm({
|
|
14092
14145
|
"libs/sdk/src/builtin/config/providers/config-loader.ts"() {
|
|
14093
14146
|
"use strict";
|
|
14094
14147
|
init_config_service();
|
|
14095
14148
|
init_env_loader();
|
|
14149
|
+
UNSAFE_MERGE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
14096
14150
|
}
|
|
14097
14151
|
});
|
|
14098
14152
|
|
|
@@ -20746,6 +20800,7 @@ var init_tool_credentials_gate = __esm({
|
|
|
20746
20800
|
});
|
|
20747
20801
|
|
|
20748
20802
|
// libs/sdk/src/tool/flows/call-tool.flow.ts
|
|
20803
|
+
import { signIncrementalAuthTicket } from "@frontmcp/auth";
|
|
20749
20804
|
import { ConcurrencyLimitError, ExecutionTimeoutError, withTimeout } from "@frontmcp/guard";
|
|
20750
20805
|
import { z as z56 } from "@frontmcp/lazy-zod";
|
|
20751
20806
|
import { CallToolRequestSchema as CallToolRequestSchema2, CallToolResultSchema as CallToolResultSchema2 } from "@frontmcp/protocol";
|
|
@@ -21134,7 +21189,7 @@ var init_call_tool_flow = __esm({
|
|
|
21134
21189
|
this.logger.verbose(`checkToolAuthorization: using anonymous for ${appId}`);
|
|
21135
21190
|
return;
|
|
21136
21191
|
}
|
|
21137
|
-
const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "");
|
|
21192
|
+
const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "", authInfo, claimAuthorizedApps);
|
|
21138
21193
|
this.logger.info(`checkToolAuthorization: authorization required for app "${appId}"`);
|
|
21139
21194
|
throw new AuthorizationRequiredError({
|
|
21140
21195
|
appId,
|
|
@@ -21146,11 +21201,51 @@ var init_call_tool_flow = __esm({
|
|
|
21146
21201
|
this.logger.verbose("checkToolAuthorization:done");
|
|
21147
21202
|
}
|
|
21148
21203
|
/**
|
|
21149
|
-
* Build the URL for progressive/incremental authorization
|
|
21204
|
+
* Build the URL for progressive/incremental authorization.
|
|
21205
|
+
*
|
|
21206
|
+
* This is the ONE point in the request lifecycle where the caller's identity
|
|
21207
|
+
* is both known and verified, so it is where the incremental-authorization
|
|
21208
|
+
* ticket is minted (GHSA-2c4g-9c8x-6m8g). The ticket is what later authorizes
|
|
21209
|
+
* `/oauth/authorize` to skip the login step; `app`/`tool` are carried purely
|
|
21210
|
+
* for display and are re-read from the signed payload server-side.
|
|
21211
|
+
*
|
|
21212
|
+
* When no verified subject or signing secret is available the URL is emitted
|
|
21213
|
+
* WITHOUT a ticket — the user then completes an ordinary login, which is the
|
|
21214
|
+
* correct fail-closed outcome rather than a credential-free shortcut.
|
|
21150
21215
|
*/
|
|
21151
|
-
buildProgressiveAuthUrl(appId, toolId) {
|
|
21216
|
+
buildProgressiveAuthUrl(appId, toolId, authInfo, priorAppIds) {
|
|
21152
21217
|
const baseUrl = this.scope.fullPath || "";
|
|
21153
|
-
|
|
21218
|
+
const url = new URL(`${baseUrl}/oauth/authorize`, "http://placeholder.invalid");
|
|
21219
|
+
url.searchParams.set("app", appId);
|
|
21220
|
+
url.searchParams.set("tool", toolId);
|
|
21221
|
+
const ticket = this.mintIncrementalTicket(appId, toolId, authInfo, priorAppIds);
|
|
21222
|
+
if (ticket) {
|
|
21223
|
+
url.searchParams.set("ticket", ticket);
|
|
21224
|
+
} else {
|
|
21225
|
+
this.logger.verbose("buildProgressiveAuthUrl: no incremental ticket minted \u2014 link requires a full login");
|
|
21226
|
+
}
|
|
21227
|
+
return baseUrl.startsWith("http") ? url.toString() : `${url.pathname}${url.search}`;
|
|
21228
|
+
}
|
|
21229
|
+
/**
|
|
21230
|
+
* Mint a single-use, short-lived incremental-authorization ticket for the
|
|
21231
|
+
* verified caller. Returns undefined when the scope has no local signing
|
|
21232
|
+
* secret or the request has no verified subject.
|
|
21233
|
+
*/
|
|
21234
|
+
mintIncrementalTicket(appId, toolId, authInfo, priorAppIds) {
|
|
21235
|
+
const extraUser = authInfo?.extra?.user;
|
|
21236
|
+
const sub = extraUser?.sub ?? authInfo?.user?.sub;
|
|
21237
|
+
if (!sub) return void 0;
|
|
21238
|
+
const secret = this.scope.auth?.signingSecret;
|
|
21239
|
+
if (!secret) return void 0;
|
|
21240
|
+
return signIncrementalAuthTicket(
|
|
21241
|
+
{
|
|
21242
|
+
sub,
|
|
21243
|
+
appId,
|
|
21244
|
+
toolId,
|
|
21245
|
+
...priorAppIds && priorAppIds.size > 0 ? { priorAppIds: [...priorAppIds] } : {}
|
|
21246
|
+
},
|
|
21247
|
+
secret
|
|
21248
|
+
);
|
|
21154
21249
|
}
|
|
21155
21250
|
async checkEntryAuthorities() {
|
|
21156
21251
|
this.logger.verbose("checkEntryAuthorities:start");
|
|
@@ -35896,7 +35991,8 @@ import {
|
|
|
35896
35991
|
createFederatedAuthSession,
|
|
35897
35992
|
escapeHtml as escapeHtml2,
|
|
35898
35993
|
renderLocalLoginPage,
|
|
35899
|
-
startNextProvider
|
|
35994
|
+
startNextProvider,
|
|
35995
|
+
verifyIncrementalAuthTicket
|
|
35900
35996
|
} from "@frontmcp/auth";
|
|
35901
35997
|
import { z as z64 } from "@frontmcp/lazy-zod";
|
|
35902
35998
|
import { generateCodeVerifier, randomUUID as randomUUID15, sha256Base64url } from "@frontmcp/utils";
|
|
@@ -35971,6 +36067,13 @@ var init_oauth_authorize_flow = __esm({
|
|
|
35971
36067
|
targetAppId: z64.string().optional().describe("Target app ID for incremental authorization"),
|
|
35972
36068
|
targetToolId: z64.string().optional().describe("Target tool ID that triggered the incremental auth"),
|
|
35973
36069
|
existingSessionId: z64.string().optional().describe("Existing session ID for incremental auth"),
|
|
36070
|
+
/**
|
|
36071
|
+
* The VERIFIED incremental-authorization ticket (GHSA-2c4g-9c8x-6m8g). Set
|
|
36072
|
+
* only when `?ticket=` carried a valid, unexpired server signature. Its `sub`
|
|
36073
|
+
* is the proven identity the expanded grant is minted for; `isIncrementalAuth`
|
|
36074
|
+
* is derived from its presence, never from a raw query parameter.
|
|
36075
|
+
*/
|
|
36076
|
+
incrementalTicket: z64.custom().optional(),
|
|
35974
36077
|
/**
|
|
35975
36078
|
* Apps the client already holds a grant for (the prior `authorized_apps`
|
|
35976
36079
|
* claim), carried forward on an incremental authorize so the newly-minted
|
|
@@ -36023,18 +36126,21 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36023
36126
|
const { request } = this.rawInput;
|
|
36024
36127
|
const rawRedirectUri = request.query["redirect_uri"];
|
|
36025
36128
|
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"];
|
|
36129
|
+
const incrementalTicket = this.verifyIncrementalTicket(request.query["ticket"]);
|
|
36130
|
+
const isIncrementalAuth = incrementalTicket !== void 0;
|
|
36131
|
+
const targetAppId = incrementalTicket?.appId;
|
|
36132
|
+
const targetToolId = incrementalTicket?.toolId;
|
|
36032
36133
|
let priorAuthorizedAppIds;
|
|
36033
|
-
if (
|
|
36034
|
-
|
|
36035
|
-
|
|
36036
|
-
|
|
36134
|
+
if (incrementalTicket) {
|
|
36135
|
+
priorAuthorizedAppIds = incrementalTicket.priorAppIds && incrementalTicket.priorAppIds.length > 0 ? Array.from(new Set(incrementalTicket.priorAppIds)) : void 0;
|
|
36136
|
+
} else {
|
|
36137
|
+
const appsParam = request.query["apps"];
|
|
36138
|
+
if (appsParam) {
|
|
36139
|
+
const ids = (Array.isArray(appsParam) ? appsParam : [appsParam]).flatMap((v) => String(v).split(",")).map((v) => v.trim()).filter(Boolean);
|
|
36140
|
+
priorAuthorizedAppIds = ids.length > 0 ? Array.from(new Set(ids)) : void 0;
|
|
36141
|
+
}
|
|
36037
36142
|
}
|
|
36143
|
+
const existingSessionId = request.query["session_id"];
|
|
36038
36144
|
const isDefaultAuthProvider = !metadata.auth;
|
|
36039
36145
|
let requiresFederatedLogin = false;
|
|
36040
36146
|
if (metadata.auth && isOrchestratedMode(metadata.auth)) {
|
|
@@ -36060,6 +36166,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36060
36166
|
targetToolId,
|
|
36061
36167
|
existingSessionId,
|
|
36062
36168
|
priorAuthorizedAppIds,
|
|
36169
|
+
incrementalTicket,
|
|
36063
36170
|
// Federated Login
|
|
36064
36171
|
requiresFederatedLogin,
|
|
36065
36172
|
// Consent Flow
|
|
@@ -36185,6 +36292,28 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36185
36292
|
}
|
|
36186
36293
|
}
|
|
36187
36294
|
}
|
|
36295
|
+
/**
|
|
36296
|
+
* Verify a client-supplied incremental-authorization ticket.
|
|
36297
|
+
*
|
|
36298
|
+
* Returns the verified payload, or `undefined` when the parameter is absent,
|
|
36299
|
+
* malformed, expired, or not signed by this server. Verification is
|
|
36300
|
+
* side-effect free — the ticket is CLAIMED (single use) later, in
|
|
36301
|
+
* {@link prepareAuthorizationRequest}, so a request that fails OAuth
|
|
36302
|
+
* validation does not burn the user's ticket.
|
|
36303
|
+
*/
|
|
36304
|
+
verifyIncrementalTicket(raw) {
|
|
36305
|
+
const token = typeof raw === "string" ? raw : Array.isArray(raw) ? raw[0] : void 0;
|
|
36306
|
+
if (typeof token !== "string" || token.length === 0) return void 0;
|
|
36307
|
+
const localAuth = this.scope.auth;
|
|
36308
|
+
const secret = localAuth?.signingSecret;
|
|
36309
|
+
if (!secret) return void 0;
|
|
36310
|
+
const payload = verifyIncrementalAuthTicket(token, secret);
|
|
36311
|
+
if (!payload) {
|
|
36312
|
+
this.logger.warn("Incremental auth ticket failed verification \u2014 treating as an ordinary login");
|
|
36313
|
+
return void 0;
|
|
36314
|
+
}
|
|
36315
|
+
return payload;
|
|
36316
|
+
}
|
|
36188
36317
|
async checkIfAuthorized() {
|
|
36189
36318
|
}
|
|
36190
36319
|
async prepareAuthorizationRequest() {
|
|
@@ -36196,7 +36325,8 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36196
36325
|
existingSessionId,
|
|
36197
36326
|
priorAuthorizedAppIds,
|
|
36198
36327
|
requiresFederatedLogin,
|
|
36199
|
-
requiresConsent
|
|
36328
|
+
requiresConsent,
|
|
36329
|
+
incrementalTicket
|
|
36200
36330
|
} = this.state;
|
|
36201
36331
|
const { metadata } = this.scope;
|
|
36202
36332
|
if (!validatedRequest) {
|
|
@@ -36209,6 +36339,18 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36209
36339
|
}
|
|
36210
36340
|
const localAuth = auth;
|
|
36211
36341
|
const store = localAuth.authorizationStore;
|
|
36342
|
+
let effectiveIncremental = isIncrementalAuth;
|
|
36343
|
+
if (incrementalTicket) {
|
|
36344
|
+
const claimed = await localAuth.claimIncrementalTicket(
|
|
36345
|
+
incrementalTicket.jti,
|
|
36346
|
+
Math.max(0, incrementalTicket.exp - Date.now())
|
|
36347
|
+
);
|
|
36348
|
+
if (!claimed) {
|
|
36349
|
+
this.logger.warn("Incremental auth ticket already used \u2014 falling back to an ordinary login");
|
|
36350
|
+
effectiveIncremental = false;
|
|
36351
|
+
this.state.set({ isIncrementalAuth: false, targetAppId: void 0, targetToolId: void 0 });
|
|
36352
|
+
}
|
|
36353
|
+
}
|
|
36212
36354
|
let federatedLogin;
|
|
36213
36355
|
if (requiresFederatedLogin) {
|
|
36214
36356
|
const apps = this.scope.apps.getApps();
|
|
@@ -36256,10 +36398,14 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36256
36398
|
},
|
|
36257
36399
|
state: validatedRequest.state,
|
|
36258
36400
|
resource: validatedRequest.resource,
|
|
36259
|
-
// Progressive/Incremental Authorization Fields
|
|
36260
|
-
|
|
36261
|
-
|
|
36262
|
-
|
|
36401
|
+
// Progressive/Incremental Authorization Fields. `isIncremental` reflects a
|
|
36402
|
+
// CLAIMED, server-signed ticket; `incrementalSub` is the identity that
|
|
36403
|
+
// ticket proved, and is the only subject the callback may mint for when
|
|
36404
|
+
// it skips the credential gate.
|
|
36405
|
+
isIncremental: effectiveIncremental,
|
|
36406
|
+
incrementalSub: effectiveIncremental ? incrementalTicket?.sub : void 0,
|
|
36407
|
+
targetAppId: effectiveIncremental ? targetAppId : void 0,
|
|
36408
|
+
targetToolId: effectiveIncremental ? targetToolId : void 0,
|
|
36263
36409
|
existingSessionId,
|
|
36264
36410
|
priorAuthorizedAppIds,
|
|
36265
36411
|
// Federated Login State
|
|
@@ -36269,7 +36415,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36269
36415
|
});
|
|
36270
36416
|
await localAuth.authorizationStore.storePendingAuthorization(pendingRecord);
|
|
36271
36417
|
this.logger.info(
|
|
36272
|
-
`Pending authorization created: ${pendingRecord.id}${
|
|
36418
|
+
`Pending authorization created: ${pendingRecord.id}${effectiveIncremental ? ` (incremental for app: ${targetAppId})` : ""}${requiresFederatedLogin ? " (federated)" : ""}${requiresConsent ? " (consent enabled)" : ""}`
|
|
36273
36419
|
);
|
|
36274
36420
|
this.state.set("pendingAuthId", pendingRecord.id);
|
|
36275
36421
|
}
|
|
@@ -36836,8 +36982,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36836
36982
|
const email = request.query["email"];
|
|
36837
36983
|
const name42 = request.query["name"];
|
|
36838
36984
|
const loginFields = this.collectLoginFields(request);
|
|
36839
|
-
const isIncremental = request.query["incremental"] === "true";
|
|
36840
|
-
const targetAppId = request.query["app_id"];
|
|
36841
36985
|
const isFederated = request.query["federated"] === "true";
|
|
36842
36986
|
const providersParam = request.query["providers"];
|
|
36843
36987
|
let selectedProviders;
|
|
@@ -36856,8 +37000,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36856
37000
|
pendingAuthId,
|
|
36857
37001
|
email,
|
|
36858
37002
|
name: name42,
|
|
36859
|
-
isIncremental,
|
|
36860
|
-
targetAppId,
|
|
36861
37003
|
isFederated,
|
|
36862
37004
|
selectedProviders,
|
|
36863
37005
|
selectedTools,
|
|
@@ -36865,9 +37007,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36865
37007
|
loginFields,
|
|
36866
37008
|
csrf
|
|
36867
37009
|
});
|
|
36868
|
-
if (isIncremental) {
|
|
36869
|
-
this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
|
|
36870
|
-
}
|
|
36871
37010
|
if (isFederated) {
|
|
36872
37011
|
this.logger.info(`Federated login callback with ${selectedProviders?.length || 0} selected providers`);
|
|
36873
37012
|
}
|
|
@@ -36876,31 +37015,13 @@ var init_oauth_callback_flow = __esm({
|
|
|
36876
37015
|
}
|
|
36877
37016
|
}
|
|
36878
37017
|
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;
|
|
37018
|
+
const { pendingAuthId, email, name: name42, isFederated, selectedProviders, selectedTools, consentSubmitted, loginFields } = this.state;
|
|
36890
37019
|
if (!pendingAuthId) {
|
|
36891
37020
|
this.logger.warn("Missing pending_auth_id in callback");
|
|
36892
37021
|
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Missing pending_auth_id parameter"), 400));
|
|
36893
37022
|
return;
|
|
36894
37023
|
}
|
|
36895
37024
|
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
37025
|
const pendingAuth = await localAuth.authorizationStore.getPendingAuthorization(pendingAuthId);
|
|
36905
37026
|
if (!pendingAuth) {
|
|
36906
37027
|
this.logger.warn(`Pending authorization not found or expired: ${pendingAuthId}`);
|
|
@@ -36912,6 +37033,20 @@ var init_oauth_callback_flow = __esm({
|
|
|
36912
37033
|
);
|
|
36913
37034
|
return;
|
|
36914
37035
|
}
|
|
37036
|
+
const isIncremental = pendingAuth.isIncremental === true && typeof pendingAuth.incrementalSub === "string";
|
|
37037
|
+
const targetAppId = isIncremental ? pendingAuth.targetAppId : void 0;
|
|
37038
|
+
this.state.set({ isIncremental, targetAppId });
|
|
37039
|
+
if (isIncremental) {
|
|
37040
|
+
this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
|
|
37041
|
+
}
|
|
37042
|
+
const localOptions = localAuth.options;
|
|
37043
|
+
const authenticateFn = typeof localOptions.authenticate === "function" ? localOptions.authenticate : void 0;
|
|
37044
|
+
const requireEmail = localOptions.requireEmail ?? true;
|
|
37045
|
+
if (!authenticateFn && !isIncremental && !email && requireEmail) {
|
|
37046
|
+
this.logger.warn("Missing email in callback");
|
|
37047
|
+
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Email is required"), 400));
|
|
37048
|
+
return;
|
|
37049
|
+
}
|
|
36915
37050
|
if (pendingAuth.authUiCsrf) {
|
|
36916
37051
|
const submitted = this.state.csrf;
|
|
36917
37052
|
if (!submitted || !timingSafeEqualStr3(pendingAuth.authUiCsrf, submitted)) {
|
|
@@ -36926,9 +37061,11 @@ var init_oauth_callback_flow = __esm({
|
|
|
36926
37061
|
return;
|
|
36927
37062
|
}
|
|
36928
37063
|
let userSub;
|
|
36929
|
-
if (
|
|
37064
|
+
if (isIncremental) {
|
|
37065
|
+
userSub = pendingAuth.incrementalSub;
|
|
37066
|
+
} else if (email) {
|
|
36930
37067
|
userSub = this.generateUserSub(email);
|
|
36931
|
-
} else if (!requireEmail
|
|
37068
|
+
} else if (!requireEmail) {
|
|
36932
37069
|
const anonymousSubject = localOptions.anonymousSubject ?? "local-operator";
|
|
36933
37070
|
userSub = this.generateUserSub(anonymousSubject);
|
|
36934
37071
|
} else {
|
|
@@ -37097,10 +37234,13 @@ var init_oauth_callback_flow = __esm({
|
|
|
37097
37234
|
originalState: pendingAuth.state,
|
|
37098
37235
|
resource: pendingAuth.resource,
|
|
37099
37236
|
userSub,
|
|
37100
|
-
// Progressive/Incremental Authorization from pending record
|
|
37101
|
-
|
|
37102
|
-
|
|
37103
|
-
|
|
37237
|
+
// Progressive/Incremental Authorization — from the pending record ONLY.
|
|
37238
|
+
// `isIncremental` was derived above from the record plus its proven
|
|
37239
|
+
// subject; OR-ing a client-supplied flag in here is what let a caller
|
|
37240
|
+
// override the server's `false` (GHSA-2c4g-9c8x-6m8g).
|
|
37241
|
+
isIncremental,
|
|
37242
|
+
targetAppId,
|
|
37243
|
+
targetToolId: isIncremental ? pendingAuth.targetToolId : void 0,
|
|
37104
37244
|
existingSessionId: pendingAuth.existingSessionId,
|
|
37105
37245
|
existingAuthorizationId: pendingAuth.existingAuthorizationId,
|
|
37106
37246
|
priorAuthorizedAppIds: pendingAuth.priorAuthorizedAppIds,
|
|
@@ -37704,6 +37844,7 @@ var init_oauth_callback_flow = __esm({
|
|
|
37704
37844
|
"pending_auth_id",
|
|
37705
37845
|
"incremental",
|
|
37706
37846
|
"app_id",
|
|
37847
|
+
"ticket",
|
|
37707
37848
|
"federated",
|
|
37708
37849
|
"providers",
|
|
37709
37850
|
"tools",
|
|
@@ -40261,7 +40402,8 @@ import {
|
|
|
40261
40402
|
MemoryStorageAdapter,
|
|
40262
40403
|
randomBytes as randomBytes6,
|
|
40263
40404
|
randomUUID as randomUUID21,
|
|
40264
|
-
sha256Hex as sha256Hex9
|
|
40405
|
+
sha256Hex as sha256Hex9,
|
|
40406
|
+
StorageNotSupportedError
|
|
40265
40407
|
} from "@frontmcp/utils";
|
|
40266
40408
|
function getDefaultNoAuthSecret() {
|
|
40267
40409
|
if (!defaultNoAuthSecret) {
|
|
@@ -40283,6 +40425,9 @@ function validateAuthorizationIssuer(received, expected) {
|
|
|
40283
40425
|
}
|
|
40284
40426
|
return { ok: true };
|
|
40285
40427
|
}
|
|
40428
|
+
function isUnsupportedOperation(error) {
|
|
40429
|
+
return error instanceof StorageNotSupportedError;
|
|
40430
|
+
}
|
|
40286
40431
|
var defaultNoAuthSecret, RESERVED_JWT_CLAIMS, LocalPrimaryAuth;
|
|
40287
40432
|
var init_instance_local_primary_auth = __esm({
|
|
40288
40433
|
"libs/sdk/src/auth/instances/instance.local-primary-auth.ts"() {
|
|
@@ -40394,6 +40539,17 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40394
40539
|
dcrClientRegistryImpl;
|
|
40395
40540
|
/** Storage adapter backing the persistent stores (kept for disposal). */
|
|
40396
40541
|
storageAdapter;
|
|
40542
|
+
/**
|
|
40543
|
+
* Replay guard for incremental-authorization tickets (GHSA-2c4g-9c8x-6m8g).
|
|
40544
|
+
* Shares the persistent adapter when one is configured so the guard holds
|
|
40545
|
+
* across a distributed deployment; falls back to a dedicated in-memory
|
|
40546
|
+
* adapter, matching how the credential vault selects its backing store.
|
|
40547
|
+
*
|
|
40548
|
+
* Holds the in-flight PROMISE, not the resolved adapter: two concurrent
|
|
40549
|
+
* claims for the same ticket must not each build their own store, or each
|
|
40550
|
+
* would write and read back its own nonce and both would win.
|
|
40551
|
+
*/
|
|
40552
|
+
ticketReplayStorage;
|
|
40397
40553
|
/**
|
|
40398
40554
|
* Per-session encrypted credential vault (Checkpoint 3b). Backs
|
|
40399
40555
|
* `this.credentials` in tools and persists `authenticate()` credentials.
|
|
@@ -40561,6 +40717,81 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40561
40717
|
* (`this.secret`), so resume URLs are framework-signed with the same trust
|
|
40562
40718
|
* root as the access tokens.
|
|
40563
40719
|
*/
|
|
40720
|
+
/**
|
|
40721
|
+
* The server HMAC signing key, as the string form the `signData`/`verifyData`
|
|
40722
|
+
* helpers take. These are the exact bytes the access tokens are signed with,
|
|
40723
|
+
* so framework-signed URLs share the access tokens' trust root.
|
|
40724
|
+
*/
|
|
40725
|
+
get signingSecret() {
|
|
40726
|
+
return new TextDecoder().decode(this.secret);
|
|
40727
|
+
}
|
|
40728
|
+
/**
|
|
40729
|
+
* Claim an incremental-authorization ticket, exactly once.
|
|
40730
|
+
*
|
|
40731
|
+
* A signature check proves a ticket was minted by this server for a verified
|
|
40732
|
+
* subject; it cannot prove the ticket has not already been used. The `auth_url`
|
|
40733
|
+
* carrying the ticket travels through agent transcripts and server logs, so a
|
|
40734
|
+
* leaked ticket must not stay usable for the rest of its TTL.
|
|
40735
|
+
*
|
|
40736
|
+
* The claim is race-free without a transaction: each attempt writes its OWN
|
|
40737
|
+
* nonce under `ifNotExists` and reads the key back. Only the attempt whose
|
|
40738
|
+
* nonce survives won, so concurrent replays all lose.
|
|
40739
|
+
*
|
|
40740
|
+
* @returns true when this caller claimed the ticket, false when it was already used.
|
|
40741
|
+
*/
|
|
40742
|
+
async claimIncrementalTicket(jti, ttlMs) {
|
|
40743
|
+
const key = `incremental-ticket:${jti}`;
|
|
40744
|
+
const nonce = randomUUID21();
|
|
40745
|
+
const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1e3));
|
|
40746
|
+
let storage = await this.getTicketReplayStorage();
|
|
40747
|
+
try {
|
|
40748
|
+
await storage.set(key, nonce, { ifNotExists: true, ttlSeconds });
|
|
40749
|
+
} catch (error) {
|
|
40750
|
+
if (!isUnsupportedOperation(error)) {
|
|
40751
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(error)}`);
|
|
40752
|
+
return false;
|
|
40753
|
+
}
|
|
40754
|
+
this.logger.warn(
|
|
40755
|
+
"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)."
|
|
40756
|
+
);
|
|
40757
|
+
storage = await this.useInMemoryTicketReplayStorage();
|
|
40758
|
+
try {
|
|
40759
|
+
await storage.set(key, nonce, { ifNotExists: true, ttlSeconds });
|
|
40760
|
+
} catch (fallbackError) {
|
|
40761
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(fallbackError)}`);
|
|
40762
|
+
return false;
|
|
40763
|
+
}
|
|
40764
|
+
}
|
|
40765
|
+
try {
|
|
40766
|
+
return await storage.get(key) === nonce;
|
|
40767
|
+
} catch (error) {
|
|
40768
|
+
this.logger.warn(`Incremental ticket replay guard unavailable: ${String(error)}`);
|
|
40769
|
+
return false;
|
|
40770
|
+
}
|
|
40771
|
+
}
|
|
40772
|
+
getTicketReplayStorage() {
|
|
40773
|
+
if (!this.ticketReplayStorage) {
|
|
40774
|
+
if (this.storageAdapter) {
|
|
40775
|
+
this.ticketReplayStorage = Promise.resolve(this.storageAdapter);
|
|
40776
|
+
} else {
|
|
40777
|
+
this.logger.debug(
|
|
40778
|
+
"Incremental ticket replay guard is in-memory (no persistent tokenStorage configured); single use holds within this instance only."
|
|
40779
|
+
);
|
|
40780
|
+
this.ticketReplayStorage = this.useInMemoryTicketReplayStorage();
|
|
40781
|
+
}
|
|
40782
|
+
}
|
|
40783
|
+
return this.ticketReplayStorage;
|
|
40784
|
+
}
|
|
40785
|
+
/** Replace the replay guard with a fresh in-memory adapter, memoized. */
|
|
40786
|
+
useInMemoryTicketReplayStorage() {
|
|
40787
|
+
const pending = (async () => {
|
|
40788
|
+
const memory = new MemoryStorageAdapter();
|
|
40789
|
+
await memory.connect();
|
|
40790
|
+
return memory;
|
|
40791
|
+
})();
|
|
40792
|
+
this.ticketReplayStorage = pending;
|
|
40793
|
+
return pending;
|
|
40794
|
+
}
|
|
40564
40795
|
async initializeCredentialVault() {
|
|
40565
40796
|
if (isPublicMode(this.options)) {
|
|
40566
40797
|
return;
|
|
@@ -40579,7 +40810,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40579
40810
|
pepper,
|
|
40580
40811
|
logger: this.logger.child("SessionCredentialVault")
|
|
40581
40812
|
});
|
|
40582
|
-
const signingSecret =
|
|
40813
|
+
const signingSecret = this.signingSecret;
|
|
40583
40814
|
const basePath = this.issuer;
|
|
40584
40815
|
await this.providers.addDynamicProviders(
|
|
40585
40816
|
createCredentialsProviders({
|
|
@@ -44884,6 +45115,7 @@ var init_execute_workflow_tool = __esm({
|
|
|
44884
45115
|
"libs/sdk/src/workflow/tools/execute-workflow.tool.ts"() {
|
|
44885
45116
|
"use strict";
|
|
44886
45117
|
init_common();
|
|
45118
|
+
init_errors();
|
|
44887
45119
|
ExecuteWorkflowTool = class extends ToolContext {
|
|
44888
45120
|
async execute(input) {
|
|
44889
45121
|
const scope = this.scope;
|
|
@@ -44895,12 +45127,13 @@ var init_execute_workflow_tool = __esm({
|
|
|
44895
45127
|
}
|
|
44896
45128
|
const workflow = workflowRegistry.findByName(input.name);
|
|
44897
45129
|
if (!workflow) {
|
|
44898
|
-
return this.fail(new
|
|
45130
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
44899
45131
|
}
|
|
44900
45132
|
const result = await executionManager.executeWorkflow(workflow, jobRegistry, {
|
|
44901
45133
|
background: input.background,
|
|
44902
45134
|
sessionId: this.authInfo.sessionId,
|
|
44903
45135
|
authInfo: this.authInfo,
|
|
45136
|
+
authoritiesContextBuilder: this.scope.authoritiesContextBuilder,
|
|
44904
45137
|
workflowInput: input.input
|
|
44905
45138
|
});
|
|
44906
45139
|
return result;
|
|
@@ -44925,6 +45158,55 @@ var init_execute_workflow_tool = __esm({
|
|
|
44925
45158
|
}
|
|
44926
45159
|
});
|
|
44927
45160
|
|
|
45161
|
+
// libs/sdk/src/common/utils/principal.utils.ts
|
|
45162
|
+
function toStringArray(value) {
|
|
45163
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
45164
|
+
if (typeof value === "string" && value.length > 0) return value.split(" ").filter(Boolean);
|
|
45165
|
+
return [];
|
|
45166
|
+
}
|
|
45167
|
+
function asRecord(value) {
|
|
45168
|
+
return value && typeof value === "object" ? value : void 0;
|
|
45169
|
+
}
|
|
45170
|
+
function firstPresent(sources, convert) {
|
|
45171
|
+
for (const source of sources) {
|
|
45172
|
+
if (source !== void 0 && source !== null) return convert(source);
|
|
45173
|
+
}
|
|
45174
|
+
return [];
|
|
45175
|
+
}
|
|
45176
|
+
function resolvePrincipal2(authInfo, contextBuilder) {
|
|
45177
|
+
const info = authInfo ?? {};
|
|
45178
|
+
const user = asRecord(info["user"]) ?? {};
|
|
45179
|
+
const extraAuthorization = asRecord(asRecord(info["extra"])?.["authorization"]);
|
|
45180
|
+
const authInfoClaims = asRecord(info["claims"]) ?? {};
|
|
45181
|
+
const authorizationClaims = asRecord(extraAuthorization?.["claims"]) ?? {};
|
|
45182
|
+
const claims = { ...authorizationClaims, ...authInfoClaims, ...user };
|
|
45183
|
+
const scopes = toStringArray(info["scopes"]).length ? toStringArray(info["scopes"]) : firstPresent([extraAuthorization?.["scopes"], claims["scope"], claims["scopes"]], toStringArray);
|
|
45184
|
+
if (contextBuilder) {
|
|
45185
|
+
const ctx = contextBuilder.build(info);
|
|
45186
|
+
return {
|
|
45187
|
+
sub: ctx.user.sub,
|
|
45188
|
+
roles: ctx.user.roles,
|
|
45189
|
+
permissions: ctx.user.permissions,
|
|
45190
|
+
scopes,
|
|
45191
|
+
claims: ctx.user.claims
|
|
45192
|
+
};
|
|
45193
|
+
}
|
|
45194
|
+
const roles = firstPresent([user["roles"], claims["roles"], extraAuthorization?.["scopes"]], toStringArray);
|
|
45195
|
+
const permissions = firstPresent([user["permissions"], claims["permissions"]], toStringArray);
|
|
45196
|
+
return {
|
|
45197
|
+
sub: typeof user["sub"] === "string" ? user["sub"] : "",
|
|
45198
|
+
roles,
|
|
45199
|
+
permissions,
|
|
45200
|
+
scopes,
|
|
45201
|
+
claims
|
|
45202
|
+
};
|
|
45203
|
+
}
|
|
45204
|
+
var init_principal_utils = __esm({
|
|
45205
|
+
"libs/sdk/src/common/utils/principal.utils.ts"() {
|
|
45206
|
+
"use strict";
|
|
45207
|
+
}
|
|
45208
|
+
});
|
|
45209
|
+
|
|
44928
45210
|
// libs/sdk/src/workflow/tools/get-workflow-status.tool.ts
|
|
44929
45211
|
import { z as z82 } from "@frontmcp/lazy-zod";
|
|
44930
45212
|
var GetWorkflowStatusTool;
|
|
@@ -44932,6 +45214,7 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44932
45214
|
"libs/sdk/src/workflow/tools/get-workflow-status.tool.ts"() {
|
|
44933
45215
|
"use strict";
|
|
44934
45216
|
init_common();
|
|
45217
|
+
init_principal_utils();
|
|
44935
45218
|
GetWorkflowStatusTool = class extends ToolContext {
|
|
44936
45219
|
async execute(input) {
|
|
44937
45220
|
const scope = this.scope;
|
|
@@ -44940,7 +45223,7 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44940
45223
|
return this.fail(new Error("Jobs system is not enabled"));
|
|
44941
45224
|
}
|
|
44942
45225
|
const record = await executionManager.getStatus(input.runId);
|
|
44943
|
-
if (!record) {
|
|
45226
|
+
if (!record || !this.ownsRun(record)) {
|
|
44944
45227
|
return this.fail(new Error(`Run "${input.runId}" not found`));
|
|
44945
45228
|
}
|
|
44946
45229
|
const workflowRecord = record;
|
|
@@ -44953,6 +45236,23 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44953
45236
|
completedAt: record.completedAt
|
|
44954
45237
|
};
|
|
44955
45238
|
}
|
|
45239
|
+
/**
|
|
45240
|
+
* Match on the owning subject, falling back to the session for runs started
|
|
45241
|
+
* before an owner was recorded (and for anonymous/public servers, where every
|
|
45242
|
+
* caller has an empty subject and the session is the only identity there is).
|
|
45243
|
+
*
|
|
45244
|
+
* A record carrying NEITHER identity is refused. Such a run cannot be
|
|
45245
|
+
* attributed to anyone, and its inputs and results are exactly what this check
|
|
45246
|
+
* exists to protect — anyone holding the id would otherwise be able to read
|
|
45247
|
+
* it. Internal callers that must read unattributed runs should go through the
|
|
45248
|
+
* execution manager directly rather than this tool.
|
|
45249
|
+
*/
|
|
45250
|
+
ownsRun(record) {
|
|
45251
|
+
const callerSub = resolvePrincipal2(this.authInfo, this.scope.authoritiesContextBuilder).sub;
|
|
45252
|
+
if (record.ownerSub) return record.ownerSub === callerSub;
|
|
45253
|
+
if (record.sessionId) return record.sessionId === this.authInfo.sessionId;
|
|
45254
|
+
return false;
|
|
45255
|
+
}
|
|
44956
45256
|
};
|
|
44957
45257
|
GetWorkflowStatusTool = __decorateClass([
|
|
44958
45258
|
FrontMcpTool({
|
|
@@ -44980,6 +45280,53 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44980
45280
|
}
|
|
44981
45281
|
});
|
|
44982
45282
|
|
|
45283
|
+
// libs/sdk/src/job/job-permission.guard.ts
|
|
45284
|
+
var JobPermissionGuard;
|
|
45285
|
+
var init_job_permission_guard = __esm({
|
|
45286
|
+
"libs/sdk/src/job/job-permission.guard.ts"() {
|
|
45287
|
+
"use strict";
|
|
45288
|
+
init_principal_utils();
|
|
45289
|
+
JobPermissionGuard = class _JobPermissionGuard {
|
|
45290
|
+
/**
|
|
45291
|
+
* Check whether the caller may perform `action`.
|
|
45292
|
+
*
|
|
45293
|
+
* @param permissions - the entry's declared rules (undefined = unrestricted).
|
|
45294
|
+
* @param action - the action being attempted.
|
|
45295
|
+
* @param authInfo - the request's AuthInfo.
|
|
45296
|
+
* @param contextBuilder - the scope's authorities context builder, when configured.
|
|
45297
|
+
*/
|
|
45298
|
+
static async check(permissions, action, authInfo, contextBuilder) {
|
|
45299
|
+
if (!permissions || permissions.length === 0) {
|
|
45300
|
+
return true;
|
|
45301
|
+
}
|
|
45302
|
+
const relevant = permissions.filter((p) => p.action === action);
|
|
45303
|
+
if (relevant.length === 0) {
|
|
45304
|
+
return true;
|
|
45305
|
+
}
|
|
45306
|
+
const principal = resolvePrincipal2(authInfo, contextBuilder);
|
|
45307
|
+
for (const perm of relevant) {
|
|
45308
|
+
const passed = await _JobPermissionGuard.checkSingle(perm, principal, authInfo);
|
|
45309
|
+
if (!passed) return false;
|
|
45310
|
+
}
|
|
45311
|
+
return true;
|
|
45312
|
+
}
|
|
45313
|
+
static async checkSingle(perm, principal, authInfo) {
|
|
45314
|
+
if (perm.roles && perm.roles.length > 0) {
|
|
45315
|
+
if (!perm.roles.some((role) => principal.roles.includes(role))) return false;
|
|
45316
|
+
}
|
|
45317
|
+
if (perm.scopes && perm.scopes.length > 0) {
|
|
45318
|
+
if (!perm.scopes.some((scope) => principal.scopes.includes(scope))) return false;
|
|
45319
|
+
}
|
|
45320
|
+
if (perm.custom) {
|
|
45321
|
+
const result = await perm.custom(authInfo ?? {});
|
|
45322
|
+
if (!result) return false;
|
|
45323
|
+
}
|
|
45324
|
+
return true;
|
|
45325
|
+
}
|
|
45326
|
+
};
|
|
45327
|
+
}
|
|
45328
|
+
});
|
|
45329
|
+
|
|
44983
45330
|
// libs/sdk/src/workflow/tools/list-workflows.tool.ts
|
|
44984
45331
|
import { z as z83 } from "@frontmcp/lazy-zod";
|
|
44985
45332
|
var ListWorkflowsTool;
|
|
@@ -44987,6 +45334,7 @@ var init_list_workflows_tool = __esm({
|
|
|
44987
45334
|
"libs/sdk/src/workflow/tools/list-workflows.tool.ts"() {
|
|
44988
45335
|
"use strict";
|
|
44989
45336
|
init_common();
|
|
45337
|
+
init_job_permission_guard();
|
|
44990
45338
|
ListWorkflowsTool = class extends ToolContext {
|
|
44991
45339
|
async execute(input) {
|
|
44992
45340
|
const scope = this.scope;
|
|
@@ -44998,7 +45346,11 @@ var init_list_workflows_tool = __esm({
|
|
|
44998
45346
|
tags: input.tags,
|
|
44999
45347
|
labels: input.labels
|
|
45000
45348
|
});
|
|
45001
|
-
const
|
|
45349
|
+
const visible = [];
|
|
45350
|
+
for (const workflow of workflows) {
|
|
45351
|
+
if (await this.maySee(workflow)) visible.push(workflow);
|
|
45352
|
+
}
|
|
45353
|
+
const mapped = visible.map((w) => ({
|
|
45002
45354
|
name: w.name,
|
|
45003
45355
|
description: w.metadata.description,
|
|
45004
45356
|
trigger: w.getTrigger(),
|
|
@@ -45010,6 +45362,12 @@ var init_list_workflows_tool = __esm({
|
|
|
45010
45362
|
count: mapped.length
|
|
45011
45363
|
};
|
|
45012
45364
|
}
|
|
45365
|
+
async maySee(workflow) {
|
|
45366
|
+
const permissions = workflow.metadata.permissions;
|
|
45367
|
+
const builder = this.scope.authoritiesContextBuilder;
|
|
45368
|
+
if (!await JobPermissionGuard.check(permissions, "list", this.authInfo, builder)) return false;
|
|
45369
|
+
return JobPermissionGuard.check(permissions, "execute", this.authInfo, builder);
|
|
45370
|
+
}
|
|
45013
45371
|
};
|
|
45014
45372
|
ListWorkflowsTool = __decorateClass([
|
|
45015
45373
|
FrontMcpTool({
|
|
@@ -45045,6 +45403,7 @@ var init_register_workflow_tool = __esm({
|
|
|
45045
45403
|
"libs/sdk/src/workflow/tools/register-workflow.tool.ts"() {
|
|
45046
45404
|
"use strict";
|
|
45047
45405
|
init_common();
|
|
45406
|
+
init_errors();
|
|
45048
45407
|
RegisterWorkflowTool = class extends ToolContext {
|
|
45049
45408
|
async execute(input) {
|
|
45050
45409
|
const scope = this.scope;
|
|
@@ -45052,6 +45411,9 @@ var init_register_workflow_tool = __esm({
|
|
|
45052
45411
|
if (!workflowRegistry) {
|
|
45053
45412
|
return this.fail(new Error("Workflows system is not enabled"));
|
|
45054
45413
|
}
|
|
45414
|
+
if (!this.dynamicRegistrationEnabled()) {
|
|
45415
|
+
return this.fail(new DynamicJobRegistrationDisabledError("workflow"));
|
|
45416
|
+
}
|
|
45055
45417
|
const existing = workflowRegistry.findByName(input.name);
|
|
45056
45418
|
if (existing) {
|
|
45057
45419
|
return this.fail(new Error(`Workflow "${input.name}" already exists`));
|
|
@@ -45073,6 +45435,10 @@ var init_register_workflow_tool = __esm({
|
|
|
45073
45435
|
});
|
|
45074
45436
|
return { success: true, workflowId };
|
|
45075
45437
|
}
|
|
45438
|
+
dynamicRegistrationEnabled() {
|
|
45439
|
+
const jobs = this.scope.metadata?.jobs;
|
|
45440
|
+
return jobs?.allowDynamicRegistration === true;
|
|
45441
|
+
}
|
|
45076
45442
|
};
|
|
45077
45443
|
RegisterWorkflowTool = __decorateClass([
|
|
45078
45444
|
FrontMcpTool({
|
|
@@ -45111,6 +45477,8 @@ var init_remove_workflow_tool = __esm({
|
|
|
45111
45477
|
"libs/sdk/src/workflow/tools/remove-workflow.tool.ts"() {
|
|
45112
45478
|
"use strict";
|
|
45113
45479
|
init_common();
|
|
45480
|
+
init_errors();
|
|
45481
|
+
init_job_permission_guard();
|
|
45114
45482
|
RemoveWorkflowTool = class extends ToolContext {
|
|
45115
45483
|
async execute(input) {
|
|
45116
45484
|
const scope = this.scope;
|
|
@@ -45120,7 +45488,16 @@ var init_remove_workflow_tool = __esm({
|
|
|
45120
45488
|
}
|
|
45121
45489
|
const workflow = workflowRegistry.findByName(input.name);
|
|
45122
45490
|
if (!workflow) {
|
|
45123
|
-
return this.fail(new
|
|
45491
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
45492
|
+
}
|
|
45493
|
+
const allowed = await JobPermissionGuard.check(
|
|
45494
|
+
workflow.metadata.permissions,
|
|
45495
|
+
"delete",
|
|
45496
|
+
this.authInfo,
|
|
45497
|
+
this.scope.authoritiesContextBuilder
|
|
45498
|
+
);
|
|
45499
|
+
if (!allowed) {
|
|
45500
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
45124
45501
|
}
|
|
45125
45502
|
if (!workflow.isDynamic()) {
|
|
45126
45503
|
return this.fail(new Error(`Workflow "${input.name}" is not dynamic and cannot be removed`));
|
|
@@ -45729,7 +46106,10 @@ var JobExecutionManager;
|
|
|
45729
46106
|
var init_job_execution_manager = __esm({
|
|
45730
46107
|
"libs/sdk/src/job/execution/job-execution.manager.ts"() {
|
|
45731
46108
|
"use strict";
|
|
46109
|
+
init_principal_utils();
|
|
46110
|
+
init_errors();
|
|
45732
46111
|
init_workflow_engine();
|
|
46112
|
+
init_job_permission_guard();
|
|
45733
46113
|
JobExecutionManager = class {
|
|
45734
46114
|
stateStore;
|
|
45735
46115
|
logger;
|
|
@@ -45743,6 +46123,7 @@ var init_job_execution_manager = __esm({
|
|
|
45743
46123
|
* Execute a job inline (synchronous) or in background.
|
|
45744
46124
|
*/
|
|
45745
46125
|
async executeJob(job, input, opts = {}) {
|
|
46126
|
+
await this.assertMayExecute(job.metadata.permissions, job.name, opts);
|
|
45746
46127
|
const runId = randomUUID23();
|
|
45747
46128
|
const retryConfig = job.metadata.retry ?? {};
|
|
45748
46129
|
const maxAttempts = retryConfig.maxAttempts ?? 1;
|
|
@@ -45751,6 +46132,7 @@ var init_job_execution_manager = __esm({
|
|
|
45751
46132
|
jobId: job.metadata.id ?? job.name,
|
|
45752
46133
|
jobName: job.name,
|
|
45753
46134
|
sessionId: opts.sessionId,
|
|
46135
|
+
ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
|
|
45754
46136
|
state: "pending",
|
|
45755
46137
|
input,
|
|
45756
46138
|
startedAt: Date.now(),
|
|
@@ -45781,6 +46163,7 @@ var init_job_execution_manager = __esm({
|
|
|
45781
46163
|
* Execute a workflow inline or in background.
|
|
45782
46164
|
*/
|
|
45783
46165
|
async executeWorkflow(workflow, jobRegistry, opts = {}) {
|
|
46166
|
+
await this.assertMayExecute(workflow.metadata.permissions, workflow.name, opts);
|
|
45784
46167
|
const runId = randomUUID23();
|
|
45785
46168
|
const runRecord = {
|
|
45786
46169
|
runId,
|
|
@@ -45788,6 +46171,7 @@ var init_job_execution_manager = __esm({
|
|
|
45788
46171
|
jobName: workflow.name,
|
|
45789
46172
|
workflowName: workflow.name,
|
|
45790
46173
|
sessionId: opts.sessionId,
|
|
46174
|
+
ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
|
|
45791
46175
|
state: "pending",
|
|
45792
46176
|
input: opts.workflowInput,
|
|
45793
46177
|
startedAt: Date.now(),
|
|
@@ -45815,6 +46199,25 @@ var init_job_execution_manager = __esm({
|
|
|
45815
46199
|
}
|
|
45816
46200
|
return this.executeWorkflowInline(workflow, jobRegistry, runId, opts);
|
|
45817
46201
|
}
|
|
46202
|
+
/**
|
|
46203
|
+
* Throw unless the caller may execute the entry.
|
|
46204
|
+
*
|
|
46205
|
+
* Uses the same error for "not authorized" as the tools use for "not found",
|
|
46206
|
+
* so a caller cannot enumerate which restricted jobs exist by comparing
|
|
46207
|
+
* responses.
|
|
46208
|
+
*/
|
|
46209
|
+
async assertMayExecute(permissions, name42, opts) {
|
|
46210
|
+
const allowed = await JobPermissionGuard.check(
|
|
46211
|
+
permissions,
|
|
46212
|
+
"execute",
|
|
46213
|
+
opts.authInfo,
|
|
46214
|
+
opts.authoritiesContextBuilder
|
|
46215
|
+
);
|
|
46216
|
+
if (!allowed) {
|
|
46217
|
+
this.logger.warn(`Execution of "${name42}" denied: caller does not satisfy its execute permissions`);
|
|
46218
|
+
throw new JobNotAuthorizedError(name42);
|
|
46219
|
+
}
|
|
46220
|
+
}
|
|
45818
46221
|
/**
|
|
45819
46222
|
* Get execution status.
|
|
45820
46223
|
*/
|
|
@@ -46839,6 +47242,7 @@ var init_execute_job_tool = __esm({
|
|
|
46839
47242
|
"libs/sdk/src/job/tools/execute-job.tool.ts"() {
|
|
46840
47243
|
"use strict";
|
|
46841
47244
|
init_common();
|
|
47245
|
+
init_errors();
|
|
46842
47246
|
ExecuteJobTool = class extends ToolContext {
|
|
46843
47247
|
async execute(input) {
|
|
46844
47248
|
const scope = this.scope;
|
|
@@ -46849,12 +47253,13 @@ var init_execute_job_tool = __esm({
|
|
|
46849
47253
|
}
|
|
46850
47254
|
const job = jobRegistry.findByName(input.name);
|
|
46851
47255
|
if (!job) {
|
|
46852
|
-
return this.fail(new
|
|
47256
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
46853
47257
|
}
|
|
46854
47258
|
const result = await executionManager.executeJob(job, input.input ?? {}, {
|
|
46855
47259
|
background: input.background,
|
|
46856
47260
|
sessionId: this.authInfo.sessionId,
|
|
46857
|
-
authInfo: this.authInfo
|
|
47261
|
+
authInfo: this.authInfo,
|
|
47262
|
+
authoritiesContextBuilder: this.scope.authoritiesContextBuilder
|
|
46858
47263
|
});
|
|
46859
47264
|
return result;
|
|
46860
47265
|
}
|
|
@@ -46886,6 +47291,7 @@ var init_get_job_status_tool = __esm({
|
|
|
46886
47291
|
"libs/sdk/src/job/tools/get-job-status.tool.ts"() {
|
|
46887
47292
|
"use strict";
|
|
46888
47293
|
init_common();
|
|
47294
|
+
init_principal_utils();
|
|
46889
47295
|
init_errors();
|
|
46890
47296
|
GetJobStatusTool = class extends ToolContext {
|
|
46891
47297
|
async execute(input) {
|
|
@@ -46895,7 +47301,7 @@ var init_get_job_status_tool = __esm({
|
|
|
46895
47301
|
return this.fail(new GenericServerError("Jobs system is not enabled"));
|
|
46896
47302
|
}
|
|
46897
47303
|
const record = await executionManager.getStatus(input.runId);
|
|
46898
|
-
if (!record) {
|
|
47304
|
+
if (!record || !this.ownsRun(record)) {
|
|
46899
47305
|
return this.fail(new InvalidInputError(`Run "${input.runId}" not found`));
|
|
46900
47306
|
}
|
|
46901
47307
|
return {
|
|
@@ -46910,6 +47316,23 @@ var init_get_job_status_tool = __esm({
|
|
|
46910
47316
|
logs: record.logs
|
|
46911
47317
|
};
|
|
46912
47318
|
}
|
|
47319
|
+
/**
|
|
47320
|
+
* Match on the owning subject, falling back to the session for runs started
|
|
47321
|
+
* before an owner was recorded (and for anonymous/public servers, where every
|
|
47322
|
+
* caller has an empty subject and the session is the only identity there is).
|
|
47323
|
+
*
|
|
47324
|
+
* A record carrying NEITHER identity is refused. Such a run cannot be
|
|
47325
|
+
* attributed to anyone, and its inputs and results are exactly what this check
|
|
47326
|
+
* exists to protect — anyone holding the id would otherwise be able to read
|
|
47327
|
+
* it. Internal callers that must read unattributed runs should go through the
|
|
47328
|
+
* execution manager directly rather than this tool.
|
|
47329
|
+
*/
|
|
47330
|
+
ownsRun(record) {
|
|
47331
|
+
const callerSub = resolvePrincipal2(this.authInfo, this.scope.authoritiesContextBuilder).sub;
|
|
47332
|
+
if (record.ownerSub) return record.ownerSub === callerSub;
|
|
47333
|
+
if (record.sessionId) return record.sessionId === this.authInfo.sessionId;
|
|
47334
|
+
return false;
|
|
47335
|
+
}
|
|
46913
47336
|
};
|
|
46914
47337
|
GetJobStatusTool = __decorateClass([
|
|
46915
47338
|
FrontMcpTool({
|
|
@@ -46944,6 +47367,7 @@ var init_list_jobs_tool = __esm({
|
|
|
46944
47367
|
"libs/sdk/src/job/tools/list-jobs.tool.ts"() {
|
|
46945
47368
|
"use strict";
|
|
46946
47369
|
init_common();
|
|
47370
|
+
init_job_permission_guard();
|
|
46947
47371
|
ListJobsTool = class extends ToolContext {
|
|
46948
47372
|
async execute(input) {
|
|
46949
47373
|
const jobRegistry = this.scope.jobs;
|
|
@@ -46954,7 +47378,11 @@ var init_list_jobs_tool = __esm({
|
|
|
46954
47378
|
tags: input.tags,
|
|
46955
47379
|
labels: input.labels
|
|
46956
47380
|
});
|
|
46957
|
-
const
|
|
47381
|
+
const visible = [];
|
|
47382
|
+
for (const job of jobs) {
|
|
47383
|
+
if (await this.maySee(job)) visible.push(job);
|
|
47384
|
+
}
|
|
47385
|
+
const mapped = visible.map((j) => ({
|
|
46958
47386
|
name: j.name,
|
|
46959
47387
|
description: j.metadata.description,
|
|
46960
47388
|
tags: j.getTags(),
|
|
@@ -46966,6 +47394,17 @@ var init_list_jobs_tool = __esm({
|
|
|
46966
47394
|
count: mapped.length
|
|
46967
47395
|
};
|
|
46968
47396
|
}
|
|
47397
|
+
/**
|
|
47398
|
+
* A job is listable when the caller passes its `list` rules and is not
|
|
47399
|
+
* blocked from executing it. Checking `execute` too keeps the listing honest:
|
|
47400
|
+
* showing a job the caller can never run is the disclosure this closes.
|
|
47401
|
+
*/
|
|
47402
|
+
async maySee(job) {
|
|
47403
|
+
const permissions = job.metadata.permissions;
|
|
47404
|
+
const builder = this.scope.authoritiesContextBuilder;
|
|
47405
|
+
if (!await JobPermissionGuard.check(permissions, "list", this.authInfo, builder)) return false;
|
|
47406
|
+
return JobPermissionGuard.check(permissions, "execute", this.authInfo, builder);
|
|
47407
|
+
}
|
|
46969
47408
|
};
|
|
46970
47409
|
ListJobsTool = __decorateClass([
|
|
46971
47410
|
FrontMcpTool({
|
|
@@ -47001,6 +47440,7 @@ var init_register_job_tool = __esm({
|
|
|
47001
47440
|
"libs/sdk/src/job/tools/register-job.tool.ts"() {
|
|
47002
47441
|
"use strict";
|
|
47003
47442
|
init_common();
|
|
47443
|
+
init_errors();
|
|
47004
47444
|
RegisterJobTool = class extends ToolContext {
|
|
47005
47445
|
async execute(input) {
|
|
47006
47446
|
const scope = this.scope;
|
|
@@ -47008,6 +47448,9 @@ var init_register_job_tool = __esm({
|
|
|
47008
47448
|
if (!jobRegistry) {
|
|
47009
47449
|
return this.fail(new Error("Jobs system is not enabled"));
|
|
47010
47450
|
}
|
|
47451
|
+
if (!this.dynamicRegistrationEnabled()) {
|
|
47452
|
+
return this.fail(new DynamicJobRegistrationDisabledError("job"));
|
|
47453
|
+
}
|
|
47011
47454
|
const existing = jobRegistry.findByName(input.name);
|
|
47012
47455
|
if (existing) {
|
|
47013
47456
|
return this.fail(new Error(`Job "${input.name}" already exists`));
|
|
@@ -47030,6 +47473,10 @@ var init_register_job_tool = __esm({
|
|
|
47030
47473
|
});
|
|
47031
47474
|
return { success: true, jobId };
|
|
47032
47475
|
}
|
|
47476
|
+
dynamicRegistrationEnabled() {
|
|
47477
|
+
const jobs = this.scope.metadata?.jobs;
|
|
47478
|
+
return jobs?.allowDynamicRegistration === true;
|
|
47479
|
+
}
|
|
47033
47480
|
};
|
|
47034
47481
|
RegisterJobTool = __decorateClass([
|
|
47035
47482
|
FrontMcpTool({
|
|
@@ -47060,6 +47507,8 @@ var init_remove_job_tool = __esm({
|
|
|
47060
47507
|
"libs/sdk/src/job/tools/remove-job.tool.ts"() {
|
|
47061
47508
|
"use strict";
|
|
47062
47509
|
init_common();
|
|
47510
|
+
init_errors();
|
|
47511
|
+
init_job_permission_guard();
|
|
47063
47512
|
RemoveJobTool = class extends ToolContext {
|
|
47064
47513
|
async execute(input) {
|
|
47065
47514
|
const scope = this.scope;
|
|
@@ -47069,7 +47518,16 @@ var init_remove_job_tool = __esm({
|
|
|
47069
47518
|
}
|
|
47070
47519
|
const job = jobRegistry.findByName(input.name);
|
|
47071
47520
|
if (!job) {
|
|
47072
|
-
return this.fail(new
|
|
47521
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
47522
|
+
}
|
|
47523
|
+
const allowed = await JobPermissionGuard.check(
|
|
47524
|
+
job.metadata.permissions,
|
|
47525
|
+
"delete",
|
|
47526
|
+
this.authInfo,
|
|
47527
|
+
this.scope.authoritiesContextBuilder
|
|
47528
|
+
);
|
|
47529
|
+
if (!allowed) {
|
|
47530
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
47073
47531
|
}
|
|
47074
47532
|
if (!job.isDynamic()) {
|
|
47075
47533
|
return this.fail(new Error(`Job "${input.name}" is not a dynamic job and cannot be removed`));
|
|
@@ -47130,14 +47588,18 @@ async function registerJobCapabilities(args) {
|
|
|
47130
47588
|
ListJobsTool,
|
|
47131
47589
|
ExecuteJobTool,
|
|
47132
47590
|
GetJobStatusTool,
|
|
47133
|
-
RegisterJobTool,
|
|
47134
47591
|
RemoveJobTool,
|
|
47135
47592
|
ListWorkflowsTool,
|
|
47136
47593
|
ExecuteWorkflowTool,
|
|
47137
47594
|
GetWorkflowStatusTool,
|
|
47138
|
-
RegisterWorkflowTool,
|
|
47139
47595
|
RemoveWorkflowTool
|
|
47140
47596
|
];
|
|
47597
|
+
if (jobsConfig.allowDynamicRegistration === true) {
|
|
47598
|
+
managementTools.push(RegisterJobTool, RegisterWorkflowTool);
|
|
47599
|
+
logger.warn(
|
|
47600
|
+
"Dynamic job registration is ENABLED: register_job/register_workflow accept a raw script and register it as an executable job."
|
|
47601
|
+
);
|
|
47602
|
+
}
|
|
47141
47603
|
logger.info(
|
|
47142
47604
|
`Jobs initialized: ${jobRegistry.getJobs().length} jobs, ${workflowRegistry.getWorkflows().length} workflows`
|
|
47143
47605
|
);
|
|
@@ -51968,6 +52430,7 @@ var init_utils2 = __esm({
|
|
|
51968
52430
|
init_caller_dir_utils();
|
|
51969
52431
|
init_decide_request_intent_utils();
|
|
51970
52432
|
init_path_utils();
|
|
52433
|
+
init_principal_utils();
|
|
51971
52434
|
init_global_config_utils();
|
|
51972
52435
|
init_primitive_filter();
|
|
51973
52436
|
init_validate_remote_url();
|
|
@@ -52913,9 +53376,24 @@ var init_redis_event_store = __esm({
|
|
|
52913
53376
|
await client.expire(streamKey, this.ttlSeconds);
|
|
52914
53377
|
return `${streamId}:${redisId}`;
|
|
52915
53378
|
}
|
|
53379
|
+
/**
|
|
53380
|
+
* Resolve the stream an event belongs to.
|
|
53381
|
+
*
|
|
53382
|
+
* Optional in the upstream `EventStore` contract, and the transport's
|
|
53383
|
+
* 409-conflict guard (refusing a second live stream for the same id) is dead
|
|
53384
|
+
* code while no store provides it (GHSA-84j6-jc92-77jm).
|
|
53385
|
+
*/
|
|
53386
|
+
async getStreamIdForEventId(eventId) {
|
|
53387
|
+
const parsed = this.parseEventId(eventId);
|
|
53388
|
+
return parsed ? parsed[0] : void 0;
|
|
53389
|
+
}
|
|
52916
53390
|
async replayEventsAfter(lastEventId, { send }) {
|
|
53391
|
+
const parsed = this.parseEventId(lastEventId);
|
|
53392
|
+
if (!parsed) {
|
|
53393
|
+
return "default-stream";
|
|
53394
|
+
}
|
|
52917
53395
|
const client = await this.getClient();
|
|
52918
|
-
const [streamId, redisId] =
|
|
53396
|
+
const [streamId, redisId] = parsed;
|
|
52919
53397
|
const streamKey = `${this.keyPrefix}${streamId}`;
|
|
52920
53398
|
const entries = await client.xrange(streamKey, `(${redisId}`, "+");
|
|
52921
53399
|
for (const [id, fields] of entries) {
|
|
@@ -52934,16 +53412,32 @@ var init_redis_event_store = __esm({
|
|
|
52934
53412
|
* Event IDs have format: `{streamId}:{redisStreamEntryId}`
|
|
52935
53413
|
* Redis stream entry IDs have format: `{timestamp}-{sequence}`
|
|
52936
53414
|
*
|
|
53415
|
+
* The entry-id half is validated because it is interpolated into the `xrange`
|
|
53416
|
+
* range argument (`(${redisId}`), where a malformed value is a protocol error
|
|
53417
|
+
* rather than data.
|
|
53418
|
+
*
|
|
53419
|
+
* The stream-id half is NOT character-filtered. `xadd`/`xrange` take the key
|
|
53420
|
+
* as an exact argument — Redis applies no glob matching there — and ioredis
|
|
53421
|
+
* length-prefixes it, so no character is special. Filtering here would also
|
|
53422
|
+
* make `storeEvent`, which accepts any `StreamId`, able to mint ids this
|
|
53423
|
+
* function then refuses, silently breaking replay for that stream. Ownership
|
|
53424
|
+
* is enforced by the session-scoped facade (GHSA-84j6-jc92-77jm), not by
|
|
53425
|
+
* guessing at the id's characters.
|
|
53426
|
+
*
|
|
53427
|
+
* @returns the pair, or `undefined` when the id is malformed.
|
|
53428
|
+
*
|
|
52937
53429
|
* @example
|
|
52938
53430
|
* 'my-stream:1234567890123-0' -> ['my-stream', '1234567890123-0']
|
|
52939
53431
|
*/
|
|
52940
53432
|
parseEventId(eventId) {
|
|
52941
|
-
const
|
|
52942
|
-
|
|
52943
|
-
|
|
52944
|
-
|
|
52945
|
-
|
|
52946
|
-
|
|
53433
|
+
const value = String(eventId);
|
|
53434
|
+
const lastDash = value.lastIndexOf("-");
|
|
53435
|
+
const separator = lastDash === -1 ? value.lastIndexOf(":") : value.lastIndexOf(":", lastDash);
|
|
53436
|
+
if (separator <= 0 || separator === value.length - 1) return void 0;
|
|
53437
|
+
const streamId = value.slice(0, separator);
|
|
53438
|
+
const redisId = value.slice(separator + 1);
|
|
53439
|
+
if (!/^\d+(-\d+)?$/.test(redisId)) return void 0;
|
|
53440
|
+
return [streamId, redisId];
|
|
52947
53441
|
}
|
|
52948
53442
|
/**
|
|
52949
53443
|
* Close the Redis client connection.
|
|
@@ -52989,6 +53483,16 @@ var init_memory_event_store = __esm({
|
|
|
52989
53483
|
}
|
|
52990
53484
|
return id;
|
|
52991
53485
|
}
|
|
53486
|
+
/**
|
|
53487
|
+
* Resolve the stream an event belongs to.
|
|
53488
|
+
*
|
|
53489
|
+
* Optional in the upstream `EventStore` contract, and the transport's
|
|
53490
|
+
* 409-conflict guard (refusing a second live stream for the same id) is dead
|
|
53491
|
+
* code while no store provides it (GHSA-84j6-jc92-77jm).
|
|
53492
|
+
*/
|
|
53493
|
+
async getStreamIdForEventId(eventId) {
|
|
53494
|
+
return this.index.get(eventId)?.streamId;
|
|
53495
|
+
}
|
|
52992
53496
|
async replayEventsAfter(lastEventId, { send }) {
|
|
52993
53497
|
const meta = this.index.get(lastEventId);
|
|
52994
53498
|
if (!meta) {
|
|
@@ -53140,6 +53644,50 @@ var init_event_store_factory = __esm({
|
|
|
53140
53644
|
}
|
|
53141
53645
|
});
|
|
53142
53646
|
|
|
53647
|
+
// libs/sdk/src/transport/event-stores/session-scoped.event-store.ts
|
|
53648
|
+
function createSessionScopedEventStore(store, sessionId) {
|
|
53649
|
+
const prefix = `${sessionId}${SCOPE_SEPARATOR}`;
|
|
53650
|
+
const scope = (streamId) => `${prefix}${streamId}`;
|
|
53651
|
+
const unscope = (streamId) => {
|
|
53652
|
+
const value = String(streamId);
|
|
53653
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
53654
|
+
};
|
|
53655
|
+
const resolveOwnedStream = async (eventId) => {
|
|
53656
|
+
const lookup = store.getStreamIdForEventId;
|
|
53657
|
+
if (lookup) {
|
|
53658
|
+
const resolved = await lookup.call(store, eventId);
|
|
53659
|
+
return resolved !== void 0 && String(resolved).startsWith(prefix) ? resolved : void 0;
|
|
53660
|
+
}
|
|
53661
|
+
if (!String(eventId).startsWith(prefix)) return void 0;
|
|
53662
|
+
const value = String(eventId);
|
|
53663
|
+
const lastColon = value.lastIndexOf(":");
|
|
53664
|
+
return lastColon === -1 ? void 0 : value.slice(0, lastColon);
|
|
53665
|
+
};
|
|
53666
|
+
return {
|
|
53667
|
+
async storeEvent(streamId, message) {
|
|
53668
|
+
return store.storeEvent(scope(streamId), message);
|
|
53669
|
+
},
|
|
53670
|
+
async getStreamIdForEventId(eventId) {
|
|
53671
|
+
const owned = await resolveOwnedStream(eventId);
|
|
53672
|
+
return owned === void 0 ? void 0 : unscope(owned);
|
|
53673
|
+
},
|
|
53674
|
+
async replayEventsAfter(lastEventId, options) {
|
|
53675
|
+
const owned = await resolveOwnedStream(lastEventId);
|
|
53676
|
+
if (owned === void 0) {
|
|
53677
|
+
return "default-stream";
|
|
53678
|
+
}
|
|
53679
|
+
return unscope(await store.replayEventsAfter(lastEventId, options));
|
|
53680
|
+
}
|
|
53681
|
+
};
|
|
53682
|
+
}
|
|
53683
|
+
var SCOPE_SEPARATOR;
|
|
53684
|
+
var init_session_scoped_event_store = __esm({
|
|
53685
|
+
"libs/sdk/src/transport/event-stores/session-scoped.event-store.ts"() {
|
|
53686
|
+
"use strict";
|
|
53687
|
+
SCOPE_SEPARATOR = " ";
|
|
53688
|
+
}
|
|
53689
|
+
});
|
|
53690
|
+
|
|
53143
53691
|
// libs/sdk/src/transport/event-stores/index.ts
|
|
53144
53692
|
var init_event_stores = __esm({
|
|
53145
53693
|
"libs/sdk/src/transport/event-stores/index.ts"() {
|
|
@@ -54809,6 +55357,61 @@ var init_transport_error = __esm({
|
|
|
54809
55357
|
}
|
|
54810
55358
|
});
|
|
54811
55359
|
|
|
55360
|
+
// libs/sdk/src/server/security/host-validation.ts
|
|
55361
|
+
function normalizeHost(value) {
|
|
55362
|
+
const host = value.trim().toLowerCase();
|
|
55363
|
+
if (host.length === 0) return host;
|
|
55364
|
+
const portSeparator = host.startsWith("[") ? host.indexOf(":", host.indexOf("]")) : host.lastIndexOf(":");
|
|
55365
|
+
if (portSeparator === -1) return host;
|
|
55366
|
+
const name42 = host.slice(0, portSeparator);
|
|
55367
|
+
const port = host.slice(portSeparator + 1);
|
|
55368
|
+
if (port.length === 0 || !/^\d+$/.test(port)) return host;
|
|
55369
|
+
return DEFAULT_PORTS.has(port) ? name42 : `${name42}:${port}`;
|
|
55370
|
+
}
|
|
55371
|
+
function normalizeOrigin(value) {
|
|
55372
|
+
const origin = value.trim().toLowerCase();
|
|
55373
|
+
try {
|
|
55374
|
+
return new URL(origin).origin.toLowerCase();
|
|
55375
|
+
} catch {
|
|
55376
|
+
return origin;
|
|
55377
|
+
}
|
|
55378
|
+
}
|
|
55379
|
+
function compileHostValidation(rules) {
|
|
55380
|
+
return {
|
|
55381
|
+
hosts: rules.allowedHosts ? new Set(rules.allowedHosts.map(normalizeHost)) : void 0,
|
|
55382
|
+
origins: rules.allowedOrigins ? new Set(rules.allowedOrigins.map(normalizeOrigin)) : void 0
|
|
55383
|
+
};
|
|
55384
|
+
}
|
|
55385
|
+
function validateHostHeaders(headers, compiled) {
|
|
55386
|
+
if (compiled.hosts) {
|
|
55387
|
+
const host = headers.host;
|
|
55388
|
+
if (!host || !compiled.hosts.has(normalizeHost(host))) {
|
|
55389
|
+
return { status: 403, error: "Forbidden", message: "Invalid Host header" };
|
|
55390
|
+
}
|
|
55391
|
+
const forwardedHost = headers.forwardedHost;
|
|
55392
|
+
if (forwardedHost) {
|
|
55393
|
+
const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
55394
|
+
if (candidates.some((h2) => !compiled.hosts?.has(normalizeHost(h2)))) {
|
|
55395
|
+
return { status: 403, error: "Forbidden", message: "Invalid X-Forwarded-Host header" };
|
|
55396
|
+
}
|
|
55397
|
+
}
|
|
55398
|
+
}
|
|
55399
|
+
if (compiled.origins) {
|
|
55400
|
+
const origin = headers.origin;
|
|
55401
|
+
if (origin && !compiled.origins.has(normalizeOrigin(origin))) {
|
|
55402
|
+
return { status: 403, error: "Forbidden", message: "Invalid Origin header" };
|
|
55403
|
+
}
|
|
55404
|
+
}
|
|
55405
|
+
return void 0;
|
|
55406
|
+
}
|
|
55407
|
+
var DEFAULT_PORTS;
|
|
55408
|
+
var init_host_validation = __esm({
|
|
55409
|
+
"libs/sdk/src/server/security/host-validation.ts"() {
|
|
55410
|
+
"use strict";
|
|
55411
|
+
DEFAULT_PORTS = /* @__PURE__ */ new Set(["80", "443"]);
|
|
55412
|
+
}
|
|
55413
|
+
});
|
|
55414
|
+
|
|
54812
55415
|
// libs/sdk/src/transport/adapters/base-sse-transport.ts
|
|
54813
55416
|
import contentType from "content-type";
|
|
54814
55417
|
import getRawBody from "raw-body";
|
|
@@ -54821,6 +55424,7 @@ var init_base_sse_transport = __esm({
|
|
|
54821
55424
|
"libs/sdk/src/transport/adapters/base-sse-transport.ts"() {
|
|
54822
55425
|
"use strict";
|
|
54823
55426
|
init_transport_errors();
|
|
55427
|
+
init_host_validation();
|
|
54824
55428
|
MAXIMUM_MESSAGE_SIZE = "4mb";
|
|
54825
55429
|
SSEServerTransport = class {
|
|
54826
55430
|
/**
|
|
@@ -54844,25 +55448,31 @@ var init_base_sse_transport = __esm({
|
|
|
54844
55448
|
onmessage;
|
|
54845
55449
|
/**
|
|
54846
55450
|
* Validates request headers for DNS rebinding protection.
|
|
55451
|
+
*
|
|
55452
|
+
* Since v1.7.2 the primary defence is the adapter-level Host validation, which
|
|
55453
|
+
* runs before routing and therefore already covers `/sse` and `/message`
|
|
55454
|
+
* (GHSA-mc9g-v2cp-vfff). These per-transport options remain for a host that
|
|
55455
|
+
* constructs the transport directly; they delegate to the same rules so the
|
|
55456
|
+
* two can never disagree about what a valid Host looks like.
|
|
55457
|
+
*
|
|
54847
55458
|
* @returns Error message if validation fails, undefined if validation passes.
|
|
54848
55459
|
*/
|
|
54849
55460
|
validateRequestHeaders(req) {
|
|
54850
55461
|
if (!this._options.enableDnsRebindingProtection) {
|
|
54851
55462
|
return void 0;
|
|
54852
55463
|
}
|
|
54853
|
-
|
|
54854
|
-
|
|
54855
|
-
|
|
54856
|
-
|
|
54857
|
-
|
|
54858
|
-
|
|
54859
|
-
|
|
54860
|
-
|
|
54861
|
-
|
|
54862
|
-
|
|
54863
|
-
|
|
54864
|
-
|
|
54865
|
-
return void 0;
|
|
55464
|
+
const rejection = validateHostHeaders(
|
|
55465
|
+
{
|
|
55466
|
+
host: req.headers.host,
|
|
55467
|
+
forwardedHost: req.headers["x-forwarded-host"],
|
|
55468
|
+
origin: req.headers.origin
|
|
55469
|
+
},
|
|
55470
|
+
compileHostValidation({
|
|
55471
|
+
allowedHosts: this._options.allowedHosts?.length ? this._options.allowedHosts : void 0,
|
|
55472
|
+
allowedOrigins: this._options.allowedOrigins?.length ? this._options.allowedOrigins : void 0
|
|
55473
|
+
})
|
|
55474
|
+
);
|
|
55475
|
+
return rejection?.message;
|
|
54866
55476
|
}
|
|
54867
55477
|
/**
|
|
54868
55478
|
* Handles the initial SSE connection request.
|
|
@@ -55364,7 +55974,7 @@ var init_transport_local_adapter = __esm({
|
|
|
55364
55974
|
}
|
|
55365
55975
|
}
|
|
55366
55976
|
ensureAuthInfo(req, transport) {
|
|
55367
|
-
const { token, user, session } = req[ServerRequestTokens.auth];
|
|
55977
|
+
const { token, user, session, scopes, claims } = req[ServerRequestTokens.auth];
|
|
55368
55978
|
if (!session?.id) {
|
|
55369
55979
|
throw new UnauthorizedError(
|
|
55370
55980
|
"Session could not be reconstructed for the provided token. Re-authenticate to start a new session.",
|
|
@@ -55393,7 +56003,16 @@ var init_transport_local_adapter = __esm({
|
|
|
55393
56003
|
user,
|
|
55394
56004
|
sessionId,
|
|
55395
56005
|
sessionIdPayload: sessionPayload,
|
|
55396
|
-
scopes
|
|
56006
|
+
// Carry the scopes and claims the verified Authorization actually holds.
|
|
56007
|
+
// These used to be dropped (`scopes: []`, no claims), which left every
|
|
56008
|
+
// scope- or claim-based check downstream inert.
|
|
56009
|
+
//
|
|
56010
|
+
// Both are COPIED, not aliased. Tool code receives this object, and a
|
|
56011
|
+
// nested mutation (`authInfo.claims.roles.push('admin')`) on a shared
|
|
56012
|
+
// reference would rewrite the request's own verified authorization — which
|
|
56013
|
+
// the job/workflow permission guard then reads.
|
|
56014
|
+
scopes: Array.isArray(scopes) ? [...scopes] : [],
|
|
56015
|
+
...claims ? { claims: structuredClone(claims) } : {},
|
|
55397
56016
|
clientId: user.sub ?? "",
|
|
55398
56017
|
transport
|
|
55399
56018
|
};
|
|
@@ -55874,6 +56493,7 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
55874
56493
|
"libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts"() {
|
|
55875
56494
|
"use strict";
|
|
55876
56495
|
init_errors();
|
|
56496
|
+
init_session_scoped_event_store();
|
|
55877
56497
|
init_transport_error();
|
|
55878
56498
|
init_streamable_http_transport();
|
|
55879
56499
|
init_transport_local_adapter();
|
|
@@ -55883,7 +56503,8 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
55883
56503
|
TransportStreamableHttpAdapter = class extends LocalTransportAdapter {
|
|
55884
56504
|
createTransport(sessionId, response) {
|
|
55885
56505
|
const sessionIdGenerator = resolveSessionIdGenerator(this.key.type, sessionId);
|
|
55886
|
-
const
|
|
56506
|
+
const sharedEventStore = this.scope.eventStore;
|
|
56507
|
+
const eventStore = sharedEventStore ? createSessionScopedEventStore(sharedEventStore, sessionId) : void 0;
|
|
55887
56508
|
const isStateless = this.key.type === "stateless-http";
|
|
55888
56509
|
return new RecreateableStreamableHTTPServerTransport({
|
|
55889
56510
|
sessionIdGenerator,
|
|
@@ -59775,7 +60396,13 @@ function normalizeAppScope(appItem, metadata) {
|
|
|
59775
60396
|
...metadata,
|
|
59776
60397
|
id: scopeId,
|
|
59777
60398
|
apps: [appItem],
|
|
59778
|
-
auth
|
|
60399
|
+
// An app that declares no `auth` INHERITS the server's policy. Writing
|
|
60400
|
+
// `appMetadata.auth` unconditionally set the key to `undefined`, which
|
|
60401
|
+
// overrode the spread above and dropped the server's auth entirely — so a
|
|
60402
|
+
// standalone app silently became public (GHSA-rgxj-434m-vxh3, where the
|
|
60403
|
+
// dashboard's own scope was the case in point). Declaring
|
|
60404
|
+
// `auth: { mode: 'public' }` explicitly is still honoured.
|
|
60405
|
+
auth: appMetadata.auth ?? metadata.auth,
|
|
59779
60406
|
// For a splitByApp scope, auth (and any auth.ui paths) come from `@App`, so
|
|
59780
60407
|
// anchor on the App's captured source dir. Fall back to the server's.
|
|
59781
60408
|
// `__sourceDir` lives on LocalAppMetadata only; read it defensively.
|
|
@@ -60037,6 +60664,11 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
60037
60664
|
const entryPaths = new Set((Array.isArray(rawEntry) ? rawEntry : [rawEntry]).map(normalizePath));
|
|
60038
60665
|
const cors2 = options.cors ?? mapHttpCors(httpConfig?.cors, scope);
|
|
60039
60666
|
const corsEnabled = cors2?.origin !== void 0 && cors2.origin !== false;
|
|
60667
|
+
const rebinding = httpConfig?.security?.dnsRebindingProtection;
|
|
60668
|
+
const hostValidation = rebinding?.enabled === false || !rebinding?.allowedHosts?.length && !rebinding?.allowedOrigins?.length ? void 0 : compileHostValidation({
|
|
60669
|
+
allowedHosts: rebinding.allowedHosts,
|
|
60670
|
+
allowedOrigins: rebinding.allowedOrigins
|
|
60671
|
+
});
|
|
60040
60672
|
const corsHeadersFor = (request) => {
|
|
60041
60673
|
if (!corsEnabled) return {};
|
|
60042
60674
|
const reqOrigin = request.headers.get("origin") ?? "";
|
|
@@ -60067,6 +60699,19 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
60067
60699
|
};
|
|
60068
60700
|
return async function handle(request, ctx, env) {
|
|
60069
60701
|
const url = new URL(request.url);
|
|
60702
|
+
if (hostValidation) {
|
|
60703
|
+
const rejection = validateHostHeaders(
|
|
60704
|
+
{
|
|
60705
|
+
host: request.headers.get("host") ?? url.host,
|
|
60706
|
+
forwardedHost: request.headers.get("x-forwarded-host") ?? void 0,
|
|
60707
|
+
origin: request.headers.get("origin") ?? void 0
|
|
60708
|
+
},
|
|
60709
|
+
hostValidation
|
|
60710
|
+
);
|
|
60711
|
+
if (rejection) {
|
|
60712
|
+
return Response.json({ error: rejection.error, message: rejection.message }, { status: rejection.status });
|
|
60713
|
+
}
|
|
60714
|
+
}
|
|
60070
60715
|
if (corsEnabled && request.method === "OPTIONS") {
|
|
60071
60716
|
return new Response(null, { status: 204, headers: corsHeadersFor(request) });
|
|
60072
60717
|
}
|
|
@@ -60200,6 +60845,7 @@ var init_web_fetch_handler = __esm({
|
|
|
60200
60845
|
init_server_tokens();
|
|
60201
60846
|
init_path_utils();
|
|
60202
60847
|
init_errors();
|
|
60848
|
+
init_host_validation();
|
|
60203
60849
|
init_web_response_renderer();
|
|
60204
60850
|
}
|
|
60205
60851
|
});
|
|
@@ -60352,39 +60998,22 @@ function createHostValidationMiddleware(options) {
|
|
|
60352
60998
|
if (!options.enabled) {
|
|
60353
60999
|
return (_req, _res, next) => next();
|
|
60354
61000
|
}
|
|
60355
|
-
const
|
|
60356
|
-
|
|
61001
|
+
const compiled = compileHostValidation({
|
|
61002
|
+
allowedHosts: options.allowedHosts,
|
|
61003
|
+
allowedOrigins: options.allowedOrigins
|
|
61004
|
+
});
|
|
60357
61005
|
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
|
-
}
|
|
61006
|
+
const rejection = validateHostHeaders(
|
|
61007
|
+
{
|
|
61008
|
+
host: req.headers?.["host"],
|
|
61009
|
+
forwardedHost: req.headers?.["x-forwarded-host"],
|
|
61010
|
+
origin: req.headers?.["origin"]
|
|
61011
|
+
},
|
|
61012
|
+
compiled
|
|
61013
|
+
);
|
|
61014
|
+
if (rejection) {
|
|
61015
|
+
res.status(rejection.status).json({ error: rejection.error, message: rejection.message });
|
|
61016
|
+
return;
|
|
60388
61017
|
}
|
|
60389
61018
|
next();
|
|
60390
61019
|
};
|
|
@@ -60392,6 +61021,82 @@ function createHostValidationMiddleware(options) {
|
|
|
60392
61021
|
var init_host_validation_middleware = __esm({
|
|
60393
61022
|
"libs/sdk/src/server/middleware/host-validation.middleware.ts"() {
|
|
60394
61023
|
"use strict";
|
|
61024
|
+
init_host_validation();
|
|
61025
|
+
}
|
|
61026
|
+
});
|
|
61027
|
+
|
|
61028
|
+
// libs/sdk/src/server/security/resolve-allowed-hosts.ts
|
|
61029
|
+
function canonicalizeAddress(address) {
|
|
61030
|
+
const bare = address.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
61031
|
+
if (!bare.includes(":")) return bare;
|
|
61032
|
+
try {
|
|
61033
|
+
return new URL(`http://[${bare}]`).hostname.replace(/^\[|\]$/g, "");
|
|
61034
|
+
} catch {
|
|
61035
|
+
return bare;
|
|
61036
|
+
}
|
|
61037
|
+
}
|
|
61038
|
+
function isLoopbackAddress(address) {
|
|
61039
|
+
if (!address) return false;
|
|
61040
|
+
const normalized = canonicalizeAddress(address);
|
|
61041
|
+
return normalized === "::1" || normalized === "localhost" || normalized.startsWith("127.") || // IPv4-mapped IPv6 loopback, which `URL` renders as `::ffff:7f00:1`.
|
|
61042
|
+
normalized === "::ffff:7f00:1" || normalized.startsWith("::ffff:127.");
|
|
61043
|
+
}
|
|
61044
|
+
function withAndWithoutPort(host, port) {
|
|
61045
|
+
return port ? [host, `${host}:${port}`] : [host];
|
|
61046
|
+
}
|
|
61047
|
+
function hostFromUrl(url) {
|
|
61048
|
+
if (!url) return void 0;
|
|
61049
|
+
try {
|
|
61050
|
+
return new URL(url).host || void 0;
|
|
61051
|
+
} catch {
|
|
61052
|
+
return void 0;
|
|
61053
|
+
}
|
|
61054
|
+
}
|
|
61055
|
+
function isRoutableBind(address) {
|
|
61056
|
+
if (!address) return false;
|
|
61057
|
+
return !isLoopbackAddress(address);
|
|
61058
|
+
}
|
|
61059
|
+
function deriveAllowedHosts(input) {
|
|
61060
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
61061
|
+
if (input.socketPath) return [];
|
|
61062
|
+
for (const name42 of LOOPBACK_NAMES) {
|
|
61063
|
+
for (const value of withAndWithoutPort(name42, input.port)) hosts.add(value);
|
|
61064
|
+
}
|
|
61065
|
+
const bindAddress = input.bindAddress?.trim();
|
|
61066
|
+
if (bindAddress && !isLoopbackAddress(bindAddress) && bindAddress !== "0.0.0.0" && bindAddress !== "::") {
|
|
61067
|
+
const literal = bindAddress.includes(":") && !bindAddress.startsWith("[") ? `[${bindAddress}]` : bindAddress;
|
|
61068
|
+
for (const value of withAndWithoutPort(literal, input.port)) hosts.add(value);
|
|
61069
|
+
}
|
|
61070
|
+
const issuerHost = hostFromUrl(input.issuer);
|
|
61071
|
+
if (issuerHost) {
|
|
61072
|
+
hosts.add(issuerHost);
|
|
61073
|
+
const [name42] = issuerHost.split(":");
|
|
61074
|
+
if (!issuerHost.includes(":") && input.port) hosts.add(`${name42}:${input.port}`);
|
|
61075
|
+
}
|
|
61076
|
+
for (const extra of input.extraHosts ?? []) {
|
|
61077
|
+
const trimmed = extra.trim();
|
|
61078
|
+
if (trimmed) hosts.add(trimmed);
|
|
61079
|
+
}
|
|
61080
|
+
return [...hosts];
|
|
61081
|
+
}
|
|
61082
|
+
function shouldEnforceDerivedHosts(input) {
|
|
61083
|
+
if (input.socketPath) return false;
|
|
61084
|
+
const hasExplicitName = hostFromUrl(input.issuer) !== void 0 || (input.extraHosts ?? []).some((h2) => h2.trim());
|
|
61085
|
+
if (hasExplicitName) return true;
|
|
61086
|
+
if (input.port === void 0) return false;
|
|
61087
|
+
return !isRoutableBind(input.bindAddress);
|
|
61088
|
+
}
|
|
61089
|
+
function allowedHostsFromEnv(env = process.env) {
|
|
61090
|
+
const raw = env["FRONTMCP_ALLOWED_HOSTS"]?.trim();
|
|
61091
|
+
if (!raw) return void 0;
|
|
61092
|
+
const hosts = raw.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
61093
|
+
return hosts.length > 0 ? hosts : void 0;
|
|
61094
|
+
}
|
|
61095
|
+
var LOOPBACK_NAMES;
|
|
61096
|
+
var init_resolve_allowed_hosts = __esm({
|
|
61097
|
+
"libs/sdk/src/server/security/resolve-allowed-hosts.ts"() {
|
|
61098
|
+
"use strict";
|
|
61099
|
+
LOOPBACK_NAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
60395
61100
|
}
|
|
60396
61101
|
});
|
|
60397
61102
|
|
|
@@ -60417,12 +61122,17 @@ var init_express_host_adapter = __esm({
|
|
|
60417
61122
|
"use strict";
|
|
60418
61123
|
init_mcp_error();
|
|
60419
61124
|
init_host_validation_middleware();
|
|
61125
|
+
init_resolve_allowed_hosts();
|
|
60420
61126
|
init_base_host_adapter();
|
|
60421
61127
|
DEFAULT_EXPRESS_BODY_LIMIT = "4mb";
|
|
60422
61128
|
ExpressHostAdapter = class extends HostServerAdapter {
|
|
60423
61129
|
app = express();
|
|
60424
61130
|
router = express.Router();
|
|
60425
61131
|
prepared = false;
|
|
61132
|
+
/** Active host-validation middleware, or undefined while nothing is enforced. */
|
|
61133
|
+
hostValidation;
|
|
61134
|
+
/** Builds the derived allow-list, once this process commits to listening. */
|
|
61135
|
+
deriveHostValidation;
|
|
60426
61136
|
constructor(options) {
|
|
60427
61137
|
super();
|
|
60428
61138
|
const corsOptions = options?.cors;
|
|
@@ -60436,6 +61146,7 @@ var init_express_host_adapter = __esm({
|
|
|
60436
61146
|
})
|
|
60437
61147
|
);
|
|
60438
61148
|
}
|
|
61149
|
+
this.installHostValidation(options);
|
|
60439
61150
|
const jsonLimit = options?.bodyLimit ?? DEFAULT_EXPRESS_BODY_LIMIT;
|
|
60440
61151
|
const formLimit = options?.urlencodedLimit ?? jsonLimit;
|
|
60441
61152
|
const headerContains = (req, needle) => (req.headers["content-type"] ?? "").toLowerCase().includes(needle);
|
|
@@ -60456,23 +61167,6 @@ var init_express_host_adapter = __esm({
|
|
|
60456
61167
|
}
|
|
60457
61168
|
next(err);
|
|
60458
61169
|
});
|
|
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
61170
|
this.app.use((req, res, next) => {
|
|
60477
61171
|
if (corsEnabled) {
|
|
60478
61172
|
res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate, Mcp-Session-Id");
|
|
@@ -60482,6 +61176,64 @@ var init_express_host_adapter = __esm({
|
|
|
60482
61176
|
next();
|
|
60483
61177
|
});
|
|
60484
61178
|
}
|
|
61179
|
+
/**
|
|
61180
|
+
* Install DNS-rebinding protection.
|
|
61181
|
+
*
|
|
61182
|
+
* ON by default since v1.7.2 (BC-035). `allowedHosts` falls back to the names
|
|
61183
|
+
* this process actually answers to — the loopback aliases with and without
|
|
61184
|
+
* the bound port, the bound NIC address, and the configured issuer host.
|
|
61185
|
+
*
|
|
61186
|
+
* Escape hatches, in precedence order: explicit
|
|
61187
|
+
* `security.dnsRebindingProtection.allowedHosts`, the
|
|
61188
|
+
* `FRONTMCP_ALLOWED_HOSTS` env var, or `enabled: false` to turn it off.
|
|
61189
|
+
*/
|
|
61190
|
+
installHostValidation(options) {
|
|
61191
|
+
const protection = options?.security?.dnsRebindingProtection;
|
|
61192
|
+
if (protection?.enabled === false) return;
|
|
61193
|
+
const allowedOrigins = protection?.allowedOrigins;
|
|
61194
|
+
const explicitHosts = protection?.allowedHosts ?? allowedHostsFromEnv();
|
|
61195
|
+
if (explicitHosts?.length || allowedOrigins?.length) {
|
|
61196
|
+
this.hostValidation = createHostValidationMiddleware({
|
|
61197
|
+
enabled: true,
|
|
61198
|
+
allowedHosts: explicitHosts?.length ? explicitHosts : void 0,
|
|
61199
|
+
allowedOrigins
|
|
61200
|
+
});
|
|
61201
|
+
} else {
|
|
61202
|
+
this.deriveHostValidation = (portOrSocketPath, bindAddress) => {
|
|
61203
|
+
const isSocket = typeof portOrSocketPath === "string";
|
|
61204
|
+
const derivation = {
|
|
61205
|
+
// `start()` binds `bindAddress ?? '0.0.0.0'`; mirror that exactly.
|
|
61206
|
+
bindAddress: isSocket ? void 0 : bindAddress ?? "0.0.0.0",
|
|
61207
|
+
port: isSocket ? void 0 : portOrSocketPath,
|
|
61208
|
+
socketPath: isSocket ? portOrSocketPath : void 0,
|
|
61209
|
+
issuer: options?.listen?.issuer
|
|
61210
|
+
};
|
|
61211
|
+
if (!shouldEnforceDerivedHosts(derivation)) {
|
|
61212
|
+
console.warn(
|
|
61213
|
+
`[frontmcp] DNS-rebinding protection is not enforcing a Host allow-list: the server binds a routable address (${derivation.bindAddress ?? "unknown"}) and no allowed hosts are configured. Set security.dnsRebindingProtection.allowedHosts (or FRONTMCP_ALLOWED_HOSTS) to your public hostname(s).`
|
|
61214
|
+
);
|
|
61215
|
+
return void 0;
|
|
61216
|
+
}
|
|
61217
|
+
const allowedHosts = deriveAllowedHosts(derivation);
|
|
61218
|
+
if (!allowedHosts.length) return void 0;
|
|
61219
|
+
return createHostValidationMiddleware({ enabled: true, allowedHosts });
|
|
61220
|
+
};
|
|
61221
|
+
}
|
|
61222
|
+
this.app.use(((req, res, next) => {
|
|
61223
|
+
const validate = this.hostValidation;
|
|
61224
|
+
if (!validate) return next();
|
|
61225
|
+
return validate(req, res, next);
|
|
61226
|
+
}));
|
|
61227
|
+
}
|
|
61228
|
+
/**
|
|
61229
|
+
* Enable the DERIVED host allow-list. Called from `start()` only — see
|
|
61230
|
+
* `installHostValidation`.
|
|
61231
|
+
*/
|
|
61232
|
+
enableDerivedHostValidation(portOrSocketPath, bindAddress) {
|
|
61233
|
+
if (this.hostValidation || !this.deriveHostValidation) return;
|
|
61234
|
+
this.hostValidation = this.deriveHostValidation(portOrSocketPath, bindAddress);
|
|
61235
|
+
this.deriveHostValidation = void 0;
|
|
61236
|
+
}
|
|
60485
61237
|
registerRoute(method, path3, handler) {
|
|
60486
61238
|
this.router[method.toLowerCase()](path3, this.enhancedHandler(handler));
|
|
60487
61239
|
}
|
|
@@ -60514,6 +61266,7 @@ var init_express_host_adapter = __esm({
|
|
|
60514
61266
|
return this.app;
|
|
60515
61267
|
}
|
|
60516
61268
|
async start(portOrSocketPath, bindAddress) {
|
|
61269
|
+
this.enableDerivedHostValidation(portOrSocketPath, bindAddress);
|
|
60517
61270
|
this.prepare();
|
|
60518
61271
|
const server = http.createServer(this.app);
|
|
60519
61272
|
server.requestTimeout = 0;
|
|
@@ -63372,49 +64125,7 @@ init_job_registry();
|
|
|
63372
64125
|
init_job_instance();
|
|
63373
64126
|
init_job_events();
|
|
63374
64127
|
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
|
|
64128
|
+
init_job_permission_guard();
|
|
63418
64129
|
init_job_scope_helper();
|
|
63419
64130
|
|
|
63420
64131
|
// libs/sdk/src/job/execution/index.ts
|
|
@@ -63816,6 +64527,7 @@ export {
|
|
|
63816
64527
|
DynamicAdapter,
|
|
63817
64528
|
DynamicAdapterNameError,
|
|
63818
64529
|
DynamicJobDirectExecutionError,
|
|
64530
|
+
DynamicJobRegistrationDisabledError,
|
|
63819
64531
|
DynamicPlugin,
|
|
63820
64532
|
ELICITATION_FALLBACK_JSON_SCHEMA,
|
|
63821
64533
|
ESM_SH_BASE_URL,
|
|
@@ -63970,6 +64682,7 @@ export {
|
|
|
63970
64682
|
JobExecutionManager,
|
|
63971
64683
|
JobInstance,
|
|
63972
64684
|
JobKind,
|
|
64685
|
+
JobNotAuthorizedError,
|
|
63973
64686
|
JobPermissionGuard,
|
|
63974
64687
|
JobRegistry,
|
|
63975
64688
|
LLM_ADAPTER,
|
|
@@ -64487,6 +65200,7 @@ export {
|
|
|
64487
65200
|
resolveNameConflicts,
|
|
64488
65201
|
resolveOutputSchemaMode,
|
|
64489
65202
|
resolvePendingTC39HooksForClass,
|
|
65203
|
+
resolvePrincipal2 as resolvePrincipal,
|
|
64490
65204
|
resolveRegistryToken,
|
|
64491
65205
|
resolveSchemaDescriptionFormat,
|
|
64492
65206
|
resolveToolVisibility,
|