@classytic/arc 2.22.0 → 2.25.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 (147) hide show
  1. package/dist/{BaseController-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  6. package/dist/audit/index.d.mts +32 -3
  7. package/dist/audit/index.mjs +7 -4
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +264 -260
  10. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  11. package/dist/cache/index.d.mts +41 -3
  12. package/dist/cache/index.mjs +33 -4
  13. package/dist/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/describe.mjs +1 -1
  15. package/dist/cli/commands/docs.mjs +2 -2
  16. package/dist/cli/commands/generate.mjs +1 -1
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.mjs +1 -1
  19. package/dist/cli/index.mjs +2 -2
  20. package/dist/context/index.mjs +2 -64
  21. package/dist/core/index.d.mts +3 -3
  22. package/dist/core/index.mjs +4 -4
  23. package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
  24. package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
  25. package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  29. package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
  30. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  31. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  32. package/dist/events/index.d.mts +57 -314
  33. package/dist/events/index.mjs +168 -93
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  36. package/dist/factory/index.d.mts +21 -6
  37. package/dist/factory/index.mjs +58 -18
  38. package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  39. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  40. package/dist/headers-BvlQSDMB.mjs +45 -0
  41. package/dist/health-CB_m4S32.mjs +377 -0
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/idempotency/index.d.mts +7 -0
  44. package/dist/idempotency/index.mjs +32 -20
  45. package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
  48. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  49. package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +6 -5
  52. package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
  53. package/dist/integrations/event-gateway.d.mts +1 -1
  54. package/dist/integrations/event-gateway.mjs +1 -1
  55. package/dist/integrations/index.d.mts +3 -3
  56. package/dist/integrations/jobs/index.d.mts +2 -0
  57. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  58. package/dist/integrations/mcp/index.d.mts +68 -5
  59. package/dist/integrations/mcp/index.mjs +2 -2
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/webhooks.d.mts +149 -22
  63. package/dist/integrations/webhooks.mjs +186 -42
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +41 -14
  67. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  68. package/dist/interface-n5hpKg1d.mjs +16 -0
  69. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  70. package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
  71. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  72. package/dist/middleware/index.d.mts +17 -1
  73. package/dist/middleware/index.mjs +1 -1
  74. package/dist/migrations/index.d.mts +94 -44
  75. package/dist/migrations/index.mjs +196 -44
  76. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  77. package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
  78. package/dist/permissions/index.d.mts +3 -3
  79. package/dist/permissions/index.mjs +4 -3
  80. package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
  81. package/dist/pipeline/index.d.mts +1 -1
  82. package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
  83. package/dist/plugins/index.d.mts +7 -78
  84. package/dist/plugins/index.mjs +14 -97
  85. package/dist/plugins/response-cache.mjs +1 -0
  86. package/dist/plugins/tracing-entry.mjs +1 -1
  87. package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
  88. package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
  89. package/dist/presets/filesUpload.d.mts +3 -3
  90. package/dist/presets/filesUpload.mjs +3 -3
  91. package/dist/presets/index.d.mts +2 -1
  92. package/dist/presets/index.mjs +1 -1
  93. package/dist/presets/multiTenant.d.mts +1 -1
  94. package/dist/presets/multiTenant.mjs +1 -1
  95. package/dist/presets/search.d.mts +2 -2
  96. package/dist/presets/search.mjs +1 -1
  97. package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
  98. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  99. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  100. package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  101. package/dist/realtime-Cuw_deA-.mjs +375 -0
  102. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  103. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  104. package/dist/registry/index.d.mts +2 -2
  105. package/dist/registry/index.mjs +1 -1
  106. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  107. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  108. package/dist/schedules-CjOd2MG9.mjs +113 -0
  109. package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
  110. package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +67 -67
  112. package/dist/schemas/index.mjs +2 -3
  113. package/dist/scim/index.mjs +1 -2
  114. package/dist/scope/index.d.mts +2 -2
  115. package/dist/scope/index.mjs +4 -4
  116. package/dist/scope-DfaPntpo.mjs +36 -0
  117. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  118. package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
  119. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  120. package/dist/testing/index.d.mts +4 -3
  121. package/dist/testing/index.mjs +2 -2
  122. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  123. package/dist/types/index.d.mts +4 -4
  124. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  125. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  126. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  127. package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
  128. package/dist/usage/index.d.mts +3 -52
  129. package/dist/usage/index.mjs +0 -0
  130. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  131. package/dist/utils/index.d.mts +90 -57
  132. package/dist/utils/index.mjs +7 -3
  133. package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
  134. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  135. package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
  136. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  137. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  138. package/package.json +526 -453
  139. package/skills/arc/references/api-reference.md +3 -1
  140. package/skills/arc/references/integrations.md +20 -11
  141. package/skills/arc/references/production.md +734 -696
  142. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  143. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  144. package/dist/health-ScyjIVo5.mjs +0 -202
  145. package/dist/requestId-5BhTVzBv.mjs +0 -113
  146. package/dist/scope-k15RXfcq.mjs +0 -64
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,15 +1,18 @@
1
1
  import { p as isArcError, t as ArcError } from "./errors-DkO8Nk6-.mjs";
2
- import { c as getOrgId, g as getUserId } from "./types-bHblZ9YN.mjs";
3
- import { t as BaseController } from "./BaseController-C6FlRWxF.mjs";
4
- import { P as normalizePermissionResult } from "./permissions-BJMuKMl7.mjs";
2
+ import { n as requireSingleHeaderValue } from "./headers-BvlQSDMB.mjs";
3
+ import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-CSyCxl2V.mjs";
4
+ import { t as BaseController } from "./BaseController--M8EMzt0.mjs";
5
+ import { i as applyFieldReadPermissions, r as normalizePermissionResult, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
5
6
  import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
6
- import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-ze70xXJi.mjs";
7
- import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
8
- import { n as executeAggregation } from "./buildHandler-D_2ULWxZ.mjs";
7
+ import { r as resolvePipelineSteps, u as buildRequestScopeProjection } from "./pipeline-BN5G05rd.mjs";
8
+ import { n as validateAggregations } from "./validate-Bk44F2yT.mjs";
9
+ import { n as executeAggregation } from "./buildHandler-Dtwd8fdz.mjs";
9
10
  import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
10
- import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-B-6SPe7g.mjs";
11
- import { createHash, randomUUID } from "node:crypto";
11
+ import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
12
+ import { t as createIdempotencyResult } from "./interface-n5hpKg1d.mjs";
13
+ import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-Ca7ySNPx.mjs";
12
14
  import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
15
+ import { createHash, randomUUID } from "node:crypto";
13
16
  import fp from "fastify-plugin";
14
17
  import { z } from "zod";
15
18
  //#region src/integrations/mcp/createMcpServer.ts
@@ -160,6 +163,13 @@ function registerPrompt(server, prompt) {
160
163
  //#endregion
161
164
  //#region src/integrations/mcp/buildRequestContext.ts
162
165
  /**
166
+ * @classytic/arc — MCP → IRequestContext Bridge
167
+ *
168
+ * Builds an IRequestContext from MCP tool input + session identity.
169
+ * Ensures MCP tool handlers go through the same BaseController pipeline
170
+ * as REST requests (AccessControl, BodySanitizer, QueryResolver, HookSystem).
171
+ */
172
+ /**
163
173
  * Build an IRequestContext from MCP tool input and session auth.
164
174
  *
165
175
  * | Operation | params | query | body |
@@ -177,7 +187,7 @@ function registerPrompt(server, prompt) {
177
187
  * the session-derived scope is `public` (i.e. MCP called with `auth: false`).
178
188
  * An authenticated session scope is never overwritten.
179
189
  */
180
- function buildRequestContext(input, auth, operation, policyFilters, scopeOverride) {
190
+ function buildRequestContext(input, auth, operation, policyFilters, scopeOverride, extras) {
181
191
  const sessionScope = buildScope(auth);
182
192
  const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
183
193
  const base = {
@@ -188,10 +198,13 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
188
198
  } : null,
189
199
  headers: {},
190
200
  context: {},
201
+ scope: buildRequestScopeProjection(scope),
191
202
  metadata: {
192
203
  _scope: scope,
193
- _policyFilters: policyFilters ?? {}
194
- }
204
+ _policyFilters: policyFilters ?? {},
205
+ ...extras?.arc ? { arc: extras.arc } : {}
206
+ },
207
+ ...extras?.server ? { server: extras.server } : {}
195
208
  };
196
209
  switch (operation) {
197
210
  case "list": return {
@@ -375,6 +388,42 @@ async function evaluatePermission(check, session, resource, action, input) {
375
388
  }));
376
389
  }
377
390
  /**
391
+ * Apply field-level READ permissions to an MCP tool payload — the same
392
+ * masking `sendControllerResponse` applies on the HTTP wire and the
393
+ * realtime plugin applies per frame. Without this, a resource declaring
394
+ * `fields: { salary: fields.visibleTo(['admin']) }` masked salary over
395
+ * REST and SSE but leaked it verbatim through every MCP tool.
396
+ *
397
+ * Mirrors the HTTP adapter's semantics exactly:
398
+ * - Elevated scope (platform admin) bypasses masking — consistent with
399
+ * `requireOrgRole()` and `BodySanitizer` bypass logic.
400
+ * - Effective roles = session roles ∪ org roles (member scope only),
401
+ * via the shared `resolveEffectiveRoles`.
402
+ * - `scopeOverride` (a `PermissionResult.scope`) follows the same
403
+ * non-downgrade rule as `buildRequestContext`: honored only when the
404
+ * session-derived scope is `public`.
405
+ *
406
+ * Handles the three payload shapes MCP tools emit: a single record, an
407
+ * array of records, and a paginated result object carrying an inner
408
+ * `data: T[]`. Scalars and `null` pass through untouched.
409
+ */
410
+ function applyMcpReadMasking(data, options) {
411
+ const { fields, session, scopeOverride } = options;
412
+ if (!fields || data === null || typeof data !== "object") return data;
413
+ const sessionScope = buildScope(session);
414
+ const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
415
+ if (isElevated(scope)) return data;
416
+ const roles = resolveEffectiveRoles(session?.roles ?? [], isMember(scope) ? scope.orgRoles : []);
417
+ const maskItem = (item) => item !== null && typeof item === "object" && !Array.isArray(item) ? applyFieldReadPermissions(item, fields, roles) : item;
418
+ if (Array.isArray(data)) return data.map(maskItem);
419
+ const inner = data.data;
420
+ if (Array.isArray(inner)) return {
421
+ ...data,
422
+ data: inner.map(maskItem)
423
+ };
424
+ return maskItem(data);
425
+ }
426
+ /**
378
427
  * Convert a controller response envelope into an MCP `CallToolResult`.
379
428
  * Carries `meta` into the serialized payload so consumers see pagination
380
429
  * totals, stripped-field arrays, etc.
@@ -476,6 +525,78 @@ function createMcpController(resource) {
476
525
  //#endregion
477
526
  //#region src/integrations/mcp/invokeController.ts
478
527
  /**
528
+ * @classytic/arc — Non-HTTP controller invocation for MCP / jobs / workflows.
529
+ *
530
+ * Internal callers (MCP tools, scheduled jobs, workflow steps) need to invoke
531
+ * controller methods without going through Fastify's preHandler chain.
532
+ * Before this module landed, every call site reinvented the same shim:
533
+ * fabricate `params` / `body` / `headers`, project the session into a
534
+ * `RequestScope`, run the permission check by hand, translate thrown errors
535
+ * to the MCP `CallToolResult` shape. The drift across those copies is what
536
+ * the mentora review flagged as the "buildCtx synthetic-context hack."
537
+ *
538
+ * Two public surfaces, layered so callers reach for the smallest piece that
539
+ * does the job:
540
+ *
541
+ * - {@link invokeController} full pipeline: permission → context →
542
+ * dispatch → canonical envelope/error
543
+ * conversion. Returns `CallToolResult`.
544
+ * - {@link mcpHandlerAdapter} thin try/catch wrapper that turns any
545
+ * async function into a `ToolDefinition`
546
+ * handler with canonical error shape. Use
547
+ * when you want to compose multiple
548
+ * controller calls or weave in custom logic
549
+ * between them.
550
+ *
551
+ * Both honor the same {@link buildRequestContext} primitive — re-exported
552
+ * from this module so authors who want the synthetic context as data (e.g.
553
+ * to pass into `executePipeline`) can grab it without depending on an
554
+ * internal subpath.
555
+ */
556
+ /**
557
+ * Assemble the {@link McpContextExtras} for a controller invocation from
558
+ * resource-scoped metadata + app-level wiring. Mirrors what
559
+ * `buildArcDecorator` (metadata.arc) and `createRequestContext` (server
560
+ * accessor) assemble on the HTTP path.
561
+ */
562
+ function buildContextExtras(options) {
563
+ const { wiring, resourceName, permissions, fields, schemaOptions, idField } = options;
564
+ if (!wiring) return void 0;
565
+ return {
566
+ arc: {
567
+ resourceName,
568
+ schemaOptions,
569
+ permissions,
570
+ hooks: wiring.hooks,
571
+ events: wiring.events,
572
+ fields,
573
+ idField
574
+ },
575
+ server: {
576
+ events: wiring.events,
577
+ audit: wiring.audit,
578
+ log: wiring.log
579
+ }
580
+ };
581
+ }
582
+ /**
583
+ * Stable fingerprint of a tool invocation — sorted-key JSON, hashed.
584
+ * Scopes an idempotency key to (resource, action, input, caller) so a
585
+ * reused key with DIFFERENT input is a fresh execution, matching the HTTP
586
+ * plugin's method+url+bodyHash+user fingerprint semantics.
587
+ */
588
+ function invocationFingerprint(resourceName, actionName, input, session) {
589
+ const sortKeys = (value) => {
590
+ if (Array.isArray(value)) return value.map(sortKeys);
591
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
592
+ return value;
593
+ };
594
+ return `mcp:${resourceName}:${actionName}:${createHash("sha256").update(JSON.stringify(sortKeys(input))).digest("hex").substring(0, 16)}:u=${session?.userId ?? session?.clientId ?? "anon"}`;
595
+ }
596
+ /** TTLs matching the HTTP idempotency plugin's defaults. */
597
+ const MCP_IDEMPOTENCY_TTL_MS = 1440 * 60 * 1e3;
598
+ const MCP_IDEMPOTENCY_LOCK_MS = 1e4;
599
+ /**
479
600
  * Invoke a controller method through the MCP synthetic-context path.
480
601
  *
481
602
  * Runs the same chain CRUD MCP tools run:
@@ -508,7 +629,7 @@ function createMcpController(resource) {
508
629
  * checks compose.
509
630
  */
510
631
  async function invokeController(controller, op, input, options) {
511
- const { session, resourceName, actionName = op, permissions, methodName = op } = options;
632
+ const { session, resourceName, actionName = op, permissions, methodName = op, fields, wiring, idempotencyKey } = options;
512
633
  const ctrl = controller;
513
634
  try {
514
635
  const method = ctrl[methodName];
@@ -524,7 +645,39 @@ async function invokeController(controller, op, input, options) {
524
645
  reason: permResult.reason,
525
646
  session
526
647
  });
527
- return toCallToolResult(await method(buildRequestContext(input, session, op, permResult?.filters, permResult?.scope)));
648
+ const dispatch = async () => {
649
+ const reqCtx = buildRequestContext(input, session, op, permResult?.filters, permResult?.scope, buildContextExtras(options));
650
+ const response = await method(reqCtx);
651
+ const data = applyMcpReadMasking(response.data, {
652
+ fields,
653
+ session,
654
+ scopeOverride: permResult?.scope
655
+ });
656
+ return toCallToolResult(data === response.data ? response : {
657
+ ...response,
658
+ data
659
+ });
660
+ };
661
+ const store = wiring?.idempotencyStore;
662
+ if (idempotencyKey && store) {
663
+ const fullKey = `${idempotencyKey}:${invocationFingerprint(resourceName, actionName, input, session)}`;
664
+ const cached = await store.get(fullKey);
665
+ if (cached) return cached.body;
666
+ const lockId = randomUUID();
667
+ if (!await store.tryLock(fullKey, lockId, MCP_IDEMPOTENCY_LOCK_MS)) return toCallToolError({
668
+ code: "arc.idempotency_conflict",
669
+ message: "A call with this idempotency key is already in progress",
670
+ status: 409
671
+ });
672
+ try {
673
+ const result = await dispatch();
674
+ if (!result.isError) await store.set(fullKey, createIdempotencyResult(200, result, {}, MCP_IDEMPOTENCY_TTL_MS));
675
+ return result;
676
+ } finally {
677
+ await store.unlock(fullKey, lockId);
678
+ }
679
+ }
680
+ return await dispatch();
528
681
  } catch (err) {
529
682
  return toCallToolError(err instanceof Error ? err : new Error(String(err)));
530
683
  }
@@ -605,13 +758,33 @@ const CRUD_ANNOTATIONS = {
605
758
  * build + envelope/error conversion all live in one place — this factory
606
759
  * only resolves the op-specific permission and threads it through.
607
760
  */
608
- function createCrudHandler(op, controller, resourceName, permissions) {
761
+ /** Ops where an idempotency key is meaningful (mutations). */
762
+ const MUTATING_CRUD_OPS = /* @__PURE__ */ new Set([
763
+ "create",
764
+ "update",
765
+ "delete"
766
+ ]);
767
+ function createCrudHandler(op, controller, resourceName, permissions, fields, extras) {
609
768
  const permission = permissions?.[op];
610
- return (input, ctx) => invokeController(controller, op, input, {
611
- session: ctx.session,
612
- resourceName,
613
- permissions: permission
614
- });
769
+ return (input, ctx) => {
770
+ let idempotencyKey;
771
+ let payload = input;
772
+ if ("_idempotencyKey" in input) {
773
+ const { _idempotencyKey, ...rest } = input;
774
+ payload = rest;
775
+ if (MUTATING_CRUD_OPS.has(op) && typeof _idempotencyKey === "string" && _idempotencyKey) idempotencyKey = _idempotencyKey;
776
+ }
777
+ return invokeController(controller, op, payload, {
778
+ session: ctx.session,
779
+ resourceName,
780
+ permissions: permission,
781
+ fields,
782
+ wiring: extras?.wiring,
783
+ schemaOptions: extras?.schemaOptions,
784
+ idField: extras?.idField,
785
+ idempotencyKey
786
+ });
787
+ };
615
788
  }
616
789
  /**
617
790
  * Default description for a CRUD tool. Enriches list descriptions with the
@@ -1008,7 +1181,24 @@ function convertActionSchemaToZod(raw) {
1008
1181
  * in the 2.10.8 review: REST and MCP action tools share identical
1009
1182
  * context-building machinery.
1010
1183
  */
1011
- function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false) {
1184
+ function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false, fields, extras) {
1185
+ const wiring = extras?.wiring;
1186
+ const contextExtras = wiring ? {
1187
+ arc: {
1188
+ resourceName,
1189
+ schemaOptions: extras?.schemaOptions,
1190
+ permissions,
1191
+ hooks: wiring.hooks,
1192
+ events: wiring.events,
1193
+ fields,
1194
+ idField: extras?.idField
1195
+ },
1196
+ server: {
1197
+ events: wiring.events,
1198
+ audit: wiring.audit,
1199
+ log: wiring.log
1200
+ }
1201
+ } : void 0;
1012
1202
  const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
1013
1203
  const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? /* @__PURE__ */ new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
1014
1204
  return async (input, ctx) => {
@@ -1036,11 +1226,15 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
1036
1226
  const reqCtx = buildRequestContext({
1037
1227
  ...input,
1038
1228
  action: actionName
1039
- }, session, "action", permResult?.filters, permResult?.scope);
1229
+ }, session, "action", permResult?.filters, permResult?.scope, contextExtras);
1040
1230
  const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
1041
1231
  const { id: _discardId, ...data } = input;
1042
1232
  try {
1043
- return toCallToolSuccess(await handler(id, data, reqCtx));
1233
+ return toCallToolSuccess(applyMcpReadMasking(await handler(id, data, reqCtx), {
1234
+ fields,
1235
+ session,
1236
+ scopeOverride: permResult?.scope
1237
+ }));
1044
1238
  } catch (err) {
1045
1239
  return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1046
1240
  }
@@ -1055,7 +1249,7 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
1055
1249
  * every aggregation opts out via `mcp: false`.
1056
1250
  */
1057
1251
  function buildAggregationTools(args) {
1058
- const { resourceName, displayName, aggregations, schemaOptions, repo, buildOptionsFromSession, prefix } = args;
1252
+ const { resourceName, displayName, aggregations, schemaOptions, repo, buildOptionsFromSession, prefix, fields } = args;
1059
1253
  if (!aggregations || Object.keys(aggregations).length === 0) return [];
1060
1254
  const normalized = validateAggregations(resourceName, aggregations, schemaOptions);
1061
1255
  const tools = [];
@@ -1079,7 +1273,8 @@ function buildAggregationTools(args) {
1079
1273
  permissions: config.permissions,
1080
1274
  resourceName,
1081
1275
  repo,
1082
- buildOptionsFromSession
1276
+ buildOptionsFromSession,
1277
+ fields
1083
1278
  })
1084
1279
  });
1085
1280
  }
