@classytic/arc 2.30.0 → 2.32.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 (242) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +7 -0
  3. package/TRADEMARK.md +22 -0
  4. package/dist/{BaseController-B5DwufbB.mjs → BaseController-Dk34dxhI.mjs} +21 -19
  5. package/dist/{EventTransport-QmY7A258.d.mts → EventTransport-2uVV34WU.d.mts} +1 -0
  6. package/dist/{EventTransport-C6YA8xY9.mjs → EventTransport-BV10D0oG.mjs} +1 -0
  7. package/dist/{HookSystem-WkIougo6.mjs → HookSystem-vimw5D7Z.mjs} +2 -1
  8. package/dist/{QueryCache-DOMT_iq7.d.mts → QueryCache-DFA-QrU9.d.mts} +2 -1
  9. package/dist/{ResourceRegistry-y9Hnypus.mjs → ResourceRegistry-BVr7PgA0.mjs} +18 -15
  10. package/dist/{actionPermissions-CAphcmUG.mjs → actionPermissions-Dti7ebyv.mjs} +1 -0
  11. package/dist/audit/index.d.mts +3 -2
  12. package/dist/audit/index.mjs +2 -1
  13. package/dist/auth/audit.d.mts +1 -0
  14. package/dist/auth/audit.mjs +1 -0
  15. package/dist/auth/index.d.mts +5 -4
  16. package/dist/auth/index.mjs +11 -9
  17. package/dist/auth/redis-session.d.mts +2 -1
  18. package/dist/auth/redis-session.mjs +1 -0
  19. package/dist/{applyPermissionResult-Cf6zUCvb.mjs → authorizationDecision-__T3EJDA.mjs} +19 -10
  20. package/dist/{buildHandler-BYS3Vkfz.mjs → buildHandler-Dn7rzUJo.mjs} +6 -4
  21. package/dist/cache/index.d.mts +4 -3
  22. package/dist/cache/index.mjs +4 -3
  23. package/dist/{caching-_NJeDd1_.mjs → caching-DFJqHyFs.mjs} +2 -1
  24. package/dist/cleanup/index.d.mts +39 -16
  25. package/dist/cleanup/index.mjs +50 -38
  26. package/dist/cli/commands/describe.d.mts +2 -1
  27. package/dist/cli/commands/describe.mjs +4 -5
  28. package/dist/cli/commands/docs.d.mts +1 -0
  29. package/dist/cli/commands/docs.mjs +3 -2
  30. package/dist/cli/commands/doctor.d.mts +1 -0
  31. package/dist/cli/commands/doctor.mjs +1 -0
  32. package/dist/cli/commands/generate.d.mts +2 -1
  33. package/dist/cli/commands/generate.mjs +2 -1
  34. package/dist/cli/commands/init.d.mts +2 -1
  35. package/dist/cli/commands/init.mjs +2 -1
  36. package/dist/cli/commands/introspect.d.mts +1 -0
  37. package/dist/cli/commands/introspect.mjs +2 -1
  38. package/dist/cli/index.d.mts +3 -2
  39. package/dist/cli/index.mjs +3 -2
  40. package/dist/{constants-DNre0wRg.mjs → constants-J99LGTJI.mjs} +1 -0
  41. package/dist/context/index.d.mts +1 -0
  42. package/dist/context/index.mjs +3 -2
  43. package/dist/core/index.d.mts +3 -2
  44. package/dist/core/index.mjs +7 -6
  45. package/dist/{core-poalJLvp.mjs → core-BwwmNSp7.mjs} +3 -2
  46. package/dist/{createActionRouter-BxuywVRB.mjs → createActionRouter-DkJZWo0m.mjs} +14 -10
  47. package/dist/{createAggregationRouter-BiY-lGdY.mjs → createAggregationRouter-B1exOVD8.mjs} +10 -7
  48. package/dist/{createApp-CXrKFciH.mjs → createApp-Bv0m-Q6_.mjs} +625 -229
  49. package/dist/{defineAggregation-ufWjUwml.mjs → defineAggregation-gX4No1gt.mjs} +1 -0
  50. package/dist/{defineResource-CxWqDCAy.mjs → defineResource-XFPPUu2c.mjs} +84 -50
  51. package/dist/discovery/index.d.mts +1 -0
  52. package/dist/discovery/index.mjs +1 -0
  53. package/dist/docs/index.d.mts +3 -2
  54. package/dist/docs/index.mjs +3 -2
  55. package/dist/{elevation-C9s5hFP_.d.mts → elevation-BXCfVcNG.d.mts} +1 -0
  56. package/dist/{elevation-CIz1GjGm.mjs → elevation-DNXXxgjl.mjs} +4 -3
  57. package/dist/encryption/index.d.mts +2 -1
  58. package/dist/encryption/index.mjs +5 -3
  59. package/dist/{errorHandler-Bq2OeEQL.mjs → errorHandler-B2AZUJUj.mjs} +3 -2
  60. package/dist/{errorHandler-C6yv4ZTW.d.mts → errorHandler-CpYUJmPA.d.mts} +1 -0
  61. package/dist/{errors-DHcHVAgk.d.mts → errors-BGdVwP25.d.mts} +1 -0
  62. package/dist/{errors-DkO8Nk6-.mjs → errors-Bh1KK5ps.mjs} +1 -0
  63. package/dist/{eventPlugin-D2NNR4py.mjs → eventPlugin-Dk_TyXeE.mjs} +8 -6
  64. package/dist/{eventPlugin-Bg3cP2_u.d.mts → eventPlugin-GF5nnBIq.d.mts} +2 -1
  65. package/dist/events/index.d.mts +112 -44
  66. package/dist/events/index.mjs +177 -141
  67. package/dist/events/transports/redis-stream-entry.d.mts +2 -1
  68. package/dist/events/transports/redis-stream-entry.mjs +14 -9
  69. package/dist/events/transports/redis.d.mts +2 -1
  70. package/dist/events/transports/redis.mjs +1 -0
  71. package/dist/{externalPaths-BE6wFpYv.d.mts → externalPaths-CmGOxOtD.d.mts} +1 -0
  72. package/dist/factory/index.d.mts +9 -5
  73. package/dist/factory/index.mjs +131 -7
  74. package/dist/fastify-augmentation-BppG5bMT.d.mts +179 -0
  75. package/dist/{fields-CQ_7KQWw.mjs → fields-DEi65pGj.mjs} +6 -10
  76. package/dist/{fields-D3lhgzhD.d.mts → fields-I_EeRWND.d.mts} +1 -0
  77. package/dist/{generate-D0wZupBg.mjs → generate-CRSG0jTv.mjs} +2 -1
  78. package/dist/{generate-BnsL9GBm.d.mts → generate-DCGuEbqI.d.mts} +1 -0
  79. package/dist/{headers-BvlQSDMB.mjs → headers-BXjQYbeC.mjs} +2 -1
  80. package/dist/{health-CB_m4S32.mjs → health-5XnTa2uo.mjs} +1 -0
  81. package/dist/hooks/index.d.mts +2 -1
  82. package/dist/hooks/index.mjs +2 -1
  83. package/dist/idempotency/index.d.mts +3 -2
  84. package/dist/idempotency/index.mjs +8 -5
  85. package/dist/idempotency/redis.d.mts +2 -1
  86. package/dist/idempotency/redis.mjs +1 -0
  87. package/dist/{index-CG4yqMDB.d.mts → index-C-481Zf2.d.mts} +74 -18
  88. package/dist/{index-Q5gzEIWY.d.mts → index-CpzxqePp.d.mts} +107 -32
  89. package/dist/{index-Cx7bro4R.d.mts → index-DXlHwvuE.d.mts} +5 -4
  90. package/dist/index-ZWhLTp_s.d.mts +290 -0
  91. package/dist/{index-Ch16aAah.d.mts → index-zq6VrjaB.d.mts} +122 -15
  92. package/dist/index.d.mts +9 -8
  93. package/dist/index.mjs +11 -10
  94. package/dist/{init-tlRSt667.mjs → init-BbfP8aky.mjs} +25 -27
  95. package/dist/{init-BPEu8cs-.d.mts → init-Cscs4lab.d.mts} +1 -0
  96. package/dist/integrations/event-gateway.d.mts +3 -2
  97. package/dist/integrations/event-gateway.mjs +2 -1
  98. package/dist/integrations/index.d.mts +4 -3
  99. package/dist/integrations/index.mjs +1 -0
  100. package/dist/integrations/jobs/index.d.mts +2 -1
  101. package/dist/integrations/jobs/index.mjs +109 -33
  102. package/dist/integrations/mcp/index.d.mts +6 -6
  103. package/dist/integrations/mcp/index.mjs +2 -1
  104. package/dist/integrations/mcp/testing.d.mts +3 -2
  105. package/dist/integrations/mcp/testing.mjs +2 -2
  106. package/dist/integrations/streamline.d.mts +1 -0
  107. package/dist/integrations/streamline.mjs +3 -2
  108. package/dist/integrations/webhooks.d.mts +1 -0
  109. package/dist/integrations/webhooks.mjs +1 -0
  110. package/dist/integrations/websocket-pushref-redis.d.mts +2 -1
  111. package/dist/integrations/websocket-pushref-redis.mjs +1 -0
  112. package/dist/integrations/websocket-redis.d.mts +2 -1
  113. package/dist/integrations/websocket-redis.mjs +1 -0
  114. package/dist/integrations/websocket.d.mts +3 -2
  115. package/dist/integrations/websocket.mjs +38 -34
  116. package/dist/{interface-BGZCzOGL.d.mts → interface-BOk3YnTz.d.mts} +1 -0
  117. package/dist/{interface-n5hpKg1d.mjs → interface-BP4IJCBO.mjs} +1 -0
  118. package/dist/{interface-Uk8ONJv7.d.mts → interface-DEWNOMza.d.mts} +1 -0
  119. package/dist/{jsonBody-BlzAoGCe.mjs → jsonBody-DDSR2HFl.mjs} +2 -1
  120. package/dist/{keys-Cd316u_y.mjs → keys-B-HBTGXl.mjs} +1 -0
  121. package/dist/{loadResources-DpU8pfjV.mjs → loadResources-DisEAmkm.mjs} +4 -2
  122. package/dist/{loadResourcesFromEntry-kvGT6EcO.mjs → loadResourcesFromEntry-D1MJp13f.mjs} +2 -1
  123. package/dist/logger/index.d.mts +1 -0
  124. package/dist/logger/index.mjs +1 -0
  125. package/dist/{mcpPlugin-nRzm5g2s.mjs → mcpPlugin-CpfVpKHE.mjs} +38 -38
  126. package/dist/{memory-CmnzXE5Z.mjs → memory-DsuWnj4T.mjs} +5 -4
  127. package/dist/{metrics-fdxI5mn_.mjs → metrics-DdQL8syd.mjs} +2 -1
  128. package/dist/middleware/index.d.mts +2 -1
  129. package/dist/middleware/index.mjs +3 -2
  130. package/dist/migrations/index.d.mts +1 -0
  131. package/dist/migrations/index.mjs +3 -2
  132. package/dist/module-Br81am6m.mjs +78 -0
  133. package/dist/{multipartBody-D0eBvkeq.mjs → multipartBody-BnkBLN7v.mjs} +3 -2
  134. package/dist/{openapi-B8Vkbxbn.mjs → openapi-DIRzvxC0.mjs} +6 -5
  135. package/dist/outbox-admin/index.d.mts +85 -0
  136. package/dist/outbox-admin/index.mjs +115 -0
  137. package/dist/permissions/index.d.mts +5 -4
  138. package/dist/permissions/index.mjs +6 -5
  139. package/dist/{permissions-Bfu_8y1U.mjs → permissions-Dl_m0NCe.mjs} +308 -42
  140. package/dist/{pipe-BOLmGIxa.mjs → pipe-ELHCP3zj.mjs} +3 -4
  141. package/dist/pipeline/index.d.mts +2 -1
  142. package/dist/pipeline/index.mjs +2 -1
  143. package/dist/{pipeline-BJnpkY9i.mjs → pipeline-CPA-jx0M.mjs} +17 -14
  144. package/dist/plugins/index.d.mts +8 -7
  145. package/dist/plugins/index.mjs +16 -15
  146. package/dist/plugins/response-cache.d.mts +1 -0
  147. package/dist/plugins/response-cache.mjs +2 -1
  148. package/dist/plugins/tracing-entry.d.mts +2 -1
  149. package/dist/plugins/tracing-entry.mjs +2 -1
  150. package/dist/{pluralize-BhrgwZb9.mjs → pluralize-C2ZkKbsn.mjs} +1 -0
  151. package/dist/{preloadResources-DSY5HGpS.mjs → preloadResources-CDs87fTw.mjs} +1 -0
  152. package/dist/{preloadResources-DCfa2egz.d.mts → preloadResources-T1hmlTEZ.d.mts} +117 -12
  153. package/dist/presets/filesUpload.d.mts +6 -5
  154. package/dist/presets/filesUpload.mjs +10 -12
  155. package/dist/presets/index.d.mts +3 -2
  156. package/dist/presets/index.mjs +2 -1
  157. package/dist/presets/multiTenant.d.mts +2 -1
  158. package/dist/presets/multiTenant.mjs +3 -2
  159. package/dist/presets/search.d.mts +3 -2
  160. package/dist/presets/search.mjs +2 -1
  161. package/dist/{presets-CJOEXLuk.mjs → presets-Dntd_pUO.mjs} +6 -4
  162. package/dist/{projection-UBDgfMwu.mjs → projection-CjHhM5W1.mjs} +2 -1
  163. package/dist/{pushref-registry-BbcnpNTm.d.mts → pushref-registry-Dov9l4qE.d.mts} +1 -0
  164. package/dist/{queryCachePlugin-br4otiqF.d.mts → queryCachePlugin-BzqxconG.d.mts} +3 -2
  165. package/dist/{queryCachePlugin-DSGuSVLW.mjs → queryCachePlugin-Dhps-Ldt.mjs} +6 -5
  166. package/dist/{rateLimit-DrED-C4v.mjs → rateLimit-C7mmAGs3.mjs} +7 -3
  167. package/dist/{rateLimitKey-BoWRH3e7.mjs → rateLimitKey-CccEFBfk.mjs} +1 -0
  168. package/dist/{realtime-B9nmIY5V.mjs → realtime-DVvVZZFz.mjs} +20 -9
  169. package/dist/{redis-UDtLaNj6.d.mts → redis-CduhKsJi.d.mts} +2 -1
  170. package/dist/{redis-stream-uIaauNpZ.d.mts → redis-stream-qN6A-RFv.d.mts} +2 -1
  171. package/dist/{registerRoute-JkxIEBJq.mjs → registerRoute-B_QrjYZS.mjs} +10 -7
  172. package/dist/registry/index.d.mts +3 -2
  173. package/dist/registry/index.mjs +3 -2
  174. package/dist/{registry-BYXUUg21.mjs → registry-DqUmzA9W.mjs} +2 -1
  175. package/dist/{renewingLease-3cuQH7h8.mjs → renewingLease-BpP9wwj7.mjs} +1 -0
  176. package/dist/{replyHelpers-67tRawQZ.mjs → replyHelpers-DS-P0b32.mjs} +2 -1
  177. package/dist/{repositoryFilter-Dz8k3S4P.mjs → repositoryFilter-DotlxG3L.mjs} +1 -0
  178. package/dist/{requestContext-MrlJwECf.mjs → requestContext-B6vQF-sy.mjs} +1 -0
  179. package/dist/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-BhflRrBs.mjs} +1 -0
  180. package/dist/{schedules-CjOd2MG9.mjs → schedules-C2Q3uv2d.mjs} +3 -2
  181. package/dist/{schemaConverter-DJvodiHj.mjs → schemaConverter-Crjvczi_.mjs} +70 -17
  182. package/dist/{schemaIR-Ca7ySNPx.mjs → schemaIR-CnyKK_1j.mjs} +2 -1
  183. package/dist/schemas/index.d.mts +1 -0
  184. package/dist/schemas/index.mjs +1 -0
  185. package/dist/scim/index.d.mts +1 -0
  186. package/dist/scim/index.mjs +2 -1
  187. package/dist/scope/index.d.mts +3 -2
  188. package/dist/scope/index.mjs +5 -4
  189. package/dist/{scope-DfaPntpo.mjs → scope-afz_Ieek.mjs} +4 -3
  190. package/dist/{sessionManager-BHOM3N_0.d.mts → sessionManager-qimlKV0P.d.mts} +1 -0
  191. package/dist/{simpleEqualityMatcher-Cg05HPsn.mjs → simpleEqualityMatcher-C4KiMWUa.mjs} +1 -0
  192. package/dist/{sse-BQ1w0dfl.mjs → sse-B9P4Y-UI.mjs} +7 -5
  193. package/dist/{sseStream-BQ7Qaynp.mjs → sseStream-B3zb_d1h.mjs} +2 -1
  194. package/dist/{storage-DLUZ04kK.d.mts → storage-Cx9Sz3Ou.d.mts} +1 -0
  195. package/dist/{store-helpers-DY6xq4sM.mjs → store-helpers-DmEMHlPQ.mjs} +1 -0
  196. package/dist/{streaming-BQcW6_Kq.mjs → streaming-DPZdQmDz.mjs} +1 -0
  197. package/dist/{subscribe-helpers-DfzsivM6.mjs → subscribe-helpers-sp5xTDVA.mjs} +2 -1
  198. package/dist/sync/changelog.d.mts +1 -0
  199. package/dist/sync/changelog.mjs +1 -0
  200. package/dist/testing/cleanupStoreContract.d.mts +2 -1
  201. package/dist/testing/cleanupStoreContract.mjs +10 -5
  202. package/dist/testing/index.d.mts +166 -12
  203. package/dist/testing/index.mjs +315 -43
  204. package/dist/testing/outboxStoreContract.d.mts +12 -2
  205. package/dist/testing/outboxStoreContract.mjs +110 -20
  206. package/dist/testing/storageContract.d.mts +2 -1
  207. package/dist/testing/storageContract.mjs +11 -5
  208. package/dist/{tracing-CsGKQJGg.d.mts → tracing-Dro5eH6j.d.mts} +1 -0
  209. package/dist/{transactionContext-PNdoyLLm.mjs → transactionContext-BvLKvOIL.mjs} +1 -0
  210. package/dist/{typeGuards-C9KXUly6.mjs → typeGuards-B_5-hKjJ.mjs} +1 -0
  211. package/dist/types/index.d.mts +6 -5
  212. package/dist/types/index.mjs +1 -0
  213. package/dist/types/storage.d.mts +2 -1
  214. package/dist/types/storage.mjs +1 -0
  215. package/dist/{types-ChvvVNP8.d.mts → types-B7XeLEgK.d.mts} +34 -2
  216. package/dist/{types-Cc91nXuA.d.mts → types-ByHn9rEO.d.mts} +1 -0
  217. package/dist/{types-C_ZZRcD6.d.mts → types-C4QE8qnm.d.mts} +1 -0
  218. package/dist/{types-D_n8L6VY.d.mts → types-CT5vu6M0.d.mts} +132 -17
  219. package/dist/{types-DjC-KUCz.d.mts → types-ChuShpCP.d.mts} +1 -0
  220. package/dist/{types-Bmmn-fO0.d.mts → types-CwcUQ610.d.mts} +2 -6
  221. package/dist/{types-CSyCxl2V.mjs → types-DXYX7dIR.mjs} +4 -3
  222. package/dist/usage/index.d.mts +3 -2
  223. package/dist/usage/index.mjs +0 -0
  224. package/dist/{userHelpers-Cg_y3fBz.mjs → userHelpers-BNsOXzOd.mjs} +1 -0
  225. package/dist/utils/index.d.mts +16 -7
  226. package/dist/utils/index.mjs +10 -9
  227. package/dist/{utils-taUVD4p3.mjs → utils-BtOha_Sp.mjs} +3 -2
  228. package/dist/{validate-Bk44F2yT.mjs → validate-PMCPR9El.mjs} +2 -1
  229. package/dist/{versioning-gKWyryT3.mjs → versioning-CKv2jz_U.mjs} +4 -3
  230. package/dist/{versioning-DXgY5BUm.d.mts → versioning-D_rhHfZm.d.mts} +5 -2
  231. package/dist/{websocket-_Pd0CgR0.d.mts → websocket-BOXVggMf.d.mts} +2 -1
  232. package/package.json +557 -539
  233. package/skills/arc/SKILL.md +22 -8
  234. package/skills/arc/references/api-reference.md +4 -0
  235. package/skills/arc/references/auth.md +3 -0
  236. package/skills/arc/references/integrations.md +33 -0
  237. package/skills/arc/references/mcp.md +6 -7
  238. package/skills/arc/references/production.md +24 -25
  239. package/skills/arc-code-review/references/anti-patterns.md +3 -3
  240. package/dist/fastify-augmentation-0WhPnUH7.d.mts +0 -76
  241. package/dist/index-CCzVR-N3.d.mts +0 -107
  242. package/dist/module-_-M7C5ES.mjs +0 -29
