@frontmcp/sdk 1.7.1 → 1.7.2

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