@@ -1121,7 +1316,7 @@ function buildAggregationInputSchema(norm) {
1121
1316
  return shape;
1122
1317
  }
1123
1318
  function createAggregationToolHandler(args) {
1124
- const { normalized, permissions, resourceName, repo, buildOptionsFromSession } = args;
1319
+ const { normalized, permissions, resourceName, repo, buildOptionsFromSession, fields } = args;
1125
1320
  return async (input, ctx) => {
1126
1321
  const permResult = await evaluatePermission(permissions, ctx.session, resourceName, `aggregation:${normalized.name}`, input);
1127
1322
  if (permResult && !permResult.granted) return permissionDeniedResult({
@@ -1140,7 +1335,21 @@ function createAggregationToolHandler(args) {
1140
1335
  query: filterInput,
1141
1336
  tenantOptions
1142
1337
  });
1143
- if (result.status === 200) return toCallToolSuccess(result.body);
1338
+ if (result.status === 200) {
1339
+ const body = result.body;
1340
+ if (fields && Array.isArray(body?.rows)) {
1341
+ const rows = applyMcpReadMasking(body.rows, {
1342
+ fields,
1343
+ session: ctx.session,
1344
+ scopeOverride: permResult?.scope
1345
+ });
1346
+ return toCallToolSuccess({
1347
+ ...body,
1348
+ rows
1349
+ });
1350
+ }
1351
+ return toCallToolSuccess(result.body);
1352
+ }
1144
1353
  return toCallToolError(result.body);
1145
1354
  };
1146
1355
  }
@@ -1480,8 +1689,24 @@ function operationKindForRoute(method, hasId) {
1480
1689
  function createCustomRouteHandler(route, controller, hasId, options) {
1481
1690
  const ctrl = controller;
1482
1691
  const handlerName = typeof route.handler === "string" ? route.handler : route.operation ?? slugifyRoute(route.method, route.path);
1483
- const { resourceName, operationName, permissions, pipeline } = options;
1692
+ const { resourceName, operationName, permissions, pipeline, fields, wiring } = options;
1484
1693
  const pipelineSteps = resolvePipelineSteps(pipeline, operationName);
1694
+ const contextExtras = wiring ? {
1695
+ arc: {
1696
+ resourceName,
1697
+ schemaOptions: options.schemaOptions,
1698
+ permissions,
1699
+ hooks: wiring.hooks,
1700
+ events: wiring.events,
1701
+ fields,
1702
+ idField: options.idField
1703
+ },
1704
+ server: {
1705
+ events: wiring.events,
1706
+ audit: wiring.audit,
1707
+ log: wiring.log
1708
+ }
1709
+ } : void 0;
1485
1710
  return async (input, _ctx) => {
1486
1711
  const session = _ctx.session;
1487
1712
  const permResult = await evaluatePermission(permissions, session, resourceName, operationName, input);
@@ -1492,7 +1717,18 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1492
1717
  session
1493
1718
  });
1494
1719
  try {
1495
- const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope);
1720
+ const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope, contextExtras);
1721
+ const emit = (envelope) => {
1722
+ const data = applyMcpReadMasking(envelope.data, {
1723
+ fields,
1724
+ session,
1725
+ scopeOverride: permResult?.scope
1726
+ });
1727
+ return toCallToolResult(data === envelope.data ? envelope : {
1728
+ ...envelope,
1729
+ data
1730
+ });
1731
+ };
1496
1732
  if (typeof route.handler === "function") {
1497
1733
  const fn = route.handler;
1498
1734
  if (pipelineSteps.length > 0) {
@@ -1501,13 +1737,13 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1501
1737
  resource: resourceName,
1502
1738
  operation: operationName
1503
1739
  };
1504
- return toCallToolResult(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
1740
+ return emit(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
1505
1741
  const raw = await fn(ctx);
1506
1742
  return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
1507
1743
  }, operationName));
1508
1744
  }
