@frontmcp/sdk 1.7.1 → 1.8.0

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