@classytic/arc 2.41.1 → 2.43.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 (230) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
  3. package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
  6. package/dist/actionPermissions-BotX1YOa.mjs +776 -0
  7. package/dist/audit/index.d.mts +43 -5
  8. package/dist/audit/index.mjs +48 -26
  9. package/dist/auth/audit.mjs +1 -1
  10. package/dist/auth/index.d.mts +43 -13
  11. package/dist/auth/index.mjs +130 -155
  12. package/dist/auth/redis-session.mjs +1 -1
  13. package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
  14. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  15. package/dist/boundedMap-BULzfXYU.mjs +57 -0
  16. package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
  17. package/dist/cache/index.d.mts +22 -34
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-DbCBxlgw.mjs → caching-Cc2M1DwO.mjs} +3 -3
  20. package/dist/claimed-B10RTfkM.mjs +8 -0
  21. package/dist/cleanup/index.d.mts +6 -10
  22. package/dist/cleanup/index.mjs +27 -73
  23. package/dist/cli/commands/describe.d.mts +4 -2
  24. package/dist/cli/commands/describe.mjs +7 -4
  25. package/dist/cli/commands/docs.mjs +3 -3
  26. package/dist/cli/commands/doctor.mjs +1 -1
  27. package/dist/cli/commands/generate.mjs +2 -2
  28. package/dist/cli/commands/init.mjs +2 -2
  29. package/dist/cli/commands/introspect.mjs +2 -2
  30. package/dist/cli/index.mjs +3 -3
  31. package/dist/{constants-D5Ubn1fe.mjs → constants-Cu7Mx7_U.mjs} +4 -2
  32. package/dist/context/index.d.mts +2 -5
  33. package/dist/context/index.mjs +18 -38
  34. package/dist/core/index.d.mts +3 -3
  35. package/dist/core/index.mjs +8 -8
  36. package/dist/{core-5lazhva9.mjs → core-Cb0LjccA.mjs} +5 -4
  37. package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
  38. package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
  39. package/dist/{preloadResources-f3boenVZ.mjs → createApp-D-953ZAq.mjs} +299 -390
  40. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
  41. package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
  42. package/dist/{defineResource-DxkMTjRn.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
  43. package/dist/discovery/index.mjs +1 -1
  44. package/dist/docs/index.d.mts +10 -4
  45. package/dist/docs/index.mjs +4 -4
  46. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  47. package/dist/{elevation-CkvG0LG2.mjs → elevation-DG8SrLN-.mjs} +41 -12
  48. package/dist/encryption/index.mjs +3 -3
  49. package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-BnfDviL1.mjs} +26 -5
  50. package/dist/{errors-BH7YU_9O.mjs → errors-CtrywMHP.mjs} +58 -23
  51. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  52. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  53. package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
  54. package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
  55. package/dist/events/index.d.mts +33 -8
  56. package/dist/events/index.mjs +49 -53
  57. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  58. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  59. package/dist/events/transports/redis.d.mts +1 -1
  60. package/dist/events/transports/redis.mjs +2 -2
  61. package/dist/factory/index.d.mts +32 -17
  62. package/dist/factory/index.mjs +213 -32
  63. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
  64. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  65. package/dist/{fields-BHrkoF5y.mjs → fields-ToPYpMqS.mjs} +1 -1
  66. package/dist/filter-merge-IpGywvym.mjs +99 -0
  67. package/dist/{generate-BLRf2m3y.mjs → generate-DRTkLlIl.mjs} +2 -2
  68. package/dist/{headers-BIXWKrzQ.mjs → headers-BbkIF6vA.mjs} +2 -2
  69. package/dist/health-D-Ft6JM6.mjs +22 -0
  70. package/dist/{health-ZRHeDHXH.mjs → health-DqbsG-_w.mjs} +115 -55
  71. package/dist/hooks/index.d.mts +2 -2
  72. package/dist/hooks/index.mjs +2 -2
  73. package/dist/idempotency/index.d.mts +31 -7
  74. package/dist/idempotency/index.mjs +155 -98
  75. package/dist/idempotency/redis.d.mts +1 -1
  76. package/dist/idempotency/redis.mjs +48 -11
  77. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  78. package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
  79. package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
  80. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  81. package/dist/index-JdbZAIsm.d.mts +510 -0
  82. package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
  83. package/dist/index.d.mts +8 -8
  84. package/dist/index.mjs +11 -11
  85. package/dist/{init-CpLkjU18.mjs → init-B_oUYDXI.mjs} +1 -1
  86. package/dist/integrations/event-gateway.d.mts +8 -3
  87. package/dist/integrations/event-gateway.mjs +4 -4
  88. package/dist/integrations/index.d.mts +3 -3
  89. package/dist/integrations/index.mjs +1 -1
  90. package/dist/integrations/jobs/index.d.mts +1 -1
  91. package/dist/integrations/jobs/index.mjs +18 -5
  92. package/dist/integrations/mcp/index.d.mts +72 -59
  93. package/dist/integrations/mcp/index.mjs +19 -4
  94. package/dist/integrations/mcp/testing.d.mts +2 -2
  95. package/dist/integrations/mcp/testing.mjs +2 -2
  96. package/dist/integrations/streamline.d.mts +78 -136
  97. package/dist/integrations/streamline.mjs +116 -54
  98. package/dist/integrations/webhooks.d.mts +60 -120
  99. package/dist/integrations/webhooks.mjs +36 -80
  100. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  101. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  102. package/dist/integrations/websocket-redis.d.mts +1 -1
  103. package/dist/integrations/websocket-redis.mjs +1 -1
  104. package/dist/integrations/websocket.d.mts +3 -3
  105. package/dist/integrations/websocket.mjs +2 -2
  106. package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
  107. package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-B4YVGcql.mjs} +2 -2
  108. package/dist/{keys-cpZCtcIE.mjs → keys-B2KtgMLJ.mjs} +7 -18
  109. package/dist/{loadResources-BPfWiI5s.mjs → loadResources-Dn4lalw2.mjs} +3 -3
  110. package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
  111. package/dist/logger/index.d.mts +1 -8
  112. package/dist/logger/index.mjs +3 -153
  113. package/dist/logger-DXjfIqjV.mjs +160 -0
  114. package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
  115. package/dist/{memory-Iotz0cCx.mjs → memory-9Cp7611C.mjs} +36 -34
  116. package/dist/{metrics-NMsmpRmY.mjs → metrics-CWI11eRu.mjs} +39 -72
  117. package/dist/middleware/index.d.mts +2 -2
  118. package/dist/middleware/index.mjs +3 -3
  119. package/dist/migrations/index.mjs +2 -2
  120. package/dist/{module-DpLJG2_N.mjs → module-BSvOQC7E.mjs} +8 -8
  121. package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
  122. package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
  123. package/dist/{openapi-CmFf3yq_.mjs → openapi-DT_G4VhY.mjs} +48 -10
  124. package/dist/outbox-admin/index.d.mts +3 -3
  125. package/dist/outbox-admin/index.mjs +6 -18
  126. package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
  127. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  128. package/dist/permissions/index.d.mts +5 -4
  129. package/dist/permissions/index.mjs +8 -6
  130. package/dist/{permissions-D8TBH44E.mjs → permissions-Cm0uYHvd.mjs} +149 -33
  131. package/dist/{pipe-FMWNheSO.mjs → pipe-BqeGT6UO.mjs} +2 -2
  132. package/dist/pipeline/index.d.mts +2 -2
  133. package/dist/pipeline/index.mjs +2 -2
  134. package/dist/plan-FVR6LP59.mjs +565 -0
  135. package/dist/{BaseController-CYEwjyjN.mjs → plugin-BccET1tL.mjs} +1256 -345
  136. package/dist/plugins/index.d.mts +14 -11
  137. package/dist/plugins/index.mjs +44 -22
  138. package/dist/plugins/response-cache.mjs +8 -14
  139. package/dist/plugins/tracing-entry.mjs +2 -2
  140. package/dist/{pluralize-CXWZIzTv.mjs → pluralize-CVIOcgMF.mjs} +1 -1
  141. package/dist/presets/filesUpload.d.mts +2 -2
  142. package/dist/presets/filesUpload.mjs +5 -5
  143. package/dist/presets/index.d.mts +26 -31
  144. package/dist/presets/index.mjs +4 -4
  145. package/dist/presets/multiTenant.d.mts +2 -107
  146. package/dist/presets/multiTenant.mjs +22 -162
  147. package/dist/presets/search.d.mts +2 -2
  148. package/dist/presets/search.mjs +2 -2
  149. package/dist/{presets-ZJSPgmT2.mjs → presets-Cpvj6fGO.mjs} +99 -107
  150. package/dist/{projection-BzIgzoKZ.mjs → projection-Ce4aQhQk.mjs} +2 -2
  151. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  152. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  153. package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
  154. package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
  155. package/dist/{realtime-8tIQWZz4.mjs → realtime-DmPE9S54.mjs} +25 -21
  156. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  157. package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
  158. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  159. package/dist/registry/index.d.mts +9 -4
  160. package/dist/registry/index.mjs +4 -4
  161. package/dist/{registry-DNkgGQcK.mjs → registry-Bcz9G1bh.mjs} +10 -8
  162. package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
  163. package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
  164. package/dist/{requestContext-cDkAUr-P.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
  165. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  166. package/dist/requirement-KDdoRcOP.mjs +35 -0
  167. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  168. package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
  169. package/dist/{rolldown-runtime-eysyzFAF.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
  170. package/dist/routeValidator-D36SW8qt.mjs +70 -0
  171. package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
  172. package/dist/{schedules-j6uXXAeJ.mjs → schedules-C-OpmHBq.mjs} +46 -37
  173. package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BORek3O-.mjs} +2 -2
  174. package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
  175. package/dist/schemas/index.d.mts +10 -0
  176. package/dist/schemas/index.mjs +8 -3
  177. package/dist/scim/index.mjs +2 -2
  178. package/dist/scope/index.d.mts +2 -2
  179. package/dist/scope/index.mjs +6 -6
  180. package/dist/{scope-BUi1xV1I.mjs → scope-DQthe88b.mjs} +4 -4
  181. package/dist/shutdown-MqA81z_P.mjs +59 -0
  182. package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
  183. package/dist/{sse-BeqVKQKu.mjs → sse-CYzQBVFe.mjs} +11 -15
  184. package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-B5_Afiqn.mjs} +2 -2
  185. package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
  186. package/dist/{streaming-IcRFjWQU.mjs → streaming-DjTQhZrN.mjs} +1 -1
  187. package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
  188. package/dist/tenancy--ed9eH_z.mjs +43 -0
  189. package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
  190. package/dist/testing/cleanupStoreContract.mjs +1 -1
  191. package/dist/testing/index.d.mts +193 -175
  192. package/dist/testing/index.mjs +286 -212
  193. package/dist/testing/outboxStoreContract.mjs +137 -2
  194. package/dist/testing/requiredPorts.d.mts +17 -0
  195. package/dist/testing/requiredPorts.mjs +65 -0
  196. package/dist/testing/storageContract.mjs +1 -1
  197. package/dist/timing-qczZ0YC-.mjs +46 -0
  198. package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
  199. package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-CySF12G-.mjs} +1 -1
  200. package/dist/types/index.d.mts +4 -4
  201. package/dist/types/index.mjs +1 -1
  202. package/dist/types/storage.mjs +1 -1
  203. package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
  204. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  205. package/dist/{types-CgenB1gv.mjs → types-Bk9NOgOC.mjs} +4 -4
  206. package/dist/usage/index.d.mts +2 -3
  207. package/dist/usage/index.mjs +5 -5
  208. package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-aklawRaK.mjs} +1 -1
  209. package/dist/utils/index.d.mts +84 -223
  210. package/dist/utils/index.mjs +13 -12
  211. package/dist/{utils-CxcctrAh.mjs → utils-ec1oTo1n.mjs} +315 -404
  212. package/dist/{validate-DrYWnOzI.mjs → validate-9yzHC1e2.mjs} +6 -14
  213. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  214. package/dist/{versioning-DKkT4hNJ.mjs → versioning-fhhWHV74.mjs} +4 -4
  215. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  216. package/dist/{workScope-CxKqoIFn.mjs → workScope-zpPyge4t.mjs} +2 -2
  217. package/package.json +16 -12
  218. package/skills/arc/SKILL.md +13 -4
  219. package/skills/arc/references/auth.md +8 -5
  220. package/skills/arc/references/integrations.md +1 -1
  221. package/skills/arc/references/mcp.md +8 -6
  222. package/skills/arc/references/multi-tenancy.md +19 -14
  223. package/skills/arc/references/production.md +27 -10
  224. package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
  225. package/dist/index-CFfLKMH4.d.mts +0 -290
  226. package/dist/interface-YzIbF7Ii.mjs +0 -17
  227. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  228. package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
  229. package/dist/sync/changelog.d.mts +0 -3
  230. 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.43.0 | MIT | https://classytic.com */