1509
1745
  const out = await fn(reqCtx);
1510
- return toCallToolResult(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
1746
+ return emit(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
1511
1747
  }
1512
1748
  if (!ctrl) return {
1513
1749
  content: [{
@@ -1524,7 +1760,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1524
1760
  }],
1525
1761
  isError: true
1526
1762
  };
1527
- return toCallToolResult(await method(reqCtx));
1763
+ return emit(await method(reqCtx));
1528
1764
  } catch (err) {
1529
1765
  return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1530
1766
  }
@@ -1598,6 +1834,7 @@ function resourceToTools(resource, config = {}) {
1598
1834
  const hasSoftDelete = resource._appliedPresets?.includes("softDelete") ?? false;
1599
1835
  const tools = [];
1600
1836
  const prefix = config.toolNamePrefix;
1837
+ const wiring = config.wiring;
1601
1838
  if (controller) {
1602
1839
  let ops = ALL_CRUD_OPS.filter((op) => !resource.disabledRoutes?.includes(op));
1603
1840
  if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
@@ -1608,6 +1845,15 @@ function resourceToTools(resource, config = {}) {
1608
1845
  allowedOperators,
1609
1846
  sortableFields
1610
1847
  });
1848
+ const inputSchema = buildInputSchema(op, fieldRules, {
1849
+ hiddenFields,
1850
+ readonlyFields,
1851
+ extraHideFields: config.hideFields,
1852
+ filterableFields,
1853
+ allowedOperators,
1854
+ adapterBodies
1855
+ });
1856
+ if (wiring?.idempotencyStore && op !== "list" && op !== "get") inputSchema._idempotencyKey = z.string().optional().describe("Optional idempotency key — retrying with the same key and input replays the first successful result instead of re-executing.");
1611
1857
  tools.push({
1612
1858
  name,
1613
1859
  description: resolveCrudDescription(config.descriptions?.[op], {
@@ -1620,15 +1866,12 @@ function resourceToTools(resource, config = {}) {
1620
1866
  sortableFields
1621
1867
  }),
1622
1868
  annotations: CRUD_ANNOTATIONS[op],
1623
- inputSchema: buildInputSchema(op, fieldRules, {
1624
- hiddenFields,
1625
- readonlyFields,
1626
- extraHideFields: config.hideFields,
1627
- filterableFields,
1628
- allowedOperators,
1629
- adapterBodies
1869
+ inputSchema,
1870
+ handler: createCrudHandler(op, controller, resource.name, resource.permissions, resource.fields, {
1871
+ wiring,
1872
+ schemaOptions: resource.schemaOptions,
1873
+ idField: resource.idField
1630
1874
  }),
1631
- handler: createCrudHandler(op, controller, resource.name, resource.permissions),
1632
1875
  source: `crud:${resource.name}:${op}`
1633
1876
  });
1634
1877
  }
@@ -1680,7 +1923,11 @@ function resourceToTools(resource, config = {}) {
1680
1923
  resourceName: resource.name,
1681
1924
  operationName: opName,
1682
1925
  permissions: route.permissions,
1683
- pipeline: resource.pipe
1926
+ pipeline: resource.pipe,
1927
+ fields: resource.fields,
1928
+ wiring,
1929
+ schemaOptions: resource.schemaOptions,
1930
+ idField: resource.idField
1684
1931
  })
1685
1932
  });
