@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.
Files changed (95) hide show
  1. package/agent/flows/call-agent.flow.d.ts +2 -6
  2. package/agent/flows/call-agent.flow.d.ts.map +1 -1
  3. package/auth/flows/oauth.authorize.flow.d.ts +30 -0
  4. package/auth/flows/oauth.authorize.flow.d.ts.map +1 -1
  5. package/auth/flows/oauth.callback.flow.d.ts.map +1 -1
  6. package/auth/instances/instance.local-primary-auth.d.ts +35 -0
  7. package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
  8. package/builtin/config/providers/config-loader.d.ts +5 -0
  9. package/builtin/config/providers/config-loader.d.ts.map +1 -1
  10. package/common/metadata/agent.metadata.d.ts +2 -2
  11. package/common/metadata/app.metadata.d.ts +6 -6
  12. package/common/metadata/flow.metadata.d.ts +4 -4
  13. package/common/metadata/front-mcp.metadata.d.ts +43 -31
  14. package/common/metadata/front-mcp.metadata.d.ts.map +1 -1
  15. package/common/metadata/job.metadata.d.ts +2 -2
  16. package/common/metadata/tool.metadata.d.ts +3 -3
  17. package/common/metadata/workflow.metadata.d.ts +2 -2
  18. package/common/schemas/http-output.schema.d.ts +4 -4
  19. package/common/types/options/http/interfaces.d.ts +26 -10
  20. package/common/types/options/http/interfaces.d.ts.map +1 -1
  21. package/common/types/options/http/schema.d.ts.map +1 -1
  22. package/common/types/options/session/schema.d.ts +3 -3
  23. package/common/types/options/transport/schema.d.ts +3 -1
  24. package/common/types/options/transport/schema.d.ts.map +1 -1
  25. package/common/utils/index.d.ts +1 -0
  26. package/common/utils/index.d.ts.map +1 -1
  27. package/common/utils/principal.utils.d.ts +29 -0
  28. package/common/utils/principal.utils.d.ts.map +1 -0
  29. package/direct/llm-platform.d.ts +2 -2
  30. package/direct/llm-platform.d.ts.map +1 -1
  31. package/errors/index.d.ts +1 -0
  32. package/errors/index.d.ts.map +1 -1
  33. package/errors/job.errors.d.ts +26 -0
  34. package/errors/job.errors.d.ts.map +1 -0
  35. package/esm/index.mjs +928 -207
  36. package/front-mcp/front-mcp.providers.d.ts +4 -4
  37. package/index.js +1041 -320
  38. package/job/execution/job-execution.manager.d.ts +15 -0
  39. package/job/execution/job-execution.manager.d.ts.map +1 -1
  40. package/job/job-permission.guard.d.ts +23 -4
  41. package/job/job-permission.guard.d.ts.map +1 -1
  42. package/job/job-scope.helper.d.ts +12 -0
  43. package/job/job-scope.helper.d.ts.map +1 -1
  44. package/job/store/job-state.interface.d.ts +6 -0
  45. package/job/store/job-state.interface.d.ts.map +1 -1
  46. package/job/tools/execute-job.tool.d.ts.map +1 -1
  47. package/job/tools/get-job-status.tool.d.ts +12 -0
  48. package/job/tools/get-job-status.tool.d.ts.map +1 -1
  49. package/job/tools/list-jobs.tool.d.ts +6 -0
  50. package/job/tools/list-jobs.tool.d.ts.map +1 -1
  51. package/job/tools/register-job.tool.d.ts +1 -0
  52. package/job/tools/register-job.tool.d.ts.map +1 -1
  53. package/job/tools/remove-job.tool.d.ts.map +1 -1
  54. package/package.json +10 -10
  55. package/resource/flows/resource-templates-list.flow.d.ts +2 -2
  56. package/resource/flows/resources-list.flow.d.ts +2 -2
  57. package/scope/scope.utils.d.ts.map +1 -1
  58. package/server/adapters/express.host.adapter.d.ts +34 -2
  59. package/server/adapters/express.host.adapter.d.ts.map +1 -1
  60. package/server/middleware/host-validation.middleware.d.ts +8 -4
  61. package/server/middleware/host-validation.middleware.d.ts.map +1 -1
  62. package/server/security/host-validation.d.ts +66 -0
  63. package/server/security/host-validation.d.ts.map +1 -0
  64. package/server/security/resolve-allowed-hosts.d.ts +63 -0
  65. package/server/security/resolve-allowed-hosts.d.ts.map +1 -0
  66. package/server/security/security-audit.d.ts +7 -4
  67. package/server/security/security-audit.d.ts.map +1 -1
  68. package/server/server.instance.d.ts.map +1 -1
  69. package/server/server.types.d.ts +11 -6
  70. package/server/server.types.d.ts.map +1 -1
  71. package/tool/flows/call-tool.flow.d.ts +19 -7
  72. package/tool/flows/call-tool.flow.d.ts.map +1 -1
  73. package/tool/flows/tools-list.flow.d.ts +2 -6
  74. package/tool/flows/tools-list.flow.d.ts.map +1 -1
  75. package/transport/adapters/base-sse-transport.d.ts +7 -0
  76. package/transport/adapters/base-sse-transport.d.ts.map +1 -1
  77. package/transport/adapters/transport.local.adapter.d.ts.map +1 -1
  78. package/transport/adapters/transport.streamable-http.adapter.d.ts.map +1 -1
  79. package/transport/event-stores/index.d.ts +2 -0
  80. package/transport/event-stores/index.d.ts.map +1 -1
  81. package/transport/event-stores/memory.event-store.d.ts +9 -2
  82. package/transport/event-stores/memory.event-store.d.ts.map +1 -1
  83. package/transport/event-stores/redis.event-store.d.ts +24 -3
  84. package/transport/event-stores/redis.event-store.d.ts.map +1 -1
  85. package/transport/event-stores/session-scoped.event-store.d.ts +17 -0
  86. package/transport/event-stores/session-scoped.event-store.d.ts.map +1 -0
  87. package/transport/web-fetch-handler.d.ts.map +1 -1
  88. package/workflow/tools/execute-workflow.tool.d.ts.map +1 -1
  89. package/workflow/tools/get-workflow-status.tool.d.ts +12 -0
  90. package/workflow/tools/get-workflow-status.tool.d.ts.map +1 -1
  91. package/workflow/tools/list-workflows.tool.d.ts +1 -0
  92. package/workflow/tools/list-workflows.tool.d.ts.map +1 -1
  93. package/workflow/tools/register-workflow.tool.d.ts +1 -0
  94. package/workflow/tools/register-workflow.tool.d.ts.map +1 -1
  95. 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): permissive CORS (all origins, no credentials)