@@ -1,3 +1,4 @@
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
1
2
  import { arcLog } from "./logger/index.mjs";
2
3
  //#region src/utils/schemaConverter.ts
3
4
  /**
@@ -25,12 +26,53 @@ import { arcLog } from "./logger/index.mjs";
25
26
  * Using `draft-7` fixes `.positive() / .negative() / .gt() / .lt()` out of the box.
26
27
  * - **`openapi-3.0`** — for OpenAPI doc generation (arc emits OpenAPI 3.0.3). Keeps
27
28
  * the boolean exclusive form that 3.0 tooling expects.
29
+ *
30
+ * ## Input vs output (`io`)
31
+ *
32
+ * A Zod schema has TWO shapes and `z.toJSONSchema()` emits one of them. Getting
33
+ * this wrong is not cosmetic — it rejects legal traffic:
34
+ *
35
+ * ```ts
36
+ * z.object({ title: z.string(), status: z.string().default("draft") })
37
+ * // io: "output" → required: ["title", "status"] ← a client omitting `status` is REJECTED
38
+ * // io: "input" → required: ["title"] ← correct: the default fills it in
39
+ * ```
40
+ *
41
+ * The same applies to `.transform()`: in output mode the node is unrepresentable
42
+ * and degrades to `{}` (no validation at all), while input mode emits the real
43
+ * input type. So arc converts by DIRECTION:
44
+ *
45
+ * - **request** (`body`, `querystring`, `params`, `headers`) → `io: "input"` —
46
+ * validates what the client SENDS, before defaults/transforms/coercion apply.
47
+ * - **response** (and the `entity` doc shape) → `io: "output"` — describes what
48
+ * the server RETURNS, after they apply.
49
+ *
50
+ * One deliberate consequence: a plain `z.object()` emits
51
+ * `additionalProperties: false` in output mode but NOT in input mode, because
52
+ * `z.object()` STRIPS unknown keys rather than rejecting them — so extra keys
53
+ * are legal input. Use `z.strictObject()` when the wire contract must reject
54
+ * them; it keeps `additionalProperties: false` in both modes.
28
55
  */
