@frontmcp/sdk 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/flows/call-agent.flow.d.ts +2 -6
- package/agent/flows/call-agent.flow.d.ts.map +1 -1
- package/auth/flows/oauth.authorize.flow.d.ts +30 -0
- package/auth/flows/oauth.authorize.flow.d.ts.map +1 -1
- package/auth/flows/oauth.callback.flow.d.ts.map +1 -1
- package/auth/instances/instance.local-primary-auth.d.ts +35 -0
- package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
- package/builtin/config/providers/config-loader.d.ts +5 -0
- package/builtin/config/providers/config-loader.d.ts.map +1 -1
- package/common/metadata/agent.metadata.d.ts +2 -2
- package/common/metadata/app.metadata.d.ts +6 -6
- package/common/metadata/flow.metadata.d.ts +4 -4
- package/common/metadata/front-mcp.metadata.d.ts +43 -31
- package/common/metadata/front-mcp.metadata.d.ts.map +1 -1
- package/common/metadata/job.metadata.d.ts +2 -2
- package/common/metadata/tool.metadata.d.ts +3 -3
- package/common/metadata/workflow.metadata.d.ts +2 -2
- package/common/schemas/http-output.schema.d.ts +4 -4
- package/common/types/options/http/interfaces.d.ts +26 -10
- package/common/types/options/http/interfaces.d.ts.map +1 -1
- package/common/types/options/http/schema.d.ts.map +1 -1
- package/common/types/options/session/schema.d.ts +3 -3
- package/common/types/options/transport/schema.d.ts +3 -1
- package/common/types/options/transport/schema.d.ts.map +1 -1
- package/common/utils/index.d.ts +1 -0
- package/common/utils/index.d.ts.map +1 -1
- package/common/utils/principal.utils.d.ts +29 -0
- package/common/utils/principal.utils.d.ts.map +1 -0
- package/direct/llm-platform.d.ts +2 -2
- package/direct/llm-platform.d.ts.map +1 -1
- package/errors/index.d.ts +1 -0
- package/errors/index.d.ts.map +1 -1
- package/errors/job.errors.d.ts +26 -0
- package/errors/job.errors.d.ts.map +1 -0
- package/esm/index.mjs +928 -207
- package/front-mcp/front-mcp.providers.d.ts +4 -4
- package/index.js +1041 -320
- package/job/execution/job-execution.manager.d.ts +15 -0
- package/job/execution/job-execution.manager.d.ts.map +1 -1
- package/job/job-permission.guard.d.ts +23 -4
- package/job/job-permission.guard.d.ts.map +1 -1
- package/job/job-scope.helper.d.ts +12 -0
- package/job/job-scope.helper.d.ts.map +1 -1
- package/job/store/job-state.interface.d.ts +6 -0
- package/job/store/job-state.interface.d.ts.map +1 -1
- package/job/tools/execute-job.tool.d.ts.map +1 -1
- package/job/tools/get-job-status.tool.d.ts +12 -0
- package/job/tools/get-job-status.tool.d.ts.map +1 -1
- package/job/tools/list-jobs.tool.d.ts +6 -0
- package/job/tools/list-jobs.tool.d.ts.map +1 -1
- package/job/tools/register-job.tool.d.ts +1 -0
- package/job/tools/register-job.tool.d.ts.map +1 -1
- package/job/tools/remove-job.tool.d.ts.map +1 -1
- package/package.json +10 -10
- package/resource/flows/resource-templates-list.flow.d.ts +2 -2
- package/resource/flows/resources-list.flow.d.ts +2 -2
- package/scope/scope.utils.d.ts.map +1 -1
- package/server/adapters/express.host.adapter.d.ts +34 -2
- package/server/adapters/express.host.adapter.d.ts.map +1 -1
- package/server/middleware/host-validation.middleware.d.ts +8 -4
- package/server/middleware/host-validation.middleware.d.ts.map +1 -1
- package/server/security/host-validation.d.ts +66 -0
- package/server/security/host-validation.d.ts.map +1 -0
- package/server/security/resolve-allowed-hosts.d.ts +63 -0
- package/server/security/resolve-allowed-hosts.d.ts.map +1 -0
- package/server/security/security-audit.d.ts +7 -4
- package/server/security/security-audit.d.ts.map +1 -1
- package/server/server.instance.d.ts.map +1 -1
- package/server/server.types.d.ts +11 -6
- package/server/server.types.d.ts.map +1 -1
- package/tool/flows/call-tool.flow.d.ts +19 -7
- package/tool/flows/call-tool.flow.d.ts.map +1 -1
- package/tool/flows/tools-list.flow.d.ts +2 -6
- package/tool/flows/tools-list.flow.d.ts.map +1 -1
- package/transport/adapters/base-sse-transport.d.ts +7 -0
- package/transport/adapters/base-sse-transport.d.ts.map +1 -1
- package/transport/adapters/transport.local.adapter.d.ts.map +1 -1
- package/transport/adapters/transport.streamable-http.adapter.d.ts.map +1 -1
- package/transport/event-stores/index.d.ts +2 -0
- package/transport/event-stores/index.d.ts.map +1 -1
- package/transport/event-stores/memory.event-store.d.ts +9 -2
- package/transport/event-stores/memory.event-store.d.ts.map +1 -1
- package/transport/event-stores/redis.event-store.d.ts +24 -3
- package/transport/event-stores/redis.event-store.d.ts.map +1 -1
- package/transport/event-stores/session-scoped.event-store.d.ts +17 -0
- package/transport/event-stores/session-scoped.event-store.d.ts.map +1 -0
- package/transport/web-fetch-handler.d.ts.map +1 -1
- package/workflow/tools/execute-workflow.tool.d.ts.map +1 -1
- package/workflow/tools/get-workflow-status.tool.d.ts +12 -0
- package/workflow/tools/get-workflow-status.tool.d.ts.map +1 -1
- package/workflow/tools/list-workflows.tool.d.ts +1 -0
- package/workflow/tools/list-workflows.tool.d.ts.map +1 -1
- package/workflow/tools/register-workflow.tool.d.ts +1 -0
- package/workflow/tools/register-workflow.tool.d.ts.map +1 -1
- package/workflow/tools/remove-workflow.tool.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -1864,14 +1864,19 @@ var init_schema3 = __esm({
|
|
|
1864
1864
|
socketPath: z8.string().optional(),
|
|
1865
1865
|
/**
|
|
1866
1866
|
* CORS configuration.
|
|
1867
|
-
* - undefined (default):
|
|
1868
|
-
*
|
|
1867
|
+
* - undefined (default): NO CORS headers — same-origin only. A browser will not let another
|
|
1868
|
+
* origin read the response. Set `{ origin: true }` for the old permissive behaviour.
|
|
1869
|
+
* - false: CORS disabled (same effect as the default; kept for explicitness)
|
|
1869
1870
|
* - CorsOptions object: custom CORS config
|
|
1870
1871
|
*/
|
|
1871
1872
|
cors: z8.union([z8.literal(false), corsOptionsSchema]).optional(),
|
|
1872
1873
|
/**
|
|
1873
1874
|
* Security configuration for transport hardening.
|
|
1874
|
-
*
|
|
1875
|
+
*
|
|
1876
|
+
* The defaults are the SAFE choice: the server binds loopback and sends no CORS headers. Reaching
|
|
1877
|
+
* it from another host or another origin is something you opt into — `bindAddress: 'all'`, the
|
|
1878
|
+
* `FRONTMCP_BIND_ADDRESS=all` env var (no rebuild; the right fit for a Dockerfile), or a
|
|
1879
|
+
* distributed build — and an explicit `cors` config.
|
|
1875
1880
|
*/
|
|
1876
1881
|
security: z8.object({
|
|
1877
1882
|
strict: z8.boolean().optional(),
|
|
@@ -2340,7 +2345,10 @@ var init_schema7 = __esm({
|
|
|
2340
2345
|
});
|
|
2341
2346
|
eventStoreConfigSchema = z12.object({
|
|
2342
2347
|
enabled: z12.boolean().default(false),
|
|
2343
|
-
|
|
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"),
|
|
2344
2352
|
maxEvents: z12.number().int().positive().optional().default(1e4),
|
|
2345
2353
|
ttlMs: z12.number().int().positive().optional().default(3e5),
|
|
2346
2354
|
redis: redisOptionsSchema.optional()
|
|
@@ -4076,7 +4084,7 @@ function formatToolsForPlatform(tools, platform) {
|
|
|
4076
4084
|
})
|
|
4077
4085
|
);
|
|
4078
4086
|
case "vercel-ai": {
|
|
4079
|
-
const result =
|
|
4087
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
4080
4088
|
for (const tool of tools) {
|
|
4081
4089
|
result[tool.name] = {
|
|
4082
4090
|
description: tool.description ?? `Execute ${tool.name}`,
|
|
@@ -5631,6 +5639,32 @@ var init_sdk_errors = __esm({
|
|
|
5631
5639
|
}
|
|
5632
5640
|
});
|
|
5633
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
|
+
|
|
5634
5668
|
// libs/sdk/src/errors/index.ts
|
|
5635
5669
|
var errors_exports = {};
|
|
5636
5670
|
__export(errors_exports, {
|
|
@@ -5656,6 +5690,7 @@ __export(errors_exports, {
|
|
|
5656
5690
|
DependencyNotFoundError: () => DependencyNotFoundError,
|
|
5657
5691
|
DynamicAdapterNameError: () => DynamicAdapterNameError,
|
|
5658
5692
|
DynamicJobDirectExecutionError: () => DynamicJobDirectExecutionError,
|
|
5693
|
+
DynamicJobRegistrationDisabledError: () => DynamicJobRegistrationDisabledError,
|
|
5659
5694
|
ElicitationDisabledError: () => ElicitationDisabledError,
|
|
5660
5695
|
ElicitationEncryptionError: () => ElicitationEncryptionError,
|
|
5661
5696
|
ElicitationFallbackRequired: () => ElicitationFallbackRequired,
|
|
@@ -5704,6 +5739,7 @@ __export(errors_exports, {
|
|
|
5704
5739
|
InvalidUseFactoryError: () => InvalidUseFactoryError,
|
|
5705
5740
|
InvalidUseValueError: () => InvalidUseValueError,
|
|
5706
5741
|
InvokeStateMissingKeyError: () => InvokeStateMissingKeyError,
|
|
5742
|
+
JobNotAuthorizedError: () => JobNotAuthorizedError,
|
|
5707
5743
|
MCP_ERROR_CODES: () => MCP_ERROR_CODES,
|
|
5708
5744
|
McpError: () => McpError,
|
|
5709
5745
|
MethodNotImplementedError: () => MethodNotImplementedError,
|
|
@@ -5826,6 +5862,7 @@ var init_errors = __esm({
|
|
|
5826
5862
|
init_workflow_errors();
|
|
5827
5863
|
init_esm_errors();
|
|
5828
5864
|
init_sdk_errors();
|
|
5865
|
+
init_job_errors();
|
|
5829
5866
|
}
|
|
5830
5867
|
});
|
|
5831
5868
|
|
|
@@ -7153,6 +7190,10 @@ var require_dist = __commonJS({
|
|
|
7153
7190
|
}
|
|
7154
7191
|
return id;
|
|
7155
7192
|
}
|
|
7193
|
+
async getStreamIdForEventId(eventId) {
|
|
7194
|
+
const row = this.prepared().getStreamId.get(eventId);
|
|
7195
|
+
return row?.stream_id;
|
|
7196
|
+
}
|
|
7156
7197
|
async replayEventsAfter(lastEventId, { send }) {
|
|
7157
7198
|
const stmts = this.prepared();
|
|
7158
7199
|
const streamRow = stmts.getStreamId.get(lastEventId);
|
|
@@ -14068,26 +14109,44 @@ async function loadYamlConfig(basePath, configPath) {
|
|
|
14068
14109
|
}
|
|
14069
14110
|
return {};
|
|
14070
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
|
+
}
|
|
14071
14127
|
function deepMerge(target, source) {
|
|
14072
14128
|
const result = { ...target };
|
|
14073
14129
|
for (const key in source) {
|
|
14130
|
+
if (UNSAFE_MERGE_KEYS.has(key)) continue;
|
|
14074
14131
|
const sourceVal = source[key];
|
|
14075
14132
|
const targetVal = result[key];
|
|
14076
14133
|
if (sourceVal !== void 0 && sourceVal !== null) {
|
|
14077
14134
|
if (typeof sourceVal === "object" && !Array.isArray(sourceVal) && typeof targetVal === "object" && !Array.isArray(targetVal) && targetVal !== null) {
|
|
14078
14135
|
result[key] = deepMerge(targetVal, sourceVal);
|
|
14079
14136
|
} else {
|
|
14080
|
-
result[key] = sourceVal;
|
|
14137
|
+
result[key] = stripUnsafeKeys(sourceVal);
|
|
14081
14138
|
}
|
|
14082
14139
|
}
|
|
14083
14140
|
}
|
|
14084
14141
|
return result;
|
|
14085
14142
|
}
|
|
14143
|
+
var UNSAFE_MERGE_KEYS;
|
|
14086
14144
|
var init_config_loader = __esm({
|
|
14087
14145
|
"libs/sdk/src/builtin/config/providers/config-loader.ts"() {
|
|
14088
14146
|
"use strict";
|
|
14089
14147
|
init_config_service();
|
|
14090
14148
|
init_env_loader();
|
|
14149
|
+
UNSAFE_MERGE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
14091
14150
|
}
|
|
14092
14151
|
});
|
|
14093
14152
|
|
|
@@ -20741,6 +20800,7 @@ var init_tool_credentials_gate = __esm({
|
|
|
20741
20800
|
});
|
|
20742
20801
|
|
|
20743
20802
|
// libs/sdk/src/tool/flows/call-tool.flow.ts
|
|
20803
|
+
import { signIncrementalAuthTicket } from "@frontmcp/auth";
|
|
20744
20804
|
import { ConcurrencyLimitError, ExecutionTimeoutError, withTimeout } from "@frontmcp/guard";
|
|
20745
20805
|
import { z as z56 } from "@frontmcp/lazy-zod";
|
|
20746
20806
|
import { CallToolRequestSchema as CallToolRequestSchema2, CallToolResultSchema as CallToolResultSchema2 } from "@frontmcp/protocol";
|
|
@@ -21129,7 +21189,7 @@ var init_call_tool_flow = __esm({
|
|
|
21129
21189
|
this.logger.verbose(`checkToolAuthorization: using anonymous for ${appId}`);
|
|
21130
21190
|
return;
|
|
21131
21191
|
}
|
|
21132
|
-
const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "");
|
|
21192
|
+
const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "", authInfo, claimAuthorizedApps);
|
|
21133
21193
|
this.logger.info(`checkToolAuthorization: authorization required for app "${appId}"`);
|
|
21134
21194
|
throw new AuthorizationRequiredError({
|
|
21135
21195
|
appId,
|
|
@@ -21141,11 +21201,51 @@ var init_call_tool_flow = __esm({
|
|
|
21141
21201
|
this.logger.verbose("checkToolAuthorization:done");
|
|
21142
21202
|
}
|
|
21143
21203
|
/**
|
|
21144
|
-
* 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.
|
|
21145
21215
|
*/
|
|
21146
|
-
buildProgressiveAuthUrl(appId, toolId) {
|
|
21216
|
+
buildProgressiveAuthUrl(appId, toolId, authInfo, priorAppIds) {
|
|
21147
21217
|
const baseUrl = this.scope.fullPath || "";
|
|
21148
|
-
|
|
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
|
+
);
|
|
21149
21249
|
}
|
|
21150
21250
|
async checkEntryAuthorities() {
|
|
21151
21251
|
this.logger.verbose("checkEntryAuthorities:start");
|
|
@@ -35891,7 +35991,8 @@ import {
|
|
|
35891
35991
|
createFederatedAuthSession,
|
|
35892
35992
|
escapeHtml as escapeHtml2,
|
|
35893
35993
|
renderLocalLoginPage,
|
|
35894
|
-
startNextProvider
|
|
35994
|
+
startNextProvider,
|
|
35995
|
+
verifyIncrementalAuthTicket
|
|
35895
35996
|
} from "@frontmcp/auth";
|
|
35896
35997
|
import { z as z64 } from "@frontmcp/lazy-zod";
|
|
35897
35998
|
import { generateCodeVerifier, randomUUID as randomUUID15, sha256Base64url } from "@frontmcp/utils";
|
|
@@ -35966,6 +36067,13 @@ var init_oauth_authorize_flow = __esm({
|
|
|
35966
36067
|
targetAppId: z64.string().optional().describe("Target app ID for incremental authorization"),
|
|
35967
36068
|
targetToolId: z64.string().optional().describe("Target tool ID that triggered the incremental auth"),
|
|
35968
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(),
|
|
35969
36077
|
/**
|
|
35970
36078
|
* Apps the client already holds a grant for (the prior `authorized_apps`
|
|
35971
36079
|
* claim), carried forward on an incremental authorize so the newly-minted
|
|
@@ -36018,18 +36126,21 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36018
36126
|
const { request } = this.rawInput;
|
|
36019
36127
|
const rawRedirectUri = request.query["redirect_uri"];
|
|
36020
36128
|
const rawState = request.query["state"];
|
|
36021
|
-
const
|
|
36022
|
-
const
|
|
36023
|
-
const
|
|
36024
|
-
const
|
|
36025
|
-
const isIncrementalAuth = mode === "incremental" || !!targetAppId;
|
|
36026
|
-
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;
|
|
36027
36133
|
let priorAuthorizedAppIds;
|
|
36028
|
-
if (
|
|
36029
|
-
|
|
36030
|
-
|
|
36031
|
-
|
|
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
|
+
}
|
|
36032
36142
|
}
|
|
36143
|
+
const existingSessionId = request.query["session_id"];
|
|
36033
36144
|
const isDefaultAuthProvider = !metadata.auth;
|
|
36034
36145
|
let requiresFederatedLogin = false;
|
|
36035
36146
|
if (metadata.auth && isOrchestratedMode(metadata.auth)) {
|
|
@@ -36055,6 +36166,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36055
36166
|
targetToolId,
|
|
36056
36167
|
existingSessionId,
|
|
36057
36168
|
priorAuthorizedAppIds,
|
|
36169
|
+
incrementalTicket,
|
|
36058
36170
|
// Federated Login
|
|
36059
36171
|
requiresFederatedLogin,
|
|
36060
36172
|
// Consent Flow
|
|
@@ -36180,6 +36292,28 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36180
36292
|
}
|
|
36181
36293
|
}
|
|
36182
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
|
+
}
|
|
36183
36317
|
async checkIfAuthorized() {
|
|
36184
36318
|
}
|
|
36185
36319
|
async prepareAuthorizationRequest() {
|
|
@@ -36191,7 +36325,8 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36191
36325
|
existingSessionId,
|
|
36192
36326
|
priorAuthorizedAppIds,
|
|
36193
36327
|
requiresFederatedLogin,
|
|
36194
|
-
requiresConsent
|
|
36328
|
+
requiresConsent,
|
|
36329
|
+
incrementalTicket
|
|
36195
36330
|
} = this.state;
|
|
36196
36331
|
const { metadata } = this.scope;
|
|
36197
36332
|
if (!validatedRequest) {
|
|
@@ -36204,6 +36339,18 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36204
36339
|
}
|
|
36205
36340
|
const localAuth = auth;
|
|
36206
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
|
+
}
|
|
36207
36354
|
let federatedLogin;
|
|
36208
36355
|
if (requiresFederatedLogin) {
|
|
36209
36356
|
const apps = this.scope.apps.getApps();
|
|
@@ -36251,10 +36398,14 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36251
36398
|
},
|
|
36252
36399
|
state: validatedRequest.state,
|
|
36253
36400
|
resource: validatedRequest.resource,
|
|
36254
|
-
// Progressive/Incremental Authorization Fields
|
|
36255
|
-
|
|
36256
|
-
|
|
36257
|
-
|
|
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,
|
|
36258
36409
|
existingSessionId,
|
|
36259
36410
|
priorAuthorizedAppIds,
|
|
36260
36411
|
// Federated Login State
|
|
@@ -36264,7 +36415,7 @@ var init_oauth_authorize_flow = __esm({
|
|
|
36264
36415
|
});
|
|
36265
36416
|
await localAuth.authorizationStore.storePendingAuthorization(pendingRecord);
|
|
36266
36417
|
this.logger.info(
|
|
36267
|
-
`Pending authorization created: ${pendingRecord.id}${
|
|
36418
|
+
`Pending authorization created: ${pendingRecord.id}${effectiveIncremental ? ` (incremental for app: ${targetAppId})` : ""}${requiresFederatedLogin ? " (federated)" : ""}${requiresConsent ? " (consent enabled)" : ""}`
|
|
36268
36419
|
);
|
|
36269
36420
|
this.state.set("pendingAuthId", pendingRecord.id);
|
|
36270
36421
|
}
|
|
@@ -36831,8 +36982,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36831
36982
|
const email = request.query["email"];
|
|
36832
36983
|
const name42 = request.query["name"];
|
|
36833
36984
|
const loginFields = this.collectLoginFields(request);
|
|
36834
|
-
const isIncremental = request.query["incremental"] === "true";
|
|
36835
|
-
const targetAppId = request.query["app_id"];
|
|
36836
36985
|
const isFederated = request.query["federated"] === "true";
|
|
36837
36986
|
const providersParam = request.query["providers"];
|
|
36838
36987
|
let selectedProviders;
|
|
@@ -36851,8 +37000,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36851
37000
|
pendingAuthId,
|
|
36852
37001
|
email,
|
|
36853
37002
|
name: name42,
|
|
36854
|
-
isIncremental,
|
|
36855
|
-
targetAppId,
|
|
36856
37003
|
isFederated,
|
|
36857
37004
|
selectedProviders,
|
|
36858
37005
|
selectedTools,
|
|
@@ -36860,9 +37007,6 @@ var init_oauth_callback_flow = __esm({
|
|
|
36860
37007
|
loginFields,
|
|
36861
37008
|
csrf
|
|
36862
37009
|
});
|
|
36863
|
-
if (isIncremental) {
|
|
36864
|
-
this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
|
|
36865
|
-
}
|
|
36866
37010
|
if (isFederated) {
|
|
36867
37011
|
this.logger.info(`Federated login callback with ${selectedProviders?.length || 0} selected providers`);
|
|
36868
37012
|
}
|
|
@@ -36871,31 +37015,13 @@ var init_oauth_callback_flow = __esm({
|
|
|
36871
37015
|
}
|
|
36872
37016
|
}
|
|
36873
37017
|
async validatePendingAuth() {
|
|
36874
|
-
const {
|
|
36875
|
-
pendingAuthId,
|
|
36876
|
-
email,
|
|
36877
|
-
name: name42,
|
|
36878
|
-
isIncremental,
|
|
36879
|
-
isFederated,
|
|
36880
|
-
selectedProviders,
|
|
36881
|
-
selectedTools,
|
|
36882
|
-
consentSubmitted,
|
|
36883
|
-
loginFields
|
|
36884
|
-
} = this.state;
|
|
37018
|
+
const { pendingAuthId, email, name: name42, isFederated, selectedProviders, selectedTools, consentSubmitted, loginFields } = this.state;
|
|
36885
37019
|
if (!pendingAuthId) {
|
|
36886
37020
|
this.logger.warn("Missing pending_auth_id in callback");
|
|
36887
37021
|
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Missing pending_auth_id parameter"), 400));
|
|
36888
37022
|
return;
|
|
36889
37023
|
}
|
|
36890
37024
|
const localAuth = this.scope.auth;
|
|
36891
|
-
const localOptions = localAuth.options;
|
|
36892
|
-
const authenticateFn = typeof localOptions.authenticate === "function" ? localOptions.authenticate : void 0;
|
|
36893
|
-
const requireEmail = localOptions.requireEmail ?? true;
|
|
36894
|
-
if (!authenticateFn && !isIncremental && !email && requireEmail) {
|
|
36895
|
-
this.logger.warn("Missing email in callback");
|
|
36896
|
-
this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Email is required"), 400));
|
|
36897
|
-
return;
|
|
36898
|
-
}
|
|
36899
37025
|
const pendingAuth = await localAuth.authorizationStore.getPendingAuthorization(pendingAuthId);
|
|
36900
37026
|
if (!pendingAuth) {
|
|
36901
37027
|
this.logger.warn(`Pending authorization not found or expired: ${pendingAuthId}`);
|
|
@@ -36907,6 +37033,20 @@ var init_oauth_callback_flow = __esm({
|
|
|
36907
37033
|
);
|
|
36908
37034
|
return;
|
|
36909
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
|
+
}
|
|
36910
37050
|
if (pendingAuth.authUiCsrf) {
|
|
36911
37051
|
const submitted = this.state.csrf;
|
|
36912
37052
|
if (!submitted || !timingSafeEqualStr3(pendingAuth.authUiCsrf, submitted)) {
|
|
@@ -36921,9 +37061,11 @@ var init_oauth_callback_flow = __esm({
|
|
|
36921
37061
|
return;
|
|
36922
37062
|
}
|
|
36923
37063
|
let userSub;
|
|
36924
|
-
if (
|
|
37064
|
+
if (isIncremental) {
|
|
37065
|
+
userSub = pendingAuth.incrementalSub;
|
|
37066
|
+
} else if (email) {
|
|
36925
37067
|
userSub = this.generateUserSub(email);
|
|
36926
|
-
} else if (!requireEmail
|
|
37068
|
+
} else if (!requireEmail) {
|
|
36927
37069
|
const anonymousSubject = localOptions.anonymousSubject ?? "local-operator";
|
|
36928
37070
|
userSub = this.generateUserSub(anonymousSubject);
|
|
36929
37071
|
} else {
|
|
@@ -37092,10 +37234,13 @@ var init_oauth_callback_flow = __esm({
|
|
|
37092
37234
|
originalState: pendingAuth.state,
|
|
37093
37235
|
resource: pendingAuth.resource,
|
|
37094
37236
|
userSub,
|
|
37095
|
-
// Progressive/Incremental Authorization from pending record
|
|
37096
|
-
|
|
37097
|
-
|
|
37098
|
-
|
|
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,
|
|
37099
37244
|
existingSessionId: pendingAuth.existingSessionId,
|
|
37100
37245
|
existingAuthorizationId: pendingAuth.existingAuthorizationId,
|
|
37101
37246
|
priorAuthorizedAppIds: pendingAuth.priorAuthorizedAppIds,
|
|
@@ -37699,6 +37844,7 @@ var init_oauth_callback_flow = __esm({
|
|
|
37699
37844
|
"pending_auth_id",
|
|
37700
37845
|
"incremental",
|
|
37701
37846
|
"app_id",
|
|
37847
|
+
"ticket",
|
|
37702
37848
|
"federated",
|
|
37703
37849
|
"providers",
|
|
37704
37850
|
"tools",
|
|
@@ -40256,7 +40402,8 @@ import {
|
|
|
40256
40402
|
MemoryStorageAdapter,
|
|
40257
40403
|
randomBytes as randomBytes6,
|
|
40258
40404
|
randomUUID as randomUUID21,
|
|
40259
|
-
sha256Hex as sha256Hex9
|
|
40405
|
+
sha256Hex as sha256Hex9,
|
|
40406
|
+
StorageNotSupportedError
|
|
40260
40407
|
} from "@frontmcp/utils";
|
|
40261
40408
|
function getDefaultNoAuthSecret() {
|
|
40262
40409
|
if (!defaultNoAuthSecret) {
|
|
@@ -40278,6 +40425,9 @@ function validateAuthorizationIssuer(received, expected) {
|
|
|
40278
40425
|
}
|
|
40279
40426
|
return { ok: true };
|
|
40280
40427
|
}
|
|
40428
|
+
function isUnsupportedOperation(error) {
|
|
40429
|
+
return error instanceof StorageNotSupportedError;
|
|
40430
|
+
}
|
|
40281
40431
|
var defaultNoAuthSecret, RESERVED_JWT_CLAIMS, LocalPrimaryAuth;
|
|
40282
40432
|
var init_instance_local_primary_auth = __esm({
|
|
40283
40433
|
"libs/sdk/src/auth/instances/instance.local-primary-auth.ts"() {
|
|
@@ -40389,6 +40539,17 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40389
40539
|
dcrClientRegistryImpl;
|
|
40390
40540
|
/** Storage adapter backing the persistent stores (kept for disposal). */
|
|
40391
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;
|
|
40392
40553
|
/**
|
|
40393
40554
|
* Per-session encrypted credential vault (Checkpoint 3b). Backs
|
|
40394
40555
|
* `this.credentials` in tools and persists `authenticate()` credentials.
|
|
@@ -40556,6 +40717,81 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40556
40717
|
* (`this.secret`), so resume URLs are framework-signed with the same trust
|
|
40557
40718
|
* root as the access tokens.
|
|
40558
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
|
+
}
|
|
40559
40795
|
async initializeCredentialVault() {
|
|
40560
40796
|
if (isPublicMode(this.options)) {
|
|
40561
40797
|
return;
|
|
@@ -40574,7 +40810,7 @@ var init_instance_local_primary_auth = __esm({
|
|
|
40574
40810
|
pepper,
|
|
40575
40811
|
logger: this.logger.child("SessionCredentialVault")
|
|
40576
40812
|
});
|
|
40577
|
-
const signingSecret =
|
|
40813
|
+
const signingSecret = this.signingSecret;
|
|
40578
40814
|
const basePath = this.issuer;
|
|
40579
40815
|
await this.providers.addDynamicProviders(
|
|
40580
40816
|
createCredentialsProviders({
|
|
@@ -44879,6 +45115,7 @@ var init_execute_workflow_tool = __esm({
|
|
|
44879
45115
|
"libs/sdk/src/workflow/tools/execute-workflow.tool.ts"() {
|
|
44880
45116
|
"use strict";
|
|
44881
45117
|
init_common();
|
|
45118
|
+
init_errors();
|
|
44882
45119
|
ExecuteWorkflowTool = class extends ToolContext {
|
|
44883
45120
|
async execute(input) {
|
|
44884
45121
|
const scope = this.scope;
|
|
@@ -44890,12 +45127,13 @@ var init_execute_workflow_tool = __esm({
|
|
|
44890
45127
|
}
|
|
44891
45128
|
const workflow = workflowRegistry.findByName(input.name);
|
|
44892
45129
|
if (!workflow) {
|
|
44893
|
-
return this.fail(new
|
|
45130
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
44894
45131
|
}
|
|
44895
45132
|
const result = await executionManager.executeWorkflow(workflow, jobRegistry, {
|
|
44896
45133
|
background: input.background,
|
|
44897
45134
|
sessionId: this.authInfo.sessionId,
|
|
44898
45135
|
authInfo: this.authInfo,
|
|
45136
|
+
authoritiesContextBuilder: this.scope.authoritiesContextBuilder,
|
|
44899
45137
|
workflowInput: input.input
|
|
44900
45138
|
});
|
|
44901
45139
|
return result;
|
|
@@ -44920,6 +45158,55 @@ var init_execute_workflow_tool = __esm({
|
|
|
44920
45158
|
}
|
|
44921
45159
|
});
|
|
44922
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
|
+
|
|
44923
45210
|
// libs/sdk/src/workflow/tools/get-workflow-status.tool.ts
|
|
44924
45211
|
import { z as z82 } from "@frontmcp/lazy-zod";
|
|
44925
45212
|
var GetWorkflowStatusTool;
|
|
@@ -44927,6 +45214,7 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44927
45214
|
"libs/sdk/src/workflow/tools/get-workflow-status.tool.ts"() {
|
|
44928
45215
|
"use strict";
|
|
44929
45216
|
init_common();
|
|
45217
|
+
init_principal_utils();
|
|
44930
45218
|
GetWorkflowStatusTool = class extends ToolContext {
|
|
44931
45219
|
async execute(input) {
|
|
44932
45220
|
const scope = this.scope;
|
|
@@ -44935,7 +45223,7 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44935
45223
|
return this.fail(new Error("Jobs system is not enabled"));
|
|
44936
45224
|
}
|
|
44937
45225
|
const record = await executionManager.getStatus(input.runId);
|
|
44938
|
-
if (!record) {
|
|
45226
|
+
if (!record || !this.ownsRun(record)) {
|
|
44939
45227
|
return this.fail(new Error(`Run "${input.runId}" not found`));
|
|
44940
45228
|
}
|
|
44941
45229
|
const workflowRecord = record;
|
|
@@ -44948,6 +45236,23 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44948
45236
|
completedAt: record.completedAt
|
|
44949
45237
|
};
|
|
44950
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
|
+
}
|
|
44951
45256
|
};
|
|
44952
45257
|
GetWorkflowStatusTool = __decorateClass([
|
|
44953
45258
|
FrontMcpTool({
|
|
@@ -44975,6 +45280,53 @@ var init_get_workflow_status_tool = __esm({
|
|
|
44975
45280
|
}
|
|
44976
45281
|
});
|
|
44977
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
|
+
|
|
44978
45330
|
// libs/sdk/src/workflow/tools/list-workflows.tool.ts
|
|
44979
45331
|
import { z as z83 } from "@frontmcp/lazy-zod";
|
|
44980
45332
|
var ListWorkflowsTool;
|
|
@@ -44982,6 +45334,7 @@ var init_list_workflows_tool = __esm({
|
|
|
44982
45334
|
"libs/sdk/src/workflow/tools/list-workflows.tool.ts"() {
|
|
44983
45335
|
"use strict";
|
|
44984
45336
|
init_common();
|
|
45337
|
+
init_job_permission_guard();
|
|
44985
45338
|
ListWorkflowsTool = class extends ToolContext {
|
|
44986
45339
|
async execute(input) {
|
|
44987
45340
|
const scope = this.scope;
|
|
@@ -44993,7 +45346,11 @@ var init_list_workflows_tool = __esm({
|
|
|
44993
45346
|
tags: input.tags,
|
|
44994
45347
|
labels: input.labels
|
|
44995
45348
|
});
|
|
44996
|
-
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) => ({
|
|
44997
45354
|
name: w.name,
|
|
44998
45355
|
description: w.metadata.description,
|
|
44999
45356
|
trigger: w.getTrigger(),
|
|
@@ -45005,6 +45362,12 @@ var init_list_workflows_tool = __esm({
|
|
|
45005
45362
|
count: mapped.length
|
|
45006
45363
|
};
|
|
45007
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
|
+
}
|
|
45008
45371
|
};
|
|
45009
45372
|
ListWorkflowsTool = __decorateClass([
|
|
45010
45373
|
FrontMcpTool({
|
|
@@ -45040,6 +45403,7 @@ var init_register_workflow_tool = __esm({
|
|
|
45040
45403
|
"libs/sdk/src/workflow/tools/register-workflow.tool.ts"() {
|
|
45041
45404
|
"use strict";
|
|
45042
45405
|
init_common();
|
|
45406
|
+
init_errors();
|
|
45043
45407
|
RegisterWorkflowTool = class extends ToolContext {
|
|
45044
45408
|
async execute(input) {
|
|
45045
45409
|
const scope = this.scope;
|
|
@@ -45047,6 +45411,9 @@ var init_register_workflow_tool = __esm({
|
|
|
45047
45411
|
if (!workflowRegistry) {
|
|
45048
45412
|
return this.fail(new Error("Workflows system is not enabled"));
|
|
45049
45413
|
}
|
|
45414
|
+
if (!this.dynamicRegistrationEnabled()) {
|
|
45415
|
+
return this.fail(new DynamicJobRegistrationDisabledError("workflow"));
|
|
45416
|
+
}
|
|
45050
45417
|
const existing = workflowRegistry.findByName(input.name);
|
|
45051
45418
|
if (existing) {
|
|
45052
45419
|
return this.fail(new Error(`Workflow "${input.name}" already exists`));
|
|
@@ -45068,6 +45435,10 @@ var init_register_workflow_tool = __esm({
|
|
|
45068
45435
|
});
|
|
45069
45436
|
return { success: true, workflowId };
|
|
45070
45437
|
}
|
|
45438
|
+
dynamicRegistrationEnabled() {
|
|
45439
|
+
const jobs = this.scope.metadata?.jobs;
|
|
45440
|
+
return jobs?.allowDynamicRegistration === true;
|
|
45441
|
+
}
|
|
45071
45442
|
};
|
|
45072
45443
|
RegisterWorkflowTool = __decorateClass([
|
|
45073
45444
|
FrontMcpTool({
|
|
@@ -45106,6 +45477,8 @@ var init_remove_workflow_tool = __esm({
|
|
|
45106
45477
|
"libs/sdk/src/workflow/tools/remove-workflow.tool.ts"() {
|
|
45107
45478
|
"use strict";
|
|
45108
45479
|
init_common();
|
|
45480
|
+
init_errors();
|
|
45481
|
+
init_job_permission_guard();
|
|
45109
45482
|
RemoveWorkflowTool = class extends ToolContext {
|
|
45110
45483
|
async execute(input) {
|
|
45111
45484
|
const scope = this.scope;
|
|
@@ -45115,7 +45488,16 @@ var init_remove_workflow_tool = __esm({
|
|
|
45115
45488
|
}
|
|
45116
45489
|
const workflow = workflowRegistry.findByName(input.name);
|
|
45117
45490
|
if (!workflow) {
|
|
45118
|
-
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));
|
|
45119
45501
|
}
|
|
45120
45502
|
if (!workflow.isDynamic()) {
|
|
45121
45503
|
return this.fail(new Error(`Workflow "${input.name}" is not dynamic and cannot be removed`));
|
|
@@ -45724,7 +46106,10 @@ var JobExecutionManager;
|
|
|
45724
46106
|
var init_job_execution_manager = __esm({
|
|
45725
46107
|
"libs/sdk/src/job/execution/job-execution.manager.ts"() {
|
|
45726
46108
|
"use strict";
|
|
46109
|
+
init_principal_utils();
|
|
46110
|
+
init_errors();
|
|
45727
46111
|
init_workflow_engine();
|
|
46112
|
+
init_job_permission_guard();
|
|
45728
46113
|
JobExecutionManager = class {
|
|
45729
46114
|
stateStore;
|
|
45730
46115
|
logger;
|
|
@@ -45738,6 +46123,7 @@ var init_job_execution_manager = __esm({
|
|
|
45738
46123
|
* Execute a job inline (synchronous) or in background.
|
|
45739
46124
|
*/
|
|
45740
46125
|
async executeJob(job, input, opts = {}) {
|
|
46126
|
+
await this.assertMayExecute(job.metadata.permissions, job.name, opts);
|
|
45741
46127
|
const runId = randomUUID23();
|
|
45742
46128
|
const retryConfig = job.metadata.retry ?? {};
|
|
45743
46129
|
const maxAttempts = retryConfig.maxAttempts ?? 1;
|
|
@@ -45746,6 +46132,7 @@ var init_job_execution_manager = __esm({
|
|
|
45746
46132
|
jobId: job.metadata.id ?? job.name,
|
|
45747
46133
|
jobName: job.name,
|
|
45748
46134
|
sessionId: opts.sessionId,
|
|
46135
|
+
ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
|
|
45749
46136
|
state: "pending",
|
|
45750
46137
|
input,
|
|
45751
46138
|
startedAt: Date.now(),
|
|
@@ -45776,6 +46163,7 @@ var init_job_execution_manager = __esm({
|
|
|
45776
46163
|
* Execute a workflow inline or in background.
|
|
45777
46164
|
*/
|
|
45778
46165
|
async executeWorkflow(workflow, jobRegistry, opts = {}) {
|
|
46166
|
+
await this.assertMayExecute(workflow.metadata.permissions, workflow.name, opts);
|
|
45779
46167
|
const runId = randomUUID23();
|
|
45780
46168
|
const runRecord = {
|
|
45781
46169
|
runId,
|
|
@@ -45783,6 +46171,7 @@ var init_job_execution_manager = __esm({
|
|
|
45783
46171
|
jobName: workflow.name,
|
|
45784
46172
|
workflowName: workflow.name,
|
|
45785
46173
|
sessionId: opts.sessionId,
|
|
46174
|
+
ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
|
|
45786
46175
|
state: "pending",
|
|
45787
46176
|
input: opts.workflowInput,
|
|
45788
46177
|
startedAt: Date.now(),
|
|
@@ -45810,6 +46199,25 @@ var init_job_execution_manager = __esm({
|
|
|
45810
46199
|
}
|
|
45811
46200
|
return this.executeWorkflowInline(workflow, jobRegistry, runId, opts);
|
|
45812
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
|
+
}
|
|
45813
46221
|
/**
|
|
45814
46222
|
* Get execution status.
|
|
45815
46223
|
*/
|
|
@@ -46834,6 +47242,7 @@ var init_execute_job_tool = __esm({
|
|
|
46834
47242
|
"libs/sdk/src/job/tools/execute-job.tool.ts"() {
|
|
46835
47243
|
"use strict";
|
|
46836
47244
|
init_common();
|
|
47245
|
+
init_errors();
|
|
46837
47246
|
ExecuteJobTool = class extends ToolContext {
|
|
46838
47247
|
async execute(input) {
|
|
46839
47248
|
const scope = this.scope;
|
|
@@ -46844,12 +47253,13 @@ var init_execute_job_tool = __esm({
|
|
|
46844
47253
|
}
|
|
46845
47254
|
const job = jobRegistry.findByName(input.name);
|
|
46846
47255
|
if (!job) {
|
|
46847
|
-
return this.fail(new
|
|
47256
|
+
return this.fail(new JobNotAuthorizedError(input.name));
|
|
46848
47257
|
}
|
|
46849
47258
|
const result = await executionManager.executeJob(job, input.input ?? {}, {
|
|
46850
47259
|
background: input.background,
|
|
46851
47260
|
sessionId: this.authInfo.sessionId,
|
|
46852
|
-
authInfo: this.authInfo
|
|
47261
|
+
authInfo: this.authInfo,
|
|
47262
|
+
authoritiesContextBuilder: this.scope.authoritiesContextBuilder
|
|
46853
47263
|
});
|
|
46854
47264
|
return result;
|
|
46855
47265
|
}
|
|
@@ -46881,6 +47291,7 @@ var init_get_job_status_tool = __esm({
|
|
|
46881
47291
|
"libs/sdk/src/job/tools/get-job-status.tool.ts"() {
|
|
46882
47292
|
"use strict";
|
|
46883
47293
|
init_common();
|
|
47294
|
+
init_principal_utils();
|
|
46884
47295
|
init_errors();
|
|
46885
47296
|
GetJobStatusTool = class extends ToolContext {
|
|
46886
47297
|
async execute(input) {
|
|
@@ -46890,7 +47301,7 @@ var init_get_job_status_tool = __esm({
|
|
|
46890
47301
|
return this.fail(new GenericServerError("Jobs system is not enabled"));
|
|
46891
47302
|
}
|
|
46892
47303
|
const record = await executionManager.getStatus(input.runId);
|
|
46893
|
-
if (!record) {
|
|
47304
|
+
if (!record || !this.ownsRun(record)) {
|
|
46894
47305
|
return this.fail(new InvalidInputError(`Run "${input.runId}" not found`));
|
|
46895
47306
|
}
|
|
46896
47307
|
return {
|
|
@@ -46905,6 +47316,23 @@ var init_get_job_status_tool = __esm({
|
|
|
46905
47316
|
logs: record.logs
|
|
46906
47317
|
};
|
|
46907
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
|
+
}
|
|
46908
47336
|
};
|
|
46909
47337
|
GetJobStatusTool = __decorateClass([
|
|
46910
47338
|
FrontMcpTool({
|
|
@@ -46939,6 +47367,7 @@ var init_list_jobs_tool = __esm({
|
|
|
46939
47367
|
"libs/sdk/src/job/tools/list-jobs.tool.ts"() {
|
|
46940
47368
|
"use strict";
|
|
46941
47369
|
init_common();
|
|
47370
|
+
init_job_permission_guard();
|
|
46942
47371
|
ListJobsTool = class extends ToolContext {
|
|
46943
47372
|
async execute(input) {
|
|
46944
47373
|
const jobRegistry = this.scope.jobs;
|
|
@@ -46949,7 +47378,11 @@ var init_list_jobs_tool = __esm({
|
|
|
46949
47378
|
tags: input.tags,
|
|
46950
47379
|
labels: input.labels
|
|
46951
47380
|
});
|
|
46952
|
-
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) => ({
|
|
46953
47386
|
name: j.name,
|
|
46954
47387
|
description: j.metadata.description,
|
|
46955
47388
|
tags: j.getTags(),
|
|
@@ -46961,6 +47394,17 @@ var init_list_jobs_tool = __esm({
|
|
|
46961
47394
|
count: mapped.length
|
|
46962
47395
|
};
|
|
46963
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
|
+
}
|
|
46964
47408
|
};
|
|
46965
47409
|
ListJobsTool = __decorateClass([
|
|
46966
47410
|
FrontMcpTool({
|
|
@@ -46996,6 +47440,7 @@ var init_register_job_tool = __esm({
|
|
|
46996
47440
|
"libs/sdk/src/job/tools/register-job.tool.ts"() {
|
|
46997
47441
|
"use strict";
|
|
46998
47442
|
init_common();
|
|
47443
|
+
init_errors();
|
|
46999
47444
|
RegisterJobTool = class extends ToolContext {
|
|
47000
47445
|
async execute(input) {
|
|
47001
47446
|
const scope = this.scope;
|
|
@@ -47003,6 +47448,9 @@ var init_register_job_tool = __esm({
|
|
|
47003
47448
|
if (!jobRegistry) {
|
|
47004
47449
|
return this.fail(new Error("Jobs system is not enabled"));
|
|
47005
47450
|
}
|
|
47451
|
+
if (!this.dynamicRegistrationEnabled()) {
|
|
47452
|
+
return this.fail(new DynamicJobRegistrationDisabledError("job"));
|
|
47453
|
+
}
|
|
47006
47454
|
const existing = jobRegistry.findByName(input.name);
|
|
47007
47455
|
if (existing) {
|
|
47008
47456
|
return this.fail(new Error(`Job "${input.name}" already exists`));
|
|
@@ -47025,6 +47473,10 @@ var init_register_job_tool = __esm({
|
|
|
47025
47473
|
});
|
|
47026
47474
|
return { success: true, jobId };
|
|
47027
47475
|
}
|
|
47476
|
+
dynamicRegistrationEnabled() {
|
|
47477
|
+
const jobs = this.scope.metadata?.jobs;
|
|
47478
|
+
return jobs?.allowDynamicRegistration === true;
|
|
47479
|
+
}
|
|
47028
47480
|
};
|
|
47029
47481
|
RegisterJobTool = __decorateClass([
|
|
47030
47482
|
FrontMcpTool({
|
|
@@ -47055,6 +47507,8 @@ var init_remove_job_tool = __esm({
|
|
|
47055
47507
|
"libs/sdk/src/job/tools/remove-job.tool.ts"() {
|
|
47056
47508
|
"use strict";
|
|
47057
47509
|
init_common();
|
|
47510
|
+
init_errors();
|
|
47511
|
+
init_job_permission_guard();
|
|
47058
47512
|
RemoveJobTool = class extends ToolContext {
|
|
47059
47513
|
async execute(input) {
|
|
47060
47514
|
const scope = this.scope;
|
|
@@ -47064,7 +47518,16 @@ var init_remove_job_tool = __esm({
|
|
|
47064
47518
|
}
|
|
47065
47519
|
const job = jobRegistry.findByName(input.name);
|
|
47066
47520
|
if (!job) {
|
|
47067
|
-
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));
|
|
47068
47531
|
}
|
|
47069
47532
|
if (!job.isDynamic()) {
|
|
47070
47533
|
return this.fail(new Error(`Job "${input.name}" is not a dynamic job and cannot be removed`));
|
|
@@ -47125,14 +47588,18 @@ async function registerJobCapabilities(args) {
|
|
|
47125
47588
|
ListJobsTool,
|
|
47126
47589
|
ExecuteJobTool,
|
|
47127
47590
|
GetJobStatusTool,
|
|
47128
|
-
RegisterJobTool,
|
|
47129
47591
|
RemoveJobTool,
|
|
47130
47592
|
ListWorkflowsTool,
|
|
47131
47593
|
ExecuteWorkflowTool,
|
|
47132
47594
|
GetWorkflowStatusTool,
|
|
47133
|
-
RegisterWorkflowTool,
|
|
47134
47595
|
RemoveWorkflowTool
|
|
47135
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
|
+
}
|
|
47136
47603
|
logger.info(
|
|
47137
47604
|
`Jobs initialized: ${jobRegistry.getJobs().length} jobs, ${workflowRegistry.getWorkflows().length} workflows`
|
|
47138
47605
|
);
|
|
@@ -51963,6 +52430,7 @@ var init_utils2 = __esm({
|
|
|
51963
52430
|
init_caller_dir_utils();
|
|
51964
52431
|
init_decide_request_intent_utils();
|
|
51965
52432
|
init_path_utils();
|
|
52433
|
+
init_principal_utils();
|
|
51966
52434
|
init_global_config_utils();
|
|
51967
52435
|
init_primitive_filter();
|
|
51968
52436
|
init_validate_remote_url();
|
|
@@ -52908,9 +53376,24 @@ var init_redis_event_store = __esm({
|
|
|
52908
53376
|
await client.expire(streamKey, this.ttlSeconds);
|
|
52909
53377
|
return `${streamId}:${redisId}`;
|
|
52910
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
|
+
}
|
|
52911
53390
|
async replayEventsAfter(lastEventId, { send }) {
|
|
53391
|
+
const parsed = this.parseEventId(lastEventId);
|
|
53392
|
+
if (!parsed) {
|
|
53393
|
+
return "default-stream";
|
|
53394
|
+
}
|
|
52912
53395
|
const client = await this.getClient();
|
|
52913
|
-
const [streamId, redisId] =
|
|
53396
|
+
const [streamId, redisId] = parsed;
|
|
52914
53397
|
const streamKey = `${this.keyPrefix}${streamId}`;
|
|
52915
53398
|
const entries = await client.xrange(streamKey, `(${redisId}`, "+");
|
|
52916
53399
|
for (const [id, fields] of entries) {
|
|
@@ -52929,16 +53412,32 @@ var init_redis_event_store = __esm({
|
|
|
52929
53412
|
* Event IDs have format: `{streamId}:{redisStreamEntryId}`
|
|
52930
53413
|
* Redis stream entry IDs have format: `{timestamp}-{sequence}`
|
|
52931
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
|
+
*
|
|
52932
53429
|
* @example
|
|
52933
53430
|
* 'my-stream:1234567890123-0' -> ['my-stream', '1234567890123-0']
|
|
52934
53431
|
*/
|
|
52935
53432
|
parseEventId(eventId) {
|
|
52936
|
-
const
|
|
52937
|
-
|
|
52938
|
-
|
|
52939
|
-
|
|
52940
|
-
|
|
52941
|
-
|
|
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];
|
|
52942
53441
|
}
|
|
52943
53442
|
/**
|
|
52944
53443
|
* Close the Redis client connection.
|
|
@@ -52984,6 +53483,16 @@ var init_memory_event_store = __esm({
|
|
|
52984
53483
|
}
|
|
52985
53484
|
return id;
|
|
52986
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
|
+
}
|
|
52987
53496
|
async replayEventsAfter(lastEventId, { send }) {
|
|
52988
53497
|
const meta = this.index.get(lastEventId);
|
|
52989
53498
|
if (!meta) {
|
|
@@ -53135,6 +53644,50 @@ var init_event_store_factory = __esm({
|
|
|
53135
53644
|
}
|
|
53136
53645
|
});
|
|
53137
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
|
+
|
|
53138
53691
|
// libs/sdk/src/transport/event-stores/index.ts
|
|
53139
53692
|
var init_event_stores = __esm({
|
|
53140
53693
|
"libs/sdk/src/transport/event-stores/index.ts"() {
|
|
@@ -54804,6 +55357,61 @@ var init_transport_error = __esm({
|
|
|
54804
55357
|
}
|
|
54805
55358
|
});
|
|
54806
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
|
+
|
|
54807
55415
|
// libs/sdk/src/transport/adapters/base-sse-transport.ts
|
|
54808
55416
|
import contentType from "content-type";
|
|
54809
55417
|
import getRawBody from "raw-body";
|
|
@@ -54816,6 +55424,7 @@ var init_base_sse_transport = __esm({
|
|
|
54816
55424
|
"libs/sdk/src/transport/adapters/base-sse-transport.ts"() {
|
|
54817
55425
|
"use strict";
|
|
54818
55426
|
init_transport_errors();
|
|
55427
|
+
init_host_validation();
|
|
54819
55428
|
MAXIMUM_MESSAGE_SIZE = "4mb";
|
|
54820
55429
|
SSEServerTransport = class {
|
|
54821
55430
|
/**
|
|
@@ -54839,25 +55448,31 @@ var init_base_sse_transport = __esm({
|
|
|
54839
55448
|
onmessage;
|
|
54840
55449
|
/**
|
|
54841
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
|
+
*
|
|
54842
55458
|
* @returns Error message if validation fails, undefined if validation passes.
|
|
54843
55459
|
*/
|
|
54844
55460
|
validateRequestHeaders(req) {
|
|
54845
55461
|
if (!this._options.enableDnsRebindingProtection) {
|
|
54846
55462
|
return void 0;
|
|
54847
55463
|
}
|
|
54848
|
-
|
|
54849
|
-
|
|
54850
|
-
|
|
54851
|
-
|
|
54852
|
-
|
|
54853
|
-
|
|
54854
|
-
|
|
54855
|
-
|
|
54856
|
-
|
|
54857
|
-
|
|
54858
|
-
|
|
54859
|
-
|
|
54860
|
-
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;
|
|
54861
55476
|
}
|
|
54862
55477
|
/**
|
|
54863
55478
|
* Handles the initial SSE connection request.
|
|
@@ -55359,7 +55974,7 @@ var init_transport_local_adapter = __esm({
|
|
|
55359
55974
|
}
|
|
55360
55975
|
}
|
|
55361
55976
|
ensureAuthInfo(req, transport) {
|
|
55362
|
-
const { token, user, session } = req[ServerRequestTokens.auth];
|
|
55977
|
+
const { token, user, session, scopes, claims } = req[ServerRequestTokens.auth];
|
|
55363
55978
|
if (!session?.id) {
|
|
55364
55979
|
throw new UnauthorizedError(
|
|
55365
55980
|
"Session could not be reconstructed for the provided token. Re-authenticate to start a new session.",
|
|
@@ -55388,7 +56003,16 @@ var init_transport_local_adapter = __esm({
|
|
|
55388
56003
|
user,
|
|
55389
56004
|
sessionId,
|
|
55390
56005
|
sessionIdPayload: sessionPayload,
|
|
55391
|
-
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) } : {},
|
|
55392
56016
|
clientId: user.sub ?? "",
|
|
55393
56017
|
transport
|
|
55394
56018
|
};
|
|
@@ -55869,6 +56493,7 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
55869
56493
|
"libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts"() {
|
|
55870
56494
|
"use strict";
|
|
55871
56495
|
init_errors();
|
|
56496
|
+
init_session_scoped_event_store();
|
|
55872
56497
|
init_transport_error();
|
|
55873
56498
|
init_streamable_http_transport();
|
|
55874
56499
|
init_transport_local_adapter();
|
|
@@ -55878,7 +56503,8 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
55878
56503
|
TransportStreamableHttpAdapter = class extends LocalTransportAdapter {
|
|
55879
56504
|
createTransport(sessionId, response) {
|
|
55880
56505
|
const sessionIdGenerator = resolveSessionIdGenerator(this.key.type, sessionId);
|
|
55881
|
-
const
|
|
56506
|
+
const sharedEventStore = this.scope.eventStore;
|
|
56507
|
+
const eventStore = sharedEventStore ? createSessionScopedEventStore(sharedEventStore, sessionId) : void 0;
|
|
55882
56508
|
const isStateless = this.key.type === "stateless-http";
|
|
55883
56509
|
return new RecreateableStreamableHTTPServerTransport({
|
|
55884
56510
|
sessionIdGenerator,
|
|
@@ -59770,7 +60396,13 @@ function normalizeAppScope(appItem, metadata) {
|
|
|
59770
60396
|
...metadata,
|
|
59771
60397
|
id: scopeId,
|
|
59772
60398
|
apps: [appItem],
|
|
59773
|
-
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,
|
|
59774
60406
|
// For a splitByApp scope, auth (and any auth.ui paths) come from `@App`, so
|
|
59775
60407
|
// anchor on the App's captured source dir. Fall back to the server's.
|
|
59776
60408
|
// `__sourceDir` lives on LocalAppMetadata only; read it defensively.
|
|
@@ -60032,6 +60664,11 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
60032
60664
|
const entryPaths = new Set((Array.isArray(rawEntry) ? rawEntry : [rawEntry]).map(normalizePath));
|
|
60033
60665
|
const cors2 = options.cors ?? mapHttpCors(httpConfig?.cors, scope);
|
|
60034
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
|
+
});
|
|
60035
60672
|
const corsHeadersFor = (request) => {
|
|
60036
60673
|
if (!corsEnabled) return {};
|
|
60037
60674
|
const reqOrigin = request.headers.get("origin") ?? "";
|
|
@@ -60062,6 +60699,19 @@ function createWebFetchHandler(scope, options = {}) {
|
|
|
60062
60699
|
};
|
|
60063
60700
|
return async function handle(request, ctx, env) {
|
|
60064
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
|
+
}
|
|
60065
60715
|
if (corsEnabled && request.method === "OPTIONS") {
|
|
60066
60716
|
return new Response(null, { status: 204, headers: corsHeadersFor(request) });
|
|
60067
60717
|
}
|
|
@@ -60195,6 +60845,7 @@ var init_web_fetch_handler = __esm({
|
|
|
60195
60845
|
init_server_tokens();
|
|
60196
60846
|
init_path_utils();
|
|
60197
60847
|
init_errors();
|
|
60848
|
+
init_host_validation();
|
|
60198
60849
|
init_web_response_renderer();
|
|
60199
60850
|
}
|
|
60200
60851
|
});
|
|
@@ -60231,26 +60882,20 @@ function auditSecurityDefaults(config, isProduction5) {
|
|
|
60231
60882
|
return findings;
|
|
60232
60883
|
}
|
|
60233
60884
|
const strict = config.security?.strict === true;
|
|
60234
|
-
|
|
60885
|
+
const corsOrigin = config.cors === false ? void 0 : config.cors?.origin;
|
|
60886
|
+
if (corsOrigin === void 0 || corsOrigin === false) {
|
|
60235
60887
|
findings.push({
|
|
60236
|
-
level: "
|
|
60237
|
-
code: "
|
|
60238
|
-
message: "CORS
|
|
60239
|
-
recommendation: "Set explicit cors.origin to restrict allowed origins in production."
|
|
60888
|
+
level: "info",
|
|
60889
|
+
code: "CORS_DISABLED",
|
|
60890
|
+
message: "No CORS headers are sent. Cross-origin requests still reach the server \u2014 a browser just will not let the calling page read the response. CORS is not server-side access control."
|
|
60240
60891
|
});
|
|
60241
|
-
} else if (
|
|
60892
|
+
} else if (corsOrigin === true) {
|
|
60242
60893
|
findings.push({
|
|
60243
60894
|
level: "warn",
|
|
60244
60895
|
code: "CORS_ORIGIN_TRUE",
|
|
60245
60896
|
message: "CORS origin=true allows all origins to make cross-origin requests.",
|
|
60246
60897
|
recommendation: "Set cors.origin to specific allowed origins."
|
|
60247
60898
|
});
|
|
60248
|
-
} else if (config.cors === false) {
|
|
60249
|
-
findings.push({
|
|
60250
|
-
level: "info",
|
|
60251
|
-
code: "CORS_DISABLED",
|
|
60252
|
-
message: "CORS is disabled. Cross-origin requests will be blocked by browsers."
|
|
60253
|
-
});
|
|
60254
60899
|
} else {
|
|
60255
60900
|
findings.push({
|
|
60256
60901
|
level: "info",
|
|
@@ -60258,7 +60903,7 @@ function auditSecurityDefaults(config, isProduction5) {
|
|
|
60258
60903
|
message: "CORS is explicitly configured."
|
|
60259
60904
|
});
|
|
60260
60905
|
}
|
|
60261
|
-
const bindAddress = config.resolvedBindAddress ?? "
|
|
60906
|
+
const bindAddress = config.resolvedBindAddress ?? "127.0.0.1";
|
|
60262
60907
|
if (bindAddress === "0.0.0.0" || bindAddress === "::") {
|
|
60263
60908
|
if (config.deploymentMode !== "distributed") {
|
|
60264
60909
|
findings.push({
|
|
@@ -60300,7 +60945,7 @@ function auditSecurityDefaults(config, isProduction5) {
|
|
|
60300
60945
|
findings.push({
|
|
60301
60946
|
level: "info",
|
|
60302
60947
|
code: "STRICT_MODE_ENABLED",
|
|
60303
|
-
message: "Strict security mode is enabled: loopback binding
|
|
60948
|
+
message: config.deploymentMode === "distributed" ? "Strict security mode is enabled: DNS rebinding protection. Binding is unchanged for a distributed deployment \u2014 it still listens on all interfaces so peers can reach it." : "Strict security mode is enabled: loopback binding and DNS rebinding protection."
|
|
60304
60949
|
});
|
|
60305
60950
|
} else {
|
|
60306
60951
|
findings.push({
|
|
@@ -60323,16 +60968,24 @@ function logSecurityFindings(findings, logger) {
|
|
|
60323
60968
|
}
|
|
60324
60969
|
}
|
|
60325
60970
|
}
|
|
60971
|
+
function toBindAddress(value) {
|
|
60972
|
+
if (value === "loopback") return "127.0.0.1";
|
|
60973
|
+
if (value === "all") return "0.0.0.0";
|
|
60974
|
+
return value;
|
|
60975
|
+
}
|
|
60326
60976
|
function resolveBindAddress(security, deploymentMode) {
|
|
60327
60977
|
if (security?.bindAddress) {
|
|
60328
|
-
|
|
60329
|
-
|
|
60330
|
-
|
|
60978
|
+
return toBindAddress(security.bindAddress);
|
|
60979
|
+
}
|
|
60980
|
+
const envBindAddress = process.env["FRONTMCP_BIND_ADDRESS"]?.trim();
|
|
60981
|
+
if (envBindAddress) {
|
|
60982
|
+
return toBindAddress(envBindAddress);
|
|
60331
60983
|
}
|
|
60332
60984
|
if (security?.strict) {
|
|
60333
60985
|
return deploymentMode === "distributed" ? "0.0.0.0" : "127.0.0.1";
|
|
60334
60986
|
}
|
|
60335
|
-
return "0.0.0.0";
|
|
60987
|
+
if (deploymentMode === "distributed") return "0.0.0.0";
|
|
60988
|
+
return "127.0.0.1";
|
|
60336
60989
|
}
|
|
60337
60990
|
var init_security_audit = __esm({
|
|
60338
60991
|
"libs/sdk/src/server/security/security-audit.ts"() {
|
|
@@ -60345,39 +60998,22 @@ function createHostValidationMiddleware(options) {
|
|
|
60345
60998
|
if (!options.enabled) {
|
|
60346
60999
|
return (_req, _res, next) => next();
|
|
60347
61000
|
}
|
|
60348
|
-
const
|
|
60349
|
-
|
|
61001
|
+
const compiled = compileHostValidation({
|
|
61002
|
+
allowedHosts: options.allowedHosts,
|
|
61003
|
+
allowedOrigins: options.allowedOrigins
|
|
61004
|
+
});
|
|
60350
61005
|
return (req, res, next) => {
|
|
60351
|
-
|
|
60352
|
-
|
|
60353
|
-
|
|
60354
|
-
|
|
60355
|
-
|
|
60356
|
-
|
|
60357
|
-
|
|
60358
|
-
|
|
60359
|
-
|
|
60360
|
-
|
|
60361
|
-
|
|
60362
|
-
const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
|
|
60363
|
-
if (candidates.some((h2) => !allowedHostsSet.has(h2))) {
|
|
60364
|
-
res.status(403).json({
|
|
60365
|
-
error: "Forbidden",
|
|
60366
|
-
message: "Invalid X-Forwarded-Host header"
|
|
60367
|
-
});
|
|
60368
|
-
return;
|
|
60369
|
-
}
|
|
60370
|
-
}
|
|
60371
|
-
}
|
|
60372
|
-
if (allowedOriginsSet) {
|
|
60373
|
-
const origin = req.headers?.["origin"];
|
|
60374
|
-
if (origin && !allowedOriginsSet.has(origin)) {
|
|
60375
|
-
res.status(403).json({
|
|
60376
|
-
error: "Forbidden",
|
|
60377
|
-
message: "Invalid Origin header"
|
|
60378
|
-
});
|
|
60379
|
-
return;
|
|
60380
|
-
}
|
|
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;
|
|
60381
61017
|
}
|
|
60382
61018
|
next();
|
|
60383
61019
|
};
|
|
@@ -60385,6 +61021,82 @@ function createHostValidationMiddleware(options) {
|
|
|
60385
61021
|
var init_host_validation_middleware = __esm({
|
|
60386
61022
|
"libs/sdk/src/server/middleware/host-validation.middleware.ts"() {
|
|
60387
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]"];
|
|
60388
61100
|
}
|
|
60389
61101
|
});
|
|
60390
61102
|
|
|
@@ -60410,12 +61122,17 @@ var init_express_host_adapter = __esm({
|
|
|
60410
61122
|
"use strict";
|
|
60411
61123
|
init_mcp_error();
|
|
60412
61124
|
init_host_validation_middleware();
|
|
61125
|
+
init_resolve_allowed_hosts();
|
|
60413
61126
|
init_base_host_adapter();
|
|
60414
61127
|
DEFAULT_EXPRESS_BODY_LIMIT = "4mb";
|
|
60415
61128
|
ExpressHostAdapter = class extends HostServerAdapter {
|
|
60416
61129
|
app = express();
|
|
60417
61130
|
router = express.Router();
|
|
60418
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;
|
|
60419
61136
|
constructor(options) {
|
|
60420
61137
|
super();
|
|
60421
61138
|
const corsOptions = options?.cors;
|
|
@@ -60429,6 +61146,7 @@ var init_express_host_adapter = __esm({
|
|
|
60429
61146
|
})
|
|
60430
61147
|
);
|
|
60431
61148
|
}
|
|
61149
|
+
this.installHostValidation(options);
|
|
60432
61150
|
const jsonLimit = options?.bodyLimit ?? DEFAULT_EXPRESS_BODY_LIMIT;
|
|
60433
61151
|
const formLimit = options?.urlencodedLimit ?? jsonLimit;
|
|
60434
61152
|
const headerContains = (req, needle) => (req.headers["content-type"] ?? "").toLowerCase().includes(needle);
|
|
@@ -60449,23 +61167,6 @@ var init_express_host_adapter = __esm({
|
|
|
60449
61167
|
}
|
|
60450
61168
|
next(err);
|
|
60451
61169
|
});
|
|
60452
|
-
const securityOpts = options?.security;
|
|
60453
|
-
if (securityOpts?.dnsRebindingProtection?.enabled || securityOpts?.strict) {
|
|
60454
|
-
const allowedHosts = securityOpts.dnsRebindingProtection?.allowedHosts;
|
|
60455
|
-
const allowedOrigins = securityOpts.dnsRebindingProtection?.allowedOrigins;
|
|
60456
|
-
const effectiveHosts = allowedHosts ?? (securityOpts.strict ? ["localhost", "127.0.0.1"] : void 0);
|
|
60457
|
-
if (!effectiveHosts?.length && !allowedOrigins?.length) {
|
|
60458
|
-
throw new Error(
|
|
60459
|
-
"security.dnsRebindingProtection is enabled but no allowedHosts or allowedOrigins are configured. Provide at least one allowedHosts entry or disable dnsRebindingProtection."
|
|
60460
|
-
);
|
|
60461
|
-
}
|
|
60462
|
-
const hostValidation = createHostValidationMiddleware({
|
|
60463
|
-
enabled: true,
|
|
60464
|
-
allowedHosts: effectiveHosts,
|
|
60465
|
-
allowedOrigins
|
|
60466
|
-
});
|
|
60467
|
-
this.app.use(hostValidation);
|
|
60468
|
-
}
|
|
60469
61170
|
this.app.use((req, res, next) => {
|
|
60470
61171
|
if (corsEnabled) {
|
|
60471
61172
|
res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate, Mcp-Session-Id");
|
|
@@ -60475,6 +61176,64 @@ var init_express_host_adapter = __esm({
|
|
|
60475
61176
|
next();
|
|
60476
61177
|
});
|
|
60477
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
|
+
}
|
|
60478
61237
|
registerRoute(method, path3, handler) {
|
|
60479
61238
|
this.router[method.toLowerCase()](path3, this.enhancedHandler(handler));
|
|
60480
61239
|
}
|
|
@@ -60507,6 +61266,7 @@ var init_express_host_adapter = __esm({
|
|
|
60507
61266
|
return this.app;
|
|
60508
61267
|
}
|
|
60509
61268
|
async start(portOrSocketPath, bindAddress) {
|
|
61269
|
+
this.enableDerivedHostValidation(portOrSocketPath, bindAddress);
|
|
60510
61270
|
this.prepare();
|
|
60511
61271
|
const server = http.createServer(this.app);
|
|
60512
61272
|
server.requestTimeout = 0;
|
|
@@ -60572,7 +61332,7 @@ var init_server_instance = __esm({
|
|
|
60572
61332
|
init_health2();
|
|
60573
61333
|
init_metrics2();
|
|
60574
61334
|
init_security_audit();
|
|
60575
|
-
DEFAULT_CORS =
|
|
61335
|
+
DEFAULT_CORS = void 0;
|
|
60576
61336
|
FrontMcpServerInstance = class extends FrontMcpServer {
|
|
60577
61337
|
config;
|
|
60578
61338
|
host;
|
|
@@ -63365,49 +64125,7 @@ init_job_registry();
|
|
|
63365
64125
|
init_job_instance();
|
|
63366
64126
|
init_job_events();
|
|
63367
64127
|
init_job_utils();
|
|
63368
|
-
|
|
63369
|
-
// libs/sdk/src/job/job-permission.guard.ts
|
|
63370
|
-
var JobPermissionGuard = class _JobPermissionGuard {
|
|
63371
|
-
/**
|
|
63372
|
-
* Check if the given auth info is allowed to perform the action.
|
|
63373
|
-
* Returns true if no permissions are defined (permissive by default).
|
|
63374
|
-
*/
|
|
63375
|
-
static async check(permissions, action, authInfo) {
|
|
63376
|
-
if (!permissions || permissions.length === 0) {
|
|
63377
|
-
return true;
|
|
63378
|
-
}
|
|
63379
|
-
const relevant = permissions.filter((p) => p.action === action);
|
|
63380
|
-
if (relevant.length === 0) {
|
|
63381
|
-
return true;
|
|
63382
|
-
}
|
|
63383
|
-
for (const perm of relevant) {
|
|
63384
|
-
const passed = await _JobPermissionGuard.checkSingle(perm, authInfo);
|
|
63385
|
-
if (!passed) return false;
|
|
63386
|
-
}
|
|
63387
|
-
return true;
|
|
63388
|
-
}
|
|
63389
|
-
static async checkSingle(perm, authInfo) {
|
|
63390
|
-
if (perm.roles && perm.roles.length > 0) {
|
|
63391
|
-
const userRoles = authInfo["roles"];
|
|
63392
|
-
if (!Array.isArray(userRoles)) return false;
|
|
63393
|
-
const hasRole = perm.roles.some((r) => userRoles.includes(r));
|
|
63394
|
-
if (!hasRole) return false;
|
|
63395
|
-
}
|
|
63396
|
-
if (perm.scopes && perm.scopes.length > 0) {
|
|
63397
|
-
const userScopes = authInfo["scopes"] ?? authInfo["scope"];
|
|
63398
|
-
const scopeArr = typeof userScopes === "string" ? userScopes.split(" ") : Array.isArray(userScopes) ? userScopes : [];
|
|
63399
|
-
const hasScope = perm.scopes.some((s) => scopeArr.includes(s));
|
|
63400
|
-
if (!hasScope) return false;
|
|
63401
|
-
}
|
|
63402
|
-
if (perm.custom) {
|
|
63403
|
-
const result = await perm.custom(authInfo);
|
|
63404
|
-
if (!result) return false;
|
|
63405
|
-
}
|
|
63406
|
-
return true;
|
|
63407
|
-
}
|
|
63408
|
-
};
|
|
63409
|
-
|
|
63410
|
-
// libs/sdk/src/job/index.ts
|
|
64128
|
+
init_job_permission_guard();
|
|
63411
64129
|
init_job_scope_helper();
|
|
63412
64130
|
|
|
63413
64131
|
// libs/sdk/src/job/execution/index.ts
|
|
@@ -63809,6 +64527,7 @@ export {
|
|
|
63809
64527
|
DynamicAdapter,
|
|
63810
64528
|
DynamicAdapterNameError,
|
|
63811
64529
|
DynamicJobDirectExecutionError,
|
|
64530
|
+
DynamicJobRegistrationDisabledError,
|
|
63812
64531
|
DynamicPlugin,
|
|
63813
64532
|
ELICITATION_FALLBACK_JSON_SCHEMA,
|
|
63814
64533
|
ESM_SH_BASE_URL,
|
|
@@ -63963,6 +64682,7 @@ export {
|
|
|
63963
64682
|
JobExecutionManager,
|
|
63964
64683
|
JobInstance,
|
|
63965
64684
|
JobKind,
|
|
64685
|
+
JobNotAuthorizedError,
|
|
63966
64686
|
JobPermissionGuard,
|
|
63967
64687
|
JobRegistry,
|
|
63968
64688
|
LLM_ADAPTER,
|
|
@@ -64480,6 +65200,7 @@ export {
|
|
|
64480
65200
|
resolveNameConflicts,
|
|
64481
65201
|
resolveOutputSchemaMode,
|
|
64482
65202
|
resolvePendingTC39HooksForClass,
|
|
65203
|
+
resolvePrincipal2 as resolvePrincipal,
|
|
64483
65204
|
resolveRegistryToken,
|
|
64484
65205
|
resolveSchemaDescriptionFormat,
|
|
64485
65206
|
resolveToolVisibility,
|