@classytic/arc 2.18.5 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +1 -1
  2. package/bin/arc.js +37 -0
  3. package/dist/{BaseController-Dy6EQHjM.mjs → BaseController-D-MHZ27_.mjs} +4 -4
  4. package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
  5. package/dist/{actionPermissions-h7DYDNMg.mjs → actionPermissions-C9RMiIpc.mjs} +2 -1
  6. package/dist/audit/index.mjs +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +17 -6
  9. package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
  10. package/dist/cache/index.mjs +2 -2
  11. package/dist/cli/commands/describe.d.mts +1 -1
  12. package/dist/cli/commands/describe.mjs +2 -2
  13. package/dist/cli/commands/docs.mjs +2 -2
  14. package/dist/cli/commands/generate.mjs +1 -1
  15. package/dist/cli/commands/init.mjs +1 -1
  16. package/dist/cli/commands/introspect.mjs +4 -13
  17. package/dist/cli/index.mjs +2 -2
  18. package/dist/{constants-gaEeMlz-.mjs → constants-DNre0wRg.mjs} +1 -1
  19. package/dist/core/index.d.mts +2 -2
  20. package/dist/core/index.mjs +4 -4
  21. package/dist/{core-ZnQBa5rh.mjs → core-CWhyGkiM.mjs} +136 -59
  22. package/dist/{createActionRouter-CagItpxI.mjs → createActionRouter-HlQso0jO.mjs} +8 -7
  23. package/dist/{createAggregationRouter-DVKroXhC.mjs → createAggregationRouter-5LzPzgw1.mjs} +5 -4
  24. package/dist/{createApp--703hQE1.mjs → createApp-D6uwzCKu.mjs} +210 -17
  25. package/dist/docs/index.d.mts +1 -1
  26. package/dist/docs/index.mjs +1 -1
  27. package/dist/encryption/index.d.mts +222 -0
  28. package/dist/encryption/index.mjs +270 -0
  29. package/dist/events/index.mjs +9 -6
  30. package/dist/factory/index.d.mts +3 -2
  31. package/dist/factory/index.mjs +2 -2
  32. package/dist/{fastify-augmentation-Cxb0ROIw.d.mts → fastify-augmentation-CCQJEDgh.d.mts} +12 -1
  33. package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
  34. package/dist/hooks/index.d.mts +1 -1
  35. package/dist/idempotency/index.mjs +1 -1
  36. package/dist/{index-CRKiiP5o.d.mts → index-CdFkIMEd.d.mts} +5 -0
  37. package/dist/{index-Dh0_RSTd.d.mts → index-DUa8WHz3.d.mts} +7 -7
  38. package/dist/{index-CHNQIhcb.d.mts → index-_--GyzcO.d.mts} +111 -2
  39. package/dist/index.d.mts +3 -3
  40. package/dist/index.mjs +6 -6
  41. package/dist/{init-Bqpt1Hxf.mjs → init-YH_nslQR.mjs} +87 -5
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +2 -2
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/integrations/streamline.d.mts +17 -5
  48. package/dist/integrations/streamline.mjs +9 -5
  49. package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
  50. package/dist/{mcpPlugin-D1wuRD2c.mjs → mcpPlugin-CuW6L_uD.mjs} +148 -49
  51. package/dist/middleware/index.d.mts +1 -1
  52. package/dist/middleware/index.mjs +1 -1
  53. package/dist/module-JYH9V0I8.d.mts +384 -0
  54. package/dist/{openapi-xVJibapr.mjs → openapi-Dq6RbEGK.mjs} +3 -3
  55. package/dist/permissions/index.d.mts +1 -1
  56. package/dist/permissions/index.mjs +1 -1
  57. package/dist/{permissions-BPB1xqGf.mjs → permissions-CgbOLJZG.mjs} +10 -2
  58. package/dist/pipeline/index.d.mts +1 -1
  59. package/dist/plugins/index.d.mts +10 -5
  60. package/dist/plugins/index.mjs +3 -3
  61. package/dist/plugins/tracing-entry.mjs +1 -1
  62. package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
  63. package/dist/presets/filesUpload.d.mts +1 -1
  64. package/dist/presets/filesUpload.mjs +1 -1
  65. package/dist/presets/index.d.mts +16 -2
  66. package/dist/presets/index.mjs +1 -1
  67. package/dist/presets/multiTenant.d.mts +1 -1
  68. package/dist/presets/multiTenant.mjs +1 -1
  69. package/dist/presets/search.d.mts +1 -1
  70. package/dist/presets/search.mjs +3 -2
  71. package/dist/{presets-BtyFwTO4.mjs → presets-DWsYQdbV.mjs} +1 -1
  72. package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
  73. package/dist/{registerRoute-CPmdkZGu.mjs → registerRoute-DGSREwPw.mjs} +20 -2
  74. package/dist/registry/index.d.mts +1 -1
  75. package/dist/registry/index.mjs +2 -2
  76. package/dist/{registry-Bm9LjWKs.mjs → registry-vcOCtPij.mjs} +1 -1
  77. package/dist/{schemaIR-D1MJwqmp.mjs → schemaIR-ajetw3Fv.mjs} +1 -42
  78. package/dist/scim/index.mjs +2 -2
  79. package/dist/sync/changelog.d.mts +2 -0
  80. package/dist/sync/changelog.mjs +2 -0
  81. package/dist/testing/index.d.mts +2 -2
  82. package/dist/testing/index.mjs +2 -2
  83. package/dist/types/index.d.mts +2 -2
  84. package/dist/{types-CmkknLzI.d.mts → types-BHMKlMBU.d.mts} +53 -195
  85. package/dist/{types-D60PFwfG.d.mts → types-C9b2HR5W.d.mts} +1 -1
  86. package/dist/utils/index.d.mts +1 -1
  87. package/dist/utils/index.mjs +1 -1
  88. package/dist/{utils-C6QQ9-hd.mjs → utils-ChjYQpjn.mjs} +1400 -1400
  89. package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
  90. package/package.json +22 -8
  91. package/skills/arc/SKILL.md +10 -0
  92. package/skills/arc/references/mcp.md +2 -0