29
56
  const log = arcLog("schema");
30
57
  /** Default target for Fastify-consumed schemas (matches Fastify v5's default AJV draft). */
31
58
  const DEFAULT_FASTIFY_TARGET = "draft-7";
32
59
  /** Default target for OpenAPI document generation (matches arc's emitted OpenAPI version). */
33
60
  const DEFAULT_OPENAPI_TARGET = "openapi-3.0";
61
+ /**
62
+ * Map arc's target onto one Zod actually branches on.
63
+ *
64
+ * Zod's `target` is typed `"draft-04" | "draft-07" | "draft-2020-12" |
65
+ * "openapi-3.0" | ({} & string)` — that last member accepts ANY string without
66
+ * narrowing, so an unrecognized value is not rejected, it just matches none of
67
+ * the internal `target === ...` branches and silently gets draft-2020-12
68
+ * behaviour. `"draft-7"` is safe (Zod normalizes the alias to `"draft-07"`),
69
+ * but `"openapi-3.1"` is neither official nor normalized. Mapping it explicitly
70
+ * keeps the fallback intentional rather than accidental — and it is the right
71
+ * dialect, since OpenAPI 3.1 aligned its Schema Object with JSON Schema 2020-12.
72
+ */
73
+ function zodTarget(target) {
74
+ return target === "openapi-3.1" ? "draft-2020-12" : target;
75
+ }
34
76
  let _toJSONSchema = null;