1868
- * - false: CORS disabled
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
- * Opt-in — defaults remain backwards-compatible.
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
- provider: z12.enum(["memory", "redis"]).optional().default("memory"),
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
- return `${baseUrl}/oauth/authorize?app=${encodeURIComponent(appId)}&tool=${encodeURIComponent(toolId)}`;
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 targetAppId = request.query["app"];
36022
- const targetToolId = request.query["tool"];
36023
- const existingSessionId = request.query["session_id"];
36024
- const mode = request.query["mode"];
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 (appsParam) {
36029
- const raw = Array.isArray(appsParam) ? appsParam : [appsParam];
36030
- const ids = raw.flatMap((v) => String(v).split(",")).map((s) => s.trim()).filter(Boolean);
36031
- priorAuthorizedAppIds = ids.length > 0 ? Array.from(new Set(ids)) : void 0;
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
- isIncremental: isIncrementalAuth,
36256
- targetAppId,
36257
- targetToolId,
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}${isIncrementalAuth ? ` (incremental for app: ${targetAppId})` : ""}${requiresFederatedLogin ? " (federated)" : ""}${requiresConsent ? " (consent enabled)" : ""}`
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 (email) {
37064
+ if (isIncremental) {
37065
+ userSub = pendingAuth.incrementalSub;
37066
+ } else if (email) {
36925
37067
  userSub = this.generateUserSub(email);
36926
- } else if (!requireEmail || isIncremental) {
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
- isIncremental: pendingAuth.isIncremental || isIncremental,
37097
- targetAppId: pendingAuth.targetAppId || this.state.targetAppId,
37098
- targetToolId: pendingAuth.targetToolId,
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 = new TextDecoder().decode(this.secret);
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 Error(`Workflow "${input.name}" not found`));
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 mapped = workflows.map((w) => ({
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 Error(`Workflow "${input.name}" not found`));
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 Error(`Job "${input.name}" not found`));
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 mapped = jobs.map((j) => ({
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 Error(`Job "${input.name}" not found`));
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] = this.parseEventId(lastEventId);
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 lastColonBeforeDash = eventId.lastIndexOf(":", eventId.lastIndexOf("-"));
52937
- if (lastColonBeforeDash === -1) {
52938
- const lastColon = eventId.lastIndexOf(":");
52939
- return [eventId.slice(0, lastColon), eventId.slice(lastColon + 1)];
52940
- }
52941
- return [eventId.slice(0, lastColonBeforeDash), eventId.slice(lastColonBeforeDash + 1)];
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
- if (this._options.allowedHosts && this._options.allowedHosts.length > 0) {
54849
- const hostHeader = req.headers.host;
54850
- if (!hostHeader || !this._options.allowedHosts.includes(hostHeader)) {
54851
- return `Invalid Host header: ${hostHeader}`;
54852
- }
54853
- }
54854
- if (this._options.allowedOrigins && this._options.allowedOrigins.length > 0) {
54855
- const originHeader = req.headers.origin;
54856
- if (!originHeader || !this._options.allowedOrigins.includes(originHeader)) {
54857
- return `Invalid Origin header: ${originHeader}`;
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 eventStore = this.scope.eventStore;
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: appMetadata.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
- if (config.cors === void 0 && !strict) {
60885
+ const corsOrigin = config.cors === false ? void 0 : config.cors?.origin;
60886
+ if (corsOrigin === void 0 || corsOrigin === false) {
60235
60887
  findings.push({
60236
- level: "warn",
60237
- code: "CORS_PERMISSIVE_DEFAULT",
60238
- message: "CORS is using the permissive default (origin: true), which allows all origins.",
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 (config.cors !== false && config.cors?.origin === true && !strict) {
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 ?? "0.0.0.0";
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, restrictive CORS, DNS rebinding protection."
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
- if (security.bindAddress === "loopback") return "127.0.0.1";
60329
- if (security.bindAddress === "all") return "0.0.0.0";
60330
- return security.bindAddress;
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 allowedHostsSet = options.allowedHosts ? new Set(options.allowedHosts) : void 0;
60349
- const allowedOriginsSet = options.allowedOrigins ? new Set(options.allowedOrigins) : void 0;
61001
+ const compiled = compileHostValidation({
61002
+ allowedHosts: options.allowedHosts,
61003
+ allowedOrigins: options.allowedOrigins
61004
+ });
60350
61005
  return (req, res, next) => {
60351
- if (allowedHostsSet) {
60352
- const host = req.headers?.["host"];
60353
- if (!host || !allowedHostsSet.has(host)) {
60354
- res.status(403).json({
60355
- error: "Forbidden",
60356
- message: "Invalid Host header"
60357
- });
60358
- return;
60359
- }
60360
- const forwardedHost = req.headers?.["x-forwarded-host"];
60361
- if (forwardedHost) {
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 = { origin: true, credentials: false };
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,