@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
@@ -1,15 +1,16 @@
1
1
 
2
2
  import { P as ArcHeaders, i as RequestScope } from "./types-Ddl9OjmE.mjs";
3
- import { d as UserBase, l as PermissionCheck, t as AnyRecord } from "./base-CxrV1LmG.mjs";
4
- import { a as QueryCacheConfig } from "./QueryCache-DFA-QrU9.mjs";
5
- import "./elevation-BXCfVcNG.mjs";
6
- import { n as FieldPermissionMap } from "./fields-I_EeRWND.mjs";
3
+ import { d as UserBase, l as PermissionCheck, t as AnyRecord } from "./base-CZyixEQR.mjs";
4
+ import { a as QueryCacheConfig } from "./QueryCache--5uAuYPW.mjs";
5
+ import "./elevation-BW2J3tNj.mjs";
6
+ import "./requirement-CbiLdQ1n.mjs";
7
+ import { n as FieldPermissionMap } from "./fields-BBSpf8pS.mjs";
7
8
  import { n as EncryptionDirective } from "./types-ByHn9rEO.mjs";
8
- import { n as DomainEvent } from "./EventTransport-CbUKDzZZ.mjs";
9
+ import { n as DomainEvent } from "./EventTransport-CiicKz2Z.mjs";
9
10
  import { Filter } from "@classytic/repo-core/filter";