2
+ import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-CtrywMHP.mjs";
3
+ import { n as requireSingleHeaderValue } from "../headers-BbkIF6vA.mjs";
4
+ import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-aklawRaK.mjs";
5
+ import { o as toJsonSchema } from "../schemaConverter-BORek3O-.mjs";
6
+ import { n as declareRuntimeCapability } from "../runtimeCapabilities-0tccjRlj.mjs";
7
+ import { E as requireOrgRole, T as requireOrgMembership, k as requireTeamMembership } from "../permissions-Cm0uYHvd.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";
@@ -543,6 +544,22 @@ function mergeUserFieldsIntoRequestBody(requestBody, userFields, endpointPath) {
543
544
  //#endregion
544
545
  //#region src/auth/betterAuth.ts
545
546
  /**
547
+ * Better Auth Adapter for Arc/Fastify
548
+ *
549
+ * Bridges Fastify <-> Better Auth's Fetch API (Request/Response).
550
+ * Better Auth is the USER's dependency -- Arc only provides this thin adapter.
551
+ *
552
+ * @example
553
+ * import { betterAuth } from 'better-auth';
554
+ * import { createBetterAuthAdapter } from '@classytic/arc/auth';
555
+ *
556
+ * const auth = betterAuth({ ... });
557
+ *
558
+ * const app = await createApp({
559
+ * auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth }) },
560
+ * });
561
+ */
562
+ /**
546
563
  * Convert a Fastify request into a Fetch API Request.
547
564
  *
548
565
  * Better Auth expects standard Web API Request objects.
@@ -604,28 +621,23 @@ async function sendFetchResponse(response, reply) {
604
621
  }
605
622
  }
606
623
  /**
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.
624
+ * Resolve the current session via `auth.api.getSession`. Throws
625
+ * `ArcError(BETTER_AUTH_API_MISSING)` when it is absent (a stub, not a real
626
+ * `betterAuth()` instance).
612
627
  */
613
628
  async function getSessionDirect(auth, headers) {
614
629
  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",
630
+ 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.", {
631
+ code: ARC_ERROR_CODES.AUTH_MISCONFIGURED,
617
632
  statusCode: 500
618
633
  });
