@classytic/arc 2.41.0 → 2.43.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 (230) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-n7qO5Cz3.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
  3. package/dist/{HookSystem-CxvZBo77.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-Cu7uTJp9.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
  6. package/dist/actionPermissions-BotX1YOa.mjs +776 -0
  7. package/dist/audit/index.d.mts +43 -5
  8. package/dist/audit/index.mjs +48 -26
  9. package/dist/auth/audit.mjs +1 -1
  10. package/dist/auth/index.d.mts +43 -13
  11. package/dist/auth/index.mjs +130 -155
  12. package/dist/auth/redis-session.mjs +1 -1
  13. package/dist/{authorizationDecision-2YY_Kaj1.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
  14. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  15. package/dist/boundedMap-BULzfXYU.mjs +57 -0
  16. package/dist/{buildHandler-CUhjGHGZ.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
  17. package/dist/cache/index.d.mts +22 -34
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-DsWDjIxx.mjs → caching-Cc2M1DwO.mjs} +3 -3
  20. package/dist/claimed-B10RTfkM.mjs +8 -0
  21. package/dist/cleanup/index.d.mts +6 -10
  22. package/dist/cleanup/index.mjs +27 -73
  23. package/dist/cli/commands/describe.d.mts +4 -2
  24. package/dist/cli/commands/describe.mjs +7 -4
  25. package/dist/cli/commands/docs.mjs +3 -3
  26. package/dist/cli/commands/doctor.mjs +1 -1
  27. package/dist/cli/commands/generate.mjs +2 -2
  28. package/dist/cli/commands/init.mjs +2 -2
  29. package/dist/cli/commands/introspect.mjs +2 -2
  30. package/dist/cli/index.mjs +3 -3
  31. package/dist/{constants-BxLgNFF7.mjs → constants-Cu7Mx7_U.mjs} +4 -2
  32. package/dist/context/index.d.mts +2 -5
  33. package/dist/context/index.mjs +18 -38
  34. package/dist/core/index.d.mts +3 -3
  35. package/dist/core/index.mjs +8 -8
  36. package/dist/{core-iNGqYerO.mjs → core-Cb0LjccA.mjs} +5 -4
  37. package/dist/{createActionRouter-BBlbPs3k.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
  38. package/dist/{createAggregationRouter-Dv5SuK2x.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
  39. package/dist/{preloadResources-Bbp4QJ8V.mjs → createApp-D-953ZAq.mjs} +299 -390
  40. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
  41. package/dist/{defineAggregation-UyeurGkp.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
  42. package/dist/{defineResource-DzGT-2LP.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
  43. package/dist/discovery/index.mjs +1 -1
  44. package/dist/docs/index.d.mts +10 -4
  45. package/dist/docs/index.mjs +4 -4
  46. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  47. package/dist/{elevation-BF-vhGxY.mjs → elevation-DG8SrLN-.mjs} +41 -12
  48. package/dist/encryption/index.mjs +3 -3
  49. package/dist/{errorHandler-CIMkCOWm.mjs → errorHandler-BnfDviL1.mjs} +26 -5
  50. package/dist/{errors-C_YhELhe.mjs → errors-CtrywMHP.mjs} +58 -23
  51. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  52. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  53. package/dist/{eventPlugin-CmXaPWLv.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
  54. package/dist/{subscribe-helpers-AVYPgqTl.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
  55. package/dist/events/index.d.mts +33 -8
  56. package/dist/events/index.mjs +49 -53
  57. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  58. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  59. package/dist/events/transports/redis.d.mts +1 -1
  60. package/dist/events/transports/redis.mjs +2 -2
  61. package/dist/factory/index.d.mts +32 -17
  62. package/dist/factory/index.mjs +213 -32
  63. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
  64. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  65. package/dist/{fields-BTyPLHK0.mjs → fields-ToPYpMqS.mjs} +1 -1
  66. package/dist/filter-merge-IpGywvym.mjs +99 -0
  67. package/dist/{generate-D_YmiCNf.mjs → generate-DRTkLlIl.mjs} +2 -2
  68. package/dist/{headers-CNECDE6q.mjs → headers-BbkIF6vA.mjs} +2 -2
  69. package/dist/health-D-Ft6JM6.mjs +22 -0
  70. package/dist/{health-CxQzbJmr.mjs → health-DqbsG-_w.mjs} +115 -55
  71. package/dist/hooks/index.d.mts +2 -2
  72. package/dist/hooks/index.mjs +2 -2
  73. package/dist/idempotency/index.d.mts +31 -7
  74. package/dist/idempotency/index.mjs +155 -98
  75. package/dist/idempotency/redis.d.mts +1 -1
  76. package/dist/idempotency/redis.mjs +48 -11
  77. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  78. package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
  79. package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
  80. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  81. package/dist/index-JdbZAIsm.d.mts +510 -0
  82. package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
  83. package/dist/index.d.mts +8 -8
  84. package/dist/index.mjs +11 -11
  85. package/dist/{init-Y2MlSN9n.mjs → init-B_oUYDXI.mjs} +1 -1
  86. package/dist/integrations/event-gateway.d.mts +8 -3
  87. package/dist/integrations/event-gateway.mjs +4 -4
  88. package/dist/integrations/index.d.mts +3 -3
  89. package/dist/integrations/index.mjs +1 -1
  90. package/dist/integrations/jobs/index.d.mts +1 -1
  91. package/dist/integrations/jobs/index.mjs +18 -5
  92. package/dist/integrations/mcp/index.d.mts +72 -59
  93. package/dist/integrations/mcp/index.mjs +19 -4
  94. package/dist/integrations/mcp/testing.d.mts +2 -2
  95. package/dist/integrations/mcp/testing.mjs +2 -2
  96. package/dist/integrations/streamline.d.mts +78 -136
  97. package/dist/integrations/streamline.mjs +116 -54
  98. package/dist/integrations/webhooks.d.mts +60 -120
  99. package/dist/integrations/webhooks.mjs +36 -80
  100. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  101. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  102. package/dist/integrations/websocket-redis.d.mts +1 -1
  103. package/dist/integrations/websocket-redis.mjs +1 -1
  104. package/dist/integrations/websocket.d.mts +3 -3
  105. package/dist/integrations/websocket.mjs +2 -2
  106. package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
  107. package/dist/{jsonBody-Cq9KYl2Z.mjs → jsonBody-B4YVGcql.mjs} +2 -2
  108. package/dist/{keys-Cmv4cBRa.mjs → keys-B2KtgMLJ.mjs} +7 -18
  109. package/dist/{loadResources-CblF7xt2.mjs → loadResources-Dn4lalw2.mjs} +3 -3
  110. package/dist/{loadResourcesFromEntry-99fIMC3p.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
  111. package/dist/logger/index.d.mts +1 -8
  112. package/dist/logger/index.mjs +3 -153
  113. package/dist/logger-DXjfIqjV.mjs +160 -0
  114. package/dist/{mcpPlugin-xF3Gw-HW.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
  115. package/dist/{memory-CpDZ9NPw.mjs → memory-9Cp7611C.mjs} +36 -34
  116. package/dist/{metrics-BaKnq8ag.mjs → metrics-CWI11eRu.mjs} +39 -72
  117. package/dist/middleware/index.d.mts +2 -2
  118. package/dist/middleware/index.mjs +3 -3
  119. package/dist/migrations/index.mjs +2 -2
  120. package/dist/{module-BkQEesNQ.mjs → module-BSvOQC7E.mjs} +8 -8
  121. package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
  122. package/dist/{multipartBody-C92aJ8am.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
  123. package/dist/{openapi-C3LFRzPq.mjs → openapi-DT_G4VhY.mjs} +48 -10
  124. package/dist/outbox-admin/index.d.mts +3 -3
  125. package/dist/outbox-admin/index.mjs +6 -18
  126. package/dist/{outbox-module-dsCyLxXH.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
  127. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  128. package/dist/permissions/index.d.mts +5 -4
  129. package/dist/permissions/index.mjs +8 -6
  130. package/dist/{permissions-DiHXy8Ck.mjs → permissions-Cm0uYHvd.mjs} +149 -33
  131. package/dist/{pipe-CBVmUJ-j.mjs → pipe-BqeGT6UO.mjs} +2 -2
  132. package/dist/pipeline/index.d.mts +2 -2
  133. package/dist/pipeline/index.mjs +2 -2
  134. package/dist/plan-FVR6LP59.mjs +565 -0
  135. package/dist/{BaseController-DYJRZ0EJ.mjs → plugin-BccET1tL.mjs} +1256 -344
  136. package/dist/plugins/index.d.mts +14 -11
  137. package/dist/plugins/index.mjs +44 -22
  138. package/dist/plugins/response-cache.mjs +8 -14
  139. package/dist/plugins/tracing-entry.mjs +2 -2
  140. package/dist/{pluralize-2_U6QZQg.mjs → pluralize-CVIOcgMF.mjs} +1 -1
  141. package/dist/presets/filesUpload.d.mts +2 -2
  142. package/dist/presets/filesUpload.mjs +5 -5
  143. package/dist/presets/index.d.mts +26 -31
  144. package/dist/presets/index.mjs +4 -4
  145. package/dist/presets/multiTenant.d.mts +2 -107
  146. package/dist/presets/multiTenant.mjs +22 -162
  147. package/dist/presets/search.d.mts +2 -2
  148. package/dist/presets/search.mjs +2 -2
  149. package/dist/{presets-BdlugYTc.mjs → presets-Cpvj6fGO.mjs} +99 -107
  150. package/dist/{projection-Uvgp-EEj.mjs → projection-Ce4aQhQk.mjs} +2 -2
  151. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  152. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  153. package/dist/{queryCachePlugin-B5F09bYr.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
  154. package/dist/{rateLimitKey-CK-EU2L8.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
  155. package/dist/{realtime-FnnD0n3Z.mjs → realtime-DmPE9S54.mjs} +25 -21
  156. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  157. package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
  158. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  159. package/dist/registry/index.d.mts +9 -4
  160. package/dist/registry/index.mjs +4 -4
  161. package/dist/{registry-D3y8VEJ1.mjs → registry-Bcz9G1bh.mjs} +10 -8
  162. package/dist/{renewingLease-B3VYh5aU.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
  163. package/dist/{replyHelpers-DPHsdw-5.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
  164. package/dist/{requestContext-cSVKnYPk.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
  165. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  166. package/dist/requirement-KDdoRcOP.mjs +35 -0
  167. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  168. package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
  169. package/dist/{rolldown-runtime-DjDuyglE.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
  170. package/dist/routeValidator-D36SW8qt.mjs +70 -0
  171. package/dist/{runtimeCapabilities-DSA_dH1_.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
  172. package/dist/{schedules-BktJb3Y3.mjs → schedules-C-OpmHBq.mjs} +46 -37
  173. package/dist/{schemaConverter-BLCSmNbr.mjs → schemaConverter-BORek3O-.mjs} +2 -2
  174. package/dist/{schemaIR-K16Bb5BX.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
  175. package/dist/schemas/index.d.mts +10 -0
  176. package/dist/schemas/index.mjs +8 -3
  177. package/dist/scim/index.mjs +2 -2
  178. package/dist/scope/index.d.mts +2 -2
  179. package/dist/scope/index.mjs +6 -6
  180. package/dist/{scope-jSxuBTFM.mjs → scope-DQthe88b.mjs} +4 -4
  181. package/dist/shutdown-MqA81z_P.mjs +59 -0
  182. package/dist/{simpleEqualityMatcher-BiOnyflP.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
  183. package/dist/{sse-CH7Slxc1.mjs → sse-CYzQBVFe.mjs} +11 -15
  184. package/dist/{sseStream-CN-Lh1Tu.mjs → sseStream-B5_Afiqn.mjs} +2 -2
  185. package/dist/{store-helpers-9DX9-1j_.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
  186. package/dist/{streaming-dvppnxsr.mjs → streaming-DjTQhZrN.mjs} +1 -1
  187. package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
  188. package/dist/tenancy--ed9eH_z.mjs +43 -0
  189. package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
  190. package/dist/testing/cleanupStoreContract.mjs +1 -1
  191. package/dist/testing/index.d.mts +193 -175
  192. package/dist/testing/index.mjs +286 -212
  193. package/dist/testing/outboxStoreContract.mjs +137 -2
  194. package/dist/testing/requiredPorts.d.mts +17 -0
  195. package/dist/testing/requiredPorts.mjs +65 -0
  196. package/dist/testing/storageContract.mjs +1 -1
  197. package/dist/timing-qczZ0YC-.mjs +46 -0
  198. package/dist/{transactionContext-DCWMI6hV.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
  199. package/dist/{typeGuards-D7tHFdH6.mjs → typeGuards-CySF12G-.mjs} +1 -1
  200. package/dist/types/index.d.mts +4 -4
  201. package/dist/types/index.mjs +1 -1
  202. package/dist/types/storage.mjs +1 -1
  203. package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
  204. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  205. package/dist/{types-CDy3-zw1.mjs → types-Bk9NOgOC.mjs} +4 -4
  206. package/dist/usage/index.d.mts +2 -3
  207. package/dist/usage/index.mjs +5 -5
  208. package/dist/{userHelpers-DDMOZ_Pm.mjs → userHelpers-aklawRaK.mjs} +1 -1
  209. package/dist/utils/index.d.mts +84 -223
  210. package/dist/utils/index.mjs +13 -12
  211. package/dist/{utils-B-zj_b__.mjs → utils-ec1oTo1n.mjs} +315 -404
  212. package/dist/{validate-R0G_JrnK.mjs → validate-9yzHC1e2.mjs} +6 -14
  213. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  214. package/dist/{versioning-DuVWcu-F.mjs → versioning-fhhWHV74.mjs} +4 -4
  215. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  216. package/dist/{workScope-BhsWde4k.mjs → workScope-zpPyge4t.mjs} +2 -2
  217. package/package.json +578 -574
  218. package/skills/arc/SKILL.md +13 -4
  219. package/skills/arc/references/auth.md +8 -5
  220. package/skills/arc/references/integrations.md +1 -1
  221. package/skills/arc/references/mcp.md +8 -6
  222. package/skills/arc/references/multi-tenancy.md +19 -14
  223. package/skills/arc/references/production.md +27 -10
  224. package/dist/actionPermissions-DPwPWPOV.mjs +0 -350
  225. package/dist/index-CFfLKMH4.d.mts +0 -290
  226. package/dist/interface-BIpgzbZl.mjs +0 -17
  227. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  228. package/dist/registerRoute-DRsSHPSM.mjs +0 -364
  229. package/dist/sync/changelog.d.mts +0 -3
  230. package/dist/sync/changelog.mjs +0 -3
@@ -1,21 +1,28 @@
1
- /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
- import { r as evaluatePermissionDecision } from "./authorizationDecision-2YY_Kaj1.mjs";
3
- import { p as isArcError, t as ArcError } from "./errors-C_YhELhe.mjs";
4
- import { n as requireSingleHeaderValue } from "./headers-CNECDE6q.mjs";
5
- import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-CDy3-zw1.mjs";
6
- import { t as BaseController } from "./BaseController-DYJRZ0EJ.mjs";
7
- import { a as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-BTyPLHK0.mjs";
8
- import { t as buildRequestScopeProjection } from "./projection-Uvgp-EEj.mjs";
9
- import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-DPwPWPOV.mjs";
10
- import { t as executePipeline } from "./pipe-CBVmUJ-j.mjs";
11
- import { t as pluralize } from "./pluralize-2_U6QZQg.mjs";
12
- import { t as createIdempotencyResult } from "./interface-BIpgzbZl.mjs";
13
- import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-K16Bb5BX.mjs";
14
- import { n as validateAggregations } from "./validate-R0G_JrnK.mjs";
15
- import { n as executeAggregation } from "./buildHandler-CUhjGHGZ.mjs";
16
- import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as arcLog } from "./logger-DXjfIqjV.mjs";
3
+ import { i as NotFoundError, p as isArcError, t as ArcError } from "./errors-CtrywMHP.mjs";
4
+ import { c as BaseController, n as normalizeActionsToRouterConfig, r as resolveActionTargetFence } from "./plugin-BccET1tL.mjs";
5
+ import { n as requireSingleHeaderValue } from "./headers-BbkIF6vA.mjs";
6
+ import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-Bk9NOgOC.mjs";
7
+ import { a as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-ToPYpMqS.mjs";
8
+ import { o as toJsonSchema } from "./schemaConverter-BORek3O-.mjs";
9
+ import { t as buildRequestScopeProjection } from "./projection-Ce4aQhQk.mjs";
10
+ import { l as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-BotX1YOa.mjs";
11
+ import { a as withoutTenantInjection, r as prepareTenantContext } from "./tenantPreparation-BKon6Yox.mjs";
12
+ import { r as evaluatePermissionDecision } from "./authorizationDecision-DQYrkl5f.mjs";
13
+ import { n as declareRuntimeCapability } from "./runtimeCapabilities-0tccjRlj.mjs";
14
+ import { t as executePipeline } from "./pipe-BqeGT6UO.mjs";
15
+ import { a as resourceRateLimitWindowMs, i as resolveResourceRateLimiter, t as createMemoryResourceRateLimiter } from "./resourceRateLimit-dSjVr1pk.mjs";
16
+ import { n as evictToSize, r as touch, t as assertCapacity } from "./boundedMap-BULzfXYU.mjs";
17
+ import { t as pluralize } from "./pluralize-CVIOcgMF.mjs";
18
+ import { n as createIdempotencyResult, r as operationIdentity, t as resolveIdempotencyTiming } from "./timing-qczZ0YC-.mjs";
19
+ import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-C7Vbp71I.mjs";
20
+ import { n as validateAggregations } from "./validate-9yzHC1e2.mjs";
21
+ import { n as executeAggregation } from "./buildHandler-CRQ8EcX5.mjs";
22
+ import { ARC_ERROR_CODES, isHttpError, toErrorContract } from "@classytic/repo-core/errors";
17
23
  import { createHash, randomUUID } from "node:crypto";
18
24
  import fp from "fastify-plugin";
25
+ import { IdempotencyError, fingerprintRequest } from "@classytic/repo-core/idempotency";
19
26
  import { z } from "zod";
20
27
  //#region src/integrations/mcp/createMcpServer.ts
21
28
  /**
@@ -222,51 +229,55 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
222
229
  },
223
230
  ...extras?.server ? { server: extras.server } : {}
224
231
  };
232
+ const prepared = (ctx) => {
233
+ prepareTenantContext(ctx, extras?.tenantMiddlewares);
234
+ return ctx;
235
+ };
225
236
  switch (operation) {
226
- case "list": return {
237
+ case "list": return prepared({
227
238
  ...base,
228
239
  params: {},
229
240
  query: expandOperatorKeys(input),
230
241
  body: void 0
231
- };
242
+ });
232
243
  case "get": {
233
244
  const { id, ...rest } = input;
234
- return {
245
+ return prepared({
235
246
  ...base,
236
247
  params: { id: String(id ?? "") },
237
248
  query: expandOperatorKeys(rest),
238
249
  body: void 0
239
- };
250
+ });
240
251
  }
241
- case "create": return {
252
+ case "create": return prepared({
242
253
  ...base,
243
254
  params: {},
244
255
  query: {},
245
256
  body: { ...input }
246
- };
257
+ });
247
258
  case "update": {
248
259
  const { id: _id, ...body } = input;
249
- return {
260
+ return prepared({
250
261
  ...base,
251
262
  params: { id: String(_id ?? "") },
252
263
  query: {},
253
264
  body
254
- };
265
+ });
255
266
  }
256
- case "delete": return {
267
+ case "delete": return prepared({
257
268
  ...base,
258
269
  params: { id: String(input.id ?? "") },
259
270
  query: {},
260
271
  body: void 0
261
- };
272
+ });
262
273
  case "action": {
263
274
  const { id: _actionId, ...actionBody } = input;
264
- return {
275
+ return prepared({
265
276
  ...base,
266
277
  params: { id: String(_actionId ?? "") },
267
278
  query: {},
268
279
  body: actionBody
269
- };
280
+ });
270
281
  }
271
282
  }
272
283
  }
@@ -563,36 +574,137 @@ function createMcpController(resource) {
563
574
  });
564
575
  }
565
576
  //#endregion
566
- //#region src/integrations/mcp/invokeController.ts
577
+ //#region src/integrations/mcp/http-parity.ts
567
578
  /**
568
- * @classytic/arc — Non-HTTP controller invocation for MCP / jobs / workflows.
569
- *
570
- * Internal callers (MCP tools, scheduled jobs, workflow steps) need to invoke
571
- * controller methods without going through Fastify's preHandler chain.
572
- * Before this module landed, every call site reinvented the same shim:
573
- * fabricate `params` / `body` / `headers`, project the session into a
574
- * `RequestScope`, run the permission check by hand, translate thrown errors
575
- * to the MCP `CallToolResult` shape. The drift across those copies is what
576
- * the mentora review flagged as the "buildCtx synthetic-context hack."
577
- *
578
- * Two public surfaces, layered so callers reach for the smallest piece that
579
- * does the job:
580
- *
581
- * - {@link invokeController} full pipeline: permission → context →
582
- * dispatch → canonical envelope/error
583
- * conversion. Returns `CallToolResult`.
584
- * - {@link mcpHandlerAdapter} thin try/catch wrapper that turns any
585
- * async function into a `ToolDefinition`
586
- * handler with canonical error shape. Use
587
- * when you want to compose multiple
588
- * controller calls or weave in custom logic
589
- * between them.
590
- *
591
- * Both honor the same {@link buildRequestContext} primitive — re-exported
592
- * from this module so authors who want the synthetic context as data (e.g.
593
- * to pass into `executePipeline`) can grab it without depending on an
594
- * internal subpath.
579
+ * The gates an HTTP route applies that an MCP tool must apply too, because a tool calls the
580
+ * controller or action handler directly and skips the route: idempotent execution, the
581
+ * key-required refusal, the per-caller rate limit and the action audit record.
595
582
  */
583
+ function isIdempotencyStore(value) {
584
+ if (typeof value !== "object" || value === null) return false;
585
+ const s = value;
586
+ return typeof s.get === "function" && typeof s.set === "function" && typeof s.tryLock === "function" && typeof s.unlock === "function";
587
+ }
588
+ /** The wired store, when it is one. */
589
+ function idempotencyStoreOf(store) {
590
+ return isIdempotencyStore(store) ? store : void 0;
591
+ }
592
+ function isFencedStore(store) {
593
+ return typeof store.setIfLocked === "function" && store.supportsFencedCompletion !== false;
594
+ }
595
+ /** Refuse an unsafe standalone store; an explicit migration escape is logged at wiring. */
596
+ function assertMcpIdempotencyFencing(wiring) {
597
+ resolveIdempotencyTiming(wiring?.idempotencyTtlMs, wiring?.idempotencyLockTimeoutMs);
598
+ const store = idempotencyStoreOf(wiring?.idempotencyStore);
599
+ if (!store || isFencedStore(store)) return;
600
+ const message = "[Arc/MCP] Idempotency store must implement atomic setIfLocked completion fencing. Upgrade the store or explicitly allowUnfencedCompletion during migration.";
601
+ if (wiring?.allowUnfencedCompletion !== true) throw new Error(message);
602
+ const logger = wiring.log && typeof wiring.log === "object" ? wiring.log : {};
603
+ const warning = "[Arc/MCP] allowUnfencedCompletion permits an expired worker to overwrite a newer result; migrate the idempotency store to atomic setIfLocked.";
604
+ if (typeof logger.warn === "function") logger.warn.call(wiring.log, warning);
605
+ else arcLog("mcp:idempotency").warn(warning);
606
+ }
607
+ /** Same protocol as the HTTP plugin: cached → replay; locked → 409; else lock → run → record. */
608
+ async function runIdempotentTool(args) {
609
+ const { store, dispatch } = args;
610
+ const { ttlMs, lockTimeoutMs } = resolveIdempotencyTiming(args.idempotencyTtlMs, args.idempotencyLockTimeoutMs);
611
+ if (!isFencedStore(store) && args.allowUnfencedCompletion !== true) return toCallToolError({
612
+ code: ARC_ERROR_CODES.INTERNAL_ERROR,
613
+ message: "Idempotency store requires atomic setIfLocked completion fencing",
614
+ status: 500
615
+ });
616
+ const fullKey = `${args.key}:${operationIdentity(`${args.resourceName}:${args.operation}`, args.scope ?? buildScope(args.session), void 0, "mcp")}`;
617
+ let fingerprint;
618
+ try {
619
+ fingerprint = fingerprintRequest(args.input);
620
+ } catch (error) {
621
+ if (error instanceof IdempotencyError) return toCallToolError({
622
+ code: ARC_ERROR_CODES.IDEMPOTENCY_BODY_UNFINGERPRINTABLE,
623
+ message: error.message,
624
+ status: 400
625
+ });
626
+ throw error;
627
+ }
628
+ const replay = (cached) => {
629
+ if (cached) {
630
+ if (cached.requestFingerprint && cached.requestFingerprint !== fingerprint) return toCallToolError({
631
+ code: ARC_ERROR_CODES.IDEMPOTENCY_KEY_REUSED,
632
+ message: "Idempotency key was already used for a different request body. Use a new key for a new request.",
633
+ status: 422
634
+ });
635
+ return cached.body;
636
+ }
637
+ };
638
+ const cachedResult = replay(await store.get(fullKey));
639
+ if (cachedResult) return cachedResult;
640
+ const lockId = randomUUID();
641
+ if (!await store.tryLock(fullKey, lockId, lockTimeoutMs)) return toCallToolError({
642
+ code: ARC_ERROR_CODES.IDEMPOTENCY_CONFLICT,
643
+ message: "A call with this idempotency key is already in progress",
644
+ status: 409
645
+ });
646
+ try {
647
+ const completed = replay(await store.get(fullKey));
648
+ if (completed) return completed;
649
+ const result = JSON.parse(JSON.stringify(await dispatch()));
650
+ if (!result.isError) {
651
+ const completion = createIdempotencyResult(200, result, {}, ttlMs, fingerprint);
652
+ if (store.setIfLocked && store.supportsFencedCompletion !== false) {
653
+ if (!await store.setIfLocked(fullKey, lockId, completion)) return toCallToolError({
654
+ code: ARC_ERROR_CODES.IDEMPOTENCY_CONFLICT,
655
+ message: "The idempotency completion lease was lost; success could not be recorded",
656
+ status: 409
657
+ });
658
+ } else await store.set(fullKey, completion);
659
+ }
660
+ return result;
661
+ } finally {
662
+ await store.unlock(fullKey, lockId);
663
+ }
664
+ }
665
+ /** The 400 a keyless call to a key-required route gets over HTTP. */
666
+ function idempotencyKeyRequiredResult(toolName) {
667
+ return toCallToolError({
668
+ code: ARC_ERROR_CODES.IDEMPOTENCY_KEY_REQUIRED,
669
+ message: `'${toolName}' moves money or stock: pass \`_idempotencyKey\` and reuse it on a retry.`,
670
+ status: 400
671
+ });
672
+ }
673
+ function createToolRateLimiter(config, resourceName, limiter = createMemoryResourceRateLimiter()) {
674
+ if (!config) return void 0;
675
+ resourceRateLimitWindowMs(config.timeWindow);
676
+ return async (session, scope) => (await limiter.consume(resourceName, config, { scope: scope ?? buildScope(session) })).allowed;
677
+ }
678
+ function rateLimitedResult(toolName) {
679
+ return toCallToolError({
680
+ code: "arc.rate_limited",
681
+ message: `Too many calls to '${toolName}' — retry later.`,
682
+ status: 429
683
+ });
684
+ }
685
+ /** `audit.custom(resource, id, action, {}, context)` after a successful action, like the route. */
686
+ async function auditToolAction(audit, args) {
687
+ if (typeof audit !== "object" || audit === null) return;
688
+ const logger = audit;
689
+ if (typeof logger.custom !== "function" || logger._noop === true) return;
690
+ const { session } = args;
691
+ const actorId = session?.userId ?? session?.clientId;
692
+ try {
693
+ await logger.custom.call(audit, args.resourceName, args.id, args.action, { channel: "mcp" }, {
694
+ ...actorId !== void 0 ? { actorId } : {},
695
+ ...session?.organizationId !== void 0 ? { organizationId: session.organizationId } : {},
696
+ endpoint: `MCP ${args.toolName}`
697
+ });
698
+ } catch (err) {
699
+ const log = typeof args.log === "object" && args.log !== null ? args.log : void 0;
700
+ if (typeof log?.error === "function") log.error.call(log, {
701
+ err,
702
+ tool: args.toolName
703
+ }, "[arc/mcp] action audit write failed");
704
+ }
705
+ }
706
+ //#endregion
707
+ //#region src/integrations/mcp/invokeController.ts
596
708
  /**
597
709
  * Assemble the {@link McpContextExtras} for a controller invocation from
598
710
  * resource-scoped metadata + app-level wiring. Mirrors what
@@ -601,8 +713,9 @@ function createMcpController(resource) {
601
713
  */
602
714
  function buildContextExtras(options) {
603
715
  const { wiring, resourceName, permissions, fields, schemaOptions, idField } = options;
604
- if (!wiring) return void 0;
716
+ if (!wiring) return { tenantMiddlewares: options.tenantMiddlewares };
605
717
  return {
718
+ tenantMiddlewares: options.tenantMiddlewares,
606
719
  arc: {
607
720
  resourceName,
608
721
  schemaOptions,
@@ -620,23 +733,6 @@ function buildContextExtras(options) {
620
733
  };
621
734
  }
622
735
  /**
623
- * Stable fingerprint of a tool invocation — sorted-key JSON, hashed.
624
- * Scopes an idempotency key to (resource, action, input, caller) so a
625
- * reused key with DIFFERENT input is a fresh execution, matching the HTTP
626
- * plugin's method+url+bodyHash+user fingerprint semantics.
627
- */
628
- function invocationFingerprint(resourceName, actionName, input, session) {
629
- const sortKeys = (value) => {
630
- if (Array.isArray(value)) return value.map(sortKeys);
631
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
632
- return value;
633
- };
634
- return `mcp:${resourceName}:${actionName}:${createHash("sha256").update(JSON.stringify(sortKeys(input))).digest("hex").substring(0, 16)}:u=${session?.userId ?? session?.clientId ?? "anon"}`;
635
- }
636
- /** TTLs matching the HTTP idempotency plugin's defaults. */
637
- const MCP_IDEMPOTENCY_TTL_MS = 864e5;
638
- const MCP_IDEMPOTENCY_LOCK_MS = 1e4;
639
- /**
640
736
  * Invoke a controller method through the MCP synthetic-context path.
641
737
  *
642
738
  * Runs the same chain CRUD MCP tools run:
@@ -685,8 +781,9 @@ async function invokeController(controller, op, input, options) {
685
781
  reason: permResult.reason,
686
782
  session
687
783
  });
784
+ const reqCtx = buildRequestContext(input, session, op, permResult?.policy, permResult?.scope, buildContextExtras(options));
785
+ if (options.allow && !await options.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(actionName);
688
786
  const dispatch = async () => {
689
- const reqCtx = buildRequestContext(input, session, op, permResult?.policy, permResult?.scope, buildContextExtras(options));
690
787
  const response = await method(reqCtx);
691
788
  const data = applyMcpReadMasking(response.data, {
692
789
  fields,
@@ -698,25 +795,20 @@ async function invokeController(controller, op, input, options) {
698
795
  data
699
796
  });
700
797
  };
701
- const store = wiring?.idempotencyStore;
702
- if (idempotencyKey && store) {
703
- const fullKey = `${idempotencyKey}:${invocationFingerprint(resourceName, actionName, input, session)}`;
704
- const cached = await store.get(fullKey);
705
- if (cached) return cached.body;
706
- const lockId = randomUUID();
707
- if (!await store.tryLock(fullKey, lockId, MCP_IDEMPOTENCY_LOCK_MS)) return toCallToolError({
708
- code: "arc.idempotency_conflict",
709
- message: "A call with this idempotency key is already in progress",
710
- status: 409
711
- });
712
- try {
713
- const result = await dispatch();
714
- if (!result.isError) await store.set(fullKey, createIdempotencyResult(200, result, {}, MCP_IDEMPOTENCY_TTL_MS));
715
- return result;
716
- } finally {
717
- await store.unlock(fullKey, lockId);
718
- }
719
- }
798
+ const store = idempotencyStoreOf(wiring?.idempotencyStore);
799
+ if (idempotencyKey && store) return await runIdempotentTool({
800
+ store,
801
+ allowUnfencedCompletion: wiring?.allowUnfencedCompletion,
802
+ idempotencyTtlMs: wiring?.idempotencyTtlMs,
803
+ idempotencyLockTimeoutMs: wiring?.idempotencyLockTimeoutMs,
804
+ key: idempotencyKey,
805
+ resourceName,
806
+ operation: actionName,
807
+ input,
808
+ session,
809
+ scope: reqCtx.metadata._scope,
810
+ dispatch
811
+ });
720
812
  return await dispatch();
721
813
  } catch (err) {
722
814
  return toCallToolError(err instanceof Error ? err : new Error(String(err)));
@@ -806,7 +898,9 @@ const MUTATING_CRUD_OPS = /* @__PURE__ */ new Set([
806
898
  ]);
807
899
  function createCrudHandler(op, controller, resourceName, permissions, fields, extras) {
808
900
  const permission = permissions?.[op];
809
- return (input, ctx) => {
901
+ const toolName = extras?.toolName ?? `${op}_${resourceName}`;
902
+ return async (input, ctx) => {
903
+ if (extras?.keyRequired && (typeof input._idempotencyKey !== "string" || input._idempotencyKey === "")) return idempotencyKeyRequiredResult(toolName);
810
904
  let idempotencyKey;
811
905
  let payload = input;
812
906
  if ("_idempotencyKey" in input) {
@@ -816,10 +910,12 @@ function createCrudHandler(op, controller, resourceName, permissions, fields, ex
816
910
  }
817
911
  return invokeController(controller, op, payload, {
818
912
  session: ctx.session,
913
+ allow: extras?.allow,
819
914
  resourceName,
820
915
  permissions: permission,
821
916
  fields,
822
917
  wiring: extras?.wiring,
918
+ tenantMiddlewares: extras?.tenantMiddlewares,
823
919
  schemaOptions: extras?.schemaOptions,
824
920
  idField: extras?.idField,
825
921
  idempotencyKey
@@ -1082,7 +1178,7 @@ var McpAuthCache = class {
1082
1178
  maxEntries;
1083
1179
  constructor(opts) {
1084
1180
  this.ttlMs = opts?.ttlMs ?? DEFAULT_AUTH_CACHE_TTL_MS;
1085
- this.maxEntries = opts?.maxEntries ?? DEFAULT_AUTH_CACHE_MAX;
1181
+ this.maxEntries = assertCapacity("McpAuthCache maxEntries", opts?.maxEntries ?? DEFAULT_AUTH_CACHE_MAX);
1086
1182
  }
1087
1183
  get(key) {
1088
1184
  const entry = this.cache.get(key);
@@ -1097,15 +1193,12 @@ var McpAuthCache = class {
1097
1193
  if (this.cache.size >= this.maxEntries) {
1098
1194
  const now = Date.now();
1099
1195
  for (const [k, v] of this.cache) if (now > v.expires) this.cache.delete(k);
1100
- if (this.cache.size >= this.maxEntries) {
1101
- const firstKey = this.cache.keys().next().value;
1102
- if (firstKey) this.cache.delete(firstKey);
1103
- }
1104
1196
  }
1105
- this.cache.set(key, {
1197
+ touch(this.cache, key, {
1106
1198
  result,
1107
1199
  expires: Date.now() + this.ttlMs
1108
1200
  });
1201
+ evictToSize(this.cache, this.maxEntries);
1109
1202
  }
1110
1203
  };
1111
1204
  /**
@@ -1229,7 +1322,10 @@ function convertActionSchemaToZod(raw) {
1229
1322
  */
1230
1323
  function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false, fields, extras) {
1231
1324
  const wiring = extras?.wiring;
1325
+ const parity = extras?.parity;
1326
+ const toolName = parity?.toolName ?? `${actionName}_${resourceName}`;
1232
1327
  const contextExtras = wiring ? {
1328
+ tenantMiddlewares: extras?.tenantMiddlewares,
1233
1329
  arc: {
1234
1330
  resourceName,
1235
1331
  schemaOptions: extras?.schemaOptions,
@@ -1244,11 +1340,16 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
1244
1340
  audit: wiring.audit,
1245
1341
  log: wiring.log
1246
1342
  }
1247
- } : void 0;
1343
+ } : { tenantMiddlewares: extras?.tenantMiddlewares };
1248
1344
  const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
1249
- const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? /* @__PURE__ */ new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
1250
- return async (input, ctx) => {
1345
+ const strict = ir ? shouldRejectAdditionalProperties(ir) : false;
1346
+ const allowedBaseKeys = [...idLess ? [] : ["id"], ...parity?.keyRequired ? ["_idempotencyKey"] : []];
1347
+ const allowedKeys = strict && ir ? /* @__PURE__ */ new Set([...allowedBaseKeys, ...Object.keys(ir.properties)]) : void 0;
1348
+ return async (rawInput, ctx) => {
1251
1349
  const session = ctx.session;
1350
+ const { _idempotencyKey: rawKey, ...input } = rawInput;
1351
+ const idempotencyKey = typeof rawKey === "string" && rawKey !== "" ? rawKey : void 0;
1352
+ if (parity?.keyRequired && idempotencyKey === void 0) return idempotencyKeyRequiredResult(toolName);
1252
1353
  if (allowedKeys) {
1253
1354
  const extras = Object.keys(input).filter((k) => !allowedKeys.has(k));
1254
1355
  if (extras.length > 0) return toCallToolError({
@@ -1269,18 +1370,50 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
1269
1370
  reason: permResult.reason,
1270
1371
  session
1271
1372
  });
1272
- const reqCtx = buildRequestContext({
1273
- ...input,
1274
- action: actionName
1275
- }, session, "action", permResult?.policy, permResult?.scope, contextExtras);
1276
- const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
1277
- const { id: _discardId, ...data } = input;
1278
1373
  try {
1279
- return toCallToolSuccess(applyMcpReadMasking(await handler(id, data, reqCtx), {
1280
- fields,
1374
+ const reqCtx = buildRequestContext({
1375
+ ...input,
1376
+ action: actionName
1377
+ }, session, "action", permResult?.policy, permResult?.scope, contextExtras);
1378
+ const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
1379
+ const { id: _discardId, ...data } = input;
1380
+ if (!idLess && parity?.fence && !await parity.fence(id, reqCtx)) throw new NotFoundError(resourceName);
1381
+ if (parity?.allow && !await parity.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(toolName);
1382
+ const run = async () => {
1383
+ const req = idempotencyKey === void 0 ? reqCtx : {
1384
+ ...reqCtx,
1385
+ idempotencyKey
1386
+ };
1387
+ const result = await handler(id, data, req);
1388
+ if (parity?.audit) await auditToolAction(wiring?.audit, {
1389
+ resourceName,
1390
+ id,
1391
+ action: actionName,
1392
+ toolName,
1393
+ session,
1394
+ log: wiring?.log
1395
+ });
1396
+ return toCallToolSuccess(applyMcpReadMasking(result, {
1397
+ fields,
1398
+ session,
1399
+ scopeOverride: permResult?.scope
1400
+ }));
1401
+ };
1402
+ const store = idempotencyStoreOf(wiring?.idempotencyStore);
1403
+ if (idempotencyKey !== void 0 && store && !parity?.handlerDedup) return await runIdempotentTool({
1404
+ store,
1405
+ allowUnfencedCompletion: wiring?.allowUnfencedCompletion,
1406
+ idempotencyTtlMs: wiring?.idempotencyTtlMs,
1407
+ idempotencyLockTimeoutMs: wiring?.idempotencyLockTimeoutMs,
1408
+ key: idempotencyKey,
1409
+ resourceName,
1410
+ operation: `action.${actionName}`,
1411
+ input,
1281
1412
  session,
1282
- scopeOverride: permResult?.scope
1283
- }));
1413
+ scope: reqCtx.metadata._scope,
1414
+ dispatch: run
1415
+ });
1416
+ return await run();
1284
1417
  } catch (err) {
1285
1418
  return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1286
1419
  }
@@ -1320,7 +1453,8 @@ function buildAggregationTools(args) {
1320
1453
  resourceName,
1321
1454
  repo,
1322
1455
  buildOptionsFromSession,
1323
- fields
1456
+ fields,
1457
+ tenantMiddlewares: args.tenantMiddlewares
1324
1458
  })
1325
1459
  });
1326
1460
  }
@@ -1372,14 +1506,24 @@ function createAggregationToolHandler(args) {
1372
1506
  session: ctx.session
1373
1507
  });
1374
1508
  const filterInput = input.filter ?? {};
1375
- const tenantOptions = buildOptionsFromSession(ctx.session);
1509
+ let prepared;
1510
+ try {
1511
+ prepared = buildRequestContext(filterInput, ctx.session, "list", permResult?.policy, permResult?.scope, { tenantMiddlewares: args.tenantMiddlewares });
1512
+ } catch (error) {
1513
+ return toCallToolError(error instanceof Error ? error : new Error(String(error)));
1514
+ }
1515
+ const meta = prepared.metadata;
1516
+ const tenantOptions = {
1517
+ ...buildOptionsFromSession(ctx.session),
1518
+ ...meta._tenantFields
1519
+ };
1376
1520
  const result = await executeAggregation(normalized, {
1377
1521
  repo,
1378
1522
  buildOptions: () => tenantOptions
1379
1523
  }, {
1380
1524
  query: filterInput,
1381
1525
  tenantOptions,
1382
- policyFilter: permResult?.policy
1526
+ policyFilter: meta._policyFilters
1383
1527
  });
1384
1528
  if (result.status === 200) {
1385
1529
  const body = result.body;
@@ -1677,6 +1821,73 @@ function mapJsonSchemaTypeToArcType(jsonType) {
1677
1821
  }
1678
1822
  }
1679
1823
  //#endregion
1824
+ //#region src/integrations/mcp/route-input-schema.ts
1825
+ /**
1826
+ * Custom-route → MCP tool INPUT schema. The route's own schema is the contract:
1827
+ * an input the REST route would refuse must be refused on MCP, never stripped.
1828
+ *
1829
+ * - A Zod schema that is not a plain object (a union of strict arms) is registered
1830
+ * WHOLE — the MCP SDK validates and advertises a Standard Schema as-is. Flattening
1831
+ * it yields `{}`, and the SDK's `z.object({})` then strips every key the caller sent.
1832
+ * - A flat schema declaring `additionalProperties: false` registers as `z.strictObject`,
1833
+ * so an unknown key is an input error instead of a silently dropped filter. So
1834
+ * does a route with NO schema: a key it was sent could never reach the handler.
1835
+ * - Any other non-object schema (a JSON Schema `anyOf`, or a Zod union beside `:id`
1836
+ * or a second source) throws at tool build: there is no lossless flat form, so
1837
+ * bridge it with `mcpHandler` or opt out with `mcp: false`.
1838
+ */
1839
+ /** A Zod v4 schema (carries the `_zod` marker and the Standard Schema slot). */
1840
+ function isZodTypeSchema(value) {
1841
+ return value !== null && typeof value === "object" && "_zod" in value && "~standard" in value;
1842
+ }
1843
+ function isRecord(value) {
1844
+ return value !== null && typeof value === "object";
1845
+ }
1846
+ /** The JSON Schema HTTP validates this request slot with (`input` io, as the router converts it). */
1847
+ function requestJsonSchema(schema) {
1848
+ return toJsonSchema(schema, "draft-7", "throw", "input");
1849
+ }
1850
+ /** Does this JSON Schema have a single top-level object a flat shape can carry? */
1851
+ function flattensLosslessly(json) {
1852
+ if (!json) return true;
1853
+ if ("anyOf" in json || "oneOf" in json || "allOf" in json) return false;
1854
+ return json.type === "object" || "properties" in json;
1855
+ }
1856
+ function buildRouteInputSchema(route, hasId) {
1857
+ const routeSchema = isRecord(route.schema) ? route.schema : void 0;
1858
+ const sources = [routeSchema?.body, routeSchema?.querystring].filter(isRecord);
1859
+ const whole = [];
1860
+ let unflattenable = 0;
1861
+ for (const source of sources) {
1862
+ if (flattensLosslessly(requestJsonSchema(source))) continue;
1863
+ unflattenable++;
1864
+ if (isZodTypeSchema(source)) whole.push(source);
1865
+ }
1866
+ const [only] = whole;
1867
+ if (unflattenable > 0) {
1868
+ if (only && sources.length === 1 && !hasId) return only;
1869
+ throw new Error(`[Arc/MCP] Route '${route.method} ${route.path}': a non-object input schema (union) is bridged only as a Zod schema that is the route's sole input with no ':id' — anything else loses validation. Declare \`mcpHandler\` for this route or set \`mcp: false\`.`);
1870
+ }
1871
+ const shape = {};
1872
+ if (hasId) shape.id = z.string().describe("Resource ID");
1873
+ let strict = sources.length === 0;
1874
+ for (const source of sources) {
1875
+ if (requestJsonSchema(source)?.additionalProperties === false) strict = true;
1876
+ const ir = normalizeSchemaIR(source);
1877
+ for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) if (!(key in shape)) shape[key] = val;
1878
+ }
1879
+ return strict ? z.strictObject(shape) : shape;
1880
+ }
1881
+ /**
1882
+ * The field shape a tool advertises — the flat record itself, or a whole object
1883
+ * schema's `.shape`. `undefined` for a union, which has no single shape.
1884
+ */
1885
+ function toolInputShape(schema) {
1886
+ if (schema === void 0) return void 0;
1887
+ if (!isZodTypeSchema(schema)) return schema;
1888
+ return schema instanceof z.ZodObject ? schema.shape : void 0;
1889
+ }
1890
+ //#endregion
1680
1891
  //#region src/integrations/mcp/route-tools.ts
1681
1892
  /**
1682
1893
  * Custom-route → MCP tool generation.
@@ -1737,6 +1948,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1737
1948
  const { resourceName, operationName, permissions, pipeline, fields, wiring } = options;
1738
1949
  const pipelineSteps = resolvePipelineSteps(pipeline, operationName);
1739
1950
  const contextExtras = wiring ? {
1951
+ tenantMiddlewares: options.tenantMiddlewares,
1740
1952
  arc: {
1741
1953
  resourceName,
1742
1954
  schemaOptions: options.schemaOptions,
@@ -1751,7 +1963,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1751
1963
  audit: wiring.audit,
1752
1964
  log: wiring.log
1753
1965
  }
1754
- } : void 0;
1966
+ } : { tenantMiddlewares: options.tenantMiddlewares };
1755
1967
  return async (input, _ctx) => {
1756
1968
  const session = _ctx.session;
1757
1969
  const permResult = await evaluatePermission(permissions, session, resourceName, operationName, input);
@@ -1763,6 +1975,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1763
1975
  });
1764
1976
  try {
1765
1977
  const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.policy, permResult?.scope, contextExtras);
1978
+ if (options.allow && !await options.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(operationName);
1766
1979
  const emit = (envelope) => {
1767
1980
  const data = applyMcpReadMasking(envelope.data, {
1768
1981
  fields,
@@ -1881,10 +2094,18 @@ function resourceToTools(resource, config = {}) {
1881
2094
  const tools = [];
1882
2095
  const prefix = config.toolNamePrefix;
1883
2096
  const wiring = config.wiring;
2097
+ assertMcpIdempotencyFencing(wiring);
2098
+ const hasIdempotencyStore = idempotencyStoreOf(wiring?.idempotencyStore) !== void 0;
2099
+ const keyRequiredOps = new Set(resource.idempotency?.required ?? []);
2100
+ const rateLimit = resource.rateLimit;
2101
+ const rateLimiter = config.wiring?.rateLimiter ?? createMemoryResourceRateLimiter();
1884
2102
  if (controller) {
1885
2103
  let ops = ALL_CRUD_OPS.filter((op) => !resource.disabledRoutes?.includes(op));
1886
2104
  if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
1887
2105
  for (const op of ops) {
2106
+ if (!resource.permissions?.[op]) continue;
2107
+ const keyRequired = keyRequiredOps.has(op);
2108
+ if (keyRequired && !hasIdempotencyStore) throw new Error(`[Arc/MCP] Resource '${resource.name}': '${op}' declares idempotency.required but no idempotency store is wired — register idempotencyPlugin before mcpPlugin, or the key would be demanded and never deduplicated.`);
1888
2109
  const name = config.names?.[op] ?? (op === "list" ? `${prefix ? `${prefix}_` : ""}list_${pluralize(resource.name)}` : `${prefix ? `${prefix}_` : ""}${op}_${resource.name}`);
1889
2110
  const defaultDescription = defaultCrudDescription(op, resource.displayName, hasSoftDelete, {
1890
2111
  filterableFields,
@@ -1900,7 +2121,8 @@ function resourceToTools(resource, config = {}) {
1900
2121
  maxLimit,
1901
2122
  adapterBodies
1902
2123
  });
1903
- 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.");
2124
+ if (keyRequired) inputSchema._idempotencyKey = z.string().min(1).describe("Required idempotency key — reuse it on a retry; the first result replays.");
2125
+ else if (hasIdempotencyStore && 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.");
1904
2126
  tools.push({
1905
2127
  name,
1906
2128
  description: resolveCrudDescription(config.descriptions?.[op], {
@@ -1913,11 +2135,15 @@ function resourceToTools(resource, config = {}) {
1913
2135
  sortableFields
1914
2136
  }),
1915
2137
  annotations: CRUD_ANNOTATIONS[op],
1916
- inputSchema,
2138
+ inputSchema: op === "list" || op === "get" ? z.strictObject(inputSchema) : inputSchema,
1917
2139
  handler: createCrudHandler(op, controller, resource.name, resource.permissions, resource.fields, {
1918
2140
  wiring,
2141
+ tenantMiddlewares: resource.middlewares?.[op],
1919
2142
  schemaOptions: resource.schemaOptions,
1920
- idField: resource.idField
2143
+ idField: resource.idField,
2144
+ keyRequired,
2145
+ allow: createToolRateLimiter(rateLimit, resource.name, rateLimiter),
2146
+ toolName: name
1921
2147
  }),
1922
2148
  source: `crud:${resource.name}:${op}`
1923
2149
  });
@@ -1941,27 +2167,26 @@ function resourceToTools(resource, config = {}) {
1941
2167
  const hasId = route.path.includes(":id");
1942
2168
  const mcpConfig = typeof route.mcp === "object" && route.mcp !== null ? route.mcp : void 0;
1943
2169
  const toolDescription = mcpConfig?.description ?? route.summary ?? route.description ?? `${opName} on ${resource.displayName}`;
1944
- const toolAnnotations = mcpConfig?.annotations ? { ...mcpConfig.annotations } : { openWorldHint: true };
1945
- const inputShape = {};
1946
- if (hasId) inputShape.id = z.string().describe("Resource ID");
1947
- const routeSchema = route.schema;
1948
- if (routeSchema?.body) {
1949
- const ir = normalizeSchemaIR(routeSchema.body);
1950
- for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) inputShape[key] = val;
1951
- }
1952
- if (routeSchema?.querystring) {
1953
- const ir = normalizeSchemaIR(routeSchema.querystring);
1954
- for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) if (!(key in inputShape)) inputShape[key] = val;
1955
- }
2170
+ const toolAnnotations = mcpConfig?.annotations ? { ...mcpConfig.annotations } : route.method.toUpperCase() === "GET" ? { readOnlyHint: true } : { openWorldHint: true };
2171
+ const inputShape = buildRouteInputSchema(route, hasId);
1956
2172
  const toolName = prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`;
1957
2173
  const routeSource = route._presetSource ? `preset:${route._presetSource}:${resource.name}:${opName}` : `route:${resource.name}:${route.method} ${route.path}`;
2174
+ const routePolicy = route.rateLimit !== void 0 ? route.rateLimit : rateLimit;
2175
+ const allowRoute = createToolRateLimiter(routePolicy && typeof routePolicy === "object" ? {
2176
+ ...routePolicy,
2177
+ ...route.rateLimit ? { bucket: `route:${route.method}:${route.path}` } : {}
2178
+ } : routePolicy, resource.name, rateLimiter);
2179
+ const passthrough = mcpHandler ? createMcpHandlerPassthrough(mcpHandler) : void 0;
1958
2180
  tools.push({
1959
2181
  name: toolName,
1960
2182
  description: toolDescription,
1961
2183
  annotations: toolAnnotations,
1962
2184
  inputSchema: inputShape,
1963
2185
  source: routeSource,
1964
- handler: mcpHandler ? createMcpHandlerPassthrough(mcpHandler) : createCustomRouteHandler({
2186
+ handler: passthrough ? async (input, ctx) => {
2187
+ if (allowRoute && !await allowRoute(ctx.session)) return rateLimitedResult(toolName);
2188
+ return passthrough(input, ctx);
2189
+ } : createCustomRouteHandler({
1965
2190
  handler: resolvedRouteHandler,
1966
2191
  operation: route.operation,
1967
2192
  method: route.method,
@@ -1969,48 +2194,70 @@ function resourceToTools(resource, config = {}) {
1969
2194
  }, controller, hasId, {
1970
2195
  resourceName: resource.name,
1971
2196
  operationName: opName,
2197
+ allow: allowRoute,
1972
2198
  permissions: route.permissions,
1973
2199
  pipeline: resource.pipe,
1974
2200
  fields: resource.fields,
1975
2201
  wiring,
2202
+ tenantMiddlewares: route.tenantScope ? resource.middlewares?.tenantScope : void 0,
1976
2203
  schemaOptions: resource.schemaOptions,
1977
2204
  idField: resource.idField
1978
2205
  })
1979
2206
  });
1980
2207
  }
1981
- if (resource.actions) for (const [actionName, entry] of Object.entries(resource.actions)) {
1982
- const def = typeof entry === "function" ? { handler: entry } : entry;
1983
- if (typeof def !== "function" && "mcp" in def && def.mcp === false) continue;
1984
- const mcpCfg = typeof def !== "function" && typeof def.mcp === "object" ? def.mcp : void 0;
1985
- const description = mcpCfg?.description ?? (typeof def !== "function" ? def.description : void 0) ?? `${actionName} action on ${resource.displayName}`;
1986
- const annotations = mcpCfg?.annotations ? { ...mcpCfg.annotations } : { destructiveHint: true };
1987
- const idLess = typeof def !== "function" && def.id === false;
1988
- const inputShape = idLess ? {} : { id: z.string().describe("Resource ID") };
1989
- const rawSchema = typeof def !== "function" ? def.schema : void 0;
1990
- if (rawSchema && typeof rawSchema === "object") {
1991
- const converted = convertActionSchemaToZod(rawSchema);
1992
- for (const [key, val] of Object.entries(converted)) inputShape[key] = val;
2208
+ if (resource.actions) {
2209
+ const actionConfig = normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, void 0);
2210
+ const fence = resolveActionTargetFence(resource, actionConfig);
2211
+ const handlerScoped = new Set(actionConfig.handlerScopedActionNames);
2212
+ const keyRequiredActions = new Set(actionConfig.idempotentActionNames);
2213
+ const allow = createToolRateLimiter(rateLimit, resource.name, rateLimiter);
2214
+ for (const [actionName, entry] of Object.entries(resource.actions)) {
2215
+ const def = typeof entry === "function" ? { handler: entry } : entry;
2216
+ if (typeof def !== "function" && "mcp" in def && def.mcp === false) continue;
2217
+ const mcpCfg = typeof def !== "function" && typeof def.mcp === "object" ? def.mcp : void 0;
2218
+ const description = mcpCfg?.description ?? (typeof def !== "function" ? def.description : void 0) ?? `${actionName} action on ${resource.displayName}`;
2219
+ const annotations = mcpCfg?.annotations ? { ...mcpCfg.annotations } : { destructiveHint: true };
2220
+ const idLess = typeof def !== "function" && def.id === false;
2221
+ const inputShape = idLess ? {} : { id: z.string().describe("Resource ID") };
2222
+ const rawSchema = typeof def !== "function" ? def.schema : void 0;
2223
+ if (rawSchema && typeof rawSchema === "object") {
2224
+ const converted = convertActionSchemaToZod(rawSchema);
2225
+ for (const [key, val] of Object.entries(converted)) inputShape[key] = val;
2226
+ }
2227
+ const toolName = prefix ? `${prefix}_${actionName}_${resource.name}` : `${actionName}_${resource.name}`;
2228
+ const keyRequired = keyRequiredActions.has(actionName);
2229
+ const handlerDedup = actionConfig.handlerDedupOwners[actionName] !== void 0;
2230
+ if (keyRequired && !handlerDedup && !hasIdempotencyStore) throw new Error(`[Arc/MCP] Resource '${resource.name}': action '${actionName}' declares idempotency.required but no idempotency store is wired — register idempotencyPlugin before mcpPlugin, or the key would be demanded and never deduplicated.`);
2231
+ if (keyRequired) inputShape._idempotencyKey = z.string().min(1).describe("Required idempotency key — reuse it on a retry; the first result replays.");
2232
+ const handler = typeof entry === "function" ? entry : def.handler;
2233
+ const actionPerms = resolveActionPermission({
2234
+ action: entry,
2235
+ resourcePermissions: resource.permissions,
2236
+ resourceActionPermissions: resource.actionPermissions
2237
+ });
2238
+ if (!actionPerms) throw new Error(`[Arc/MCP] Resource '${resource.name}': action '${actionName}' has no permission gate and the resource defines no \`permissions.update\` fallback. Declare one of:\n - \`actions.${actionName}.permissions: <PermissionCheck>\` (per-action)\n - \`actionPermissions: <PermissionCheck>\` (resource-wide)\n - \`permissions.update: <PermissionCheck>\` (inherited by actions)\nUse \`allowPublic()\` if you genuinely want the action unauthenticated.`);
2239
+ tools.push({
2240
+ name: toolName,
2241
+ description: String(description),
2242
+ annotations,
2243
+ inputSchema: inputShape,
2244
+ source: `action:${resource.name}:${actionName}`,
2245
+ handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
2246
+ wiring,
2247
+ tenantMiddlewares: withoutTenantInjection(resource.middlewares?.tenantScope),
2248
+ schemaOptions: resource.schemaOptions,
2249
+ idField: resource.idField,
2250
+ parity: {
2251
+ toolName,
2252
+ keyRequired,
2253
+ handlerDedup,
2254
+ ...fence && !idLess && !handlerScoped.has(actionName) ? { fence } : {},
2255
+ audit: Boolean(resource.audit),
2256
+ ...allow ? { allow } : {}
2257
+ }
2258
+ })
2259
+ });
1993
2260
  }
1994
- const toolName = prefix ? `${prefix}_${actionName}_${resource.name}` : `${actionName}_${resource.name}`;
1995
- const handler = typeof entry === "function" ? entry : def.handler;
1996
- const actionPerms = resolveActionPermission({
1997
- action: entry,
1998
- resourcePermissions: resource.permissions,
1999
- resourceActionPermissions: resource.actionPermissions
2000
- });
2001
- if (!actionPerms) throw new Error(`[Arc/MCP] Resource '${resource.name}': action '${actionName}' has no permission gate and the resource defines no \`permissions.update\` fallback. Declare one of:\n - \`actions.${actionName}.permissions: <PermissionCheck>\` (per-action)\n - \`actionPermissions: <PermissionCheck>\` (resource-wide)\n - \`permissions.update: <PermissionCheck>\` (inherited by actions)\nUse \`allowPublic()\` if you genuinely want the action unauthenticated.`);
2002
- tools.push({
2003
- name: toolName,
2004
- description: String(description),
2005
- annotations,
2006
- inputSchema: inputShape,
2007
- source: `action:${resource.name}:${actionName}`,
2008
- handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
2009
- wiring,
2010
- schemaOptions: resource.schemaOptions,
2011
- idField: resource.idField
2012
- })
2013
- });
2014
2261
  }
2015
2262
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
2016
2263
  const repoForAgg = resource.controller?.repository;
@@ -2036,6 +2283,7 @@ function resourceToTools(resource, config = {}) {
2036
2283
  schemaOptions: resource.schemaOptions,
2037
2284
  repo: repoForAgg,
2038
2285
  buildOptionsFromSession,
2286
+ tenantMiddlewares: resource.middlewares?.aggregations,
2039
2287
  prefix,
2040
2288
  fields: resource.fields
2041
2289
  }));
@@ -2247,7 +2495,14 @@ const mcpPluginImpl = async (fastify, options) => {
2247
2495
  exclude: options.exclude
2248
2496
  });
2249
2497
  const fastifyAny = fastify;
2498
+ const rateLimiter = resolveResourceRateLimiter(fastify) ?? createMemoryResourceRateLimiter();
2499
+ if (enabledResources.some((resource) => resource.rateLimit)) declareRuntimeCapability(fastify, {
2500
+ subsystem: "mcp.resource-rate-limit",
2501
+ durability: rateLimiter.durability,
2502
+ detail: "REST and MCP resource budgets require a shared rate-limit backend across replicas."
2503
+ });
2250
2504
  const wiring = {
2505
+ rateLimiter,
2251
2506
  get hooks() {
2252
2507
  return fastifyAny.arc?.hooks;
2253
2508
  },
@@ -2260,6 +2515,15 @@ const mcpPluginImpl = async (fastify, options) => {
2260
2515
  get log() {
2261
2516
  return fastify.log;
2262
2517
  },
2518
+ get allowUnfencedCompletion() {
2519
+ return fastifyAny.idempotency?.allowUnfencedCompletion;
2520
+ },
2521
+ get idempotencyTtlMs() {
2522
+ return fastifyAny.idempotency?.ttlMs;
2523
+ },
2524
+ get idempotencyLockTimeoutMs() {
2525
+ return fastifyAny.idempotency?.lockTimeoutMs;
2526
+ },
2263
2527
  get idempotencyStore() {
2264
2528
  return fastifyAny.idempotency?.store;
2265
2529
  }
@@ -2526,11 +2790,10 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2526
2790
  });
2527
2791
  }
2528
2792
  /**
2529
- * Resolve `expose` / `include` / `exclude` into the filtered resource list
2530
- * `mcpPlugin` should surface. Three mutually exclusive intents:
2793
+ * Resolve `expose` / `exclude` into the filtered resource list `mcpPlugin`
2794
+ * should surface. Two mutually exclusive intents:
2531
2795
  *
2532
2796
  * - `expose`: default-deny allowlist (preferred — new resources auto-deny).
2533
- * - `include`: legacy alias for `expose`, kept for back-compat.
2534
2797
  * - `exclude`: default-allow opt-out (drift-prone — new resources auto-leak).
2535
2798
  *
2536
2799
  * Conflicting combinations throw with an actionable message so a host that
@@ -2564,4 +2827,4 @@ const mcpPlugin = fp(mcpPluginImpl, {
2564
2827
  fastify: "5.x"
2565
2828
  });
2566
2829
  //#endregion
2567
- export { defaultCrudDescription as a, mcpHandlerAdapter as c, toCallToolResult as d, toCallToolSuccess as f, resolveToolCollisions as h, fieldRulesToZod as i, permissionDeniedResult as l, createMcpServer as m, mcpPlugin as n, resolveCrudDescription as o, buildRequestContext as p, resourceToTools as r, invokeController as s, filterResourcesForMcp as t, toCallToolError as u };
2830
+ export { resolveToolCollisions as _, toolInputShape as a, resolveCrudDescription as c, permissionDeniedResult as d, toCallToolError as f, createMcpServer as g, buildRequestContext as h, isZodTypeSchema as i, invokeController as l, toCallToolSuccess as m, mcpPlugin as n, fieldRulesToZod as o, toCallToolResult as p, resourceToTools as r, defaultCrudDescription as s, filterResourcesForMcp as t, mcpHandlerAdapter as u };