@@ -1,14 +1,15 @@
1
1
  import { p as isArcError, t as ArcError } from "./errors-DkO8Nk6-.mjs";
2
- import { t as BaseController } from "./BaseController-Dy6EQHjM.mjs";
3
- import { N as normalizePermissionResult } from "./permissions-BPB1xqGf.mjs";
4
- import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-h7DYDNMg.mjs";
2
+ import { c as getOrgId, g as getUserId } from "./types-bHblZ9YN.mjs";
3
+ import { t as BaseController } from "./BaseController-D-MHZ27_.mjs";
4
+ import { N as normalizePermissionResult } from "./permissions-CgbOLJZG.mjs";
5
5
  import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
6
- import { t as pluralize } from "./pluralize-DccFV9R7.mjs";
7
- import { i as shouldRejectAdditionalProperties, r as schemaIRToZodShape, t as normalizeSchemaIR } from "./schemaIR-D1MJwqmp.mjs";
8
- import { n as validateAggregations } from "./validate-CRcaWzp2.mjs";
9
- import { n as executeAggregation } from "./buildHandler-B0qvAguf.mjs";
10
- import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
6
+ import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-C9RMiIpc.mjs";
7
+ import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
8
+ import { n as executeAggregation } from "./buildHandler-D_2ULWxZ.mjs";
9
+ import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
10
+ import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-ajetw3Fv.mjs";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
+ import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
12
13
  import fp from "fastify-plugin";
13
14
  import { z } from "zod";
14
15
  //#region src/integrations/mcp/createMcpServer.ts
@@ -264,7 +265,7 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
264
265
  * them explicitly here rather than asking the parser at runtime — Arc must
265
266
  * not import MongoKit internals just to know what an operator looks like.
266
267
  */