619
634
  const result = await api.getSession({ headers });
620
635
  return result?.user ? result : null;
621
636
  }
622
637
  /**
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.
638
+ * Read a method from `auth.api`: the flat shape real instances expose
639
+ * (`api.getActiveMember`), falling back to the nested stub shape
640
+ * (`api.organization.getActiveMember`).
629
641
  */
630
642
  function pickApiMethod(auth, name, group) {
631
643
  const api = auth.api;
@@ -649,11 +661,8 @@ async function getActiveMemberRoles(auth, headers) {
649
661
  }
650
662
  }
651
663
  /**
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.
664
+ * Resolve org roles for an explicit organizationId — for sessions without
665
+ * `activeOrganizationId` (e.g. API-key auth via `x-organization-id`).
657
666
  */
658
667
  async function getMemberRolesByOrg(auth, headers, organizationId) {
659
668
  const fn = pickApiMethod(auth, "getActiveMemberRole", "organization");
@@ -669,21 +678,12 @@ async function getMemberRolesByOrg(auth, headers, organizationId) {
669
678
  }
670
679
  }
671
680
  /**
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.
681
+ * Teams the current user is a MEMBER of in one organization, used to validate
682
+ * `activeTeamId` before binding it to scope.
681
683
  *
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.
684
+ * Must stay `listUserTeams`: `listOrganizationTeams` lists EVERY team in the
685
+ * org and would admit a team the user isn't a member of. (`listTeams` is the
686
+ * client-side name; `auth.api` has no such method.)
687
687
  */
