@classytic/arc 2.41.1 → 2.44.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 (248) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-DZWDsaPa.mjs} +22 -17
  3. package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-yjEgn7Cm.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-DJxHUpE4.mjs} +44 -13
  6. package/dist/acquire-BozJ-9yt.mjs +35 -0
  7. package/dist/actionPermissions-5N04zBL4.mjs +772 -0
  8. package/dist/audit/index.d.mts +46 -8
  9. package/dist/audit/index.mjs +48 -26
  10. package/dist/auth/audit.d.mts +8 -8
  11. package/dist/auth/audit.mjs +1 -1
  12. package/dist/auth/index.d.mts +46 -16
  13. package/dist/auth/index.mjs +134 -156
  14. package/dist/auth/redis-session.d.mts +4 -5
  15. package/dist/auth/redis-session.mjs +1 -1
  16. package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-wX3SA9w_.mjs} +7 -102
  17. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  18. package/dist/boundedMap-DWiKW7lP.mjs +57 -0
  19. package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-Bswu_vNb.mjs} +4 -4
  20. package/dist/cache/index.d.mts +31 -43
  21. package/dist/cache/index.mjs +4 -4
  22. package/dist/{caching-DbCBxlgw.mjs → caching-Drp68DIS.mjs} +3 -3
  23. package/dist/claimed-DYvQoZLv.mjs +8 -0
  24. package/dist/cleanup/index.d.mts +18 -22
  25. package/dist/cleanup/index.mjs +27 -73
  26. package/dist/cli/commands/describe.d.mts +7 -5
  27. package/dist/cli/commands/describe.mjs +7 -4
  28. package/dist/cli/commands/docs.d.mts +2 -3
  29. package/dist/cli/commands/docs.mjs +3 -3
  30. package/dist/cli/commands/doctor.d.mts +2 -3
  31. package/dist/cli/commands/doctor.mjs +1 -1
  32. package/dist/cli/commands/generate.mjs +2 -3
  33. package/dist/cli/commands/init.mjs +2 -3
  34. package/dist/cli/commands/introspect.d.mts +2 -3
  35. package/dist/cli/commands/introspect.mjs +2 -2
  36. package/dist/cli/index.mjs +3 -3
  37. package/dist/{constants-D5Ubn1fe.mjs → constants-B2QHb6u6.mjs} +4 -2
  38. package/dist/context/index.d.mts +11 -14
  39. package/dist/context/index.mjs +18 -38
  40. package/dist/core/index.d.mts +3 -3
  41. package/dist/core/index.mjs +8 -8
  42. package/dist/{core-5lazhva9.mjs → core-COcqo0Cp.mjs} +15 -5
  43. package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-zG13rpPJ.mjs} +97 -39
  44. package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-CWRz96Vr.mjs} +11 -8
  45. package/dist/{preloadResources-f3boenVZ.mjs → createApp-BGLYtErt.mjs} +305 -394
  46. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-DSztofa9.d.mts} +1 -1
  47. package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation--3xguLkk.mjs} +1 -1
  48. package/dist/{defineResource-DxkMTjRn.mjs → defineResource-gUp9ug66.mjs} +118 -1024
  49. package/dist/discovery/index.d.mts +6 -7
  50. package/dist/discovery/index.mjs +1 -1
  51. package/dist/docs/index.d.mts +11 -5
  52. package/dist/docs/index.mjs +4 -4
  53. package/dist/{elevation-CkvG0LG2.mjs → elevation-BG4gnMsJ.mjs} +41 -12
  54. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  55. package/dist/encryption/index.d.mts +5 -5
  56. package/dist/encryption/index.mjs +3 -3
  57. package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-COHw3N1b.mjs} +26 -5
  58. package/dist/{errors-BH7YU_9O.mjs → errors-CW4anJBy.mjs} +58 -23
  59. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  60. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  61. package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-BweHAVws.mjs} +82 -21
  62. package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-N4-omQFR.mjs} +151 -17
  63. package/dist/events/index.d.mts +43 -18
  64. package/dist/events/index.mjs +49 -53
  65. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  66. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  67. package/dist/events/transports/redis.d.mts +5 -6
  68. package/dist/events/transports/redis.mjs +2 -2
  69. package/dist/factory/index.d.mts +42 -27
  70. package/dist/factory/index.mjs +213 -32
  71. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DRlCIWE9.d.mts} +11 -25
  72. package/dist/{fields-BHrkoF5y.mjs → fields-A5crhsJb.mjs} +1 -1
  73. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  74. package/dist/filter-merge-DGM-GQ1J.mjs +99 -0
  75. package/dist/{generate-BLRf2m3y.mjs → generate-BXKo2Gc8.mjs} +2 -2
  76. package/dist/{headers-BIXWKrzQ.mjs → headers-DKZQKL-Z.mjs} +2 -2
  77. package/dist/health-BYAwA6YS.mjs +22 -0
  78. package/dist/{health-ZRHeDHXH.mjs → health-CrqUp3sz.mjs} +115 -55
  79. package/dist/hooks/index.d.mts +2 -2
  80. package/dist/hooks/index.mjs +2 -3
  81. package/dist/idempotency/index.d.mts +36 -10
  82. package/dist/idempotency/index.mjs +230 -111
  83. package/dist/idempotency/redis.d.mts +1 -1
  84. package/dist/idempotency/redis.mjs +77 -12
  85. package/dist/{index-D4u76eQW.d.mts → index-Beg3aNnK.d.mts} +23 -101
  86. package/dist/{preloadResources-CjS2OfpU.d.mts → index-BkV55WlV.d.mts} +37 -38
  87. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  88. package/dist/index-CUJvoyw3.d.mts +510 -0
  89. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  90. package/dist/{index-DHbyPo5j.d.mts → index-WH7xgvxS.d.mts} +304 -254
  91. package/dist/index.d.mts +9 -9
  92. package/dist/index.mjs +11 -11
  93. package/dist/{init-CpLkjU18.mjs → init-BPzK5Lnr.mjs} +1 -1
  94. package/dist/integrations/event-gateway.d.mts +11 -7
  95. package/dist/integrations/event-gateway.mjs +4 -4
  96. package/dist/integrations/index.d.mts +3 -3
  97. package/dist/integrations/index.mjs +1 -1
  98. package/dist/integrations/jobs/index.d.mts +1 -1
  99. package/dist/integrations/jobs/index.mjs +18 -5
  100. package/dist/integrations/mcp/index.d.mts +102 -89
  101. package/dist/integrations/mcp/index.mjs +19 -4
  102. package/dist/integrations/mcp/testing.d.mts +8 -9
  103. package/dist/integrations/mcp/testing.mjs +2 -2
  104. package/dist/integrations/streamline.d.mts +85 -144
  105. package/dist/integrations/streamline.mjs +131 -55
  106. package/dist/integrations/webhooks.d.mts +83 -143
  107. package/dist/integrations/webhooks.mjs +36 -80
  108. package/dist/integrations/websocket-pushref-redis.d.mts +5 -6
  109. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  110. package/dist/integrations/websocket-redis.d.mts +5 -6
  111. package/dist/integrations/websocket-redis.mjs +1 -1
  112. package/dist/integrations/websocket.d.mts +3 -3
  113. package/dist/integrations/websocket.mjs +2 -2
  114. package/dist/{interface-DEWNOMza.d.mts → interface-JJzwH4Ro.d.mts} +34 -2
  115. package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-YIq-vGLv.mjs} +2 -2
  116. package/dist/{keys-cpZCtcIE.mjs → keys-CpoiZ8hU.mjs} +7 -18
  117. package/dist/{loadResources-BPfWiI5s.mjs → loadResources-6jO52-RR.mjs} +3 -3
  118. package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-MP8xEppu.mjs} +2 -2
  119. package/dist/logger/index.d.mts +9 -17
  120. package/dist/logger/index.mjs +2 -153
  121. package/dist/logger-D4V1zGLI.mjs +160 -0
  122. package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-srPc7ItN.mjs} +446 -170
  123. package/dist/{memory-Iotz0cCx.mjs → memory-CTa5pOCW.mjs} +36 -34
  124. package/dist/{metrics-NMsmpRmY.mjs → metrics-DsZrvnpB.mjs} +39 -72
  125. package/dist/middleware/index.d.mts +5 -5
  126. package/dist/middleware/index.mjs +3 -3
  127. package/dist/migrations/index.d.mts +13 -13
  128. package/dist/migrations/index.mjs +2 -2
  129. package/dist/{module-DpLJG2_N.mjs → module--L0iBIZz.mjs} +8 -8
  130. package/dist/mongodb-memory-server-CHhYSq5y.mjs +35539 -0
  131. package/dist/mongoose-CYJCLyi-.mjs +67451 -0
  132. package/dist/multiTenant-CAtYlW6H.d.mts +104 -0
  133. package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-Cc7eZ9cq.mjs} +46 -7
  134. package/dist/node-D8UZv_8c.mjs +41236 -0
  135. package/dist/{openapi-CmFf3yq_.mjs → openapi-CMegfsm8.mjs} +48 -10
  136. package/dist/outbox-admin/index.d.mts +5 -5
  137. package/dist/outbox-admin/index.mjs +6 -18
  138. package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Ryk270jd.mjs} +203 -33
  139. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  140. package/dist/permissions/index.d.mts +5 -4
  141. package/dist/permissions/index.mjs +8 -6
  142. package/dist/{permissions-D8TBH44E.mjs → permissions-C5ijIz5Q.mjs} +149 -33
  143. package/dist/{pipe-FMWNheSO.mjs → pipe-D81zMtWs.mjs} +2 -2
  144. package/dist/pipeline/index.d.mts +7 -7
  145. package/dist/pipeline/index.mjs +2 -2
  146. package/dist/plan-CJGMKoui.mjs +565 -0
  147. package/dist/{BaseController-CYEwjyjN.mjs → plugin-DALd9dcF.mjs} +1269 -346
  148. package/dist/plugins/index.d.mts +17 -14
  149. package/dist/plugins/index.mjs +44 -22
  150. package/dist/plugins/response-cache.d.mts +5 -6
  151. package/dist/plugins/response-cache.mjs +8 -14
  152. package/dist/plugins/tracing-entry.mjs +2 -2
  153. package/dist/{pluralize-CXWZIzTv.mjs → pluralize-COUNYo3d.mjs} +1 -1
  154. package/dist/presets/filesUpload.d.mts +8 -8
  155. package/dist/presets/filesUpload.mjs +5 -5
  156. package/dist/presets/index.d.mts +35 -40
  157. package/dist/presets/index.mjs +4 -4
  158. package/dist/presets/multiTenant.d.mts +2 -107
  159. package/dist/presets/multiTenant.mjs +22 -162
  160. package/dist/presets/search.d.mts +9 -10
  161. package/dist/presets/search.mjs +2 -2
  162. package/dist/{presets-ZJSPgmT2.mjs → presets-CONf2Qem.mjs} +99 -107
  163. package/dist/{projection-BzIgzoKZ.mjs → projection-wFoLqdss.mjs} +2 -2
  164. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  165. package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-BATECpyh.mjs} +12 -14
  166. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  167. package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-DVEZ_UpV.mjs} +1 -1
  168. package/dist/{realtime-8tIQWZz4.mjs → realtime-xyubMKSB.mjs} +25 -21
  169. package/dist/{redis-Bzi1y3F9.d.mts → redis-BDgv9Glw.d.mts} +17 -24
  170. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  171. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  172. package/dist/registry/index.d.mts +15 -10
  173. package/dist/registry/index.mjs +4 -4
  174. package/dist/{registry-DNkgGQcK.mjs → registry-Bdzg-hdm.mjs} +13 -10
  175. package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-BFJxGPuh.mjs} +1 -1
  176. package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-Bnc6lJyy.mjs} +2 -2
  177. package/dist/{requestContext-cDkAUr-P.mjs → requestContext-C10UeFe5.mjs} +1 -1
  178. package/dist/requirement-Bu50QALH.mjs +35 -0
  179. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  180. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  181. package/dist/resourceRateLimit-urWPJxrt.mjs +180 -0
  182. package/dist/rolldown-runtime-MYrLNGnq.mjs +45 -0
  183. package/dist/routeValidator-DHtpzrzk.mjs +70 -0
  184. package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-DIMKMV2G.mjs} +1 -1
  185. package/dist/{schedules-j6uXXAeJ.mjs → schedules-CKTs5ys8.mjs} +46 -37
  186. package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BVjkQ5DD.mjs} +2 -2
  187. package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-BA0Hb21Y.mjs} +2 -2
  188. package/dist/schemas/index.d.mts +20 -10
  189. package/dist/schemas/index.mjs +8 -3
  190. package/dist/scim/index.d.mts +13 -13
  191. package/dist/scim/index.mjs +2 -2
  192. package/dist/scope/index.d.mts +4 -4
  193. package/dist/scope/index.mjs +6 -6
  194. package/dist/{scope-BUi1xV1I.mjs → scope-DINaj7Ax.mjs} +4 -4
  195. package/dist/shutdown-LpLMH8T1.mjs +59 -0
  196. package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-DbwIM1f1.mjs} +1 -1
  197. package/dist/{sse-BeqVKQKu.mjs → sse-BzJFPX_1.mjs} +11 -15
  198. package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-DQChzGTs.mjs} +2 -2
  199. package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-Coj9RhOZ.mjs} +1 -1
  200. package/dist/{streaming-IcRFjWQU.mjs → streaming-JzB5CAga.mjs} +1 -1
  201. package/dist/subscriberIdentity-DeyppIif.mjs +28 -0
  202. package/dist/tenancy-DxV8OuX2.mjs +43 -0
  203. package/dist/tenantPreparation-CBvvAcAj.mjs +67 -0
  204. package/dist/testing/cleanupStoreContract.d.mts +4 -5
  205. package/dist/testing/cleanupStoreContract.mjs +1 -1
  206. package/dist/testing/index.d.mts +215 -197
  207. package/dist/testing/index.mjs +289 -214
  208. package/dist/testing/outboxStoreContract.d.mts +4 -5
  209. package/dist/testing/outboxStoreContract.mjs +137 -2
  210. package/dist/testing/requiredPorts.d.mts +16 -0
  211. package/dist/testing/requiredPorts.mjs +65 -0
  212. package/dist/testing/storageContract.d.mts +4 -5
  213. package/dist/testing/storageContract.mjs +1 -1
  214. package/dist/timing-CntavTlR.mjs +46 -0
  215. package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-orvnGu4F.mjs} +1 -1
  216. package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-kEaP_WmO.mjs} +1 -1
  217. package/dist/types/index.d.mts +4 -4
  218. package/dist/types/index.mjs +1 -1
  219. package/dist/types/storage.mjs +1 -1
  220. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  221. package/dist/{types-CWQn9-IC.d.mts → types-D3Ixsh00.d.mts} +22 -9
  222. package/dist/{types-CgenB1gv.mjs → types-fvO6Ovzx.mjs} +4 -4
  223. package/dist/usage/index.d.mts +5 -6
  224. package/dist/usage/index.mjs +5 -5
  225. package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-C76zD5pa.mjs} +1 -1
  226. package/dist/utils/index.d.mts +125 -264
  227. package/dist/utils/index.mjs +13 -12
  228. package/dist/{utils-CxcctrAh.mjs → utils-BpkVZ8rB.mjs} +315 -404
  229. package/dist/{validate-DrYWnOzI.mjs → validate-CWp-lsrm.mjs} +6 -14
  230. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  231. package/dist/{versioning-DKkT4hNJ.mjs → versioning-CHhL9P3o.mjs} +4 -4
  232. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  233. package/dist/{workScope-CxKqoIFn.mjs → workScope-n9a7d_lZ.mjs} +2 -2
  234. package/package.json +57 -59
  235. package/skills/arc/SKILL.md +13 -4
  236. package/skills/arc/references/auth.md +8 -5
  237. package/skills/arc/references/integrations.md +1 -1
  238. package/skills/arc/references/mcp.md +8 -6
  239. package/skills/arc/references/multi-tenancy.md +19 -14
  240. package/skills/arc/references/production.md +27 -10
  241. package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
  242. package/dist/index-CFfLKMH4.d.mts +0 -290
  243. package/dist/interface-YzIbF7Ii.mjs +0 -17
  244. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  245. package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
  246. package/dist/rolldown-runtime-eysyzFAF.mjs +0 -15
  247. package/dist/sync/changelog.d.mts +0 -3
  248. package/dist/sync/changelog.mjs +0 -3
