@frontmcp/sdk 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/agent/flows/call-agent.flow.d.ts +2 -6
  2. package/agent/flows/call-agent.flow.d.ts.map +1 -1
  3. package/auth/flows/oauth.authorize.flow.d.ts +30 -0
  4. package/auth/flows/oauth.authorize.flow.d.ts.map +1 -1
  5. package/auth/flows/oauth.callback.flow.d.ts.map +1 -1
  6. package/auth/instances/instance.local-primary-auth.d.ts +35 -0
  7. package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
  8. package/builtin/config/providers/config-loader.d.ts +5 -0
  9. package/builtin/config/providers/config-loader.d.ts.map +1 -1
  10. package/common/metadata/agent.metadata.d.ts +2 -2
  11. package/common/metadata/app.metadata.d.ts +6 -6
  12. package/common/metadata/flow.metadata.d.ts +4 -4
  13. package/common/metadata/front-mcp.metadata.d.ts +43 -31
  14. package/common/metadata/front-mcp.metadata.d.ts.map +1 -1
  15. package/common/metadata/job.metadata.d.ts +2 -2
  16. package/common/metadata/tool.metadata.d.ts +3 -3
  17. package/common/metadata/workflow.metadata.d.ts +2 -2
  18. package/common/schemas/http-output.schema.d.ts +4 -4
  19. package/common/types/options/http/interfaces.d.ts +26 -10
  20. package/common/types/options/http/interfaces.d.ts.map +1 -1
  21. package/common/types/options/http/schema.d.ts.map +1 -1
  22. package/common/types/options/session/schema.d.ts +3 -3
  23. package/common/types/options/transport/schema.d.ts +3 -1
  24. package/common/types/options/transport/schema.d.ts.map +1 -1
  25. package/common/utils/index.d.ts +1 -0
  26. package/common/utils/index.d.ts.map +1 -1
  27. package/common/utils/principal.utils.d.ts +29 -0
  28. package/common/utils/principal.utils.d.ts.map +1 -0
  29. package/direct/llm-platform.d.ts +2 -2
  30. package/direct/llm-platform.d.ts.map +1 -1
  31. package/errors/index.d.ts +1 -0
  32. package/errors/index.d.ts.map +1 -1
  33. package/errors/job.errors.d.ts +26 -0
  34. package/errors/job.errors.d.ts.map +1 -0
  35. package/esm/index.mjs +928 -207
  36. package/front-mcp/front-mcp.providers.d.ts +4 -4
  37. package/index.js +1041 -320
  38. package/job/execution/job-execution.manager.d.ts +15 -0
  39. package/job/execution/job-execution.manager.d.ts.map +1 -1
  40. package/job/job-permission.guard.d.ts +23 -4
  41. package/job/job-permission.guard.d.ts.map +1 -1
  42. package/job/job-scope.helper.d.ts +12 -0
  43. package/job/job-scope.helper.d.ts.map +1 -1
  44. package/job/store/job-state.interface.d.ts +6 -0
  45. package/job/store/job-state.interface.d.ts.map +1 -1
  46. package/job/tools/execute-job.tool.d.ts.map +1 -1
  47. package/job/tools/get-job-status.tool.d.ts +12 -0
  48. package/job/tools/get-job-status.tool.d.ts.map +1 -1
  49. package/job/tools/list-jobs.tool.d.ts +6 -0
  50. package/job/tools/list-jobs.tool.d.ts.map +1 -1
  51. package/job/tools/register-job.tool.d.ts +1 -0
  52. package/job/tools/register-job.tool.d.ts.map +1 -1
  53. package/job/tools/remove-job.tool.d.ts.map +1 -1
  54. package/package.json +10 -10
  55. package/resource/flows/resource-templates-list.flow.d.ts +2 -2
  56. package/resource/flows/resources-list.flow.d.ts +2 -2
  57. package/scope/scope.utils.d.ts.map +1 -1
  58. package/server/adapters/express.host.adapter.d.ts +34 -2
  59. package/server/adapters/express.host.adapter.d.ts.map +1 -1
  60. package/server/middleware/host-validation.middleware.d.ts +8 -4
  61. package/server/middleware/host-validation.middleware.d.ts.map +1 -1
  62. package/server/security/host-validation.d.ts +66 -0
  63. package/server/security/host-validation.d.ts.map +1 -0
  64. package/server/security/resolve-allowed-hosts.d.ts +63 -0
  65. package/server/security/resolve-allowed-hosts.d.ts.map +1 -0
  66. package/server/security/security-audit.d.ts +7 -4
  67. package/server/security/security-audit.d.ts.map +1 -1
  68. package/server/server.instance.d.ts.map +1 -1
  69. package/server/server.types.d.ts +11 -6
  70. package/server/server.types.d.ts.map +1 -1
  71. package/tool/flows/call-tool.flow.d.ts +19 -7
  72. package/tool/flows/call-tool.flow.d.ts.map +1 -1
  73. package/tool/flows/tools-list.flow.d.ts +2 -6
  74. package/tool/flows/tools-list.flow.d.ts.map +1 -1
  75. package/transport/adapters/base-sse-transport.d.ts +7 -0
  76. package/transport/adapters/base-sse-transport.d.ts.map +1 -1
  77. package/transport/adapters/transport.local.adapter.d.ts.map +1 -1
  78. package/transport/adapters/transport.streamable-http.adapter.d.ts.map +1 -1
  79. package/transport/event-stores/index.d.ts +2 -0
  80. package/transport/event-stores/index.d.ts.map +1 -1
  81. package/transport/event-stores/memory.event-store.d.ts +9 -2
  82. package/transport/event-stores/memory.event-store.d.ts.map +1 -1
  83. package/transport/event-stores/redis.event-store.d.ts +24 -3
  84. package/transport/event-stores/redis.event-store.d.ts.map +1 -1
  85. package/transport/event-stores/session-scoped.event-store.d.ts +17 -0
  86. package/transport/event-stores/session-scoped.event-store.d.ts.map +1 -0
  87. package/transport/web-fetch-handler.d.ts.map +1 -1
  88. package/workflow/tools/execute-workflow.tool.d.ts.map +1 -1
  89. package/workflow/tools/get-workflow-status.tool.d.ts +12 -0
  90. package/workflow/tools/get-workflow-status.tool.d.ts.map +1 -1
  91. package/workflow/tools/list-workflows.tool.d.ts +1 -0
  92. package/workflow/tools/list-workflows.tool.d.ts.map +1 -1
  93. package/workflow/tools/register-workflow.tool.d.ts +1 -0
  94. package/workflow/tools/register-workflow.tool.d.ts.map +1 -1
  95. package/workflow/tools/remove-workflow.tool.d.ts.map +1 -1