1686
1933
  }
@@ -1711,7 +1958,11 @@ function resourceToTools(resource, config = {}) {
1711
1958
  annotations,
1712
1959
  inputSchema: inputShape,
1713
1960
  source: `action:${resource.name}:${actionName}`,
1714
- handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess)
1961
+ handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
1962
+ wiring,
1963
+ schemaOptions: resource.schemaOptions,
1964
+ idField: resource.idField
1965
+ })
1715
1966
  });
1716
1967
  }
1717
1968
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
@@ -1738,7 +1989,8 @@ function resourceToTools(resource, config = {}) {
1738
1989
  schemaOptions: resource.schemaOptions,
1739
1990
  repo: repoForAgg,
1740
1991
  buildOptionsFromSession,
1741
- prefix
1992
+ prefix,
1993
+ fields: resource.fields
1742
1994
  }));
1743
1995
  }
1744
1996
  return tools;
@@ -1948,10 +2200,29 @@ const mcpPluginImpl = async (fastify, options) => {
1948
2200
  include: options.include,
1949
2201
  exclude: options.exclude
1950
2202
  });
2203
+ const fastifyAny = fastify;
2204
+ const wiring = {
2205
+ get hooks() {
2206
+ return fastifyAny.arc?.hooks;
2207
+ },
2208
+ get events() {
2209
+ return fastifyAny.events;
2210
+ },
2211
+ get audit() {
2212
+ return fastifyAny.audit;
2213
+ },
2214
+ get log() {
2215
+ return fastify.log;
2216
+ },
2217
+ get idempotencyStore() {
2218
+ return fastifyAny.idempotency?.store;
2219
+ }
2220
+ };
1951
2221
  const overrides = options.overrides ?? {};
