@classytic/arc 2.41.1 → 2.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-DZWDsaPa.mjs} +22 -17
  3. package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-yjEgn7Cm.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-DJxHUpE4.mjs} +44 -13
  6. package/dist/acquire-BozJ-9yt.mjs +35 -0
  7. package/dist/actionPermissions-5N04zBL4.mjs +772 -0
  8. package/dist/audit/index.d.mts +46 -8
  9. package/dist/audit/index.mjs +48 -26
  10. package/dist/auth/audit.d.mts +8 -8
  11. package/dist/auth/audit.mjs +1 -1
  12. package/dist/auth/index.d.mts +46 -16
  13. package/dist/auth/index.mjs +134 -156
  14. package/dist/auth/redis-session.d.mts +4 -5
  15. package/dist/auth/redis-session.mjs +1 -1
  16. package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-wX3SA9w_.mjs} +7 -102
  17. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  18. package/dist/boundedMap-DWiKW7lP.mjs +57 -0
  19. package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-Bswu_vNb.mjs} +4 -4
  20. package/dist/cache/index.d.mts +31 -43
  21. package/dist/cache/index.mjs +4 -4
  22. package/dist/{caching-DbCBxlgw.mjs → caching-Drp68DIS.mjs} +3 -3
  23. package/dist/claimed-DYvQoZLv.mjs +8 -0
  24. package/dist/cleanup/index.d.mts +18 -22
  25. package/dist/cleanup/index.mjs +27 -73
  26. package/dist/cli/commands/describe.d.mts +7 -5
  27. package/dist/cli/commands/describe.mjs +7 -4
  28. package/dist/cli/commands/docs.d.mts +2 -3
  29. package/dist/cli/commands/docs.mjs +3 -3
  30. package/dist/cli/commands/doctor.d.mts +2 -3
  31. package/dist/cli/commands/doctor.mjs +1 -1
  32. package/dist/cli/commands/generate.mjs +2 -3
  33. package/dist/cli/commands/init.mjs +2 -3
  34. package/dist/cli/commands/introspect.d.mts +2 -3
  35. package/dist/cli/commands/introspect.mjs +2 -2
  36. package/dist/cli/index.mjs +3 -3
  37. package/dist/{constants-D5Ubn1fe.mjs → constants-B2QHb6u6.mjs} +4 -2
  38. package/dist/context/index.d.mts +11 -14
  39. package/dist/context/index.mjs +18 -38
  40. package/dist/core/index.d.mts +3 -3
  41. package/dist/core/index.mjs +8 -8
  42. package/dist/{core-5lazhva9.mjs → core-COcqo0Cp.mjs} +15 -5
  43. package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-zG13rpPJ.mjs} +97 -39
  44. package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-CWRz96Vr.mjs} +11 -8
  45. package/dist/{preloadResources-f3boenVZ.mjs → createApp-BGLYtErt.mjs} +305 -394
  46. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-DSztofa9.d.mts} +1 -1
  47. package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation--3xguLkk.mjs} +1 -1
  48. package/dist/{defineResource-DxkMTjRn.mjs → defineResource-gUp9ug66.mjs} +118 -1024
  49. package/dist/discovery/index.d.mts +6 -7
  50. package/dist/discovery/index.mjs +1 -1
  51. package/dist/docs/index.d.mts +11 -5
  52. package/dist/docs/index.mjs +4 -4
  53. package/dist/{elevation-CkvG0LG2.mjs → elevation-BG4gnMsJ.mjs} +41 -12
  54. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  55. package/dist/encryption/index.d.mts +5 -5
  56. package/dist/encryption/index.mjs +3 -3
  57. package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-COHw3N1b.mjs} +26 -5
  58. package/dist/{errors-BH7YU_9O.mjs → errors-CW4anJBy.mjs} +58 -23
  59. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  60. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  61. package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-BweHAVws.mjs} +82 -21
  62. package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-N4-omQFR.mjs} +151 -17
  63. package/dist/events/index.d.mts +43 -18
  64. package/dist/events/index.mjs +49 -53
  65. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  66. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  67. package/dist/events/transports/redis.d.mts +5 -6
  68. package/dist/events/transports/redis.mjs +2 -2
  69. package/dist/factory/index.d.mts +42 -27
  70. package/dist/factory/index.mjs +213 -32
  71. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DRlCIWE9.d.mts} +11 -25
  72. package/dist/{fields-BHrkoF5y.mjs → fields-A5crhsJb.mjs} +1 -1
  73. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  74. package/dist/filter-merge-DGM-GQ1J.mjs +99 -0
  75. package/dist/{generate-BLRf2m3y.mjs → generate-BXKo2Gc8.mjs} +2 -2
  76. package/dist/{headers-BIXWKrzQ.mjs → headers-DKZQKL-Z.mjs} +2 -2
  77. package/dist/health-BYAwA6YS.mjs +22 -0
  78. package/dist/{health-ZRHeDHXH.mjs → health-CrqUp3sz.mjs} +115 -55
  79. package/dist/hooks/index.d.mts +2 -2
  80. package/dist/hooks/index.mjs +2 -3
  81. package/dist/idempotency/index.d.mts +36 -10
  82. package/dist/idempotency/index.mjs +230 -111
  83. package/dist/idempotency/redis.d.mts +1 -1
  84. package/dist/idempotency/redis.mjs +77 -12
  85. package/dist/{index-D4u76eQW.d.mts → index-Beg3aNnK.d.mts} +23 -101
  86. package/dist/{preloadResources-CjS2OfpU.d.mts → index-BkV55WlV.d.mts} +37 -38
  87. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  88. package/dist/index-CUJvoyw3.d.mts +510 -0
  89. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  90. package/dist/{index-DHbyPo5j.d.mts → index-WH7xgvxS.d.mts} +304 -254
  91. package/dist/index.d.mts +9 -9
  92. package/dist/index.mjs +11 -11
  93. package/dist/{init-CpLkjU18.mjs → init-BPzK5Lnr.mjs} +1 -1
  94. package/dist/integrations/event-gateway.d.mts +11 -7
  95. package/dist/integrations/event-gateway.mjs +4 -4
  96. package/dist/integrations/index.d.mts +3 -3
  97. package/dist/integrations/index.mjs +1 -1
  98. package/dist/integrations/jobs/index.d.mts +1 -1
  99. package/dist/integrations/jobs/index.mjs +18 -5
  100. package/dist/integrations/mcp/index.d.mts +102 -89
  101. package/dist/integrations/mcp/index.mjs +19 -4
  102. package/dist/integrations/mcp/testing.d.mts +8 -9
  103. package/dist/integrations/mcp/testing.mjs +2 -2
  104. package/dist/integrations/streamline.d.mts +85 -144
  105. package/dist/integrations/streamline.mjs +131 -55
  106. package/dist/integrations/webhooks.d.mts +83 -143
  107. package/dist/integrations/webhooks.mjs +36 -80
  108. package/dist/integrations/websocket-pushref-redis.d.mts +5 -6
  109. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  110. package/dist/integrations/websocket-redis.d.mts +5 -6
  111. package/dist/integrations/websocket-redis.mjs +1 -1
  112. package/dist/integrations/websocket.d.mts +3 -3
  113. package/dist/integrations/websocket.mjs +2 -2
  114. package/dist/{interface-DEWNOMza.d.mts → interface-JJzwH4Ro.d.mts} +34 -2
  115. package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-YIq-vGLv.mjs} +2 -2
  116. package/dist/{keys-cpZCtcIE.mjs → keys-CpoiZ8hU.mjs} +7 -18
  117. package/dist/{loadResources-BPfWiI5s.mjs → loadResources-6jO52-RR.mjs} +3 -3
  118. package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-MP8xEppu.mjs} +2 -2
  119. package/dist/logger/index.d.mts +9 -17
  120. package/dist/logger/index.mjs +2 -153
  121. package/dist/logger-D4V1zGLI.mjs +160 -0
  122. package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-srPc7ItN.mjs} +446 -170
  123. package/dist/{memory-Iotz0cCx.mjs → memory-CTa5pOCW.mjs} +36 -34
  124. package/dist/{metrics-NMsmpRmY.mjs → metrics-DsZrvnpB.mjs} +39 -72
  125. package/dist/middleware/index.d.mts +5 -5
  126. package/dist/middleware/index.mjs +3 -3
  127. package/dist/migrations/index.d.mts +13 -13
  128. package/dist/migrations/index.mjs +2 -2
  129. package/dist/{module-DpLJG2_N.mjs → module--L0iBIZz.mjs} +8 -8
  130. package/dist/mongodb-memory-server-CHhYSq5y.mjs +35539 -0
  131. package/dist/mongoose-CYJCLyi-.mjs +67451 -0
  132. package/dist/multiTenant-CAtYlW6H.d.mts +104 -0
  133. package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-Cc7eZ9cq.mjs} +46 -7
  134. package/dist/node-D8UZv_8c.mjs +41236 -0
  135. package/dist/{openapi-CmFf3yq_.mjs → openapi-CMegfsm8.mjs} +48 -10
  136. package/dist/outbox-admin/index.d.mts +5 -5
  137. package/dist/outbox-admin/index.mjs +6 -18
  138. package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Ryk270jd.mjs} +203 -33
  139. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  140. package/dist/permissions/index.d.mts +5 -4
  141. package/dist/permissions/index.mjs +8 -6
  142. package/dist/{permissions-D8TBH44E.mjs → permissions-C5ijIz5Q.mjs} +149 -33
  143. package/dist/{pipe-FMWNheSO.mjs → pipe-D81zMtWs.mjs} +2 -2
  144. package/dist/pipeline/index.d.mts +7 -7
  145. package/dist/pipeline/index.mjs +2 -2
  146. package/dist/plan-CJGMKoui.mjs +565 -0
  147. package/dist/{BaseController-CYEwjyjN.mjs → plugin-DALd9dcF.mjs} +1269 -346
  148. package/dist/plugins/index.d.mts +17 -14
  149. package/dist/plugins/index.mjs +44 -22
  150. package/dist/plugins/response-cache.d.mts +5 -6
  151. package/dist/plugins/response-cache.mjs +8 -14
  152. package/dist/plugins/tracing-entry.mjs +2 -2
  153. package/dist/{pluralize-CXWZIzTv.mjs → pluralize-COUNYo3d.mjs} +1 -1
  154. package/dist/presets/filesUpload.d.mts +8 -8
  155. package/dist/presets/filesUpload.mjs +5 -5
  156. package/dist/presets/index.d.mts +35 -40
  157. package/dist/presets/index.mjs +4 -4
  158. package/dist/presets/multiTenant.d.mts +2 -107
  159. package/dist/presets/multiTenant.mjs +22 -162
  160. package/dist/presets/search.d.mts +9 -10
  161. package/dist/presets/search.mjs +2 -2
  162. package/dist/{presets-ZJSPgmT2.mjs → presets-CONf2Qem.mjs} +99 -107
  163. package/dist/{projection-BzIgzoKZ.mjs → projection-wFoLqdss.mjs} +2 -2
  164. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  165. package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-BATECpyh.mjs} +12 -14
  166. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  167. package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-DVEZ_UpV.mjs} +1 -1
  168. package/dist/{realtime-8tIQWZz4.mjs → realtime-xyubMKSB.mjs} +25 -21
  169. package/dist/{redis-Bzi1y3F9.d.mts → redis-BDgv9Glw.d.mts} +17 -24
  170. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  171. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  172. package/dist/registry/index.d.mts +15 -10
  173. package/dist/registry/index.mjs +4 -4
  174. package/dist/{registry-DNkgGQcK.mjs → registry-Bdzg-hdm.mjs} +13 -10
  175. package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-BFJxGPuh.mjs} +1 -1
  176. package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-Bnc6lJyy.mjs} +2 -2
  177. package/dist/{requestContext-cDkAUr-P.mjs → requestContext-C10UeFe5.mjs} +1 -1
  178. package/dist/requirement-Bu50QALH.mjs +35 -0
  179. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  180. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  181. package/dist/resourceRateLimit-urWPJxrt.mjs +180 -0
  182. package/dist/rolldown-runtime-MYrLNGnq.mjs +45 -0
  183. package/dist/routeValidator-DHtpzrzk.mjs +70 -0
  184. package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-DIMKMV2G.mjs} +1 -1
  185. package/dist/{schedules-j6uXXAeJ.mjs → schedules-CKTs5ys8.mjs} +46 -37
  186. package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BVjkQ5DD.mjs} +2 -2
  187. package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-BA0Hb21Y.mjs} +2 -2
  188. package/dist/schemas/index.d.mts +20 -10
  189. package/dist/schemas/index.mjs +8 -3
  190. package/dist/scim/index.d.mts +13 -13
  191. package/dist/scim/index.mjs +2 -2
  192. package/dist/scope/index.d.mts +4 -4
  193. package/dist/scope/index.mjs +6 -6
  194. package/dist/{scope-BUi1xV1I.mjs → scope-DINaj7Ax.mjs} +4 -4
  195. package/dist/shutdown-LpLMH8T1.mjs +59 -0
  196. package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-DbwIM1f1.mjs} +1 -1
  197. package/dist/{sse-BeqVKQKu.mjs → sse-BzJFPX_1.mjs} +11 -15
  198. package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-DQChzGTs.mjs} +2 -2
  199. package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-Coj9RhOZ.mjs} +1 -1
  200. package/dist/{streaming-IcRFjWQU.mjs → streaming-JzB5CAga.mjs} +1 -1
  201. package/dist/subscriberIdentity-DeyppIif.mjs +28 -0
  202. package/dist/tenancy-DxV8OuX2.mjs +43 -0
  203. package/dist/tenantPreparation-CBvvAcAj.mjs +67 -0
  204. package/dist/testing/cleanupStoreContract.d.mts +4 -5
  205. package/dist/testing/cleanupStoreContract.mjs +1 -1
  206. package/dist/testing/index.d.mts +215 -197
  207. package/dist/testing/index.mjs +289 -214
  208. package/dist/testing/outboxStoreContract.d.mts +4 -5
  209. package/dist/testing/outboxStoreContract.mjs +137 -2
  210. package/dist/testing/requiredPorts.d.mts +16 -0
  211. package/dist/testing/requiredPorts.mjs +65 -0
  212. package/dist/testing/storageContract.d.mts +4 -5
  213. package/dist/testing/storageContract.mjs +1 -1
  214. package/dist/timing-CntavTlR.mjs +46 -0
  215. package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-orvnGu4F.mjs} +1 -1
  216. package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-kEaP_WmO.mjs} +1 -1
  217. package/dist/types/index.d.mts +4 -4
  218. package/dist/types/index.mjs +1 -1
  219. package/dist/types/storage.mjs +1 -1
  220. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  221. package/dist/{types-CWQn9-IC.d.mts → types-D3Ixsh00.d.mts} +22 -9
  222. package/dist/{types-CgenB1gv.mjs → types-fvO6Ovzx.mjs} +4 -4
  223. package/dist/usage/index.d.mts +5 -6
  224. package/dist/usage/index.mjs +5 -5
  225. package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-C76zD5pa.mjs} +1 -1
  226. package/dist/utils/index.d.mts +125 -264
  227. package/dist/utils/index.mjs +13 -12
  228. package/dist/{utils-CxcctrAh.mjs → utils-BpkVZ8rB.mjs} +315 -404
  229. package/dist/{validate-DrYWnOzI.mjs → validate-CWp-lsrm.mjs} +6 -14
  230. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  231. package/dist/{versioning-DKkT4hNJ.mjs → versioning-CHhL9P3o.mjs} +4 -4
  232. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  233. package/dist/{workScope-CxKqoIFn.mjs → workScope-n9a7d_lZ.mjs} +2 -2
  234. package/package.json +57 -59
  235. package/skills/arc/SKILL.md +13 -4
  236. package/skills/arc/references/auth.md +8 -5
  237. package/skills/arc/references/integrations.md +1 -1
  238. package/skills/arc/references/mcp.md +8 -6
  239. package/skills/arc/references/multi-tenancy.md +19 -14
  240. package/skills/arc/references/production.md +27 -10
  241. package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
  242. package/dist/index-CFfLKMH4.d.mts +0 -290
  243. package/dist/interface-YzIbF7Ii.mjs +0 -17
  244. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  245. package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
  246. package/dist/rolldown-runtime-eysyzFAF.mjs +0 -15
  247. package/dist/sync/changelog.d.mts +0 -3
  248. package/dist/sync/changelog.mjs +0 -3