10
- import { AggDateBucket, AggMeasure, AggTopN, QueryOptions, StandardRepo, TenantPurgeStrategy, TransactionHandle } from "@classytic/repo-core/repository";
11
+ import { AggDateBucket, AggMeasure, AggTopN, QueryOptions, TenantPurgeStrategy, TransactionHandle } from "@classytic/repo-core/repository";
11
12
  import { KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
12
- import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
13
+ import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
13
14
  import { DataAdapter, RepositoryLike } from "@classytic/repo-core/adapter";
14
15
  import { LookupSpec } from "@classytic/repo-core/lookup";
15
16
  import { FieldRule, SchemaBuilderOptions } from "@classytic/repo-core/schema";
@@ -608,6 +609,12 @@ interface ControllerConfigurableOptions {
608
609
  * own `idField` when unset.
609
610
  */
610
611
  idField?: string;
612
+ /**
613
+ * Further unique keys a single-row read and an action target accept, tried in order after
614
+ * `idField` (e.g. `['rmaNumber']` beside `_id`). Each lookup carries the same tenant and row
615
+ * policy as the primary one — an alternate key never widens what a caller can reach.
616
+ */
617
+ lookupFields?: readonly string[];
611
618
  /**
612
619
  * Custom filter matching for policy enforcement.
613
620
  * Provided by the DataAdapter for non-MongoDB databases (SQL, etc.).
@@ -645,6 +652,10 @@ interface ControllerConfigurableOptions {
645
652
  * `configure()` like `writes`.
646
653
  */
647
654
  transactional?: boolean;
655
+ /** Optimistic concurrency — see `ResourceConfig.concurrency`. */
656
+ concurrency?: {
657
+ versionField?: string;
658
+ };
648
659
  }
649
660
  /**
650
661
  * Full constructor options — union of construction-only + configurable.
@@ -1031,6 +1042,50 @@ declare class AccessControl {
1031
1042
  private _meta;
1032
1043
  }
1033
1044
  //#endregion
1045
+ //#region src/core/entityHelpers.d.ts
1046
+ /**
1047
+ * Read the resource's configured `idField` for the current request.
1048
+ * Falls back to the framework default (`_id`) when the route hasn't
1049
+ * bound an idField — keeps handlers safe to author without checking
1050
+ * the resource config.
1051
+ */
1052
+ declare function getEntityIdField(req: RequestWithExtras): string;
1053
+ /**
1054
+ * Read the URL `:id` path param value as the resource handle.
1055
+ * Returns `undefined` when the route has no `:id` segment (collection
1056
+ * routes) — handlers that need the entity must be on row routes.
1057
+ */
1058
+ declare function getEntityId(req: RequestWithExtras): string | undefined;
1059
+ /**
1060
+ * Compose a `findOne` filter that resolves the current request's
1061
+ * entity, regardless of whether the resource binds `_id` or a custom
1062
+ * field. Idiomatic shape for arc action handlers.
1063
+ *
1064
+ * ```ts
1065
+ * const doc = await Model.findOne(getEntityQuery(req));
1066
+ * ```
1067
+ *
1068
+ * Returns `{}` when the route has no entity context (collection routes
1069
+ * or tests bypassing the router) — caller decides what that means.
1070
+ */
1071
+ declare function getEntityQuery(req: RequestWithExtras): Record<string, string>;
1072
+ /** The action-target fence's answer and, when it read one, the row (`GET /:id`'s scope). */
1073
+ interface ActionTargetCheck {
1074
+ readonly visible: boolean;
1075
+ readonly doc?: unknown;
1076
+ }
1077
+ /**
1078
+ * The row the action-target fence loaded for this request, so the handler need not read it
1079
+ * again. `undefined` when no fence read happened (untenanted resource, `scope: 'handler'`,
1080
+ * id-less action) — load it yourself then. It is a snapshot taken after the idempotency lock
1081
+ * and before the handler: a guarded write must still carry its precondition in the filter.
1082
+ */
1083
+ declare function getActionTarget<TDoc = Record<string, unknown>>(req: {
1084
+ readonly arc?: {
1085
+ readonly actionTarget?: unknown;
1086
+ };
1087
+ }): TDoc | undefined;
1088
+ //#endregion
1034
1089
  //#region src/core/QueryResolver.d.ts
1035
1090
  interface QueryResolverConfig {
1036
1091
  /** Query parser instance (default: Arc built-in parser) */
@@ -1103,9 +1158,9 @@ declare class QueryResolver {
1103
1158
  /** Sanitize advanced populate options against allowedPopulate */
1104
1159
  private sanitizePopulateOptions;
1105
1160
  /**
1106
- * Sanitize lookup/join options. Deny-by-default: `allowedLookups` must be
1107
- * configured for any client-driven lookup to pass; listed collections are
1108
- * then structurally validated to prevent injection.
1161
+ * Client lookups, deny-by-default. An allowed one is structurally validated, and its joined side
1162
+ * is filtered by the resource's server-owned `LookupPolicy.scope` — the base collection's tenant
1163
+ * filter never reaches a join, and a client-supplied `where` is replaced, never merged.
1109
1164
  */
1110
1165
  private sanitizeLookups;
1111
1166
  /**
@@ -1147,10 +1202,7 @@ declare class QueryResolver {
1147
1202
  //#region src/core/BaseCrudController.d.ts
1148
1203
  /**
1149
1204
  * Framework-agnostic CRUD controller implementing IController.
1150
- *
1151
- * Composes AccessControl, BodySanitizer, and QueryResolver. All shared
1152
- * state and helpers are `protected` so the preset mixins (SoftDelete,
1153
- * Tree, Slug, Bulk) can extend cleanly.
1205
+ * Composes AccessControl, BodySanitizer, and QueryResolver.
1154
1206
  *
1155
1207
  * @template TDoc - The document type.
1156
1208
  * @template TRepository - The repository type (defaults to RepositoryLike).
@@ -1164,29 +1216,14 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1164
1216
  protected resourceName?: string;
1165
1217
  protected tenantField: string | false;
1166
1218
  protected idField: string;
1167
- /**
1168
- * Composable access control (ID filtering, policy checks, org scope, ownership).
1169
- *
1170
- * Not `readonly` — `configure()` rebuilds it when the host
1171
- * supplies tenant/idField/matchesFilter post-construction. Same model as
1172
- * `queryResolver` after `setQueryParser` shipped in 2.10.9.
1173
- */
1219
+ /** Alternate unique keys, each with its own access control over the SAME scope. */
1220
+ protected lookupAccess: AccessControl[];
1221
+ private _lookupFields;
1222
+ /** Access control (ID filtering, policy checks, org scope, ownership). */
1174
1223
  accessControl: AccessControl;
1175
- /**
1176
- * Composable body sanitization (field permissions, system fields).
1177
- *
1178
- * Not `readonly` — `configure()` rebuilds it when the host
1179
- * supplies schemaOptions/onFieldWriteDenied post-construction.
1180
- */
1224
+ /** Body sanitization (field permissions, system fields). */
1181
1225
  bodySanitizer: BodySanitizer;
1182
- /**
1183
- * Composable query resolution (parsing, pagination, sort, select/populate).
1184
- *
1185
- * Not `readonly` — `setQueryParser()` rebuilds this resolver to swap in a
1186
- * different parser (e.g. mongokit's `QueryParser`). `defineResource` calls
1187
- * it automatically when a resource supplies both `controller` and
1188
- * `queryParser`.
1189
- */
1226
+ /** Query resolution (parsing, pagination, sort, select/populate). */
1190
1227
  queryResolver: QueryResolver;
1191
1228
  protected _matchesFilter?: (item: unknown, filters: Record<string, unknown>) => boolean;
1192
1229
  protected _presetFields: {
@@ -1195,82 +1232,51 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1195
1232
  };
1196
1233
  protected _cacheConfig?: ResourceCacheConfig;
1197
1234
  /**
1198
- * Retained construction-time values so partial `configure()` rebuilds
1199
- * (e.g. `configure({ schemaOptions })`) reuse them instead of silently
1200
- * falling back to the sub-component defaults (`-createdAt` sort,
1201
- * `reject` write-denial) — `defaultSort: false` matters for kits
1202
- * without a `createdAt` column, `strip` for legacy write semantics.
1235
+ * Retained so partial `configure()` rebuilds reuse them instead of falling back
1236
+ * to sub-component defaults (`-createdAt` sort breaks kits without that column).
1203
1237
  */
1204
1238
  protected _defaultSort?: string | false;
1205
1239
  protected _onFieldWriteDenied?: FieldWriteDenialPolicy;
1206
1240
  protected _onImmutableWrite?: FieldWriteDenialPolicy;
1207
1241
  /**
1208
- * Domain commands bound to the write slots (`ResourceWrites`). Consulted at
1209
- * the PERSISTENCE step only — everything upstream of it (sanitizer, tenant
1210
- * injection, actor stamp, hook sandwich) runs exactly as it does without
1211
- * one, which is the entire point of the seam.
1242
+ * Domain commands bound to the write slots. Consulted at the PERSISTENCE step
1243
+ * only — sanitizer, tenant injection, actor stamp, and hooks run unchanged.
1212
1244
  */
1213
1245
  protected _writes?: ResourceWrites;
1214
1246
  /** Transactional write envelope — see `ResourceConfig.transactional`. */
1215
1247
  protected _transactional: boolean;
1248
+ /** Version field under optimistic concurrency — `null` when the resource did not opt in. */
1249
+ protected _versionField: string | null;
1216
1250
  constructor(repository: TRepository, options?: BaseControllerOptions);
1217
1251
  /**
1218
- * Swap the controller's query parser. Mutates the existing `QueryResolver`
1219
- * in place via `QueryResolver.setParser()` — the resolver instance stays
1220
- * referentially stable, and there is no second copy of `defaultSort` /
1221
- * `tenantField` / `schemaOptions` for the swap to drift away from.
1222
- *
1223
- * Closes the v2.10.9 gap where `defineResource({ controller, queryParser })`
1224
- * forwarded the parser only to auto-constructed controllers. `defineResource`
1225
- * calls this via duck-typing when both `controller` and `queryParser` are
1226
- * supplied; controllers that don't implement it are left untouched.
1252
+ * Bind the resource this controller serves — hooks, audit, events and cache key on it.
1253
+ * `defineResource` calls it for a supplied controller; an unset name is assigned, a
1254
+ * different one throws (the controller would run another resource's hooks).
1255
+ */
1256
+ bindResourceName(name: string): void;
1257
+ /**
1258
+ * Swap the controller's query parser. Mutates the existing `QueryResolver` in
1259
+ * place, so the resolver stays referentially stable. `defineResource` calls this
1260
+ * (duck-typed) when a resource supplies both `controller` and `queryParser`.
1227
1261
  */
1228
1262
  setQueryParser(queryParser: QueryParserInterface): void;
1229
1263
  /**
1230
- * Apply resource-level options to a custom controller AFTER construction.
1231
- *
1232
- * Closes the pre-2.15 footgun where `defineResource({ controller, tenantField,
1233
- * schemaOptions, ... })` warned that the options were "dropped" because the
1234
- * user-supplied controller never received them. Hosts had to remember to
1235
- * forward each one through `super(repo, { ... })` — easy to miss, silently
1236
- * mis-scopes queries when missed.
1237
- *
1238
- * `defineResource()` now calls `controller.configure(resolvedOpts)` after
1239
- * `resolveOrAutoCreateController()` runs. Configure-aware controllers receive
1240
- * the resolved values; arc skips the dropped-options warn for them.
1264
+ * Apply resource-level options to a controller AFTER construction.
1265
+ * `defineResource()` calls this once after resolving the controller, so custom
1266
+ * controllers receive tenant scope, field rules, sort/limit, cache, and
1267
+ * write-denial policy without forwarding them through `super(repo, { … })`.
1241
1268
  *
1242
- * Only the keys that affect cross-cutting state (tenant scope, schema/field
1243
- * rules, sort/limit policy, cache, write-denial policy) are honoured —
1244
- * `repository` / `resourceName` are constructor-only because they participate
1245
- * in mixin composition. Each known key rebuilds the affected sub-component
1246
- * (AccessControl / BodySanitizer / QueryResolver) so referentially-stable
1247
- * consumers don't see stale state.
1248
- *
1249
- * Idempotent: safe to call zero, one, or many times before first request;
1250
- * arc calls it exactly once.
1251
- *
1252
- * Type narrowed to `ControllerConfigurableOptions` — `resourceName` is
1253
- * construction-only and intentionally excluded so accidental "rename
1254
- * the resource at runtime" calls fail to typecheck.
1269
+ * Each key rebuilds only the affected sub-component. Idempotent.
1270
+ * `repository` / `resourceName` are construction-only and excluded by type.
1255
1271
  */
1256
1272
  configure(options: ControllerConfigurableOptions): void;
1257
- /**
1258
- * Get the tenant field name if multi-tenant scoping is enabled.
1259
- * Returns `undefined` when `tenantField` is `false`.
1260
- */
1273
+ /** Tenant field name, or `undefined` when `tenantField` is `false`. */
1261
1274
  protected getTenantField(): string | undefined;
1262
1275
  /**
1263
- * Build the canonical repo-options bag (tenant scope, audit
1264
- * attribution, trace correlation) from the Fastify request. Full
1265
- * contract + rationale on `buildTenantRepoOptions` in
1266
- * `crud/requestPipeline.ts` — this delegate exists so host overrides
1267
- * and duck-typed consumers (aggregation router, MCP bridge) keep the
1268
- * same `protected` entry point.
1269
- *
1270
- * Method kept named `tenantRepoOptions` for back-compat with hosts
1271
- * that spread `...this.tenantRepoOptions(req)` (10+ call sites in
1272
- * arc, plus host overrides). The bag has always grown over time —
1273
- * hosts that don't want audit forwarding never read those keys.
1276
+ * Canonical repo-options bag (tenant scope, audit attribution, trace
1277
+ * correlation) — see `buildTenantRepoOptions` in `crud/requestPipeline.ts`.
1278
+ * Kept as a `protected` entry point for host overrides and duck-typed
1279
+ * consumers (aggregation router, MCP bridge).
1274
1280
  */
1275
1281
  protected tenantRepoOptions(req: IRequestContext): AnyRecord;
1276
1282
  /** Extract typed Arc internal metadata from request */
@@ -1285,18 +1291,11 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1285
1291
  */
1286
1292
  protected resolveRepoId(id: string, existing: AnyRecord | null): string;
1287
1293
  /**
1288
- * Read-side preflight for mutable-target operations (`update`, `delete`).
1289
- *
1290
- * Bundles the four steps that every mutation must do before touching the
1291
- * repo: (1) extract `:id`, (2) fetch under access control + tenant scope,
1292
- * (3) verify ownership, (4) translate the route id to the repo's primary
1293
- * key. Returning `{id, existing, repoId}` keeps the call sites a single
1294
- * line and makes drift between `update` and `delete` structurally
1295
- * impossible — there is one preflight, one denial-reason mapping, one
1296
- * ownership check.
1294
+ * Shared preflight for mutations: extract `:id`, fetch under access control +
1295
+ * tenant scope, verify ownership, translate the route id to the repo primary
1296
+ * key. One path so `update` and `delete` cannot drift.
1297
1297
  *
1298
- * Pass `extraFetchOptions` for callers (e.g. soft-delete restore) that
1299
- * need to widen the fetch (`{ includeDeleted: true }`).
1298
+ * `extraFetchOptions` widens the fetch (e.g. restore's `{ includeDeleted: true }`).
1300
1299
  */
1301
1300
  protected loadMutableTarget(req: IRequestContext, extraFetchOptions?: AnyRecord): Promise<{
1302
1301
  id: string;
@@ -1304,18 +1303,50 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1304
1303
  repoId: string;
1305
1304
  }>;
1306
1305
  /**
1307
- * Centralized 404 thrower. Maps the denial reason from `fetchDetailed()`
1308
- * into a `NotFoundError` so consumers can distinguish "doc doesn't
1309
- * exist" from "doc filtered by policy/org scope" via the error
1310
- * `details.code` set by the global error handler.
1306
+ * Whether the target of an id-bound ACTION is a row this request may read — the same
1307
+ * compound filter (id + tenant + `_policyFilters`) `GET /:id` uses. The action router
1308
+ * refuses the action with a 404 when it is not, so a caller can act on a row iff it can
1309
+ * read it. Skips the read when neither tenancy nor a row policy narrows the request.
1310
+ */
1311
+ private buildLookupAccess;
1312
+ /**
1313
+ * THE single-row lookup — `GET /:id` and the action-target fence both read through it.
1314
+ * The primary key first; when it finds nothing (or the id cannot be that key's type), each
1315
+ * `lookupFields` key in turn, under the same tenant and row policy.
1316
+ */
1317
+ protected fetchByAnyKey(id: string, req: IRequestContext, options: QueryOptions | undefined): Promise<{
1318
+ doc: TDoc | null;
1319
+ reason: FetchDenialReason | null;
1320
+ }>;
1321
+ /** Whether {@link isActionTargetVisible} can answer — it reads through the repository. */
1322
+ canFenceActionTargets(): boolean;
1323
+ isActionTargetVisible(req: IRequestContext, id: string): Promise<boolean>;
1324
+ /**
1325
+ * The fence's answer plus the row it read (absent when the resource has no row scope, so
1326
+ * nothing was read). The router hands the row to the handler as `getActionTarget(req)`.
1327
+ */
1328
+ loadActionTarget(req: IRequestContext, id: string): Promise<ActionTargetCheck>;
1329
+ /** `ETag: "<version>"` for a versioned document under optimistic concurrency. */
1330
+ protected etagHeaders(doc: unknown): Record<string, string> | undefined;
1331
+ /**
1332
+ * The version an update must apply at. `If-Match` must name the CURRENT version or the
1333
+ * request fails 412 before anything is written; without it (or with `*`) the write is a CAS
1334
+ * on the version just loaded, so a concurrent write in between is a 409, never overwritten.
1335
+ */
1336
+ protected expectedVersion(req: IRequestContext, existing: unknown): {
1337
+ version?: number;
1338
+ fromClient: boolean;
1339
+ };
1340
+ /**
1341
+ * 404 thrower. Maps the `fetchDetailed()` denial reason into `details.code` so
1342
+ * "doesn't exist" is distinguishable from "filtered by policy/org scope".
1311
1343
  */
1312
1344
  protected throwNotFound(reason?: FetchDenialReason | null): never;
1313
1345
  /** Resolve cache config for a specific operation, merging per-op overrides */
1314
1346
  protected resolveCacheConfig(operation: "list" | "byId"): QueryCacheConfig | null;
1315
1347
  /**
1316
- * Extract user/org IDs from request for cache key scoping.
1317
- * Only includes orgId when the resource uses tenant-scoped data (tenantField is set).
1318
- * Universal resources (tenantField: false) get shared cache keys.
1348
+ * User/org IDs for cache key scoping. orgId is included only when `tenantField`
1349
+ * is set; universal resources share cache keys.
1319
1350
  */
1320
1351
  protected cacheScope(req: IRequestContext): {
1321
1352
  userId?: string;
@@ -1325,20 +1356,13 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1325
1356
  protected cacheResponse<T>(data: T, cacheStatus: CacheStatus): IControllerResponse<T>;
1326
1357
  /** Required route-id helper shared by get/update/delete. Throws on missing id. */
1327
1358
  protected requireIdParam(req: IRequestContext): string;
1328
- /**
1329
- * Normalizes `repo.exists()` return shapes across adapters. Per
1330
- * StandardRepo's contract, `exists` may return `boolean`, `{ _id }`,
1331
- * or `null` — every truthy non-null shape collapses to `true`.
1332
- */
1359
+ /** Normalizes `repo.exists()` (`boolean | { _id } | null`) to a boolean. */
1333
1360
  protected isExistsTruthy(result: unknown): boolean;
1334
1361
  /** Assemble the per-request hook context from the protected accessors. */
1335
1362
  private hookOpContext;
1336
1363
  /**
1337
- * Run `executeBefore` then `executeAround` (or just the executor if no
1338
- * hooks are wired). Returns the around-phase result directly. Throws an
1339
- * `ArcError` (status 400, code `BEFORE_<OP>_HOOK_ERROR`) when the
1340
- * before-hook fails. Full contract on `executeHookedOp` in
1341
- * `crud/requestPipeline.ts`.
1364
+ * Run before + around hooks (or just the executor if none are wired) and
1365
+ * return the result. A failing before-hook throws 400 `BEFORE_<OP>_HOOK_ERROR`.
1342
1366
  */
1343
1367
  protected runHookedOpUntilResult<TInput, TResult>(req: IRequestContext, args: {
1344
1368
  op: "create" | "update" | "delete";
@@ -1348,14 +1372,10 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1348
1372
  }, executor: (processed: TInput) => Promise<TResult>): Promise<TResult>;
1349
1373
  /**
1350
1374
  * Run a write's PERSISTENCE step, transactionally when the resource asked.
1351
- *
1352
- * Plain mode hands `fn` the live repository. Transactional mode runs it
1353
- * through repo-core's `retryingTransaction`: the callback receives the
1354
- * TX-BOUND repository (so declared verbs and repository calls join the same
1355
- * transaction), transient conflicts re-run `fn` with jittered bounded
1356
- * backoff, everything else — including `VersionConflictError` — surfaces
1357
- * once. Hooks stay OUTSIDE: before/around ran already and must not re-run;
1358
- * after-hooks run post-commit, which they already do by running after this.
1375
+ * Transactional mode uses `retryingTransaction`: `fn` gets the TX-BOUND repo and
1376
+ * re-runs on transient conflicts; anything else (incl. `VersionConflictError`)
1377
+ * surfaces once. Hooks stay OUTSIDE so before/around never re-run and after
1378
+ * runs post-commit.
1359
1379
  */
1360
1380
  protected runWritePersistence<T>(fn: (repo: TRepository, uow?: TransactionHandle) => Promise<T>): Promise<T>;
1361
1381
  /** Assemble the `WriteContext` a `create` verb receives. */
@@ -1363,43 +1383,26 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1363
1383
  /**
1364
1384
  * Assemble the `MutationWriteContext` an `update` / `delete` verb receives.
1365
1385
  *
1366
- * `existing` is the document arc ALREADY loaded to run its permission and
1367
- * tenant checks — `loadMutableTarget` throws before any verb runs when it
1368
- * cannot produce one, which is why the context types it required. Handing
1369
- * it over rather than letting the verb re-fetch is deliberate: a second
1370
- * read can observe a different document than the one the request was
1371
- * authorised against.
1372
- *
1373
- * `id` is the REPOSITORY PRIMARY KEY (`repoId`), never the raw route param —
1374
- * see the note on `MutationWriteContext.id`. Callers pass `resolveRepoId`'s
1375
- * output.
1386
+ * `existing` is the document the request was AUTHORISED against — handed over
1387
+ * so the verb does not re-fetch and possibly observe a different document.
1388
+ * `id` is the REPOSITORY PRIMARY KEY (`resolveRepoId` output), never the route param.
1376
1389
  */
1377
1390
  protected mutationWriteContext(req: IRequestContext, id: string, existing: unknown, repo?: TRepository, uow?: TransactionHandle): MutationWriteContext;
1378
1391
  /**
1379
- * Run `executeAfter` for the given op + data. No-op when hooks aren't
1380
- * wired or `resourceName` isn't set. Caller passes the data shape it
1381
- * wants downstream after-handlers to receive — typically the result for
1382
- * create/update, the original input (`existing`) for delete.
1392
+ * Run after-hooks for `op`. No-op without hooks or `resourceName`. `data` is the
1393
+ * result for create/update, `existing` for delete.
1383
1394
  */
1384
1395
  protected runAfterHook(req: IRequestContext, op: "create" | "update" | "delete" | "list" | "read", data: AnyRecord, meta?: Record<string, unknown>): Promise<void>;
1385
1396
  /**
1386
- * Per-instance single-flight for cache fills — concurrent requests for
1387
- * the SAME cache key coalesce onto one repository query instead of each
1388
- * issuing its own (a popular entry expiring under 1,000 concurrent
1389
- * readers must produce 1 DB query, not 1,000). Keys already encode the
1390
- * resolved query + user/org scope + resource version, so coalescing can
1391
- * never cross tenants, scopes, or invalidation generations. Per-process
1392
- * by design; distributed refresh leases are a store-level concern
1393
- * (planned with the repo-core CacheEngine façade, which ships
1394
- * single-flight natively).
1397
+ * Per-process single-flight for cache fills: concurrent requests for the SAME
1398
+ * key coalesce onto one repository query. Keys encode query + user/org scope +
1399
+ * resource version, so coalescing never crosses tenants or invalidation generations.
1395
1400
  */
1396
1401
  private readonly inFlightCacheFills;
1397
1402
  private cacheSingleFlight;
1398
1403
  /**
1399
- * Register a background SWR refresh for `key` unless one is already in
1400
- * flight. Registration is SYNCHRONOUS (before the deferred tick) so a
1401
- * burst of stale readers arriving in the same tick schedules exactly one
1402
- * revalidation.
1404
+ * Register a background SWR refresh for `key` unless one is in flight.
1405
+ * Registration is SYNCHRONOUS so a same-tick burst of stale readers schedules one.
1403
1406
  */
1404
1407
  private scheduleCacheRefresh;
1405
1408
  /** Cached `list()` flow with SWR semantics. Returns null when cache is disabled. */
@@ -1408,19 +1411,14 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1408
1411
  protected withGetCache(req: IRequestContext, id: string, options: ControllerQueryOptions): Promise<IControllerResponse<TDoc> | null>;
1409
1412
  list(req: IRequestContext): Promise<IControllerResponse<ListResult<TDoc>>>;
1410
1413
  /**
1411
- * Resource-dispatch verbs router. Returns `null` when the request is
1412
- * a regular list query, otherwise returns the dispatch promise.
1413
- *
1414
- * Verbs (mutually exclusive — first match wins):
1414
+ * Resource-dispatch verbs router; `null` for a regular list query.
1415
+ * First match wins:
1415
1416
  * - `?_count=true` → `{ count: number }` via `repo.count()`
1416
1417
  * - `?_distinct=field` → `unknown[]` via `repo.distinct(field)`
1417
1418
  * - `?_exists=true` → `{ exists: boolean }` via `repo.exists()`
1418
1419
  *
1419
- * All verbs share the resolved filter (parsed query + policy filters
1420
- * + tenant scope). Adapters that don't ship the underlying repo
1421
- * method get a `501` so failures surface loudly instead of falling
1422
- * back to a full table scan. Matching + repo invocation live in
1423
- * `crud/resourceVerbs.ts`.
1420
+ * All share the resolved filter (query + policy + tenant scope). A missing repo
1421
+ * method is a `501`, never a fallback to a full scan.
1424
1422
  */
1425
1423
  protected dispatchResourceVerb(req: IRequestContext): Promise<IControllerResponse<unknown>> | null;
1426
1424
  /** Resolve filter + tenant/audit options for a dispatch verb. */
@@ -1438,13 +1436,8 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1438
1436
  exists: boolean;
1439
1437
  }>>;
1440
1438
  /**
1441
- * True when `field` is safe to expose via `_distinct`.
1442
- *
1443
- * Read-side gate only — only `hidden: true` blocks. `systemManaged`
1444
- * is a *write* rule (clients can't PATCH the value); the field is
1445
- * still in every list response, so blocking `_distinct` adds nothing
1446
- * but inconvenience. See `core/fieldRulePredicates.ts` for the
1447
- * canonical predicate shared with `QueryResolver`.
1439
+ * True when `field` is safe to expose via `_distinct`. Only `hidden: true`
1440
+ * blocks — `systemManaged` is a write rule and the field is already readable.
1448
1441
  */
1449
1442
  protected isFieldExposedForRead(field: string): boolean;
1450
1443
  /** Execute list query through hooks (extracted for cache revalidation) */
@@ -1467,7 +1460,7 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
1467
1460
  //#region src/core/mixins/bulk.d.ts
1468
1461
  /** Public surface contributed by BulkMixin. */
1469
1462
  interface BulkExt {
1470
- bulkCreate(req: IRequestContext): Promise<IControllerResponse<AnyRecord[]>>;
1463
+ bulkCreate(req: IRequestContext): Promise<IControllerResponse<object[]>>;
1471
1464
  bulkUpdate(req: IRequestContext): Promise<IControllerResponse<{
1472
1465
  matchedCount: number;
1473
1466
  modifiedCount: number;
@@ -1476,69 +1469,42 @@ interface BulkExt {
1476
1469
  deletedCount: number;
1477
1470
  }>>;
1478
1471
  }
1479
- declare function BulkMixin<TBase extends MixinConstructor<BaseCrudController>>(Base: TBase): TBase & MixinConstructor<BulkExt>;
1472
+ declare function BulkMixin<TBase extends MixinConstructor<BaseCrudController<object>>>(Base: TBase): TBase & MixinConstructor<BulkExt>;
1480
1473
  //#endregion
1481
1474
  //#region src/core/mixins/slug.d.ts
1482
1475
  /** Public surface contributed by SlugMixin. */
1483
1476
  interface SlugExt {
1484
- getBySlug(req: IRequestContext): Promise<IControllerResponse<AnyRecord>>;
1477
+ getBySlug(req: IRequestContext): Promise<IControllerResponse<object>>;
1485
1478
  }
1486
- declare function SlugMixin<TBase extends MixinConstructor<BaseCrudController>>(Base: TBase): TBase & MixinConstructor<SlugExt>;
1479
+ declare function SlugMixin<TBase extends MixinConstructor<BaseCrudController<object>>>(Base: TBase): TBase & MixinConstructor<SlugExt>;
1487
1480
  //#endregion
1488
1481
  //#region src/core/mixins/tree.d.ts
1489
1482
  /** Public surface contributed by TreeMixin. */
1490
1483
  interface TreeExt {
1491
- getTree(req: IRequestContext): Promise<IControllerResponse<AnyRecord[]>>;
1492
- getChildren(req: IRequestContext): Promise<IControllerResponse<AnyRecord[]>>;
1484
+ getTree(req: IRequestContext): Promise<IControllerResponse<object[]>>;
1485
+ getChildren(req: IRequestContext): Promise<IControllerResponse<object[]>>;
1493
1486
  }
1494
- declare function TreeMixin<TBase extends MixinConstructor<BaseCrudController>>(Base: TBase): TBase & MixinConstructor<TreeExt>;
1487
+ declare function TreeMixin<TBase extends MixinConstructor<BaseCrudController<object>>>(Base: TBase): TBase & MixinConstructor<TreeExt>;
1495
1488
  //#endregion
1496
1489
  //#region src/core/mixins/softDelete.d.ts
1497
1490
  /** Public surface contributed by SoftDeleteMixin. */
1498
1491
  interface SoftDeleteExt {
1499
- getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginationResult<AnyRecord>>>;
1500
- restore(req: IRequestContext): Promise<IControllerResponse<AnyRecord>>;
1492
+ getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginationResult<object>>>;
1493
+ restore(req: IRequestContext): Promise<IControllerResponse<object>>;
1501
1494
  }
1502
- declare function SoftDeleteMixin<TBase extends MixinConstructor<BaseCrudController>>(Base: TBase): TBase & MixinConstructor<SoftDeleteExt>;
1495
+ declare function SoftDeleteMixin<TBase extends MixinConstructor<BaseCrudController<object>>>(Base: TBase): TBase & MixinConstructor<SoftDeleteExt>;
1503
1496
  //#endregion
1504
1497
  //#region src/core/BaseController.d.ts
1505
- /**
1506
- * The `Arc*Result` types above stay: `src/index.ts` re-exports them from THIS
1507
- * path. `BaseCrudController` and the four mixins (+ their `*Ext` types) were
1508
- * also re-exported here and are not, as of 2.37.1 — `core/index.ts` takes them
1509
- * straight from `./BaseCrudController.js` and `./mixins/*.js`, so this file's
1510
- * copies had no importer. Two public paths to one symbol is a drift seam, not
1511
- * convenience: composition belongs to `core/index.ts`, and this module is the
1512
- * composed `BaseController` itself.
1513
- */
1514
1498
  /**
1515
1499
  * Fully-composed controller shape: all CRUD methods + every preset method
1516
1500
  * (SoftDelete / Tree / Slug / Bulk) typed over the caller-supplied `TDoc`.
1517
1501
  *
1518
- * Composes `BaseCrudController` (list/get/create/update/delete) → `BulkMixin`
1519
- * (bulk*) → `SlugMixin` (getBySlug) → `TreeMixin` (getTree/getChildren) →
1520
- * `SoftDeleteMixin` (getDeleted/restore). CRUD-only hosts extend
1521
- * `BaseCrudController` for a smaller surface; others compose mixins by hand.
1522
- *
1523
- * DECLARATION MERGING, not a generic mixin chain: TypeScript cannot thread one
1524
- * `TDoc` through four chained mixin factories without losing the
1525
- * `extends Constructor<Base>` constraint. So the runtime pins the chain base to
1526
- * `AnyRecord` while the TYPE surface threads `TDoc`, and merging bridges them.
1527
- *
1528
- * `TDoc extends AnyRecord` is LOAD-BEARING: inherited methods return
1529
- * `ListResult<AnyRecord>` and the merged interface returns `ListResult<TDoc>`,
1530
- * so TS's derived-assignable-to-base check needs the bound. Without it the
1531
- * class declaration fails with `Type 'TDoc[]' is not assignable to type
1532
- * 'AnyRecord[]'`. Hosts pick one of:
1533
- * (a) drop the generic — lose return narrowing;
1534
- * (b) `interface IUser extends AnyRecord` — preferred when you own the type;
1535
- * (c) `ArcListResult<typeof this>` etc. when overriding — reads the return
1536
- * type off the class, so the bound never applies.
1537
- *
1538
- * COST: every `TDoc`-narrowed method is redeclared on the interface, so a new
1539
- * mixin method must be added here too.
1502
+ * DECLARATION MERGING, not a generic mixin chain: chained mixin factories cannot thread one
1503
+ * `TDoc`, so the runtime chain is instantiated over `object` and the TYPE surface threads `TDoc`.
1504
+ * Any `TDoc extends object` works — an interface-typed document needs no index signature.
1505
+ * A new mixin method must be redeclared here too.
1540
1506
  */
1541
- interface BaseController<TDoc extends AnyRecord = AnyRecord, _TRepository extends RepositoryLike = RepositoryLike<TDoc>> {
1507
+ interface BaseController<TDoc extends object = AnyRecord, _TRepository extends RepositoryLike = RepositoryLike<TDoc>> {
1542
1508
  accessControl: AccessControl;
1543
1509
  bodySanitizer: BodySanitizer;
1544
1510
  queryResolver: QueryResolver;
@@ -1567,14 +1533,14 @@ interface BaseController<TDoc extends AnyRecord = AnyRecord, _TRepository extend
1567
1533
  deletedCount: number;
1568
1534
  }>>;
1569
1535
  }
1570
- declare const BaseController_base: (new (repository: any, options?: BaseControllerOptions) => BaseCrudController<AnyRecord, any>) & MixinConstructor<BulkExt> & MixinConstructor<SlugExt> & MixinConstructor<TreeExt> & MixinConstructor<SoftDeleteExt>;
1536
+ declare const BaseController_base: {
1537
+ new (repository: RepositoryLike, options?: BaseControllerOptions): BaseCrudController<object, RepositoryLike>;
1538
+ } & MixinConstructor<BulkExt> & MixinConstructor<SlugExt> & MixinConstructor<TreeExt> & MixinConstructor<SoftDeleteExt>;
1571
1539
  /**
1572
1540
  * Fully-composed controller: `BaseCrudController` + SoftDelete + Tree +
1573
- * Slug + Bulk. Drop-in replacement for the pre-2.11 god class. The
1574
- * companion interface above gives every method full generic precision
1575
- * on `TDoc` via declaration merging.
1541
+ * Slug + Bulk. The companion interface above types every method over `TDoc`.
1576
1542
  */
1577
- declare class BaseController<TDoc extends AnyRecord = AnyRecord, _TRepository extends RepositoryLike = RepositoryLike<TDoc>> extends BaseController_base {
1543
+ declare class BaseController<TDoc extends object = AnyRecord, _TRepository extends RepositoryLike = RepositoryLike<TDoc>> extends BaseController_base {
1578
1544
  readonly _phantom?: [TDoc, _TRepository];
1579
1545
  }
1580
1546
  //#endregion
@@ -1924,6 +1890,9 @@ interface RouteDefinition {
1924
1890
  * Accepts a controller-method NAME as well, same as {@link handler} — the
1925
1891
  * lookup is identical and only the calling convention differs, so a
1926
1892
  * Fastify-native controller method stays reachable by name.
1893
+ *
1894
+ * An async raw handler that calls `reply.send()` must `return reply` (as `handleRaw`
1895
+ * does): resolving `undefined` while an async `preSerialization` hook runs sends an empty body.
1927
1896
  */
1928
1897
  readonly rawHandler?: string | RawRouteHandler;
1929
1898
  /** Logical operation name (pipeline keys, MCP tool naming). */
@@ -1941,8 +1910,8 @@ interface RouteDefinition {
1941
1910
  /**
1942
1911
  * Opt this custom route into the multiTenant preset's tenant scoping.
1943
1912
  *
1944
- * When `true` AND the resource declares `multiTenantPreset` (or
1945
- * `flexibleMultiTenantPreset`), arc prepends the preset's tenant
1913
+ * When `true` AND the resource declares `multiTenantPreset`, arc
1914
+ * prepends the preset's tenant
1946
1915
  * filter + injection middleware to this route's chain — the same
1947
1916
  * pair `update` gets. The handler can then read the resolved tenant
1948
1917
  * via `getOrgId(req.scope)` / `req._tenantFields` (or, for write
@@ -2139,6 +2108,28 @@ interface ActionDefinition {
2139
2108
  * resource root and drops `id` from the MCP tool's input shape.
2140
2109
  */
2141
2110
  readonly id?: boolean;
2111
+ /**
2112
+ * Where an id-bound action's TARGET is authorized. Default: arc loads it through the
2113
+ * resource's access control (id + tenant + row policy, as `GET /:id` does) and answers
2114
+ * 404 when the caller could not read it. `{ scope: 'handler', reason }` hands that check
2115
+ * to the handler — for a target that is deliberately not a row of this resource's tenant.
2116
+ */
2117
+ readonly target?: {
2118
+ readonly scope: "handler";
2119
+ readonly reason: string;
2120
+ };
2121
+ /**
2122
+ * `{ required: true }`: a call of THIS action without an idempotency key is refused (400
2123
+ * `arc.idempotency_key_required`) and a keyed one is deduplicated; other actions on the
2124
+ * same mount stay optional. Needs `idempotencyPlugin` enabled.
2125
+ */
2126
+ readonly idempotency?: {
2127
+ readonly required: boolean;
2128
+ /** `'handler'`: the handler deduplicates; arc only demands and exposes the key. Needs `owner`. */
2129
+ readonly dedup?: "handler";
2130
+ /** The runner that owns at-most-once under `dedup: 'handler'`. */
2131
+ readonly owner?: string;
2132
+ };
2142
2133
  /**
2143
2134
  * MCP tool generation:
2144
2135
  * - omitted/true: auto-generate
@@ -2817,6 +2808,16 @@ interface FieldRule$1 {
2817
2808
  }
2818
2809
  //#endregion
2819
2810
  //#region src/types/resource/schemas.d.ts
2811
+ /**
2812
+ * One client-requestable join. The base collection's tenant filter never reaches a joined
2813
+ * collection, so its scope is REQUIRED and is always the server's — a client `where` is discarded.
2814
+ */
2815
+ interface LookupPolicy {
2816
+ /** Filter on the JOINED rows (tenant, soft-delete, visibility). `{}` only for a genuinely global collection. */
2817
+ scope: (ctx: {
2818
+ scope: RequestScope | undefined;
2819
+ }) => Record<string, unknown>;
2820
+ }
2820
2821
  /**
2821
2822
  * Schema-shaping options for a resource.
2822
2823
  *
@@ -2913,11 +2914,10 @@ interface RouteSchemaOptions extends SchemaBuilderOptions {
2913
2914
  */
2914
2915
  allowedPopulate?: string[];
2915
2916
  /**
2916
- * Lookup-collection whitelist for `QueryResolver.sanitizeLookups`.
2917
- * When set, only the listed collections may be `$lookup`'d. Omit to
2918
- * disable the whitelist (kit-level rules still apply).
2917
+ * Client-requestable joins, keyed by collection, each with the SERVER-owned scope of its
2918
+ * joined side. Omit to deny every client lookup (the default).
2919
2919
  */
2920
- allowedLookups?: string[];
2920
+ allowedLookups?: Readonly<Record<string, LookupPolicy>>;
2921
2921
  };
2922
2922
  }
2923
2923
  /**
@@ -3070,6 +3070,14 @@ interface ResolvedTenantPurge {
3070
3070
  }
3071
3071
  //#endregion
3072
3072
  //#region src/types/resource/config.d.ts
3073
+ /**
3074
+ * Built-in write routes whose retry must not execute twice: each listed op answers a
3075
+ * keyless call with 400 `arc.idempotency_key_required` and dedupes a keyed one. Needs
3076
+ * `idempotencyPlugin` enabled; boot refuses an op that is not mounted.
3077
+ */
3078
+ interface ResourceIdempotencyConfig {
3079
+ readonly required: readonly ("create" | "update" | "delete")[];
3080
+ }
3073
3081
  /** Standard controller type alias for CRUD operations. */
3074
3082
  type CrudController<TDoc> = IController<TDoc>;
3075
3083
  /** Resource-level permissions — only `PermissionCheck` functions allowed. */
@@ -3140,6 +3148,16 @@ interface ResourceConfig<TDoc = AnyRecord> {
3140
3148
  * that silently degrades is the defect, not a mode.
3141
3149
  */
3142
3150
  transactional?: boolean;
3151
+ /**
3152
+ * Optimistic concurrency over HTTP (RFC 9110). Reads send `ETag: "<version>"`; an update
3153
+ * with `If-Match` applies only at that version (else 412), and EVERY update is a CAS write on
3154
+ * the version it loaded (else 409) — so the version, and the ETag, change on every write.
3155
+ * `versionField` defaults to `'__v'`. Boot-fatal on a repository without the
3156
+ * `optimisticConcurrency` capability.
3157
+ */
3158
+ concurrency?: {
3159
+ versionField?: string;
3160
+ };
3143
3161
  queryParser?: unknown;
3144
3162
  permissions?: ResourcePermissions;
3145
3163
  schemaOptions?: RouteSchemaOptions;
@@ -3382,6 +3400,11 @@ interface ResourceConfig<TDoc = AnyRecord> {
3382
3400
  * ```
3383
3401
  */
3384
3402
  idField?: (keyof TDoc & string) | (string & {});
3403
+ /**
3404
+ * Further unique keys `GET /:id` and an action's `:id` accept, tried in order after `idField`
3405
+ * — a business number beside `_id`. Same tenant and row-policy scope as the primary lookup.
3406
+ */
3407
+ lookupFields?: readonly ((keyof TDoc & string) | (string & {}))[];
3385
3408
  /** For grouping in registry */
3386
3409
  module?: string;
3387
3410
  /** Domain events */
@@ -3513,6 +3536,8 @@ interface ResourceConfig<TDoc = AnyRecord> {
3513
3536
  * registered. Set to `false` to disable for this resource.
3514
3537
  */
3515
3538
  rateLimit?: RateLimitConfig | false;
3539
+ /** Idempotency-key fence for the built-in write routes. See `ResourceIdempotencyConfig`. */
3540
+ idempotency?: ResourceIdempotencyConfig;
3516
3541
  /**
3517
3542
  * QueryCache configuration for this resource. Enables
3518
3543
  * stale-while-revalidate and auto-invalidation. Requires
@@ -3656,7 +3681,7 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
3656
3681
  readonly permissions: ResourcePermissions;
3657
3682
  readonly routes: readonly RouteDefinition[];
3658
3683
  readonly middlewares: MiddlewareConfig;
3659
- readonly routeGuards?: RouteHandlerMethod$1[];
3684
+ readonly routeGuards?: RouteHandlerMethod[];
3660
3685
  readonly disableDefaultRoutes: boolean;
3661
3686
  readonly disabledRoutes: readonly CrudRouteKey[];
3662
3687
  readonly actions?: ActionsMap;
@@ -3664,6 +3689,8 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
3664
3689
  readonly aggregations?: AggregationsMap;
3665
3690
  readonly events: Record<string, EventDefinition>;
3666
3691
  readonly rateLimit?: RateLimitConfig | false;
3692
+ readonly idempotency?: ResourceIdempotencyConfig;
3693
+ readonly lookupFields?: readonly string[];
3667
3694
  readonly audit?: boolean | {
3668
3695
  operations?: ("create" | "update" | "delete")[];
3669
3696
  };
@@ -3699,6 +3726,12 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
3699
3726
  */
3700
3727
  readonly resolvedTenantPurge: ResolvedTenantPurge;
3701
3728
  readonly idField?: string;
3729
+ /**
3730
+ * The optimistic-concurrency version field, resolved (`concurrency: {}` → `'__v'`);
3731
+ * `undefined` when the resource did not opt in. Published in the resource manifest
3732
+ * so a client sends `If-Match` without repeating the field name.
3733
+ */
3734
+ readonly versionField?: string;
3702
3735
  readonly queryParser?: QueryParserInterface;
3703
3736
  readonly _appliedPresets: string[];
3704
3737
  _pendingHooks: Array<{
@@ -3800,7 +3833,7 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
3800
3833
  //#region src/core/defineResource.d.ts
3801
3834
  /**
3802
3835
  * `TDoc` is **unconstrained** at this layer. The previous `TDoc
3803
- * extends AnyRecord` bound leaked out of `BaseController`'s
3836
+ * extends object` bound leaked out of `BaseController`'s
3804
3837
  * mixin-composition requirement into every host's adapter boundary:
3805
3838
  * Mongoose's `HydratedDocument<T>`, Prisma's generated row types,
3806
3839
  * and any domain interface without an explicit index signature all
@@ -3811,7 +3844,7 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
3811
3844
  * known envelope fields.
3812
3845
  *
3813
3846
  * The cast moved inside `resolveOrAutoCreateController` where
3814
- * `BaseController<TDoc extends AnyRecord>` actually requires it.
3847
+ * `BaseController<TDoc extends object>` actually requires it.
3815
3848
  * One internal boundary cast replaces N host-side casts.
3816
3849
  */
3817
3850
  /**
@@ -3864,8 +3897,19 @@ declare class ResourceRegistry {
3864
3897
  * runtime-only handle is still reachable.
3865
3898
  */
3866
3899
  private _adapters;
3900
+ /** The mounted definitions, in registration order — see `getDefinitions`. */
3901
+ private _definitions;
3867
3902
  private _frozen;
3868
3903
  constructor();
3904
+ /**
3905
+ * The live `ResourceDefinition`s that mounted, in registration order — what a host hands to
3906
+ * `mcpPlugin({ resources })` or a docs generator instead of re-collecting them. A resource
3907
+ * declaring `skipRegistry` (incl. `customRoutesOnly`) opted out and is absent. Fresh array per call.
3908
+ * One composed module's share: `getModuleResources(fastify, name)` from `@classytic/arc/factory`.
3909
+ */
3910
+ getDefinitions(): ResourceDefinition<unknown>[];
3911
+ /** The live definition registered under `name`, or `undefined`. */
3912
+ getDefinition(name: string): ResourceDefinition<unknown> | undefined;
3869
3913
  /**
3870
3914
  * Get the live `DataAdapter` for a registered resource — the handle whose
3871
3915
  * `.repository` cascade / cleanup helpers actually call. Returns `undefined`
@@ -3874,8 +3918,13 @@ declare class ResourceRegistry {
3874
3918
  * consumers cast to their kit's adapter type at the use site.
3875
3919
  */
3876
3920
  getAdapter<T = unknown>(name: string): T | undefined;
3921
+ /** Set only by an explicit `strictResources: false`: a duplicate name keeps the FIRST entry. */
3922
+ private _tolerateDuplicates;
3923
+ /** Accept a duplicate resource name (the host opted out of `strictResources`; it was warned). */
3924
+ tolerateDuplicateNames(): void;
3877
3925
  /**
3878
- * Register a resource
3926
+ * Register a resource. A duplicate name throws — a resource missing from the registry still
3927
+ * serves traffic but vanishes from introspection, the permission matrix and MCP.
3879
3928
  */
3880
3929
  register(resource: ResourceDefinition<unknown>, options?: RegisterOptions): this;
3881
3930
  /**
@@ -3955,10 +4004,6 @@ declare class ResourceRegistry {
3955
4004
  * iterate over a defunct registry entry. Clear both halves together.
3956
4005
  */
3957
4006
  reset(): void;
3958
- /** @internal Alias for unfreeze() */
3959
- _unfreeze(): void;
3960
- /** @internal Alias for reset() */
3961
- _clear(): void;
3962
4007
  /**
3963
4008
  * Group by key
3964
4009
  */
@@ -3998,6 +4043,8 @@ interface RequestWithExtras extends FastifyRequest {
3998
4043
  * can still read the entity handle.
3999
4044
  */
4000
4045
  entityId?: string;
4046
+ /** The id-bound action's target as the fence read it — read via `getActionTarget(req)`. */
4047
+ actionTarget?: unknown;
4001
4048
  };
4002
4049
  context?: Record<string, unknown>;
4003
4050
  _policyFilters?: Record<string, unknown>;
@@ -4132,6 +4179,8 @@ interface CrudRouterOptions {
4132
4179
  * registered. Set to `false` to disable for this resource.
4133
4180
  */
4134
4181
  rateLimit?: RateLimitConfig | false;
4182
+ /** Idempotency-key fence for the built-in write routes. */
4183
+ idempotency?: ResourceIdempotencyConfig;
4135
4184
  /** PreHandler guards applied to every route (CRUD + custom + preset). */
4136
4185
  routeGuards?: RouteHandlerMethod[];
4137
4186
  /**
@@ -4167,6 +4216,8 @@ interface RequestContext {
4167
4216
  * them without `as AnyRecord` casts.
4168
4217
  */
4169
4218
  interface ArcInternalMetadata extends RequestContext {
4219
+ /** Trusted resolved tenant dimensions forwarded across transports. */
4220
+ _tenantFields?: Record<string, unknown>;
4170
4221
  /** Policy filters from permission middleware */
4171
4222
  _policyFilters?: Record<string, unknown>;
4172
4223
  /** Request scope from scope resolution */
@@ -4241,6 +4292,8 @@ interface LookupOption {
4241
4292
  single?: boolean;
4242
4293
  /** Field selection on the joined collection */
4243
4294
  select?: string | Record<string, 0 | 1>;
4295
+ /** Filter on the joined rows — set by arc from the resource's `LookupPolicy`, never by the client. */
4296
+ where?: Record<string, unknown>;
4244
4297
  }
4245
4298
  /**
4246
4299
  * Mongoose-compatible populate option for advanced field selection.
@@ -4375,6 +4428,12 @@ interface ResourceMetadata {
4375
4428
  capability?: string;
4376
4429
  raw?: boolean;
4377
4430
  schema?: unknown;
4431
+ /** The route's `config.idempotency` — `required: true` means a key is mandatory. */
4432
+ idempotency?: {
4433
+ required?: boolean;
4434
+ dedup?: "handler";
4435
+ owner?: string;
4436
+ };
4378
4437
  }>;
4379
4438
  routes: Array<{
4380
4439
  method: string;
@@ -4400,6 +4459,10 @@ interface RegistryEntry extends ResourceMetadata {
4400
4459
  * without re-reading the resource definition.
4401
4460
  */
4402
4461
  tenantField?: string | false;
4462
+ /** The resource's primary key field (`_id` when unset). */
4463
+ idField?: string;
4464
+ /** Resolved optimistic-concurrency version field — set only under `concurrency`. */
4465
+ versionField?: string;
4403
4466
  /**
4404
4467
  * Resolved tenant-purge strategy — what arc actually runs when
4405
4468
  * `cascadeDeleteForOrganization` fires for this resource. Computed
@@ -4436,6 +4499,8 @@ interface RegistryEntry extends ResourceMetadata {
4436
4499
  disabledRoutes?: string[];
4437
4500
  /** Rate limit config */
4438
4501
  rateLimit?: RateLimitConfig | false;
4502
+ /** Built-in write ops that require an idempotency key (`defineResource({ idempotency })`). */
4503
+ idempotency?: ResourceIdempotencyConfig;
4439
4504
  /** Per-resource audit opt-in flag (read by `auditPlugin` perResource mode) */
4440
4505
  audit?: boolean | {
4441
4506
  operations?: ("create" | "update" | "delete")[];
@@ -4461,6 +4526,12 @@ interface RegistryEntry extends ResourceMetadata {
4461
4526
  * reading the action definition itself.
4462
4527
  */
4463
4528
  readonly id?: boolean;
4529
+ /** `{ required: true }` — a call of this action requires an idempotency key. */
4530
+ readonly idempotency?: {
4531
+ readonly required: boolean;
4532
+ readonly dedup?: "handler";
4533
+ readonly owner?: string;
4534
+ };
4464
4535
  /** MCP tool generation flag — `false` to skip, object for overrides */
4465
4536
  readonly mcp?: boolean | {
4466
4537
  readonly description?: string;
@@ -4584,26 +4655,5 @@ type InferDocType<T> = T extends DataAdapter<infer D> ? D : T extends ResourceCo
4584
4655
  */
4585
4656
  type InferAdapterDoc<A> = A extends DataAdapter<infer D> ? D : unknown;
4586
4657
  type InferResourceDoc<T> = T extends ResourceConfig<infer D> ? D : never;
4587
- type TypedResourceConfig<TDoc> = ResourceConfig<TDoc>;
4588
- type TypedController<TDoc> = IController<TDoc>;
4589
- type TypedRepository<TDoc> = StandardRepo<TDoc>;
4590
- //#endregion
4591
- //#region src/types/validation.d.ts
4592
- /**
4593
- * Validation result types — produced by `validateResourceConfig` and
4594
- * consumed by `assertValidConfig` / `formatValidationErrors`.
4595
- */
4596
- interface ConfigError {
4597
- field: string;
4598
- message: string;
4599
- code?: string;
4600
- }
4601
- interface ValidationResult {
4602
- valid: boolean;
4603
- errors: ConfigError[];
4604
- }
4605
- interface ValidateOptions {
4606
- strict?: boolean;
4607
- }
4608
4658
  //#endregion
4609
- export { OpenApiSchemas as $, AccessControlConfig as $t, RequestIdOptions as A, WriteVerbKey as An, RouteDefinition as At, defineResource as B, BaseController as Bt, RequestContext as C, ControllerConstructionOptions as Cn, PipelineStep as Ct, HealthCheck as D, MutationWriteContext as Dn, ActionEntry as Dt, GracefulShutdownOptions as E, BodySanitizerConfig as En, ActionDefinition as Et, FastifyWithDecorators as F, IControllerResponse as Fn, AuthPluginOptions as Ft, OnTenantDeleteConfig as G, SlugExt as Gt, CrudController as H, SoftDeleteMixin as Ht, MiddlewareHandler as I, IRequestContext as In, Authenticator as It, PresetHook as J, BulkMixin as Jt, ResolvedTenantPurge as K, SlugMixin as Kt, RequestWithExtras as L, RawRouteHandler as Ln, AuthenticatorContext as Lt, EventsDecorator as M, ControllerLike as Mn, RouteMethod as Mt, FastifyRequestExtras as N, FastifyHandler as Nn, RateLimitConfig as Nt, HealthOptions as O, ResourceWrites as On, ActionHandlerFn as Ot, FastifyWithAuth as P, IController as Pn, AuthHelpers as Pt, MiddlewareConfig as Q, AccessControl as Qt, RegisterOptions as R, RouteHandler as Rn, JwtContext as Rt, QueryParserInterface as S, ControllerConfigurableOptions as Sn, PipelineContext as St, CrudRouterOptions as T, BodySanitizer as Tn, ResourceCacheConfig as Tt, ResourceConfig as U, TreeExt as Ut, ResourceDefinition as V, SoftDeleteExt as Vt, ResourcePermissions as W, TreeMixin as Wt, CrudRouteKey as X, QueryResolver as Xt, PresetResult as Y, BaseCrudController as Yt, CrudSchemas as Z, QueryResolverConfig as Zt, ControllerQueryOptions as _, ArcDeleteResult as _n, Guard as _t, InferAdapterDoc as a, HookRegistration as an, ResourceExtensions as at, ParsedQuery as b, ArcUpdateResult as bn, OperationFilter as bt, TypedController as c, afterCreate as cn, AggMeasureShorthand as ct, PaginationResult as d, beforeCreate as dn, AggregationDateRangeRequirement as dt, DefineHookOptions as en, RouteSchemaOptions as et, IntrospectionData as f, beforeDelete as fn, AggregationIndexHint as ft, ArcInternalMetadata as g, ArcCreateResult as gn, AggregationsMap as gt, ResourceMetadata as h, defineHook as hn, AggregationRateLimit as ht, ValidationResult as i, HookPhase as in, ResourceHooks as it, ArcDecorator as j, ControllerHandler as jn, RouteMcpConfig as jt, IntrospectionPluginOptions as k, WriteContext as kn, ActionsMap as kt, TypedRepository as l, afterDelete as ln, AggregationCacheConfig as lt, RegistryStats as m, createHookSystem as mn, AggregationMaterializedResult as mt, ConfigError as n, HookHandler as nn, FieldRule$1 as nt, InferDocType as o, HookSystem as on, EventDefinition as ot, RegistryEntry as p, beforeUpdate as pn, AggregationMaterializedContext as pt, PresetFunction as q, BulkExt as qt, ValidateOptions as r, HookOperation as rn, ResourceHookContext as rt, InferResourceDoc as s, HookSystemOptions as sn, AggMeasureInput as st, RouteHandlerMethod$1 as t, HookContext as tn, ArcFieldRule as tt, TypedResourceConfig as u, afterUpdate as un, AggregationConfig as ut, LookupOption as v, ArcGetResult as vn, Interceptor as vt, ServiceContext as w, ListResult as wn, Transform as wt, PopulateOption as x, BaseControllerOptions as xn, PipelineConfig as xt, OwnershipCheck as y, ArcListResult as yn, NextFunction as yt, ResourceRegistry as z, ServerAccessor as zn, TokenPair as zt };
4659
+ export { ResourceHookContext as $, AccessControl as $t, RequestWithExtras as A, MixinConstructor as An, AuthHelpers as At, OnTenantDeleteConfig as B, IController as Bn, TreeMixin as Bt, RequestIdOptions as C, ArcGetResult as Cn, ActionEntry as Ct, FastifyWithAuth as D, ControllerConfigurableOptions as Dn, RouteMcpConfig as Dt, FastifyRequestExtras as E, BaseControllerOptions as En, RouteDefinition as Et, ResourceDefinition as F, WriteContext as Fn, TokenPair as Ft, CrudRouteKey as G, ServerAccessor as Gn, BaseCrudController as Gt, PresetFunction as H, IRequestContext as Hn, SlugMixin as Ht, CrudController as I, WriteVerbKey as In, BaseController as It, MiddlewareConfig as J, ActionTargetCheck as Jt, CrudSchemas as K, QueryResolver as Kt, ResourceConfig as L, ControllerHandler as Ln, SoftDeleteExt as Lt, ResourceRegistry as M, BodySanitizerConfig as Mn, Authenticator as Mt, RouteRow as N, MutationWriteContext as Nn, AuthenticatorContext as Nt, FastifyWithDecorators as O, ControllerConstructionOptions as On, RouteMethod as Ot, defineResource as P, ResourceWrites as Pn, JwtContext as Pt, FieldRule$1 as Q, getEntityQuery as Qt, ResourceIdempotencyConfig as R, ControllerLike as Rn, SoftDeleteMixin as Rt, IntrospectionPluginOptions as S, ArcDeleteResult as Sn, ActionDefinition as St, EventsDecorator as T, ArcUpdateResult as Tn, ActionsMap as Tt, PresetHook as U, RawRouteHandler as Un, BulkExt as Ut, ResolvedTenantPurge as V, IControllerResponse as Vn, SlugExt as Vt, PresetResult as W, RouteHandler as Wn, BulkMixin as Wt, RouteSchemaOptions as X, getEntityId as Xt, OpenApiSchemas as Y, getActionTarget as Yt, ArcFieldRule as Z, getEntityIdField as Zt, ServiceContext as _, beforeDelete as _n, PipelineConfig as _t, IntrospectionData as a, DefineHookOptions as an, AggregationCacheConfig as at, HealthCheck as b, defineHook as bn, Transform as bt, ResourceMetadata as c, HookOperation as cn, AggregationIndexHint as ct, LookupOption as d, HookSystem as dn, AggregationRateLimit as dt, AccessControlConfig as en, ResourceHooks as et, OwnershipCheck as f, HookSystemOptions as fn, AggregationsMap as ft, RequestContext as g, beforeCreate as gn, OperationFilter as gt, QueryParserInterface as h, afterUpdate as hn, NextFunction as ht, PaginationResult as i, AroundHookHandler as in, AggMeasureShorthand as it, RegisterOptions as j, BodySanitizer as jn, AuthPluginOptions as jt, MiddlewareHandler as k, ListResult as kn, RateLimitConfig as kt, ArcInternalMetadata as l, HookPhase as ln, AggregationMaterializedContext as lt, PopulateOption as m, afterDelete as mn, Interceptor as mt, InferDocType as n, FetchDenialReason as nn, EventDefinition as nt, RegistryEntry as o, HookContext as on, AggregationConfig as ot, ParsedQuery as p, afterCreate as pn, Guard as pt, LookupPolicy as q, QueryResolverConfig as qt, InferResourceDoc as r, FetchResult as rn, AggMeasureInput as rt, RegistryStats as s, HookHandler as sn, AggregationDateRangeRequirement as st, InferAdapterDoc as t, AccessControlRepository as tn, ResourceExtensions as tt, ControllerQueryOptions as u, HookRegistration as un, AggregationMaterializedResult as ut, CrudRouterOptions as v, beforeUpdate as vn, PipelineContext as vt, ArcDecorator as w, ArcListResult as wn, ActionHandlerFn as wt, HealthOptions as x, ArcCreateResult as xn, ResourceCacheConfig as xt, GracefulShutdownOptions as y, createHookSystem as yn, PipelineStep as yt, ResourcePermissions as z, FastifyHandler as zn, TreeExt as zt };