1952
2222
  const allTools = enabledResources.flatMap((r) => {
1953
2223
  const resOverrides = overrides[r.name] ?? {};
1954
2224
  return resourceToTools(r, {
2225
+ wiring,
1955
2226
  ...resOverrides,
1956
2227
  toolNamePrefix: resOverrides.toolNamePrefix ?? options.toolNamePrefix
1957
2228
  });
@@ -2113,7 +2384,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2113
2384
  }
2114
2385
  });
2115
2386
  }
2116
- const sessionId = request.headers["mcp-session-id"];
2387
+ const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
2117
2388
  if (sessionId) {
2118
2389
  const entry = cache.get(sessionId);
2119
2390
  if (entry) {
@@ -2148,7 +2419,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2148
2419
  await transport.handleRequest(request.raw, reply.raw, request.body);
2149
2420
  });
2150
2421
  fastify.get(prefix, async (request, reply) => {
2151
- const sessionId = request.headers["mcp-session-id"];
2422
+ const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
2152
2423
  if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
2153
2424
  const entry = cache.get(sessionId);
2154
2425
  if (!entry) return reply.code(403).send({ error: "Unauthorized" });
@@ -2163,7 +2434,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2163
2434
  await entry.transport.handleRequest(request.raw, reply.raw);
2164
2435
  });