@@ -1,10 +1,11 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-BH7YU_9O.mjs";
3
- import { n as requireSingleHeaderValue } from "../headers-BIXWKrzQ.mjs";
4
- import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-ChRvkuSZ.mjs";
5
- import { o as toJsonSchema } from "../schemaConverter-BVA7cFRG.mjs";
6
- import { n as declareRuntimeCapability } from "../runtimeCapabilities-CkhS6jn6.mjs";
7
- import { E as requireOrgRole, T as requireOrgMembership, k as requireTeamMembership } from "../permissions-D8TBH44E.mjs";
1
+ /*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
2
+ import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-CW4anJBy.mjs";
3
+ import { n as requireSingleHeaderValue } from "../headers-DKZQKL-Z.mjs";
4
+ import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-C76zD5pa.mjs";
5
+ import { o as toJsonSchema } from "../schemaConverter-BVjkQ5DD.mjs";
6
+ import { n as declareRuntimeCapability } from "../runtimeCapabilities-DIMKMV2G.mjs";
7
+ import { E as requireOrgRole, T as requireOrgMembership, k as requireTeamMembership } from "../permissions-C5ijIz5Q.mjs";
8
+ import { ARC_ERROR_CODES } from "@classytic/repo-core/errors";
8
9
  import { isProductionEnv } from "@classytic/primitives/environment";
9
10
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
10
11
  import fp from "fastify-plugin";
@@ -47,7 +48,10 @@ const authPlugin = async (fastify, opts = {}) => {
47
48
  let jwtContext = null;
48
49
  if (jwtConfig?.secret) {
49
50
  if (jwtConfig.secret.length < 32) throw new Error(`JWT secret must be at least 32 characters (current: ${jwtConfig.secret.length}).\nUse a strong random secret for production.`);
50
- const jwtPlugin = await import("@fastify/jwt");
51
+ const jwtPlugin = await import("@fastify/jwt").catch((error) => {
52
+ if (error instanceof Error && "code" in error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes("@fastify/jwt")) throw new Error("Arc JWT authentication requires @fastify/jwt. Run `npm install @fastify/jwt`. Better Auth and custom authenticators do not require this package.", { cause: error });
53
+ throw error;
54
+ });
51
55
  await fastify.register(jwtPlugin.default ?? jwtPlugin, {
52
56
  secret: jwtConfig.secret,
53
57
  sign: {
@@ -543,6 +547,22 @@ function mergeUserFieldsIntoRequestBody(requestBody, userFields, endpointPath) {
543
547
  //#endregion
544
548
  //#region src/auth/betterAuth.ts
545
549
  /**
550
+ * Better Auth Adapter for Arc/Fastify
551
+ *
552
+ * Bridges Fastify <-> Better Auth's Fetch API (Request/Response).
553
+ * Better Auth is the USER's dependency -- Arc only provides this thin adapter.
554
+ *
555
+ * @example
556
+ * import { betterAuth } from 'better-auth';
557
+ * import { createBetterAuthAdapter } from '@classytic/arc/auth';
558
+ *
559
+ * const auth = betterAuth({ ... });
560
+ *
561
+ * const app = await createApp({
562
+ * auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth }) },
563
+ * });
564
+ */
565
+ /**
546
566
  * Convert a Fastify request into a Fetch API Request.
547
567
  *
548
568
  * Better Auth expects standard Web API Request objects.
@@ -604,28 +624,23 @@ async function sendFetchResponse(response, reply) {
604
624
  }
605
625
  }
606
626
  /**
607
- * Resolve the current session via Better Auth's direct JS API.
608
- *
609
- * Throws `ArcError(BETTER_AUTH_API_MISSING)` when `auth.api.getSession` is
610
- * absent — this surfaces immediately and clearly when an integrator passes a
611
- * stub handler instead of a real `betterAuth()` instance.
627
+ * Resolve the current session via `auth.api.getSession`. Throws
628
+ * `ArcError(BETTER_AUTH_API_MISSING)` when it is absent (a stub, not a real
629
+ * `betterAuth()` instance).
612
630
  */
