@synapsor/runner 1.5.3 → 1.5.4

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 (39) hide show
  1. package/CHANGELOG.md +44 -2
  2. package/README.md +20 -12
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/runner.mjs +2332 -501
  5. package/dist/runtime.mjs +788 -148
  6. package/dist/shadow.mjs +101 -1
  7. package/docs/capability-authoring.md +6 -1
  8. package/docs/cloud-mode.md +18 -0
  9. package/docs/database-enforced-scope.md +8 -0
  10. package/docs/dsl-reference.md +45 -4
  11. package/docs/getting-started-own-database.md +6 -3
  12. package/docs/guarded-crud-writeback.md +10 -1
  13. package/docs/http-mcp.md +222 -207
  14. package/docs/limitations.md +9 -2
  15. package/docs/local-mode.md +5 -2
  16. package/docs/mcp-client-setup.md +24 -11
  17. package/docs/mcp-clients.md +10 -4
  18. package/docs/openai-agents-sdk.md +16 -2
  19. package/docs/production.md +72 -7
  20. package/docs/release-notes.md +44 -3
  21. package/docs/runner-bundles.md +7 -2
  22. package/docs/runner-config-reference.md +96 -6
  23. package/docs/running-a-runner-fleet.md +42 -13
  24. package/docs/security-boundary.md +38 -3
  25. package/docs/store-lifecycle.md +93 -5
  26. package/examples/openai-agents-http/README.md +10 -4
  27. package/examples/openai-agents-http/agent.py +2 -2
  28. package/examples/runner-fleet/Dockerfile +7 -2
  29. package/examples/runner-fleet/README.md +11 -7
  30. package/examples/runner-fleet/docker-compose.yml +4 -4
  31. package/examples/runner-fleet/mint-dev-token.mjs +1 -1
  32. package/examples/runner-fleet/start-tls-runner.sh +21 -0
  33. package/examples/runner-fleet/synapsor.runner.json +27 -1
  34. package/examples/support-plan-credit/README.md +6 -1
  35. package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.json +4 -1
  36. package/examples/support-plan-credit/mcp-client-examples/openai-agents-streamable-http.ts +4 -0
  37. package/examples/support-plan-credit/synapsor.contract.json +0 -3
  38. package/package.json +1 -1
  39. package/schemas/synapsor.runner.schema.json +80 -0
package/dist/runtime.mjs CHANGED
@@ -11,9 +11,11 @@ import fs2 from "node:fs";
11
11
  import { createServer } from "node:http";
12
12
  import { createServer as createHttpsServer } from "node:https";
13
13
  import path2 from "node:path";
14
+ import { createSecureContext } from "node:tls";
14
15
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
15
16
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
17
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
+ import { OAuthProtectedResourceMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
17
19
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
18
20
 
19
21
  // node_modules/.pnpm/@modelcontextprotocol+ext-apps@1.7.4_@modelcontextprotocol+sdk@1.29.0_zod@3.25.76__zod@3.25.76/node_modules/@modelcontextprotocol/ext-apps/dist/src/server/index.js
@@ -118,7 +120,7 @@ function N3(Z, $, J, X, V) {
118
120
  }
119
121
 
120
122
  // packages/config/src/index.ts
121
- var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "operator_identity", "session_auth", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "strict", "result_format"]);
123
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "operator_identity", "session_auth", "http_security", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "strict", "result_format"]);
122
124
  var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path", "shared_postgres"]);
123
125
  var SHARED_POSTGRES_STORAGE_KEYS = /* @__PURE__ */ new Set(["mode", "url_env", "schema", "lock_timeout_ms", "max_entries"]);
124
126
  var APPROVALS_KEYS = /* @__PURE__ */ new Set(["disable_auto_approval"]);
@@ -180,6 +182,26 @@ var SESSION_AUTH_KEYS = /* @__PURE__ */ new Set([
180
182
  "fetch_timeout_ms",
181
183
  "max_response_bytes"
182
184
  ]);
185
+ var HTTP_SECURITY_KEYS = /* @__PURE__ */ new Set(["deployment", "channel", "static_token", "oauth_resource", "allowed_origins", "allowed_hosts", "limits"]);
186
+ var HTTP_STATIC_TOKEN_KEYS = /* @__PURE__ */ new Set(["active_env", "previous_env"]);
187
+ var HTTP_OAUTH_RESOURCE_KEYS = /* @__PURE__ */ new Set([
188
+ "resource",
189
+ "authorization_servers",
190
+ "scopes_supported",
191
+ "required_scopes",
192
+ "resource_name",
193
+ "resource_documentation"
194
+ ]);
195
+ var HTTP_LIMIT_KEYS = /* @__PURE__ */ new Set([
196
+ "max_request_bytes",
197
+ "max_header_bytes",
198
+ "max_sessions",
199
+ "session_idle_timeout_seconds",
200
+ "request_timeout_ms",
201
+ "headers_timeout_ms",
202
+ "keep_alive_timeout_ms",
203
+ "max_connections"
204
+ ]);
183
205
  var RATE_LIMITS_KEYS = /* @__PURE__ */ new Set(["enabled", "default", "capabilities"]);
184
206
  var RATE_LIMIT_RULE_KEYS = /* @__PURE__ */ new Set(["requests", "window_seconds"]);
185
207
  var CLOUD_KEYS = /* @__PURE__ */ new Set(["base_url_env", "runner_token_env", "runner_id", "runner_version", "project_id", "adapter_id", "source_id", "engines", "capabilities", "session"]);