35
77
  const zodReady = import("zod").then(({ z }) => {
36
78
  if (typeof z?.toJSONSchema === "function") _toJSONSchema = (schema, opts) => z.toJSONSchema(schema, opts);
@@ -92,22 +134,33 @@ function unconvertible(mode, message, cause) {
92
134
  * Pass `openapi-3.0`/`openapi-3.1` for OpenAPI document generation.
93
135
  * @param mode Failure policy for unconvertible Zod schemas — see
94
136
  * {@link UnconvertibleMode}. Defaults to `'warn'`.
137
+ * @param io Which shape to emit — see {@link SchemaIo} and the module
138
+ * docblock. Defaults to `'output'`, matching Zod's own default;
139
+ * arc's request-side call sites pass `'input'` explicitly.
95
140
  */
96
- function toJsonSchema(input, target = DEFAULT_FASTIFY_TARGET, mode = "warn") {
141
+ function toJsonSchema(input, target = DEFAULT_FASTIFY_TARGET, mode = "warn", io = "output") {
97
142
  if (input == null) return void 0;
98
143
  if (typeof input !== "object") return void 0;
99
- if (isJsonSchema(input)) return input;
144
+ if (isJsonSchema(input)) {
145
+ const plain = input;
146
+ if ("$schema" in plain) {
147
+ const { $schema: _dropped, ...rest } = plain;
148
+ return rest;
149
+ }
150
+ return plain;
151
+ }
100
152
  if (isZodSchema(input)) {
101
153
  if (!_toJSONSchema) return unconvertible(mode, "A Zod schema was passed but zod v4 is not installed (or not yet loaded). Install zod >=4 — arc converts via zod's native z.toJSONSchema().");
102
154
  try {
103
155
  const converted = _toJSONSchema(input, {
104
- target,
156
+ target: zodTarget(target),
157
+ io,
105
158
  unrepresentable: mode === "throw" ? "throw" : "any"
106
159
  });
107
160
  if ("$schema" in converted) delete converted.$schema;
108
161
  return converted;
109
162
  } catch (cause) {
110
- return unconvertible(mode, `z.toJSONSchema() failed for a Zod schema (target: ${target}): ${cause instanceof Error ? cause.message : String(cause)}. Zod-only features with no JSON Schema equivalent (.refine/.transform/custom checks) cannot express wire validation — enforce those in the handler or a pipeline step.`, cause);
163
+ return unconvertible(mode, `z.toJSONSchema() failed for a Zod schema (target: ${target}, io: ${io}): ${cause instanceof Error ? cause.message : String(cause)}. Types with no JSON Schema equivalent cannot express wire validation. For \`z.date()\` / \`z.bigint()\` on a REQUEST schema, use the wire-shaped form (\`z.iso.datetime()\` / \`z.string()\`) and convert in a hook — JSON has no date or bigint. For \`.refine()\` / \`.transform()\` / custom checks, enforce them in the handler or a pipeline step.`, cause);
111
164
  }
112
165
  }
113
166
  if (isZodV3Schema(input)) return unconvertible(mode, "A Zod v3 schema was passed (has `_def` but no v4 `_zod` marker). arc requires zod >=4 for schema conversion — upgrade zod, or pass plain JSON Schema.");
@@ -122,19 +175,19 @@ function toJsonSchema(input, target = DEFAULT_FASTIFY_TARGET, mode = "warn") {
122
175
  */
123
176
  function convertOpenApiSchemas(schemas, target = DEFAULT_OPENAPI_TARGET) {
124
177
  const result = {};
125
- const schemaFields = [
126
- "entity",
127
- "createBody",
128
- "updateBody",
129
- "params",
130
- "listQuery",
131
- "response"
132
- ];
133
- for (const field of schemaFields) {
178
+ const schemaFields = {
179
+ entity: "output",
180
+ createBody: "input",
181
+ updateBody: "input",
182
+ params: "input",
183
+ listQuery: "input",
184
+ response: "output"
185
+ };
186
+ for (const [field, io] of Object.entries(schemaFields)) {
134
187
  const value = schemas[field];
135
- if (value !== void 0) result[field] = toJsonSchema(value, target) ?? value;
188
+ if (value !== void 0) result[field] = toJsonSchema(value, target, "warn", io) ?? value;
136
189
  }
137
- for (const [key, value] of Object.entries(schemas)) if (!schemaFields.includes(key)) result[key] = value;
190
+ for (const [key, value] of Object.entries(schemas)) if (!(key in schemaFields)) result[key] = value;
138
191
  return result;
139
192
  }
140
193
  /**
@@ -158,11 +211,11 @@ function convertRouteSchema(schema, target = DEFAULT_FASTIFY_TARGET) {
158
211
  "querystring",
159
212
  "params",
160
213
  "headers"
161
- ]) if (result[field] !== void 0) result[field] = toJsonSchema(result[field], target, mode) ?? result[field];
214
+ ]) if (result[field] !== void 0) result[field] = toJsonSchema(result[field], target, mode, "input") ?? result[field];
162
215
  if (result.response !== void 0 && typeof result.response === "object" && result.response !== null) {
163
216
  const responseObj = result.response;
164
217
  const convertedResponse = {};
165
- for (const [statusCode, responseSchema] of Object.entries(responseObj)) convertedResponse[statusCode] = toJsonSchema(responseSchema, target, mode) ?? responseSchema;
218
+ for (const [statusCode, responseSchema] of Object.entries(responseObj)) convertedResponse[statusCode] = toJsonSchema(responseSchema, target, mode, "output") ?? responseSchema;
166
219
  result.response = convertedResponse;
167
220
  }
168
221
  return result;
@@ -1,4 +1,5 @@
1
- import { o as toJsonSchema } from "./schemaConverter-DJvodiHj.mjs";
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
2
+ import { o as toJsonSchema } from "./schemaConverter-Crjvczi_.mjs";
2
3
  //#region src/core/schemaIR.ts
3
4
  /**
4
5
  * Schema IR — one canonical representation, two adapters.
@@ -1,3 +1,4 @@
1
+
1
2
  import { ErrorContract, ErrorDetail } from "@classytic/repo-core/errors";
2
3
  import { Static, TObject, TSchema, TSchema as TSchema$1, Type, Type as Type$1 } from "typebox";
3
4
  import { FastifyPluginAsyncTypebox, FastifyPluginCallbackTypebox, TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
@@ -1,3 +1,4 @@
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
1
2
  import { errorContractSchema, errorDetailSchema } from "@classytic/repo-core/errors";
2
3
  import { Type, Type as Type$1 } from "typebox";
3
4
  //#region src/schemas/index.ts
@@ -1,3 +1,4 @@
1
+
1
2
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
2
3
  import { RepositoryLike } from "@classytic/repo-core/adapter";
3
4
  //#region src/scim/mapping.d.ts
@@ -1,4 +1,5 @@
1
- import { t as parseJsonBody } from "../jsonBody-BlzAoGCe.mjs";
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
2
+ import { t as parseJsonBody } from "../jsonBody-DDSR2HFl.mjs";
2
3
  import fp from "fastify-plugin";
3
4
  //#region src/scim/errors.ts
4
5
  var ScimError = class extends Error {
@@ -1,5 +1,6 @@
1
- import { A as requireOrgId, C as hasOrgAccess, D as isOrgInScope, E as isMember, M as requireUserId, N as scopeFirstCtx, O as isService, S as getUserRoles, T as isElevated, _ as getScopeContextMap, a as ScopeFirstCtx, b as getTenantFromRequest, c as getAncestorOrgIds, d as getMandate, f as getOrgContext, g as getScopeContext, h as getRequestScope, i as RequestScope, j as requireTeamId, k as requireClientId, l as getClientId, m as getOrgRoles, n as Mandate, o as ScopeFirstCtxOptions, p as getOrgId, r as PUBLIC_SCOPE, s as ScopeFirstCtxRequest, t as AUTHENTICATED_SCOPE, u as getDPoPJkt, v as getServiceScopes, w as isAuthenticated, x as getUserId, y as getTeamId } from "../types-DjC-KUCz.mjs";
2
- import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-C9s5hFP_.mjs";
1
+
2
+ import { A as requireOrgId, C as hasOrgAccess, D as isOrgInScope, E as isMember, M as requireUserId, N as scopeFirstCtx, O as isService, S as getUserRoles, T as isElevated, _ as getScopeContextMap, a as ScopeFirstCtx, b as getTenantFromRequest, c as getAncestorOrgIds, d as getMandate, f as getOrgContext, g as getScopeContext, h as getRequestScope, i as RequestScope, j as requireTeamId, k as requireClientId, l as getClientId, m as getOrgRoles, n as Mandate, o as ScopeFirstCtxOptions, p as getOrgId, r as PUBLIC_SCOPE, s as ScopeFirstCtxRequest, t as AUTHENTICATED_SCOPE, u as getDPoPJkt, v as getServiceScopes, w as isAuthenticated, x as getUserId, y as getTeamId } from "../types-ChuShpCP.mjs";
3
+ import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-BXCfVcNG.mjs";
3
4
  import { FastifyReply, FastifyRequest } from "fastify";
4
5
  //#region src/scope/rateLimitKey.d.ts
5
6
  interface RateLimitKeyContext {
@@ -1,5 +1,6 @@
1
- import { C as isService, D as requireUserId, E as requireTeamId, O as scopeFirstCtx, S as isOrgInScope, T as requireOrgId, _ as getUserRoles, a as getDPoPJkt, b as isElevated, c as getOrgId, d as getScopeContext, f as getScopeContextMap, g as getUserId, h as getTenantFromRequest, i as getClientId, l as getOrgRoles, m as getTeamId, n as PUBLIC_SCOPE, o as getMandate, p as getServiceScopes, r as getAncestorOrgIds, s as getOrgContext, t as AUTHENTICATED_SCOPE, u as getRequestScope, v as hasOrgAccess, w as requireClientId, x as isMember, y as isAuthenticated } from "../types-CSyCxl2V.mjs";
2
- import { n as elevation_default, t as elevationPlugin } from "../elevation-CIz1GjGm.mjs";
3
- import { t as createTenantKeyGenerator } from "../rateLimitKey-BoWRH3e7.mjs";
4
- import { t as resolveOrgFromHeader } from "../scope-DfaPntpo.mjs";
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
2
+ import { C as isService, D as requireUserId, E as requireTeamId, O as scopeFirstCtx, S as isOrgInScope, T as requireOrgId, _ as getUserRoles, a as getDPoPJkt, b as isElevated, c as getOrgId, d as getScopeContext, f as getScopeContextMap, g as getUserId, h as getTenantFromRequest, i as getClientId, l as getOrgRoles, m as getTeamId, n as PUBLIC_SCOPE, o as getMandate, p as getServiceScopes, r as getAncestorOrgIds, s as getOrgContext, t as AUTHENTICATED_SCOPE, u as getRequestScope, v as hasOrgAccess, w as requireClientId, x as isMember, y as isAuthenticated } from "../types-DXYX7dIR.mjs";
3
+ import { n as elevation_default, t as elevationPlugin } from "../elevation-DNXXxgjl.mjs";
4
+ import { t as createTenantKeyGenerator } from "../rateLimitKey-CccEFBfk.mjs";
5
+ import { t as resolveOrgFromHeader } from "../scope-afz_Ieek.mjs";
5
6
  export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getAncestorOrgIds, getClientId, getDPoPJkt, getMandate, getOrgContext, getOrgId, getOrgRoles, getRequestScope, getScopeContext, getScopeContextMap, getServiceScopes, getTeamId, getTenantFromRequest, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, isOrgInScope, isService, requireClientId, requireOrgId, requireTeamId, requireUserId, resolveOrgFromHeader, scopeFirstCtx };
@@ -1,6 +1,7 @@
1
- import { a as OrgAccessDeniedError, l as UnauthorizedError } from "./errors-DkO8Nk6-.mjs";
2
- import { n as requireSingleHeaderValue } from "./headers-BvlQSDMB.mjs";
3
- import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
2
+ import { a as OrgAccessDeniedError, l as UnauthorizedError } from "./errors-Bh1KK5ps.mjs";
3
+ import { n as requireSingleHeaderValue } from "./headers-BXjQYbeC.mjs";
4
+ import { n as getUserRoles } from "./userHelpers-BNsOXzOd.mjs";
4
5
  //#region src/scope/resolveOrgFromHeader.ts
5
6
  /**
6
7
  * Create a preHandler hook that resolves org scope from a header.
@@ -1,3 +1,4 @@
1
+
1
2
  import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
2
3
  //#region src/auth/sessionManager.d.ts
3
4
  /**
@@ -1,3 +1,4 @@
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
1
2
  //#region src/utils/simpleEqualityMatcher.ts
2
3
  /**
3
4
  * `simpleEqualityMatcher` — a minimal, dialect-agnostic flat-key equality
@@ -1,8 +1,9 @@
1
- import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-BhflRrBs.mjs";
2
3
  import { arcLog } from "./logger/index.mjs";
3
- import { c as getOrgId, n as PUBLIC_SCOPE } from "./types-CSyCxl2V.mjs";
4
- import { a as promoteStreamTokenToHeader } from "./streaming-BQcW6_Kq.mjs";
5
- import { t as openSseStream } from "./sseStream-BQ7Qaynp.mjs";
4
+ import { c as getOrgId, n as PUBLIC_SCOPE } from "./types-DXYX7dIR.mjs";
5
+ import { a as promoteStreamTokenToHeader } from "./streaming-DPZdQmDz.mjs";
6
+ import { t as openSseStream } from "./sseStream-B3zb_d1h.mjs";
6
7
  import fp from "fastify-plugin";
7
8
  //#region src/plugins/sse.ts
8
9
  var sse_exports = /* @__PURE__ */ __exportAll({
@@ -32,7 +33,8 @@ const ssePlugin = async (fastify, opts = {}) => {
32
33
  handler: async (request, reply) => {
33
34
  const stream = openSseStream(request, reply, { heartbeatMs: heartbeat });
34
35
  stream.onCleanup(() => activeConnections.delete(stream.close));
35
- const requestOrgId = getOrgId(request.scope ?? PUBLIC_SCOPE);
36
+ const scope = request.scope ?? PUBLIC_SCOPE;
37
+ const requestOrgId = getOrgId(scope);
36
38
  const dropOrgEvents = orgScoped && !requestOrgId;
37
39
  for (const pattern of patterns) {
38
40
  const unsub = await fastify.events.subscribe(pattern, async (event) => {
@@ -1,4 +1,5 @@
1
- import { n as forwardedStreamHeaders } from "./streaming-BQcW6_Kq.mjs";
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
2
+ import { n as forwardedStreamHeaders } from "./streaming-DPZdQmDz.mjs";
2
3
  //#region src/utils/sseStream.ts
3
4
  /**
4
5
  * Take over the reply socket and open an SSE stream. Call ONLY after every
@@ -1,3 +1,4 @@
1
+
1
2
  //#region src/types/storage.d.ts
2
3
  /**
3
4
  * Arc's minimal backend-agnostic storage contract.
@@ -1,3 +1,4 @@
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
1
2
  //#region src/utils/store-helpers.ts
2
3
  /**
3
4
  * Classify an error thrown by `getOne` / `getById` / `update` as a
@@ -1,3 +1,4 @@
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
1
2
  import { Readable } from "node:stream";
2
3
  //#region src/utils/streaming.ts
3
4
  /**
@@ -1,4 +1,5 @@
1
- import { n as createEvent } from "./EventTransport-C6YA8xY9.mjs";
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
2
+ import { n as createEvent } from "./EventTransport-BV10D0oG.mjs";
2
3
  //#region src/events/defineEvent.ts
3
4
  /**
4
5
  * defineEvent — Typed Event Definitions with Optional Schema Validation
@@ -1,2 +1,3 @@
1
+
1
2
  import { ChangeEntry, ChangeLogAppendOptions, ChangeLogStore, ChangeOp, ChangesPage, ChangesSinceOptions, CursorExpiredError, MemoryChangeLogStore, PushMutation, PushVerdict, PushVerdictStatus } from "@classytic/repo-core/sync";
2
3
  export { type ChangeEntry, type ChangeLogAppendOptions, type ChangeLogStore, type ChangeOp, type ChangesPage, type ChangesSinceOptions, CursorExpiredError, MemoryChangeLogStore, type PushMutation, type PushVerdict, type PushVerdictStatus };
@@ -1,2 +1,3 @@
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
1
2
  import { CursorExpiredError, MemoryChangeLogStore } from "@classytic/repo-core/sync";
2
3
  export { CursorExpiredError, MemoryChangeLogStore };
@@ -1,4 +1,5 @@
1
- import { D as CleanupRunStore } from "../types-C_ZZRcD6.mjs";
1
+
2
+ import { D as CleanupRunStore } from "../types-C4QE8qnm.mjs";
2
3
  //#region src/testing/cleanupStoreContract.d.ts
3
4
  interface CleanupRunStoreContractSetupResult {
4
5
  store: CleanupRunStore;
@@ -1,3 +1,4 @@
1
+ /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
1
2
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
3
  //#region src/testing/cleanupStoreContract.ts
3
4
  /**
@@ -151,10 +152,11 @@ function runCleanupRunStoreContract(name, setup) {
151
152
  expiresAt: future()
152
153
  });
153
154
  await store.compareAndTransition(run.id, ["queued"], "running");
154
- expect(await store.claim(run.id, {
155
+ const stolen = await store.claim(run.id, {
155
156
  token: uid("thief"),
156
157
  expiresAt: future()
157
- })).toBeNull();
158
+ });
159
+ expect(stolen).toBeNull();
158
160
  });
159
161
  it("recovers a running run whose lease EXPIRED (crash recovery)", async () => {
160
162
  const run = makeRun(uid("crashed"), { concurrencyKey: void 0 });
@@ -233,9 +235,11 @@ function runCleanupRunStoreContract(name, setup) {
233
235
  token: live,
234
236
  expiresAt: future()
235
237
  });
236
- expect(await store.compareAndTransition(run.id, ["running"], "completed", { completedAt: /* @__PURE__ */ new Date() }, "a-stale-token")).toBeNull();
238
+ const clobbered = await store.compareAndTransition(run.id, ["running"], "completed", { completedAt: /* @__PURE__ */ new Date() }, "a-stale-token");
239
+ expect(clobbered).toBeNull();
237
240
  expect((await store.get(run.id))?.status).toBe("running");
238
- expect((await store.compareAndTransition(run.id, ["running"], "completed", void 0, live))?.status).toBe("completed");
241
+ const ok = await store.compareAndTransition(run.id, ["running"], "completed", void 0, live);
242
+ expect(ok?.status).toBe("completed");
239
243
  });
240
244
  });
241
245
  describe("requestCancel — durable cancellation flag", () => {
@@ -352,7 +356,8 @@ function runCleanupRunStoreContract(name, setup) {
352
356
  const run = makeRun(uid("rearm-cancelled"), { concurrencyKey: void 0 });
353
357
  await store.createIfPermitted(run);
354
358
  await store.compareAndTransition(run.id, ["queued"], "cancelled");
355
- expect((await store.reArmIfPermitted(run.id, { queuedAt: /* @__PURE__ */ new Date() }))?.status).toBe("queued");
359
+ const requeued = await store.reArmIfPermitted(run.id, { queuedAt: /* @__PURE__ */ new Date() });
360
+ expect(requeued?.status).toBe("queued");
356
361
  });
357
362
  it("refuses a run that is not in a terminal status", async () => {
358
363
  const run = makeRun(uid("rearm-live"), { concurrencyKey: void 0 });
@@ -1,8 +1,10 @@
1
- import { V as ResourceDefinition } from "../index-Q5gzEIWY.mjs";
2
- import { l as AnyRecord, r as PermissionCheck } from "../types-ChvvVNP8.mjs";
3
- import { n as ArcModuleInput } from "../types-D_n8L6VY.mjs";
4
- import "../index-CCzVR-N3.mjs";
5
- import { n as preloadResourcesAsync, r as CreateAppOptions, t as preloadResources } from "../preloadResources-DCfa2egz.mjs";
1
+
2
+ import { V as ResourceDefinition } from "../index-CpzxqePp.mjs";
3
+ import { i as RequestScope } from "../types-ChuShpCP.mjs";
4
+ import { l as AnyRecord, r as PermissionCheck } from "../types-B7XeLEgK.mjs";
5
+ import { n as ArcModuleInput, o as ModuleDisposer, s as ModuleSetupContext } from "../types-CT5vu6M0.mjs";
6
+ import "../index-ZWhLTp_s.mjs";
7
+ import { n as preloadResourcesAsync, r as CreateAppOptions, t as preloadResources } from "../preloadResources-T1hmlTEZ.mjs";
6
8
  import { StorageContractSetup, StorageContractSetupResult, runStorageContract } from "./storageContract.mjs";
7
9
  import { FastifyInstance, FastifyServerOptions } from "fastify";
8
10
  import { Mock } from "vitest";
@@ -84,6 +86,18 @@ interface SurfaceObservation {
84
86
  interface AuthorizationConformance {
85
87
  /** `GET /confs` — the CRUD list surface. */
86
88
  list: SurfaceObservation;
89
+ /**
90
+ * `GET /confs/:id` — the SINGLE-RECORD surface. Its `filter` is the compound
91
+ * `buildIdFilter` output (route id AND policy AND tenant), so a policy that
92
+ * tried to redirect/widen the target would show here — the surface the
93
+ * filter-overwrite class of bug lives on.
94
+ *
95
+ * `filter` is `undefined` when the permission carries NO policy: `fetchDetailed`
96
+ * only builds a compound filter when there is something to compound, and takes a
97
+ * plain by-id read otherwise. `status` is meaningful on both paths — assert the
98
+ * filter only when the permission under test emits a policy.
99
+ */
100
+ get: SurfaceObservation;
87
101
  /** `GET /confs/aggregations/tally` — the aggregation surface. */
88
102
  aggregation: SurfaceObservation;
89
103
  /** Close the throwaway app. Always call (e.g. in `finally`). */
@@ -327,6 +341,81 @@ declare function createBetterAuthTestHelpers(options?: BetterAuthTestHelpersOpti
327
341
  */
328
342
  declare function setupBetterAuthTestApp(input: SetupBetterAuthTestAppInput): Promise<SetupBetterAuthTestAppResult>;
329
343
  //#endregion
344
+ //#region src/testing/testActor.d.ts
345
+ /** The header the testing harness uses to carry a request's actor. */
346
+ declare const TEST_ACTOR_HEADER = "x-test-actor";
347
+ /**
348
+ * What a test wants to be. An `organizationId` produces a `member` scope (the
349
+ * org-scoped case); without one the actor is `authenticated`.
350
+ */
351
+ interface TestActor {
352
+ /**
353
+ * Defaults to a synthetic id derived from the roles. Pass one explicitly when
354
+ * a test distinguishes two actors — ownership and audit assertions do, and
355
+ * two actors sharing roles would otherwise share an id.
356
+ */
357
+ userId?: string;
358
+ /** Global roles — what `requireRoles` reads. */
359
+ roles?: string[];
360
+ /** Tenant. Present ⇒ a `member` scope. */
361
+ organizationId?: string;
362
+ /** Org-level roles from membership records. Defaults to `roles`. */
363
+ orgRoles?: string[];
364
+ teamId?: string;
365
+ /** App-defined dimensions (branch, project, region) read via `getScopeContext`. */
366
+ context?: Record<string, string>;
367
+ /**
368
+ * Elevated (platform-admin) scope. The value is the `elevatedBy` audit string
369
+ * arc requires — an elevated scope must record who granted it. `true` is
370
+ * shorthand for `'test'`.
371
+ *
372
+ * An elevated scope carries no roles: elevation is a different kind of actor,
373
+ * not a role. A test needing a role gate AND elevation wants `member`.
374
+ */
375
+ elevated?: string | boolean;
376
+ }
377
+ /**
378
+ * Build the headers that make an injected request act as `actor`.
379
+ *
380
+ * A string is shorthand for a single role. `null` means "stay public", so a test
381
+ * can prove an endpoint is reachable anonymously.
382
+ *
383
+ * @example
384
+ * ```ts
385
+ * t.app.inject({ method: 'GET', url: '/pos/shifts/current',
386
+ * headers: testActorHeaders('manager', ORG) });
387
+ * ```
388
+ */
389
+ declare function testActorHeaders(actor: TestActor | string | null, organizationId?: string): Record<string, string>;
390
+ /**
391
+ * Turn a spec into a `RequestScope`.
392
+ *
393
+ * `orgRoles` defaults to `roles`: a test that says "manager" means it at both
394
+ * levels, and making the caller repeat itself only produces tests that pass a
395
+ * global gate and mysteriously fail an org one.
396
+ */
397
+ declare function scopeFromTestActor(spec: TestActor): RequestScope;
398
+ /**
399
+ * Build the `auth` option that makes the test actor arc's authenticator.
400
+ *
401
+ * It must be an authenticator, not a hook. Fastify encapsulates plugins and
402
+ * modules register DURING `createApp`, so a hook added to the root instance
403
+ * afterwards never runs for their routes — every request still resolves as
404
+ * public and every role gate still answers 401, which reads exactly like a
405
+ * broken authorization rule in the code under test. Registering as
406
+ * `auth: { type: 'authenticator' }` puts the scope where arc's own auth would,
407
+ * at the phase arc expects, so tenant filtering and permission checks behave
408
+ * identically to production.
409
+ *
410
+ * `defaultActor` applies to requests carrying no header. A malformed header
411
+ * REJECTS rather than falling back to public — a test that meant to be an admin
412
+ * and got quietly demoted is the most expensive way to fail.
413
+ */
414
+ declare function testActorAuth(defaultActor?: TestActor): {
415
+ type: "authenticator";
416
+ authenticate: (request: unknown) => Promise<void>;
417
+ };
418
+ //#endregion
330
419
  //#region src/testing/bootModuleApp.d.ts
331
420
  /** Handed to a modules factory — the in-memory DB is live at call time. */
332
421
  /**
@@ -350,8 +439,6 @@ type TestDatabaseFactory<TConn = unknown> = (opts: {
350
439
  interface TestkitContext<TConn = unknown> {
351
440
  /** The provisioned database's connection string. */
352
441
  uri: string;
353
- /** @deprecated Alias of `uri` from the Mongo-default era — prefer `uri`. */
354
- mongoUri: string;
355
442
  /**
356
443
  * Driver connection handle (the default factory yields mongoose's
357
444
  * DEFAULT connection). Generic so DB-typed wrappers assert the driver
@@ -377,13 +464,20 @@ interface BootModuleAppOptions<TConn = unknown> {
377
464
  * boot when a module genuinely exercises one).
378
465
  */
379
466
  app?: Partial<CreateAppOptions>;
467
+ /**
468
+ * Default actor for every injected request that carries no `x-test-actor` header.
469
+ *
470
+ * The harness boots with `auth: false`, so without this every request is
471
+ * `PUBLIC_SCOPE` and role-gated routes answer 401 — correct, but it left tests with
472
+ * no supported way to be somebody. See `testActor.ts` for why the seam lives in the
473
+ * testing entrypoint and can never be reached from production.
474
+ */
475
+ actor?: TestActor;
380
476
  }
381
477
  interface ModuleTestApp<TConn = unknown> {
382
478
  app: FastifyInstance;
383
479
  /** The provisioned database's connection string. */
384
480
  uri: string;
385
- /** @deprecated Alias of `uri` from the Mongo-default era — prefer `uri`. */
386
- mongoUri: string;
387
481
  /** The mongoose `Connection` handed to the modules factory. */
388
482
  connection: TConn;
389
483
  /**
@@ -555,13 +649,26 @@ declare function createSpy<T extends (...args: unknown[]) => unknown>(_name?: st
555
649
  getLastCall(): unknown[];
556
650
  };
557
651
  /**
558
- * Wait for a condition to be true
652
+ * Wait until `condition` holds — the standard way to assert on asynchronous
653
+ * effects in arc's suites.
559
654
  *
560
- * Useful for async testing
655
+ * **Prefer this over `await sleep(n)` + assert.** A fixed delay encodes a guess
656
+ * about scheduling: too short and it flakes the moment the pool is busy, too
657
+ * long and every run pays for the worst case. Polling returns as soon as the
658
+ * effect lands, so the fast path stays fast and a loaded machine simply waits —
659
+ * and the failure mode becomes "this never happened", which is the thing worth
660
+ * asserting.
661
+ *
662
+ * Supply `label`: without it a timeout reports only its own duration, which is
663
+ * the least useful sentence a flaky test can print.
664
+ *
665
+ * @example
666
+ * await waitFor(() => runs >= 3, { label: "3 scheduler ticks" });
561
667
  */
562
668
  declare function waitFor(condition: () => boolean | Promise<boolean>, options?: {
563
669
  timeout?: number;
564
670
  interval?: number;
671
+ label?: string;
565
672
  }): Promise<void>;
566
673
  /**
567
674
  * Create a test timer that can be controlled
@@ -573,6 +680,53 @@ declare function createTestTimer(): {
573
680
  reset: () => void;
574
681
  };
575
682
  //#endregion
683
+ //#region src/testing/moduleSetup.d.ts
684
+ interface TestModuleSetup {
685
+ /** Pass this as the second argument to `bootstrap` / `plugins` / `afterResources`. */
686
+ readonly context: ModuleSetupContext;
687
+ /**
688
+ * Disposers the module registered, in REGISTRATION order (`dispose()` runs
689
+ * them backwards). Assert on `.length` to prove a phase registered teardown
690
+ * at all — the check a no-op `defer` can never fail.
691
+ */
692
+ readonly deferred: readonly ModuleDisposer[];
693
+ /**
694
+ * Run every collected disposer LIFO, then clear them.
695
+ *
696
+ * Best-effort like arc's teardown: one throwing disposer never blocks the
697
+ * ones behind it. Any errors are collected and thrown together at the end, so
698
+ * a test still fails loudly instead of a rejection vanishing into a floating
699
+ * promise.
700
+ */
701
+ dispose(): Promise<void>;
702
+ }
703
+ declare function createTestModuleSetup(): TestModuleSetup;
704
+ //#endregion
705
+ //#region src/testing/scopeRoleGate.d.ts
706
+ /**
707
+ * Grants iff the caller is an org member holding one of `allowed` as an ORG role.
708
+ *
709
+ * This is `requireOrgRole` under a name package tests reach for — deliberately a
710
+ * delegation, not a reimplementation. It previously read `scope.userRoles` while
711
+ * requiring an organization, so it disagreed with production for any actor whose
712
+ * global and org roles differ: given
713
+ * `{ roles: ["employee"], orgRoles: ["manager"], organizationId: "org-1" }`,
714
+ * `requireOrgRole("manager")` allowed and this denied. The tests never caught it
715
+ * because `scopeFromTestActor` defaults `orgRoles` to `roles`, making the two
716
+ * dimensions identical for the common actor.
717
+ *
718
+ * Delegating also inherits the semantics a hand-rolled gate kept missing:
719
+ * elevated scopes pass, and a service (API-key) identity is refused with a
720
+ * message naming `requireServiceScope` instead of silently failing a role test.
721
+ *
722
+ * **Pair it with an org-bearing actor.** `testActorHeaders('manager', ORG)`
723
+ * yields a `member` scope and passes; `testActorHeaders('manager')` yields an
724
+ * `authenticated` scope with no tenant, which is refused — arc's generated CRUD
725
+ * filters by tenant, so a gate that waves a tenant-less request through is how a
726
+ * green test reads another org's rows.
727
+ */
728
+ declare function scopeRoleGate(...allowed: string[]): PermissionCheck;
729
+ //#endregion
576
730
  //#region src/testing/testApp.d.ts
577
731
  type DbMode = "in-memory" | {
578
732
  uri: string;
@@ -650,4 +804,4 @@ declare function createTestApp(options?: CreateTestAppOptions): Promise<TestAppC
650
804
  */
651
805
  declare function createMinimalTestApp(options?: FastifyServerOptions): FastifyInstance;
652
806
  //#endregion
653
- export { type ArcAssertion, type ArcResponseLike, type AuthMode, type AuthResponse, type AuthorizationConformance, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type BetterAuthTestUser, type BootModuleAppOptions, type BootModulesInput, type CreateOrgInput, type CreateTestAppOptions, type DbMode, type FixtureDestroyer, type FixtureFactory, type FixtureRegistration, HttpTestHarness, type HttpTestHarnessOptions, type MockRepository, type ModuleTestApp, type OrgResponse, type RoleConfig, type SetupBetterAuthTestAppInput, type SetupBetterAuthTestAppResult, type SignInInput, type SignUpInput, type StorageContractSetup, type StorageContractSetupResult, type SurfaceObservation, type TestAppContext, type TestAuthProvider, type TestAuthSession, type TestDatabase, type TestDatabaseFactory, type TestFixtures, type TestkitContext, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runAuthorizationConformance, runStorageContract, safeParseBody, setupBetterAuthTestApp, waitFor };
807
+ export { type ArcAssertion, type ArcResponseLike, type AuthMode, type AuthResponse, type AuthorizationConformance, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type BetterAuthTestUser, type BootModuleAppOptions, type BootModulesInput, type CreateOrgInput, type CreateTestAppOptions, type DbMode, type FixtureDestroyer, type FixtureFactory, type FixtureRegistration, HttpTestHarness, type HttpTestHarnessOptions, type MockRepository, type ModuleTestApp, type OrgResponse, type RoleConfig, type SetupBetterAuthTestAppInput, type SetupBetterAuthTestAppResult, type SignInInput, type SignUpInput, type StorageContractSetup, type StorageContractSetupResult, type SurfaceObservation, TEST_ACTOR_HEADER, type TestActor, type TestAppContext, type TestAuthProvider, type TestAuthSession, type TestDatabase, type TestDatabaseFactory, type TestFixtures, type TestModuleSetup, type TestkitContext, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestModuleSetup, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runAuthorizationConformance, runStorageContract, safeParseBody, scopeFromTestActor, scopeRoleGate, setupBetterAuthTestApp, testActorAuth, testActorHeaders, waitFor };