@@ -0,0 +1,772 @@
1
+ /*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
2
+ import { f as createError } from "./errors-CW4anJBy.mjs";
3
+ import { n as getUserRoles } from "./userHelpers-C76zD5pa.mjs";
4
+ import { b as isElevated, n as PUBLIC_SCOPE, u as getRequestScope, x as isMember } from "./types-fvO6Ovzx.mjs";
5
+ import { a as resolveEffectiveRoles, r as deriveHiddenFieldPermissions, t as applyFieldReadPermissions } from "./fields-A5crhsJb.mjs";
6
+ import { t as buildRequestScopeProjection } from "./projection-wFoLqdss.mjs";
7
+ import { n as requestContext } from "./requestContext-C10UeFe5.mjs";
8
+ import { n as isTenantPreparation } from "./tenantPreparation-CBvvAcAj.mjs";
9
+ import { n as evaluateAndApplyPermission } from "./authorizationDecision-wX3SA9w_.mjs";
10
+ import { t as executePipeline } from "./pipe-D81zMtWs.mjs";
11
+ import { n as idempotencyConfigProblem, t as declaresIdempotencyFence } from "./requirement-Bu50QALH.mjs";
12
+ import { i as resolveResourceRateLimiter, r as recordUnenforcedRateLimit } from "./resourceRateLimit-urWPJxrt.mjs";
13
+ import { n as arcRouteValidatorCompiler } from "./routeValidator-DHtpzrzk.mjs";
14
+ import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
15
+ //#region src/core/fastifyAdapter.ts
16
+ /** Type guard for Mongoose-like documents with toObject() */
17
+ function isMongooseDoc(obj) {
18
+ return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
19
+ }
20
+ /** Apply an include/exclude field mask to a single object. */
21
+ function applyFieldMaskToObject(obj, fieldMask) {
22
+ if (!obj || typeof obj !== "object") return obj;
23
+ const plain = isMongooseDoc(obj) ? obj.toObject() : obj;
24
+ const { include, exclude } = fieldMask;
25
+ if (include && include.length > 0) {
26
+ const filtered = {};
27
+ for (const field of include) if (field in plain) filtered[field] = plain[field];
28
+ return filtered;
29
+ }
30
+ if (exclude && exclude.length > 0) {
31
+ const filtered = { ...plain };
32
+ for (const field of exclude) delete filtered[field];
33
+ return filtered;
34
+ }
35
+ return plain;
36
+ }
37
+ /** Apply a field mask to an object or array of objects. */
38
+ function applyFieldMask(data, fieldMask) {
39
+ if (!fieldMask) return data;
40
+ if (Array.isArray(data)) return data.map((item) => applyFieldMaskToObject(item, fieldMask));
41
+ if (data && typeof data === "object") return applyFieldMaskToObject(data, fieldMask);
42
+ return data;
43
+ }
44
+ /**
45
+ * Project `FastifyRequest` into the `IRequestContext` controllers receive,
46
+ * lifting scope, context, and a server accessor onto a flat, typed surface.
47
+ */
48
+ function createRequestContext(req) {
49
+ const reqWithExtras = req;
50
+ const requestContext = reqWithExtras.context ?? {};
51
+ const srv = req.server;
52
+ const serverAccessor = {
53
+ events: srv && "events" in srv ? srv.events : void 0,
54
+ audit: srv && "audit" in srv ? srv.audit : void 0,
55
+ queryCache: srv && "queryCache" in srv ? srv.queryCache : void 0,
56
+ log: req.log
57
+ };
58
+ const rawScope = reqWithExtras.scope;
59
+ const scopeProjection = buildRequestScopeProjection(rawScope);
60
+ return {
61
+ query: reqWithExtras.query ?? {},
62
+ body: reqWithExtras.body ?? {},
63
+ params: reqWithExtras.params ?? {},
64
+ headers: reqWithExtras.headers,
65
+ user: reqWithExtras.user ? (() => {
66
+ const user = reqWithExtras.user;
67
+ const rawId = user._id ?? user.id;
68
+ const normalizedId = rawId ? String(rawId) : void 0;
69
+ return {
70
+ ...user,
71
+ id: normalizedId,
72
+ _id: normalizedId
73
+ };
74
+ })() : null,
75
+ context: requestContext,
76
+ scope: scopeProjection,
77
+ metadata: {
78
+ ...reqWithExtras.context,
79
+ arc: reqWithExtras.arc,
80
+ _scope: rawScope,
81
+ _ownershipCheck: reqWithExtras._ownershipCheck,
82
+ _crossTenantRead: reqWithExtras._crossTenantRead,
83
+ _tenantFields: reqWithExtras._tenantFields,
84
+ _policyFilters: reqWithExtras._policyFilters ?? {},
85
+ log: reqWithExtras.log
86
+ },
87
+ server: serverAccessor
88
+ };
89
+ }
90
+ /**
91
+ * Typed auth context from an IRequestContext, for controller overrides.
92
+ * For org scope, use `getControllerScope(req)`.
93
+ */
94
+ function getControllerContext(req) {
95
+ return req.context ?? req.metadata ?? {};
96
+ }
97
+ /** The RequestScope set by auth adapters (`PUBLIC_SCOPE` when absent). */
98
+ function getControllerScope(req) {
99
+ return req.metadata?._scope ?? PUBLIC_SCOPE;
100
+ }
101
+ /**
102
+ * Per-field capability metadata for the current user. Only restricted fields
103
+ * appear; clients default the rest to `{ readable: true, writable: true }`.
104
+ */
105
+ function computeFieldCapabilities(fieldPerms, effectiveRoles) {
106
+ const caps = {};
107
+ for (const [field, perm] of Object.entries(fieldPerms)) {
108
+ let readable = true;
109
+ let writable = true;
110
+ switch (perm._type) {
111
+ case "hidden":
112
+ readable = false;
113
+ writable = false;
114
+ break;
115
+ case "visibleTo":
116
+ readable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
117
+ break;
118
+ case "writableBy": writable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
119
+ }
120
+ caps[field] = {
121
+ readable,
122
+ writable
123
+ };
124
+ }
125
+ return caps;
126
+ }
127
+ /**
128
+ * Send a controller's `IControllerResponse` envelope (status, meta, headers)
129
+ * through Fastify's reply, applying field masks and field read permissions.
130
+ *
131
+ * A bare return (no `data` key) is wrapped as `{ data, status: 200 }` so
132
+ * `fields:` permissions still apply — otherwise restricted fields would leak.
133
+ * The envelope remains the canonical contract.
134
+ */
135
+ function sendControllerResponse(reply, response, request) {
136
+ if (response === null || typeof response !== "object" || !Object.hasOwn(response, "data")) response = {
137
+ data: response,
138
+ status: 200
139
+ };
140
+ const reqWithExtras = request;
141
+ const fieldMaskConfig = reqWithExtras?.fieldMask;
142
+ const arcMeta = reqWithExtras?.arc;
143
+ const scope = reqWithExtras?.scope ?? PUBLIC_SCOPE;
144
+ const fieldPerms = isElevated(scope) ? void 0 : arcMeta?.fields;
145
+ const effectiveRoles = fieldPerms ? resolveEffectiveRoles(getUserRoles(reqWithExtras?.user), isMember(scope) ? scope.orgRoles : []) : [];
146
+ const fieldCaps = fieldPerms ? computeFieldCapabilities(fieldPerms, effectiveRoles) : void 0;
147
+ const hasFieldRestrictions = !!(fieldMaskConfig || fieldPerms);
148
+ /** Apply both field mask and field-level permissions to a data item */
149
+ const applyPermissions = (data) => {
150
+ let result = fieldMaskConfig ? applyFieldMask(data, fieldMaskConfig) : data;
151
+ if (fieldPerms && result && typeof result === "object") {
152
+ if (Array.isArray(result)) result = result.map((item) => applyFieldReadPermissions(item, fieldPerms, effectiveRoles));
153
+ else result = applyFieldReadPermissions(result, fieldPerms, effectiveRoles);
154
+ }
155
+ return result;
156
+ };
157
+ if (response.headers) for (const [key, value] of Object.entries(response.headers)) reply.header(key, value);
158
+ if (response.data && typeof response.data === "object" && Array.isArray(response.data.data)) {
159
+ const payload = response.data;
160
+ const filteredRows = hasFieldRestrictions ? applyPermissions(payload.data) : payload.data;
161
+ let canonical;
162
+ if (isPaginatedResult(payload)) canonical = toCanonicalList({
163
+ ...payload,
164
+ data: filteredRows
165
+ });
166
+ else if (typeof payload.page === "number" && typeof payload.limit === "number" && typeof payload.total === "number") canonical = toCanonicalList({
167
+ method: "offset",
168
+ data: filteredRows,
169
+ page: payload.page,
170
+ limit: payload.limit,
171
+ total: payload.total,
172
+ pages: payload.pages ?? Math.ceil(payload.total / Math.max(1, payload.limit)),
173
+ hasNext: payload.hasNext ?? false,
174
+ hasPrev: payload.hasPrev ?? false
175
+ });
176
+ else canonical = toCanonicalList(filteredRows);
177
+ reply.code(response.status ?? 200).send({
178
+ ...canonical,
179
+ ...response.meta ?? {},
180
+ ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
181
+ });
182
+ return;
183
+ }
184
+ const rawData = hasFieldRestrictions ? applyPermissions(response.data) : response.data;
185
+ const filteredData = rawData !== null && typeof rawData === "object" && typeof rawData.toJSON === "function" ? rawData.toJSON() : rawData;
186
+ const status = response.status ?? 200;
187
+ if (filteredData !== null && typeof filteredData === "object" && !Array.isArray(filteredData) && (response.meta || fieldCaps)) {
188
+ reply.code(status).send({
189
+ ...filteredData,
190
+ ...response.meta ?? {},
191
+ ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
192
+ });
193
+ return;
194
+ }
195
+ reply.code(status).send(filteredData);
196
+ }
197
+ /**
198
+ * Bridge an `IController` method into a Fastify route handler, sending the
199
+ * envelope through `sendControllerResponse`.
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const controller = new BaseController(repository);
204
+ * const listHandler = createFastifyHandler(controller.list.bind(controller));
205
+ * fastify.get('/products', listHandler);
206
+ * ```
207
+ */
208
+ function createFastifyHandler(controllerMethod) {
209
+ return async (req, reply) => {
210
+ sendControllerResponse(reply, await controllerMethod(createRequestContext(req)), req);
211
+ return reply;
212
+ };
213
+ }
214
+ /**
215
+ * Fastify handlers for each CRUD method of an IController.
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * const controller = new BaseController(repository);
220
+ * const handlers = createCrudHandlers(controller);
221
+ *
222
+ * fastify.get('/', handlers.list);
223
+ * fastify.get('/:id', handlers.get);
224
+ * fastify.post('/', handlers.create);
225
+ * fastify.patch('/:id', handlers.update);
226
+ * fastify.delete('/:id', handlers.delete);
227
+ * ```
228
+ */
229
+ function createCrudHandlers(controller) {
230
+ return {
231
+ list: createFastifyHandler(controller.list.bind(controller)),
232
+ get: createFastifyHandler(controller.get.bind(controller)),
233
+ create: createFastifyHandler(controller.create.bind(controller)),
234
+ update: createFastifyHandler(controller.update.bind(controller)),
235
+ delete: createFastifyHandler(controller.delete.bind(controller))
236
+ };
237
+ }
238
+ //#endregion
239
+ //#region src/core/middlewares/arcDecorator.ts
240
+ /**
241
+ * Build the `arcDecorator` preHandler for a resource.
242
+ *
243
+ * The decorator is a closure over frozen metadata — allocated once per
244
+ * resource and shared across every request. Stamps `req.arc` with the
245
+ * resource's field permissions, hooks, events bus, and schema options
246
+ * so `sendControllerResponse`, `BaseController.run*`, and custom
247
+ * middleware can read a consistent view.
248
+ *
249
+ * Also populates `requestContext.resourceName` for async-context access
250
+ * in code paths that can't reach `req.arc` directly (e.g. detached logger
251
+ * formatters).
252
+ */
253
+ function buildArcDecorator(meta) {
254
+ const frozen = Object.freeze({
255
+ ...meta,
256
+ fields: deriveHiddenFieldPermissions(meta.schemaOptions, meta.fields)
257
+ });
258
+ return async (req, _reply) => {
259
+ req.arc = frozen;
260
+ const store = requestContext.get();
261
+ if (store) store.resourceName = frozen.resourceName;
262
+ };
263
+ }
264
+ //#endregion
265
+ //#region src/core/middlewares/auth.ts
266
+ /**
267
+ * A permission requires authentication unless it carries the `_isPublic`
268
+ * marker set by `allowPublic()`. Absence of a permission is treated as
269
+ * public (no auth) — matches historical CRUD behaviour.
270
+ */
271
+ function requiresAuthentication(permission) {
272
+ if (!permission) return false;
273
+ return !permission._isPublic;
274
+ }
275
+ /**
276
+ * Pick the right Fastify auth decorator for a single-permission route:
277
+ * - protected route → `fastify.authenticate` (401 on missing token)
278
+ * - public route → `fastify.optionalAuthenticate` (parses token if present)
279
+ *
280
+ * Public routes still get optional auth so downstream multi-tenant filters
281
+ * can narrow queries when a Bearer token IS supplied.
282
+ */
283
+ function buildAuthMiddleware(fastify, permission) {
284
+ if (requiresAuthentication(permission)) return fastify.authenticate ?? null;
285
+ return fastify.optionalAuthenticate ?? null;
286
+ }
287
+ /**
288
+ * Pick the right auth decorator for a multi-permission route (Action router).
289
+ *
290
+ * The input is the array of resolved per-action permissions — one slot per
291
+ * action, in registration order, already flattened against `globalAuth`
292
+ * fallback by the caller (`actionPermissions[name] ?? globalAuth`). A slot
293
+ * may be `undefined` when the action has no per-action check AND no
294
+ * `globalAuth` fallback — that is "public by omission" and must be honored
295
+ * here the same way `buildActionPermissionMw` honors it (by skipping the
296
+ * permission evaluation entirely). If we filtered undefineds out at this
297
+ * layer, a mixed endpoint like `{ ping: undefined, promote: requireRoles(...) }`
298
+ * would collapse to "all protected" and 401 the public `ping` action at the
299
+ * auth layer before the permission prehandler could let it through.
300
+ *
301
+ * Rules:
302
+ * - ALL public (explicit allowPublic OR omission) → `optionalAuthenticate`
303
+ * - ALL protected → `authenticate` (fail-fast)
304
+ * - MIXED → `optionalAuthenticate`
305
+ * (parse token if present; per-action check fails-closed when user=null)
306
+ *
307
+ * The mixed case was previously handled by an in-handler
308
+ * `fastify.authenticate()` call that bypassed the preHandler chain; this
309
+ * helper moves that logic back into the preHandler stack so the request
310
+ * lifecycle is consistent across router types.
311
+ */
312
+ function buildAuthMiddlewareForPermissions(fastify, permissions) {
313
+ if (permissions.length === 0) return fastify.optionalAuthenticate ?? null;
314
+ const hasProtected = permissions.some((p) => requiresAuthentication(p));
315
+ const hasPublic = permissions.some((p) => p && p._isPublic === true) || permissions.some((p) => !p);
316
+ if (hasProtected && !hasPublic) return fastify.authenticate ?? null;
317
+ return fastify.optionalAuthenticate ?? null;
318
+ }
319
+ //#endregion
320
+ //#region src/core/middlewares/chain.ts
321
+ const validationGates = /* @__PURE__ */ new WeakSet();
322
+ const replayBoundaries = /* @__PURE__ */ new WeakSet();
323
+ /** Rate limiting belongs before a response cache or idempotency replay can return. */
324
+ function isReplayBoundary(hook) {
325
+ return typeof hook === "function" && replayBoundaries.has(hook);
326
+ }
327
+ /** True iff `hook` is a validation gate built by `buildRouteHooks`. */
328
+ function isValidationGate(hook) {
329
+ return typeof hook === "function" && validationGates.has(hook);
330
+ }
331
+ /**
332
+ * Build the gate that surfaces a parked validation error. `format` may rethrow it in a
333
+ * route-specific shape; whatever it does, a still-parked error is rethrown verbatim, so
334
+ * invalid input never reaches a later hook or the handler.
335
+ */
336
+ function buildValidationGate(format) {
337
+ const gate = async function arcValidationGate(req, reply) {
338
+ const parked = req.validationError;
339
+ if (!parked) return;
340
+ if (format) await format.call(this, req, reply);
341
+ if (reply.sent) return;
342
+ throw parked;
343
+ };
344
+ validationGates.add(gate);
345
+ return gate;
346
+ }
347
+ /**
348
+ * Compose the route-level `onRequest` + `preHandler` arrays in the canonical
349
+ * order. Every null/undefined entry is dropped. Keeps the three routers from
350
+ * accidentally ordering the same ingredients differently (regression risk:
351
+ * cache before auth → user-scoped cache keys leak across users; auth after
352
+ * body parse → anonymous schema probing).
353
+ *
354
+ * onRequest constraints (Fastify lifecycle): `request.body` is undefined —
355
+ * everything placed there must be header/cookie/param/query-driven. arc's
356
+ * `authenticate` / `optionalAuthenticate` qualify by contract (Bearer
357
+ * header, session cookie, api-key header); `preAuth` handlers exist
358
+ * precisely to promote query tokens into headers (SSE `?token=`); the arc
359
+ * decorator stamps static route metadata.
360
+ */
361
+ function buildRouteHooks(parts) {
362
+ const gated = Boolean(parts.permissionMw);
363
+ const customMws = parts.customMws ?? [];
364
+ const tenancy = customMws.filter(isTenantPreparation);
365
+ const remaining = customMws.filter((middleware) => !isTenantPreparation(middleware));
366
+ if (parts.pluginMw) replayBoundaries.add(parts.pluginMw);
367
+ return {
368
+ onRequest: [
369
+ ...parts.preAuth ?? [],
370
+ parts.arcDecorator,
371
+ parts.authMw ?? null
372
+ ].filter(Boolean),
373
+ preHandler: [
374
+ gated ? parts.prePermission ?? null : null,
375
+ parts.permissionMw ?? null,
376
+ gated ? buildValidationGate(parts.validationFormatter) : null,
377
+ ...tenancy,
378
+ ...parts.routeGuards ?? [],
379
+ parts.pluginMw ?? null,
380
+ ...remaining
381
+ ].filter(Boolean),
382
+ attachValidation: gated
383
+ };
384
+ }
385
+ /**
386
+ * Spread helper — turns `RouteHookChains` into the two Fastify route-option
387
+ * fields, omitting empty arrays so routes without middleware stay
388
+ * option-free (Fastify skips absent hooks faster than empty arrays).
389
+ */
390
+ function routeHookOptions(hooks) {
391
+ return {
392
+ ...hooks.attachValidation ? { attachValidation: true } : {},
393
+ ...hooks.onRequest.length > 0 ? { onRequest: hooks.onRequest } : {},
394
+ ...hooks.preHandler.length > 0 ? { preHandler: hooks.preHandler } : {}
395
+ };
396
+ }
397
+ /**
398
+ * `RouteDefinition.preHandler` accepts two shapes:
399
+ *
400
+ * 1. **Array form** — `RouteHandlerMethod[]`. Used directly.
401
+ * 2. **Factory form** — `(fastify) => RouteHandlerMethod[]`. Called once at
402
+ * route-registration time with the Fastify instance, so handlers can
403
+ * capture decorators (`fastify.authenticate`, `fastify.events`, etc.)
404
+ * that aren't on the request.
405
+ *
406
+ * The two forms are equally idiomatic, but the discrimination is by
407
+ * `typeof preHandler === "function"`. Single-function shapes such as
408
+ * `multipartBody({...})` (a `RouteHandlerMethod`) **structurally satisfy
409
+ * the factory branch** at the call site, then fail with a cryptic
410
+ * `Cannot read properties of undefined (reading 'content-type')` once the
411
+ * handler runs with `fastify` in the request slot.
412
+ *
413
+ * This resolver:
414
+ * 1. Distinguishes the two valid shapes.
415
+ * 2. Validates the factory's RETURN — must be an array of functions.
416
+ * 3. Throws an actionable error pointing at the route + the fix when
417
+ * a single `RouteHandlerMethod` was passed instead of an array, OR
418
+ * when a factory returned the wrong shape.
419
+ *
420
+ * The error message names the route (`{method} {path}`) and the
421
+ * canonical fix (`preHandler: [yourHandler]`) so the failure mode is
422
+ * obvious instead of debug-archaeology.
423
+ *
424
+ * @param preHandler The `route.preHandler` value (any of the valid shapes
425
+ * plus the common bare-handler mistake).
426
+ * @param fastify Passed to factory-form preHandlers.
427
+ * @param routeId `"GET /todos/:id/attach"` (or similar) — used in the
428
+ * error message so a multi-route file points at the
429
+ * actual offender.
430
+ */
431
+ function resolveRoutePreHandlers(preHandler, fastify, routeId) {
432
+ if (preHandler === void 0 || preHandler === null) return [];
433
+ if (Array.isArray(preHandler)) return preHandler.filter((h) => typeof h === "function");
434
+ if (typeof preHandler === "function") {
435
+ let result;
436
+ try {
437
+ result = preHandler(fastify);
438
+ } catch (err) {
439
+ const msg = err instanceof Error ? err.message : String(err);
440
+ throw new TypeError(`Route ${routeId}: preHandler factory threw during route registration: ${msg}.\nIf you intended to pass a single handler (e.g. \`multipartBody({...})\`), wrap it in an array: \`preHandler: [yourHandler]\`. The factory form is \`(fastify) => RouteHandlerMethod[]\` — it must return an array.`, { cause: err instanceof Error ? err : void 0 });
441
+ }
442
+ if (result && typeof result.then === "function") result.catch(() => void 0);
443
+ if (!Array.isArray(result)) throw new TypeError(`Route ${routeId}: preHandler factory must return an array of handlers, got ${describeValue(result)}.\nCommon cause: passing a single \`RouteHandlerMethod\` (e.g. \`multipartBody({...})\`) where an array was expected. Wrap it: \`preHandler: [yourHandler]\`. The factory form \`(fastify) => RouteHandlerMethod[]\` is for cases that need the Fastify instance — e.g. \`(fastify) => [fastify.authenticate, myHandler]\`.`);
444
+ return result.filter((h) => typeof h === "function");
445
+ }
446
+ throw new TypeError(`Route ${routeId}: preHandler must be an array of handlers OR a factory \`(fastify) => RouteHandlerMethod[]\`. Got ${describeValue(preHandler)}.`);
447
+ }
448
+ function describeValue(v) {
449
+ if (v === null) return "null";
450
+ if (v === void 0) return "undefined";
451
+ if (typeof v === "function") return "a function (single handler — wrap in array)";
452
+ if (Array.isArray(v)) return `an array of length ${v.length}`;
453
+ return `${typeof v} (${JSON.stringify(v).slice(0, 80)})`;
454
+ }
455
+ //#endregion
456
+ //#region src/core/middlewares/permissions.ts
457
+ /**
458
+ * Build a PermissionContext from a Fastify request. Extracted so the CRUD
459
+ * permission middleware and the dynamic action-permission check use the same
460
+ * field layout — divergence here silently broke policy filters for actions.
461
+ */
462
+ function buildPermissionContext(req, opts) {
463
+ const reqWithExtras = req;
464
+ const params = req.params;
465
+ return {
466
+ user: reqWithExtras.user ?? null,
467
+ scope: getRequestScope(req),
468
+ request: req,
469
+ resource: opts.resource,
470
+ action: opts.action,
471
+ resourceId: opts.resourceId ?? params?.id,
472
+ params,
473
+ data: opts.data ?? req.body
474
+ };
475
+ }
476
+ /**
477
+ * Static per-route CRUD permission gate. The permission and action are known
478
+ * at route-registration time, so the gate is a plain preHandler.
479
+ *
480
+ * Actions use the dynamic counterpart `buildActionPermissionMw` — their
481
+ * permission is resolved from `body.action` at request time.
482
+ */
483
+ function buildCrudPermissionMw(permissionCheck, resourceName, action) {
484
+ if (!permissionCheck) return null;
485
+ return async (req, reply) => {
486
+ const context = buildPermissionContext(req, {
487
+ resource: resourceName,
488
+ action
489
+ });
490
+ await evaluateAndApplyPermission(permissionCheck, context, req, reply);
491
+ };
492
+ }
493
+ /**
494
+ * Dynamic per-action permission gate for the action router.
495
+ *
496
+ * Resolves the permission from `body.action` at request time and runs
497
+ * `evaluateAndApplyPermission` from the canonical `permissionMw` slot — so
498
+ * `_policyFilters` and `request.scope` are installed BEFORE `pluginMw`
499
+ * (idempotency) and `routeGuards` run. Previously this check lived inside
500
+ * the main action handler, which meant idempotency recorded unauthorized
501
+ * requests and route guards saw unfiltered scope — the very divergence
502
+ * routerShared exists to prevent.
503
+ *
504
+ * Also acts as a defensive fallback for invalid action names — the
505
+ * `oneOf` body schema normally rejects these at AJV validation, but
506
+ * hosts that disable schema validation still get a 400 here.
507
+ */
508
+ function buildActionPermissionMw(actionEnum, actionPermissions, globalAuth, resourceName) {
509
+ const enumSet = new Set(actionEnum);
510
+ const validActions = [...actionEnum];
511
+ return async (req, reply) => {
512
+ const body = req.body ?? {};
513
+ const action = body.action;
514
+ if (!action || !enumSet.has(action)) throw createError(400, `Invalid action '${action ?? ""}'. Valid actions: ${validActions.join(", ")}`, { validActions });
515
+ const permissionCheck = actionPermissions[action] ?? globalAuth;
516
+ if (!permissionCheck) return;
517
+ const { action: _discard, ...data } = body;
518
+ const params = req.params;
519
+ const context = buildPermissionContext(req, {
520
+ resource: resourceName,
521
+ action,
522
+ resourceId: params?.id,
523
+ data
524
+ });
525
+ await evaluateAndApplyPermission(permissionCheck, context, req, reply, { defaultDenialMessage: (user) => user ? `Permission denied for '${action}'` : "Authentication required" });
526
+ };
527
+ }
528
+ //#endregion
529
+ //#region src/core/middlewares/pipeline.ts
530
+ /**
531
+ * Resolve pipeline steps for a specific operation.
532
+ * Flat-array config applies to every op; map config applies per-op.
533
+ */
534
+ function resolvePipelineSteps(pipeline, operation) {
535
+ if (!pipeline) return [];
536
+ if (Array.isArray(pipeline)) return pipeline;
537
+ return pipeline[operation] ?? [];
538
+ }
539
+ /**
540
+ * Wrap a controller method (one that takes `IRequestContext` and returns
541
+ * `IControllerResponse<T>`) with pipeline execution. Used by CRUD ops and
542
+ * string-handler custom routes.
543
+ */
544
+ function buildPipelineHandler(controllerMethod, steps, operation, resourceName) {
545
+ return async (req, reply) => {
546
+ const pipeCtx = {
547
+ ...createRequestContext(req),
548
+ resource: resourceName,
549
+ operation
550
+ };
551
+ sendControllerResponse(reply, await executePipeline(steps, pipeCtx, (ctx) => controllerMethod(ctx), operation), req);
552
+ };
553
+ }
554
+ /**
555
+ * Wrap an action handler (one that takes `(id, data, req)` and returns a raw
556
+ * result) with pipeline execution. Returns a function that produces a full
557
+ * `IControllerResponse<unknown>` — the action router feeds this directly into
558
+ * `sendControllerResponse`, so field masking, custom status codes, `meta`,
559
+ * `details`, and structured error codes from pipeline interceptors flow
560
+ * through to the client unchanged.
561
+ *
562
+ * CRUD and actions now share the same parity invariant: a pipeline that
563
+ * returns `{ success: false, status: 422, error, details, meta }` reaches the
564
+ * client with all four fields intact. Previously the action path stringified
565
+ * failures into a generic `Error` and dropped everything except `statusCode`.
566
+ *
567
+ * Handler throws still bubble out — the caller's try/catch handles `onError`
568
+ * shaping and the generic `ACTION_FAILED` fallback.
569
+ */
570
+ function buildActionPipelineHandler(handler, steps, operation, resourceName) {
571
+ if (steps.length === 0) return async (id, data, req) => ({
572
+ status: 200,
573
+ data: await handler(id, data, req)
574
+ });
575
+ return async (id, data, req) => {
576
+ const pipeCtx = {
577
+ ...createRequestContext(req),
578
+ resource: resourceName,
579
+ operation
580
+ };
581
+ return executePipeline(steps, pipeCtx, async (_ctx) => ({
582
+ status: 200,
583
+ data: await handler(id, data, req)
584
+ }), operation);
585
+ };
586
+ }
587
+ //#endregion
588
+ //#region src/core/middlewares/pluginMw.ts
589
+ /**
590
+ * Pick the request-lifecycle plugin middleware for an HTTP method:
591
+ * - GET / HEAD → response cache (if present)
592
+ * - POST / PUT / PATCH / DELETE → idempotency (if present); the middleware itself decides
593
+ * whether a key is honoured (plugin `methods`, or the route's `config.idempotency`)
594
+ *
595
+ * Either field may be `null` if the corresponding plugin wasn't registered.
596
+ */
597
+ function selectPluginMw(method, mws) {
598
+ const upper = method.toUpperCase();
599
+ if (upper === "GET" || upper === "HEAD") return mws.cacheMw;
600
+ if (upper === "POST" || upper === "PUT" || upper === "PATCH" || upper === "DELETE") return mws.idempotencyMw;
601
+ return null;
602
+ }
603
+ /**
604
+ * Resolve the default cache/idempotency middlewares for a resource.
605
+ *
606
+ * Skips response-cache when the resource has QueryCache active — QueryCache
607
+ * handles caching at the controller level with SWR, so the HTTP-level
608
+ * response-cache would double-cache.
609
+ */
610
+ function resolveRouterPluginMw(fastify, resourceHasQueryCache) {
611
+ return {
612
+ cacheMw: !resourceHasQueryCache && fastify.hasDecorator("responseCache") ? fastify.responseCache.middleware : null,
613
+ idempotencyMw: fastify.hasDecorator("idempotency") ? fastify.idempotency.middleware : null
614
+ };
615
+ }
616
+ //#endregion
617
+ //#region src/core/middlewares/rateLimit.ts
618
+ /**
619
+ * Build the per-route Fastify `config` object shared by the CRUD, custom, and
620
+ * action routers. Two independent concerns ride on `routeOptions.config`:
621
+ * - `config.rateLimit` — read by `@fastify/rate-limit`.
622
+ * - `config.cors` — read by `@fastify/cors` as a per-route override.
623
+ * - `config.arcExtensions` — read by arc plugins (encryption, …) at request
624
+ * time, sourced from `defineResource({ extensions })`.
625
+ * - `config.idempotency` — read by `idempotencyPlugin` (the key fence).
626
+ *
627
+ * Returns `{}` (no `config` key) when neither is present, so routes without
628
+ * either stay config-free and the spread is a genuine no-op. Both fields are
629
+ * optional in the return type, so no cast is needed when only one is set.
630
+ */
631
+ function buildRouteConfig(rateLimitConfig, extensions, cors, idempotency) {
632
+ const hasCors = cors !== void 0;
633
+ if (!rateLimitConfig && !extensions && !hasCors && !idempotency) return {};
634
+ return { config: {
635
+ ...rateLimitConfig ?? {},
636
+ ...hasCors ? { cors } : {},
637
+ ...extensions ? { arcExtensions: extensions } : {},
638
+ ...idempotency ? { idempotency } : {}
639
+ } };
640
+ }
641
+ /**
642
+ * Build the `config` object for Fastify route options so
643
+ * @fastify/rate-limit picks up per-route overrides.
644
+ *
645
+ * - `undefined` → no override (inherits instance config)
646
+ * - `false` → explicitly disable rate limiting
647
+ * - `{ max, timeWindow }` → apply that limit
648
+ */
649
+ function buildRateLimitConfig(rateLimit) {
650
+ if (rateLimit === void 0) return void 0;
651
+ if (rateLimit === false) return { rateLimit: false };
652
+ return { rateLimit: {
653
+ max: rateLimit.max,
654
+ timeWindow: rateLimit.timeWindow
655
+ } };
656
+ }
657
+ //#endregion
658
+ //#region src/core/middlewares/registerRoute.ts
659
+ /**
660
+ * Register a route with friendly handling of `FST_ERR_DUPLICATED_ROUTE`.
661
+ *
662
+ * Boot-time `validateRouteCrudCollisions()` catches custom-route → auto-CRUD
663
+ * overlaps before Fastify ever runs, but a few cases still slip through to
664
+ * Fastify itself: two presets contributing the same `routes` entry, hosts
665
+ * mounting two resources at the same prefix, custom routes that share a
666
+ * URL with another plugin. Fastify's default message ("Method 'GET'
667
+ * already declared for route '/'") gives no hint at the resource-shaped
668
+ * fix — `disabledRoutes: ['list']` — so wrap it with one.
669
+ */
670
+ function tryRegisterRoute(fastify, opts, context) {
671
+ const ratePolicy = opts.config?.rateLimit;
672
+ if (ratePolicy && typeof ratePolicy === "object" && typeof ratePolicy.max === "number" && typeof ratePolicy.timeWindow === "string") {
673
+ const limiter = resolveResourceRateLimiter(fastify);
674
+ if (!limiter && !recordUnenforcedRateLimit(fastify, context.resourceName)) throw new Error(`[arc] Resource "${context.resourceName}" declares rateLimit; register @fastify/rate-limit before mounting the resource, or disable rate limiting for the app (createApp({ rateLimit: false })).`);
675
+ if (!limiter) opts = {
676
+ ...opts,
677
+ config: {
678
+ ...opts.config,
679
+ rateLimit: false
680
+ }
681
+ };
682
+ else {
683
+ const policy = {
684
+ max: ratePolicy.max,
685
+ timeWindow: ratePolicy.timeWindow,
686
+ bucket: context.rateLimitBucket
687
+ };
688
+ const handlers = Array.isArray(opts.preHandler) ? opts.preHandler : opts.preHandler ? [opts.preHandler] : [];
689
+ const limit = async (req, reply) => {
690
+ const decision = await limiter.consume(context.resourceName, policy, {
691
+ scope: req.scope,
692
+ ip: req.ip
693
+ });
694
+ for (const [header, value] of Object.entries(decision.headers)) reply.header(header, value);
695
+ if (!decision.allowed) await reply.code(429).send({
696
+ code: "arc.rate_limited",
697
+ message: "Too many calls to this resource — retry later.",
698
+ status: 429
699
+ });
700
+ };
701
+ const replayIndex = handlers.findIndex(isReplayBoundary);
702
+ const at = replayIndex < 0 ? handlers.length : replayIndex;
703
+ opts = {
704
+ ...opts,
705
+ config: {
706
+ ...opts.config,
707
+ rateLimit: false
708
+ },
709
+ preHandler: [
710
+ ...handlers.slice(0, at),
711
+ limit,
712
+ ...handlers.slice(at)
713
+ ]
714
+ };
715
+ }
716
+ }
717
+ const problem = idempotencyConfigProblem(opts.config);
718
+ if (problem) throw new Error(`[arc] ${String(opts.method)} ${opts.url} on resource "${context.resourceName}": config.idempotency: ${problem}.`);
719
+ if (declaresIdempotencyFence(opts.config) && !fastify.hasDecorator("idempotency")) throw new Error(`[arc] ${String(opts.method)} ${opts.url} on resource "${context.resourceName}" declares config.idempotency.required, but idempotencyPlugin is not registered — register it (enabled) before the resources, or drop the declaration.`);
720
+ if (opts.attachValidation === true) {
721
+ if (!(Array.isArray(opts.preHandler) ? opts.preHandler : [opts.preHandler]).some(isValidationGate)) throw new Error(`[arc] ${String(opts.method)} ${opts.url} on resource "${context.resourceName}" sets attachValidation without arc's validation gate — build its hooks with buildRouteHooks.`);
722
+ }
723
+ const validatorCompiler = opts.validatorCompiler ?? arcRouteValidatorCompiler(fastify);
724
+ try {
725
+ fastify.route(validatorCompiler ? {
726
+ ...opts,
727
+ validatorCompiler
728
+ } : opts);
729
+ } catch (err) {
730
+ const fastifyErr = err;
731
+ if (fastifyErr?.code !== "FST_ERR_DUPLICATED_ROUTE") throw err;
732
+ const method = opts.method;
733
+ const url = opts.url;
734
+ const opHint = context.op ? ` (op: ${context.op})` : "";
735
+ const fixHint = context.op && [
736
+ "list",
737
+ "get",
738
+ "create",
739
+ "update",
740
+ "delete"
741
+ ].includes(context.op) ? ` Suppress the auto-CRUD route with \`disabledRoutes: ['${context.op}']\`.` : " If a custom route in `routes:` collides with auto-CRUD, add `disabledRoutes: ['list' | 'get' | 'create' | 'update' | 'delete']` to `defineResource()` to suppress the matching auto-CRUD route.";
742
+ const enhanced = /* @__PURE__ */ new Error(`[arc] Duplicate route ${method} ${url} on resource "${context.resourceName}"${opHint}.${fixHint} Original: ${fastifyErr.message ?? "FST_ERR_DUPLICATED_ROUTE"}`);
743
+ enhanced.code = "FST_ERR_DUPLICATED_ROUTE";
744
+ enhanced.cause = err;
745
+ throw enhanced;
746
+ }
747
+ }
748
+ //#endregion
749
+ //#region src/core/actionPermissions.ts
750
+ /**
751
+ * Return the effective `PermissionCheck` for a single action, or `undefined`
752
+ * when the resource declares no gate at any level.
753
+ *
754
+ * Callers decide what "no gate" means:
755
+ * - HTTP: boot-time throw in `normalizeActionsToRouterConfig`.
756
+ * - MCP: tool-generation throw in `resourceToTools` (mirrors HTTP — the
757
+ * two surfaces fail closed identically so MCP can't expose an
758
+ * unauthenticated mutating tool when the HTTP plugin lifecycle hasn't
759
+ * run).
760
+ * - OpenAPI: docs advertise the endpoint as unauthenticated.
761
+ */
762
+ function resolveActionPermission(input) {
763
+ const { action, resourcePermissions, resourceActionPermissions, globalAuth } = input;
764
+ const explicit = typeof action !== "function" && action.permissions ? action.permissions : void 0;
765
+ if (explicit) return explicit;
766
+ if (resourceActionPermissions) return resourceActionPermissions;
767
+ if (globalAuth) return globalAuth;
768
+ const updateFallback = resourcePermissions?.update;
769
+ if (updateFallback) return updateFallback;
770
+ }
771
+ //#endregion
772
+ export { sendControllerResponse as C, getControllerScope as S, buildArcDecorator as _, resolveRouterPluginMw as a, createRequestContext as b, buildPipelineHandler as c, buildCrudPermissionMw as d, buildRouteHooks as f, buildAuthMiddlewareForPermissions as g, buildAuthMiddleware as h, buildRouteConfig as i, resolvePipelineSteps as l, routeHookOptions as m, tryRegisterRoute as n, selectPluginMw as o, resolveRoutePreHandlers as p, buildRateLimitConfig as r, buildActionPipelineHandler as s, resolveActionPermission as t, buildActionPermissionMw as u, createCrudHandlers as v, getControllerContext as x, createFastifyHandler as y };