package/index.js CHANGED
@@ -1863,14 +1863,19 @@ var init_schema3 = __esm({
1863
1863
  socketPath: import_lazy_zod8.z.string().optional(),
1864
1864
  /**
1865
1865
  * CORS configuration.
1866
- * - undefined (default): permissive CORS (all origins, no credentials)
1867
- * - false: CORS disabled
1866
+ * - undefined (default): NO CORS headers — same-origin only. A browser will not let another
1867
+ * origin read the response. Set `{ origin: true }` for the old permissive behaviour.
1868
+ * - false: CORS disabled (same effect as the default; kept for explicitness)
1868
1869
  * - CorsOptions object: custom CORS config
1869
1870
  */
1870
1871
  cors: import_lazy_zod8.z.union([import_lazy_zod8.z.literal(false), corsOptionsSchema]).optional(),
1871
1872
  /**
1872
1873
  * Security configuration for transport hardening.
1873
- * Opt-in — defaults remain backwards-compatible.
1874
+ *
1875
+ * The defaults are the SAFE choice: the server binds loopback and sends no CORS headers. Reaching
1876
+ * it from another host or another origin is something you opt into — `bindAddress: 'all'`, the
1877
+ * `FRONTMCP_BIND_ADDRESS=all` env var (no rebuild; the right fit for a Dockerfile), or a
1878
+ * distributed build — and an explicit `cors` config.
1874
1879
  */
1875
1880
  security: import_lazy_zod8.z.object({
1876
1881
  strict: import_lazy_zod8.z.boolean().optional(),
@@ -2304,7 +2309,10 @@ var init_schema7 = __esm({
2304
2309
  });
2305
2310
  eventStoreConfigSchema = import_lazy_zod12.z.object({
2306
2311
  enabled: import_lazy_zod12.z.boolean().default(false),
2307
- provider: import_lazy_zod12.z.enum(["memory", "redis"]).optional().default("memory"),
2312
+ // 'sqlite' is supported by createEventStore and the TS interface but was
2313
+ // missing from this enum, so a valid `provider: 'sqlite'` config failed
2314
+ // validation.
2315
+ provider: import_lazy_zod12.z.enum(["memory", "redis", "sqlite"]).optional().default("memory"),
2308
2316
  maxEvents: import_lazy_zod12.z.number().int().positive().optional().default(1e4),
2309
2317
  ttlMs: import_lazy_zod12.z.number().int().positive().optional().default(3e5),
2310
2318
  redis: redisOptionsSchema.optional()
@@ -4033,7 +4041,7 @@ function formatToolsForPlatform(tools, platform) {
4033
4041
  })
4034
4042
  );
4035
4043
  case "vercel-ai": {
4036
- const result = {};
4044
+ const result = /* @__PURE__ */ Object.create(null);
4037
4045
  for (const tool of tools) {
4038
4046
  result[tool.name] = {
4039
4047
  description: tool.description ?? `Execute ${tool.name}`,
@@ -5571,6 +5579,32 @@ var init_sdk_errors = __esm({
5571
5579
  }
5572
5580
  });
5573
5581
 
5582
+ // libs/sdk/src/errors/job.errors.ts
5583
+ var JobNotAuthorizedError, DynamicJobRegistrationDisabledError;
5584
+ var init_job_errors = __esm({
5585
+ "libs/sdk/src/errors/job.errors.ts"() {
5586
+ "use strict";
5587
+ init_mcp_error();
5588
+ JobNotAuthorizedError = class extends PublicMcpError {
5589
+ /** The requested name, for internal logs only — never surfaced publicly. */
5590
+ requestedName;
5591
+ constructor(requestedName) {
5592
+ super(`Job or workflow "${requestedName}" not found or not permitted`, "JOB_NOT_AUTHORIZED", 403);
5593
+ this.requestedName = requestedName;
5594
+ }
5595
+ };
5596
+ DynamicJobRegistrationDisabledError = class extends PublicMcpError {
5597
+ constructor(kind = "job") {
5598
+ super(
5599
+ `Dynamic ${kind} registration is disabled. Enable it with jobs: { allowDynamicRegistration: true }.`,
5600
+ "DYNAMIC_REGISTRATION_DISABLED",
5601
+ 403
5602
+ );
5603
+ }
5604
+ };
5605
+ }
5606
+ });
5607
+
5574
5608
  // libs/sdk/src/errors/index.ts
5575
5609
  var errors_exports = {};
5576
5610
  __export(errors_exports, {
@@ -5596,6 +5630,7 @@ __export(errors_exports, {
5596
5630
  DependencyNotFoundError: () => DependencyNotFoundError,
5597
5631
  DynamicAdapterNameError: () => DynamicAdapterNameError,
5598
5632
  DynamicJobDirectExecutionError: () => DynamicJobDirectExecutionError,
5633
+ DynamicJobRegistrationDisabledError: () => DynamicJobRegistrationDisabledError,
5599
5634
  ElicitationDisabledError: () => ElicitationDisabledError,
5600
5635
  ElicitationEncryptionError: () => ElicitationEncryptionError,
5601
5636
  ElicitationFallbackRequired: () => ElicitationFallbackRequired,
@@ -5644,6 +5679,7 @@ __export(errors_exports, {
5644
5679
  InvalidUseFactoryError: () => InvalidUseFactoryError,
5645
5680
  InvalidUseValueError: () => InvalidUseValueError,
5646
5681
  InvokeStateMissingKeyError: () => InvokeStateMissingKeyError,
5682
+ JobNotAuthorizedError: () => JobNotAuthorizedError,
5647
5683
  MCP_ERROR_CODES: () => MCP_ERROR_CODES,
5648
5684
  McpError: () => McpError,
5649
5685
  MethodNotImplementedError: () => MethodNotImplementedError,
@@ -5766,6 +5802,7 @@ var init_errors = __esm({
5766
5802
  init_workflow_errors();
5767
5803
  init_esm_errors();
5768
5804
  init_sdk_errors();
5805
+ init_job_errors();
5769
5806
  }
5770
5807
  });
5771
5808
 
@@ -7088,6 +7125,10 @@ var require_dist = __commonJS({
7088
7125
  }
7089
7126
  return id;
7090
7127
  }
7128
+ async getStreamIdForEventId(eventId) {
7129
+ const row = this.prepared().getStreamId.get(eventId);
7130
+ return row?.stream_id;
7131
+ }
7091
7132
  async replayEventsAfter(lastEventId, { send }) {
7092
7133
  const stmts = this.prepared();
7093
7134
  const streamRow = stmts.getStreamId.get(lastEventId);
@@ -14010,22 +14051,38 @@ async function loadYamlConfig(basePath, configPath) {
14010
14051
  }
14011
14052
  return {};
14012
14053
  }
14054
+ function stripUnsafeKeys(value) {
14055
+ if (Array.isArray(value)) return value.map(stripUnsafeKeys);
14056
+ if (!isPlainObject(value)) return value;
14057
+ const clean = {};
14058
+ for (const [key, nested] of Object.entries(value)) {
14059
+ if (UNSAFE_MERGE_KEYS.has(key)) continue;
14060
+ clean[key] = stripUnsafeKeys(nested);
14061
+ }
14062
+ return clean;
14063
+ }
14064
+ function isPlainObject(value) {
14065
+ if (!value || typeof value !== "object") return false;
14066
+ const proto = Object.getPrototypeOf(value);
14067
+ return proto === Object.prototype || proto === null;
14068
+ }
14013
14069
  function deepMerge(target, source) {
14014
14070
  const result = { ...target };
14015
14071
  for (const key in source) {
14072
+ if (UNSAFE_MERGE_KEYS.has(key)) continue;
14016
14073
  const sourceVal = source[key];
14017
14074
  const targetVal = result[key];
14018
14075
  if (sourceVal !== void 0 && sourceVal !== null) {
14019
14076
  if (typeof sourceVal === "object" && !Array.isArray(sourceVal) && typeof targetVal === "object" && !Array.isArray(targetVal) && targetVal !== null) {
14020
14077
  result[key] = deepMerge(targetVal, sourceVal);
14021
14078
  } else {
14022
- result[key] = sourceVal;
14079
+ result[key] = stripUnsafeKeys(sourceVal);
14023
14080
  }
14024
14081
  }
14025
14082
  }
14026
14083
  return result;
14027
14084
  }
14028
- var yaml2, import_utils32;
14085
+ var yaml2, import_utils32, UNSAFE_MERGE_KEYS;
14029
14086
  var init_config_loader = __esm({
14030
14087
  "libs/sdk/src/builtin/config/providers/config-loader.ts"() {
14031
14088
  "use strict";
@@ -14033,6 +14090,7 @@ var init_config_loader = __esm({
14033
14090
  import_utils32 = require("@frontmcp/utils");
14034
14091
  init_config_service();
14035
14092
  init_env_loader();
14093
+ UNSAFE_MERGE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
14036
14094
  }
14037
14095
  });
14038
14096
 
@@ -20699,10 +20757,11 @@ var init_tool_credentials_gate = __esm({
20699
20757
  });
20700
20758
 
20701
20759
  // libs/sdk/src/tool/flows/call-tool.flow.ts
20702
- var import_guard4, import_lazy_zod56, import_protocol38, import_adapters4, import_utils45, inputSchema13, outputSchema12, stateSchema12, plan12, name12, Stage12, CallToolFlow;
20760
+ var import_auth17, import_guard4, import_lazy_zod56, import_protocol38, import_adapters4, import_utils45, inputSchema13, outputSchema12, stateSchema12, plan12, name12, Stage12, CallToolFlow;
20703
20761
  var init_call_tool_flow = __esm({
20704
20762
  "libs/sdk/src/tool/flows/call-tool.flow.ts"() {
20705
20763
  "use strict";
20764
+ import_auth17 = require("@frontmcp/auth");
20706
20765
  import_guard4 = require("@frontmcp/guard");
20707
20766
  import_lazy_zod56 = require("@frontmcp/lazy-zod");
20708
20767
  import_protocol38 = require("@frontmcp/protocol");
@@ -21075,7 +21134,7 @@ var init_call_tool_flow = __esm({
21075
21134
  this.logger.verbose(`checkToolAuthorization: using anonymous for ${appId}`);
21076
21135
  return;
21077
21136
  }
21078
- const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "");
21137
+ const authUrl = this.buildProgressiveAuthUrl(appId, tool?.fullName || "", authInfo, claimAuthorizedApps);
21079
21138
  this.logger.info(`checkToolAuthorization: authorization required for app "${appId}"`);
21080
21139
  throw new AuthorizationRequiredError({
21081
21140
  appId,
@@ -21087,11 +21146,51 @@ var init_call_tool_flow = __esm({
21087
21146
  this.logger.verbose("checkToolAuthorization:done");
21088
21147
  }
21089
21148
  /**
21090
- * Build the URL for progressive/incremental authorization
21149
+ * Build the URL for progressive/incremental authorization.
21150
+ *
21151
+ * This is the ONE point in the request lifecycle where the caller's identity
21152
+ * is both known and verified, so it is where the incremental-authorization
21153
+ * ticket is minted (GHSA-2c4g-9c8x-6m8g). The ticket is what later authorizes
21154
+ * `/oauth/authorize` to skip the login step; `app`/`tool` are carried purely
21155
+ * for display and are re-read from the signed payload server-side.
21156
+ *
21157
+ * When no verified subject or signing secret is available the URL is emitted
21158
+ * WITHOUT a ticket — the user then completes an ordinary login, which is the
21159
+ * correct fail-closed outcome rather than a credential-free shortcut.
21091
21160
  */
21092
- buildProgressiveAuthUrl(appId, toolId) {
21161
+ buildProgressiveAuthUrl(appId, toolId, authInfo, priorAppIds) {
21093
21162
  const baseUrl = this.scope.fullPath || "";
21094
- return `${baseUrl}/oauth/authorize?app=${encodeURIComponent(appId)}&tool=${encodeURIComponent(toolId)}`;
21163
+ const url = new URL(`${baseUrl}/oauth/authorize`, "http://placeholder.invalid");
21164
+ url.searchParams.set("app", appId);
21165
+ url.searchParams.set("tool", toolId);
21166
+ const ticket = this.mintIncrementalTicket(appId, toolId, authInfo, priorAppIds);
21167
+ if (ticket) {
21168
+ url.searchParams.set("ticket", ticket);
21169
+ } else {
21170
+ this.logger.verbose("buildProgressiveAuthUrl: no incremental ticket minted \u2014 link requires a full login");
21171
+ }
21172
+ return baseUrl.startsWith("http") ? url.toString() : `${url.pathname}${url.search}`;
21173
+ }
21174
+ /**
21175
+ * Mint a single-use, short-lived incremental-authorization ticket for the
21176
+ * verified caller. Returns undefined when the scope has no local signing
21177
+ * secret or the request has no verified subject.
21178
+ */
21179
+ mintIncrementalTicket(appId, toolId, authInfo, priorAppIds) {
21180
+ const extraUser = authInfo?.extra?.user;
21181
+ const sub = extraUser?.sub ?? authInfo?.user?.sub;
21182
+ if (!sub) return void 0;
21183
+ const secret = this.scope.auth?.signingSecret;
21184
+ if (!secret) return void 0;
21185
+ return (0, import_auth17.signIncrementalAuthTicket)(
21186
+ {
21187
+ sub,
21188
+ appId,
21189
+ toolId,
21190
+ ...priorAppIds && priorAppIds.size > 0 ? { priorAppIds: [...priorAppIds] } : {}
21191
+ },
21192
+ secret
21193
+ );
21095
21194
  }
21096
21195
  async checkEntryAuthorities() {
21097
21196
  this.logger.verbose("checkEntryAuthorities:start");
@@ -35572,25 +35671,25 @@ var init_auth_utils = __esm({
35572
35671
  });
35573
35672
 
35574
35673
  // libs/sdk/src/auth/authorization/orchestrated.context-extension.ts
35575
- var import_auth17, orchestratedAuthContextExtension;
35674
+ var import_auth18, orchestratedAuthContextExtension;
35576
35675
  var init_orchestrated_context_extension = __esm({
35577
35676
  "libs/sdk/src/auth/authorization/orchestrated.context-extension.ts"() {
35578
35677
  "use strict";
35579
- import_auth17 = require("@frontmcp/auth");
35678
+ import_auth18 = require("@frontmcp/auth");
35580
35679
  orchestratedAuthContextExtension = {
35581
35680
  property: "orchestration",
35582
- token: import_auth17.ORCHESTRATED_AUTH_ACCESSOR,
35681
+ token: import_auth18.ORCHESTRATED_AUTH_ACCESSOR,
35583
35682
  errorMessage: "Orchestrated authorization not available. Ensure your server is configured with orchestrated mode and the user has completed authentication."
35584
35683
  };
35585
35684
  }
35586
35685
  });
35587
35686
 
35588
35687
  // libs/sdk/src/auth/cimd/index.ts
35589
- var import_auth18;
35688
+ var import_auth19;
35590
35689
  var init_cimd = __esm({
35591
35690
  "libs/sdk/src/auth/cimd/index.ts"() {
35592
35691
  "use strict";
35593
- import_auth18 = require("@frontmcp/auth");
35692
+ import_auth19 = require("@frontmcp/auth");
35594
35693
  }
35595
35694
  });
35596
35695
 
@@ -35612,9 +35711,9 @@ function createCredentialsProviders(opts) {
35612
35711
  name: "SessionCredentialVault"
35613
35712
  };
35614
35713
  const accessorProvider = {
35615
- provide: import_auth19.CREDENTIALS_ACCESSOR,
35714
+ provide: import_auth20.CREDENTIALS_ACCESSOR,
35616
35715
  inject: () => [FRONTMCP_CONTEXT, FrontMcpLogger],
35617
- useFactory: (ctx, logger) => new import_auth19.CredentialsAccessorImpl({
35716
+ useFactory: (ctx, logger) => new import_auth20.CredentialsAccessorImpl({
35618
35717
  vault,
35619
35718
  resolveSub: () => resolveRequestSub(ctx),
35620
35719
  signingSecret,
@@ -35627,11 +35726,11 @@ function createCredentialsProviders(opts) {
35627
35726
  };
35628
35727
  return [vaultProvider, accessorProvider];
35629
35728
  }
35630
- var import_auth19, import_di31, SESSION_CREDENTIAL_VAULT;
35729
+ var import_auth20, import_di31, SESSION_CREDENTIAL_VAULT;
35631
35730
  var init_credentials_providers = __esm({
35632
35731
  "libs/sdk/src/auth/credentials/credentials.providers.ts"() {
35633
35732
  "use strict";
35634
- import_auth19 = require("@frontmcp/auth");
35733
+ import_auth20 = require("@frontmcp/auth");
35635
35734
  import_di31 = require("@frontmcp/di");
35636
35735
  init_common();
35637
35736
  init_frontmcp_context_provider();
@@ -35642,14 +35741,14 @@ var init_credentials_providers = __esm({
35642
35741
  });
35643
35742
 
35644
35743
  // libs/sdk/src/auth/credentials/credentials.context-extension.ts
35645
- var import_auth20, credentialsContextExtension;
35744
+ var import_auth21, credentialsContextExtension;
35646
35745
  var init_credentials_context_extension = __esm({
35647
35746
  "libs/sdk/src/auth/credentials/credentials.context-extension.ts"() {
35648
35747
  "use strict";
35649
- import_auth20 = require("@frontmcp/auth");
35748
+ import_auth21 = require("@frontmcp/auth");
35650
35749
  credentialsContextExtension = {
35651
35750
  property: "credentials",
35652
- token: import_auth20.CREDENTIALS_ACCESSOR,
35751
+ token: import_auth21.CREDENTIALS_ACCESSOR,
35653
35752
  errorMessage: "Per-session credentials are not available. Ensure the server uses auth mode 'local' (or 'remote'); credentials are persisted by a local authenticate() verifier."
35654
35753
  };
35655
35754
  }
@@ -35818,11 +35917,11 @@ function getConfiguredProviders(auth) {
35818
35917
  function getConfiguredProviderIds(auth) {
35819
35918
  return getConfiguredProviders(auth).map((p) => p.id);
35820
35919
  }
35821
- var import_auth21, import_lazy_zod64, import_utils66, pkceChallengeSchema, codeChallengeMethodSchema, responseTypeSchema, safeRedirectUriSchema, oauthAuthorizeRequestSchema, anonymousAuthorizeRequestSchema, inputSchema16, stateSchema15, outputSchema15, plan15, name15, Stage15, OauthAuthorizeFlow;
35920
+ var import_auth22, import_lazy_zod64, import_utils66, pkceChallengeSchema, codeChallengeMethodSchema, responseTypeSchema, safeRedirectUriSchema, oauthAuthorizeRequestSchema, anonymousAuthorizeRequestSchema, inputSchema16, stateSchema15, outputSchema15, plan15, name15, Stage15, OauthAuthorizeFlow;
35822
35921
  var init_oauth_authorize_flow = __esm({
35823
35922
  "libs/sdk/src/auth/flows/oauth.authorize.flow.ts"() {
35824
35923
  "use strict";
35825
- import_auth21 = require("@frontmcp/auth");
35924
+ import_auth22 = require("@frontmcp/auth");
35826
35925
  import_lazy_zod64 = require("@frontmcp/lazy-zod");
35827
35926
  import_utils66 = require("@frontmcp/utils");
35828
35927
  init_common();
@@ -35880,6 +35979,13 @@ var init_oauth_authorize_flow = __esm({
35880
35979
  targetAppId: import_lazy_zod64.z.string().optional().describe("Target app ID for incremental authorization"),
35881
35980
  targetToolId: import_lazy_zod64.z.string().optional().describe("Target tool ID that triggered the incremental auth"),
35882
35981
  existingSessionId: import_lazy_zod64.z.string().optional().describe("Existing session ID for incremental auth"),
35982
+ /**
35983
+ * The VERIFIED incremental-authorization ticket (GHSA-2c4g-9c8x-6m8g). Set
35984
+ * only when `?ticket=` carried a valid, unexpired server signature. Its `sub`
35985
+ * is the proven identity the expanded grant is minted for; `isIncrementalAuth`
35986
+ * is derived from its presence, never from a raw query parameter.
35987
+ */
35988
+ incrementalTicket: import_lazy_zod64.z.custom().optional(),
35883
35989
  /**
35884
35990
  * Apps the client already holds a grant for (the prior `authorized_apps`
35885
35991
  * claim), carried forward on an incremental authorize so the newly-minted
@@ -35893,7 +35999,7 @@ var init_oauth_authorize_flow = __esm({
35893
35999
  requiresConsent: import_lazy_zod64.z.boolean().default(false).describe("Whether consent flow is enabled"),
35894
36000
  // CIMD (Client ID Metadata Documents)
35895
36001
  isCimdClient: import_lazy_zod64.z.boolean().default(false).describe("Whether client_id is a CIMD URL"),
35896
- cimdMetadata: import_auth18.clientMetadataDocumentSchema.optional().describe("CIMD metadata document for the client")
36002
+ cimdMetadata: import_auth19.clientMetadataDocumentSchema.optional().describe("CIMD metadata document for the client")
35897
36003
  });
35898
36004
  outputSchema15 = import_lazy_zod64.z.union([
35899
36005
  HttpRedirectSchema,
@@ -35932,18 +36038,21 @@ var init_oauth_authorize_flow = __esm({
35932
36038
  const { request } = this.rawInput;
35933
36039
  const rawRedirectUri = request.query["redirect_uri"];
35934
36040
  const rawState = request.query["state"];
35935
- const targetAppId = request.query["app"];
35936
- const targetToolId = request.query["tool"];
35937
- const existingSessionId = request.query["session_id"];
35938
- const mode = request.query["mode"];
35939
- const isIncrementalAuth = mode === "incremental" || !!targetAppId;
35940
- const appsParam = request.query["apps"];
36041
+ const incrementalTicket = this.verifyIncrementalTicket(request.query["ticket"]);
36042
+ const isIncrementalAuth = incrementalTicket !== void 0;
36043
+ const targetAppId = incrementalTicket?.appId;
36044
+ const targetToolId = incrementalTicket?.toolId;
35941
36045
  let priorAuthorizedAppIds;
35942
- if (appsParam) {
35943
- const raw = Array.isArray(appsParam) ? appsParam : [appsParam];
35944
- const ids = raw.flatMap((v) => String(v).split(",")).map((s) => s.trim()).filter(Boolean);
35945
- priorAuthorizedAppIds = ids.length > 0 ? Array.from(new Set(ids)) : void 0;
36046
+ if (incrementalTicket) {
36047
+ priorAuthorizedAppIds = incrementalTicket.priorAppIds && incrementalTicket.priorAppIds.length > 0 ? Array.from(new Set(incrementalTicket.priorAppIds)) : void 0;
36048
+ } else {
36049
+ const appsParam = request.query["apps"];
36050
+ if (appsParam) {
36051
+ const ids = (Array.isArray(appsParam) ? appsParam : [appsParam]).flatMap((v) => String(v).split(",")).map((v) => v.trim()).filter(Boolean);
36052
+ priorAuthorizedAppIds = ids.length > 0 ? Array.from(new Set(ids)) : void 0;
36053
+ }
35946
36054
  }
36055
+ const existingSessionId = request.query["session_id"];
35947
36056
  const isDefaultAuthProvider = !metadata.auth;
35948
36057
  let requiresFederatedLogin = false;
35949
36058
  if (metadata.auth && (0, import_auth10.isOrchestratedMode)(metadata.auth)) {
@@ -35969,6 +36078,7 @@ var init_oauth_authorize_flow = __esm({
35969
36078
  targetToolId,
35970
36079
  existingSessionId,
35971
36080
  priorAuthorizedAppIds,
36081
+ incrementalTicket,
35972
36082
  // Federated Login
35973
36083
  requiresFederatedLogin,
35974
36084
  // Consent Flow
@@ -36040,7 +36150,7 @@ var init_oauth_authorize_flow = __esm({
36040
36150
  }
36041
36151
  }
36042
36152
  const { client_id, redirect_uri } = result.data;
36043
- const cimdService = this.get(import_auth18.CimdService);
36153
+ const cimdService = this.get(import_auth19.CimdService);
36044
36154
  const isCimdClientId2 = !!cimdService?.enabled && cimdService.isCimdClientId(client_id);
36045
36155
  const dcrError = this.checkDcrAllowlist(client_id, redirect_uri, isCimdClientId2);
36046
36156
  if (dcrError) {
@@ -36094,6 +36204,28 @@ var init_oauth_authorize_flow = __esm({
36094
36204
  }
36095
36205
  }
36096
36206
  }
36207
+ /**
36208
+ * Verify a client-supplied incremental-authorization ticket.
36209
+ *
36210
+ * Returns the verified payload, or `undefined` when the parameter is absent,
36211
+ * malformed, expired, or not signed by this server. Verification is
36212
+ * side-effect free — the ticket is CLAIMED (single use) later, in
36213
+ * {@link prepareAuthorizationRequest}, so a request that fails OAuth
36214
+ * validation does not burn the user's ticket.
36215
+ */
36216
+ verifyIncrementalTicket(raw) {
36217
+ const token = typeof raw === "string" ? raw : Array.isArray(raw) ? raw[0] : void 0;
36218
+ if (typeof token !== "string" || token.length === 0) return void 0;
36219
+ const localAuth = this.scope.auth;
36220
+ const secret = localAuth?.signingSecret;
36221
+ if (!secret) return void 0;
36222
+ const payload = (0, import_auth22.verifyIncrementalAuthTicket)(token, secret);
36223
+ if (!payload) {
36224
+ this.logger.warn("Incremental auth ticket failed verification \u2014 treating as an ordinary login");
36225
+ return void 0;
36226
+ }
36227
+ return payload;
36228
+ }
36097
36229
  async checkIfAuthorized() {
36098
36230
  }
36099
36231
  async prepareAuthorizationRequest() {
@@ -36105,7 +36237,8 @@ var init_oauth_authorize_flow = __esm({
36105
36237
  existingSessionId,
36106
36238
  priorAuthorizedAppIds,
36107
36239
  requiresFederatedLogin,
36108
- requiresConsent
36240
+ requiresConsent,
36241
+ incrementalTicket
36109
36242
  } = this.state;
36110
36243
  const { metadata } = this.scope;
36111
36244
  if (!validatedRequest) {
@@ -36118,6 +36251,18 @@ var init_oauth_authorize_flow = __esm({
36118
36251
  }
36119
36252
  const localAuth = auth;
36120
36253
  const store = localAuth.authorizationStore;
36254
+ let effectiveIncremental = isIncrementalAuth;
36255
+ if (incrementalTicket) {
36256
+ const claimed = await localAuth.claimIncrementalTicket(
36257
+ incrementalTicket.jti,
36258
+ Math.max(0, incrementalTicket.exp - Date.now())
36259
+ );
36260
+ if (!claimed) {
36261
+ this.logger.warn("Incremental auth ticket already used \u2014 falling back to an ordinary login");
36262
+ effectiveIncremental = false;
36263
+ this.state.set({ isIncrementalAuth: false, targetAppId: void 0, targetToolId: void 0 });
36264
+ }
36265
+ }
36121
36266
  let federatedLogin;
36122
36267
  if (requiresFederatedLogin) {
36123
36268
  const apps = this.scope.apps.getApps();
@@ -36165,10 +36310,14 @@ var init_oauth_authorize_flow = __esm({
36165
36310
  },
36166
36311
  state: validatedRequest.state,
36167
36312
  resource: validatedRequest.resource,
36168
- // Progressive/Incremental Authorization Fields
36169
- isIncremental: isIncrementalAuth,
36170
- targetAppId,
36171
- targetToolId,
36313
+ // Progressive/Incremental Authorization Fields. `isIncremental` reflects a
36314
+ // CLAIMED, server-signed ticket; `incrementalSub` is the identity that
36315
+ // ticket proved, and is the only subject the callback may mint for when
36316
+ // it skips the credential gate.
36317
+ isIncremental: effectiveIncremental,
36318
+ incrementalSub: effectiveIncremental ? incrementalTicket?.sub : void 0,
36319
+ targetAppId: effectiveIncremental ? targetAppId : void 0,
36320
+ targetToolId: effectiveIncremental ? targetToolId : void 0,
36172
36321
  existingSessionId,
36173
36322
  priorAuthorizedAppIds,
36174
36323
  // Federated Login State
@@ -36178,7 +36327,7 @@ var init_oauth_authorize_flow = __esm({
36178
36327
  });
36179
36328
  await localAuth.authorizationStore.storePendingAuthorization(pendingRecord);
36180
36329
  this.logger.info(
36181
- `Pending authorization created: ${pendingRecord.id}${isIncrementalAuth ? ` (incremental for app: ${targetAppId})` : ""}${requiresFederatedLogin ? " (federated)" : ""}${requiresConsent ? " (consent enabled)" : ""}`
36330
+ `Pending authorization created: ${pendingRecord.id}${effectiveIncremental ? ` (incremental for app: ${targetAppId})` : ""}${requiresFederatedLogin ? " (federated)" : ""}${requiresConsent ? " (consent enabled)" : ""}`
36182
36331
  );
36183
36332
  this.state.set("pendingAuthId", pendingRecord.id);
36184
36333
  }
@@ -36399,7 +36548,7 @@ var init_oauth_authorize_flow = __esm({
36399
36548
  );
36400
36549
  return;
36401
36550
  }
36402
- const session = (0, import_auth21.createFederatedAuthSession)({
36551
+ const session = (0, import_auth22.createFederatedAuthSession)({
36403
36552
  pendingAuthId,
36404
36553
  clientId: validatedRequest.client_id,
36405
36554
  redirectUri: validatedRequest.redirect_uri,
@@ -36414,7 +36563,7 @@ var init_oauth_authorize_flow = __esm({
36414
36563
  const challenge = (0, import_utils66.sha256Base64url)(verifier);
36415
36564
  const pkce = { verifier, challenge, method: "S256" };
36416
36565
  const providerState = `federated:${session.id}:${(0, import_utils66.randomUUID)()}`;
36417
- (0, import_auth21.startNextProvider)(session, pkce, providerState);
36566
+ (0, import_auth22.startNextProvider)(session, pkce, providerState);
36418
36567
  await localAuth.federatedSessionStore.store(session);
36419
36568
  const redirectUrl = await localAuth.buildProviderAuthorizeUrl(providerId, {
36420
36569
  state: providerState,
@@ -36515,7 +36664,7 @@ var init_oauth_authorize_flow = __esm({
36515
36664
  callbackPath,
36516
36665
  fields: login?.fields ?? {}
36517
36666
  };
36518
- return (0, import_auth21.renderLocalLoginPage)(login, ctx);
36667
+ return (0, import_auth22.renderLocalLoginPage)(login, ctx);
36519
36668
  }
36520
36669
  /**
36521
36670
  * Render incremental authorization page for a single app using HTMX templates
@@ -36528,7 +36677,7 @@ var init_oauth_authorize_flow = __esm({
36528
36677
  appName,
36529
36678
  description: appDescription
36530
36679
  };
36531
- return (0, import_auth21.buildIncrementalAuthPage)({
36680
+ return (0, import_auth22.buildIncrementalAuthPage)({
36532
36681
  app,
36533
36682
  toolId: toolId || "unknown tool",
36534
36683
  sessionHint: pendingAuthId,
@@ -36549,7 +36698,7 @@ var init_oauth_authorize_flow = __esm({
36549
36698
  appIds: provider.appIds.filter((id) => id !== "__parent__"),
36550
36699
  isPrimary: provider.isParentProvider
36551
36700
  }));
36552
- return (0, import_auth21.buildFederatedLoginPage)({
36701
+ return (0, import_auth22.buildFederatedLoginPage)({
36553
36702
  providers,
36554
36703
  clientName: clientId,
36555
36704
  pendingAuthId,
@@ -36594,8 +36743,8 @@ var init_oauth_authorize_flow = __esm({
36594
36743
  <div class="error-container">
36595
36744
  <div class="error-icon">\u26A0\uFE0F</div>
36596
36745
  <h1>Authorization Error</h1>
36597
- <p><span class="error-code">${(0, import_auth21.escapeHtml)(error)}</span></p>
36598
- <p>${(0, import_auth21.escapeHtml)(description)}</p>
36746
+ <p><span class="error-code">${(0, import_auth22.escapeHtml)(error)}</span></p>
36747
+ <p>${(0, import_auth22.escapeHtml)(description)}</p>
36599
36748
  </div>
36600
36749
  </body>
36601
36750
  </html>`;
@@ -36644,11 +36793,11 @@ function timingSafeEqualStr3(a, b) {
36644
36793
  }
36645
36794
  return diff === 0;
36646
36795
  }
36647
- var import_auth22, import_lazy_zod65, import_utils67, inputSchema17, stateSchema16, outputSchema16, plan16, name16, Stage16, OauthCallbackFlow;
36796
+ var import_auth23, import_lazy_zod65, import_utils67, inputSchema17, stateSchema16, outputSchema16, plan16, name16, Stage16, OauthCallbackFlow;
36648
36797
  var init_oauth_callback_flow = __esm({
36649
36798
  "libs/sdk/src/auth/flows/oauth.callback.flow.ts"() {
36650
36799
  "use strict";
36651
- import_auth22 = require("@frontmcp/auth");
36800
+ import_auth23 = require("@frontmcp/auth");
36652
36801
  import_lazy_zod65 = require("@frontmcp/lazy-zod");
36653
36802
  import_utils67 = require("@frontmcp/utils");
36654
36803
  init_common();
@@ -36739,8 +36888,6 @@ var init_oauth_callback_flow = __esm({
36739
36888
  const email = request.query["email"];
36740
36889
  const name42 = request.query["name"];
36741
36890
  const loginFields = this.collectLoginFields(request);
36742
- const isIncremental = request.query["incremental"] === "true";
36743
- const targetAppId = request.query["app_id"];
36744
36891
  const isFederated = request.query["federated"] === "true";
36745
36892
  const providersParam = request.query["providers"];
36746
36893
  let selectedProviders;
@@ -36759,8 +36906,6 @@ var init_oauth_callback_flow = __esm({
36759
36906
  pendingAuthId,
36760
36907
  email,
36761
36908
  name: name42,
36762
- isIncremental,
36763
- targetAppId,
36764
36909
  isFederated,
36765
36910
  selectedProviders,
36766
36911
  selectedTools,
@@ -36768,9 +36913,6 @@ var init_oauth_callback_flow = __esm({
36768
36913
  loginFields,
36769
36914
  csrf
36770
36915
  });
36771
- if (isIncremental) {
36772
- this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
36773
- }
36774
36916
  if (isFederated) {
36775
36917
  this.logger.info(`Federated login callback with ${selectedProviders?.length || 0} selected providers`);
36776
36918
  }
@@ -36779,31 +36921,13 @@ var init_oauth_callback_flow = __esm({
36779
36921
  }
36780
36922
  }
36781
36923
  async validatePendingAuth() {
36782
- const {
36783
- pendingAuthId,
36784
- email,
36785
- name: name42,
36786
- isIncremental,
36787
- isFederated,
36788
- selectedProviders,
36789
- selectedTools,
36790
- consentSubmitted,
36791
- loginFields
36792
- } = this.state;
36924
+ const { pendingAuthId, email, name: name42, isFederated, selectedProviders, selectedTools, consentSubmitted, loginFields } = this.state;
36793
36925
  if (!pendingAuthId) {
36794
36926
  this.logger.warn("Missing pending_auth_id in callback");
36795
36927
  this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Missing pending_auth_id parameter"), 400));
36796
36928
  return;
36797
36929
  }
36798
36930
  const localAuth = this.scope.auth;
36799
- const localOptions = localAuth.options;
36800
- const authenticateFn = typeof localOptions.authenticate === "function" ? localOptions.authenticate : void 0;
36801
- const requireEmail = localOptions.requireEmail ?? true;
36802
- if (!authenticateFn && !isIncremental && !email && requireEmail) {
36803
- this.logger.warn("Missing email in callback");
36804
- this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Email is required"), 400));
36805
- return;
36806
- }
36807
36931
  const pendingAuth = await localAuth.authorizationStore.getPendingAuthorization(pendingAuthId);
36808
36932
  if (!pendingAuth) {
36809
36933
  this.logger.warn(`Pending authorization not found or expired: ${pendingAuthId}`);
@@ -36815,6 +36939,20 @@ var init_oauth_callback_flow = __esm({
36815
36939
  );
36816
36940
  return;
36817
36941
  }
36942
+ const isIncremental = pendingAuth.isIncremental === true && typeof pendingAuth.incrementalSub === "string";
36943
+ const targetAppId = isIncremental ? pendingAuth.targetAppId : void 0;
36944
+ this.state.set({ isIncremental, targetAppId });
36945
+ if (isIncremental) {
36946
+ this.logger.info(`Incremental auth callback for app: ${targetAppId}`);
36947
+ }
36948
+ const localOptions = localAuth.options;
36949
+ const authenticateFn = typeof localOptions.authenticate === "function" ? localOptions.authenticate : void 0;
36950
+ const requireEmail = localOptions.requireEmail ?? true;
36951
+ if (!authenticateFn && !isIncremental && !email && requireEmail) {
36952
+ this.logger.warn("Missing email in callback");
36953
+ this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Email is required"), 400));
36954
+ return;
36955
+ }
36818
36956
  if (pendingAuth.authUiCsrf) {
36819
36957
  const submitted = this.state.csrf;
36820
36958
  if (!submitted || !timingSafeEqualStr3(pendingAuth.authUiCsrf, submitted)) {
@@ -36829,9 +36967,11 @@ var init_oauth_callback_flow = __esm({
36829
36967
  return;
36830
36968
  }
36831
36969
  let userSub;
36832
- if (email) {
36970
+ if (isIncremental) {
36971
+ userSub = pendingAuth.incrementalSub;
36972
+ } else if (email) {
36833
36973
  userSub = this.generateUserSub(email);
36834
- } else if (!requireEmail || isIncremental) {
36974
+ } else if (!requireEmail) {
36835
36975
  const anonymousSubject = localOptions.anonymousSubject ?? "local-operator";
36836
36976
  userSub = this.generateUserSub(anonymousSubject);
36837
36977
  } else {
@@ -37000,10 +37140,13 @@ var init_oauth_callback_flow = __esm({
37000
37140
  originalState: pendingAuth.state,
37001
37141
  resource: pendingAuth.resource,
37002
37142
  userSub,
37003
- // Progressive/Incremental Authorization from pending record
37004
- isIncremental: pendingAuth.isIncremental || isIncremental,
37005
- targetAppId: pendingAuth.targetAppId || this.state.targetAppId,
37006
- targetToolId: pendingAuth.targetToolId,
37143
+ // Progressive/Incremental Authorization — from the pending record ONLY.
37144
+ // `isIncremental` was derived above from the record plus its proven
37145
+ // subject; OR-ing a client-supplied flag in here is what let a caller
37146
+ // override the server's `false` (GHSA-2c4g-9c8x-6m8g).
37147
+ isIncremental,
37148
+ targetAppId,
37149
+ targetToolId: isIncremental ? pendingAuth.targetToolId : void 0,
37007
37150
  existingSessionId: pendingAuth.existingSessionId,
37008
37151
  existingAuthorizationId: pendingAuth.existingAuthorizationId,
37009
37152
  priorAuthorizedAppIds: pendingAuth.priorAuthorizedAppIds,
@@ -37086,7 +37229,7 @@ var init_oauth_callback_flow = __esm({
37086
37229
  this.respond(httpRespond.html(this.renderErrorPage("server_error", "Authorization request incomplete"), 500));
37087
37230
  return;
37088
37231
  }
37089
- const federatedSession = (0, import_auth22.createFederatedAuthSession)({
37232
+ const federatedSession = (0, import_auth23.createFederatedAuthSession)({
37090
37233
  pendingAuthId: this.state.required.pendingAuthId || (0, import_utils67.randomUUID)(),
37091
37234
  clientId,
37092
37235
  redirectUri,
@@ -37115,7 +37258,7 @@ var init_oauth_callback_flow = __esm({
37115
37258
  method: "S256"
37116
37259
  };
37117
37260
  const providerState = `federated:${federatedSession.id}:${(0, import_utils67.randomUUID)()}`;
37118
- (0, import_auth22.startNextProvider)(federatedSession, pkce, providerState);
37261
+ (0, import_auth23.startNextProvider)(federatedSession, pkce, providerState);
37119
37262
  await sessionStore.update(federatedSession);
37120
37263
  const providerConfig = localAuth.getProviderConfig(firstProviderId);
37121
37264
  if (!providerConfig) {
@@ -37419,7 +37562,7 @@ var init_oauth_callback_flow = __esm({
37419
37562
  fields: login?.fields ?? {},
37420
37563
  error: failure.message
37421
37564
  };
37422
- return (0, import_auth22.renderLocalLoginPage)(login, ctx, fields);
37565
+ return (0, import_auth23.renderLocalLoginPage)(login, ctx, fields);
37423
37566
  }
37424
37567
  /**
37425
37568
  * Render a custom `@AuthUi({ slot: 'consent' })` page when one is registered.
@@ -37515,7 +37658,7 @@ var init_oauth_callback_flow = __esm({
37515
37658
  if (pendingAuth.authUiCsrf) {
37516
37659
  hiddenFields.push({ name: "csrf", value: pendingAuth.authUiCsrf });
37517
37660
  }
37518
- return (0, import_auth22.buildToolConsentPage)({
37661
+ return (0, import_auth23.buildToolConsentPage)({
37519
37662
  tools: toolCards,
37520
37663
  clientName: pendingAuth.clientId,
37521
37664
  pendingAuthId: pendingAuth.id,
@@ -37548,8 +37691,8 @@ var init_oauth_callback_flow = __esm({
37548
37691
  * Render an error page
37549
37692
  */
37550
37693
  renderErrorPage(error, description) {
37551
- const safeError = (0, import_auth22.escapeHtml)(error);
37552
- const safeDescription = (0, import_auth22.escapeHtml)(description);
37694
+ const safeError = (0, import_auth23.escapeHtml)(error);
37695
+ const safeDescription = (0, import_auth23.escapeHtml)(description);
37553
37696
  return `<!DOCTYPE html>
37554
37697
  <html lang="en">
37555
37698
  <head>
@@ -37607,6 +37750,7 @@ var init_oauth_callback_flow = __esm({
37607
37750
  "pending_auth_id",
37608
37751
  "incremental",
37609
37752
  "app_id",
37753
+ "ticket",
37610
37754
  "federated",
37611
37755
  "providers",
37612
37756
  "tools",
@@ -37659,11 +37803,11 @@ var init_oauth_callback_flow = __esm({
37659
37803
  });
37660
37804
 
37661
37805
  // libs/sdk/src/auth/flows/oauth.connect.flow.ts
37662
- var import_auth23, import_lazy_zod66, inputSchema18, stateSchema17, outputSchema17, plan17, name17, Stage17, RESERVED_CONNECT_PARAMS, OauthConnectFlow;
37806
+ var import_auth24, import_lazy_zod66, inputSchema18, stateSchema17, outputSchema17, plan17, name17, Stage17, RESERVED_CONNECT_PARAMS, OauthConnectFlow;
37663
37807
  var init_oauth_connect_flow = __esm({
37664
37808
  "libs/sdk/src/auth/flows/oauth.connect.flow.ts"() {
37665
37809
  "use strict";
37666
- import_auth23 = require("@frontmcp/auth");
37810
+ import_auth24 = require("@frontmcp/auth");
37667
37811
  import_lazy_zod66 = require("@frontmcp/lazy-zod");
37668
37812
  init_common();
37669
37813
  inputSchema18 = httpInputSchema;
@@ -37773,7 +37917,7 @@ var init_oauth_connect_flow = __esm({
37773
37917
  }
37774
37918
  this.logger.info(`Connected ${stored} credential(s) mid-session for key "${payload.key}"`);
37775
37919
  this.notifyResourcesUpdated(payload.key);
37776
- this.respond(httpRespond.html((0, import_auth23.buildConnectSuccessPage)({ key: payload.key })));
37920
+ this.respond(httpRespond.html((0, import_auth24.buildConnectSuccessPage)({ key: payload.key })));
37777
37921
  }
37778
37922
  // ============================================
37779
37923
  // Helpers
@@ -37781,7 +37925,7 @@ var init_oauth_connect_flow = __esm({
37781
37925
  /** Verify the signed resume token against the server secret (constant-time + expiry). */
37782
37926
  verifyToken(token, localAuth) {
37783
37927
  const secret = new TextDecoder().decode(localAuth.secret);
37784
- return (0, import_auth23.verifyCredentialResumeToken)(token, secret);
37928
+ return (0, import_auth24.verifyCredentialResumeToken)(token, secret);
37785
37929
  }
37786
37930
  /** Resolve the single credential field to render from `login.fields` (first field), or a default. */
37787
37931
  resolveConnectField(login, key) {
@@ -37797,8 +37941,8 @@ var init_oauth_connect_flow = __esm({
37797
37941
  renderConnectPage(payload, login, token, error) {
37798
37942
  const { name: name42, def } = this.resolveConnectField(login, payload.key);
37799
37943
  const action = `${this.scope.fullPath}/oauth/connect`;
37800
- const [field] = (0, import_auth23.toLoginExtraFields)({ [name42]: def });
37801
- return (0, import_auth23.buildConnectPage)({
37944
+ const [field] = (0, import_auth24.toLoginExtraFields)({ [name42]: def });
37945
+ return (0, import_auth24.buildConnectPage)({
37802
37946
  token,
37803
37947
  field,
37804
37948
  action,
@@ -37897,8 +38041,8 @@ var init_oauth_connect_flow = __esm({
37897
38041
  return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Connect Error</title></head>
37898
38042
  <body style="font-family:-apple-system,sans-serif;padding:40px;text-align:center">
37899
38043
  <h1 style="color:#e53e3e">Connect Error</h1>
37900
- <p><code>${(0, import_auth23.escapeHtml)(error)}</code></p>
37901
- <p>${(0, import_auth23.escapeHtml)(description)}</p>
38044
+ <p><code>${(0, import_auth24.escapeHtml)(error)}</code></p>
38045
+ <p>${(0, import_auth24.escapeHtml)(description)}</p>
37902
38046
  </body></html>`;
37903
38047
  }
37904
38048
  };
@@ -37925,11 +38069,11 @@ var init_oauth_connect_flow = __esm({
37925
38069
  });
37926
38070
 
37927
38071
  // libs/sdk/src/auth/flows/oauth.provider-callback.flow.ts
37928
- var import_auth24, import_lazy_zod67, import_utils68, inputSchema19, stateSchema18, outputSchema18, plan18, name18, Stage18, OauthProviderCallbackFlow;
38072
+ var import_auth25, import_lazy_zod67, import_utils68, inputSchema19, stateSchema18, outputSchema18, plan18, name18, Stage18, OauthProviderCallbackFlow;
37929
38073
  var init_oauth_provider_callback_flow = __esm({
37930
38074
  "libs/sdk/src/auth/flows/oauth.provider-callback.flow.ts"() {
37931
38075
  "use strict";
37932
- import_auth24 = require("@frontmcp/auth");
38076
+ import_auth25 = require("@frontmcp/auth");
37933
38077
  import_lazy_zod67 = require("@frontmcp/lazy-zod");
37934
38078
  import_utils68 = require("@frontmcp/utils");
37935
38079
  init_common();
@@ -38032,7 +38176,7 @@ var init_oauth_provider_callback_flow = __esm({
38032
38176
  );
38033
38177
  return;
38034
38178
  }
38035
- if (!(0, import_auth24.isSessionComplete)(session)) {
38179
+ if (!(0, import_auth25.isSessionComplete)(session)) {
38036
38180
  this.respond(
38037
38181
  httpRespond.html(this.renderErrorPage("invalid_request", "Authentication is still in progress."), 400)
38038
38182
  );
@@ -38231,7 +38375,7 @@ var init_oauth_provider_callback_flow = __esm({
38231
38375
  this.logger.info(`Stored tokens for provider ${providerId} (temp auth: ${tempAuthId})`);
38232
38376
  }
38233
38377
  if (tokens) {
38234
- (0, import_auth24.completeCurrentProvider)(session, tokens, userInfo);
38378
+ (0, import_auth25.completeCurrentProvider)(session, tokens, userInfo);
38235
38379
  if (userInfo) {
38236
38380
  if (!session.userInfo.sub && userInfo.sub) {
38237
38381
  session.userInfo.sub = userInfo.sub;
@@ -38264,11 +38408,11 @@ var init_oauth_provider_callback_flow = __esm({
38264
38408
  return;
38265
38409
  }
38266
38410
  const localAuth = this.getLocalAuth();
38267
- if ((0, import_auth24.isSessionComplete)(session)) {
38411
+ if ((0, import_auth25.isSessionComplete)(session)) {
38268
38412
  this.logger.info("All providers authenticated, completing federated auth flow");
38269
38413
  return this.completeFederatedAuth(session);
38270
38414
  }
38271
- const nextProviderId = (0, import_auth24.getNextProvider)(session);
38415
+ const nextProviderId = (0, import_auth25.getNextProvider)(session);
38272
38416
  if (!nextProviderId) {
38273
38417
  return this.completeFederatedAuth(session);
38274
38418
  }
@@ -38280,7 +38424,7 @@ var init_oauth_provider_callback_flow = __esm({
38280
38424
  method: "S256"
38281
38425
  };
38282
38426
  const providerState = `federated:${session.id}:${(0, import_utils68.randomUUID)()}`;
38283
- (0, import_auth24.startNextProvider)(session, pkce, providerState);
38427
+ (0, import_auth25.startNextProvider)(session, pkce, providerState);
38284
38428
  const sessionStore = localAuth.federatedSessionStore;
38285
38429
  if (sessionStore) {
38286
38430
  await sessionStore.update(session);
@@ -38443,7 +38587,7 @@ var init_oauth_provider_callback_flow = __esm({
38443
38587
  renderConsentScreen(session, consentConfig, error, preSelectedTools) {
38444
38588
  const callbackPath = `${this.scope.fullPath}/oauth/provider/_consent/callback`;
38445
38589
  const { toolCards } = projectConsentTools(this.scope, consentConfig.excludedTools);
38446
- return (0, import_auth24.buildToolConsentPage)({
38590
+ return (0, import_auth25.buildToolConsentPage)({
38447
38591
  tools: toolCards,
38448
38592
  clientName: session.clientId,
38449
38593
  // The pending_auth_id input is unused on the federated round-trip (we key
@@ -38480,8 +38624,8 @@ var init_oauth_provider_callback_flow = __esm({
38480
38624
  * Render an error page
38481
38625
  */
38482
38626
  renderErrorPage(error, description) {
38483
- const safeError = (0, import_auth24.escapeHtml)(error);
38484
- const safeDescription = (0, import_auth24.escapeHtml)(description);
38627
+ const safeError = (0, import_auth25.escapeHtml)(error);
38628
+ const safeDescription = (0, import_auth25.escapeHtml)(description);
38485
38629
  return `<!DOCTYPE html>
38486
38630
  <html lang="en">
38487
38631
  <head>
@@ -39224,12 +39368,12 @@ var init_oauth_token_flow = __esm({
39224
39368
  });
39225
39369
 
39226
39370
  // libs/sdk/src/auth/flows/oauth.userinfo.flow.ts
39227
- var import_reflect_metadata16, import_auth25, import_lazy_zod70, inputSchema22, stateSchema21, outputSchema21, plan21, name21, Stage21, OauthUserInfoFlow;
39371
+ var import_reflect_metadata16, import_auth26, import_lazy_zod70, inputSchema22, stateSchema21, outputSchema21, plan21, name21, Stage21, OauthUserInfoFlow;
39228
39372
  var init_oauth_userinfo_flow = __esm({
39229
39373
  "libs/sdk/src/auth/flows/oauth.userinfo.flow.ts"() {
39230
39374
  "use strict";
39231
39375
  import_reflect_metadata16 = require("reflect-metadata");
39232
- import_auth25 = require("@frontmcp/auth");
39376
+ import_auth26 = require("@frontmcp/auth");
39233
39377
  import_lazy_zod70 = require("@frontmcp/lazy-zod");
39234
39378
  init_common();
39235
39379
  inputSchema22 = httpInputSchema;
@@ -39249,7 +39393,7 @@ var init_oauth_userinfo_flow = __esm({
39249
39393
  async parseInput() {
39250
39394
  const { request } = this.rawInput;
39251
39395
  const authorizationHeader = request.headers?.["authorization"] ?? void 0;
39252
- const token = (0, import_auth25.extractBearerToken)(authorizationHeader);
39396
+ const token = (0, import_auth26.extractBearerToken)(authorizationHeader);
39253
39397
  const baseUrl = getRequestBaseUrl(request, this.scope.entryPath);
39254
39398
  this.state.set({ token, baseUrl });
39255
39399
  }
@@ -39320,13 +39464,13 @@ var init_oauth_userinfo_flow = __esm({
39320
39464
  });
39321
39465
 
39322
39466
  // libs/sdk/src/auth/flows/session.verify.flow.ts
39323
- var import_reflect_metadata17, import_auth26, import_lazy_zod71, import_utils71, inputSchema23, stateSchema22, UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema, sessionVerifyOutputSchema, plan22, name22, Stage22, warnedUnpinnedAudience, SessionVerifyFlow;
39467
+ var import_reflect_metadata17, import_auth27, import_lazy_zod71, import_utils71, inputSchema23, stateSchema22, UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema, sessionVerifyOutputSchema, plan22, name22, Stage22, warnedUnpinnedAudience, SessionVerifyFlow;
39324
39468
  var init_session_verify_flow = __esm({
39325
39469
  "libs/sdk/src/auth/flows/session.verify.flow.ts"() {
39326
39470
  "use strict";
39327
39471
  init_common();
39328
39472
  import_reflect_metadata17 = require("reflect-metadata");
39329
- import_auth26 = require("@frontmcp/auth");
39473
+ import_auth27 = require("@frontmcp/auth");
39330
39474
  import_lazy_zod71 = require("@frontmcp/lazy-zod");
39331
39475
  import_utils71 = require("@frontmcp/utils");
39332
39476
  init_notification_service();
@@ -39421,7 +39565,7 @@ var init_session_verify_flow = __esm({
39421
39565
  authMode,
39422
39566
  ...platformType !== "unknown" && { platformType }
39423
39567
  };
39424
- const sessionId = (0, import_auth26.encryptJson)(payload);
39568
+ const sessionId = (0, import_auth27.encryptJson)(payload);
39425
39569
  this.respond({
39426
39570
  kind: "authorized",
39427
39571
  authorization: {
@@ -39442,11 +39586,11 @@ var init_session_verify_flow = __esm({
39442
39586
  const sessionIdQuery = request.query["sessionId"];
39443
39587
  const sessionIdHeader = sessionIdRawHeader ?? sessionIdQuery ?? void 0;
39444
39588
  const sessionProtocol = httpTransportHeader ? "http" : sessionIdRawHeader ? "streamable-http" : sessionIdQuery ? "sse" : void 0;
39445
- const token = (0, import_auth26.extractBearerToken)(authorizationHeader);
39589
+ const token = (0, import_auth27.extractBearerToken)(authorizationHeader);
39446
39590
  const userAgent = request.headers?.["user-agent"] ?? void 0;
39447
39591
  const prmMetadataPath = `/.well-known/oauth-protected-resource${entryPath}${routeBase}`;
39448
39592
  const prmUrl = `${getRequestBaseUrl(request)}${prmMetadataPath}`;
39449
- const prmMetadataHeader = (0, import_auth26.buildUnauthorizedHeader)(prmUrl);
39593
+ const prmMetadataHeader = (0, import_auth27.buildUnauthorizedHeader)(prmUrl);
39450
39594
  this.logger.verbose("parseInput", {
39451
39595
  hasAuthHeader: !!authorizationHeader,
39452
39596
  hasToken: !!token,
@@ -39500,7 +39644,7 @@ var init_session_verify_flow = __esm({
39500
39644
  });
39501
39645
  }
39502
39646
  async verifyIfJwt() {
39503
- const jwks = this.get(import_auth26.JwksService);
39647
+ const jwks = this.get(import_auth27.JwksService);
39504
39648
  const token = this.state.token;
39505
39649
  if (!token) {
39506
39650
  this.logger.warn("verifyIfJwt: missing or empty bearer token, returning 401");
@@ -39510,11 +39654,11 @@ var init_session_verify_flow = __esm({
39510
39654
  });
39511
39655
  return;
39512
39656
  }
39513
- if (!(0, import_auth26.isJwt)(token)) {
39657
+ if (!(0, import_auth27.isJwt)(token)) {
39514
39658
  this.logger.warn("verifyIfJwt: token is not a JWT, returning 401");
39515
39659
  this.respond({
39516
39660
  kind: "unauthorized",
39517
- prmMetadataHeader: (0, import_auth26.buildInvalidTokenHeader)(this.state.required.prmUrl, "Token is not a valid JWT")
39661
+ prmMetadataHeader: (0, import_auth27.buildInvalidTokenHeader)(this.state.required.prmUrl, "Token is not a valid JWT")
39518
39662
  });
39519
39663
  return;
39520
39664
  }
@@ -39559,8 +39703,8 @@ var init_session_verify_flow = __esm({
39559
39703
  "transparent audience validation is deriving the expected audience from the request Host: set auth.expectedAudience or FRONTMCP_PUBLIC_URL to bind tokens to a fixed resource. X-Forwarded-Host is ignored unless FRONTMCP_TRUST_PROXY is enabled."
39560
39704
  );
39561
39705
  }
39562
- const expectedAudiences = configuredAudience ? Array.isArray(configuredAudience) ? configuredAudience : [configuredAudience] : (0, import_auth26.deriveExpectedAudience)(this.state.required.baseUrl);
39563
- const audResult = (0, import_auth26.validateAudience)(result.payload?.["aud"], {
39706
+ const expectedAudiences = configuredAudience ? Array.isArray(configuredAudience) ? configuredAudience : [configuredAudience] : (0, import_auth27.deriveExpectedAudience)(this.state.required.baseUrl);
39707
+ const audResult = (0, import_auth27.validateAudience)(result.payload?.["aud"], {
39564
39708
  expectedAudiences,
39565
39709
  // By default accept tokens with no `aud` claim: many IdPs omit it, and
39566
39710
  // rejecting them here would break existing transparent deployments.
@@ -39574,7 +39718,7 @@ var init_session_verify_flow = __esm({
39574
39718
  this.logger.warn("verifyIfJwt: audience validation failed", { error: audResult.error });
39575
39719
  this.respond({
39576
39720
  kind: "unauthorized",
39577
- prmMetadataHeader: (0, import_auth26.buildInvalidTokenHeader)(
39721
+ prmMetadataHeader: (0, import_auth27.buildInvalidTokenHeader)(
39578
39722
  this.state.required.prmUrl,
39579
39723
  audResult.error ?? "Token audience is not valid for this resource"
39580
39724
  )
@@ -39594,7 +39738,7 @@ var init_session_verify_flow = __esm({
39594
39738
  });
39595
39739
  this.respond({
39596
39740
  kind: "forbidden",
39597
- prmMetadataHeader: (0, import_auth26.buildInsufficientScopeHeader)(this.state.required.prmUrl, requiredScopes)
39741
+ prmMetadataHeader: (0, import_auth27.buildInsufficientScopeHeader)(this.state.required.prmUrl, requiredScopes)
39598
39742
  });
39599
39743
  return;
39600
39744
  }
@@ -39604,14 +39748,14 @@ var init_session_verify_flow = __esm({
39604
39748
  this.logger.warn("verifyIfJwt: JWT verification failed", { error: result.error });
39605
39749
  this.respond({
39606
39750
  kind: "unauthorized",
39607
- prmMetadataHeader: (0, import_auth26.buildInvalidTokenHeader)(
39751
+ prmMetadataHeader: (0, import_auth27.buildInvalidTokenHeader)(
39608
39752
  this.state.required.prmUrl,
39609
39753
  result.error ?? "Token verification failed"
39610
39754
  )
39611
39755
  });
39612
39756
  }
39613
39757
  async deriveUser() {
39614
- this.state.set("user", (0, import_auth26.deriveTypedUser)(this.state.required.jwtPayload ?? {}));
39758
+ this.state.set("user", (0, import_auth27.deriveTypedUser)(this.state.required.jwtPayload ?? {}));
39615
39759
  }
39616
39760
  async parseSessionHeader() {
39617
39761
  const {
@@ -39697,13 +39841,13 @@ var init_session_verify_flow = __esm({
39697
39841
  });
39698
39842
 
39699
39843
  // libs/sdk/src/auth/flows/well-known.jwks.flow.ts
39700
- var import_reflect_metadata18, import_auth27, import_lazy_zod72, inputSchema24, stateSchema23, outputSchema22, plan23, name23, Stage23, WellKnownJwksFlow;
39844
+ var import_reflect_metadata18, import_auth28, import_lazy_zod72, inputSchema24, stateSchema23, outputSchema22, plan23, name23, Stage23, WellKnownJwksFlow;
39701
39845
  var init_well_known_jwks_flow = __esm({
39702
39846
  "libs/sdk/src/auth/flows/well-known.jwks.flow.ts"() {
39703
39847
  "use strict";
39704
39848
  init_common();
39705
39849
  import_reflect_metadata18 = require("reflect-metadata");
39706
- import_auth27 = require("@frontmcp/auth");
39850
+ import_auth28 = require("@frontmcp/auth");
39707
39851
  import_lazy_zod72 = require("@frontmcp/lazy-zod");
39708
39852
  init_auth_internal_errors();
39709
39853
  inputSchema24 = httpInputSchema;
@@ -39729,7 +39873,7 @@ var init_well_known_jwks_flow = __esm({
39729
39873
  }
39730
39874
  async collectData() {
39731
39875
  const { isOrchestrated } = this.state.required;
39732
- const jwksSvc = this.get(import_auth27.JwksService);
39876
+ const jwksSvc = this.get(import_auth28.JwksService);
39733
39877
  if (isOrchestrated) {
39734
39878
  const keysDoc = await jwksSvc.getPublicJwks();
39735
39879
  if (!keysDoc?.keys || !Array.isArray(keysDoc.keys)) {
@@ -40059,9 +40203,9 @@ function createSecureStoreProviders(opts) {
40059
40203
  name: "SecureStoreBackend"
40060
40204
  };
40061
40205
  const accessorProvider = {
40062
- provide: import_auth28.SECURE_STORE_ACCESSOR,
40206
+ provide: import_auth29.SECURE_STORE_ACCESSOR,
40063
40207
  inject: () => [FRONTMCP_CONTEXT, FrontMcpLogger],
40064
- useFactory: (ctx, logger) => new import_auth28.SecureStoreAccessorImpl({
40208
+ useFactory: (ctx, logger) => new import_auth29.SecureStoreAccessorImpl({
40065
40209
  backend,
40066
40210
  scope,
40067
40211
  resolveSub: () => resolveRequestSub(ctx),
@@ -40074,11 +40218,11 @@ function createSecureStoreProviders(opts) {
40074
40218
  };
40075
40219
  return [backendProvider, accessorProvider];
40076
40220
  }
40077
- var import_auth28, import_di32, SECURE_STORE_BACKEND;
40221
+ var import_auth29, import_di32, SECURE_STORE_BACKEND;
40078
40222
  var init_secure_store_providers = __esm({
40079
40223
  "libs/sdk/src/auth/secure-store/secure-store.providers.ts"() {
40080
40224
  "use strict";
40081
- import_auth28 = require("@frontmcp/auth");
40225
+ import_auth29 = require("@frontmcp/auth");
40082
40226
  import_di32 = require("@frontmcp/di");
40083
40227
  init_common();
40084
40228
  init_frontmcp_context_provider();
@@ -40088,14 +40232,14 @@ var init_secure_store_providers = __esm({
40088
40232
  });
40089
40233
 
40090
40234
  // libs/sdk/src/auth/secure-store/secure-store.context-extension.ts
40091
- var import_auth29, secureStoreContextExtension;
40235
+ var import_auth30, secureStoreContextExtension;
40092
40236
  var init_secure_store_context_extension = __esm({
40093
40237
  "libs/sdk/src/auth/secure-store/secure-store.context-extension.ts"() {
40094
40238
  "use strict";
40095
- import_auth29 = require("@frontmcp/auth");
40239
+ import_auth30 = require("@frontmcp/auth");
40096
40240
  secureStoreContextExtension = {
40097
40241
  property: "secureStore",
40098
- token: import_auth29.SECURE_STORE_ACCESSOR,
40242
+ token: import_auth30.SECURE_STORE_ACCESSOR,
40099
40243
  errorMessage: "The secure store is not available. Ensure the server uses auth mode 'local' (or 'remote'); the secure store is enabled automatically there."
40100
40244
  };
40101
40245
  }
@@ -40130,12 +40274,15 @@ function validateAuthorizationIssuer(received, expected) {
40130
40274
  }
40131
40275
  return { ok: true };
40132
40276
  }
40133
- var import_jose, import_auth30, import_utils73, defaultNoAuthSecret, RESERVED_JWT_CLAIMS, LocalPrimaryAuth;
40277
+ function isUnsupportedOperation(error) {
40278
+ return error instanceof import_utils73.StorageNotSupportedError;
40279
+ }
40280
+ var import_jose, import_auth31, import_utils73, defaultNoAuthSecret, RESERVED_JWT_CLAIMS, LocalPrimaryAuth;
40134
40281
  var init_instance_local_primary_auth = __esm({
40135
40282
  "libs/sdk/src/auth/instances/instance.local-primary-auth.ts"() {
40136
40283
  "use strict";
40137
40284
  import_jose = require("jose");
40138
- import_auth30 = require("@frontmcp/auth");
40285
+ import_auth31 = require("@frontmcp/auth");
40139
40286
  import_utils73 = require("@frontmcp/utils");
40140
40287
  init_common();
40141
40288
  init_auth2();
@@ -40191,17 +40338,17 @@ var init_instance_local_primary_auth = __esm({
40191
40338
  this.secret = getDefaultNoAuthSecret();
40192
40339
  }
40193
40340
  this.tokenStorage = this.readTokenStorage(options);
40194
- this.authorizationStoreImpl = new import_auth30.InMemoryAuthorizationStore();
40195
- this.federatedSessionStoreImpl = new import_auth30.InMemoryFederatedAuthSessionStore();
40196
- this.orchestratedTokenStoreImpl = new import_auth30.InMemoryOrchestratedTokenStore({
40341
+ this.authorizationStoreImpl = new import_auth31.InMemoryAuthorizationStore();
40342
+ this.federatedSessionStoreImpl = new import_auth31.InMemoryFederatedAuthSessionStore();
40343
+ this.orchestratedTokenStoreImpl = new import_auth31.InMemoryOrchestratedTokenStore({
40197
40344
  encryptionKey: this.secret
40198
40345
  // Reuse JWT secret for token encryption
40199
40346
  });
40200
- this.consentStoreImpl = new import_auth30.InMemoryConsentStore();
40201
- this.dcrClientRegistryImpl = new import_auth30.DcrClientRegistry(this.getDcrConfig() ?? {});
40347
+ this.consentStoreImpl = new import_auth31.InMemoryConsentStore();
40348
+ this.dcrClientRegistryImpl = new import_auth31.DcrClientRegistry(this.getDcrConfig() ?? {});
40202
40349
  if ((0, import_auth10.isOrchestratedMode)(options)) {
40203
40350
  const cimdConfig = options.cimd;
40204
- this.cimdService = new import_auth18.CimdService(this.logger, cimdConfig);
40351
+ this.cimdService = new import_auth19.CimdService(this.logger, cimdConfig);
40205
40352
  }
40206
40353
  this.ready = this.initialize();
40207
40354
  }
@@ -40213,7 +40360,7 @@ var init_instance_local_primary_auth = __esm({
40213
40360
  keys = [];
40214
40361
  secret;
40215
40362
  logger;
40216
- jwks = new import_auth30.JwksService();
40363
+ jwks = new import_auth31.JwksService();
40217
40364
  cimdService;
40218
40365
  /**
40219
40366
  * Token storage backend selected from `options.tokenStorage`.
@@ -40244,6 +40391,17 @@ var init_instance_local_primary_auth = __esm({
40244
40391
  dcrClientRegistryImpl;
40245
40392
  /** Storage adapter backing the persistent stores (kept for disposal). */
40246
40393
  storageAdapter;
40394
+ /**
40395
+ * Replay guard for incremental-authorization tickets (GHSA-2c4g-9c8x-6m8g).
40396
+ * Shares the persistent adapter when one is configured so the guard holds
40397
+ * across a distributed deployment; falls back to a dedicated in-memory
40398
+ * adapter, matching how the credential vault selects its backing store.
40399
+ *
40400
+ * Holds the in-flight PROMISE, not the resolved adapter: two concurrent
40401
+ * claims for the same ticket must not each build their own store, or each
40402
+ * would write and read back its own nonce and both would win.
40403
+ */
40404
+ ticketReplayStorage;
40247
40405
  /**
40248
40406
  * Per-session encrypted credential vault (Checkpoint 3b). Backs
40249
40407
  * `this.credentials` in tools and persists `authenticate()` credentials.
@@ -40381,19 +40539,19 @@ var init_instance_local_primary_auth = __esm({
40381
40539
  * key, so upstream provider tokens stay encrypted at rest in every backend.
40382
40540
  */
40383
40541
  async initializeStores() {
40384
- if (!(0, import_auth30.isPersistentTokenStorage)(this.tokenStorage)) {
40542
+ if (!(0, import_auth31.isPersistentTokenStorage)(this.tokenStorage)) {
40385
40543
  return;
40386
40544
  }
40387
40545
  try {
40388
- const adapter = await (0, import_auth30.createTokenStorageAdapter)(this.tokenStorage);
40546
+ const adapter = await (0, import_auth31.createTokenStorageAdapter)(this.tokenStorage);
40389
40547
  this.storageAdapter = adapter;
40390
- this.authorizationStoreImpl = new import_auth30.StorageAuthorizationStore(adapter);
40391
- this.federatedSessionStoreImpl = new import_auth30.StorageFederatedAuthSessionStore(adapter);
40392
- this.orchestratedTokenStoreImpl = new import_auth30.StorageOrchestratedTokenStore(adapter, {
40548
+ this.authorizationStoreImpl = new import_auth31.StorageAuthorizationStore(adapter);
40549
+ this.federatedSessionStoreImpl = new import_auth31.StorageFederatedAuthSessionStore(adapter);
40550
+ this.orchestratedTokenStoreImpl = new import_auth31.StorageOrchestratedTokenStore(adapter, {
40393
40551
  encryptionKey: this.secret
40394
40552
  });
40395
- this.consentStoreImpl = new import_auth30.StorageConsentStore(adapter);
40396
- const backend = (0, import_auth30.isSqliteTokenStorage)(this.tokenStorage) ? "sqlite" : (0, import_auth30.isRedisTokenStorage)(this.tokenStorage) ? "redis" : "unknown";
40553
+ this.consentStoreImpl = new import_auth31.StorageConsentStore(adapter);
40554
+ const backend = (0, import_auth31.isSqliteTokenStorage)(this.tokenStorage) ? "sqlite" : (0, import_auth31.isRedisTokenStorage)(this.tokenStorage) ? "redis" : "unknown";
40397
40555
  this.logger.info(`Token storage initialized with persistent backend: ${backend}`);
40398
40556
  } catch (err) {
40399
40557
  this.logger.error("Failed to initialize persistent token storage", err);
@@ -40411,6 +40569,81 @@ var init_instance_local_primary_auth = __esm({
40411
40569
  * (`this.secret`), so resume URLs are framework-signed with the same trust
40412
40570
  * root as the access tokens.
40413
40571
  */
40572
+ /**
40573
+ * The server HMAC signing key, as the string form the `signData`/`verifyData`
40574
+ * helpers take. These are the exact bytes the access tokens are signed with,
40575
+ * so framework-signed URLs share the access tokens' trust root.
40576
+ */
40577
+ get signingSecret() {
40578
+ return new TextDecoder().decode(this.secret);
40579
+ }
40580
+ /**
40581
+ * Claim an incremental-authorization ticket, exactly once.
40582
+ *
40583
+ * A signature check proves a ticket was minted by this server for a verified
40584
+ * subject; it cannot prove the ticket has not already been used. The `auth_url`
40585
+ * carrying the ticket travels through agent transcripts and server logs, so a
40586
+ * leaked ticket must not stay usable for the rest of its TTL.
40587
+ *
40588
+ * The claim is race-free without a transaction: each attempt writes its OWN
40589
+ * nonce under `ifNotExists` and reads the key back. Only the attempt whose
40590
+ * nonce survives won, so concurrent replays all lose.
40591
+ *
40592
+ * @returns true when this caller claimed the ticket, false when it was already used.
40593
+ */
40594
+ async claimIncrementalTicket(jti, ttlMs) {
40595
+ const key = `incremental-ticket:${jti}`;
40596
+ const nonce = (0, import_utils73.randomUUID)();
40597
+ const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1e3));
40598
+ let storage = await this.getTicketReplayStorage();
40599
+ try {
40600
+ await storage.set(key, nonce, { ifNotExists: true, ttlSeconds });
40601
+ } catch (error) {
40602
+ if (!isUnsupportedOperation(error)) {
40603
+ this.logger.warn(`Incremental ticket replay guard unavailable: ${String(error)}`);
40604
+ return false;
40605
+ }
40606
+ this.logger.warn(
40607
+ "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)."
40608
+ );
40609
+ storage = await this.useInMemoryTicketReplayStorage();
40610
+ try {
40611
+ await storage.set(key, nonce, { ifNotExists: true, ttlSeconds });
40612
+ } catch (fallbackError) {
40613
+ this.logger.warn(`Incremental ticket replay guard unavailable: ${String(fallbackError)}`);
40614
+ return false;
40615
+ }
40616
+ }
40617
+ try {
40618
+ return await storage.get(key) === nonce;
40619
+ } catch (error) {
40620
+ this.logger.warn(`Incremental ticket replay guard unavailable: ${String(error)}`);
40621
+ return false;
40622
+ }
40623
+ }
40624
+ getTicketReplayStorage() {
40625
+ if (!this.ticketReplayStorage) {
40626
+ if (this.storageAdapter) {
40627
+ this.ticketReplayStorage = Promise.resolve(this.storageAdapter);
40628
+ } else {
40629
+ this.logger.debug(
40630
+ "Incremental ticket replay guard is in-memory (no persistent tokenStorage configured); single use holds within this instance only."
40631
+ );
40632
+ this.ticketReplayStorage = this.useInMemoryTicketReplayStorage();
40633
+ }
40634
+ }
40635
+ return this.ticketReplayStorage;
40636
+ }
40637
+ /** Replace the replay guard with a fresh in-memory adapter, memoized. */
40638
+ useInMemoryTicketReplayStorage() {
40639
+ const pending = (async () => {
40640
+ const memory = new import_utils73.MemoryStorageAdapter();
40641
+ await memory.connect();
40642
+ return memory;
40643
+ })();
40644
+ this.ticketReplayStorage = pending;
40645
+ return pending;
40646
+ }
40414
40647
  async initializeCredentialVault() {
40415
40648
  if ((0, import_auth10.isPublicMode)(this.options)) {
40416
40649
  return;
@@ -40424,12 +40657,12 @@ var init_instance_local_primary_auth = __esm({
40424
40657
  storage = memory;
40425
40658
  }
40426
40659
  const pepper = (0, import_utils73.getEnv)("VAULT_SECRET") ?? (0, import_utils73.getEnv)("JWT_SECRET") ?? void 0;
40427
- this.credentialVaultImpl = new import_auth30.SessionCredentialVault({
40660
+ this.credentialVaultImpl = new import_auth31.SessionCredentialVault({
40428
40661
  storage,
40429
40662
  pepper,
40430
40663
  logger: this.logger.child("SessionCredentialVault")
40431
40664
  });
40432
- const signingSecret = new TextDecoder().decode(this.secret);
40665
+ const signingSecret = this.signingSecret;
40433
40666
  const basePath = this.issuer;
40434
40667
  await this.providers.addDynamicProviders(
40435
40668
  createCredentialsProviders({
@@ -40475,7 +40708,7 @@ var init_instance_local_primary_auth = __esm({
40475
40708
  const config = this.readSecureStoreConfig();
40476
40709
  const reuseAdapter = this.storageAdapter && this.secureStoreSharesTokenStorage(config) ? this.storageAdapter : void 0;
40477
40710
  const pepper = (0, import_utils73.getEnv)("VAULT_SECRET") ?? (0, import_utils73.getEnv)("JWT_SECRET") ?? new TextDecoder().decode(this.secret);
40478
- const resolved = await (0, import_auth30.createSecureStore)({
40711
+ const resolved = await (0, import_auth31.createSecureStore)({
40479
40712
  config,
40480
40713
  pepper,
40481
40714
  storage: reuseAdapter,
@@ -40502,8 +40735,8 @@ var init_instance_local_primary_auth = __esm({
40502
40735
  */
40503
40736
  secureStoreSharesTokenStorage(config) {
40504
40737
  if (typeof config !== "object" || config === null) return false;
40505
- if ("sqlite" in config && config.sqlite) return (0, import_auth30.isSqliteTokenStorage)(this.tokenStorage);
40506
- if ("redis" in config && config.redis) return (0, import_auth30.isRedisTokenStorage)(this.tokenStorage);
40738
+ if ("sqlite" in config && config.sqlite) return (0, import_auth31.isSqliteTokenStorage)(this.tokenStorage);
40739
+ if ("redis" in config && config.redis) return (0, import_auth31.isRedisTokenStorage)(this.tokenStorage);
40507
40740
  return false;
40508
40741
  }
40509
40742
  async signAnonymousJwt() {
@@ -40631,7 +40864,7 @@ var init_instance_local_primary_auth = __esm({
40631
40864
  error_description: "Redirect URI does not match"
40632
40865
  };
40633
40866
  }
40634
- if (!(0, import_auth30.verifyPkce)(codeVerifier, codeRecord.pkce)) {
40867
+ if (!(0, import_auth31.verifyPkce)(codeVerifier, codeRecord.pkce)) {
40635
40868
  this.logger.warn(`PKCE verification failed`);
40636
40869
  return {
40637
40870
  error: "invalid_grant",
@@ -40802,7 +41035,7 @@ var init_instance_local_primary_auth = __esm({
40802
41035
  scope: import_di4.ProviderScope.GLOBAL,
40803
41036
  name: "auth:jwk-service"
40804
41037
  },
40805
- provide: import_auth30.JwksService
41038
+ provide: import_auth31.JwksService
40806
41039
  });
40807
41040
  if (this.cimdService) {
40808
41041
  this.providers.injectProvider({
@@ -40811,7 +41044,7 @@ var init_instance_local_primary_auth = __esm({
40811
41044
  scope: import_di4.ProviderScope.GLOBAL,
40812
41045
  name: "auth:cimd-service"
40813
41046
  },
40814
- provide: import_auth18.CimdService
41047
+ provide: import_auth19.CimdService
40815
41048
  });
40816
41049
  this.logger.debug("CIMD service registered");
40817
41050
  }
@@ -41177,11 +41410,11 @@ var init_instance_local_primary_auth = __esm({
41177
41410
  });
41178
41411
 
41179
41412
  // libs/sdk/src/auth/instances/instance.remote-primary-auth.ts
41180
- var import_auth32, RemotePrimaryAuth;
41413
+ var import_auth33, RemotePrimaryAuth;
41181
41414
  var init_instance_remote_primary_auth = __esm({
41182
41415
  "libs/sdk/src/auth/instances/instance.remote-primary-auth.ts"() {
41183
41416
  "use strict";
41184
- import_auth32 = require("@frontmcp/auth");
41417
+ import_auth33 = require("@frontmcp/auth");
41185
41418
  init_common();
41186
41419
  init_session_verify_flow();
41187
41420
  init_well_known_jwks_flow();
@@ -41197,7 +41430,7 @@ var init_instance_remote_primary_auth = __esm({
41197
41430
  scope;
41198
41431
  providers;
41199
41432
  ready;
41200
- jwks = new import_auth32.JwksService();
41433
+ jwks = new import_auth33.JwksService();
41201
41434
  fetch(input, init) {
41202
41435
  return fetch(input, init);
41203
41436
  }
@@ -41215,7 +41448,7 @@ var init_instance_remote_primary_auth = __esm({
41215
41448
  scope: import_di4.ProviderScope.GLOBAL,
41216
41449
  name: "auth:jwk-service"
41217
41450
  },
41218
- provide: import_auth32.JwksService
41451
+ provide: import_auth33.JwksService
41219
41452
  });
41220
41453
  await this.registerAuthFlows(scope);
41221
41454
  return Promise.resolve();
@@ -41233,12 +41466,12 @@ var init_instance_remote_primary_auth = __esm({
41233
41466
  });
41234
41467
 
41235
41468
  // libs/sdk/src/auth/auth.registry.ts
41236
- var import_reflect_metadata21, import_auth33, import_di33, DEFAULT_AUTH_OPTIONS, AuthRegistry;
41469
+ var import_reflect_metadata21, import_auth34, import_di33, DEFAULT_AUTH_OPTIONS, AuthRegistry;
41237
41470
  var init_auth_registry = __esm({
41238
41471
  "libs/sdk/src/auth/auth.registry.ts"() {
41239
41472
  "use strict";
41240
41473
  import_reflect_metadata21 = require("reflect-metadata");
41241
- import_auth33 = require("@frontmcp/auth");
41474
+ import_auth34 = require("@frontmcp/auth");
41242
41475
  import_di33 = require("@frontmcp/di");
41243
41476
  init_common();
41244
41477
  init_context_extension();
@@ -41270,7 +41503,7 @@ var init_auth_registry = __esm({
41270
41503
  this.logger = providers.get(FrontMcpLogger).child("AuthRegistry");
41271
41504
  this.parsedOptions = (0, import_auth10.parseAuthOptions)(primaryInput ?? DEFAULT_AUTH_OPTIONS);
41272
41505
  const appAuthInfos = this.extractAppAuthInfo(scope);
41273
- this.detection = (0, import_auth33.detectAuthProviders)(this.parsedOptions, appAuthInfos);
41506
+ this.detection = (0, import_auth34.detectAuthProviders)(this.parsedOptions, appAuthInfos);
41274
41507
  this.requiresOrchestration = this.detection.requiresOrchestration;
41275
41508
  this.logDetectionResults();
41276
41509
  this.validateConfiguration();
@@ -44728,6 +44961,7 @@ var init_execute_workflow_tool = __esm({
44728
44961
  "use strict";
44729
44962
  import_lazy_zod81 = require("@frontmcp/lazy-zod");
44730
44963
  init_common();
44964
+ init_errors();
44731
44965
  ExecuteWorkflowTool = class extends ToolContext {
44732
44966
  async execute(input) {
44733
44967
  const scope = this.scope;
@@ -44739,12 +44973,13 @@ var init_execute_workflow_tool = __esm({
44739
44973
  }
44740
44974
  const workflow = workflowRegistry.findByName(input.name);
44741
44975
  if (!workflow) {
44742
- return this.fail(new Error(`Workflow "${input.name}" not found`));
44976
+ return this.fail(new JobNotAuthorizedError(input.name));
44743
44977
  }
44744
44978
  const result = await executionManager.executeWorkflow(workflow, jobRegistry, {
44745
44979
  background: input.background,
44746
44980
  sessionId: this.authInfo.sessionId,
44747
44981
  authInfo: this.authInfo,
44982
+ authoritiesContextBuilder: this.scope.authoritiesContextBuilder,
44748
44983
  workflowInput: input.input
44749
44984
  });
44750
44985
  return result;
@@ -44769,6 +45004,55 @@ var init_execute_workflow_tool = __esm({
44769
45004
  }
44770
45005
  });
44771
45006
 
45007
+ // libs/sdk/src/common/utils/principal.utils.ts
45008
+ function toStringArray(value) {
45009
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
45010
+ if (typeof value === "string" && value.length > 0) return value.split(" ").filter(Boolean);
45011
+ return [];
45012
+ }
45013
+ function asRecord(value) {
45014
+ return value && typeof value === "object" ? value : void 0;
45015
+ }
45016
+ function firstPresent(sources, convert) {
45017
+ for (const source of sources) {
45018
+ if (source !== void 0 && source !== null) return convert(source);
45019
+ }
45020
+ return [];
45021
+ }
45022
+ function resolvePrincipal2(authInfo, contextBuilder) {
45023
+ const info = authInfo ?? {};
45024
+ const user = asRecord(info["user"]) ?? {};
45025
+ const extraAuthorization = asRecord(asRecord(info["extra"])?.["authorization"]);
45026
+ const authInfoClaims = asRecord(info["claims"]) ?? {};
45027
+ const authorizationClaims = asRecord(extraAuthorization?.["claims"]) ?? {};
45028
+ const claims = { ...authorizationClaims, ...authInfoClaims, ...user };
45029
+ const scopes = toStringArray(info["scopes"]).length ? toStringArray(info["scopes"]) : firstPresent([extraAuthorization?.["scopes"], claims["scope"], claims["scopes"]], toStringArray);
45030
+ if (contextBuilder) {
45031
+ const ctx = contextBuilder.build(info);
45032
+ return {
45033
+ sub: ctx.user.sub,
45034
+ roles: ctx.user.roles,
45035
+ permissions: ctx.user.permissions,
45036
+ scopes,
45037
+ claims: ctx.user.claims
45038
+ };
45039
+ }
45040
+ const roles = firstPresent([user["roles"], claims["roles"], extraAuthorization?.["scopes"]], toStringArray);
45041
+ const permissions = firstPresent([user["permissions"], claims["permissions"]], toStringArray);
45042
+ return {
45043
+ sub: typeof user["sub"] === "string" ? user["sub"] : "",
45044
+ roles,
45045
+ permissions,
45046
+ scopes,
45047
+ claims
45048
+ };
45049
+ }
45050
+ var init_principal_utils = __esm({
45051
+ "libs/sdk/src/common/utils/principal.utils.ts"() {
45052
+ "use strict";
45053
+ }
45054
+ });
45055
+
44772
45056
  // libs/sdk/src/workflow/tools/get-workflow-status.tool.ts
44773
45057
  var import_lazy_zod82, GetWorkflowStatusTool;
44774
45058
  var init_get_workflow_status_tool = __esm({
@@ -44776,6 +45060,7 @@ var init_get_workflow_status_tool = __esm({
44776
45060
  "use strict";
44777
45061
  import_lazy_zod82 = require("@frontmcp/lazy-zod");
44778
45062
  init_common();
45063
+ init_principal_utils();
44779
45064
  GetWorkflowStatusTool = class extends ToolContext {
44780
45065
  async execute(input) {
44781
45066
  const scope = this.scope;
@@ -44784,7 +45069,7 @@ var init_get_workflow_status_tool = __esm({
44784
45069
  return this.fail(new Error("Jobs system is not enabled"));
44785
45070
  }
44786
45071
  const record = await executionManager.getStatus(input.runId);
44787
- if (!record) {
45072
+ if (!record || !this.ownsRun(record)) {
44788
45073
  return this.fail(new Error(`Run "${input.runId}" not found`));
44789
45074
  }
44790
45075
  const workflowRecord = record;
@@ -44797,6 +45082,23 @@ var init_get_workflow_status_tool = __esm({
44797
45082
  completedAt: record.completedAt
44798
45083
  };
44799
45084
  }
45085
+ /**
45086
+ * Match on the owning subject, falling back to the session for runs started
45087
+ * before an owner was recorded (and for anonymous/public servers, where every
45088
+ * caller has an empty subject and the session is the only identity there is).
45089
+ *
45090
+ * A record carrying NEITHER identity is refused. Such a run cannot be
45091
+ * attributed to anyone, and its inputs and results are exactly what this check
45092
+ * exists to protect — anyone holding the id would otherwise be able to read
45093
+ * it. Internal callers that must read unattributed runs should go through the
45094
+ * execution manager directly rather than this tool.
45095
+ */
45096
+ ownsRun(record) {
45097
+ const callerSub = resolvePrincipal2(this.authInfo, this.scope.authoritiesContextBuilder).sub;
45098
+ if (record.ownerSub) return record.ownerSub === callerSub;
45099
+ if (record.sessionId) return record.sessionId === this.authInfo.sessionId;
45100
+ return false;
45101
+ }
44800
45102
  };
44801
45103
  GetWorkflowStatusTool = __decorateClass([
44802
45104
  FrontMcpTool({
@@ -44824,6 +45126,53 @@ var init_get_workflow_status_tool = __esm({
44824
45126
  }
44825
45127
  });
44826
45128
 
45129
+ // libs/sdk/src/job/job-permission.guard.ts
45130
+ var JobPermissionGuard;
45131
+ var init_job_permission_guard = __esm({
45132
+ "libs/sdk/src/job/job-permission.guard.ts"() {
45133
+ "use strict";
45134
+ init_principal_utils();
45135
+ JobPermissionGuard = class _JobPermissionGuard {
45136
+ /**
45137
+ * Check whether the caller may perform `action`.
45138
+ *
45139
+ * @param permissions - the entry's declared rules (undefined = unrestricted).
45140
+ * @param action - the action being attempted.
45141
+ * @param authInfo - the request's AuthInfo.
45142
+ * @param contextBuilder - the scope's authorities context builder, when configured.
45143
+ */
45144
+ static async check(permissions, action, authInfo, contextBuilder) {
45145
+ if (!permissions || permissions.length === 0) {
45146
+ return true;
45147
+ }
45148
+ const relevant = permissions.filter((p) => p.action === action);
45149
+ if (relevant.length === 0) {
45150
+ return true;
45151
+ }
45152
+ const principal = resolvePrincipal2(authInfo, contextBuilder);
45153
+ for (const perm of relevant) {
45154
+ const passed = await _JobPermissionGuard.checkSingle(perm, principal, authInfo);
45155
+ if (!passed) return false;
45156
+ }
45157
+ return true;
45158
+ }
45159
+ static async checkSingle(perm, principal, authInfo) {
45160
+ if (perm.roles && perm.roles.length > 0) {
45161
+ if (!perm.roles.some((role) => principal.roles.includes(role))) return false;
45162
+ }
45163
+ if (perm.scopes && perm.scopes.length > 0) {
45164
+ if (!perm.scopes.some((scope) => principal.scopes.includes(scope))) return false;
45165
+ }
45166
+ if (perm.custom) {
45167
+ const result = await perm.custom(authInfo ?? {});
45168
+ if (!result) return false;
45169
+ }
45170
+ return true;
45171
+ }
45172
+ };
45173
+ }
45174
+ });
45175
+
44827
45176
  // libs/sdk/src/workflow/tools/list-workflows.tool.ts
44828
45177
  var import_lazy_zod83, ListWorkflowsTool;
44829
45178
  var init_list_workflows_tool = __esm({
@@ -44831,6 +45180,7 @@ var init_list_workflows_tool = __esm({
44831
45180
  "use strict";
44832
45181
  import_lazy_zod83 = require("@frontmcp/lazy-zod");
44833
45182
  init_common();
45183
+ init_job_permission_guard();
44834
45184
  ListWorkflowsTool = class extends ToolContext {
44835
45185
  async execute(input) {
44836
45186
  const scope = this.scope;
@@ -44842,7 +45192,11 @@ var init_list_workflows_tool = __esm({
44842
45192
  tags: input.tags,
44843
45193
  labels: input.labels
44844
45194
  });
44845
- const mapped = workflows.map((w) => ({
45195
+ const visible = [];
45196
+ for (const workflow of workflows) {
45197
+ if (await this.maySee(workflow)) visible.push(workflow);
45198
+ }
45199
+ const mapped = visible.map((w) => ({
44846
45200
  name: w.name,
44847
45201
  description: w.metadata.description,
44848
45202
  trigger: w.getTrigger(),
@@ -44854,6 +45208,12 @@ var init_list_workflows_tool = __esm({
44854
45208
  count: mapped.length
44855
45209
  };
44856
45210
  }
45211
+ async maySee(workflow) {
45212
+ const permissions = workflow.metadata.permissions;
45213
+ const builder = this.scope.authoritiesContextBuilder;
45214
+ if (!await JobPermissionGuard.check(permissions, "list", this.authInfo, builder)) return false;
45215
+ return JobPermissionGuard.check(permissions, "execute", this.authInfo, builder);
45216
+ }
44857
45217
  };
44858
45218
  ListWorkflowsTool = __decorateClass([
44859
45219
  FrontMcpTool({
@@ -44889,6 +45249,7 @@ var init_register_workflow_tool = __esm({
44889
45249
  "use strict";
44890
45250
  import_lazy_zod84 = require("@frontmcp/lazy-zod");
44891
45251
  init_common();
45252
+ init_errors();
44892
45253
  RegisterWorkflowTool = class extends ToolContext {
44893
45254
  async execute(input) {
44894
45255
  const scope = this.scope;
@@ -44896,6 +45257,9 @@ var init_register_workflow_tool = __esm({
44896
45257
  if (!workflowRegistry) {
44897
45258
  return this.fail(new Error("Workflows system is not enabled"));
44898
45259
  }
45260
+ if (!this.dynamicRegistrationEnabled()) {
45261
+ return this.fail(new DynamicJobRegistrationDisabledError("workflow"));
45262
+ }
44899
45263
  const existing = workflowRegistry.findByName(input.name);
44900
45264
  if (existing) {
44901
45265
  return this.fail(new Error(`Workflow "${input.name}" already exists`));
@@ -44917,6 +45281,10 @@ var init_register_workflow_tool = __esm({
44917
45281
  });
44918
45282
  return { success: true, workflowId };
44919
45283
  }
45284
+ dynamicRegistrationEnabled() {
45285
+ const jobs = this.scope.metadata?.jobs;
45286
+ return jobs?.allowDynamicRegistration === true;
45287
+ }
44920
45288
  };
44921
45289
  RegisterWorkflowTool = __decorateClass([
44922
45290
  FrontMcpTool({
@@ -44955,6 +45323,8 @@ var init_remove_workflow_tool = __esm({
44955
45323
  "use strict";
44956
45324
  import_lazy_zod85 = require("@frontmcp/lazy-zod");
44957
45325
  init_common();
45326
+ init_errors();
45327
+ init_job_permission_guard();
44958
45328
  RemoveWorkflowTool = class extends ToolContext {
44959
45329
  async execute(input) {
44960
45330
  const scope = this.scope;
@@ -44964,7 +45334,16 @@ var init_remove_workflow_tool = __esm({
44964
45334
  }
44965
45335
  const workflow = workflowRegistry.findByName(input.name);
44966
45336
  if (!workflow) {
44967
- return this.fail(new Error(`Workflow "${input.name}" not found`));
45337
+ return this.fail(new JobNotAuthorizedError(input.name));
45338
+ }
45339
+ const allowed = await JobPermissionGuard.check(
45340
+ workflow.metadata.permissions,
45341
+ "delete",
45342
+ this.authInfo,
45343
+ this.scope.authoritiesContextBuilder
45344
+ );
45345
+ if (!allowed) {
45346
+ return this.fail(new JobNotAuthorizedError(input.name));
44968
45347
  }
44969
45348
  if (!workflow.isDynamic()) {
44970
45349
  return this.fail(new Error(`Workflow "${input.name}" is not dynamic and cannot be removed`));
@@ -45574,7 +45953,10 @@ var init_job_execution_manager = __esm({
45574
45953
  "libs/sdk/src/job/execution/job-execution.manager.ts"() {
45575
45954
  "use strict";
45576
45955
  import_utils79 = require("@frontmcp/utils");
45956
+ init_principal_utils();
45957
+ init_errors();
45577
45958
  init_workflow_engine();
45959
+ init_job_permission_guard();
45578
45960
  JobExecutionManager = class {
45579
45961
  stateStore;
45580
45962
  logger;
@@ -45588,6 +45970,7 @@ var init_job_execution_manager = __esm({
45588
45970
  * Execute a job inline (synchronous) or in background.
45589
45971
  */
45590
45972
  async executeJob(job, input, opts = {}) {
45973
+ await this.assertMayExecute(job.metadata.permissions, job.name, opts);
45591
45974
  const runId = (0, import_utils79.randomUUID)();
45592
45975
  const retryConfig = job.metadata.retry ?? {};
45593
45976
  const maxAttempts = retryConfig.maxAttempts ?? 1;
@@ -45596,6 +45979,7 @@ var init_job_execution_manager = __esm({
45596
45979
  jobId: job.metadata.id ?? job.name,
45597
45980
  jobName: job.name,
45598
45981
  sessionId: opts.sessionId,
45982
+ ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
45599
45983
  state: "pending",
45600
45984
  input,
45601
45985
  startedAt: Date.now(),
@@ -45626,6 +46010,7 @@ var init_job_execution_manager = __esm({
45626
46010
  * Execute a workflow inline or in background.
45627
46011
  */
45628
46012
  async executeWorkflow(workflow, jobRegistry, opts = {}) {
46013
+ await this.assertMayExecute(workflow.metadata.permissions, workflow.name, opts);
45629
46014
  const runId = (0, import_utils79.randomUUID)();
45630
46015
  const runRecord = {
45631
46016
  runId,
@@ -45633,6 +46018,7 @@ var init_job_execution_manager = __esm({
45633
46018
  jobName: workflow.name,
45634
46019
  workflowName: workflow.name,
45635
46020
  sessionId: opts.sessionId,
46021
+ ownerSub: resolvePrincipal2(opts.authInfo, opts.authoritiesContextBuilder).sub || void 0,
45636
46022
  state: "pending",
45637
46023
  input: opts.workflowInput,
45638
46024
  startedAt: Date.now(),
@@ -45660,6 +46046,25 @@ var init_job_execution_manager = __esm({
45660
46046
  }
45661
46047
  return this.executeWorkflowInline(workflow, jobRegistry, runId, opts);
45662
46048
  }
46049
+ /**
46050
+ * Throw unless the caller may execute the entry.
46051
+ *
46052
+ * Uses the same error for "not authorized" as the tools use for "not found",
46053
+ * so a caller cannot enumerate which restricted jobs exist by comparing
46054
+ * responses.
46055
+ */
46056
+ async assertMayExecute(permissions, name42, opts) {
46057
+ const allowed = await JobPermissionGuard.check(
46058
+ permissions,
46059
+ "execute",
46060
+ opts.authInfo,
46061
+ opts.authoritiesContextBuilder
46062
+ );
46063
+ if (!allowed) {
46064
+ this.logger.warn(`Execution of "${name42}" denied: caller does not satisfy its execute permissions`);
46065
+ throw new JobNotAuthorizedError(name42);
46066
+ }
46067
+ }
45663
46068
  /**
45664
46069
  * Get execution status.
45665
46070
  */
@@ -46685,6 +47090,7 @@ var init_execute_job_tool = __esm({
46685
47090
  "use strict";
46686
47091
  import_lazy_zod88 = require("@frontmcp/lazy-zod");
46687
47092
  init_common();
47093
+ init_errors();
46688
47094
  ExecuteJobTool = class extends ToolContext {
46689
47095
  async execute(input) {
46690
47096
  const scope = this.scope;
@@ -46695,12 +47101,13 @@ var init_execute_job_tool = __esm({
46695
47101
  }
46696
47102
  const job = jobRegistry.findByName(input.name);
46697
47103
  if (!job) {
46698
- return this.fail(new Error(`Job "${input.name}" not found`));
47104
+ return this.fail(new JobNotAuthorizedError(input.name));
46699
47105
  }
46700
47106
  const result = await executionManager.executeJob(job, input.input ?? {}, {
46701
47107
  background: input.background,
46702
47108
  sessionId: this.authInfo.sessionId,
46703
- authInfo: this.authInfo
47109
+ authInfo: this.authInfo,
47110
+ authoritiesContextBuilder: this.scope.authoritiesContextBuilder
46704
47111
  });
46705
47112
  return result;
46706
47113
  }
@@ -46732,6 +47139,7 @@ var init_get_job_status_tool = __esm({
46732
47139
  "use strict";
46733
47140
  import_lazy_zod89 = require("@frontmcp/lazy-zod");
46734
47141
  init_common();
47142
+ init_principal_utils();
46735
47143
  init_errors();
46736
47144
  GetJobStatusTool = class extends ToolContext {
46737
47145
  async execute(input) {
@@ -46741,7 +47149,7 @@ var init_get_job_status_tool = __esm({
46741
47149
  return this.fail(new GenericServerError("Jobs system is not enabled"));
46742
47150
  }
46743
47151
  const record = await executionManager.getStatus(input.runId);
46744
- if (!record) {
47152
+ if (!record || !this.ownsRun(record)) {
46745
47153
  return this.fail(new InvalidInputError(`Run "${input.runId}" not found`));
46746
47154
  }
46747
47155
  return {
@@ -46756,6 +47164,23 @@ var init_get_job_status_tool = __esm({
46756
47164
  logs: record.logs
46757
47165
  };
46758
47166
  }
47167
+ /**
47168
+ * Match on the owning subject, falling back to the session for runs started
47169
+ * before an owner was recorded (and for anonymous/public servers, where every
47170
+ * caller has an empty subject and the session is the only identity there is).
47171
+ *
47172
+ * A record carrying NEITHER identity is refused. Such a run cannot be
47173
+ * attributed to anyone, and its inputs and results are exactly what this check
47174
+ * exists to protect — anyone holding the id would otherwise be able to read
47175
+ * it. Internal callers that must read unattributed runs should go through the
47176
+ * execution manager directly rather than this tool.
47177
+ */
47178
+ ownsRun(record) {
47179
+ const callerSub = resolvePrincipal2(this.authInfo, this.scope.authoritiesContextBuilder).sub;
47180
+ if (record.ownerSub) return record.ownerSub === callerSub;
47181
+ if (record.sessionId) return record.sessionId === this.authInfo.sessionId;
47182
+ return false;
47183
+ }
46759
47184
  };
46760
47185
  GetJobStatusTool = __decorateClass([
46761
47186
  FrontMcpTool({
@@ -46790,6 +47215,7 @@ var init_list_jobs_tool = __esm({
46790
47215
  "use strict";
46791
47216
  import_lazy_zod90 = require("@frontmcp/lazy-zod");
46792
47217
  init_common();
47218
+ init_job_permission_guard();
46793
47219
  ListJobsTool = class extends ToolContext {
46794
47220
  async execute(input) {
46795
47221
  const jobRegistry = this.scope.jobs;
@@ -46800,7 +47226,11 @@ var init_list_jobs_tool = __esm({
46800
47226
  tags: input.tags,
46801
47227
  labels: input.labels
46802
47228
  });
46803
- const mapped = jobs.map((j) => ({
47229
+ const visible = [];
47230
+ for (const job of jobs) {
47231
+ if (await this.maySee(job)) visible.push(job);
47232
+ }
47233
+ const mapped = visible.map((j) => ({
46804
47234
  name: j.name,
46805
47235
  description: j.metadata.description,
46806
47236
  tags: j.getTags(),
@@ -46812,6 +47242,17 @@ var init_list_jobs_tool = __esm({
46812
47242
  count: mapped.length
46813
47243
  };
46814
47244
  }
47245
+ /**
47246
+ * A job is listable when the caller passes its `list` rules and is not
47247
+ * blocked from executing it. Checking `execute` too keeps the listing honest:
47248
+ * showing a job the caller can never run is the disclosure this closes.
47249
+ */
47250
+ async maySee(job) {
47251
+ const permissions = job.metadata.permissions;
47252
+ const builder = this.scope.authoritiesContextBuilder;
47253
+ if (!await JobPermissionGuard.check(permissions, "list", this.authInfo, builder)) return false;
47254
+ return JobPermissionGuard.check(permissions, "execute", this.authInfo, builder);
47255
+ }
46815
47256
  };
46816
47257
  ListJobsTool = __decorateClass([
46817
47258
  FrontMcpTool({
@@ -46847,6 +47288,7 @@ var init_register_job_tool = __esm({
46847
47288
  "use strict";
46848
47289
  import_lazy_zod91 = require("@frontmcp/lazy-zod");
46849
47290
  init_common();
47291
+ init_errors();
46850
47292
  RegisterJobTool = class extends ToolContext {
46851
47293
  async execute(input) {
46852
47294
  const scope = this.scope;
@@ -46854,6 +47296,9 @@ var init_register_job_tool = __esm({
46854
47296
  if (!jobRegistry) {
46855
47297
  return this.fail(new Error("Jobs system is not enabled"));
46856
47298
  }
47299
+ if (!this.dynamicRegistrationEnabled()) {
47300
+ return this.fail(new DynamicJobRegistrationDisabledError("job"));
47301
+ }
46857
47302
  const existing = jobRegistry.findByName(input.name);
46858
47303
  if (existing) {
46859
47304
  return this.fail(new Error(`Job "${input.name}" already exists`));
@@ -46876,6 +47321,10 @@ var init_register_job_tool = __esm({
46876
47321
  });
46877
47322
  return { success: true, jobId };
46878
47323
  }
47324
+ dynamicRegistrationEnabled() {
47325
+ const jobs = this.scope.metadata?.jobs;
47326
+ return jobs?.allowDynamicRegistration === true;
47327
+ }
46879
47328
  };
46880
47329
  RegisterJobTool = __decorateClass([
46881
47330
  FrontMcpTool({
@@ -46906,6 +47355,8 @@ var init_remove_job_tool = __esm({
46906
47355
  "use strict";
46907
47356
  import_lazy_zod92 = require("@frontmcp/lazy-zod");
46908
47357
  init_common();
47358
+ init_errors();
47359
+ init_job_permission_guard();
46909
47360
  RemoveJobTool = class extends ToolContext {
46910
47361
  async execute(input) {
46911
47362
  const scope = this.scope;
@@ -46915,7 +47366,16 @@ var init_remove_job_tool = __esm({
46915
47366
  }
46916
47367
  const job = jobRegistry.findByName(input.name);
46917
47368
  if (!job) {
46918
- return this.fail(new Error(`Job "${input.name}" not found`));
47369
+ return this.fail(new JobNotAuthorizedError(input.name));
47370
+ }
47371
+ const allowed = await JobPermissionGuard.check(
47372
+ job.metadata.permissions,
47373
+ "delete",
47374
+ this.authInfo,
47375
+ this.scope.authoritiesContextBuilder
47376
+ );
47377
+ if (!allowed) {
47378
+ return this.fail(new JobNotAuthorizedError(input.name));
46919
47379
  }
46920
47380
  if (!job.isDynamic()) {
46921
47381
  return this.fail(new Error(`Job "${input.name}" is not a dynamic job and cannot be removed`));
@@ -46976,14 +47436,18 @@ async function registerJobCapabilities(args) {
46976
47436
  ListJobsTool,
46977
47437
  ExecuteJobTool,
46978
47438
  GetJobStatusTool,
46979
- RegisterJobTool,
46980
47439
  RemoveJobTool,
46981
47440
  ListWorkflowsTool,
46982
47441
  ExecuteWorkflowTool,
46983
47442
  GetWorkflowStatusTool,
46984
- RegisterWorkflowTool,
46985
47443
  RemoveWorkflowTool
46986
47444
  ];
47445
+ if (jobsConfig.allowDynamicRegistration === true) {
47446
+ managementTools.push(RegisterJobTool, RegisterWorkflowTool);
47447
+ logger.warn(
47448
+ "Dynamic job registration is ENABLED: register_job/register_workflow accept a raw script and register it as an executable job."
47449
+ );
47450
+ }
46987
47451
  logger.info(
46988
47452
  `Jobs initialized: ${jobRegistry.getJobs().length} jobs, ${workflowRegistry.getWorkflows().length} workflows`
46989
47453
  );
@@ -51815,6 +52279,7 @@ var init_utils2 = __esm({
51815
52279
  init_caller_dir_utils();
51816
52280
  init_decide_request_intent_utils();
51817
52281
  init_path_utils();
52282
+ init_principal_utils();
51818
52283
  init_global_config_utils();
51819
52284
  init_primitive_filter();
51820
52285
  init_validate_remote_url();
@@ -52762,9 +53227,24 @@ var init_redis_event_store = __esm({
52762
53227
  await client.expire(streamKey, this.ttlSeconds);
52763
53228
  return `${streamId}:${redisId}`;
52764
53229
  }
53230
+ /**
53231
+ * Resolve the stream an event belongs to.
53232
+ *
53233
+ * Optional in the upstream `EventStore` contract, and the transport's
53234
+ * 409-conflict guard (refusing a second live stream for the same id) is dead
53235
+ * code while no store provides it (GHSA-84j6-jc92-77jm).
53236
+ */
53237
+ async getStreamIdForEventId(eventId) {
53238
+ const parsed = this.parseEventId(eventId);
53239
+ return parsed ? parsed[0] : void 0;
53240
+ }
52765
53241
  async replayEventsAfter(lastEventId, { send }) {
53242
+ const parsed = this.parseEventId(lastEventId);
53243
+ if (!parsed) {
53244
+ return "default-stream";
53245
+ }
52766
53246
  const client = await this.getClient();
52767
- const [streamId, redisId] = this.parseEventId(lastEventId);
53247
+ const [streamId, redisId] = parsed;
52768
53248
  const streamKey = `${this.keyPrefix}${streamId}`;
52769
53249
  const entries = await client.xrange(streamKey, `(${redisId}`, "+");
52770
53250
  for (const [id, fields] of entries) {
@@ -52783,16 +53263,32 @@ var init_redis_event_store = __esm({
52783
53263
  * Event IDs have format: `{streamId}:{redisStreamEntryId}`
52784
53264
  * Redis stream entry IDs have format: `{timestamp}-{sequence}`
52785
53265
  *
53266
+ * The entry-id half is validated because it is interpolated into the `xrange`
53267
+ * range argument (`(${redisId}`), where a malformed value is a protocol error
53268
+ * rather than data.
53269
+ *
53270
+ * The stream-id half is NOT character-filtered. `xadd`/`xrange` take the key
53271
+ * as an exact argument — Redis applies no glob matching there — and ioredis
53272
+ * length-prefixes it, so no character is special. Filtering here would also
53273
+ * make `storeEvent`, which accepts any `StreamId`, able to mint ids this
53274
+ * function then refuses, silently breaking replay for that stream. Ownership
53275
+ * is enforced by the session-scoped facade (GHSA-84j6-jc92-77jm), not by
53276
+ * guessing at the id's characters.
53277
+ *
53278
+ * @returns the pair, or `undefined` when the id is malformed.
53279
+ *
52786
53280
  * @example
52787
53281
  * 'my-stream:1234567890123-0' -> ['my-stream', '1234567890123-0']
52788
53282
  */
52789
53283
  parseEventId(eventId) {
52790
- const lastColonBeforeDash = eventId.lastIndexOf(":", eventId.lastIndexOf("-"));
52791
- if (lastColonBeforeDash === -1) {
52792
- const lastColon = eventId.lastIndexOf(":");
52793
- return [eventId.slice(0, lastColon), eventId.slice(lastColon + 1)];
52794
- }
52795
- return [eventId.slice(0, lastColonBeforeDash), eventId.slice(lastColonBeforeDash + 1)];
53284
+ const value = String(eventId);
53285
+ const lastDash = value.lastIndexOf("-");
53286
+ const separator = lastDash === -1 ? value.lastIndexOf(":") : value.lastIndexOf(":", lastDash);
53287
+ if (separator <= 0 || separator === value.length - 1) return void 0;
53288
+ const streamId = value.slice(0, separator);
53289
+ const redisId = value.slice(separator + 1);
53290
+ if (!/^\d+(-\d+)?$/.test(redisId)) return void 0;
53291
+ return [streamId, redisId];
52796
53292
  }
52797
53293
  /**
52798
53294
  * Close the Redis client connection.
@@ -52838,6 +53334,16 @@ var init_memory_event_store = __esm({
52838
53334
  }
52839
53335
  return id;
52840
53336
  }
53337
+ /**
53338
+ * Resolve the stream an event belongs to.
53339
+ *
53340
+ * Optional in the upstream `EventStore` contract, and the transport's
53341
+ * 409-conflict guard (refusing a second live stream for the same id) is dead
53342
+ * code while no store provides it (GHSA-84j6-jc92-77jm).
53343
+ */
53344
+ async getStreamIdForEventId(eventId) {
53345
+ return this.index.get(eventId)?.streamId;
53346
+ }
52841
53347
  async replayEventsAfter(lastEventId, { send }) {
52842
53348
  const meta = this.index.get(lastEventId);
52843
53349
  if (!meta) {
@@ -52989,6 +53495,50 @@ var init_event_store_factory = __esm({
52989
53495
  }
52990
53496
  });
52991
53497
 
53498
+ // libs/sdk/src/transport/event-stores/session-scoped.event-store.ts
53499
+ function createSessionScopedEventStore(store, sessionId) {
53500
+ const prefix = `${sessionId}${SCOPE_SEPARATOR}`;
53501
+ const scope = (streamId) => `${prefix}${streamId}`;
53502
+ const unscope = (streamId) => {
53503
+ const value = String(streamId);
53504
+ return value.startsWith(prefix) ? value.slice(prefix.length) : value;
53505
+ };
53506
+ const resolveOwnedStream = async (eventId) => {
53507
+ const lookup = store.getStreamIdForEventId;
53508
+ if (lookup) {
53509
+ const resolved = await lookup.call(store, eventId);
53510
+ return resolved !== void 0 && String(resolved).startsWith(prefix) ? resolved : void 0;
53511
+ }
53512
+ if (!String(eventId).startsWith(prefix)) return void 0;
53513
+ const value = String(eventId);
53514
+ const lastColon = value.lastIndexOf(":");
53515
+ return lastColon === -1 ? void 0 : value.slice(0, lastColon);
53516
+ };
53517
+ return {
53518
+ async storeEvent(streamId, message) {
53519
+ return store.storeEvent(scope(streamId), message);
53520
+ },
53521
+ async getStreamIdForEventId(eventId) {
53522
+ const owned = await resolveOwnedStream(eventId);
53523
+ return owned === void 0 ? void 0 : unscope(owned);
53524
+ },
53525
+ async replayEventsAfter(lastEventId, options) {
53526
+ const owned = await resolveOwnedStream(lastEventId);
53527
+ if (owned === void 0) {
53528
+ return "default-stream";
53529
+ }
53530
+ return unscope(await store.replayEventsAfter(lastEventId, options));
53531
+ }
53532
+ };
53533
+ }
53534
+ var SCOPE_SEPARATOR;
53535
+ var init_session_scoped_event_store = __esm({
53536
+ "libs/sdk/src/transport/event-stores/session-scoped.event-store.ts"() {
53537
+ "use strict";
53538
+ SCOPE_SEPARATOR = " ";
53539
+ }
53540
+ });
53541
+
52992
53542
  // libs/sdk/src/transport/event-stores/index.ts
52993
53543
  var init_event_stores = __esm({
52994
53544
  "libs/sdk/src/transport/event-stores/index.ts"() {
@@ -54659,6 +55209,61 @@ var init_transport_error = __esm({
54659
55209
  }
54660
55210
  });
54661
55211
 
55212
+ // libs/sdk/src/server/security/host-validation.ts
55213
+ function normalizeHost(value) {
55214
+ const host = value.trim().toLowerCase();
55215
+ if (host.length === 0) return host;
55216
+ const portSeparator = host.startsWith("[") ? host.indexOf(":", host.indexOf("]")) : host.lastIndexOf(":");
55217
+ if (portSeparator === -1) return host;
55218
+ const name42 = host.slice(0, portSeparator);
55219
+ const port = host.slice(portSeparator + 1);
55220
+ if (port.length === 0 || !/^\d+$/.test(port)) return host;
55221
+ return DEFAULT_PORTS.has(port) ? name42 : `${name42}:${port}`;
55222
+ }
55223
+ function normalizeOrigin(value) {
55224
+ const origin = value.trim().toLowerCase();
55225
+ try {
55226
+ return new URL(origin).origin.toLowerCase();
55227
+ } catch {
55228
+ return origin;
55229
+ }
55230
+ }
55231
+ function compileHostValidation(rules) {
55232
+ return {
55233
+ hosts: rules.allowedHosts ? new Set(rules.allowedHosts.map(normalizeHost)) : void 0,
55234
+ origins: rules.allowedOrigins ? new Set(rules.allowedOrigins.map(normalizeOrigin)) : void 0
55235
+ };
55236
+ }
55237
+ function validateHostHeaders(headers, compiled) {
55238
+ if (compiled.hosts) {
55239
+ const host = headers.host;
55240
+ if (!host || !compiled.hosts.has(normalizeHost(host))) {
55241
+ return { status: 403, error: "Forbidden", message: "Invalid Host header" };
55242
+ }
55243
+ const forwardedHost = headers.forwardedHost;
55244
+ if (forwardedHost) {
55245
+ const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
55246
+ if (candidates.some((h2) => !compiled.hosts?.has(normalizeHost(h2)))) {
55247
+ return { status: 403, error: "Forbidden", message: "Invalid X-Forwarded-Host header" };
55248
+ }
55249
+ }
55250
+ }
55251
+ if (compiled.origins) {
55252
+ const origin = headers.origin;
55253
+ if (origin && !compiled.origins.has(normalizeOrigin(origin))) {
55254
+ return { status: 403, error: "Forbidden", message: "Invalid Origin header" };
55255
+ }
55256
+ }
55257
+ return void 0;
55258
+ }
55259
+ var DEFAULT_PORTS;
55260
+ var init_host_validation = __esm({
55261
+ "libs/sdk/src/server/security/host-validation.ts"() {
55262
+ "use strict";
55263
+ DEFAULT_PORTS = /* @__PURE__ */ new Set(["80", "443"]);
55264
+ }
55265
+ });
55266
+
54662
55267
  // libs/sdk/src/transport/adapters/base-sse-transport.ts
54663
55268
  var import_content_type, import_raw_body, import_protocol54, import_utils93, MAXIMUM_MESSAGE_SIZE, SSEServerTransport;
54664
55269
  var init_base_sse_transport = __esm({
@@ -54669,6 +55274,7 @@ var init_base_sse_transport = __esm({
54669
55274
  import_protocol54 = require("@frontmcp/protocol");
54670
55275
  import_utils93 = require("@frontmcp/utils");
54671
55276
  init_transport_errors();
55277
+ init_host_validation();
54672
55278
  MAXIMUM_MESSAGE_SIZE = "4mb";
54673
55279
  SSEServerTransport = class {
54674
55280
  /**
@@ -54692,25 +55298,31 @@ var init_base_sse_transport = __esm({
54692
55298
  onmessage;
54693
55299
  /**
54694
55300
  * Validates request headers for DNS rebinding protection.
55301
+ *
55302
+ * Since v1.7.2 the primary defence is the adapter-level Host validation, which
55303
+ * runs before routing and therefore already covers `/sse` and `/message`
55304
+ * (GHSA-mc9g-v2cp-vfff). These per-transport options remain for a host that
55305
+ * constructs the transport directly; they delegate to the same rules so the
55306
+ * two can never disagree about what a valid Host looks like.
55307
+ *
54695
55308
  * @returns Error message if validation fails, undefined if validation passes.
54696
55309
  */
54697
55310
  validateRequestHeaders(req) {
54698
55311
  if (!this._options.enableDnsRebindingProtection) {
54699
55312
  return void 0;
54700
55313
  }
54701
- if (this._options.allowedHosts && this._options.allowedHosts.length > 0) {
54702
- const hostHeader = req.headers.host;
54703
- if (!hostHeader || !this._options.allowedHosts.includes(hostHeader)) {
54704
- return `Invalid Host header: ${hostHeader}`;
54705
- }
54706
- }
54707
- if (this._options.allowedOrigins && this._options.allowedOrigins.length > 0) {
54708
- const originHeader = req.headers.origin;
54709
- if (!originHeader || !this._options.allowedOrigins.includes(originHeader)) {
54710
- return `Invalid Origin header: ${originHeader}`;
54711
- }
54712
- }
54713
- return void 0;
55314
+ const rejection = validateHostHeaders(
55315
+ {
55316
+ host: req.headers.host,
55317
+ forwardedHost: req.headers["x-forwarded-host"],
55318
+ origin: req.headers.origin
55319
+ },
55320
+ compileHostValidation({
55321
+ allowedHosts: this._options.allowedHosts?.length ? this._options.allowedHosts : void 0,
55322
+ allowedOrigins: this._options.allowedOrigins?.length ? this._options.allowedOrigins : void 0
55323
+ })
55324
+ );
55325
+ return rejection?.message;
54714
55326
  }
54715
55327
  /**
54716
55328
  * Handles the initial SSE connection request.
@@ -55208,7 +55820,7 @@ var init_transport_local_adapter = __esm({
55208
55820
  }
55209
55821
  }
55210
55822
  ensureAuthInfo(req, transport) {
55211
- const { token, user, session } = req[ServerRequestTokens.auth];
55823
+ const { token, user, session, scopes, claims } = req[ServerRequestTokens.auth];
55212
55824
  if (!session?.id) {
55213
55825
  throw new UnauthorizedError(
55214
55826
  "Session could not be reconstructed for the provided token. Re-authenticate to start a new session.",
@@ -55237,7 +55849,16 @@ var init_transport_local_adapter = __esm({
55237
55849
  user,
55238
55850
  sessionId,
55239
55851
  sessionIdPayload: sessionPayload,
55240
- scopes: [],
55852
+ // Carry the scopes and claims the verified Authorization actually holds.
55853
+ // These used to be dropped (`scopes: []`, no claims), which left every
55854
+ // scope- or claim-based check downstream inert.
55855
+ //
55856
+ // Both are COPIED, not aliased. Tool code receives this object, and a
55857
+ // nested mutation (`authInfo.claims.roles.push('admin')`) on a shared
55858
+ // reference would rewrite the request's own verified authorization — which
55859
+ // the job/workflow permission guard then reads.
55860
+ scopes: Array.isArray(scopes) ? [...scopes] : [],
55861
+ ...claims ? { claims: structuredClone(claims) } : {},
55241
55862
  clientId: user.sub ?? "",
55242
55863
  transport
55243
55864
  };
@@ -55718,6 +56339,7 @@ var init_transport_streamable_http_adapter = __esm({
55718
56339
  "use strict";
55719
56340
  import_lazy_zod105 = require("@frontmcp/lazy-zod");
55720
56341
  init_errors();
56342
+ init_session_scoped_event_store();
55721
56343
  init_transport_error();
55722
56344
  init_streamable_http_transport();
55723
56345
  init_transport_local_adapter();
@@ -55727,7 +56349,8 @@ var init_transport_streamable_http_adapter = __esm({
55727
56349
  TransportStreamableHttpAdapter = class extends LocalTransportAdapter {
55728
56350
  createTransport(sessionId, response) {
55729
56351
  const sessionIdGenerator = resolveSessionIdGenerator(this.key.type, sessionId);
55730
- const eventStore = this.scope.eventStore;
56352
+ const sharedEventStore = this.scope.eventStore;
56353
+ const eventStore = sharedEventStore ? createSessionScopedEventStore(sharedEventStore, sessionId) : void 0;
55731
56354
  const isStateless = this.key.type === "stateless-http";
55732
56355
  return new RecreateableStreamableHTTPServerTransport({
55733
56356
  sessionIdGenerator,
@@ -56784,11 +57407,11 @@ function parseUserScopes(user) {
56784
57407
  const s = user?.scope;
56785
57408
  return typeof s === "string" ? s.split(/\s+/).filter(Boolean) : [];
56786
57409
  }
56787
- var import_auth38, import_lazy_zod106, import_utils96, plan40, httpRequestStateSchema, name41, Stage41, HttpRequestFlow;
57410
+ var import_auth39, import_lazy_zod106, import_utils96, plan40, httpRequestStateSchema, name41, Stage41, HttpRequestFlow;
56788
57411
  var init_http_request_flow = __esm({
56789
57412
  "libs/sdk/src/scope/flows/http.request.flow.ts"() {
56790
57413
  "use strict";
56791
- import_auth38 = require("@frontmcp/auth");
57414
+ import_auth39 = require("@frontmcp/auth");
56792
57415
  import_lazy_zod106 = require("@frontmcp/lazy-zod");
56793
57416
  import_utils96 = require("@frontmcp/utils");
56794
57417
  init_session_verify_flow();
@@ -56982,20 +57605,20 @@ var init_http_request_flow = __esm({
56982
57605
  const tokenStore = auth.orchestratedTokenStore;
56983
57606
  let providers;
56984
57607
  try {
56985
- const authorizationId = (0, import_auth38.deriveAuthorizationId)(token);
57608
+ const authorizationId = (0, import_auth39.deriveAuthorizationId)(token);
56986
57609
  const providerIds = await tokenStore.getProviderIds(authorizationId);
56987
57610
  providers = Object.fromEntries(providerIds.map((id) => [id, { id }]));
56988
57611
  } catch (err) {
56989
57612
  this.logger.warn(`[${this.requestId}] failed to load orchestrated providers: ${String(err)}`);
56990
57613
  }
56991
- const authorization = import_auth38.OrchestratedAuthorization.create({
57614
+ const authorization = import_auth39.OrchestratedAuthorization.create({
56992
57615
  token,
56993
57616
  user: { sub: user.sub ?? "unknown", name: user.name, email: user.email, picture: user.picture },
56994
57617
  expiresAt: user.exp ? user.exp * 1e3 : void 0,
56995
57618
  tokenStore,
56996
57619
  providers
56997
57620
  });
56998
- ctx.setContextToken(import_auth38.ORCHESTRATED_AUTH_ACCESSOR, new import_auth38.OrchestratedAuthAccessorAdapter(authorization));
57621
+ ctx.setContextToken(import_auth39.ORCHESTRATED_AUTH_ACCESSOR, new import_auth39.OrchestratedAuthAccessorAdapter(authorization));
56999
57622
  }
57000
57623
  async router() {
57001
57624
  try {
@@ -59606,7 +60229,13 @@ function normalizeAppScope(appItem, metadata) {
59606
60229
  ...metadata,
59607
60230
  id: scopeId,
59608
60231
  apps: [appItem],
59609
- auth: appMetadata.auth,
60232
+ // An app that declares no `auth` INHERITS the server's policy. Writing
60233
+ // `appMetadata.auth` unconditionally set the key to `undefined`, which
60234
+ // overrode the spread above and dropped the server's auth entirely — so a
60235
+ // standalone app silently became public (GHSA-rgxj-434m-vxh3, where the
60236
+ // dashboard's own scope was the case in point). Declaring
60237
+ // `auth: { mode: 'public' }` explicitly is still honoured.
60238
+ auth: appMetadata.auth ?? metadata.auth,
59610
60239
  // For a splitByApp scope, auth (and any auth.ui paths) come from `@App`, so
59611
60240
  // anchor on the App's captured source dir. Fall back to the server's.
59612
60241
  // `__sourceDir` lives on LocalAppMetadata only; read it defensively.
@@ -59870,6 +60499,11 @@ function createWebFetchHandler(scope, options = {}) {
59870
60499
  const entryPaths = new Set((Array.isArray(rawEntry) ? rawEntry : [rawEntry]).map(normalizePath));
59871
60500
  const cors2 = options.cors ?? mapHttpCors(httpConfig?.cors, scope);
59872
60501
  const corsEnabled = cors2?.origin !== void 0 && cors2.origin !== false;
60502
+ const rebinding = httpConfig?.security?.dnsRebindingProtection;
60503
+ const hostValidation = rebinding?.enabled === false || !rebinding?.allowedHosts?.length && !rebinding?.allowedOrigins?.length ? void 0 : compileHostValidation({
60504
+ allowedHosts: rebinding.allowedHosts,
60505
+ allowedOrigins: rebinding.allowedOrigins
60506
+ });
59873
60507
  const corsHeadersFor = (request) => {
59874
60508
  if (!corsEnabled) return {};
59875
60509
  const reqOrigin = request.headers.get("origin") ?? "";
@@ -59900,6 +60534,19 @@ function createWebFetchHandler(scope, options = {}) {
59900
60534
  };
59901
60535
  return async function handle(request, ctx, env) {
59902
60536
  const url = new URL(request.url);
60537
+ if (hostValidation) {
60538
+ const rejection = validateHostHeaders(
60539
+ {
60540
+ host: request.headers.get("host") ?? url.host,
60541
+ forwardedHost: request.headers.get("x-forwarded-host") ?? void 0,
60542
+ origin: request.headers.get("origin") ?? void 0
60543
+ },
60544
+ hostValidation
60545
+ );
60546
+ if (rejection) {
60547
+ return Response.json({ error: rejection.error, message: rejection.message }, { status: rejection.status });
60548
+ }
60549
+ }
59903
60550
  if (corsEnabled && request.method === "OPTIONS") {
59904
60551
  return new Response(null, { status: 204, headers: corsHeadersFor(request) });
59905
60552
  }
@@ -60033,6 +60680,7 @@ var init_web_fetch_handler = __esm({
60033
60680
  init_server_tokens();
60034
60681
  init_path_utils();
60035
60682
  init_errors();
60683
+ init_host_validation();
60036
60684
  init_web_response_renderer();
60037
60685
  }
60038
60686
  });
@@ -60069,26 +60717,20 @@ function auditSecurityDefaults(config, isProduction5) {
60069
60717
  return findings;
60070
60718
  }
60071
60719
  const strict = config.security?.strict === true;
60072
- if (config.cors === void 0 && !strict) {
60720
+ const corsOrigin = config.cors === false ? void 0 : config.cors?.origin;
60721
+ if (corsOrigin === void 0 || corsOrigin === false) {
60073
60722
  findings.push({
60074
- level: "warn",
60075
- code: "CORS_PERMISSIVE_DEFAULT",
60076
- message: "CORS is using the permissive default (origin: true), which allows all origins.",
60077
- recommendation: "Set explicit cors.origin to restrict allowed origins in production."
60723
+ level: "info",
60724
+ code: "CORS_DISABLED",
60725
+ message: "No CORS headers are sent. Cross-origin requests still reach the server \u2014 a browser just will not let the calling page read the response. CORS is not server-side access control."
60078
60726
  });
60079
- } else if (config.cors !== false && config.cors?.origin === true && !strict) {
60727
+ } else if (corsOrigin === true) {
60080
60728
  findings.push({
60081
60729
  level: "warn",
60082
60730
  code: "CORS_ORIGIN_TRUE",
60083
60731
  message: "CORS origin=true allows all origins to make cross-origin requests.",
60084
60732
  recommendation: "Set cors.origin to specific allowed origins."
60085
60733
  });
60086
- } else if (config.cors === false) {
60087
- findings.push({
60088
- level: "info",
60089
- code: "CORS_DISABLED",
60090
- message: "CORS is disabled. Cross-origin requests will be blocked by browsers."
60091
- });
60092
60734
  } else {
60093
60735
  findings.push({
60094
60736
  level: "info",
@@ -60096,7 +60738,7 @@ function auditSecurityDefaults(config, isProduction5) {
60096
60738
  message: "CORS is explicitly configured."
60097
60739
  });
60098
60740
  }
60099
- const bindAddress = config.resolvedBindAddress ?? "0.0.0.0";
60741
+ const bindAddress = config.resolvedBindAddress ?? "127.0.0.1";
60100
60742
  if (bindAddress === "0.0.0.0" || bindAddress === "::") {
60101
60743
  if (config.deploymentMode !== "distributed") {
60102
60744
  findings.push({
@@ -60138,7 +60780,7 @@ function auditSecurityDefaults(config, isProduction5) {
60138
60780
  findings.push({
60139
60781
  level: "info",
60140
60782
  code: "STRICT_MODE_ENABLED",
60141
- message: "Strict security mode is enabled: loopback binding, restrictive CORS, DNS rebinding protection."
60783
+ message: config.deploymentMode === "distributed" ? "Strict security mode is enabled: DNS rebinding protection. Binding is unchanged for a distributed deployment \u2014 it still listens on all interfaces so peers can reach it." : "Strict security mode is enabled: loopback binding and DNS rebinding protection."
60142
60784
  });
60143
60785
  } else {
60144
60786
  findings.push({
@@ -60161,16 +60803,24 @@ function logSecurityFindings(findings, logger) {
60161
60803
  }
60162
60804
  }
60163
60805
  }
60806
+ function toBindAddress(value) {
60807
+ if (value === "loopback") return "127.0.0.1";
60808
+ if (value === "all") return "0.0.0.0";
60809
+ return value;
60810
+ }
60164
60811
  function resolveBindAddress(security, deploymentMode) {
60165
60812
  if (security?.bindAddress) {
60166
- if (security.bindAddress === "loopback") return "127.0.0.1";
60167
- if (security.bindAddress === "all") return "0.0.0.0";
60168
- return security.bindAddress;
60813
+ return toBindAddress(security.bindAddress);
60814
+ }
60815
+ const envBindAddress = process.env["FRONTMCP_BIND_ADDRESS"]?.trim();
60816
+ if (envBindAddress) {
60817
+ return toBindAddress(envBindAddress);
60169
60818
  }
60170
60819
  if (security?.strict) {
60171
60820
  return deploymentMode === "distributed" ? "0.0.0.0" : "127.0.0.1";
60172
60821
  }
60173
- return "0.0.0.0";
60822
+ if (deploymentMode === "distributed") return "0.0.0.0";
60823
+ return "127.0.0.1";
60174
60824
  }
60175
60825
  var init_security_audit = __esm({
60176
60826
  "libs/sdk/src/server/security/security-audit.ts"() {
@@ -60183,39 +60833,22 @@ function createHostValidationMiddleware(options) {
60183
60833
  if (!options.enabled) {
60184
60834
  return (_req, _res, next) => next();
60185
60835
  }
60186
- const allowedHostsSet = options.allowedHosts ? new Set(options.allowedHosts) : void 0;
60187
- const allowedOriginsSet = options.allowedOrigins ? new Set(options.allowedOrigins) : void 0;
60836
+ const compiled = compileHostValidation({
60837
+ allowedHosts: options.allowedHosts,
60838
+ allowedOrigins: options.allowedOrigins
60839
+ });
60188
60840
  return (req, res, next) => {
60189
- if (allowedHostsSet) {
60190
- const host = req.headers?.["host"];
60191
- if (!host || !allowedHostsSet.has(host)) {
60192
- res.status(403).json({
60193
- error: "Forbidden",
60194
- message: "Invalid Host header"
60195
- });
60196
- return;
60197
- }
60198
- const forwardedHost = req.headers?.["x-forwarded-host"];
60199
- if (forwardedHost) {
60200
- const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
60201
- if (candidates.some((h2) => !allowedHostsSet.has(h2))) {
60202
- res.status(403).json({
60203
- error: "Forbidden",
60204
- message: "Invalid X-Forwarded-Host header"
60205
- });
60206
- return;
60207
- }
60208
- }
60209
- }
60210
- if (allowedOriginsSet) {
60211
- const origin = req.headers?.["origin"];
60212
- if (origin && !allowedOriginsSet.has(origin)) {
60213
- res.status(403).json({
60214
- error: "Forbidden",
60215
- message: "Invalid Origin header"
60216
- });
60217
- return;
60218
- }
60841
+ const rejection = validateHostHeaders(
60842
+ {
60843
+ host: req.headers?.["host"],
60844
+ forwardedHost: req.headers?.["x-forwarded-host"],
60845
+ origin: req.headers?.["origin"]
60846
+ },
60847
+ compiled
60848
+ );
60849
+ if (rejection) {
60850
+ res.status(rejection.status).json({ error: rejection.error, message: rejection.message });
60851
+ return;
60219
60852
  }
60220
60853
  next();
60221
60854
  };
@@ -60223,6 +60856,82 @@ function createHostValidationMiddleware(options) {
60223
60856
  var init_host_validation_middleware = __esm({
60224
60857
  "libs/sdk/src/server/middleware/host-validation.middleware.ts"() {
60225
60858
  "use strict";
60859
+ init_host_validation();
60860
+ }
60861
+ });
60862
+
60863
+ // libs/sdk/src/server/security/resolve-allowed-hosts.ts
60864
+ function canonicalizeAddress(address) {
60865
+ const bare = address.trim().toLowerCase().replace(/^\[|\]$/g, "");
60866
+ if (!bare.includes(":")) return bare;
60867
+ try {
60868
+ return new URL(`http://[${bare}]`).hostname.replace(/^\[|\]$/g, "");
60869
+ } catch {
60870
+ return bare;
60871
+ }
60872
+ }
60873
+ function isLoopbackAddress(address) {
60874
+ if (!address) return false;
60875
+ const normalized = canonicalizeAddress(address);
60876
+ return normalized === "::1" || normalized === "localhost" || normalized.startsWith("127.") || // IPv4-mapped IPv6 loopback, which `URL` renders as `::ffff:7f00:1`.
60877
+ normalized === "::ffff:7f00:1" || normalized.startsWith("::ffff:127.");
60878
+ }
60879
+ function withAndWithoutPort(host, port) {
60880
+ return port ? [host, `${host}:${port}`] : [host];
60881
+ }
60882
+ function hostFromUrl(url) {
60883
+ if (!url) return void 0;
60884
+ try {
60885
+ return new URL(url).host || void 0;
60886
+ } catch {
60887
+ return void 0;
60888
+ }
60889
+ }
60890
+ function isRoutableBind(address) {
60891
+ if (!address) return false;
60892
+ return !isLoopbackAddress(address);
60893
+ }
60894
+ function deriveAllowedHosts(input) {
60895
+ const hosts = /* @__PURE__ */ new Set();
60896
+ if (input.socketPath) return [];
60897
+ for (const name42 of LOOPBACK_NAMES) {
60898
+ for (const value of withAndWithoutPort(name42, input.port)) hosts.add(value);
60899
+ }
60900
+ const bindAddress = input.bindAddress?.trim();
60901
+ if (bindAddress && !isLoopbackAddress(bindAddress) && bindAddress !== "0.0.0.0" && bindAddress !== "::") {
60902
+ const literal = bindAddress.includes(":") && !bindAddress.startsWith("[") ? `[${bindAddress}]` : bindAddress;
60903
+ for (const value of withAndWithoutPort(literal, input.port)) hosts.add(value);
60904
+ }
60905
+ const issuerHost = hostFromUrl(input.issuer);
60906
+ if (issuerHost) {
60907
+ hosts.add(issuerHost);
60908
+ const [name42] = issuerHost.split(":");
60909
+ if (!issuerHost.includes(":") && input.port) hosts.add(`${name42}:${input.port}`);
60910
+ }
60911
+ for (const extra of input.extraHosts ?? []) {
60912
+ const trimmed = extra.trim();
60913
+ if (trimmed) hosts.add(trimmed);
60914
+ }
60915
+ return [...hosts];
60916
+ }
60917
+ function shouldEnforceDerivedHosts(input) {
60918
+ if (input.socketPath) return false;
60919
+ const hasExplicitName = hostFromUrl(input.issuer) !== void 0 || (input.extraHosts ?? []).some((h2) => h2.trim());
60920
+ if (hasExplicitName) return true;
60921
+ if (input.port === void 0) return false;
60922
+ return !isRoutableBind(input.bindAddress);
60923
+ }
60924
+ function allowedHostsFromEnv(env = process.env) {
60925
+ const raw = env["FRONTMCP_ALLOWED_HOSTS"]?.trim();
60926
+ if (!raw) return void 0;
60927
+ const hosts = raw.split(",").map((h2) => h2.trim()).filter(Boolean);
60928
+ return hosts.length > 0 ? hosts : void 0;
60929
+ }
60930
+ var LOOPBACK_NAMES;
60931
+ var init_resolve_allowed_hosts = __esm({
60932
+ "libs/sdk/src/server/security/resolve-allowed-hosts.ts"() {
60933
+ "use strict";
60934
+ LOOPBACK_NAMES = ["localhost", "127.0.0.1", "[::1]"];
60226
60935
  }
60227
60936
  });
60228
60937
 
@@ -60248,12 +60957,17 @@ var init_express_host_adapter = __esm({
60248
60957
  import_utils98 = require("@frontmcp/utils");
60249
60958
  init_mcp_error();
60250
60959
  init_host_validation_middleware();
60960
+ init_resolve_allowed_hosts();
60251
60961
  init_base_host_adapter();
60252
60962
  DEFAULT_EXPRESS_BODY_LIMIT = "4mb";
60253
60963
  ExpressHostAdapter = class extends HostServerAdapter {
60254
60964
  app = (0, import_express.default)();
60255
60965
  router = import_express.default.Router();
60256
60966
  prepared = false;
60967
+ /** Active host-validation middleware, or undefined while nothing is enforced. */
60968
+ hostValidation;
60969
+ /** Builds the derived allow-list, once this process commits to listening. */
60970
+ deriveHostValidation;
60257
60971
  constructor(options) {
60258
60972
  super();
60259
60973
  const corsOptions = options?.cors;
@@ -60267,6 +60981,7 @@ var init_express_host_adapter = __esm({
60267
60981
  })
60268
60982
  );
60269
60983
  }
60984
+ this.installHostValidation(options);
60270
60985
  const jsonLimit = options?.bodyLimit ?? DEFAULT_EXPRESS_BODY_LIMIT;
60271
60986
  const formLimit = options?.urlencodedLimit ?? jsonLimit;
60272
60987
  const headerContains = (req, needle) => (req.headers["content-type"] ?? "").toLowerCase().includes(needle);
@@ -60287,23 +61002,6 @@ var init_express_host_adapter = __esm({
60287
61002
  }
60288
61003
  next(err);
60289
61004
  });
60290
- const securityOpts = options?.security;
60291
- if (securityOpts?.dnsRebindingProtection?.enabled || securityOpts?.strict) {
60292
- const allowedHosts = securityOpts.dnsRebindingProtection?.allowedHosts;
60293
- const allowedOrigins = securityOpts.dnsRebindingProtection?.allowedOrigins;
60294
- const effectiveHosts = allowedHosts ?? (securityOpts.strict ? ["localhost", "127.0.0.1"] : void 0);
60295
- if (!effectiveHosts?.length && !allowedOrigins?.length) {
60296
- throw new Error(
60297
- "security.dnsRebindingProtection is enabled but no allowedHosts or allowedOrigins are configured. Provide at least one allowedHosts entry or disable dnsRebindingProtection."
60298
- );
60299
- }
60300
- const hostValidation = createHostValidationMiddleware({
60301
- enabled: true,
60302
- allowedHosts: effectiveHosts,
60303
- allowedOrigins
60304
- });
60305
- this.app.use(hostValidation);
60306
- }
60307
61005
  this.app.use((req, res, next) => {
60308
61006
  if (corsEnabled) {
60309
61007
  res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate, Mcp-Session-Id");
@@ -60313,6 +61011,64 @@ var init_express_host_adapter = __esm({
60313
61011
  next();
60314
61012
  });
60315
61013
  }
61014
+ /**
61015
+ * Install DNS-rebinding protection.
61016
+ *
61017
+ * ON by default since v1.7.2 (BC-035). `allowedHosts` falls back to the names
61018
+ * this process actually answers to — the loopback aliases with and without
61019
+ * the bound port, the bound NIC address, and the configured issuer host.
61020
+ *
61021
+ * Escape hatches, in precedence order: explicit
61022
+ * `security.dnsRebindingProtection.allowedHosts`, the
61023
+ * `FRONTMCP_ALLOWED_HOSTS` env var, or `enabled: false` to turn it off.
61024
+ */
61025
+ installHostValidation(options) {
61026
+ const protection = options?.security?.dnsRebindingProtection;
61027
+ if (protection?.enabled === false) return;
61028
+ const allowedOrigins = protection?.allowedOrigins;
61029
+ const explicitHosts = protection?.allowedHosts ?? allowedHostsFromEnv();
61030
+ if (explicitHosts?.length || allowedOrigins?.length) {
61031
+ this.hostValidation = createHostValidationMiddleware({
61032
+ enabled: true,
61033
+ allowedHosts: explicitHosts?.length ? explicitHosts : void 0,
61034
+ allowedOrigins
61035
+ });
61036
+ } else {
61037
+ this.deriveHostValidation = (portOrSocketPath, bindAddress) => {
61038
+ const isSocket = typeof portOrSocketPath === "string";
61039
+ const derivation = {
61040
+ // `start()` binds `bindAddress ?? '0.0.0.0'`; mirror that exactly.
61041
+ bindAddress: isSocket ? void 0 : bindAddress ?? "0.0.0.0",
61042
+ port: isSocket ? void 0 : portOrSocketPath,
61043
+ socketPath: isSocket ? portOrSocketPath : void 0,
61044
+ issuer: options?.listen?.issuer
61045
+ };
61046
+ if (!shouldEnforceDerivedHosts(derivation)) {
61047
+ console.warn(
61048
+ `[frontmcp] DNS-rebinding protection is not enforcing a Host allow-list: the server binds a routable address (${derivation.bindAddress ?? "unknown"}) and no allowed hosts are configured. Set security.dnsRebindingProtection.allowedHosts (or FRONTMCP_ALLOWED_HOSTS) to your public hostname(s).`
61049
+ );
61050
+ return void 0;
61051
+ }
61052
+ const allowedHosts = deriveAllowedHosts(derivation);
61053
+ if (!allowedHosts.length) return void 0;
61054
+ return createHostValidationMiddleware({ enabled: true, allowedHosts });
61055
+ };
61056
+ }
61057
+ this.app.use(((req, res, next) => {
61058
+ const validate = this.hostValidation;
61059
+ if (!validate) return next();
61060
+ return validate(req, res, next);
61061
+ }));
61062
+ }
61063
+ /**
61064
+ * Enable the DERIVED host allow-list. Called from `start()` only — see
61065
+ * `installHostValidation`.
61066
+ */
61067
+ enableDerivedHostValidation(portOrSocketPath, bindAddress) {
61068
+ if (this.hostValidation || !this.deriveHostValidation) return;
61069
+ this.hostValidation = this.deriveHostValidation(portOrSocketPath, bindAddress);
61070
+ this.deriveHostValidation = void 0;
61071
+ }
60316
61072
  registerRoute(method, path3, handler) {
60317
61073
  this.router[method.toLowerCase()](path3, this.enhancedHandler(handler));
60318
61074
  }
@@ -60345,6 +61101,7 @@ var init_express_host_adapter = __esm({
60345
61101
  return this.app;
60346
61102
  }
60347
61103
  async start(portOrSocketPath, bindAddress) {
61104
+ this.enableDerivedHostValidation(portOrSocketPath, bindAddress);
60348
61105
  this.prepare();
60349
61106
  const server = http.createServer(this.app);
60350
61107
  server.requestTimeout = 0;
@@ -60410,7 +61167,7 @@ var init_server_instance = __esm({
60410
61167
  init_health2();
60411
61168
  init_metrics2();
60412
61169
  init_security_audit();
60413
- DEFAULT_CORS = { origin: true, credentials: false };
61170
+ DEFAULT_CORS = void 0;
60414
61171
  FrontMcpServerInstance = class extends FrontMcpServer {
60415
61172
  config;
60416
61173
  host;
@@ -63234,6 +63991,7 @@ __export(index_exports, {
63234
63991
  DynamicAdapter: () => DynamicAdapter,
63235
63992
  DynamicAdapterNameError: () => DynamicAdapterNameError,
63236
63993
  DynamicJobDirectExecutionError: () => DynamicJobDirectExecutionError,
63994
+ DynamicJobRegistrationDisabledError: () => DynamicJobRegistrationDisabledError,
63237
63995
  DynamicPlugin: () => DynamicPlugin,
63238
63996
  ELICITATION_FALLBACK_JSON_SCHEMA: () => ELICITATION_FALLBACK_JSON_SCHEMA,
63239
63997
  ESM_SH_BASE_URL: () => ESM_SH_BASE_URL,
@@ -63357,7 +64115,7 @@ __export(index_exports, {
63357
64115
  HttpTextSchema: () => HttpTextSchema,
63358
64116
  HttpWebResponseSchema: () => HttpWebResponseSchema,
63359
64117
  ImageOutputSchema: () => ImageOutputSchema,
63360
- InMemoryOrchestratedTokenStore: () => import_auth40.InMemoryOrchestratedTokenStore,
64118
+ InMemoryOrchestratedTokenStore: () => import_auth41.InMemoryOrchestratedTokenStore,
63361
64119
  InMemoryStoreRequiredError: () => import_auth11.InMemoryStoreRequiredError,
63362
64120
  InProcessTaskRunner: () => InProcessTaskRunner,
63363
64121
  InputRequiredSignal: () => InputRequiredSignal,
@@ -63388,6 +64146,7 @@ __export(index_exports, {
63388
64146
  JobExecutionManager: () => JobExecutionManager,
63389
64147
  JobInstance: () => JobInstance,
63390
64148
  JobKind: () => JobKind,
64149
+ JobNotAuthorizedError: () => JobNotAuthorizedError,
63391
64150
  JobPermissionGuard: () => JobPermissionGuard,
63392
64151
  JobRegistry: () => JobRegistry,
63393
64152
  LLM_ADAPTER: () => LLM_ADAPTER,
@@ -63905,6 +64664,7 @@ __export(index_exports, {
63905
64664
  resolveNameConflicts: () => resolveNameConflicts,
63906
64665
  resolveOutputSchemaMode: () => resolveOutputSchemaMode,
63907
64666
  resolvePendingTC39HooksForClass: () => resolvePendingTC39HooksForClass,
64667
+ resolvePrincipal: () => resolvePrincipal2,
63908
64668
  resolveRegistryToken: () => resolveRegistryToken,
63909
64669
  resolveSchemaDescriptionFormat: () => resolveSchemaDescriptionFormat,
63910
64670
  resolveToolVisibility: () => resolveToolVisibility,
@@ -63997,49 +64757,7 @@ init_job_registry();
63997
64757
  init_job_instance();
63998
64758
  init_job_events();
63999
64759
  init_job_utils();
64000
-
64001
- // libs/sdk/src/job/job-permission.guard.ts
64002
- var JobPermissionGuard = class _JobPermissionGuard {
64003
- /**
64004
- * Check if the given auth info is allowed to perform the action.
64005
- * Returns true if no permissions are defined (permissive by default).
64006
- */
64007
- static async check(permissions, action, authInfo) {
64008
- if (!permissions || permissions.length === 0) {
64009
- return true;
64010
- }
64011
- const relevant = permissions.filter((p) => p.action === action);
64012
- if (relevant.length === 0) {
64013
- return true;
64014
- }
64015
- for (const perm of relevant) {
64016
- const passed = await _JobPermissionGuard.checkSingle(perm, authInfo);
64017
- if (!passed) return false;
64018
- }
64019
- return true;
64020
- }
64021
- static async checkSingle(perm, authInfo) {
64022
- if (perm.roles && perm.roles.length > 0) {
64023
- const userRoles = authInfo["roles"];
64024
- if (!Array.isArray(userRoles)) return false;
64025
- const hasRole = perm.roles.some((r) => userRoles.includes(r));
64026
- if (!hasRole) return false;
64027
- }
64028
- if (perm.scopes && perm.scopes.length > 0) {
64029
- const userScopes = authInfo["scopes"] ?? authInfo["scope"];
64030
- const scopeArr = typeof userScopes === "string" ? userScopes.split(" ") : Array.isArray(userScopes) ? userScopes : [];
64031
- const hasScope = perm.scopes.some((s) => scopeArr.includes(s));
64032
- if (!hasScope) return false;
64033
- }
64034
- if (perm.custom) {
64035
- const result = await perm.custom(authInfo);
64036
- if (!result) return false;
64037
- }
64038
- return true;
64039
- }
64040
- };
64041
-
64042
- // libs/sdk/src/job/index.ts
64760
+ init_job_permission_guard();
64043
64761
  init_job_scope_helper();
64044
64762
 
64045
64763
  // libs/sdk/src/job/execution/index.ts
@@ -64335,7 +65053,7 @@ init_mcp_20260728();
64335
65053
  init_mcp_stateless_client_adapter();
64336
65054
  var import_protocol57 = require("@frontmcp/protocol");
64337
65055
  init_ext_apps2();
64338
- var import_auth40 = require("@frontmcp/auth");
65056
+ var import_auth41 = require("@frontmcp/auth");
64339
65057
  (function suppressZodExpressWarning() {
64340
65058
  if (typeof process === "undefined" || typeof process.emitWarning !== "function") return;
64341
65059
  const originalEmitWarning = process.emitWarning.bind(process);
@@ -64442,6 +65160,7 @@ var ChannelListHook = FlowHooksOf("channels:list");
64442
65160
  DynamicAdapter,
64443
65161
  DynamicAdapterNameError,
64444
65162
  DynamicJobDirectExecutionError,
65163
+ DynamicJobRegistrationDisabledError,
64445
65164
  DynamicPlugin,
64446
65165
  ELICITATION_FALLBACK_JSON_SCHEMA,
64447
65166
  ESM_SH_BASE_URL,
@@ -64596,6 +65315,7 @@ var ChannelListHook = FlowHooksOf("channels:list");
64596
65315
  JobExecutionManager,
64597
65316
  JobInstance,
64598
65317
  JobKind,
65318
+ JobNotAuthorizedError,
64599
65319
  JobPermissionGuard,
64600
65320
  JobRegistry,
64601
65321
  LLM_ADAPTER,
@@ -65112,6 +65832,7 @@ var ChannelListHook = FlowHooksOf("channels:list");
65112
65832
  resolveNameConflicts,
65113
65833
  resolveOutputSchemaMode,
65114
65834
  resolvePendingTC39HooksForClass,
65835
+ resolvePrincipal,
65115
65836
  resolveRegistryToken,
65116
65837
  resolveSchemaDescriptionFormat,
65117
65838
  resolveToolVisibility,