2165
2436
  fastify.delete(prefix, async (request, reply) => {
2166
- const sessionId = request.headers["mcp-session-id"];
2437
+ const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
2167
2438
  if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
2168
2439
  const entry = cache.get(sessionId);
2169
2440
  if (!entry) return reply.code(204).send();
@@ -76,6 +76,35 @@ var MemoryCacheStore = class {
76
76
  const entry = this.cache.get(key);
77
77
  if (entry) this.removeEntry(key, entry);
78
78
  }
79
+ /**
80
+ * Atomic increment (canonical `CacheAdapter` signature: key, by, ttl) —
81
+ * synchronous on purpose: no `await` between read and write means
82
+ * concurrent bumps on the event loop can't interleave, which is the whole
83
+ * point of the `CacheStore.increment` contract.
84
+ *
85
+ * TTL semantics follow the repo-core contract: `ttlSeconds` applies only
86
+ * when the key is CREATED — an existing counter keeps its original expiry
87
+ * (Redis `EXPIRE ... NX` semantics), so every conforming adapter ages a
88
+ * counter identically.
89
+ */
90
+ increment(key, by = 1, ttlSeconds) {
91
+ const now = Date.now();
92
+ const entry = this.cache.get(key);
93
+ const live = entry && entry.expiresAt > now;
94
+ const next = (live && typeof entry.value === "number" ? entry.value : 0) + by;
95
+ const effectiveTtlSeconds = ttlSeconds ?? this.defaultTtlSeconds;
96
+ if (entry) this.currentBytes -= entry.size;
97
+ this.cache.delete(key);
98
+ const size = 8;
99
+ this.cache.set(key, {
100
+ value: next,
101
+ size,
102
+ expiresAt: live ? entry.expiresAt : now + Math.max(1, effectiveTtlSeconds) * 1e3
103
+ });
104
+ this.currentBytes += size;
105
+ this.evictToLimit();
106
+ return next;
107
+ }
79
108
  async clear(pattern) {
80
109
  if (pattern === void 0) {
81
110
  this.cache.clear();
@@ -1,4 +1,4 @@
1
- import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-DbtgCOCj.mjs";
1
+ import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-DPRH0Cme.mjs";
2
2
  import { RouteHandlerMethod } from "fastify";
3
3
  //#region src/middleware/middleware.d.ts
4
4
  interface NamedMiddleware {
@@ -90,6 +90,14 @@ interface MultipartBodyOptions {
90
90
  * @example ['avatar', 'cover'] // multi-field upload (profile editor)
91
91
  */
92
92
  requiredFields?: string[];
93
+ /**
94
+ * Cap on the TOTAL buffered file bytes per request (default: 25 MiB).
95
+ * Per-file and per-count limits bound individual files but not their
96
+ * SUM — without this, `maxFiles: 5` × `maxFileSize: 10 MiB` lets one
97
+ * request retain ~50 MiB of process memory, and concurrency multiplies
98
+ * it (20 such uploads ≈ 1 GiB). Exceeding the cap returns 413.
99
+ */
100
+ maxTotalBytes?: number;
93
101
  }
94
102
  /**
95
103
  * Create a multipart body parsing middleware.
@@ -102,6 +110,14 @@ interface MultipartBodyOptions {
102
110
  * For non-multipart requests (regular JSON), this is a no-op — the request
103
111
  * passes through unchanged. This makes it safe to add to create/update
104
112
  * middlewares without breaking JSON clients.
113
+ *
114
+ * **BUFFERED — for small uploads only.** Every accepted file is held fully
115
+ * in process memory for the request's lifetime; memory cost is
116
+ * `concurrency × maxTotalBytes`. Suitable for avatars, documents, and
117
+ * form attachments. For large files use PRESIGNED direct-to-object-storage
118
+ * uploads (see the files-upload preset / production guide) and pair upload
119
+ * routes with strict rate limits — buffering video through the app tier is
120
+ * the wrong architecture regardless of limits.
105
121
  */
106
122
  declare function multipartBody(options?: MultipartBodyOptions): RouteHandlerMethod;
107
123
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { t as CRUD_OPERATIONS } from "../constants-DNre0wRg.mjs";
2
- import { t as multipartBody } from "../multipartBody-CHEQSZ6G.mjs";
2
+ import { t as multipartBody } from "../multipartBody-D0eBvkeq.mjs";
3
3
  //#region src/middleware/middleware.ts
4
4
  /**
5
5
  * Named Middleware — Priority-based, conditional middleware execution.