613
631
  async function getSessionDirect(auth, headers) {
614
632
  const api = auth.api;
615
- if (!api || typeof api.getSession !== "function") throw new ArcError("Better Auth instance is missing `api.getSession` — arc 2.13+ requires the in-process API map. Pass a real `betterAuth()` instance or supply an `api: { getSession }` stub.", {
616
- code: "BETTER_AUTH_API_MISSING",
633
+ if (!api || typeof api.getSession !== "function") throw new ArcError("Better Auth instance is missing `api.getSession` — arc requires the in-process API map. Pass a real `betterAuth()` instance or supply an `api: { getSession }` stub.", {
634
+ code: ARC_ERROR_CODES.AUTH_MISCONFIGURED,
617
635
  statusCode: 500
618
636
  });
619
637
  const result = await api.getSession({ headers });
620
638
  return result?.user ? result : null;
621
639
  }
622
640
  /**
623
- * Read a method from `auth.api` — supports both the flat shape that real
624
- * `betterAuth()` instances expose (`api.getActiveMember`) and the nested
625
- * shape some test mocks / older builds use (`api.organization.getActiveMember`).
626
- *
627
- * Real Better Auth 1.6.x flattens every plugin endpoint onto the top-level
628
- * `api` object. The nested form is kept as a fallback for hand-rolled stubs.
641
+ * Read a method from `auth.api`: the flat shape real instances expose
642
+ * (`api.getActiveMember`), falling back to the nested stub shape
643
+ * (`api.organization.getActiveMember`).
629
644
  */
630
645
  function pickApiMethod(auth, name, group) {
631
646
  const api = auth.api;
@@ -649,11 +664,8 @@ async function getActiveMemberRoles(auth, headers) {
649
664
  }
650
665
  }
651
666
  /**
652
- * Resolve org roles for an explicit organizationId.
653
- *
654
- * Required for API key auth where the synthetic session lacks
655
- * `activeOrganizationId` — callers pass org context via the
656
- * `x-organization-id` header.
667
+ * Resolve org roles for an explicit organizationId — for sessions without
668
+ * `activeOrganizationId` (e.g. API-key auth via `x-organization-id`).
657
669
  */
658
670
  async function getMemberRolesByOrg(auth, headers, organizationId) {
659
671
  const fn = pickApiMethod(auth, "getActiveMemberRole", "organization");
@@ -669,21 +681,12 @@ async function getMemberRolesByOrg(auth, headers, organizationId) {
669
681
  }
670
682
  }
671
683
  /**
672
- * Teams the current user is a MEMBER of, scoped to one organization. Used to
673
- * validate `activeTeamId` before binding it to scope.
674
- *
675
- * `auth.api.listUserTeams` (`/organization/list-user-teams`) is the only name
676
- * probed. A `listTeams` fallback lived here for "older 1.5.x" and was removed:
677
- * Better Auth exposes `listUserTeams` and `listOrganizationTeams` on `auth.api`
678
- * and has never exposed a bare `listTeams` — that is the CLIENT name
679
- * (`authClient.organization.listTeams`). It could only ever resolve against a
680
- * test double, which is exactly what it was doing.
684
+ * Teams the current user is a MEMBER of in one organization, used to validate
685
+ * `activeTeamId` before binding it to scope.
681
686
  *
682
- * Do not "restore" it as `listOrganizationTeams`: that endpoint lists every team
683
- * in the organization, while this one lists the teams the USER belongs to.
684
- * Validating `activeTeamId` against the org-wide list would admit a team the
685
- * user is not a member of — the organization check below constrains the org, not
686
- * membership.
687
+ * Must stay `listUserTeams`: `listOrganizationTeams` lists EVERY team in the
688
+ * org and would admit a team the user isn't a member of. (`listTeams` is the
689
+ * client-side name; `auth.api` has no such method.)
687
690
  */
688
691
  async function listTeamsDirect(auth, headers, organizationId) {
689
692
  const fn = pickApiMethod(auth, "listUserTeams", "organization");
@@ -737,38 +740,72 @@ async function resolveActiveTeamId(input) {
737
740
  if (!activeTeamId) return void 0;
738
741
  return (await listTeamsDirect(input.auth, input.headers, input.activeOrgId))?.find((team) => normalizeId(team.id ?? team._id) === activeTeamId && normalizeId(team.organizationId) === input.activeOrgId) ? activeTeamId : void 0;
739
742
  }
740
- /**
741
- * Create a Better Auth adapter for Arc/Fastify.
742
- *
743
- * Returns a Fastify plugin (registers catch-all auth routes) and an
744
- * `authenticate` preHandler that validates sessions via Better Auth.
745
- *
746
- * @example
747
- * ```typescript
748
- * import { betterAuth } from 'better-auth';
749
- * import { createBetterAuthAdapter } from '@classytic/arc/auth';
750
- *
751
- * const auth = betterAuth({
752
- * database: ...,
753
- * emailAndPassword: { enabled: true },
754
- * });
755
- *
756
- * const { plugin, authenticate } = createBetterAuthAdapter({ auth });
757
- *
758
- * // Register the plugin (catch-all auth routes)
759
- * await fastify.register(plugin);
760
- *
761
- * // Use authenticate as a preHandler on protected routes
762
- * fastify.get('/me', { preHandler: [authenticate] }, handler);
763
- * ```
764
- */
743
+ const AUTH_SCHEME = /^[A-Za-z][A-Za-z0-9!#$%&'*+.^_`|~-]*$/;
765
744
  function createBetterAuthAdapter(options) {
766
- const { auth, basePath = "/api/auth", orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false } = options;
745
+ const { auth, basePath = "/api/auth", orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false, resolveOrgRoles, memberLookup = "better-auth", credentials = {} } = options;
746
+ const verifiers = /* @__PURE__ */ new Map();
747
+ for (const [scheme, verify] of Object.entries(credentials)) {
748
+ if (!AUTH_SCHEME.test(scheme)) throw new Error(`createBetterAuthAdapter: "${scheme}" is not an auth scheme name.`);
749
+ if (scheme.toLowerCase() === "bearer") throw new Error("createBetterAuthAdapter: the Bearer scheme is the session's.");
750
+ verifiers.set(scheme.toLowerCase(), verify);
751
+ }
752
+ /** A registered non-session credential on this request, if any. */
753
+ const presentedCredential = (request) => {
754
+ if (verifiers.size === 0) return null;
755
+ const header = request.headers.authorization;
756
+ if (typeof header !== "string") return null;
757
+ const space = header.indexOf(" ");
758
+ if (space <= 0) return null;
759
+ const verify = verifiers.get(header.slice(0, space).toLowerCase());
760
+ return verify ? {
761
+ verify,
762
+ value: header.slice(space + 1).trim()
763
+ } : null;
764
+ };
767
765
  const normalizedBase = basePath.replace(/\/+$/, "");
768
766
  const orgEnabled = !!orgContextOpt;
767
+ if (resolveOrgRoles && !orgEnabled) throw new Error("createBetterAuthAdapter: `resolveOrgRoles` requires `orgContext: true`.");
768
+ if (memberLookup !== "better-auth" && memberLookup !== "resolver") throw new Error(`createBetterAuthAdapter: \`memberLookup\` must be 'better-auth' or 'resolver' (got ${String(memberLookup)}).`);
769
+ if (memberLookup === "resolver" && !resolveOrgRoles) throw new Error("createBetterAuthAdapter: `memberLookup: 'resolver'` requires `resolveOrgRoles` — without it nothing would read membership.");
770
+ const skipMemberLookup = memberLookup === "resolver";
769
771
  /**
770
- * Validates the current session by forwarding cookies/headers
771
- * to Better Auth's `GET /api/auth/get-session` endpoint.
772
+ * The member scope for the active organization (session's active org wins; the header covers
773
+ * sessions without one), or `undefined` when the caller holds no role there. ONE path for
774
+ * `authenticate` and `optionalAuthenticate`, so `resolveOrgRoles` cannot apply on one and not the
775
+ * other.
776
+ */
777
+ const resolveMemberScope = async (headers, session, requestedOrgId, userId, userRoles) => {
778
+ const activeOrgId = session?.activeOrganizationId || requestedOrgId;
779
+ if (!activeOrgId) return void 0;
780
+ let orgRoles = null;
781
+ if (!skipMemberLookup) {
782
+ if (session?.activeOrganizationId) orgRoles = await getActiveMemberRoles(auth, headers);
783
+ if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
784
+ }
785
+ if (resolveOrgRoles && userId) orgRoles = await resolveOrgRoles({
786
+ userId,
787
+ organizationId: activeOrgId,
788
+ memberRoles: orgRoles
789
+ });
790
+ if (!orgRoles) return void 0;
791
+ const scope = {
792
+ kind: "member",
793
+ userId,
794
+ userRoles,
795
+ organizationId: activeOrgId,
796
+ orgRoles: [...orgRoles]
797
+ };
798
+ const activeTeamId = await resolveActiveTeamId({
799
+ auth,
800
+ headers,
801
+ session,
802
+ activeOrgId
803
+ });
804
+ if (activeTeamId) scope.teamId = activeTeamId;
805
+ return scope;
806
+ };
807
+ /**
808
+ * Validates the session via `auth.api.getSession` with the request headers.
772
809
  *
773
810
  * On success, sets `request.user` and `request.session`.
774
811
  * When orgContext is enabled, also sets `request.scope` to
@@ -776,12 +813,26 @@ function createBetterAuthAdapter(options) {
776
813
  * On failure, replies with 401.
777
814
  */
778
815
  const authenticate = async (request, reply) => {
816
+ const presented = presentedCredential(request);
817
+ if (presented) {
818
+ const scope = await presented.verify(presented.value, request);
819
+ if (!scope) {
820
+ reply.code(401).send({
821
+ code: ARC_ERROR_CODES.UNAUTHORIZED,
822
+ message: "Invalid credentials",
823
+ status: 401
824
+ });
825
+ return;
826
+ }
827
+ request.scope = scope;
828
+ return;
829
+ }
779
830
  try {
780
831
  const headers = buildHeaders(request);
781
832
  const sessionData = await getSessionDirect(auth, headers);
782
833
  if (!sessionData?.user) {
783
834
  reply.code(401).send({
784
- code: "arc.unauthorized",
835
+ code: ARC_ERROR_CODES.UNAUTHORIZED,
785
836
  message: "Invalid or expired session",
786
837
  status: 401
787
838
  });
@@ -799,47 +850,14 @@ function createBetterAuthAdapter(options) {
799
850
  userRoles
800
851
  };
801
852
  if (orgEnabled) {
802
- const session = sessionData.session;
803
- const requestedOrgId = requireSingleHeaderValue(request.headers, "x-organization-id");
804
- const activeOrgId = session?.activeOrganizationId || requestedOrgId;
805
- if (activeOrgId) {
806
- /**
807
- * `getActiveMember` resolves the member for the SESSION's active
808
- * organization, so with no `activeOrganizationId` it cannot succeed —
809
- * it throws `No active organization`, which the helper swallows into
810
- * `null` after paying the full endpoint round trip.
811
- *
812
- * A bearer-only deployment never calls `setActive`, so that was every
813
- * request: ~440ms spent to learn what the session already said, before
814
- * the ~870ms header-based lookup that actually answers. Ask the
815
- * session first; it is already in hand.
816
- */
817
- let orgRoles = session?.activeOrganizationId ? await getActiveMemberRoles(auth, headers) : null;
818
- if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
819
- if (orgRoles) {
820
- const scope = {
821
- kind: "member",
822
- userId,
823
- userRoles,
824
- organizationId: activeOrgId,
825
- orgRoles
826
- };
827
- const activeTeamId = await resolveActiveTeamId({
828
- auth,
829
- headers,
830
- session,
831
- activeOrgId
832
- });
833
- if (activeTeamId) scope.teamId = activeTeamId;
834
- req.scope = scope;
835
- }
836
- }
853
+ const memberScope = await resolveMemberScope(headers, sessionData.session, requireSingleHeaderValue(request.headers, "x-organization-id"), userId, userRoles);
854
+ if (memberScope) req.scope = memberScope;
837
855
  }
838
856
  } catch (err) {
839
857
  if (err instanceof ArcError) throw err;
840
858
  const message = exposeAuthErrors ? err instanceof Error ? err.message : String(err) : "Authentication required";
841
859
  reply.code(401).send({
842
- code: "arc.unauthorized",
860
+ code: ARC_ERROR_CODES.UNAUTHORIZED,
843
861
  message,
844
862
  status: 401
845
863
  });
@@ -854,6 +872,12 @@ function createBetterAuthAdapter(options) {
854
872
  * flexible filter) can apply org-scoped queries when a user IS authenticated.
855
873
  */
856
874
  const optionalAuthenticate = async (request, _reply) => {
875
+ const presented = presentedCredential(request);
876
+ if (presented) {
877
+ const scope = await presented.verify(presented.value, request);
878
+ if (scope) request.scope = scope;
879
+ return;
880
+ }
857
881
  try {
858
882
  const headers = buildHeaders(request);
859
883
  const sessionData = await getSessionDirect(auth, headers);
@@ -870,41 +894,8 @@ function createBetterAuthAdapter(options) {
870
894
  userRoles: optUserRoles
871
895
  };
872
896
  if (orgEnabled) {
873
- const session = sessionData.session;
874
- const requestedOrgId = requireSingleHeaderValue(request.headers, "x-organization-id");
875
- const activeOrgId = session?.activeOrganizationId || requestedOrgId;
876
- if (activeOrgId) {
877
- /**
878
- * `getActiveMember` resolves the member for the SESSION's active
879
- * organization, so with no `activeOrganizationId` it cannot succeed —
880
- * it throws `No active organization`, which the helper swallows into
881
- * `null` after paying the full endpoint round trip.
882
- *
883
- * A bearer-only deployment never calls `setActive`, so that was every
884
- * request: ~440ms spent to learn what the session already said, before
885
- * the ~870ms header-based lookup that actually answers. Ask the
886
- * session first; it is already in hand.
887
- */
888
- let orgRoles = session?.activeOrganizationId ? await getActiveMemberRoles(auth, headers) : null;
889
- if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
890
- if (orgRoles) {
891
- const scope = {
892
- kind: "member",
893
- userId: optUserId,
894
- userRoles: optUserRoles,
895
- organizationId: activeOrgId,
896
- orgRoles
897
- };
898
- const activeTeamId = await resolveActiveTeamId({
899
- auth,
900
- headers,
901
- session,
902
- activeOrgId
903
- });
904
- if (activeTeamId) scope.teamId = activeTeamId;
905
- req.scope = scope;
906
- }
907
- }
897
+ const memberScope = await resolveMemberScope(headers, sessionData.session, requireSingleHeaderValue(request.headers, "x-organization-id"), optUserId, optUserRoles);
898
+ if (memberScope) req.scope = memberScope;
908
899
  }
909
900
  } catch (err) {
910
901
  if (err instanceof ArcError) throw err;
@@ -914,22 +905,9 @@ function createBetterAuthAdapter(options) {
914
905
  if (openapiOpt === false) extractedOpenApi = void 0;
915
906
  else if (typeof openapiOpt === "object") extractedOpenApi = openapiOpt;
916
907
  const betterAuthPlugin = async (fastify) => {
917
- /**
918
- * `orgContext` without the `organization()` plugin is a SILENT
919
- * misconfiguration, so it fails at boot instead.
920
- *
921
- * Arc resolves org membership through `auth.api.getActiveMember` /
922
- * `getActiveMemberRole`. Absent the plugin those methods do not exist,
923
- * every lookup returns null, and `request.scope` silently stays
924
- * `'authenticated'` — so `requireOrgId()` throws 403 on every
925
- * tenant-scoped route. It fails CLOSED (no data leak), but the symptom
926
- * points nowhere near the cause: the host sees blanket 403s and no
927
- * indication that a plugin is missing. One check at registration turns
928
- * that into a sentence.
929
- */
930
908
  if (orgEnabled) {
931
909
  if (!(pickApiMethod(auth, "getActiveMember", "organization") !== void 0 || pickApiMethod(auth, "getActiveMemberRole", "organization") !== void 0)) throw new ArcError("createBetterAuthAdapter({ orgContext: true }) requires Better Auth's `organization()` plugin — `auth.api.getActiveMember` / `getActiveMemberRole` are missing. Without it arc can never resolve membership, so request.scope stays 'authenticated' and every tenant-scoped route answers 403. Add `organization()` to your betterAuth({ plugins }), or drop `orgContext`.", {
932
- code: "BETTER_AUTH_ORG_PLUGIN_MISSING",
910
+ code: ARC_ERROR_CODES.AUTH_MISCONFIGURED,
933
911
  statusCode: 500
934
912
  });
935
913
  }
@@ -940,7 +918,7 @@ function createBetterAuthAdapter(options) {
940
918
  await sendFetchResponse(await auth.handler(fetchRequest), reply);
941
919
  } catch (err) {
942
920
  throw new ArcError("Authentication service error", {
943
- code: "AUTH_SERVICE_ERROR",
921
+ code: ARC_ERROR_CODES.AUTH_SERVICE_ERROR,
944
922
  statusCode: 500,
945
923
  cause: err instanceof Error ? err : new Error(String(err))
946
924
  });
@@ -1403,4 +1381,4 @@ function mirrorTrustedOriginsFromCors(options) {
1403
1381
  return Array.from(/* @__PURE__ */ new Set([canonicalUrl, ...corsOrigins]));
1404
1382
  }
1405
1383
  //#endregion
1406
- export { MemorySessionStore, authPlugin_default as authPlugin, authPlugin as authPluginFn, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
1384
+ export { MemorySessionStore, authPlugin_default as authPlugin, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
@@ -2,7 +2,7 @@
2
2
  import { i as SessionData, s as SessionStore } from "../sessionManager-qimlKV0P.mjs";
3
3
  //#region src/auth/redis-session.d.ts
4
4
  /** Minimal Redis client interface — compatible with ioredis */
5
- interface RedisLike {
5
+ export interface RedisLike {
6
6
  get(key: string): Promise<string | null>;
7
7
  set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
8
8
  del(...keys: string[]): Promise<number>;
@@ -11,7 +11,7 @@ interface RedisLike {
11
11
  srem(key: string, ...members: string[]): Promise<number>;
12
12
  expire(key: string, seconds: number): Promise<number>;
13
13
  }
14
- interface RedisSessionStoreOptions {
14
+ export interface RedisSessionStoreOptions {
15
15
  /** Redis client instance (ioredis or compatible) */
16
16
  redis: RedisLike;
17
17
  /** Key prefix for session keys (default: 'arc:session:') */
@@ -28,7 +28,7 @@ interface RedisSessionStoreOptions {
28
28
  *
29
29
  * Session expiration is handled by Redis TTL — no cleanup interval needed.
30
30
  */
31
- declare class RedisSessionStore implements SessionStore {
31
+ export declare class RedisSessionStore implements SessionStore {
32
32
  private redis;
33
33
  private prefix;
34
34
  private userPrefix;
@@ -39,5 +39,4 @@ declare class RedisSessionStore implements SessionStore {
39
39
  deleteAll(userId: string): Promise<void>;
40
40
  deleteAllExcept(userId: string, currentSessionId: string): Promise<void>;
41
41
  }
42
- //#endregion
43
- export { RedisLike, RedisSessionStore, RedisSessionStoreOptions };
42
+ //#endregion
@@ -1,4 +1,4 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
2
2
  import sjson from "secure-json-parse";
3
3
  //#region src/auth/redis-session.ts
4
4
  /**
@@ -1,103 +1,7 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { arcLog } from "./logger/index.mjs";
3
- import { t as ArcError } from "./errors-BH7YU_9O.mjs";
4
- //#region src/permissions/filter-merge.ts
5
- /**
6
- * Policy-filter composition — logical AND, never last-writer-wins.
7
- *
8
- * Row-level policy filters can arrive from several INDEPENDENT sources for one
9
- * request: a tenant preset, an `ownedByUser` grant, an `allOf(...)` branch, a
10
- * host permission check. Every one of them is a RESTRICTION the row must
11
- * satisfy, so composing them is logical AND.
12
- *
13
- * The historical `{ ...base, ...incoming }` shallow spread did NOT implement AND:
14
- * when two sources constrained the SAME key with different values, the later one
15
- * silently REPLACED the earlier one — widening (or erasing) a restriction another
16
- * layer deliberately imposed. That is a defense-in-depth defect, not a
17
- * refinement. "Later can refine" is only sound when the refinement is provably
18
- * narrower, and a bare overwrite proves nothing.
19
- *
20
- * {@link conjoinPolicyFilters} composes with AND semantics while preserving the
21
- * common case byte-for-byte:
22
- *
23
- * - key present on only one side → carried through flat. Non-overlapping
24
- * filters (the ~99% case) produce exactly the same object the old spread did,
25
- * so there is ZERO behavior change for them.
26
- * - same key, deep-equal value → kept once (idempotent — e.g. two checks
27
- * both pinning the same `organizationId`).
28
- * - same key, DIFFERENT value → BOTH constraints are preserved under an
29
- * `$and`, so a row must satisfy both. For flat equality that conjunction is
30
- * unsatisfiable, so the query returns nothing — the safe, correct outcome for
31
- * contradictory policies (deny), and the earlier restriction is never dropped.
32
- *
33
- * Adapter safety: mongokit / sqlitekit compile `$and` natively at the DB layer,
34
- * and arc's in-memory `simpleEqualityMatcher` (AccessControl.checkPolicyFilters)
35
- * already fail-closes on operator-shaped filters. So a produced `$and` is either
36
- * evaluated correctly by the DB or rejected fail-closed in memory — both safe.
37
- *
38
- * This is the pragmatic, adapter-neutral form of the AND semantics. A canonical
39
- * filter IR in `repo-core` (nested and/or/not nodes each adapter compiles) is the
40
- * fuller design, but this closes the security defect without one.
41
- */
42
- /**
43
- * Validate + return a `$and` operand's fragment array. A security-policy
44
- * normalizer must NEVER silently drop a malformed constraint — a non-array
45
- * `$and` (e.g. `{ $and: { organizationId: "a" } }`) is a policy bug, so fail
46
- * loud (fail-closed) rather than discard the restriction it was meant to carry.
47
- */
48
- function coercePolicyAnd(value) {
49
- if (!Array.isArray(value)) throw new Error(`conjoinPolicyFilters: malformed '$and' — expected an array of filter fragments, got ${value === null ? "null" : typeof value}. A policy filter constraint must not be silently dropped.`);
50
- return value;
51
- }
52
- /** Are two flat policy-filter values equivalent for conjunction purposes? */
53
- function filterValueEqual(a, b) {
54
- if (Object.is(a, b)) return true;
55
- if (a !== null && b !== null && typeof a === "object" && typeof b === "object") try {
56
- return JSON.stringify(a) === JSON.stringify(b);
57
- } catch {
58
- return false;
59
- }
60
- return false;
61
- }
62
- /**
63
- * Conjoin two policy-filter maps with AND semantics. Never silently overwrites a
64
- * same-key constraint with a different value — conflicting keys are preserved
65
- * under `$and`. See the module doc for the full contract.
66
- */
67
- function conjoinPolicyFilters(base, incoming) {
68
- const baseEmpty = !base || Object.keys(base).length === 0;
69
- const incomingEmpty = !incoming || Object.keys(incoming).length === 0;
70
- if (baseEmpty) return incomingEmpty ? {} : { ...incoming };
71
- if (incomingEmpty) return { ...base };
72
- const result = {};
73
- const andParts = [];
74
- const baseAnd = base.$and;
75
- if (baseAnd !== void 0) andParts.push(...coercePolicyAnd(baseAnd));
76
- for (const [key, value] of Object.entries(base)) {
77
- if (key === "$and") continue;
78
- result[key] = value;
79
- }
80
- for (const [key, value] of Object.entries(incoming)) {
81
- if (key === "$and") {
82
- andParts.push(...coercePolicyAnd(value));
83
- continue;
84
- }
85
- if (!(key in result)) {
86
- result[key] = value;
87
- continue;
88
- }
89
- if (filterValueEqual(result[key], value)) continue;
90
- andParts.push({ [key]: result[key] }, { [key]: value });
91
- delete result[key];
92
- }
93
- if (andParts.length > 0) result.$and = andParts;
94
- for (const source of [base, incoming]) for (const symbol of Object.getOwnPropertySymbols(source)) {
95
- const descriptor = Object.getOwnPropertyDescriptor(source, symbol);
96
- if (descriptor) Object.defineProperty(result, symbol, descriptor);
97
- }
98
- return result;
99
- }
100
- //#endregion
1
+ /*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
2
+ import { t as arcLog } from "./logger-D4V1zGLI.mjs";
3
+ import { t as conjoinPolicyFilters } from "./filter-merge-DGM-GQ1J.mjs";
4
+ import { t as ArcError } from "./errors-CW4anJBy.mjs";
101
5
  //#region src/permissions/authorizationDecision.ts
102
6
  const log = arcLog("permissions:enforce");
103
7
  /**
@@ -194,8 +98,9 @@ async function evaluatePermissionDecision(check, context) {
194
98
  * This is the enforcement point (PEP) — the ONE place scope + data policy are
195
99
  * installed (and the obligation seam lives).
196
100
  */
197
- function applyAuthorizationDecision(decision, request) {
101
+ function applyAuthorizationDecision(decision, req) {
198
102
  if (decision.effect !== "allow") return;
103
+ const request = req;
199
104
  if (decision.policy) request._policyFilters = conjoinPolicyFilters(request._policyFilters, decision.policy);
200
105
  if (decision.scope) {
201
106
  const current = request.scope;
@@ -267,4 +172,4 @@ async function evaluateAndApplyPermission(check, context, request, reply, opts)
267
172
  return true;
268
173
  }
269
174
  //#endregion
270
- export { conjoinPolicyFilters as a, normalizeToDecision as i, evaluateAndApplyPermission as n, evaluatePermissionDecision as r, applyAuthorizationDecision as t };
175
+ export { normalizeToDecision as i, evaluateAndApplyPermission as n, evaluatePermissionDecision as r, applyAuthorizationDecision as t };
@@ -169,6 +169,15 @@ type PermissionCheck<TDoc = Record<string, unknown>> = ((context: PermissionCont
169
169
  interface PermissionCheckMeta {
170
170
  /** Set by allowPublic() — marks the endpoint as publicly accessible */
171
171
  _isPublic?: boolean;
172
+ /**
173
+ * Set by `allowPublic({ policy })` — the gate admits EVERYONE, but what they
174
+ * can read or write is narrowed by a row policy. Public and row-filtered are
175
+ * independent facts: `_isPublic` says who may call the endpoint, this says the
176
+ * rows are not all of them. Surfaced as `rowFiltered` by `describePermission`,
177
+ * so a "what is publicly exposed?" audit can tell an open catalogue from a
178
+ * catalogue that shows strangers only its published rows.
179
+ */
180
+ _rowPolicy?: boolean;
172
181
  /**
173
182
  * Set by `denyAll()` — nobody passes this gate, ever.
174
183
  *
@@ -184,6 +193,14 @@ interface PermissionCheckMeta {
184
193
  * and an access audit reports a surface that does not exist.
185
194
  */
186
195
  _isDenied?: boolean;
196
+ /**
197
+ * A deployment-static closed door, read LAZILY by `describePermission`: when it returns
198
+ * `true`, nobody passes. Set it on a check whose ENTIRE runtime decision is `!_deniedWhen()`
199
+ * (a tier / feature flag fixed at boot but unknown when the check is built). `allOf` is
200
+ * denied when any child is; `anyOf` when every child is; such a check never taints `allOf`
201
+ * as `conditional`.
202
+ */
203
+ _deniedWhen?: () => boolean;
187
204
  /**
188
205
  * Set by requireAuth() — a pure identity gate (must be signed in) that adds NO
189
206
  * row-level or environmental condition. Marked so `allOf` does not mistake it
@@ -297,13 +314,18 @@ declare module "fastify" {
297
314
  /**
298
315
  * This read is deliberately NOT scoped to a tenant — a marketplace listing.
299
316
  *
300
- * Set by `multiTenantPreset({ crossTenant: [...] })`. Tenancy is applied in four separate
301
- * places for a read (the preset middleware, `QueryResolver`, `buildTenantRepoOptions` and the
302
- * post-fetch `checkOrgScope`), each deriving the org from the scope on its own, so a decision
303
- * made in one of them is silently re-made by the other three. This flag is how that decision
304
- * travels. Never set from user input.
317
+ * Set by `multiTenantPreset({ crossTenant: [...] })`, read by `resolveTenancy`
318
+ * (`core/tenancy.ts`) — the one rule every read-scoping layer switches on.
319
+ * Never set from user input.
305
320
  */
306
321
  _crossTenantRead?: boolean;
322
+ /**
323
+ * Resolved tenant dimensions (`{ organizationId, teamId, … }`) from
324
+ * `multiTenantPreset`'s multi-field specs. Forwarded to the repository as
325
+ * top-level options by `buildTenantRepoOptions`, for kits whose tenant
326
+ * plugin reads `context.<field>`. Never set from user input.
327
+ */
328
+ _tenantFields?: Record<string, unknown>;
307
329
  }
308
330
  }
309
331
  type AnyRecord = Record<string, unknown>;