@classytic/arc 2.28.0 → 2.31.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 (239) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +7 -0
  3. package/TRADEMARK.md +22 -0
  4. package/dist/{BaseController-B65zbz9m.mjs → BaseController-De7GqHck.mjs} +25 -24
  5. package/dist/{EventTransport-QmY7A258.d.mts → EventTransport-2uVV34WU.d.mts} +1 -0
  6. package/dist/EventTransport-9mlTgqzE.mjs +104 -0
  7. package/dist/{HookSystem-C1vB_Gc9.mjs → HookSystem-BcmPyq5I.mjs} +16 -35
  8. package/dist/{QueryCache-DOMT_iq7.d.mts → QueryCache-DFA-QrU9.d.mts} +2 -1
  9. package/dist/{ResourceRegistry-y9Hnypus.mjs → ResourceRegistry-qq0mn2s9.mjs} +3 -2
  10. package/dist/{actionPermissions-CAphcmUG.mjs → actionPermissions-BE5NwaQk.mjs} +1 -0
  11. package/dist/audit/index.d.mts +3 -2
  12. package/dist/audit/index.mjs +3 -2
  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 +6 -5
  17. package/dist/auth/redis-session.d.mts +2 -1
  18. package/dist/auth/redis-session.mjs +1 -0
  19. package/dist/authorizationDecision-ChvUQGG1.mjs +266 -0
  20. package/dist/{buildHandler-Dtwd8fdz.mjs → buildHandler-CDrCJzmB.mjs} +34 -7
  21. package/dist/cache/index.d.mts +4 -3
  22. package/dist/cache/index.mjs +4 -3
  23. package/dist/{caching-_NJeDd1_.mjs → caching-BFHA6Scf.mjs} +2 -1
  24. package/dist/cleanup/index.d.mts +4 -3
  25. package/dist/cleanup/index.mjs +9 -13
  26. package/dist/cli/commands/describe.d.mts +2 -1
  27. package/dist/cli/commands/describe.mjs +3 -2
  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-D3Dv4LAV.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 -5
  45. package/dist/{core-BGa8TXJR.mjs → core-BGpIxINN.mjs} +4 -49
  46. package/dist/{createActionRouter-C-N7z2EL.mjs → createActionRouter-CDXEdasl.mjs} +8 -7
  47. package/dist/{createAggregationRouter-wzwFa_dW.mjs → createAggregationRouter-KrQDzx-k.mjs} +10 -22
  48. package/dist/{createApp-Du5G54TM.mjs → createApp-Df89BEni.mjs} +505 -209
  49. package/dist/defineAggregation-DbPPS8RI.mjs +48 -0
  50. package/dist/{defineResource-2zvVVpxg.mjs → defineResource-Bv6EtN1x.mjs} +90 -72
  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-D2l1mh4g.mjs} +4 -3
  57. package/dist/encryption/index.d.mts +2 -1
  58. package/dist/encryption/index.mjs +3 -2
  59. package/dist/{errorHandler-Bq2OeEQL.mjs → errorHandler-C5NeD6bT.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-Cj42AntO.mjs} +1 -0
  63. package/dist/{eventPlugin-DrQ_FesK.mjs → eventPlugin-CkffEihm.mjs} +6 -106
  64. package/dist/{eventPlugin-Bg3cP2_u.d.mts → eventPlugin-GF5nnBIq.d.mts} +2 -1
  65. package/dist/events/index.d.mts +4 -3
  66. package/dist/events/index.mjs +17 -340
  67. package/dist/events/transports/redis-stream-entry.d.mts +2 -1
  68. package/dist/events/transports/redis-stream-entry.mjs +1 -0
  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 +7 -6
  74. package/dist/{fastify-augmentation-OzYGIiU1.d.mts → fastify-augmentation-zh0_AYHa.d.mts} +13 -4
  75. package/dist/{applyPermissionResult-BnyVE9eD.mjs → fields-CB5Cuzk9.mjs} +2 -107
  76. package/dist/{fields-D3lhgzhD.d.mts → fields-I_EeRWND.d.mts} +1 -0
  77. package/dist/{generate-BnsL9GBm.d.mts → generate-DCGuEbqI.d.mts} +1 -0
  78. package/dist/{generate-D0wZupBg.mjs → generate-EcA6ui4i.mjs} +2 -1
  79. package/dist/{headers-BvlQSDMB.mjs → headers-3zvE0rM3.mjs} +2 -1
  80. package/dist/{health-CB_m4S32.mjs → health-DjZYjM7H.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 +5 -4
  85. package/dist/idempotency/redis.d.mts +2 -1
  86. package/dist/idempotency/redis.mjs +1 -0
  87. package/dist/{index-CjVg-Epg.d.mts → index-Bc2ox8_U.d.mts} +6 -5
  88. package/dist/{index-CG4yqMDB.d.mts → index-C-481Zf2.d.mts} +74 -18
  89. package/dist/{index-BMnbzvtO.d.mts → index-CEJNZFtX.d.mts} +242 -24
  90. package/dist/index-CFThn1K9.d.mts +224 -0
  91. package/dist/{index-DPRH0Cme.d.mts → index-CpzxqePp.d.mts} +203 -97
  92. package/dist/index.d.mts +9 -8
  93. package/dist/index.mjs +11 -9
  94. package/dist/{init-tlRSt667.mjs → init-BEiNZBsJ.mjs} +19 -23
  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 +108 -32
  102. package/dist/integrations/mcp/index.d.mts +9 -9
  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 +1 -0
  116. package/dist/{interface-BGZCzOGL.d.mts → interface-BOk3YnTz.d.mts} +1 -0
  117. package/dist/{interface-Uk8ONJv7.d.mts → interface-DEWNOMza.d.mts} +1 -0
  118. package/dist/{interface-n5hpKg1d.mjs → interface-DYzM0q-u.mjs} +1 -0
  119. package/dist/{jsonBody-BlzAoGCe.mjs → jsonBody-BYbLDi5D.mjs} +2 -1
  120. package/dist/{keys-Cd316u_y.mjs → keys-C0_pa1tY.mjs} +1 -0
  121. package/dist/{loadResources-DpU8pfjV.mjs → loadResources-yawi9tAE.mjs} +2 -1
  122. package/dist/{loadResourcesFromEntry-kvGT6EcO.mjs → loadResourcesFromEntry-BAjYZonx.mjs} +2 -1
  123. package/dist/logger/index.d.mts +1 -0
  124. package/dist/logger/index.mjs +1 -0
  125. package/dist/{mcpPlugin-Cs6nRGCt.mjs → mcpPlugin-B_es0hiF.mjs} +52 -60
  126. package/dist/{memory-CmnzXE5Z.mjs → memory-DR6wpa9F.mjs} +2 -1
  127. package/dist/{metrics-fdxI5mn_.mjs → metrics-CUdw_wLB.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 -3
  131. package/dist/migrations/index.mjs +2 -4
  132. package/dist/module-0pFJA6YW.mjs +78 -0
  133. package/dist/{multipartBody-D0eBvkeq.mjs → multipartBody-wOgj_fnj.mjs} +1 -0
  134. package/dist/{openapi-BLV4OMXb.mjs → openapi-B2L4js1v.mjs} +6 -5
  135. package/dist/permissions/index.d.mts +5 -4
  136. package/dist/permissions/index.mjs +6 -4
  137. package/dist/{permissions-BrtiBKGs.mjs → permissions-L--QDAir.mjs} +518 -316
  138. package/dist/{pipe-BOLmGIxa.mjs → pipe-DRDNsI2T.mjs} +2 -1
  139. package/dist/pipeline/index.d.mts +2 -1
  140. package/dist/pipeline/index.mjs +2 -1
  141. package/dist/{pipeline-BGRnV-To.mjs → pipeline-UPQFatit.mjs} +7 -20
  142. package/dist/plugins/index.d.mts +8 -7
  143. package/dist/plugins/index.mjs +16 -15
  144. package/dist/plugins/response-cache.d.mts +1 -0
  145. package/dist/plugins/response-cache.mjs +2 -1
  146. package/dist/plugins/tracing-entry.d.mts +2 -1
  147. package/dist/plugins/tracing-entry.mjs +2 -1
  148. package/dist/{pluralize-BhrgwZb9.mjs → pluralize-O0FZgESl.mjs} +1 -0
  149. package/dist/{preloadResources-CM4uUaFv.d.mts → preloadResources-CyoJMSiO.d.mts} +117 -12
  150. package/dist/{preloadResources-DSY5HGpS.mjs → preloadResources-Dz6BjzM6.mjs} +1 -0
  151. package/dist/presets/filesUpload.d.mts +6 -5
  152. package/dist/presets/filesUpload.mjs +9 -11
  153. package/dist/presets/index.d.mts +15 -2
  154. package/dist/presets/index.mjs +2 -1
  155. package/dist/presets/multiTenant.d.mts +2 -1
  156. package/dist/presets/multiTenant.mjs +3 -2
  157. package/dist/presets/search.d.mts +3 -2
  158. package/dist/presets/search.mjs +2 -1
  159. package/dist/{presets-DwFDVKP0.mjs → presets-C4-g4_4P.mjs} +27 -10
  160. package/dist/projection-BHw6xlOQ.mjs +18 -0
  161. package/dist/{pushref-registry-BbcnpNTm.d.mts → pushref-registry-Dov9l4qE.d.mts} +1 -0
  162. package/dist/{queryCachePlugin-DSGuSVLW.mjs → queryCachePlugin-BZr8v46E.mjs} +5 -4
  163. package/dist/{queryCachePlugin-br4otiqF.d.mts → queryCachePlugin-BzqxconG.d.mts} +3 -2
  164. package/dist/{rateLimit-DrED-C4v.mjs → rateLimit-CMPY5Qms.mjs} +7 -3
  165. package/dist/{rateLimitKey-BoWRH3e7.mjs → rateLimitKey-Dn5JNmkj.mjs} +1 -0
  166. package/dist/{realtime-DEuDu9io.mjs → realtime-C5NoxWFY.mjs} +20 -8
  167. package/dist/{redis-UDtLaNj6.d.mts → redis-CduhKsJi.d.mts} +2 -1
  168. package/dist/{redis-stream-uIaauNpZ.d.mts → redis-stream-qN6A-RFv.d.mts} +2 -1
  169. package/dist/{registerRoute-DTp18Ah_.mjs → registerRoute-CBN84fc7.mjs} +5 -2
  170. package/dist/registry/index.d.mts +3 -2
  171. package/dist/registry/index.mjs +3 -2
  172. package/dist/{registry-9km_w1aP.mjs → registry-4213U4zb.mjs} +3 -2
  173. package/dist/{renewingLease-3cuQH7h8.mjs → renewingLease-Dll8mGgc.mjs} +1 -0
  174. package/dist/{replyHelpers-67tRawQZ.mjs → replyHelpers-DIiZKGUt.mjs} +2 -1
  175. package/dist/repositoryFilter-f9eAfq_l.mjs +45 -0
  176. package/dist/{requestContext-MrlJwECf.mjs → requestContext-2TxXOJ7T.mjs} +1 -0
  177. package/dist/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-CUsjNX9R.mjs} +1 -0
  178. package/dist/{schedules-CjOd2MG9.mjs → schedules-DdtlICTf.mjs} +3 -2
  179. package/dist/{schemaConverter-DJvodiHj.mjs → schemaConverter-D--wdJM-.mjs} +70 -17
  180. package/dist/{schemaIR-Ca7ySNPx.mjs → schemaIR-Bm2enbnh.mjs} +2 -1
  181. package/dist/schemas/index.d.mts +1 -0
  182. package/dist/schemas/index.mjs +1 -0
  183. package/dist/scim/index.d.mts +1 -0
  184. package/dist/scim/index.mjs +2 -1
  185. package/dist/scope/index.d.mts +3 -2
  186. package/dist/scope/index.mjs +5 -4
  187. package/dist/{scope-DfaPntpo.mjs → scope-DHx5Fous.mjs} +4 -3
  188. package/dist/{sessionManager-BHOM3N_0.d.mts → sessionManager-qimlKV0P.d.mts} +1 -0
  189. package/dist/{simpleEqualityMatcher-Cg05HPsn.mjs → simpleEqualityMatcher-DjQJ7FKA.mjs} +1 -0
  190. package/dist/{sse-BQ1w0dfl.mjs → sse-CUTo9y0X.mjs} +5 -4
  191. package/dist/{sseStream-BQ7Qaynp.mjs → sseStream-B_5XcVVd.mjs} +2 -1
  192. package/dist/{storage-DLUZ04kK.d.mts → storage-Cx9Sz3Ou.d.mts} +1 -0
  193. package/dist/{store-helpers-DY6xq4sM.mjs → store-helpers-D1MP_gRG.mjs} +1 -0
  194. package/dist/{streaming-BQcW6_Kq.mjs → streaming-PSZwi8JV.mjs} +1 -0
  195. package/dist/subscribe-helpers-C0Tb2aUp.mjs +340 -0
  196. package/dist/sync/changelog.d.mts +1 -0
  197. package/dist/sync/changelog.mjs +1 -0
  198. package/dist/testing/cleanupStoreContract.d.mts +2 -1
  199. package/dist/testing/cleanupStoreContract.mjs +1 -0
  200. package/dist/testing/index.d.mts +167 -12
  201. package/dist/testing/index.mjs +522 -205
  202. package/dist/testing/outboxStoreContract.d.mts +1 -0
  203. package/dist/testing/outboxStoreContract.mjs +1 -0
  204. package/dist/testing/storageContract.d.mts +2 -1
  205. package/dist/testing/storageContract.mjs +1 -0
  206. package/dist/{tracing-CsGKQJGg.d.mts → tracing-Dro5eH6j.d.mts} +1 -0
  207. package/dist/{transactionContext-PNdoyLLm.mjs → transactionContext-dAfBVUrk.mjs} +1 -0
  208. package/dist/{typeGuards-C9KXUly6.mjs → typeGuards-B0ryPo3I.mjs} +1 -0
  209. package/dist/types/index.d.mts +6 -5
  210. package/dist/types/index.mjs +1 -0
  211. package/dist/types/storage.d.mts +2 -1
  212. package/dist/types/storage.mjs +1 -0
  213. package/dist/{types-Dmw-6Qeo.d.mts → types-B7XeLEgK.d.mts} +127 -90
  214. package/dist/{types-Cc91nXuA.d.mts → types-ByHn9rEO.d.mts} +1 -0
  215. package/dist/{types-C_ZZRcD6.d.mts → types-C4QE8qnm.d.mts} +1 -0
  216. package/dist/{types-Cu6vVJmh.d.mts → types-CBgvkp8O.d.mts} +82 -12
  217. package/dist/{types-DjC-KUCz.d.mts → types-ChuShpCP.d.mts} +1 -0
  218. package/dist/{types-QFGc3Cjv.d.mts → types-CwcUQ610.d.mts} +2 -6
  219. package/dist/{types-CSyCxl2V.mjs → types-Cys0ko9Z.mjs} +4 -3
  220. package/dist/usage/index.d.mts +3 -2
  221. package/dist/usage/index.mjs +0 -0
  222. package/dist/{userHelpers-Cg_y3fBz.mjs → userHelpers-ChYJ6CdK.mjs} +1 -0
  223. package/dist/utils/index.d.mts +16 -10
  224. package/dist/utils/index.mjs +10 -9
  225. package/dist/{utils-taUVD4p3.mjs → utils-vL8BrDE2.mjs} +3 -2
  226. package/dist/{validate-Bk44F2yT.mjs → validate-_KbifpHF.mjs} +2 -1
  227. package/dist/{versioning-gKWyryT3.mjs → versioning-CHDTor3P.mjs} +3 -2
  228. package/dist/{versioning-DXgY5BUm.d.mts → versioning-D_rhHfZm.d.mts} +5 -2
  229. package/dist/{websocket-_Pd0CgR0.d.mts → websocket-BOXVggMf.d.mts} +2 -1
  230. package/package.json +15 -7
  231. package/skills/arc/SKILL.md +22 -8
  232. package/skills/arc/references/api-reference.md +4 -0
  233. package/skills/arc/references/auth.md +3 -0
  234. package/skills/arc/references/integrations.md +33 -0
  235. package/skills/arc/references/mcp.md +6 -7
  236. package/skills/arc/references/production.md +24 -25
  237. package/skills/arc-code-review/references/anti-patterns.md +3 -3
  238. package/dist/index-CZY4EGga.d.mts +0 -107
  239. package/dist/module-_-M7C5ES.mjs +0 -29
@@ -1,7 +1,13 @@
1
- import { t as CRUD_OPERATIONS } from "../constants-DNre0wRg.mjs";
2
- import { n as createApp } from "../createApp-Du5G54TM.mjs";
3
- import { n as getModuleExports } from "../module-_-M7C5ES.mjs";
4
- import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-DSY5HGpS.mjs";
1
+ /*! @classytic/arc v2.31.0 | MIT | https://classytic.com */
2
+ import { t as CRUD_OPERATIONS } from "../constants-D3Dv4LAV.mjs";
3
+ import { g as getUserId } from "../types-Cys0ko9Z.mjs";
4
+ import { t as BaseController } from "../BaseController-De7GqHck.mjs";
5
+ import { t as defineResource } from "../defineResource-Bv6EtN1x.mjs";
6
+ import { t as defineAggregation } from "../defineAggregation-DbPPS8RI.mjs";
7
+ import { S as requireOrgRole } from "../permissions-L--QDAir.mjs";
8
+ import { n as createApp } from "../createApp-Df89BEni.mjs";
9
+ import { n as getModuleExports } from "../module-0pFJA6YW.mjs";
10
+ import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-Dz6BjzM6.mjs";
5
11
  import { runStorageContract } from "./storageContract.mjs";
6
12
  import Fastify from "fastify";
7
13
  import { afterAll, describe, expect, it, vi } from "vitest";
@@ -125,6 +131,339 @@ function expectArc(response) {
125
131
  return assertion;
126
132
  }
127
133
  //#endregion
134
+ //#region src/testing/mocks.ts
135
+ /**
136
+ * Create a mock repository for testing
137
+ *
138
+ * @example
139
+ * const mockRepo = createMockRepository<Product>({
140
+ * getById: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
141
+ * create: vi.fn().mockImplementation(data => Promise.resolve({ id: '1', ...data })),
142
+ * });
143
+ *
144
+ * await mockRepo.getById('1'); // Returns mocked product
145
+ */
146
+ function createMockRepository(overrides = {}) {
147
+ return {
148
+ getAll: vi.fn().mockResolvedValue({
149
+ method: "offset",
150
+ data: [],
151
+ total: 0,
152
+ page: 1,
153
+ limit: 20,
154
+ pages: 0,
155
+ hasNext: false,
156
+ hasPrev: false
157
+ }),
158
+ getById: vi.fn().mockResolvedValue(null),
159
+ create: vi.fn().mockImplementation((data) => Promise.resolve({
160
+ _id: "mock-id",
161
+ ...data
162
+ })),
163
+ update: vi.fn().mockImplementation((_id, data) => Promise.resolve({
164
+ _id: "mock-id",
165
+ ...data
166
+ })),
167
+ delete: vi.fn().mockResolvedValue({
168
+ success: true,
169
+ message: "Deleted"
170
+ }),
171
+ updateMany: vi.fn().mockResolvedValue({
172
+ acknowledged: true,
173
+ matchedCount: 0,
174
+ modifiedCount: 0
175
+ }),
176
+ deleteMany: vi.fn().mockResolvedValue({
177
+ acknowledged: true,
178
+ deletedCount: 0
179
+ }),
180
+ claim: vi.fn().mockResolvedValue(null),
181
+ claimVersion: vi.fn().mockResolvedValue(null),
182
+ capabilities: {
183
+ transactions: false,
184
+ nestedTransactions: false,
185
+ upsert: false,
186
+ duplicateKeyError: false,
187
+ distinct: false,
188
+ aggregate: false,
189
+ getOrCreate: false,
190
+ countAndExists: false
191
+ },
192
+ getBySlug: vi.fn().mockResolvedValue(null),
193
+ getDeleted: vi.fn().mockResolvedValue([]),
194
+ restore: vi.fn().mockResolvedValue(null),
195
+ getTree: vi.fn().mockResolvedValue([]),
196
+ getChildren: vi.fn().mockResolvedValue([]),
197
+ ...overrides
198
+ };
199
+ }
200
+ /**
201
+ * Create a mock user for authentication testing
202
+ */
203
+ function createMockUser(overrides = {}) {
204
+ return {
205
+ _id: "mock-user-id",
206
+ id: "mock-user-id",
207
+ email: "test@example.com",
208
+ roles: ["user"],
209
+ organizationId: null,
210
+ ...overrides
211
+ };
212
+ }
213
+ /**
214
+ * Create a mock Fastify request
215
+ */
216
+ function createMockRequest(overrides = {}) {
217
+ return {
218
+ body: {},
219
+ params: {},
220
+ query: {},
221
+ headers: {},
222
+ user: createMockUser(),
223
+ context: {},
224
+ log: {
225
+ info: vi.fn(),
226
+ warn: vi.fn(),
227
+ error: vi.fn(),
228
+ debug: vi.fn()
229
+ },
230
+ ...overrides
231
+ };
232
+ }
233
+ /**
234
+ * Create a mock Fastify reply
235
+ */
236
+ function createMockReply() {
237
+ return {
238
+ code: vi.fn().mockReturnThis(),
239
+ send: vi.fn().mockReturnThis(),
240
+ header: vi.fn().mockReturnThis(),
241
+ headers: vi.fn().mockReturnThis(),
242
+ status: vi.fn().mockReturnThis(),
243
+ type: vi.fn().mockReturnThis(),
244
+ redirect: vi.fn().mockReturnThis(),
245
+ callNotFound: vi.fn().mockReturnThis(),
246
+ sent: false
247
+ };
248
+ }
249
+ /**
250
+ * Create a mock controller for testing
251
+ */
252
+ function createMockController(repository) {
253
+ return {
254
+ repository,
255
+ list: vi.fn(),
256
+ get: vi.fn(),
257
+ create: vi.fn(),
258
+ update: vi.fn(),
259
+ delete: vi.fn()
260
+ };
261
+ }
262
+ /**
263
+ * Create mock data factory
264
+ *
265
+ * @example
266
+ * const productFactory = createDataFactory<Product>({
267
+ * name: () => faker.commerce.productName(),
268
+ * price: () => faker.number.int({ min: 10, max: 1000 }),
269
+ * sku: (i) => `SKU-${i}`,
270
+ * });
271
+ *
272
+ * const product = productFactory.build();
273
+ * const products = productFactory.buildMany(10);
274
+ */
275
+ function createDataFactory(template) {
276
+ let counter = 0;
277
+ return {
278
+ build(overrides = {}) {
279
+ const index = counter++;
280
+ const data = {};
281
+ for (const [key, generator] of Object.entries(template)) data[key] = generator(index);
282
+ return {
283
+ ...data,
284
+ ...overrides
285
+ };
286
+ },
287
+ buildMany(count, overrides = {}) {
288
+ return Array.from({ length: count }, () => this.build(overrides));
289
+ },
290
+ reset() {
291
+ counter = 0;
292
+ }
293
+ };
294
+ }
295
+ /**
296
+ * Create a spy that tracks function calls
297
+ *
298
+ * Useful for testing side effects without full mocking
299
+ */
300
+ function createSpy(_name = "spy") {
301
+ const calls = [];
302
+ const spy = vi.fn((...args) => {
303
+ calls.push(args);
304
+ });
305
+ spy.getCalls = () => calls;
306
+ spy.getLastCall = () => calls[calls.length - 1] || [];
307
+ return spy;
308
+ }
309
+ /**
310
+ * Wait until `condition` holds — the standard way to assert on asynchronous
311
+ * effects in arc's suites.
312
+ *
313
+ * **Prefer this over `await sleep(n)` + assert.** A fixed delay encodes a guess
314
+ * about scheduling: too short and it flakes the moment the pool is busy, too
315
+ * long and every run pays for the worst case. Polling returns as soon as the
316
+ * effect lands, so the fast path stays fast and a loaded machine simply waits —
317
+ * and the failure mode becomes "this never happened", which is the thing worth
318
+ * asserting.
319
+ *
320
+ * Supply `label`: without it a timeout reports only its own duration, which is
321
+ * the least useful sentence a flaky test can print.
322
+ *
323
+ * @example
324
+ * await waitFor(() => runs >= 3, { label: "3 scheduler ticks" });
325
+ */
326
+ async function waitFor(condition, options = {}) {
327
+ const { timeout = 5e3, interval = 25, label } = options;
328
+ const startTime = Date.now();
329
+ while (Date.now() - startTime < timeout) {
330
+ if (await condition()) return;
331
+ await new Promise((resolve) => setTimeout(resolve, interval));
332
+ }
333
+ throw new Error(label ? `Timed out after ${timeout}ms waiting for: ${label}` : `Timed out after ${timeout}ms waiting for a condition (pass "label" to name it)`);
334
+ }
335
+ /**
336
+ * Create a test timer that can be controlled
337
+ */
338
+ function createTestTimer() {
339
+ let time = Date.now();
340
+ return {
341
+ now: () => time,
342
+ advance: (ms) => {
343
+ time += ms;
344
+ },
345
+ set: (timestamp) => {
346
+ time = timestamp;
347
+ },
348
+ reset: () => {
349
+ time = Date.now();
350
+ }
351
+ };
352
+ }
353
+ //#endregion
354
+ //#region src/testing/authorizationConformance.ts
355
+ /**
356
+ * Cross-surface authorization conformance (arc 2.30).
357
+ *
358
+ * The regression wall that keeps "partial enforcement" from ever coming back:
359
+ * run the SAME permission through more than one arc surface and prove it enforces
360
+ * identically. Reviews kept finding aggregation (and MCP) drifting from CRUD —
361
+ * this makes that a one-call assertion for arc AND for host resources.
362
+ *
363
+ * `runAuthorizationConformance` boots a throwaway Fastify app with one resource
364
+ * whose `list` op AND an `aggregation` share the given permission, backed by a
365
+ * capturing mock repo (no DB). It returns, per surface, the HTTP status and the
366
+ * exact filter arc handed the repository — so the caller asserts:
367
+ * - deny → both surfaces fail closed with the SAME status;
368
+ * - allow + `policy` → the row policy reaches BOTH the list query and the
369
+ * aggregation `$match`, identically.
370
+ *
371
+ * Runner-agnostic: it returns data, it does not assert. Pair with your test
372
+ * framework's expectations (see `tests/core/authorization-conformance.test.ts`).
373
+ */
374
+ /**
375
+ * Boot a one-resource app gated by `permission` on both list + an aggregation,
376
+ * hit both surfaces anonymously, and report what arc enforced on each.
377
+ */
378
+ async function runAuthorizationConformance(opts) {
379
+ const rows = opts.rows ?? [{ _id: "1" }];
380
+ let listFilter;
381
+ let getFilter;
382
+ let aggFilter;
383
+ const repo = createMockRepository({
384
+ getAll: (async (params = {}) => {
385
+ listFilter = params.filters;
386
+ return {
387
+ method: "offset",
388
+ data: rows,
389
+ total: rows.length,
390
+ page: 1,
391
+ limit: 20,
392
+ pages: 1,
393
+ hasNext: false,
394
+ hasPrev: false
395
+ };
396
+ }),
397
+ getOne: (async (filter) => {
398
+ getFilter = filter;
399
+ return rows[0] ?? null;
400
+ }),
401
+ aggregate: (async (req = {}) => {
402
+ aggFilter = req.filter;
403
+ return { rows: [{ tally: rows.length }] };
404
+ }),
405
+ capabilities: {
406
+ count: false,
407
+ exists: false,
408
+ distinct: false,
409
+ aggregate: true,
410
+ transactions: false,
411
+ lookup: false,
412
+ softDelete: false
413
+ }
414
+ });
415
+ const resource = defineResource({
416
+ name: "conf",
417
+ prefix: "/confs",
418
+ adapter: { repository: repo },
419
+ controller: new BaseController(repo, {
420
+ resourceName: "conf",
421
+ tenantField: false
422
+ }),
423
+ permissions: {
424
+ list: opts.permission,
425
+ get: opts.permission,
426
+ create: opts.permission,
427
+ update: opts.permission,
428
+ delete: opts.permission
429
+ },
430
+ aggregations: { tally: defineAggregation({
431
+ measures: { tally: { op: "count" } },
432
+ permissions: opts.permission
433
+ }) }
434
+ });
435
+ const app = Fastify({ logger: false });
436
+ await app.register(resource.toPlugin());
437
+ await app.ready();
438
+ const list = await app.inject({
439
+ method: "GET",
440
+ url: "/confs"
441
+ });
442
+ const get = await app.inject({
443
+ method: "GET",
444
+ url: "/confs/1"
445
+ });
446
+ const aggregation = await app.inject({
447
+ method: "GET",
448
+ url: "/confs/aggregations/tally"
449
+ });
450
+ return {
451
+ list: {
452
+ status: list.statusCode,
453
+ filter: listFilter
454
+ },
455
+ get: {
456
+ status: get.statusCode,
457
+ filter: getFilter
458
+ },
459
+ aggregation: {
460
+ status: aggregation.statusCode,
461
+ filter: aggFilter
462
+ },
463
+ close: () => app.close()
464
+ };
465
+ }
466
+ //#endregion
128
467
  //#region src/testing/authSession.ts
129
468
  function buildHeaders(token, orgId, extra) {
130
469
  const headers = { authorization: `Bearer ${token}` };
@@ -464,6 +803,127 @@ async function setupBetterAuthTestApp(input) {
464
803
  };
465
804
  }
466
805
  //#endregion
806
+ //#region src/testing/testActor.ts
807
+ /**
808
+ * Test-only actor installation — how a package's HTTP tests say "run this
809
+ * request as a manager in org X".
810
+ *
811
+ * Identity in arc comes from `request.scope` and only from `request.scope`;
812
+ * headers like `x-role` are not identity. That rule left tests with no seam —
813
+ * `bootModuleApp` boots with `auth: false`, so every injected request arrived as
814
+ * `PUBLIC_SCOPE` and any role-gated route answered 401 with no supported way to
815
+ * be somebody.
816
+ *
817
+ * The `x-test-actor` header is a TRANSPORT for the test's intent, not a trusted
818
+ * claim. The authenticator that reads it is installed by `bootModuleApp` alone,
819
+ * behind the `@classytic/arc/testing` entrypoint; `createApp` never registers
820
+ * it, so no production build turns a forged header into identity. It is
821
+ * per-request rather than boot-only because the interesting authorization tests
822
+ * are the ones where a cashier is refused what a manager is allowed.
823
+ *
824
+ * Gate the routes under test with arc's own primitives — `requireRoles`,
825
+ * `requireOrgMembership`, `allOf(...)`. A test-specific gate would be a second
826
+ * implementation of authorization whose semantics could drift from the one
827
+ * actually shipping.
828
+ */
829
+ /** The header the testing harness uses to carry a request's actor. */
830
+ const TEST_ACTOR_HEADER = "x-test-actor";
831
+ /**
832
+ * Build the headers that make an injected request act as `actor`.
833
+ *
834
+ * A string is shorthand for a single role. `null` means "stay public", so a test
835
+ * can prove an endpoint is reachable anonymously.
836
+ *
837
+ * @example
838
+ * ```ts
839
+ * t.app.inject({ method: 'GET', url: '/pos/shifts/current',
840
+ * headers: testActorHeaders('manager', ORG) });
841
+ * ```
842
+ */
843
+ function testActorHeaders(actor, organizationId) {
844
+ if (actor === null) return {};
845
+ const org = organizationId !== void 0 ? { organizationId } : {};
846
+ const spec = typeof actor === "string" ? {
847
+ roles: [actor],
848
+ ...org
849
+ } : {
850
+ ...actor,
851
+ ...org
852
+ };
853
+ return { [TEST_ACTOR_HEADER]: JSON.stringify(spec) };
854
+ }
855
+ /**
856
+ * Turn a spec into a `RequestScope`.
857
+ *
858
+ * `orgRoles` defaults to `roles`: a test that says "manager" means it at both
859
+ * levels, and making the caller repeat itself only produces tests that pass a
860
+ * global gate and mysteriously fail an org one.
861
+ */
862
+ function scopeFromTestActor(spec) {
863
+ const roles = spec.roles ?? [];
864
+ const userId = spec.userId ?? `test-${roles.join("-") || "anon"}`;
865
+ if (spec.elevated !== void 0 && spec.elevated !== false) return {
866
+ kind: "elevated",
867
+ userId,
868
+ elevatedBy: spec.elevated === true ? "test" : spec.elevated,
869
+ ...spec.organizationId !== void 0 ? { organizationId: spec.organizationId } : {},
870
+ ...spec.context !== void 0 ? { context: Object.freeze({ ...spec.context }) } : {}
871
+ };
872
+ if (spec.organizationId !== void 0) return {
873
+ kind: "member",
874
+ userId,
875
+ userRoles: roles,
876
+ organizationId: spec.organizationId,
877
+ orgRoles: spec.orgRoles ?? roles,
878
+ ...spec.teamId !== void 0 ? { teamId: spec.teamId } : {},
879
+ ...spec.context !== void 0 ? { context: Object.freeze({ ...spec.context }) } : {}
880
+ };
881
+ return {
882
+ kind: "authenticated",
883
+ userId,
884
+ userRoles: roles
885
+ };
886
+ }
887
+ /**
888
+ * Build the `auth` option that makes the test actor arc's authenticator.
889
+ *
890
+ * It must be an authenticator, not a hook. Fastify encapsulates plugins and
891
+ * modules register DURING `createApp`, so a hook added to the root instance
892
+ * afterwards never runs for their routes — every request still resolves as
893
+ * public and every role gate still answers 401, which reads exactly like a
894
+ * broken authorization rule in the code under test. Registering as
895
+ * `auth: { type: 'authenticator' }` puts the scope where arc's own auth would,
896
+ * at the phase arc expects, so tenant filtering and permission checks behave
897
+ * identically to production.
898
+ *
899
+ * `defaultActor` applies to requests carrying no header. A malformed header
900
+ * REJECTS rather than falling back to public — a test that meant to be an admin
901
+ * and got quietly demoted is the most expensive way to fail.
902
+ */
903
+ function testActorAuth(defaultActor) {
904
+ const authenticate = async (request) => {
905
+ const req = request;
906
+ const raw = req.headers[TEST_ACTOR_HEADER];
907
+ let spec = defaultActor;
908
+ if (typeof raw === "string") try {
909
+ spec = JSON.parse(raw);
910
+ } catch {
911
+ throw new Error(`[arc-testing] malformed ${TEST_ACTOR_HEADER} header: ${raw}. Build it with testActorHeaders() rather than by hand.`);
912
+ }
913
+ if (spec === void 0) return;
914
+ req.scope = scopeFromTestActor(spec);
915
+ req.user = {
916
+ id: getUserId(req.scope) ?? "test-anon",
917
+ ...spec.roles !== void 0 ? { role: spec.roles } : {},
918
+ ...spec.organizationId !== void 0 ? { organizationId: spec.organizationId } : {}
919
+ };
920
+ };
921
+ return {
922
+ type: "authenticator",
923
+ authenticate
924
+ };
925
+ }
926
+ //#endregion
467
927
  //#region src/testing/bootModuleApp.ts
468
928
  /**
469
929
  * The built-in DB factory: in-memory MongoDB via lazy devDep imports.
@@ -496,7 +956,6 @@ async function bootModuleApp(modules, options = {}) {
496
956
  const db = await (options.database ?? mongoMemoryDatabase)({ replset: options.replset ?? false });
497
957
  const ctx = {
498
958
  uri: db.uri,
499
- mongoUri: db.uri,
500
959
  connection: db.connection
501
960
  };
502
961
  const resolved = typeof modules === "function" ? await modules(ctx) : modules;
@@ -505,7 +964,17 @@ async function bootModuleApp(modules, options = {}) {
505
964
  try {
506
965
  app = await createApp({
507
966
  preset: "testing",
508
- auth: false,
967
+ /**
968
+ * The test actor is arc's AUTHENTICATOR rather than a bolted-on hook, so the scope
969
+ * lands at the phase the rest of the pipeline expects — tenant filtering and
970
+ * permission checks then behave exactly as they do in production.
971
+ *
972
+ * This replaces the old `auth: false`. The authenticator is inert for a request
973
+ * carrying no `x-test-actor` header and no configured default, so a suite that
974
+ * never mentions an actor behaves as it did before. `options.app` still wins, so a
975
+ * suite exercising real auth can override it.
976
+ */
977
+ auth: testActorAuth(options.actor),
509
978
  sensible: false,
510
979
  ...options.app,
511
980
  modules: list
@@ -519,7 +988,6 @@ async function bootModuleApp(modules, options = {}) {
519
988
  return {
520
989
  app,
521
990
  uri: db.uri,
522
- mongoUri: db.uri,
523
991
  connection: db.connection,
524
992
  exports: (name) => getModuleExports(app, name),
525
993
  close: async () => {
@@ -845,212 +1313,61 @@ function createHttpTestHarness(resource, optionsOrGetter) {
845
1313
  return new HttpTestHarness(resource, optionsOrGetter);
846
1314
  }
847
1315
  //#endregion
848
- //#region src/testing/mocks.ts
1316
+ //#region src/testing/scopeRoleGate.ts
849
1317
  /**
850
- * Create a mock repository for testing
1318
+ * `scopeRoleGate` — the ONE role gate for package tests.
851
1319
  *
852
- * @example
853
- * const mockRepo = createMockRepository<Product>({
854
- * getById: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
855
- * create: vi.fn().mockImplementation(data => Promise.resolve({ id: '1', ...data })),
856
- * });
1320
+ * ## Why this is in the framework
857
1321
  *
858
- * await mockRepo.getById('1'); // Returns mocked product
859
- */
860
- function createMockRepository(overrides = {}) {
861
- return {
862
- getAll: vi.fn().mockResolvedValue({
863
- method: "offset",
864
- data: [],
865
- total: 0,
866
- page: 1,
867
- limit: 20,
868
- pages: 0,
869
- hasNext: false,
870
- hasPrev: false
871
- }),
872
- getById: vi.fn().mockResolvedValue(null),
873
- create: vi.fn().mockImplementation((data) => Promise.resolve({
874
- _id: "mock-id",
875
- ...data
876
- })),
877
- update: vi.fn().mockImplementation((_id, data) => Promise.resolve({
878
- _id: "mock-id",
879
- ...data
880
- })),
881
- delete: vi.fn().mockResolvedValue({
882
- success: true,
883
- message: "Deleted"
884
- }),
885
- updateMany: vi.fn().mockResolvedValue({
886
- acknowledged: true,
887
- matchedCount: 0,
888
- modifiedCount: 0
889
- }),
890
- deleteMany: vi.fn().mockResolvedValue({
891
- acknowledged: true,
892
- deletedCount: 0
893
- }),
894
- claim: vi.fn().mockResolvedValue(null),
895
- claimVersion: vi.fn().mockResolvedValue(null),
896
- capabilities: {
897
- transactions: false,
898
- nestedTransactions: false,
899
- upsert: false,
900
- duplicateKeyError: false,
901
- distinct: false,
902
- aggregate: false,
903
- getOrCreate: false,
904
- countAndExists: false
905
- },
906
- getBySlug: vi.fn().mockResolvedValue(null),
907
- getDeleted: vi.fn().mockResolvedValue([]),
908
- restore: vi.fn().mockResolvedValue(null),
909
- getTree: vi.fn().mockResolvedValue([]),
910
- getChildren: vi.fn().mockResolvedValue([]),
911
- ...overrides
912
- };
913
- }
914
- /**
915
- * Create a mock user for authentication testing
916
- */
917
- function createMockUser(overrides = {}) {
918
- return {
919
- _id: "mock-user-id",
920
- id: "mock-user-id",
921
- email: "test@example.com",
922
- roles: ["user"],
923
- organizationId: null,
924
- ...overrides
925
- };
926
- }
927
- /**
928
- * Create a mock Fastify request
929
- */
930
- function createMockRequest(overrides = {}) {
931
- return {
932
- body: {},
933
- params: {},
934
- query: {},
935
- headers: {},
936
- user: createMockUser(),
937
- context: {},
938
- log: {
939
- info: vi.fn(),
940
- warn: vi.fn(),
941
- error: vi.fn(),
942
- debug: vi.fn()
943
- },
944
- ...overrides
945
- };
946
- }
947
- /**
948
- * Create a mock Fastify reply
949
- */
950
- function createMockReply() {
951
- return {
952
- code: vi.fn().mockReturnThis(),
953
- send: vi.fn().mockReturnThis(),
954
- header: vi.fn().mockReturnThis(),
955
- headers: vi.fn().mockReturnThis(),
956
- status: vi.fn().mockReturnThis(),
957
- type: vi.fn().mockReturnThis(),
958
- redirect: vi.fn().mockReturnThis(),
959
- callNotFound: vi.fn().mockReturnThis(),
960
- sent: false
961
- };
962
- }
963
- /**
964
- * Create a mock controller for testing
965
- */
966
- function createMockController(repository) {
967
- return {
968
- repository,
969
- list: vi.fn(),
970
- get: vi.fn(),
971
- create: vi.fn(),
972
- update: vi.fn(),
973
- delete: vi.fn()
974
- };
975
- }
976
- /**
977
- * Create mock data factory
1322
+ * Nine `@classytic/arc-*` packages had their own copy of this helper, and all nine were
1323
+ * wrong in the same two ways:
978
1324
  *
979
- * @example
980
- * const productFactory = createDataFactory<Product>({
981
- * name: () => faker.commerce.productName(),
982
- * price: () => faker.number.int({ min: 10, max: 1000 }),
983
- * sku: (i) => `SKU-${i}`,
984
- * });
1325
+ * 1. They read the role from an `x-role` HEADER and INSTALLED the request scope from
1326
+ * inside the check — so identity came from the caller's own claim, and it arrived too
1327
+ * late for arc's tenant-filtering pipeline to see it.
1328
+ * 2. They returned the pre-2.30 `{ granted: boolean }` shape, which is neither a boolean
1329
+ * nor an `AuthorizationDecision`. It passed through `normalizeToDecision` untouched,
1330
+ * arrived with `effect: undefined`, and was read as DENY — the gate said yes and arc
1331
+ * refused anyway. Nothing type-errored, because the gate is cast at the call site.
985
1332
  *
986
- * const product = productFactory.build();
987
- * const products = productFactory.buildMany(10);
988
- */
989
- function createDataFactory(template) {
990
- let counter = 0;
991
- return {
992
- build(overrides = {}) {
993
- const index = counter++;
994
- const data = {};
995
- for (const [key, generator] of Object.entries(template)) data[key] = generator(index);
996
- return {
997
- ...data,
998
- ...overrides
999
- };
1000
- },
1001
- buildMany(count, overrides = {}) {
1002
- return Array.from({ length: count }, () => this.build(overrides));
1003
- },
1004
- reset() {
1005
- counter = 0;
1006
- }
1007
- };
1008
- }
1009
- /**
1010
- * Create a spy that tracks function calls
1333
+ * Two of the nine had already been fixed by hand, each with a docstring describing this
1334
+ * exact discovery. The knowledge reached neither of the other seven. That is the whole
1335
+ * case for a shared seam: a copy does not fail, it diverges.
1011
1336
  *
1012
- * Useful for testing side effects without full mocking
1013
- */
1014
- function createSpy(_name = "spy") {
1015
- const calls = [];
1016
- const spy = vi.fn((...args) => {
1017
- calls.push(args);
1018
- });
1019
- spy.getCalls = () => calls;
1020
- spy.getLastCall = () => calls[calls.length - 1] || [];
1021
- return spy;
1022
- }
1023
- /**
1024
- * Wait for a condition to be true
1337
+ * A test role gate is generic — "does the verified scope hold one of these roles" has no
1338
+ * domain in it — so it belongs here, beside the contract it depends on.
1339
+ *
1340
+ * Identity itself comes from `testActorHeaders()` + `bootModuleApp`'s authenticator
1341
+ * (see `testActor.ts`), which puts the scope where production puts it. This stays a pure
1342
+ * predicate over what arc verified.
1025
1343
  *
1026
- * Useful for async testing
1344
+ * Kept in its own module rather than beside `testActor.ts` so the two can be edited
1345
+ * independently.
1027
1346
  */
1028
- async function waitFor(condition, options = {}) {
1029
- const { timeout = 5e3, interval = 100 } = options;
1030
- const startTime = Date.now();
1031
- while (Date.now() - startTime < timeout) {
1032
- if (await condition()) return;
1033
- await new Promise((resolve) => setTimeout(resolve, interval));
1034
- }
1035
- throw new Error(`Timeout waiting for condition after ${timeout}ms`);
1036
- }
1037
1347
  /**
1038
- * Create a test timer that can be controlled
1348
+ * Grants iff the caller is an org member holding one of `allowed` as an ORG role.
1349
+ *
1350
+ * This is `requireOrgRole` under a name package tests reach for — deliberately a
1351
+ * delegation, not a reimplementation. It previously read `scope.userRoles` while
1352
+ * requiring an organization, so it disagreed with production for any actor whose
1353
+ * global and org roles differ: given
1354
+ * `{ roles: ["employee"], orgRoles: ["manager"], organizationId: "org-1" }`,
1355
+ * `requireOrgRole("manager")` allowed and this denied. The tests never caught it
1356
+ * because `scopeFromTestActor` defaults `orgRoles` to `roles`, making the two
1357
+ * dimensions identical for the common actor.
1358
+ *
1359
+ * Delegating also inherits the semantics a hand-rolled gate kept missing:
1360
+ * elevated scopes pass, and a service (API-key) identity is refused with a
1361
+ * message naming `requireServiceScope` instead of silently failing a role test.
1362
+ *
1363
+ * **Pair it with an org-bearing actor.** `testActorHeaders('manager', ORG)`
1364
+ * yields a `member` scope and passes; `testActorHeaders('manager')` yields an
1365
+ * `authenticated` scope with no tenant, which is refused — arc's generated CRUD
1366
+ * filters by tenant, so a gate that waves a tenant-less request through is how a
1367
+ * green test reads another org's rows.
1039
1368
  */
1040
- function createTestTimer() {
1041
- let time = Date.now();
1042
- return {
1043
- now: () => time,
1044
- advance: (ms) => {
1045
- time += ms;
1046
- },
1047
- set: (timestamp) => {
1048
- time = timestamp;
1049
- },
1050
- reset: () => {
1051
- time = Date.now();
1052
- }
1053
- };
1369
+ function scopeRoleGate(...allowed) {
1370
+ return requireOrgRole(...allowed);
1054
1371
  }
1055
1372
  //#endregion
1056
1373
  //#region src/testing/testApp.ts
@@ -1118,7 +1435,7 @@ function pickDefaultAuth(authMode, callerAuth) {
1118
1435
  };
1119
1436
  }
1120
1437
  async function createTestApp(options = {}) {
1121
- const { createApp } = await import("../createApp-Du5G54TM.mjs").then((n) => n.r);
1438
+ const { createApp } = await import("../createApp-Df89BEni.mjs").then((n) => n.r);
1122
1439
  const { resources = [], db = "in-memory", connectMongoose = false, authMode = "jwt", defaultOrgId, plugins, auth: callerAuth, ...appOptions } = options;
1123
1440
  let dbHandle;
1124
1441
  let dbUri;
@@ -1188,4 +1505,4 @@ function createMinimalTestApp(options = {}) {
1188
1505
  });
1189
1506
  }
1190
1507
  //#endregion
1191
- export { HttpTestHarness, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runStorageContract, safeParseBody, setupBetterAuthTestApp, waitFor };
1508
+ export { HttpTestHarness, TEST_ACTOR_HEADER, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runAuthorizationConformance, runStorageContract, safeParseBody, scopeFromTestActor, scopeRoleGate, setupBetterAuthTestApp, testActorAuth, testActorHeaders, waitFor };