267
- const OPERATOR_SUFFIXES = new Set([
268
+ const OPERATOR_SUFFIXES = /* @__PURE__ */ new Set([
268
269
  "eq",
269
270
  "ne",
270
271
  "gt",
@@ -671,12 +672,20 @@ function resolveCrudDescription(override, meta) {
671
672
  * // shape = { name: z.string(), price: z.number() }
672
673
  * ```
673
674
  */
675
+ /**
676
+ * Projection (`select`) field — reused by the list shape AND the single-entity
677
+ * `get` shape. The controller honors `select` on `getById` identically to list
678
+ * (both flow through `QueryResolver.resolve`), so exposing it on `get` gives an
679
+ * MCP agent the same field-projection it already has on list.
680
+ */
681
+ const PROJECTION_FIELD = z.string().optional().describe("Comma-separated field list to project (e.g. 'name,price'). Prefix with '-' to exclude (e.g. '-description').");
674
682
  const PAGINATION_SHAPE = {
675
- page: z.number().int().min(1).optional().describe("Page number (1-based)"),
683
+ page: z.number().int().min(1).optional().describe("Page number (1-based). Ignored when `cursor` is set."),
676
684
  limit: z.number().int().min(1).max(100).optional().describe("Items per page (max 100)"),
685
+ cursor: z.string().optional().describe("Opaque keyset cursor from a previous page's `next`. Pass it to fetch the following page; when set, `page` is ignored (cursor/keyset pagination — stable under concurrent inserts)."),
677
686
  sort: z.string().optional().describe("Sort field, prefix with - for descending"),
678
687
  search: z.string().optional().describe("Full-text search query"),
679
- select: z.string().optional().describe("Comma-separated field list to project (e.g. 'name,price'). Prefix with '-' to exclude (e.g. '-description')."),
688
+ select: PROJECTION_FIELD,
680
689
  populate: z.string().optional().describe("Comma-separated relation paths to hydrate (e.g. 'supplier,category'). Follows Mongoose populate syntax when the adapter is MongoKit.")
681
690
  };
682
691
  /**
@@ -688,7 +697,7 @@ function fieldRulesToZod(fieldRules, options = {}) {
688
697
  const { mode = "create", hiddenFields = [], readonlyFields = [], extraHideFields = [] } = options;
689
698
  if (mode === "list") return buildListShape(fieldRules, options);
690
699
  if (!fieldRules) return {};
691
- const allHidden = new Set([...hiddenFields, ...extraHideFields]);
700
+ const allHidden = /* @__PURE__ */ new Set([...hiddenFields, ...extraHideFields]);
692
701
  const allReadonly = new Set(readonlyFields);
693
702
  const shape = {};
694
703
  for (const [name, rule] of Object.entries(fieldRules)) {
@@ -738,7 +747,7 @@ function typeToZod(type) {
738
747
  }
739
748
  }
740
749
  /** Operators that apply to numeric/date fields */
741
- const COMPARISON_OPS = new Set([
750
+ const COMPARISON_OPS = /* @__PURE__ */ new Set([
742
751
  "gt",
743
752
  "gte",
744
753
  "lt",
@@ -761,7 +770,7 @@ function opDescription(op, fieldName) {
761
770
  /** Build list/query shape with filterable fields, operators, and pagination */
762
771
  function buildListShape(fieldRules, options) {
763
772
  const { filterableFields = [], hiddenFields = [], extraHideFields = [], allowedOperators } = options;
764
- const allHidden = new Set([...hiddenFields, ...extraHideFields]);
773
+ const allHidden = /* @__PURE__ */ new Set([...hiddenFields, ...extraHideFields]);
765
774
  const shape = { ...PAGINATION_SHAPE };
766
775
  if (!fieldRules) return shape;
767
776
  for (const name of filterableFields) {
@@ -917,6 +926,61 @@ async function forwardResponse(reply, response) {
917
926
  reply.send(await response.text());
918
927
  }
919
928
  //#endregion
929
+ //#region src/core/schemaIRZod.ts
930
+ /**
931
+ * Schema IR → Zod adapter — the MCP-facing half of [schemaIR.ts](./schemaIR.ts).
932
+ *
933
+ * Lives in its OWN module (not schemaIR.ts) because it is the only part that
934
+ * needs zod at runtime. schemaIR.ts is statically imported by
935
+ * `createActionRouter`, which every actions-bearing resource loads at boot —
936
+ * a top-level `import { z } from "zod"` there made zod a de-facto REQUIRED
937
+ * dependency for any host using `actions:`, despite zod being an optional
938
+ * peer (boot crash when absent, even with plain-JSON-Schema action bodies
939
+ * and no MCP). Only MCP code imports this module, and the MCP integration
940
+ * already hard-requires zod (the SDK does), so the top-level import is
941
+ * correct HERE and a bug THERE.
942
+ */
943
+ /**
944
+ * Emit a flat Zod shape from the IR. The MCP SDK wraps the returned record
945
+ * in `z.object()` internally, so we return the bare shape (same contract
946
+ * as `ToolDefinition.inputSchema`).
947
+ *
948
+ * `additionalProperties: false` is honored at the MCP handler layer rather
949
+ * than baked into the Zod shape — the SDK's input validation happens before
950
+ * the handler runs, and flat shapes can't express `.strict()` mode.
951
+ * `shouldRejectAdditionalProperties(ir)` (schemaIR.ts) returns the flag so
952
+ * callers can gate their handler on it.
953
+ */
954
+ function schemaIRToZodShape(ir) {
955
+ const requiredSet = new Set(ir.required);
956
+ const result = {};
957
+ for (const [name, prop] of Object.entries(ir.properties)) {
958
+ const desc = typeof prop.description === "string" && prop.description.length > 0 ? prop.description : name;
959
+ const base = jsonSchemaPropToZod(prop);
960
+ result[name] = requiredSet.has(name) ? base.describe(desc) : base.optional().describe(desc);
961
+ }
962
+ return result;
963
+ }
964
+ /**
965
+ * Convert a single JSON Schema property to a Zod type. Understands enum,
966
+ * numeric/integer/boolean/array/object, and falls back to string for
967
+ * unrecognized types (matches MCP's "strings for opaque fields" convention).
968
+ *
969
+ * Internal — use `schemaIRToZodShape` which wires this up with required/optional
970
+ * + description handling.
971
+ */
972
+ function jsonSchemaPropToZod(schema) {
973
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return z.enum(schema.enum);
974
+ switch (typeof schema.type === "string" ? schema.type : "string") {
975
+ case "number":
976
+ case "integer": return z.number();
977
+ case "boolean": return z.boolean();
978
+ case "array": return z.array(z.unknown());
979
+ case "object": return z.record(z.string(), z.unknown());
980
+ default: return z.string();
981
+ }
982
+ }
983
+ //#endregion
920
984
  //#region src/integrations/mcp/action-tools.ts
921
985
  /**
922
986
  * Convert an action's `schema` field into a Zod shape for MCP input.
@@ -942,7 +1006,7 @@ function convertActionSchemaToZod(raw) {
942
1006
  */
943
1007
  function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false) {
944
1008
  const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
945
- const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
1009
+ const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? /* @__PURE__ */ new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
946
1010
  return async (input, ctx) => {
947
1011
  const session = ctx.session;
948
1012
  if (allowedKeys) {
@@ -1239,7 +1303,10 @@ function buildInputSchema(op, fieldRules, opts) {
1239
1303
  mode: "list",
1240
1304
  ...opts
1241
1305
  });
1242
- case "get": return getIdShape();
1306
+ case "get": return {
1307
+ ...getIdShape(),
1308
+ select: PROJECTION_FIELD
1309
+ };
1243
1310
  case "create":
1244
1311
  if (!fieldRules && opts.adapterBodies?.createBody) {
1245
1312
  const shape = jsonSchemaToZodShape(opts.adapterBodies.createBody, "create");
@@ -1424,14 +1491,17 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1424
1491
  const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope);
1425
1492
  if (typeof route.handler === "function") {
1426
1493
  const fn = route.handler;
1427
- if (pipelineSteps.length > 0) return toCallToolResult(await executePipeline(pipelineSteps, {
1428
- ...reqCtx,
1429
- resource: resourceName,
1430
- operation: operationName
1431
- }, async (ctx) => {
1432
- const raw = await fn(ctx);
1433
- return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
1434
- }, operationName));
1494
+ if (pipelineSteps.length > 0) {
1495
+ const pipeCtx = {
1496
+ ...reqCtx,
1497
+ resource: resourceName,
1498
+ operation: operationName
1499
+ };
1500
+ return toCallToolResult(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
1501
+ const raw = await fn(ctx);
1502
+ return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
1503
+ }, operationName));
1504
+ }
1435
1505
  const out = await fn(reqCtx);
1436
1506
  return toCallToolResult(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
1437
1507
  }
@@ -1452,13 +1522,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1452
1522
  };
1453
1523
  return toCallToolResult(await method(reqCtx));
1454
1524
  } catch (err) {
1455
- return {
1456
- content: [{
1457
- type: "text",
1458
- text: `Error: ${err instanceof Error ? err.message : String(err)}`
1459
- }],
1460
- isError: true
1461
- };
1525
+ return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1462
1526
  }
1463
1527
  };
1464
1528
  }
@@ -1472,13 +1536,7 @@ function createMcpHandlerPassthrough(mcpHandler) {
1472
1536
  try {
1473
1537
  return await mcpHandler(input);
1474
1538
  } catch (err) {
1475
- return {
1476
- content: [{
1477
- type: "text",
1478
- text: `Error: ${err instanceof Error ? err.message : String(err)}`
1479
- }],
1480
- isError: true
1481
- };
1539
+ return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1482
1540
  }
1483
1541
  };
1484
1542
  }
@@ -1655,14 +1713,18 @@ function resourceToTools(resource, config = {}) {
1655
1713
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
1656
1714
  const repoForAgg = resource.controller?.repository;
1657
1715
  const buildOptionsFromSession = (session) => {
1658
- const s = session;
1716
+ const auth = session ?? null;
1717
+ const scope = buildScope(auth);
1659
1718
  const out = {};
1660
- const orgId = s?.scope?.organizationId;
1719
+ const orgId = getOrgId(scope);
1661
1720
  if (orgId) out.organizationId = orgId;
1662
- const userId = s?.scope?.userId ?? s?.user?.id;
1721
+ const userId = getUserId(scope);
1663
1722
  if (userId) out.userId = userId;
1664
- if (s?.user) out.user = s.user;
1665
- if (s?.requestId) out.requestId = s.requestId;
1723
+ if (auth?.userId) out.user = {
1724
+ id: auth.userId,
1725
+ roles: auth.roles ?? [],
1726
+ orgRoles: auth.orgRoles ?? []
1727
+ };
1666
1728
  return out;
1667
1729
  };
1668
1730
  tools.push(...buildAggregationTools({
@@ -1713,13 +1775,29 @@ function registerSchemaResources(server, resources, overrides) {
1713
1775
  name: r.name,
1714
1776
  displayName: r.displayName,
1715
1777
  operations: getOps(r, overrides?.[r.name]?.operations),
1716
- fields: r.schemaOptions?.fieldRules ?? {},
1778
+ fields: visibleFieldRules(r.schemaOptions?.fieldRules),
1717
1779
  filterableFields: schemaOpts?.filterableFields ?? [],
1718
1780
  presets: r._appliedPresets ?? []
1719
1781
  }, null, 2)
1720
1782
  }] }));
1721
1783
  }
1722
1784
  }
1785
+ /**
1786
+ * Strip `hidden: true` / `systemManaged: true` entries before serializing —
1787
+ * the schema-discovery resource must not re-expose exactly what
1788
+ * `fieldRulesToZod` strips from tool input schemas (tenant keys, internal
1789
+ * scoring fields, ...). Same visibility rule, one discovery surface.
1790
+ */
1791
+ function visibleFieldRules(fieldRules) {
1792
+ if (!fieldRules) return {};
1793
+ const out = {};
1794
+ for (const [name, rule] of Object.entries(fieldRules)) {
1795
+ const flags = rule ?? {};
1796
+ if (flags.hidden === true || flags.systemManaged === true) continue;
1797
+ out[name] = rule;
1798
+ }
1799
+ return out;
1800
+ }
1723
1801
  function getOps(r, override) {
1724
1802
  let ops = [
1725
1803
  "list",
@@ -1901,10 +1979,16 @@ const mcpPluginImpl = async (fastify, options) => {
1901
1979
  const more = allTools.length > 5 ? ` (+${allTools.length - 5} more)` : "";
1902
1980
  fastify.log.warn(`[arc/mcp] MCP server registered at "${prefix}" with auth DISABLED. Anyone who can reach this URL has full access to ${allTools.length} tools across ${enabledResources.length} resources: ${toolList}${more}. Pass \`auth: betterAuthInstance\` or \`auth: async (headers) => ...\` to gate access. Safe only for stdio transports, local development, or explicitly-public read-only APIs.`);
1903
1981
  }
1904
- fastify.get(`${prefix}/health`, async (_request, reply) => {
1905
- reply.send({
1982
+ fastify.get(`${prefix}/health`, async (request, reply) => {
1983
+ const liveness = {
1906
1984
  status: "ok",
1907
- mode: stateful ? "stateful" : "stateless",
1985
+ mode: stateful ? "stateful" : "stateless"
1986
+ };
1987
+ if (options.auth) {
1988
+ if (!await resolveMcpAuth(request.headers, options.auth)) return reply.send(liveness);
1989
+ }
1990
+ reply.send({
1991
+ ...liveness,
1908
1992
  tools: allTools.length,
1909
1993
  resources: enabledResources.length,
1910
1994
  toolNames: allTools.map((t) => t.name),
@@ -1991,9 +2075,22 @@ function registerStatelessRoutes(fastify, prefix, options, createServer, Transpo
1991
2075
  });
1992
2076
  }
1993
2077
  function registerStatefulRoutes(fastify, prefix, options, cache, createServer, Transport) {
1994
- /** Check if the requesting principal owns the session */
2078
+ /**
2079
+ * Check if the requesting principal owns the session. FAIL-CLOSED:
2080
+ *
2081
+ * - `authResult === null` (failed/absent credentials) NEVER owns a
2082
+ * session — pre-2.20 this returned `true`, so an unauthenticated
2083
+ * caller with a leaked `Mcp-Session-Id` could attach to the victim's
2084
+ * SSE stream, terminate the session, and (via the auth-snapshot
2085
+ * refresh) wipe `entry.auth` to null — after which ANY principal
2086
+ * passed ownership and took over the session.
2087
+ * - `entry.auth === null` under configured auth is equally a deny:
2088
+ * POST 401s before creating a session, so an auth-mode session always
2089
+ * has an owner snapshot; a missing one means corrupted state.
2090
+ */
1995
2091
  function isSessionOwner(entry, authResult) {
1996
- if (!options.auth || !entry.auth || !authResult) return true;
2092
+ if (!options.auth) return true;
2093
+ if (!authResult || !entry.auth) return false;
1997
2094
  const prev = entry.auth;
1998
2095
  return prev.userId === authResult.userId && prev.organizationId === authResult.organizationId && prev.clientId === authResult.clientId;
1999
2096
  }
@@ -2053,6 +2150,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2053
2150
  if (!entry) return reply.code(403).send({ error: "Unauthorized" });
2054
2151
  if (options.auth) {
2055
2152
  const authResult = await resolveMcpAuth(request.headers, options.auth);
2153
+ if (!authResult) return reply.code(401).send({ error: "Unauthorized" });
2056
2154
  if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
2057
2155
  entry.auth = authResult;
2058
2156
  entry.authRef.current = authResult;
@@ -2067,6 +2165,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2067
2165
  if (!entry) return reply.code(204).send();
2068
2166
  if (options.auth) {
2069
2167
  const authResult = await resolveMcpAuth(request.headers, options.auth);
2168
+ if (!authResult) return reply.code(401).send({ error: "Unauthorized" });
2070
2169
  if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
2071
2170
  entry.auth = authResult;
2072
2171
  entry.authRef.current = authResult;
@@ -1,4 +1,4 @@
1
- import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-CHNQIhcb.mjs";
1
+ import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-_--GyzcO.mjs";
2
2
  import { RouteHandlerMethod } from "fastify";
3
3
 
4
4
  //#region src/middleware/middleware.d.ts
@@ -1,4 +1,4 @@
1
- import { t as CRUD_OPERATIONS } from "../constants-gaEeMlz-.mjs";
1
+ import { t as CRUD_OPERATIONS } from "../constants-DNre0wRg.mjs";
2
2
  import { t as multipartBody } from "../multipartBody-CHEQSZ6G.mjs";
3
3
  //#region src/middleware/middleware.ts
4
4
  /**