@@ -337,6 +359,7 @@ function validateRunnerCapabilityConfig(input) {
337
359
  validatePolicies(input.policies, strict, errors);
338
360
  validateOperatorIdentity(input.operator_identity, strict, errors);
339
361
  validateSessionAuth(input.session_auth, input.trusted_context, input.contexts, strict, errors);
362
+ validateHttpSecurity(input.http_security, input.session_auth, input.trusted_context, input.contexts, strict, errors, warnings);
340
363
  validateRateLimits(input.rate_limits, strict, errors);
341
364
  validateMetrics(input.metrics, strict, errors);
342
365
  validateGraduatedTrust(input.graduated_trust, strict, errors);
@@ -347,6 +370,169 @@ function validateRunnerCapabilityConfig(input) {
347
370
  scanForForbiddenFields(input, "$", errors);
348
371
  return { ok: errors.length === 0, errors, warnings };
349
372
  }
373
+ function validateHttpSecurity(value, sessionAuth, trustedContext, contexts, strict, errors, warnings) {
374
+ if (value === void 0) return;
375
+ if (!isRecord(value)) {
376
+ errors.push({ path: "$.http_security", code: "HTTP_SECURITY_NOT_OBJECT", message: "http_security must be a Runner deployment configuration object." });
377
+ return;
378
+ }
379
+ if (strict) checkUnknownKeys(value, HTTP_SECURITY_KEYS, "$.http_security", errors);
380
+ const deployment = value.deployment;
381
+ if (deployment !== void 0 && deployment !== "loopback" && deployment !== "single_tenant" && deployment !== "shared") {
382
+ errors.push({ path: "$.http_security.deployment", code: "INVALID_HTTP_DEPLOYMENT", message: "http_security.deployment must be loopback, single_tenant, or shared." });
383
+ }
384
+ const channel = value.channel;
385
+ if (channel !== void 0 && channel !== "direct_tls" && channel !== "trusted_tls_proxy" && channel !== "insecure_http_break_glass") {
386
+ errors.push({ path: "$.http_security.channel", code: "INVALID_HTTP_CHANNEL", message: "http_security.channel must be direct_tls, trusted_tls_proxy, or insecure_http_break_glass." });
387
+ }
388
+ validateHttpStaticToken(value.static_token, strict, errors);
389
+ validateHttpOauthResource(value.oauth_resource, strict, errors);
390
+ validateExactOrigins(value.allowed_origins, errors);
391
+ validateAllowedHosts(value.allowed_hosts, errors);
392
+ validateHttpLimits(value.limits, strict, errors);
393
+ const contextsUseClaims = isRecord(trustedContext) && trustedContext.provider === "http_claims" || isRecord(contexts) && Object.values(contexts).some((context) => isRecord(context) && context.provider === "http_claims");
394
+ if (deployment === "shared") {
395
+ if (!contextsUseClaims) {
396
+ errors.push({ path: "$.http_security.deployment", code: "SHARED_HTTP_CLAIMS_REQUIRED", message: "shared HTTP deployment requires http_claims trusted context so tenant and principal come from verified per-session identity." });
397
+ }
398
+ if (!isRecord(sessionAuth)) {
399
+ errors.push({ path: "$.session_auth", code: "SHARED_HTTP_SESSION_AUTH_REQUIRED", message: "shared HTTP deployment requires signed session_auth." });
400
+ } else {
401
+ if (!isNonEmptyString(sessionAuth.issuer)) errors.push({ path: "$.session_auth.issuer", code: "SHARED_HTTP_ISSUER_REQUIRED", message: "shared HTTP deployment requires an exact JWT issuer." });
402
+ if (!isNonEmptyString(sessionAuth.audience)) errors.push({ path: "$.session_auth.audience", code: "SHARED_HTTP_AUDIENCE_REQUIRED", message: "shared HTTP deployment requires an exact JWT audience/resource." });
403
+ if (sessionAuth.provider === "jwt_hs256") {
404
+ warnings.push({ path: "$.session_auth.provider", code: "SHARED_HTTP_HS256_WARNING", message: "Shared HTTP uses a symmetric JWT verification key. Prefer jwt_asymmetric with RS256/ES256 and JWKS for production." });
405
+ }
406
+ }
407
+ if (!isRecord(value.oauth_resource)) {
408
+ errors.push({ path: "$.http_security.oauth_resource", code: "SHARED_HTTP_OAUTH_RESOURCE_REQUIRED", message: "shared HTTP deployment requires RFC 9728 protected-resource metadata for an external authorization server." });
409
+ } else if (isRecord(sessionAuth) && isNonEmptyString(sessionAuth.audience) && value.oauth_resource.resource !== sessionAuth.audience) {
410
+ errors.push({ path: "$.http_security.oauth_resource.resource", code: "HTTP_RESOURCE_AUDIENCE_MISMATCH", message: "oauth_resource.resource must exactly match session_auth.audience so tokens are bound to this Runner resource." });
411
+ }
412
+ }
413
+ if (deployment !== "shared" && value.oauth_resource !== void 0 && !contextsUseClaims) {
414
+ errors.push({ path: "$.http_security.oauth_resource", code: "HTTP_OAUTH_CLAIMS_REQUIRED", message: "OAuth protected-resource metadata requires signed http_claims session identity." });
415
+ }
416
+ }
417
+ function validateHttpStaticToken(value, strict, errors) {
418
+ if (value === void 0) return;
419
+ if (!isRecord(value)) {
420
+ errors.push({ path: "$.http_security.static_token", code: "HTTP_STATIC_TOKEN_NOT_OBJECT", message: "http_security.static_token must contain environment-variable names only." });
421
+ return;
422
+ }
423
+ if (strict) checkUnknownKeys(value, HTTP_STATIC_TOKEN_KEYS, "$.http_security.static_token", errors);
424
+ if (value.active_env !== void 0 && !isEnvName(value.active_env)) {
425
+ errors.push({ path: "$.http_security.static_token.active_env", code: "INVALID_HTTP_TOKEN_ENV", message: "active_env must name the environment variable containing the opaque endpoint token." });
426
+ }
427
+ if (value.previous_env !== void 0 && !isEnvName(value.previous_env)) {
428
+ errors.push({ path: "$.http_security.static_token.previous_env", code: "INVALID_HTTP_PREVIOUS_TOKEN_ENV", message: "previous_env must name the one previous token accepted during an operator-controlled rotation window." });
429
+ }
430
+ if (value.active_env !== void 0 && value.active_env === value.previous_env) {
431
+ errors.push({ path: "$.http_security.static_token.previous_env", code: "HTTP_TOKEN_ENV_REUSED", message: "active_env and previous_env must be different environment variables." });
432
+ }
433
+ }
434
+ function validateHttpOauthResource(value, strict, errors) {
435
+ if (value === void 0) return;
436
+ if (!isRecord(value)) {
437
+ errors.push({ path: "$.http_security.oauth_resource", code: "HTTP_OAUTH_RESOURCE_NOT_OBJECT", message: "oauth_resource must describe this Runner protected resource and its external authorization server." });
438
+ return;
439
+ }
440
+ if (strict) checkUnknownKeys(value, HTTP_OAUTH_RESOURCE_KEYS, "$.http_security.oauth_resource", errors);
441
+ if (!isHttpsUrl(value.resource)) {
442
+ errors.push({ path: "$.http_security.oauth_resource.resource", code: "INVALID_HTTP_OAUTH_RESOURCE", message: "oauth_resource.resource must be an HTTPS URL for the exact public MCP endpoint." });
443
+ }
444
+ if (!Array.isArray(value.authorization_servers) || value.authorization_servers.length < 1 || value.authorization_servers.length > 8) {
445
+ errors.push({ path: "$.http_security.oauth_resource.authorization_servers", code: "HTTP_AUTHORIZATION_SERVERS_REQUIRED", message: "oauth_resource.authorization_servers must contain 1 through 8 external authorization-server HTTPS issuer URLs." });
446
+ } else {
447
+ value.authorization_servers.forEach((server, index) => {
448
+ if (!isHttpsUrl(server)) errors.push({ path: `$.http_security.oauth_resource.authorization_servers[${index}]`, code: "INVALID_HTTP_AUTHORIZATION_SERVER", message: "authorization server URLs must use HTTPS and contain no credentials, query, or fragment." });
449
+ });
450
+ }
451
+ for (const key of ["scopes_supported", "required_scopes"]) {
452
+ const scopes = value[key];
453
+ if (scopes !== void 0 && (!Array.isArray(scopes) || scopes.length < 1 || scopes.length > 64 || scopes.some((scope) => !isOAuthScope(scope)))) {
454
+ errors.push({ path: `$.http_security.oauth_resource.${key}`, code: "INVALID_HTTP_OAUTH_SCOPES", message: `${key} must contain 1 through 64 unique, visible OAuth scope strings.` });
455
+ } else if (Array.isArray(scopes) && new Set(scopes).size !== scopes.length) {
456
+ errors.push({ path: `$.http_security.oauth_resource.${key}`, code: "DUPLICATE_HTTP_OAUTH_SCOPE", message: `${key} must not contain duplicate scopes.` });
457
+ }
458
+ }
459
+ if (Array.isArray(value.required_scopes) && Array.isArray(value.scopes_supported)) {
460
+ for (const scope of value.required_scopes) {
461
+ if (!value.scopes_supported.includes(scope)) errors.push({ path: "$.http_security.oauth_resource.required_scopes", code: "UNSUPPORTED_HTTP_OAUTH_SCOPE", message: `Required scope ${String(scope)} is not listed in scopes_supported.` });
462
+ }
463
+ }
464
+ if (value.resource_name !== void 0 && (!isNonEmptyString(value.resource_name) || value.resource_name.trim().length > 128)) {
465
+ errors.push({ path: "$.http_security.oauth_resource.resource_name", code: "INVALID_HTTP_RESOURCE_NAME", message: "resource_name must be 1 through 128 characters." });
466
+ }
467
+ if (value.resource_documentation !== void 0 && !isHttpsUrl(value.resource_documentation)) {
468
+ errors.push({ path: "$.http_security.oauth_resource.resource_documentation", code: "INVALID_HTTP_RESOURCE_DOCUMENTATION", message: "resource_documentation must be an HTTPS URL without credentials, query, or fragment." });
469
+ }
470
+ }
471
+ function validateExactOrigins(value, errors) {
472
+ if (value === void 0) return;
473
+ if (!Array.isArray(value) || value.length > 32) {
474
+ errors.push({ path: "$.http_security.allowed_origins", code: "INVALID_HTTP_ALLOWED_ORIGINS", message: "allowed_origins must be an array of at most 32 exact origins." });
475
+ return;
476
+ }
477
+ value.forEach((origin, index) => {
478
+ if (!isExactHttpOrigin(origin)) errors.push({ path: `$.http_security.allowed_origins[${index}]`, code: "INVALID_HTTP_ALLOWED_ORIGIN", message: "Each allowed origin must be an exact HTTP(S) origin; wildcards, credentials, paths, query strings, and fragments are forbidden." });
479
+ });
480
+ if (new Set(value).size !== value.length) errors.push({ path: "$.http_security.allowed_origins", code: "DUPLICATE_HTTP_ALLOWED_ORIGIN", message: "allowed_origins must not contain duplicates." });
481
+ }
482
+ function validateAllowedHosts(value, errors) {
483
+ if (value === void 0) return;
484
+ if (!Array.isArray(value) || value.length < 1 || value.length > 32 || value.some((host) => !isAllowedHost(host))) {
485
+ errors.push({ path: "$.http_security.allowed_hosts", code: "INVALID_HTTP_ALLOWED_HOSTS", message: "allowed_hosts must contain 1 through 32 exact host or host:port authorities; wildcards and forwarded-host syntax are forbidden." });
486
+ } else if (new Set(value.map((host) => String(host).toLowerCase())).size !== value.length) {
487
+ errors.push({ path: "$.http_security.allowed_hosts", code: "DUPLICATE_HTTP_ALLOWED_HOST", message: "allowed_hosts must not contain duplicates." });
488
+ }
489
+ }
490
+ function validateHttpLimits(value, strict, errors) {
491
+ if (value === void 0) return;
492
+ if (!isRecord(value)) {
493
+ errors.push({ path: "$.http_security.limits", code: "HTTP_LIMITS_NOT_OBJECT", message: "http_security.limits must be an object." });
494
+ return;
495
+ }
496
+ if (strict) checkUnknownKeys(value, HTTP_LIMIT_KEYS, "$.http_security.limits", errors);
497
+ for (const [key, minimum, maximum] of [
498
+ ["max_request_bytes", 1024, 16 * 1024 * 1024],
499
+ ["max_header_bytes", 4096, 64 * 1024],
500
+ ["max_sessions", 1, 1e5],
501
+ ["session_idle_timeout_seconds", 10, 86400],
502
+ ["request_timeout_ms", 1e3, 3e5],
503
+ ["headers_timeout_ms", 1e3, 12e4],
504
+ ["keep_alive_timeout_ms", 1e3, 12e4],
505
+ ["max_connections", 1, 1e5]
506
+ ]) {
507
+ if (value[key] !== void 0 && (!Number.isSafeInteger(value[key]) || Number(value[key]) < minimum || Number(value[key]) > maximum)) {
508
+ errors.push({ path: `$.http_security.limits.${key}`, code: "INVALID_HTTP_LIMIT", message: `${key} must be an integer from ${minimum} through ${maximum}.` });
509
+ }
510
+ }
511
+ }
512
+ function isHttpsUrl(value) {
513
+ if (typeof value !== "string") return false;
514
+ try {
515
+ const url = new URL(value);
516
+ return url.protocol === "https:" && !url.username && !url.password && !url.search && !url.hash;
517
+ } catch {
518
+ return false;
519
+ }
520
+ }
521
+ function isExactHttpOrigin(value) {
522
+ if (typeof value !== "string" || value === "*" || value === "null") return false;
523
+ try {
524
+ const url = new URL(value);
525
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash && url.origin === value;
526
+ } catch {
527
+ return false;
528
+ }
529
+ }
530
+ function isAllowedHost(value) {
531
+ return typeof value === "string" && value.length <= 255 && value === value.trim() && !/[\s,/*\\?#]/.test(value) && !value.includes("://") && /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9.-]+)(?::\d{1,5})?$/.test(value);
532
+ }
533
+ function isOAuthScope(value) {
534
+ return typeof value === "string" && value.length >= 1 && value.length <= 128 && /^[\x21\x23-\x5b\x5d-\x7e]+$/.test(value);
535
+ }
350
536
  function validateGovernance(value, strict, errors) {
351
537
  if (value === void 0) return;
352
538
  if (!isRecord(value)) {
@@ -4557,6 +4743,11 @@ var ProposalStore = class {
4557
4743
  const rows = this.db.prepare(query.sql).all(...query.params);
4558
4744
  return rows.map((row) => rowToProposal(row)).filter((proposal) => proposal !== void 0);
4559
4745
  }
4746
+ countProposals(filters = {}) {
4747
+ const query = buildProposalCountQuery(filters);
4748
+ const row = this.db.prepare(query.sql).get(...query.params);
4749
+ return isRecord2(row) ? Number(row.count ?? 0) : 0;
4750
+ }
4560
4751
  listEvidenceBundles(filters = {}) {
4561
4752
  const query = buildEvidenceQuery(filters);
4562
4753
  const rows = this.db.prepare(query.sql).all(...query.params);
@@ -4583,6 +4774,19 @@ var ProposalStore = class {
4583
4774
  const proposalId = replayId.startsWith(prefix) ? replayId.slice(prefix.length) : replayId;
4584
4775
  return this.replay(proposalId);
4585
4776
  }
4777
+ getStoredReplay(replayId) {
4778
+ const row = this.db.prepare("SELECT * FROM replay_records WHERE replay_id = ?").get(replayId);
4779
+ return rowToStoredReplay(row);
4780
+ }
4781
+ getStoredReplayForProposal(proposalId) {
4782
+ const row = this.db.prepare(`
4783
+ SELECT * FROM replay_records
4784
+ WHERE proposal_id = ?
4785
+ ORDER BY created_at DESC, replay_id DESC
4786
+ LIMIT 1
4787
+ `).get(proposalId);
4788
+ return rowToStoredReplay(row);
4789
+ }
4586
4790
  proposalIdForEvidence(evidenceBundleId) {
4587
4791
  const evidence = this.getEvidenceBundle(evidenceBundleId);
4588
4792
  if (evidence?.proposal_id) return evidence.proposal_id;
@@ -4885,6 +5089,19 @@ var ProposalStore = class {
4885
5089
  });
4886
5090
  return job;
4887
5091
  }
5092
+ getWritebackJob(writebackJobId) {
5093
+ return rowToWritebackJob(this.db.prepare("SELECT * FROM writeback_jobs WHERE writeback_job_id = ?").get(writebackJobId));
5094
+ }
5095
+ listWritebackJobs(options = {}) {
5096
+ const clauses = [];
5097
+ const values = [];
5098
+ if (options.proposal_id) {
5099
+ clauses.push("proposal_id = ?");
5100
+ values.push(options.proposal_id);
5101
+ }
5102
+ const limit = Math.min(Math.max(options.limit ?? 100, 1), 1e3);
5103
+ return this.db.prepare(`SELECT * FROM writeback_jobs${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY created_at ASC, writeback_job_id ASC LIMIT ?`).all(...values, limit).map(rowToWritebackJob).filter((job) => Boolean(job));
5104
+ }
4888
5105
  claimWritebackIntent(jobInput, runnerId) {
4889
5106
  const job = parseWritebackJob(jobInput);
4890
5107
  const proposal = this.requireProposal(job.proposal_id);
@@ -6708,6 +6925,19 @@ function inWhere(column, values) {
6708
6925
  };
6709
6926
  }
6710
6927
  function buildProposalQuery(filters) {
6928
+ const { clauses, params } = proposalQueryParts(filters);
6929
+ const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
6930
+ return {
6931
+ sql: `SELECT * FROM proposals${where} ORDER BY created_at DESC, proposal_id DESC${filters.limit ? " LIMIT ?" : ""}`,
6932
+ params: filters.limit ? [...params, filters.limit] : params
6933
+ };
6934
+ }
6935
+ function buildProposalCountQuery(filters) {
6936
+ const { clauses, params } = proposalQueryParts(filters);
6937
+ const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
6938
+ return { sql: `SELECT COUNT(*) AS count FROM proposals${where}`, params };
6939
+ }
6940
+ function proposalQueryParts(filters) {
6711
6941
  const clauses = [];
6712
6942
  const params = [];
6713
6943
  addEqual(clauses, params, "proposal_id", filters.proposal);
@@ -6719,7 +6949,7 @@ function buildProposalQuery(filters) {
6719
6949
  addEqual(clauses, params, "action", filters.capability ?? filters.action);
6720
6950
  addObjectFilter(clauses, params, "business_object", "source_table", "object_id", filters.objectType, filters.objectId);
6721
6951
  addTimeRange(clauses, params, "created_at", filters.from, filters.to);
6722
- return finishQuery("SELECT * FROM proposals", clauses, params, filters.limit);
6952
+ return { clauses, params };
6723
6953
  }
6724
6954
  function buildEvidenceQuery(filters) {
6725
6955
  const clauses = [];
@@ -7301,6 +7531,62 @@ function rowToReceipt(row) {
7301
7531
  source_table: row.source_table == null ? void 0 : String(row.source_table)
7302
7532
  };
7303
7533
  }
7534
+ function rowToWritebackJob(row) {
7535
+ if (!isRecord2(row)) return void 0;
7536
+ let payload;
7537
+ try {
7538
+ payload = JSON.parse(String(row.job_json));
7539
+ } catch {
7540
+ throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not valid JSON`);
7541
+ }
7542
+ if (!isRecord2(payload)) {
7543
+ throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not an object`);
7544
+ }
7545
+ const handler = payload.schema_version === "synapsor.handler-writeback.v1";
7546
+ let normalizedJob;
7547
+ if (!handler) {
7548
+ try {
7549
+ normalizedJob = parseWritebackJob(payload);
7550
+ } catch {
7551
+ throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not a supported writeback protocol`);
7552
+ }
7553
+ }
7554
+ const payloadJobId = handler ? payload.writeback_job_id : normalizedJob?.job_id;
7555
+ const payloadProposalId = handler ? payload.proposal_id : normalizedJob?.proposal_id;
7556
+ const payloadProposalHash = handler ? payload.proposal_hash : normalizedJob?.approval_id;
7557
+ if (payloadJobId !== String(row.writeback_job_id) || payloadProposalId !== String(row.proposal_id) || payloadProposalHash !== String(row.proposal_hash)) {
7558
+ throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} index fields do not match its payload`);
7559
+ }
7560
+ return {
7561
+ writeback_job_id: String(row.writeback_job_id),
7562
+ proposal_id: String(row.proposal_id),
7563
+ proposal_hash: String(row.proposal_hash),
7564
+ status: String(row.status),
7565
+ kind: handler ? "app_handler" : "direct_sql",
7566
+ payload,
7567
+ ...normalizedJob ? { normalized_job: normalizedJob } : {},
7568
+ created_at: String(row.created_at),
7569
+ updated_at: String(row.updated_at)
7570
+ };
7571
+ }
7572
+ function rowToStoredReplay(row) {
7573
+ if (!isRecord2(row)) return void 0;
7574
+ let payload;
7575
+ try {
7576
+ payload = JSON.parse(String(row.payload_json));
7577
+ } catch {
7578
+ throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${String(row.replay_id)} payload is not valid JSON`);
7579
+ }
7580
+ if (!isRecord2(payload) || !isRecord2(payload.proposal)) {
7581
+ throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${String(row.replay_id)} payload is not a supported replay record`);
7582
+ }
7583
+ const replayId = String(row.replay_id);
7584
+ const proposalId = String(row.proposal_id);
7585
+ if (payload.replay_id !== replayId || payload.proposal.proposal_id !== proposalId || !Array.isArray(payload.approvals) || !Array.isArray(payload.events) || !Array.isArray(payload.receipts) || !Array.isArray(payload.query_audit) || !Array.isArray(payload.evidence) || typeof payload.generated_at !== "string") {
7586
+ throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${replayId} index fields do not match its payload`);
7587
+ }
7588
+ return payload;
7589
+ }
7304
7590
  function rowToWritebackIntent(row) {
7305
7591
  if (!isRecord2(row)) return void 0;
7306
7592
  const status = String(row.status);
@@ -10839,8 +11125,30 @@ async function boundedJwksFetch(resource, init, maxBytes) {
10839
11125
  body.set(chunk, offset);
10840
11126
  offset += chunk.byteLength;
10841
11127
  }
11128
+ if (response.ok) validatePublicJwks(body);
10842
11129
  return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
10843
11130
  }
11131
+ function validatePublicJwks(body) {
11132
+ let parsed;
11133
+ try {
11134
+ parsed = JSON.parse(new TextDecoder().decode(body));
11135
+ } catch {
11136
+ throw new Error("JWKS response is not valid JSON");
11137
+ }
11138
+ if (!isRecord4(parsed) || !Array.isArray(parsed.keys) || parsed.keys.length > 128) {
11139
+ throw new Error("JWKS response must contain a bounded keys array");
11140
+ }
11141
+ const privateMembers = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth", "k"]);
11142
+ for (const key of parsed.keys) {
11143
+ if (!isRecord4(key) || typeof key.kty !== "string") throw new Error("JWKS contains an invalid key");
11144
+ if (key.kty === "oct" || Object.keys(key).some((member) => privateMembers.has(member))) {
11145
+ throw new Error("JWKS must contain public verification keys only");
11146
+ }
11147
+ }
11148
+ }
11149
+ function isRecord4(value) {
11150
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11151
+ }
10844
11152
  function trimmedEnv(env, name) {
10845
11153
  const value = env[name]?.trim();
10846
11154
  return value || void 0;
@@ -11383,8 +11691,8 @@ function loadCloudLinkedConnection(config, env = process.env) {
11383
11691
  } catch (error) {
11384
11692
  throw new McpRuntimeError("CLOUD_CONNECTION_INVALID", `Unable to read the reviewed Cloud connection file: ${error instanceof Error ? error.message : String(error)}`);
11385
11693
  }
11386
- const root = isRecord4(parsed) ? parsed : {};
11387
- const cloud = isRecord4(root.cloud) ? root.cloud : void 0;
11694
+ const root = isRecord5(parsed) ? parsed : {};
11695
+ const cloud = isRecord5(root.cloud) ? root.cloud : void 0;
11388
11696
  if (!cloud) throw new McpRuntimeError("CLOUD_CONNECTION_INVALID", "Cloud connection file must contain a cloud object.");
11389
11697
  const baseUrlEnv = nonEmptyString(cloud.base_url_env) ?? "SYNAPSOR_CLOUD_BASE_URL";
11390
11698
  const runnerTokenEnv = nonEmptyString(cloud.runner_token_env) ?? "SYNAPSOR_RUNNER_TOKEN";
@@ -11575,13 +11883,13 @@ function cloudSafeChangeSet(changeSet) {
11575
11883
  }
11576
11884
  function stripCloudPrincipalColumn(changeSet, column) {
11577
11885
  const strip = (value) => {
11578
- if (isRecord4(value)) delete value[column];
11886
+ if (isRecord5(value)) delete value[column];
11579
11887
  };
11580
11888
  strip(changeSet.before);
11581
11889
  strip(changeSet.after);
11582
- if ("frozen_set" in changeSet && isRecord4(changeSet.frozen_set) && Array.isArray(changeSet.frozen_set.members)) {
11890
+ if ("frozen_set" in changeSet && isRecord5(changeSet.frozen_set) && Array.isArray(changeSet.frozen_set.members)) {
11583
11891
  for (const member of changeSet.frozen_set.members) {
11584
- if (!isRecord4(member)) continue;
11892
+ if (!isRecord5(member)) continue;
11585
11893
  strip(member.before);
11586
11894
  strip(member.after);
11587
11895
  }
@@ -11768,7 +12076,7 @@ var CloudLinkedSynchronizer = class {
11768
12076
  if (item.kind === "proposal") return this.client.submitProposal(item.payload);
11769
12077
  if (item.kind === "activity") return this.client.submitActivity(item.payload);
11770
12078
  if (item.kind === "result") {
11771
- const result = isRecord4(item.payload.result) ? item.payload.result : void 0;
12079
+ const result = isRecord5(item.payload.result) ? item.payload.result : void 0;
11772
12080
  const leaseId = nonEmptyString(item.payload.lease_id);
11773
12081
  if (!result || !leaseId) throw new McpRuntimeError("CLOUD_RESULT_OUTBOX_INVALID", "Cloud result outbox entry is missing a result or lease identity.");
11774
12082
  return this.client.result(result, leaseId);
@@ -11777,9 +12085,9 @@ var CloudLinkedSynchronizer = class {
11777
12085
  }
11778
12086
  };
11779
12087
  function cloudGovernanceStatusPayload(response) {
11780
- const decision = isRecord4(response.decision) ? response.decision : void 0;
11781
- const job = isRecord4(response.job) ? response.job : void 0;
11782
- const result = isRecord4(response.result) ? response.result : void 0;
12088
+ const decision = isRecord5(response.decision) ? response.decision : void 0;
12089
+ const job = isRecord5(response.job) ? response.job : void 0;
12090
+ const result = isRecord5(response.result) ? response.result : void 0;
11783
12091
  const actor = nonEmptyString(decision?.actor);
11784
12092
  return JSON.parse(JSON.stringify({
11785
12093
  contract_id: nonEmptyString(response.contract_id),
@@ -11872,9 +12180,17 @@ function mergeContractIntoRuntimeConfig(config, contract, origin, seenCapabiliti
11872
12180
  }
11873
12181
  }
11874
12182
  function runtimeContextFromSpec(context) {
12183
+ const unsupportedSessionBinding = context.bindings.find((binding) => binding.source === "session");
12184
+ if (unsupportedSessionBinding) {
12185
+ throw new Error(
12186
+ `SESSION_BINDING_UNSUPPORTED: context ${context.name} binding ${unsupportedSessionBinding.name} uses canonical SESSION source, but Synapsor Runner has no generic web-session trust provider. Use ENVIRONMENT for local stdio, HTTP_CLAIM for verified HTTP JWT claims, or CLOUD_SESSION for verified Cloud-linked identity.`
12187
+ );
12188
+ }
11875
12189
  const tenantBinding = context.bindings.find((binding) => binding.name === context.tenant_binding) ?? context.bindings.find((binding) => binding.name === "tenant_id");
11876
12190
  const principalBinding = context.bindings.find((binding) => binding.name === context.principal_binding) ?? context.bindings.find((binding) => binding.name === "principal");
11877
- const provider = context.bindings.some((binding) => binding.source === "environment") ? "environment" : context.bindings.some((binding) => binding.source === "cloud_session") ? "cloud_session" : context.bindings.some((binding) => binding.source === "http_claim") ? "http_claims" : context.bindings.some((binding) => binding.source === "static_dev") ? "static_dev" : "environment";
12191
+ const provider = context.bindings.some((binding) => binding.source === "environment") ? "environment" : context.bindings.some((binding) => binding.source === "cloud_session") ? "cloud_session" : context.bindings.some((binding) => binding.source === "http_claim") ? "http_claims" : context.bindings.some((binding) => binding.source === "static_dev") ? "static_dev" : (() => {
12192
+ throw new Error(`TRUSTED_CONTEXT_BINDING_UNSUPPORTED: context ${context.name} has no binding source supported by Synapsor Runner.`);
12193
+ })();
11878
12194
  return {
11879
12195
  provider,
11880
12196
  tenant_binding: context.tenant_binding,
@@ -12286,24 +12602,253 @@ async function serveStdio(options = {}) {
12286
12602
  process.once("SIGTERM", close);
12287
12603
  });
12288
12604
  }
12605
+ function resolveHttpSecurity(config, options, host, env, usesSessionAuth) {
12606
+ const configured = config.http_security;
12607
+ const loopback = isLoopbackHost(host);
12608
+ if (options.devNoAuth && !loopback) {
12609
+ throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
12610
+ }
12611
+ const deployment = configured?.deployment ?? (loopback ? "loopback" : void 0);
12612
+ if (!deployment) {
12613
+ throw new McpRuntimeError(
12614
+ "HTTP_REMOTE_DEPLOYMENT_REQUIRED",
12615
+ "A non-loopback listener requires http_security.deployment single_tenant or shared."
12616
+ );
12617
+ }
12618
+ if (!loopback && deployment === "loopback") {
12619
+ throw new McpRuntimeError("HTTP_LOOPBACK_PROFILE_REMOTE", "http_security.deployment loopback cannot bind a non-loopback listener.");
12620
+ }
12621
+ if (deployment === "shared" && !usesSessionAuth) {
12622
+ throw new McpRuntimeError("HTTP_SHARED_SESSION_AUTH_REQUIRED", "Shared HTTP deployment requires signed per-session http_claims identity.");
12623
+ }
12624
+ if (deployment !== "shared" && usesSessionAuth && !loopback) {
12625
+ throw new McpRuntimeError("HTTP_SHARED_DEPLOYMENT_REQUIRED", "Remote http_claims identity requires http_security.deployment shared.");
12626
+ }
12627
+ const optionChannels = [
12628
+ options.trustedTlsProxy ? "trusted_tls_proxy" : void 0,
12629
+ options.unsafeAllowCleartextHttp ? "insecure_http_break_glass" : void 0
12630
+ ].filter((value) => Boolean(value));
12631
+ if (optionChannels.length > 1) {
12632
+ throw new McpRuntimeError("HTTP_CHANNEL_CONFLICT", "Choose only one of trusted TLS proxy or unsafe cleartext break-glass mode.");
12633
+ }
12634
+ const requestedChannel = optionChannels[0] ?? configured?.channel;
12635
+ if (options.tls && requestedChannel && requestedChannel !== "direct_tls") {
12636
+ throw new McpRuntimeError("HTTP_CHANNEL_CONFLICT", "Runner-owned TLS cannot be combined with trusted-proxy or insecure-cleartext channel declarations.");
12637
+ }
12638
+ let channel;
12639
+ if (options.tls) channel = "direct_tls";
12640
+ else if (requestedChannel === "direct_tls") {
12641
+ throw new McpRuntimeError("HTTP_TLS_MATERIAL_REQUIRED", "http_security.channel direct_tls requires Runner TLS certificate and key material.");
12642
+ } else if (requestedChannel === "trusted_tls_proxy") channel = "trusted_tls_proxy";
12643
+ else if (requestedChannel === "insecure_http_break_glass") channel = "insecure_http_break_glass";
12644
+ else if (loopback) channel = "loopback_cleartext";
12645
+ else {
12646
+ throw new McpRuntimeError(
12647
+ "HTTP_REMOTE_CLEARTEXT_REFUSED",
12648
+ "Refusing non-loopback cleartext HTTP. Configure Runner-owned TLS, an explicit trusted TLS proxy, or --unsafe-allow-cleartext-http break glass."
12649
+ );
12650
+ }
12651
+ if (options.devNoAuth && channel === "insecure_http_break_glass") {
12652
+ throw new McpRuntimeError("HTTP_BREAK_GLASS_AUTH_REQUIRED", "Unsafe cleartext break-glass mode never disables authentication.");
12653
+ }
12654
+ if (options.devNoAuth && deployment !== "loopback") {
12655
+ throw new McpRuntimeError("HTTP_DEV_NO_AUTH_PROFILE_INVALID", "--dev-no-auth is valid only for a loopback development deployment.");
12656
+ }
12657
+ const activeTokenEnv = options.authTokenEnv ?? configured?.static_token?.active_env ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
12658
+ const previousTokenEnv = options.previousAuthTokenEnv ?? configured?.static_token?.previous_env;
12659
+ if (previousTokenEnv && previousTokenEnv === activeTokenEnv) {
12660
+ throw new McpRuntimeError("HTTP_TOKEN_ENV_REUSED", "Active and previous HTTP token environment variables must be different.");
12661
+ }
12662
+ const activeToken = options.devNoAuth || usesSessionAuth ? void 0 : envValue(env, activeTokenEnv);
12663
+ const previousToken = options.devNoAuth || usesSessionAuth || !previousTokenEnv ? void 0 : envValue(env, previousTokenEnv);
12664
+ if (!options.devNoAuth && !usesSessionAuth && !activeToken) {
12665
+ throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${activeTokenEnv} is not set. HTTP MCP requires bearer auth by default.`);
12666
+ }
12667
+ if (previousTokenEnv && !usesSessionAuth && !options.devNoAuth && !previousToken) {
12668
+ throw new McpRuntimeError("HTTP_PREVIOUS_AUTH_TOKEN_MISSING", `${previousTokenEnv} is configured for rotation but is not set.`);
12669
+ }
12670
+ if (activeToken && previousToken && constantTimeTokenEquals(activeToken, previousToken)) {
12671
+ throw new McpRuntimeError("HTTP_TOKEN_ROTATION_DUPLICATE", "Active and previous HTTP endpoint tokens must differ.");
12672
+ }
12673
+ const weakStaticToken = Boolean(activeToken && !strongOpaqueToken(activeToken)) || Boolean(previousToken && !strongOpaqueToken(previousToken));
12674
+ if (!loopback && !usesSessionAuth && weakStaticToken) {
12675
+ throw new McpRuntimeError("HTTP_AUTH_TOKEN_WEAK", "Non-loopback static endpoint tokens must contain at least 32 bytes of high-entropy secret material.");
12676
+ }
12677
+ const configuredOrigins = configured?.allowed_origins ?? [];
12678
+ const allowedOrigins = new Set(configuredOrigins);
12679
+ if (options.corsOrigin) {
12680
+ if (!isExactHttpOrigin2(options.corsOrigin)) {
12681
+ throw new McpRuntimeError("HTTP_CORS_ORIGIN_INVALID", "--cors-origin must be one exact HTTP(S) origin; wildcards, paths, credentials, query, and fragments are forbidden.");
12682
+ }
12683
+ allowedOrigins.add(options.corsOrigin);
12684
+ }
12685
+ const allowedHosts = configured?.allowed_hosts?.map((value) => value.toLowerCase()) ?? defaultAllowedHosts(host);
12686
+ if (!loopback && allowedHosts.length === 0) {
12687
+ throw new McpRuntimeError("HTTP_ALLOWED_HOSTS_REQUIRED", "Non-loopback HTTP requires http_security.allowed_hosts with exact public/direct Host authorities.");
12688
+ }
12689
+ const rawLimits = configured?.limits;
12690
+ const limits = {
12691
+ maxRequestBytes: rawLimits?.max_request_bytes ?? 1048576,
12692
+ maxHeaderBytes: rawLimits?.max_header_bytes ?? 16384,
12693
+ maxSessions: rawLimits?.max_sessions ?? 1024,
12694
+ sessionIdleTimeoutMs: (rawLimits?.session_idle_timeout_seconds ?? 900) * 1e3,
12695
+ requestTimeoutMs: rawLimits?.request_timeout_ms ?? 3e4,
12696
+ headersTimeoutMs: rawLimits?.headers_timeout_ms ?? 1e4,
12697
+ keepAliveTimeoutMs: rawLimits?.keep_alive_timeout_ms ?? 5e3,
12698
+ maxConnections: rawLimits?.max_connections ?? 2048
12699
+ };
12700
+ const oauth = configured?.oauth_resource ? resolveOauthResource(configured.oauth_resource) : void 0;
12701
+ if (deployment === "shared") {
12702
+ if (!oauth) throw new McpRuntimeError("HTTP_OAUTH_RESOURCE_REQUIRED", "Shared HTTP deployment requires RFC 9728 protected-resource metadata.");
12703
+ const auth = config.session_auth;
12704
+ if (!auth?.issuer || !auth.audience) {
12705
+ throw new McpRuntimeError("HTTP_JWT_ISSUER_AUDIENCE_REQUIRED", "Shared HTTP deployment requires exact session_auth issuer and audience/resource.");
12706
+ }
12707
+ if (auth.audience !== configured?.oauth_resource?.resource) {
12708
+ throw new McpRuntimeError("HTTP_RESOURCE_AUDIENCE_MISMATCH", "session_auth.audience must exactly match http_security.oauth_resource.resource.");
12709
+ }
12710
+ }
12711
+ return {
12712
+ deployment,
12713
+ channel,
12714
+ activeToken,
12715
+ previousToken,
12716
+ activeTokenEnv,
12717
+ previousTokenEnv,
12718
+ weakStaticToken,
12719
+ allowedOrigins,
12720
+ allowedHosts,
12721
+ limits,
12722
+ oauth
12723
+ };
12724
+ }
12725
+ function resolveOauthResource(input) {
12726
+ const resource = new URL(input.resource);
12727
+ const pathname = resource.pathname === "/" ? "" : resource.pathname.replace(/\/$/, "");
12728
+ const metadataPath = `/.well-known/oauth-protected-resource${pathname}`;
12729
+ const metadataUrl = new URL(metadataPath || "/.well-known/oauth-protected-resource", resource.origin).toString();
12730
+ const metadata = OAuthProtectedResourceMetadataSchema.parse({
12731
+ resource: input.resource,
12732
+ authorization_servers: input.authorization_servers,
12733
+ ...input.scopes_supported ? { scopes_supported: input.scopes_supported } : {},
12734
+ bearer_methods_supported: ["header"],
12735
+ ...input.resource_name ? { resource_name: input.resource_name } : {},
12736
+ ...input.resource_documentation ? { resource_documentation: input.resource_documentation } : {}
12737
+ });
12738
+ return { metadata, metadataUrl, metadataPath, requiredScopes: input.required_scopes ?? [] };
12739
+ }
12740
+ function defaultAllowedHosts(host) {
12741
+ if (!isLoopbackHost(host)) {
12742
+ return host === "0.0.0.0" || host === "::" ? [] : [host.toLowerCase()];
12743
+ }
12744
+ return ["localhost", "127.0.0.1", "[::1]", host.toLowerCase()];
12745
+ }
12746
+ function applyHttpServerLimits(server, limits) {
12747
+ server.requestTimeout = limits.requestTimeoutMs;
12748
+ server.headersTimeout = limits.headersTimeoutMs;
12749
+ server.keepAliveTimeout = limits.keepAliveTimeoutMs;
12750
+ server.maxConnections = limits.maxConnections;
12751
+ }
12752
+ function validateTlsMaterial(tls) {
12753
+ if (!tls) return;
12754
+ try {
12755
+ createSecureContext({ cert: tls.cert, key: tls.key, ca: tls.ca });
12756
+ } catch {
12757
+ throw new McpRuntimeError("HTTP_TLS_MATERIAL_INVALID", "HTTP TLS certificate, private key, or CA material is invalid.");
12758
+ }
12759
+ }
12760
+ function validateHttpRequestSecurity(request, response, security) {
12761
+ if ((request.url?.length ?? 0) > 8192) {
12762
+ writeJson(response, 414, { ok: false, error: "uri_too_long" });
12763
+ return false;
12764
+ }
12765
+ const host = headerValue(request.headers.host);
12766
+ if (!host || !hostAllowed(host, security.allowedHosts)) {
12767
+ writeJson(response, 403, { ok: false, error: "host_forbidden" });
12768
+ return false;
12769
+ }
12770
+ const origin = headerValue(request.headers.origin);
12771
+ if (origin && !security.allowedOrigins.has(origin)) {
12772
+ writeJson(response, 403, { ok: false, error: "origin_forbidden" });
12773
+ return false;
12774
+ }
12775
+ setHttpSecurityHeaders(response);
12776
+ if (origin) setCorsHeaders(response, origin);
12777
+ return true;
12778
+ }
12779
+ function hostAllowed(rawHost, allowedHosts) {
12780
+ const actual = parseHostAuthority(rawHost);
12781
+ if (!actual) return false;
12782
+ return allowedHosts.some((allowed) => {
12783
+ const expected = parseHostAuthority(allowed);
12784
+ if (!expected || expected.hostname !== actual.hostname) return false;
12785
+ return expected.port ? expected.port === actual.port : true;
12786
+ });
12787
+ }
12788
+ function parseHostAuthority(value) {
12789
+ if (!value || value !== value.trim() || /[\s,/?#\\]/.test(value)) return void 0;
12790
+ try {
12791
+ const parsed = new URL(`http://${value}`);
12792
+ if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) return void 0;
12793
+ return { hostname: parsed.hostname.toLowerCase(), port: parsed.port };
12794
+ } catch {
12795
+ return void 0;
12796
+ }
12797
+ }
12798
+ function isExactHttpOrigin2(value) {
12799
+ if (value === "*" || value === "null") return false;
12800
+ try {
12801
+ const origin = new URL(value);
12802
+ return (origin.protocol === "http:" || origin.protocol === "https:") && !origin.username && !origin.password && origin.pathname === "/" && !origin.search && !origin.hash && origin.origin === value;
12803
+ } catch {
12804
+ return false;
12805
+ }
12806
+ }
12807
+ function setHttpSecurityHeaders(response) {
12808
+ response.setHeader("cache-control", "no-store");
12809
+ response.setHeader("x-content-type-options", "nosniff");
12810
+ response.setHeader("referrer-policy", "no-referrer");
12811
+ }
12812
+ function strongOpaqueToken(token) {
12813
+ if (Buffer.byteLength(token, "utf8") < 32) return false;
12814
+ if (new Set(token).size < 12) return false;
12815
+ return !/^(.)\1+$/.test(token) && !/(?:password|secret|token|changeme|example|development)/i.test(token);
12816
+ }
12817
+ function httpAuthChallenge(security, insufficientScope = false) {
12818
+ const parts = ["Bearer"];
12819
+ if (insufficientScope) parts.push('error="insufficient_scope"');
12820
+ if (security.oauth) {
12821
+ parts.push(`resource_metadata="${security.oauth.metadataUrl}"`);
12822
+ if (security.oauth.requiredScopes.length) parts.push(`scope="${security.oauth.requiredScopes.join(" ")}"`);
12823
+ }
12824
+ return parts.join(" ");
12825
+ }
12826
+ function maybeServeOauthMetadata(request, response, security, pathname) {
12827
+ if (!security.oauth || request.method !== "GET") return false;
12828
+ if (pathname !== security.oauth.metadataPath && pathname !== "/.well-known/oauth-protected-resource") return false;
12829
+ writeJson(response, 200, security.oauth.metadata);
12830
+ return true;
12831
+ }
12832
+ function writeAuthenticationFailure(response, security, status, error) {
12833
+ response.setHeader("www-authenticate", httpAuthChallenge(security, status === 403));
12834
+ writeJson(response, status, { ok: false, error });
12835
+ }
12289
12836
  async function startHttpMcpServer(options = {}) {
12290
12837
  const host = options.host ?? "127.0.0.1";
12291
12838
  const port = options.port ?? 8765;
12292
- const authTokenEnv = options.authTokenEnv ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
12293
12839
  const env = options.env ?? process.env;
12294
12840
  const devNoAuth = options.devNoAuth === true;
12295
12841
  const config = resolveRuntimeConfig(options.config ?? loadRuntimeConfigFromFile(options.configPath));
12296
- const metricsAccess = resolveMetricsEndpointAccess(config, env, host);
12297
- if (devNoAuth && !isLoopbackHost(host)) {
12298
- throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
12299
- }
12842
+ assertValidRunnerCapabilityConfig(config);
12300
12843
  if (configUsesHttpClaims(config)) {
12301
12844
  throw new McpRuntimeError("HTTP_CLAIMS_REQUIRES_STREAMABLE", "http_claims trusted context requires spec MCP Streamable HTTP sessions; the legacy JSON-RPC bridge cannot bind per-session context.");
12302
12845
  }
12303
- const authToken = devNoAuth ? void 0 : envValue(env, authTokenEnv);
12304
- if (!devNoAuth && !authToken) {
12305
- throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. HTTP MCP requires bearer auth by default.`);
12846
+ const security = resolveHttpSecurity(config, options, host, env, false);
12847
+ const metricsAccess = resolveMetricsEndpointAccess(config, env, host);
12848
+ if (options.tls?.requestClientCert && !options.tls.ca) {
12849
+ throw new McpRuntimeError("MTLS_CA_REQUIRED", "HTTP mTLS requires a CA bundle when client certificates are required.");
12306
12850
  }
12851
+ validateTlsMaterial(options.tls);
12307
12852
  const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, env) : void 0;
12308
12853
  if (options.readRow && Object.values(config.sources ?? {}).some((source) => source.database_scope?.mode === "postgres_rls")) {
12309
12854
  throw new McpRuntimeError("POSTGRES_RLS_CUSTOM_READER_UNVERIFIED", "Hardened postgres_rls mode requires Runner's verified PostgreSQL reader; a custom readRow cannot be attested by the stock server.");
@@ -12318,19 +12863,27 @@ async function startHttpMcpServer(options = {}) {
12318
12863
  cloudTools
12319
12864
  });
12320
12865
  const readinessCheck = options.readinessCheck ?? (() => checkRunnerReadiness(config, env));
12321
- const server = createServer((request, response) => {
12866
+ const requestHandler = (request, response) => {
12322
12867
  void handleHttpMcpRequest({
12323
12868
  request,
12324
12869
  response,
12325
12870
  runtime,
12326
- authToken,
12327
12871
  devNoAuth,
12328
- corsOrigin: options.corsOrigin,
12872
+ security,
12329
12873
  readinessCheck,
12330
12874
  metricsAccess,
12331
12875
  metricsProvider: () => renderRuntimeMetrics(runtime.store, runtime.poolMetrics(), runtime.rateLimitMetrics(), readinessCheck)
12332
12876
  });
12333
- });
12877
+ };
12878
+ const server = options.tls ? createHttpsServer({
12879
+ cert: options.tls.cert,
12880
+ key: options.tls.key,
12881
+ ca: options.tls.ca,
12882
+ requestCert: options.tls.requestClientCert === true,
12883
+ rejectUnauthorized: options.tls.requestClientCert === true,
12884
+ maxHeaderSize: security.limits.maxHeaderBytes
12885
+ }, requestHandler) : createServer({ maxHeaderSize: security.limits.maxHeaderBytes }, requestHandler);
12886
+ applyHttpServerLimits(server, security.limits);
12334
12887
  try {
12335
12888
  await new Promise((resolve2, reject) => {
12336
12889
  server.once("error", reject);
@@ -12346,13 +12899,19 @@ async function startHttpMcpServer(options = {}) {
12346
12899
  const address = server.address();
12347
12900
  const actualHost = address.address === "::" ? host : address.address;
12348
12901
  const actualPort = address.port;
12349
- const url = `http://${actualHost}:${actualPort}/mcp`;
12902
+ const scheme = options.tls ? "https" : "http";
12903
+ const url = `${scheme}://${actualHost}:${actualPort}/mcp`;
12350
12904
  if (options.log !== false) {
12351
12905
  const log = options.log ?? process.stderr;
12352
12906
  log.write(`Synapsor Runner HTTP MCP listening on ${url}
12353
12907
  `);
12354
- log.write(devNoAuth ? "Auth: disabled for localhost development only\n" : `Auth: bearer token from ${authTokenEnv}
12908
+ log.write(`Channel: ${security.channel}; deployment: ${security.deployment}
12909
+ `);
12910
+ if (options.tls) log.write(options.tls.requestClientCert ? "TLS: enabled, client certificates required in addition to Bearer auth\n" : "TLS: enabled\n");
12911
+ log.write(devNoAuth ? "Auth: disabled for loopback development only\n" : `Auth: opaque Bearer endpoint token from ${security.activeTokenEnv}${security.previousTokenEnv ? `; previous rotation token from ${security.previousTokenEnv}` : ""}
12355
12912
  `);
12913
+ if (security.weakStaticToken) log.write("Auth warning: loopback endpoint token is shorter or more predictable than the production requirement; generate at least 32 random bytes.\n");
12914
+ if (security.channel === "insecure_http_break_glass") log.write("SECURITY WARNING: remote Bearer traffic is using explicit insecure cleartext break glass. Credentials and data can be intercepted.\n");
12356
12915
  log.write(`Config: ${options.configPath ?? "synapsor.runner.json"}
12357
12916
  `);
12358
12917
  log.write(`Store: ${options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db"}
@@ -12368,29 +12927,23 @@ async function startHttpMcpServer(options = {}) {
12368
12927
  async function startStreamableHttpMcpServer(options = {}) {
12369
12928
  const host = options.host ?? "127.0.0.1";
12370
12929
  const port = options.port ?? 8766;
12371
- const authTokenEnv = options.authTokenEnv ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
12372
12930
  const env = options.env ?? process.env;
12373
12931
  const devNoAuth = options.devNoAuth === true;
12374
12932
  const config = resolveRuntimeConfig(options.config ?? loadRuntimeConfigFromFile(options.configPath));
12375
12933
  assertValidRunnerCapabilityConfig(config);
12376
12934
  const usesSessionAuth = configUsesHttpClaims(config);
12935
+ const security = resolveHttpSecurity(config, options, host, env, usesSessionAuth);
12377
12936
  const metricsAccess = resolveMetricsEndpointAccess(config, env, host);
12378
- if (devNoAuth && !isLoopbackHost(host)) {
12379
- throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
12380
- }
12381
12937
  if (devNoAuth && usesSessionAuth) {
12382
12938
  throw new McpRuntimeError("HTTP_CLAIMS_AUTH_REQUIRED", "http_claims trusted context cannot run with --dev-no-auth.");
12383
12939
  }
12384
- const authToken = devNoAuth || usesSessionAuth ? void 0 : envValue(env, authTokenEnv);
12385
- if (!devNoAuth && !usesSessionAuth && !authToken) {
12386
- throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. Streamable HTTP MCP requires bearer auth by default.`);
12387
- }
12388
12940
  assertRuntimeStoreStartupReady(config, env);
12389
12941
  const sessionVerifier = usesSessionAuth ? sessionAuthVerifier(config, env, options.configPath ? path2.dirname(path2.resolve(options.configPath)) : process.cwd()) : void 0;
12390
12942
  const readinessCheck = options.readinessCheck ?? (() => checkRunnerReadiness(config, env));
12391
12943
  if (options.tls?.requestClientCert && !options.tls.ca) {
12392
12944
  throw new McpRuntimeError("MTLS_CA_REQUIRED", "Streamable HTTP mTLS requires a CA bundle when client certificates are required.");
12393
12945
  }
12946
+ validateTlsMaterial(options.tls);
12394
12947
  const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, env) : void 0;
12395
12948
  if (options.readRow && Object.values(config.sources ?? {}).some((source) => source.database_scope?.mode === "postgres_rls")) {
12396
12949
  throw new McpRuntimeError("POSTGRES_RLS_CUSTOM_READER_UNVERIFIED", "Hardened postgres_rls mode requires Runner's verified PostgreSQL reader; a custom readRow cannot be attested by the stock server.");
@@ -12403,6 +12956,7 @@ async function startStreamableHttpMcpServer(options = {}) {
12403
12956
  const sharedResources = createMcpRuntimeSharedResources(config, env, options.readRow, Date.now, options.credentialResolver);
12404
12957
  const sessions = /* @__PURE__ */ new Map();
12405
12958
  const openSessions = /* @__PURE__ */ new Set();
12959
+ const initializingSessions = { count: 0 };
12406
12960
  const requestHandler = (request, response) => {
12407
12961
  void handleStreamableHttpMcpRequest({
12408
12962
  request,
@@ -12415,12 +12969,12 @@ async function startStreamableHttpMcpServer(options = {}) {
12415
12969
  env,
12416
12970
  toolNameStyle: options.toolNameStyle,
12417
12971
  resultFormat: options.resultFormat,
12418
- authToken,
12419
12972
  sessionVerifier,
12420
12973
  devNoAuth,
12421
- corsOrigin: options.corsOrigin,
12974
+ security,
12422
12975
  sessions,
12423
12976
  openSessions,
12977
+ initializingSessions,
12424
12978
  readinessCheck,
12425
12979
  metricsAccess,
12426
12980
  metricsProvider: () => renderRuntimeMetrics(sharedStore, sharedResources.poolMetrics(), sharedResources.rateLimitMetrics(), readinessCheck)
@@ -12431,8 +12985,14 @@ async function startStreamableHttpMcpServer(options = {}) {
12431
12985
  key: options.tls.key,
12432
12986
  ca: options.tls.ca,
12433
12987
  requestCert: options.tls.requestClientCert === true,
12434
- rejectUnauthorized: options.tls.requestClientCert === true
12435
- }, requestHandler) : createServer(requestHandler);
12988
+ rejectUnauthorized: options.tls.requestClientCert === true,
12989
+ maxHeaderSize: security.limits.maxHeaderBytes
12990
+ }, requestHandler) : createServer({ maxHeaderSize: security.limits.maxHeaderBytes }, requestHandler);
12991
+ applyHttpServerLimits(server, security.limits);
12992
+ const sessionReaper = setInterval(() => {
12993
+ void pruneExpiredStreamableSessions(sessions, openSessions, security.limits.sessionIdleTimeoutMs, Date.now());
12994
+ }, Math.min(3e4, Math.max(1e3, Math.floor(security.limits.sessionIdleTimeoutMs / 2))));
12995
+ sessionReaper.unref?.();
12436
12996
  try {
12437
12997
  await new Promise((resolve2, reject) => {
12438
12998
  server.once("error", reject);
@@ -12442,6 +13002,7 @@ async function startStreamableHttpMcpServer(options = {}) {
12442
13002
  });
12443
13003
  });
12444
13004
  } catch (error) {
13005
+ clearInterval(sessionReaper);
12445
13006
  await cloudSynchronizer?.stop();
12446
13007
  await closeStreamableSessions(openSessions);
12447
13008
  await sharedResources.close();
@@ -12457,10 +13018,16 @@ async function startStreamableHttpMcpServer(options = {}) {
12457
13018
  const log = options.log ?? process.stderr;
12458
13019
  log.write(`Synapsor Runner Streamable HTTP MCP listening on ${url}
12459
13020
  `);
12460
- if (options.tls) log.write(options.tls.requestClientCert ? "TLS: enabled, client certificates required\n" : "TLS: enabled\n");
12461
- log.write(devNoAuth ? "Auth: disabled for localhost development only\n" : usesSessionAuth ? `Auth: signed per-session JWT (${config.session_auth?.provider})
12462
- ` : `Auth: bearer token from ${authTokenEnv}
13021
+ log.write(`Channel: ${security.channel}; deployment: ${security.deployment}
13022
+ `);
13023
+ if (options.tls) log.write(options.tls.requestClientCert ? "TLS: enabled, client certificates required in addition to Bearer auth\n" : "TLS: enabled\n");
13024
+ log.write(devNoAuth ? "Auth: disabled for localhost development only\n" : usesSessionAuth ? `Auth: signed per-session JWT (${config.session_auth?.provider}); issuer and resource checked on every request
13025
+ ` : `Auth: opaque Bearer endpoint token from ${security.activeTokenEnv}${security.previousTokenEnv ? `; previous rotation token from ${security.previousTokenEnv}` : ""}
13026
+ `);
13027
+ if (security.oauth) log.write(`OAuth resource metadata: ${security.oauth.metadataUrl}
12463
13028
  `);
13029
+ if (security.weakStaticToken) log.write("Auth warning: loopback endpoint token is shorter or more predictable than the production requirement; generate at least 32 random bytes.\n");
13030
+ if (security.channel === "insecure_http_break_glass") log.write("SECURITY WARNING: remote Bearer traffic is using explicit insecure cleartext break glass. Credentials and data can be intercepted.\n");
12464
13031
  for (const assurance of describeIsolationAssurance(config)) {
12465
13032
  log.write(`Isolation ${assurance.source}: ${assurance.mode}; trusted context: ${assurance.trusted_context.request_binding}
12466
13033
  `);
@@ -12476,19 +13043,23 @@ async function startStreamableHttpMcpServer(options = {}) {
12476
13043
  host: actualHost,
12477
13044
  port: actualPort,
12478
13045
  url,
12479
- close: () => closeStreamableHttpServer(server, openSessions, sharedResources, sharedStore, cloudSynchronizer)
13046
+ close: () => {
13047
+ clearInterval(sessionReaper);
13048
+ return closeStreamableHttpServer(server, openSessions, sharedResources, sharedStore, cloudSynchronizer);
13049
+ }
12480
13050
  };
12481
13051
  }
12482
13052
  async function handleStreamableHttpMcpRequest(input) {
12483
- const { request, response, config, storePath, sharedStore, sharedResources, cloudTools, env, toolNameStyle, resultFormat, authToken, sessionVerifier, devNoAuth, corsOrigin, sessions, openSessions, readinessCheck, metricsAccess, metricsProvider } = input;
13053
+ const { request, response, config, storePath, sharedStore, sharedResources, cloudTools, env, toolNameStyle, resultFormat, sessionVerifier, devNoAuth, security, sessions, openSessions, initializingSessions, readinessCheck, metricsAccess, metricsProvider } = input;
12484
13054
  try {
12485
- setCorsHeaders(response, corsOrigin);
12486
- if (request.method === "OPTIONS" && corsOrigin) {
13055
+ if (!validateHttpRequestSecurity(request, response, security)) return;
13056
+ if (request.method === "OPTIONS" && request.headers.origin) {
12487
13057
  response.statusCode = 204;
12488
13058
  response.end();
12489
13059
  return;
12490
13060
  }
12491
13061
  const url = new URL(request.url ?? "/", "http://localhost");
13062
+ if (maybeServeOauthMetadata(request, response, security, url.pathname)) return;
12492
13063
  if (request.method === "GET" && url.pathname === "/healthz") {
12493
13064
  writeJson(response, 200, {
12494
13065
  ok: true,
@@ -12510,11 +13081,13 @@ async function handleStreamableHttpMcpRequest(input) {
12510
13081
  writeJson(response, 404, { ok: false, error: "not_found" });
12511
13082
  return;
12512
13083
  }
12513
- const authentication = await authenticateStreamableRequest(config, request.headers.authorization, sessionVerifier, authToken, devNoAuth);
12514
- if (!authentication) {
12515
- writeJson(response, 401, { ok: false, error: "unauthorized" });
13084
+ const authResult = await authenticateStreamableRequest(config, request.headers.authorization, sessionVerifier, security, devNoAuth);
13085
+ if (!authResult.ok) {
13086
+ writeAuthenticationFailure(response, security, authResult.status, authResult.error);
12516
13087
  return;
12517
13088
  }
13089
+ const authentication = authResult.authentication;
13090
+ await pruneExpiredStreamableSessions(sessions, openSessions, security.limits.sessionIdleTimeoutMs, Date.now());
12518
13091
  const sessionId = headerValue(request.headers["mcp-session-id"]);
12519
13092
  if (sessionId) {
12520
13093
  const existing = sessions.get(sessionId);
@@ -12523,9 +13096,10 @@ async function handleStreamableHttpMcpRequest(input) {
12523
13096
  return;
12524
13097
  }
12525
13098
  if (existing.authFingerprint !== authentication.fingerprint) {
12526
- writeJson(response, 401, { ok: false, error: "session_auth_mismatch" });
13099
+ writeAuthenticationFailure(response, security, 401, "unauthorized");
12527
13100
  return;
12528
13101
  }
13102
+ existing.lastSeenAt = Date.now();
12529
13103
  await existing.transport.handleRequest(request, response);
12530
13104
  return;
12531
13105
  }
@@ -12533,54 +13107,73 @@ async function handleStreamableHttpMcpRequest(input) {
12533
13107
  writeJson(response, 400, jsonRpcError(null, -32e3, "MCP initialize request is required before using this Streamable HTTP session."));
12534
13108
  return;
12535
13109
  }
12536
- const parsedBody = JSON.parse(await readRequestBody(request));
12537
- if (!containsInitializeRequest(parsedBody)) {
12538
- writeJson(response, 400, jsonRpcError(requestIdFromPayload(parsedBody), -32e3, "First Streamable HTTP MCP request must be initialize."));
13110
+ if (openSessions.size + initializingSessions.count >= security.limits.maxSessions) {
13111
+ response.setHeader("retry-after", "1");
13112
+ writeJson(response, 503, { ok: false, error: "session_capacity_exhausted", retryable: true, retry_after_ms: 1e3 });
12539
13113
  return;
12540
13114
  }
12541
- let session;
12542
- const transport = new StreamableHTTPServerTransport({
12543
- sessionIdGenerator: () => crypto3.randomUUID(),
12544
- onsessioninitialized: (newSessionId) => {
12545
- if (session) {
12546
- session.sessionId = newSessionId;
12547
- sessions.set(newSessionId, session);
12548
- }
12549
- },
12550
- onsessionclosed: (closedSessionId) => {
12551
- const closed = sessions.get(closedSessionId);
12552
- if (closed) {
12553
- disposeStreamableSession(closed, sessions, openSessions);
13115
+ initializingSessions.count += 1;
13116
+ let initializingSession;
13117
+ try {
13118
+ const parsedBody = JSON.parse(await readRequestBody(request, security.limits.maxRequestBytes));
13119
+ if (!containsInitializeRequest(parsedBody)) {
13120
+ writeJson(response, 400, jsonRpcError(requestIdFromPayload(parsedBody), -32e3, "First Streamable HTTP MCP request must be initialize."));
13121
+ return;
13122
+ }
13123
+ const transport = new StreamableHTTPServerTransport({
13124
+ sessionIdGenerator: () => crypto3.randomUUID(),
13125
+ onsessioninitialized: (newSessionId) => {
13126
+ if (initializingSession) {
13127
+ initializingSession.sessionId = newSessionId;
13128
+ sessions.set(newSessionId, initializingSession);
13129
+ }
13130
+ },
13131
+ onsessionclosed: (closedSessionId) => {
13132
+ const closed = sessions.get(closedSessionId);
13133
+ if (closed) {
13134
+ disposeStreamableSession(closed, sessions, openSessions);
13135
+ }
12554
13136
  }
13137
+ });
13138
+ const runtime = createMcpRuntime(config, {
13139
+ env,
13140
+ storePath,
13141
+ store: sharedStore,
13142
+ sharedResources,
13143
+ resultFormat,
13144
+ cloudTools,
13145
+ trustedContext: authentication.context
13146
+ });
13147
+ initializingSession = { transport, runtime, authFingerprint: authentication.fingerprint, lastSeenAt: Date.now() };
13148
+ openSessions.add(initializingSession);
13149
+ transport.onclose = () => {
13150
+ if (initializingSession) disposeStreamableSession(initializingSession, sessions, openSessions);
13151
+ };
13152
+ await createSynapsorMcpServer(runtime, { toolNameStyle }).connect(transport);
13153
+ await transport.handleRequest(request, response, parsedBody);
13154
+ } catch (error) {
13155
+ if (initializingSession) {
13156
+ disposeStreamableSession(initializingSession, sessions, openSessions);
13157
+ await initializingSession.transport.close().catch(() => void 0);
12555
13158
  }
12556
- });
12557
- const runtime = createMcpRuntime(config, {
12558
- env,
12559
- storePath,
12560
- store: sharedStore,
12561
- sharedResources,
12562
- resultFormat,
12563
- cloudTools,
12564
- trustedContext: authentication.context
12565
- });
12566
- session = { transport, runtime, authFingerprint: authentication.fingerprint };
12567
- openSessions.add(session);
12568
- transport.onclose = () => {
12569
- if (session) disposeStreamableSession(session, sessions, openSessions);
12570
- };
12571
- await createSynapsorMcpServer(runtime, { toolNameStyle }).connect(transport);
12572
- await transport.handleRequest(request, response, parsedBody);
13159
+ throw error;
13160
+ } finally {
13161
+ initializingSessions.count -= 1;
13162
+ }
12573
13163
  } catch (error) {
12574
- const message2 = sanitizeHttpError(error, authToken);
12575
- if (!response.headersSent) writeJson(response, 200, jsonRpcError(null, -32e3, message2));
13164
+ const message2 = sanitizeHttpError(error, security.activeToken, security.previousToken);
13165
+ if (!response.headersSent && error instanceof McpRuntimeError && error.code === "HTTP_BODY_TOO_LARGE") {
13166
+ writeJson(response, 413, { ok: false, error: "request_too_large" });
13167
+ } else if (!response.headersSent) writeJson(response, 200, jsonRpcError(null, -32e3, message2));
12576
13168
  else response.end();
12577
13169
  }
12578
13170
  }
12579
13171
  async function handleHttpMcpRequest(input) {
12580
- const { request, response, runtime, authToken, devNoAuth, corsOrigin, readinessCheck, metricsAccess, metricsProvider } = input;
13172
+ const { request, response, runtime, devNoAuth, security, readinessCheck, metricsAccess, metricsProvider } = input;
12581
13173
  try {
12582
- setCommonHttpHeaders(response, corsOrigin);
12583
- if (request.method === "OPTIONS" && corsOrigin) {
13174
+ setCommonHttpHeaders(response);
13175
+ if (!validateHttpRequestSecurity(request, response, security)) return;
13176
+ if (request.method === "OPTIONS" && request.headers.origin) {
12584
13177
  response.statusCode = 204;
12585
13178
  response.end();
12586
13179
  return;
@@ -12611,13 +13204,13 @@ async function handleHttpMcpRequest(input) {
12611
13204
  writeJson(response, 405, { ok: false, error: "method_not_allowed" });
12612
13205
  return;
12613
13206
  }
12614
- if (!devNoAuth && !validBearerToken(request.headers.authorization, authToken ?? "")) {
12615
- writeJson(response, 401, { ok: false, error: "unauthorized" });
13207
+ if (!devNoAuth && !validBearerTokens(request.headers.authorization, [security.activeToken, security.previousToken])) {
13208
+ writeAuthenticationFailure(response, security, 401, "unauthorized");
12616
13209
  return;
12617
13210
  }
12618
- const body = await readRequestBody(request);
13211
+ const body = await readRequestBody(request, security.limits.maxRequestBytes);
12619
13212
  const payload = JSON.parse(body);
12620
- if (!isRecord4(payload)) {
13213
+ if (!isRecord5(payload)) {
12621
13214
  writeJson(response, 400, jsonRpcError(null, -32600, "JSON-RPC request must be an object."));
12622
13215
  return;
12623
13216
  }
@@ -12627,15 +13220,18 @@ async function handleHttpMcpRequest(input) {
12627
13220
  writeJson(response, 400, jsonRpcError(id, -32600, "JSON-RPC method is required."));
12628
13221
  return;
12629
13222
  }
12630
- const result = await handleHttpJsonRpcMethod(runtime, method, isRecord4(payload.params) ? payload.params : {});
13223
+ const result = await handleHttpJsonRpcMethod(runtime, method, isRecord5(payload.params) ? payload.params : {});
12631
13224
  writeJson(response, 200, {
12632
13225
  jsonrpc: "2.0",
12633
13226
  id,
12634
- result: sanitizeHttpPayload(result, authToken)
13227
+ result: sanitizeHttpPayload(result, security.activeToken, security.previousToken)
12635
13228
  });
12636
13229
  } catch (error) {
12637
- const message2 = sanitizeHttpError(error, authToken);
12638
- writeJson(response, 200, jsonRpcError(null, -32e3, message2));
13230
+ const message2 = sanitizeHttpError(error, security.activeToken, security.previousToken);
13231
+ if (!response.headersSent && error instanceof McpRuntimeError && error.code === "HTTP_BODY_TOO_LARGE") {
13232
+ writeJson(response, 413, { ok: false, error: "request_too_large" });
13233
+ } else if (!response.headersSent) writeJson(response, 200, jsonRpcError(null, -32e3, message2));
13234
+ else response.end();
12639
13235
  }
12640
13236
  }
12641
13237
  async function handleHttpJsonRpcMethod(runtime, method, params) {
@@ -12647,7 +13243,7 @@ async function handleHttpJsonRpcMethod(runtime, method, params) {
12647
13243
  if (method === "tools/call") {
12648
13244
  const name = typeof params.name === "string" ? params.name : void 0;
12649
13245
  if (!name) throw new McpRuntimeError("HTTP_TOOL_NAME_REQUIRED", "tools/call requires params.name.");
12650
- const args = isRecord4(params.arguments) ? params.arguments : isRecord4(params.args) ? params.args : {};
13246
+ const args = isRecord5(params.arguments) ? params.arguments : isRecord5(params.args) ? params.args : {};
12651
13247
  return await toolCallResult(runtime, name, args);
12652
13248
  }
12653
13249
  if (method === "resources/read") {
@@ -12677,11 +13273,21 @@ function httpToolMetadata(tool) {
12677
13273
  };
12678
13274
  }
12679
13275
  function validBearerToken(header, expected) {
12680
- if (!header?.startsWith("Bearer ")) return false;
12681
- const actual = header.slice("Bearer ".length);
12682
- const actualBuffer = Buffer.from(actual);
12683
- const expectedBuffer = Buffer.from(expected);
12684
- return actualBuffer.length === expectedBuffer.length && crypto3.timingSafeEqual(actualBuffer, expectedBuffer);
13276
+ return validBearerTokens(header, [expected]);
13277
+ }
13278
+ function validBearerTokens(header, expected) {
13279
+ const actual = bearerToken(header);
13280
+ if (!actual) return false;
13281
+ let matched = 0;
13282
+ for (const candidate of expected.filter((value) => Boolean(value))) {
13283
+ matched |= Number(constantTimeTokenEquals(actual, candidate));
13284
+ }
13285
+ return matched === 1;
13286
+ }
13287
+ function constantTimeTokenEquals(actual, expected) {
13288
+ const actualDigest = crypto3.createHash("sha256").update(actual, "utf8").digest();
13289
+ const expectedDigest = crypto3.createHash("sha256").update(expected, "utf8").digest();
13290
+ return crypto3.timingSafeEqual(actualDigest, expectedDigest);
12685
13291
  }
12686
13292
  function resolveMetricsEndpointAccess(config, env, host) {
12687
13293
  if (config.metrics?.enabled !== true) return { enabled: false };
@@ -12788,12 +13394,12 @@ async function checkRunnerReadiness(config, env = process.env, timeoutMs = 3e3)
12788
13394
  if (!executorName || checkedExecutors.has(executorName)) continue;
12789
13395
  checkedExecutors.add(executorName);
12790
13396
  components.push(await readinessComponent(`executor:${executorName}`, "EXECUTOR_READY", "EXECUTOR_UNAVAILABLE", timeoutMs, async () => {
12791
- const executor = isRecord4(config.executors?.[executorName]) ? config.executors?.[executorName] : void 0;
13397
+ const executor = isRecord5(config.executors?.[executorName]) ? config.executors?.[executorName] : void 0;
12792
13398
  if (!executor) throw new Error("executor missing");
12793
13399
  if (executor.type === "http_handler") {
12794
13400
  if (typeof executor.url_env !== "string" || !envValue(env, executor.url_env)) throw new Error("handler URL unavailable");
12795
13401
  const handlerUrl = envValue(env, executor.url_env);
12796
- const auth = isRecord4(executor.auth) ? executor.auth : void 0;
13402
+ const auth = isRecord5(executor.auth) ? executor.auth : void 0;
12797
13403
  if (auth?.type === "bearer_env" && (typeof auth.token_env !== "string" || !envValue(env, auth.token_env))) throw new Error("handler token unavailable");
12798
13404
  const response = await fetch(handlerUrl, {
12799
13405
  method: "HEAD",
@@ -12886,20 +13492,25 @@ function sessionAuthVerifier(config, env, baseDir) {
12886
13492
  throw new McpRuntimeError("SESSION_AUTH_INVALID", message2);
12887
13493
  }
12888
13494
  }
12889
- async function authenticateStreamableRequest(config, authorization, sessionVerifier, staticToken, devNoAuth) {
12890
- if (devNoAuth) return { fingerprint: "dev-no-auth" };
13495
+ async function authenticateStreamableRequest(config, authorization, sessionVerifier, security, devNoAuth) {
13496
+ if (devNoAuth) return { ok: true, authentication: { fingerprint: "dev-no-auth" } };
12891
13497
  const token = bearerToken(authorization);
12892
- if (!token) return void 0;
13498
+ if (!token) return { ok: false, status: 401, error: "unauthorized" };
12893
13499
  if (!configUsesHttpClaims(config)) {
12894
- if (!staticToken || !validBearerToken(authorization, staticToken)) return void 0;
12895
- return { fingerprint: tokenFingerprint(token) };
13500
+ if (!validBearerTokens(authorization, [security.activeToken, security.previousToken])) {
13501
+ return { ok: false, status: 401, error: "unauthorized" };
13502
+ }
13503
+ return { ok: true, authentication: { fingerprint: tokenFingerprint(token) } };
12896
13504
  }
12897
13505
  try {
12898
- if (!sessionVerifier) return void 0;
13506
+ if (!sessionVerifier) return { ok: false, status: 401, error: "unauthorized" };
12899
13507
  const context = await verifySessionJwt(config, token, sessionVerifier);
12900
- return { fingerprint: tokenFingerprint(token), context };
12901
- } catch {
12902
- return void 0;
13508
+ return { ok: true, authentication: { fingerprint: tokenFingerprint(token), context } };
13509
+ } catch (error) {
13510
+ if (error instanceof McpRuntimeError && error.code === "HTTP_INSUFFICIENT_SCOPE") {
13511
+ return { ok: false, status: 403, error: "insufficient_scope" };
13512
+ }
13513
+ return { ok: false, status: 401, error: "unauthorized" };
12903
13514
  }
12904
13515
  }
12905
13516
  async function verifySessionJwt(config, token, verifier) {
@@ -12909,12 +13520,34 @@ async function verifySessionJwt(config, token, verifier) {
12909
13520
  const tenant = safeSessionClaim(claims[auth.tenant_claim ?? "tenant_id"]);
12910
13521
  const principal = safeSessionClaim(claims[auth.principal_claim ?? "sub"]);
12911
13522
  if (!tenant || !principal) throw new Error("JWT trusted context claims are missing or unsafe");
13523
+ const requiredScopes = config.http_security?.oauth_resource?.required_scopes ?? [];
13524
+ if (requiredScopes.length > 0) {
13525
+ const granted = safeJwtScopes(claims.scope, claims.scp);
13526
+ if (!requiredScopes.every((scope) => granted.has(scope))) {
13527
+ throw new McpRuntimeError("HTTP_INSUFFICIENT_SCOPE", "JWT does not grant the required MCP resource scope.");
13528
+ }
13529
+ }
12912
13530
  return { tenant_id: tenant, principal, provenance: "http_claims" };
12913
13531
  }
12914
13532
  function bearerToken(header) {
12915
- if (!header?.startsWith("Bearer ")) return void 0;
12916
- const token = header.slice("Bearer ".length).trim();
12917
- return token || void 0;
13533
+ const match = /^Bearer[ \t]+([^\s,]+)$/i.exec(header ?? "");
13534
+ const token = match?.[1];
13535
+ return token && token.length <= 16384 ? token : void 0;
13536
+ }
13537
+ function safeJwtScopes(scope, scp) {
13538
+ const values = [];
13539
+ if (typeof scope === "string" && scope.length <= 8192 && !/[\u0000-\u001f\u007f]/.test(scope)) {
13540
+ values.push(...scope.split(/\s+/).filter(Boolean));
13541
+ } else if (scope !== void 0) {
13542
+ throw new Error("JWT scope claim is unsafe");
13543
+ }
13544
+ if (Array.isArray(scp) && scp.length <= 64 && scp.every((value) => typeof value === "string" && value.length <= 128 && !/[\s\u0000-\u001f\u007f]/.test(value))) {
13545
+ values.push(...scp);
13546
+ } else if (scp !== void 0) {
13547
+ throw new Error("JWT scp claim is unsafe");
13548
+ }
13549
+ if (values.length > 128 || values.some((value) => value.length > 128)) throw new Error("JWT scope claim is unsafe");
13550
+ return new Set(values);
12918
13551
  }
12919
13552
  function tokenFingerprint(token) {
12920
13553
  return `sha256:${crypto3.createHash("sha256").update(token).digest("hex")}`;
@@ -12934,10 +13567,10 @@ function containsInitializeRequest(payload) {
12934
13567
  }
12935
13568
  function requestIdFromPayload(payload) {
12936
13569
  if (Array.isArray(payload)) {
12937
- const request = payload.find((message2) => isRecord4(message2) && "id" in message2);
12938
- return isRecord4(request) ? request.id ?? null : null;
13570
+ const request = payload.find((message2) => isRecord5(message2) && "id" in message2);
13571
+ return isRecord5(request) ? request.id ?? null : null;
12939
13572
  }
12940
- return isRecord4(payload) ? payload.id ?? null : null;
13573
+ return isRecord5(payload) ? payload.id ?? null : null;
12941
13574
  }
12942
13575
  function openaiToolNameAlias(canonicalName) {
12943
13576
  const sanitized = canonicalName.replace(/[^A-Za-z0-9_-]+/g, "__").replace(/_{3,}/g, "__").replace(/^_+|_+$/g, "");
@@ -12992,14 +13625,14 @@ function writeJson(response, statusCode, payload) {
12992
13625
  response.end(`${JSON.stringify(payload, null, 2)}
12993
13626
  `);
12994
13627
  }
12995
- async function readRequestBody(request) {
13628
+ async function readRequestBody(request, maxBytes = 1048576) {
12996
13629
  const chunks = [];
12997
13630
  let bytes = 0;
12998
13631
  for await (const chunk of request) {
12999
13632
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
13000
13633
  bytes += buffer.length;
13001
- if (bytes > 1024 * 1024) {
13002
- throw new McpRuntimeError("HTTP_BODY_TOO_LARGE", "HTTP MCP request body exceeds 1 MiB.");
13634
+ if (bytes > maxBytes) {
13635
+ throw new McpRuntimeError("HTTP_BODY_TOO_LARGE", `HTTP MCP request body exceeds the configured ${maxBytes}-byte limit.`);
13003
13636
  }
13004
13637
  chunks.push(buffer);
13005
13638
  }
@@ -13012,21 +13645,21 @@ function jsonRpcError(id, code, message2) {
13012
13645
  error: { code, message: message2 }
13013
13646
  };
13014
13647
  }
13015
- function sanitizeHttpError(error, authToken) {
13648
+ function sanitizeHttpError(error, ...authTokens) {
13016
13649
  const raw = error instanceof Error ? error.message : String(error);
13017
- return sanitizeHttpString(raw, authToken);
13650
+ return sanitizeHttpString(raw, ...authTokens);
13018
13651
  }
13019
- function sanitizeHttpPayload(value, authToken) {
13020
- if (typeof value === "string") return sanitizeHttpString(value, authToken);
13021
- if (Array.isArray(value)) return value.map((item) => sanitizeHttpPayload(item, authToken));
13022
- if (isRecord4(value)) {
13023
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeHttpPayload(item, authToken)]));
13652
+ function sanitizeHttpPayload(value, ...authTokens) {
13653
+ if (typeof value === "string") return sanitizeHttpString(value, ...authTokens);
13654
+ if (Array.isArray(value)) return value.map((item) => sanitizeHttpPayload(item, ...authTokens));
13655
+ if (isRecord5(value)) {
13656
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeHttpPayload(item, ...authTokens)]));
13024
13657
  }
13025
13658
  return value;
13026
13659
  }
13027
- function sanitizeHttpString(value, authToken) {
13660
+ function sanitizeHttpString(value, ...authTokens) {
13028
13661
  let redacted = value.replace(/(?:postgres(?:ql)?|mysql):\/\/[^\s"']+/gi, "[redacted-database-url]");
13029
- if (authToken) redacted = redacted.split(authToken).join("[redacted-token]");
13662
+ for (const authToken of authTokens) if (authToken) redacted = redacted.split(authToken).join("[redacted-token]");
13030
13663
  return redacted;
13031
13664
  }
13032
13665
  function isLoopbackHost(host) {
@@ -13060,6 +13693,13 @@ async function closeStreamableSessions(sessions) {
13060
13693
  disposeStreamableSession(session);
13061
13694
  }
13062
13695
  }
13696
+ async function pruneExpiredStreamableSessions(sessions, openSessions, idleTimeoutMs, now) {
13697
+ const expired = [...openSessions].filter((session) => now - session.lastSeenAt >= idleTimeoutMs);
13698
+ for (const session of expired) {
13699
+ disposeStreamableSession(session, sessions, openSessions);
13700
+ await session.transport.close().catch(() => void 0);
13701
+ }
13702
+ }
13063
13703
  function disposeStreamableSession(session, sessionMap, openSessions) {
13064
13704
  if (session.closed) return;
13065
13705
  session.closed = true;
@@ -13086,7 +13726,7 @@ async function toolCallResult(runtime, toolName, args) {
13086
13726
  }
13087
13727
  async function withProposalReviewPresentation(runtime, result) {
13088
13728
  const directId = typeof result.proposal_id === "string" ? result.proposal_id : void 0;
13089
- const nested = isRecord4(result.proposal) ? result.proposal : void 0;
13729
+ const nested = isRecord5(result.proposal) ? result.proposal : void 0;
13090
13730
  const proposalId = directId ?? (typeof nested?.id === "string" ? nested.id : void 0);
13091
13731
  if (!proposalId || proposalId === "wrp_unknown") return result;
13092
13732
  try {
@@ -13167,11 +13807,11 @@ function toolDescriptionWithCanonical(description, canonicalName, exposedName) {
13167
13807
  ${description}`;
13168
13808
  }
13169
13809
  function zodInputShapeFromJsonSchema(schema) {
13170
- const properties = isRecord4(schema.properties) ? schema.properties : {};
13810
+ const properties = isRecord5(schema.properties) ? schema.properties : {};
13171
13811
  const required = Array.isArray(schema.required) ? new Set(schema.required.map(String)) : /* @__PURE__ */ new Set();
13172
13812
  const shape = {};
13173
13813
  for (const [name, rawProperty] of Object.entries(properties)) {
13174
- const property = isRecord4(rawProperty) ? rawProperty : {};
13814
+ const property = isRecord5(rawProperty) ? rawProperty : {};
13175
13815
  let valueSchema;
13176
13816
  if (Array.isArray(property.enum)) {
13177
13817
  const allowed = property.enum.map((item) => scalar2(item));
@@ -13574,13 +14214,13 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13574
14214
  const kind = capability?.kind ?? (typeof legacy.proposal_id === "string" ? "proposal" : "read");
13575
14215
  const evidenceBundleId = typeof legacy.evidence_bundle_id === "string" ? legacy.evidence_bundle_id : void 0;
13576
14216
  const sourceChanged = Boolean(legacy.source_database_changed ?? legacy.source_database_mutated ?? false);
13577
- const context = isRecord4(legacy.trusted_context) ? legacy.trusted_context : void 0;
13578
- const target = isRecord4(legacy.target) ? legacy.target : void 0;
14217
+ const context = isRecord5(legacy.trusted_context) ? legacy.trusted_context : void 0;
14218
+ const target = isRecord5(legacy.target) ? legacy.target : void 0;
13579
14219
  if (kind === "proposal") {
13580
14220
  const proposalId = typeof legacy.proposal_id === "string" ? legacy.proposal_id : "wrp_unknown";
13581
14221
  const targetType = typeof target?.type === "string" ? target.type : capability?.target.table ?? "object";
13582
14222
  const targetId = target?.id !== void 0 ? String(target.id) : "unknown";
13583
- const approval = isRecord4(legacy.approval) ? legacy.approval : void 0;
14223
+ const approval = isRecord5(legacy.approval) ? legacy.approval : void 0;
13584
14224
  const state = typeof legacy.status === "string" ? legacy.status : "review_required";
13585
14225
  const approvalRequired = legacy.approval_required !== false;
13586
14226
  const executor = writebackExecutorName(legacy.writeback);
@@ -13596,7 +14236,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13596
14236
  id: proposalId,
13597
14237
  state,
13598
14238
  target: `${targetType}:${targetId}`,
13599
- diff: isRecord4(legacy.diff) ? legacy.diff : {},
14239
+ diff: isRecord5(legacy.diff) ? legacy.diff : {},
13600
14240
  approval_required: approvalRequired,
13601
14241
  ...approval ? { approval } : {},
13602
14242
  writeback: {
@@ -13617,7 +14257,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13617
14257
  }
13618
14258
  };
13619
14259
  }
13620
- const businessObject = isRecord4(legacy.business_object) ? legacy.business_object : void 0;
14260
+ const businessObject = isRecord5(legacy.business_object) ? legacy.business_object : void 0;
13621
14261
  const objectType = typeof businessObject?.type === "string" ? businessObject.type : capability?.target.table ?? "record";
13622
14262
  const objectId = businessObject?.id !== void 0 ? String(businessObject.id) : String(legacy.action ?? action);
13623
14263
  return {
@@ -13625,7 +14265,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13625
14265
  summary: kind === "aggregate_read" ? `Read one reviewed aggregate through ${action}. Source member rows exposed: no. Source database changed: no.` : `Read ${objectType} ${objectId} through ${action}. Source database changed: no.`,
13626
14266
  action,
13627
14267
  kind,
13628
- data: isRecord4(legacy.data) ? legacy.data : {},
14268
+ data: isRecord5(legacy.data) ? legacy.data : {},
13629
14269
  proposal: null,
13630
14270
  error: null,
13631
14271
  evidence: evidenceBundleId ? evidenceHandle(evidenceBundleId) : null,
@@ -13639,7 +14279,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13639
14279
  };
13640
14280
  }
13641
14281
  function writebackExecutorName(value) {
13642
- if (!isRecord4(value)) return void 0;
14282
+ if (!isRecord5(value)) return void 0;
13643
14283
  return typeof value.executor === "string" ? value.executor : typeof value.mode === "string" ? value.mode : void 0;
13644
14284
  }
13645
14285
  function capabilityWritebackMode2(capability) {
@@ -13673,7 +14313,7 @@ function assertProposalWritebackResolvable(config, capability) {
13673
14313
  throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but has no executor name.`);
13674
14314
  }
13675
14315
  const executor = config.executors?.[executorName];
13676
- if (!isRecord4(executor)) {
14316
+ if (!isRecord5(executor)) {
13677
14317
  throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but executor ${executorName} is not configured.`);
13678
14318
  }
13679
14319
  if (executor.type !== "http_handler" && executor.type !== "command_handler") {
@@ -13940,26 +14580,26 @@ function errorChain(error) {
13940
14580
  while (current !== void 0 && current !== null && chain.length < 6 && !seen.has(current)) {
13941
14581
  chain.push(current);
13942
14582
  seen.add(current);
13943
- current = isRecord4(current) ? current.cause : void 0;
14583
+ current = isRecord5(current) ? current.cause : void 0;
13944
14584
  }
13945
14585
  return chain;
13946
14586
  }
13947
14587
  function errorStringProperty(error, property) {
13948
- if (!isRecord4(error)) return void 0;
14588
+ if (!isRecord5(error)) return void 0;
13949
14589
  const value = error[property];
13950
14590
  if (typeof value !== "string" && typeof value !== "number") return void 0;
13951
14591
  const normalized = String(value).trim().toUpperCase();
13952
14592
  return normalized || void 0;
13953
14593
  }
13954
14594
  function errorNumberProperty(error, property) {
13955
- if (!isRecord4(error)) return void 0;
14595
+ if (!isRecord5(error)) return void 0;
13956
14596
  const value = error[property];
13957
14597
  const normalized = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
13958
14598
  return Number.isInteger(normalized) ? normalized : void 0;
13959
14599
  }
13960
14600
  function errorMessage(error) {
13961
14601
  if (error instanceof Error) return error.message;
13962
- if (isRecord4(error) && typeof error.message === "string") return error.message;
14602
+ if (isRecord5(error) && typeof error.message === "string") return error.message;
13963
14603
  return typeof error === "string" ? error : "";
13964
14604
  }
13965
14605
  function effectivePrincipalScope(config, capability, context) {
@@ -14708,7 +15348,7 @@ function validateToolArgs(capability, args) {
14708
15348
  if (!Array.isArray(value)) throw new McpRuntimeError("ARGUMENT_TYPE_INVALID", `${name} must be an array of reviewed objects.`);
14709
15349
  if (value.length < 1 || value.length > spec.max_items) throw new McpRuntimeError("ARGUMENT_ITEM_COUNT_INVALID", `${name} must contain 1 through ${spec.max_items} items.`);
14710
15350
  for (const [index, item] of value.entries()) {
14711
- if (!isRecord4(item)) throw new McpRuntimeError("ARGUMENT_ITEM_TYPE_INVALID", `${name}[${index}] must be an object.`);
15351
+ if (!isRecord5(item)) throw new McpRuntimeError("ARGUMENT_ITEM_TYPE_INVALID", `${name}[${index}] must be an object.`);
14712
15352
  for (const key of Object.keys(item)) if (!Object.prototype.hasOwnProperty.call(spec.fields, key)) throw new McpRuntimeError("ARGUMENT_ITEM_FIELD_NOT_ALLOWED", `${name}[${index}].${key} is not a reviewed item field.`);
14713
15353
  for (const [fieldName, fieldSpec] of Object.entries(spec.fields)) validateScalarArg(`${name}[${index}].${fieldName}`, fieldSpec, item[fieldName]);
14714
15354
  }
@@ -14810,7 +15450,7 @@ function buildPatch(capability, args) {
14810
15450
  function batchItemsFromArgs(capability, args) {
14811
15451
  const argumentName = capability.operation?.batch?.items_from_arg;
14812
15452
  const value = argumentName ? args[argumentName] : void 0;
14813
- if (!argumentName || !Array.isArray(value) || value.some((item) => !isRecord4(item))) {
15453
+ if (!argumentName || !Array.isArray(value) || value.some((item) => !isRecord5(item))) {
14814
15454
  throw new McpRuntimeError("BATCH_ITEMS_REQUIRED", `Bounded INSERT capability ${capability.name} requires its reviewed object-array argument.`);
14815
15455
  }
14816
15456
  return value;
@@ -15048,7 +15688,7 @@ function stableId(prefix, input) {
15048
15688
  function hashJson(input) {
15049
15689
  return `sha256:${crypto3.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
15050
15690
  }
15051
- function isRecord4(value) {
15691
+ function isRecord5(value) {
15052
15692
  return typeof value === "object" && value !== null && !Array.isArray(value);
15053
15693
  }
15054
15694
  function toolErrorPayload(error) {