688
688
  async function listTeamsDirect(auth, headers, organizationId) {
689
689
  const fn = pickApiMethod(auth, "listUserTeams", "organization");
@@ -737,38 +737,72 @@ async function resolveActiveTeamId(input) {
737
737
  if (!activeTeamId) return void 0;
738
738
  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
739
  }
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
- */
740
+ const AUTH_SCHEME = /^[A-Za-z][A-Za-z0-9!#$%&'*+.^_`|~-]*$/;
765
741
  function createBetterAuthAdapter(options) {
766
- const { auth, basePath = "/api/auth", orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false } = options;
742
+ const { auth, basePath = "/api/auth", orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false, resolveOrgRoles, memberLookup = "better-auth", credentials = {} } = options;
743
+ const verifiers = /* @__PURE__ */ new Map();
744
+ for (const [scheme, verify] of Object.entries(credentials)) {
745
+ if (!AUTH_SCHEME.test(scheme)) throw new Error(`createBetterAuthAdapter: "${scheme}" is not an auth scheme name.`);
746
+ if (scheme.toLowerCase() === "bearer") throw new Error("createBetterAuthAdapter: the Bearer scheme is the session's.");
747
+ verifiers.set(scheme.toLowerCase(), verify);
748
+ }
749
+ /** A registered non-session credential on this request, if any. */
750
+ const presentedCredential = (request) => {
751
+ if (verifiers.size === 0) return null;
752
+ const header = request.headers.authorization;
753
+ if (typeof header !== "string") return null;
754
+ const space = header.indexOf(" ");
755
+ if (space <= 0) return null;
756
+ const verify = verifiers.get(header.slice(0, space).toLowerCase());
757
+ return verify ? {
758
+ verify,
759
+ value: header.slice(space + 1).trim()
760
+ } : null;
761
+ };
767
762
  const normalizedBase = basePath.replace(/\/+$/, "");
768
763
  const orgEnabled = !!orgContextOpt;
764
+ if (resolveOrgRoles && !orgEnabled) throw new Error("createBetterAuthAdapter: `resolveOrgRoles` requires `orgContext: true`.");
765
+ if (memberLookup !== "better-auth" && memberLookup !== "resolver") throw new Error(`createBetterAuthAdapter: \`memberLookup\` must be 'better-auth' or 'resolver' (got ${String(memberLookup)}).`);
766
+ if (memberLookup === "resolver" && !resolveOrgRoles) throw new Error("createBetterAuthAdapter: `memberLookup: 'resolver'` requires `resolveOrgRoles` — without it nothing would read membership.");
767
+ const skipMemberLookup = memberLookup === "resolver";
768
+ /**
769
+ * The member scope for the active organization (session's active org wins; the header covers
770
+ * sessions without one), or `undefined` when the caller holds no role there. ONE path for
771
+ * `authenticate` and `optionalAuthenticate`, so `resolveOrgRoles` cannot apply on one and not the
772
+ * other.
773
+ */
774
+ const resolveMemberScope = async (headers, session, requestedOrgId, userId, userRoles) => {
775
+ const activeOrgId = session?.activeOrganizationId || requestedOrgId;
776
+ if (!activeOrgId) return void 0;
777
+ let orgRoles = null;
778
+ if (!skipMemberLookup) {
779
+ if (session?.activeOrganizationId) orgRoles = await getActiveMemberRoles(auth, headers);
780
+ if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
781
+ }
782
+ if (resolveOrgRoles && userId) orgRoles = await resolveOrgRoles({
783
+ userId,
784
+ organizationId: activeOrgId,
785
+ memberRoles: orgRoles
786
+ });
787
+ if (!orgRoles) return void 0;
788
+ const scope = {
789
+ kind: "member",
790
+ userId,
791
+ userRoles,
792
+ organizationId: activeOrgId,
793
+ orgRoles: [...orgRoles]
794
+ };
795
+ const activeTeamId = await resolveActiveTeamId({
796
+ auth,
797
+ headers,
798
+ session,
799
+ activeOrgId
800
+ });
801
+ if (activeTeamId) scope.teamId = activeTeamId;
802
+ return scope;
803
+ };
769
804
  /**
770
- * Validates the current session by forwarding cookies/headers
771
- * to Better Auth's `GET /api/auth/get-session` endpoint.
805
+ * Validates the session via `auth.api.getSession` with the request headers.
772
806
  *
773
807
  * On success, sets `request.user` and `request.session`.
774
808
  * When orgContext is enabled, also sets `request.scope` to
@@ -776,12 +810,26 @@ function createBetterAuthAdapter(options) {
776
810
  * On failure, replies with 401.
777
811
  */
778
812
  const authenticate = async (request, reply) => {
813
+ const presented = presentedCredential(request);
814
+ if (presented) {
815
+ const scope = await presented.verify(presented.value, request);
816
+ if (!scope) {
817
+ reply.code(401).send({
818
+ code: ARC_ERROR_CODES.UNAUTHORIZED,
819
+ message: "Invalid credentials",
820
+ status: 401
821
+ });
822
+ return;
823
+ }
824
+ request.scope = scope;
825
+ return;
826
+ }
779
827
  try {
780
828
  const headers = buildHeaders(request);
781
829
  const sessionData = await getSessionDirect(auth, headers);
782
830
  if (!sessionData?.user) {
783
831
  reply.code(401).send({
784
- code: "arc.unauthorized",
832
+ code: ARC_ERROR_CODES.UNAUTHORIZED,
785
833
  message: "Invalid or expired session",
786
834
  status: 401
787
835
  });
@@ -799,47 +847,14 @@ function createBetterAuthAdapter(options) {
799
847
  userRoles
800
848
  };
801
849
  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
- }
850
+ const memberScope = await resolveMemberScope(headers, sessionData.session, requireSingleHeaderValue(request.headers, "x-organization-id"), userId, userRoles);
851
+ if (memberScope) req.scope = memberScope;
837
852
  }
838
853
  } catch (err) {
839
854
  if (err instanceof ArcError) throw err;
840
855
  const message = exposeAuthErrors ? err instanceof Error ? err.message : String(err) : "Authentication required";
841
856
  reply.code(401).send({
842
- code: "arc.unauthorized",
857
+ code: ARC_ERROR_CODES.UNAUTHORIZED,
843
858
  message,
844
859
  status: 401
845
860
  });
@@ -854,6 +869,12 @@ function createBetterAuthAdapter(options) {
854
869
  * flexible filter) can apply org-scoped queries when a user IS authenticated.
855
870
  */
856
871
  const optionalAuthenticate = async (request, _reply) => {
872
+ const presented = presentedCredential(request);
873
+ if (presented) {
874
+ const scope = await presented.verify(presented.value, request);
875
+ if (scope) request.scope = scope;
876
+ return;
877
+ }
857
878
  try {
858
879
  const headers = buildHeaders(request);
859
880
  const sessionData = await getSessionDirect(auth, headers);
@@ -870,41 +891,8 @@ function createBetterAuthAdapter(options) {
870
891
  userRoles: optUserRoles
871
892
  };
872
893
  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
- }
894
+ const memberScope = await resolveMemberScope(headers, sessionData.session, requireSingleHeaderValue(request.headers, "x-organization-id"), optUserId, optUserRoles);
895
+ if (memberScope) req.scope = memberScope;
908
896
  }
909
897
  } catch (err) {
910
898
  if (err instanceof ArcError) throw err;
@@ -914,22 +902,9 @@ function createBetterAuthAdapter(options) {
914
902
  if (openapiOpt === false) extractedOpenApi = void 0;
915
903
  else if (typeof openapiOpt === "object") extractedOpenApi = openapiOpt;
916
904
  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
905
  if (orgEnabled) {
931
906
  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",
907
+ code: ARC_ERROR_CODES.AUTH_MISCONFIGURED,
933
908
  statusCode: 500
934
909
  });
935
910
  }
@@ -940,7 +915,7 @@ function createBetterAuthAdapter(options) {
940
915
  await sendFetchResponse(await auth.handler(fetchRequest), reply);
941
916
  } catch (err) {
942
917
  throw new ArcError("Authentication service error", {
943
- code: "AUTH_SERVICE_ERROR",
918
+ code: ARC_ERROR_CODES.AUTH_SERVICE_ERROR,
944
919
  statusCode: 500,
945
920
  cause: err instanceof Error ? err : new Error(String(err))
946
921
  });
@@ -1403,4 +1378,4 @@ function mirrorTrustedOriginsFromCors(options) {
1403
1378
  return Array.from(/* @__PURE__ */ new Set([canonicalUrl, ...corsOrigins]));
1404
1379
  }
1405
1380
  //#endregion
1406
- export { MemorySessionStore, authPlugin_default as authPlugin, authPlugin as authPluginFn, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
1381
+ export { MemorySessionStore, authPlugin_default as authPlugin, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
@@ -1,4 +1,4 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.43.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.43.0 | MIT | https://classytic.com */
2
+ import { t as arcLog } from "./logger-DXjfIqjV.mjs";
3
+ import { t as conjoinPolicyFilters } from "./filter-merge-IpGywvym.mjs";
4
+ import { t as ArcError } from "./errors-CtrywMHP.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>;
@@ -0,0 +1,57 @@
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ //#region src/collections/boundedMap.ts
3
+ /**
4
+ * Insertion-order LRU on a plain `Map` — the two moves, written once.
5
+ *
6
+ * A `Map` iterates in insertion order, so "move to most-recent" is
7
+ * delete-then-set and "least-recent" is the first key. Seven places in arc
8
+ * hand-rolled that, and hand-rolled copies drifted into real bugs:
9
+ *
10
+ * - the response cache evicted with `while (size >= maxEntries)` BEFORE
11
+ * inserting, so `maxEntries: 0` spun forever on the first write — no key
12
+ * to evict, loop condition never changes, event loop frozen;
13
+ * - `maxEntries: NaN` made the same comparison always false: unbounded;
14
+ * - the MCP auth cache guarded eviction with `if (firstKey)`, a falsy check,
15
+ * so an empty-string key could never be evicted.
16
+ *
17
+ * These are functions over the caller's own `Map`, not a class, on purpose:
18
+ * each site keeps its TTL, byte accounting and stats, and borrows only the
19
+ * part that kept going wrong.
20
+ */
21
+ /** Move `key` to most-recent and set it. Use on read-hit (LRU) or on overwrite. */
22
+ function touch(map, key, value) {
23
+ map.delete(key);
24
+ map.set(key, value);
25
+ }
26
+ /**
27
+ * Evict least-recent entries until `map.size <= maxSize`; returns how many.
28
+ *
29
+ * Call AFTER inserting. That ordering is what makes it total: with
30
+ * `maxSize: 0` the just-inserted entry is evicted (a disabled cache), never an
31
+ * infinite loop, and the loop terminates on the iterator running dry rather
32
+ * than on the size comparison alone. A non-finite or negative `maxSize` is a
33
+ * configuration bug — {@link assertCapacity} it where the option is read.
34
+ */
35
+ function evictToSize(map, maxSize, onEvict) {
36
+ let evicted = 0;
37
+ while (map.size > maxSize) {
38
+ const oldest = map.entries().next();
39
+ if (oldest.done) break;
40
+ const [key, value] = oldest.value;
41
+ map.delete(key);
42
+ onEvict?.(key, value);
43
+ evicted++;
44
+ }
45
+ return evicted;
46
+ }
47
+ /**
48
+ * Validate a capacity option at construction: a non-negative integer. `0` is
49
+ * allowed and means "keep nothing". Throws with the option's name so the error
50
+ * points at the host's config, not at a hang or an unbounded map later.
51
+ */
52
+ function assertCapacity(name, value) {
53
+ if (!Number.isInteger(value) || value < 0) throw new Error(`[arc] ${name} must be a non-negative integer, got ${String(value)}.`);
54
+ return value;
55
+ }
56
+ //#endregion
57
+ export { evictToSize as n, touch as r, assertCapacity as t };
@@ -1,7 +1,7 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { a as conjoinPolicyFilters } from "./authorizationDecision-DawKGoXb.mjs";
3
- import { g as toRepositoryFilter } from "./BaseController-CYEwjyjN.mjs";
4
- import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-DrYWnOzI.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as conjoinPolicyFilters } from "./filter-merge-IpGywvym.mjs";
3
+ import { C as toRepositoryFilter } from "./plugin-BccET1tL.mjs";
4
+ import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-9yzHC1e2.mjs";
5
5
  //#region src/core/aggregation/buildHandler.ts
6
6
  /**
7
7
  * Framework-agnostic aggregation execution. Runs safety guards,