@classytic/arc 2.41.0 → 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-n7qO5Cz3.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
  3. package/dist/{HookSystem-CxvZBo77.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-Cu7uTJp9.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-2YY_Kaj1.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-CUhjGHGZ.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-DsWDjIxx.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-BxLgNFF7.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-iNGqYerO.mjs → core-Cb0LjccA.mjs} +5 -4
  37. package/dist/{createActionRouter-BBlbPs3k.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
  38. package/dist/{createAggregationRouter-Dv5SuK2x.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
  39. package/dist/{preloadResources-Bbp4QJ8V.mjs → createApp-D-953ZAq.mjs} +299 -390
  40. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
  41. package/dist/{defineAggregation-UyeurGkp.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
  42. package/dist/{defineResource-DzGT-2LP.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-BF-vhGxY.mjs → elevation-DG8SrLN-.mjs} +41 -12
  48. package/dist/encryption/index.mjs +3 -3
  49. package/dist/{errorHandler-CIMkCOWm.mjs → errorHandler-BnfDviL1.mjs} +26 -5
  50. package/dist/{errors-C_YhELhe.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-CmXaPWLv.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
  54. package/dist/{subscribe-helpers-AVYPgqTl.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-BTyPLHK0.mjs → fields-ToPYpMqS.mjs} +1 -1
  66. package/dist/filter-merge-IpGywvym.mjs +99 -0
  67. package/dist/{generate-D_YmiCNf.mjs → generate-DRTkLlIl.mjs} +2 -2
  68. package/dist/{headers-CNECDE6q.mjs → headers-BbkIF6vA.mjs} +2 -2
  69. package/dist/health-D-Ft6JM6.mjs +22 -0
  70. package/dist/{health-CxQzbJmr.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-Y2MlSN9n.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-Cq9KYl2Z.mjs → jsonBody-B4YVGcql.mjs} +2 -2
  108. package/dist/{keys-Cmv4cBRa.mjs → keys-B2KtgMLJ.mjs} +7 -18
  109. package/dist/{loadResources-CblF7xt2.mjs → loadResources-Dn4lalw2.mjs} +3 -3
  110. package/dist/{loadResourcesFromEntry-99fIMC3p.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-xF3Gw-HW.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
  115. package/dist/{memory-CpDZ9NPw.mjs → memory-9Cp7611C.mjs} +36 -34
  116. package/dist/{metrics-BaKnq8ag.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-BkQEesNQ.mjs → module-BSvOQC7E.mjs} +8 -8
  121. package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
  122. package/dist/{multipartBody-C92aJ8am.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
  123. package/dist/{openapi-C3LFRzPq.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-dsCyLxXH.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-DiHXy8Ck.mjs → permissions-Cm0uYHvd.mjs} +149 -33
  131. package/dist/{pipe-CBVmUJ-j.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-DYJRZ0EJ.mjs → plugin-BccET1tL.mjs} +1256 -344
  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-2_U6QZQg.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-BdlugYTc.mjs → presets-Cpvj6fGO.mjs} +99 -107
  150. package/dist/{projection-Uvgp-EEj.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-B5F09bYr.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
  154. package/dist/{rateLimitKey-CK-EU2L8.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
  155. package/dist/{realtime-FnnD0n3Z.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-D3y8VEJ1.mjs → registry-Bcz9G1bh.mjs} +10 -8
  162. package/dist/{renewingLease-B3VYh5aU.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
  163. package/dist/{replyHelpers-DPHsdw-5.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
  164. package/dist/{requestContext-cSVKnYPk.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-DjDuyglE.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
  170. package/dist/routeValidator-D36SW8qt.mjs +70 -0
  171. package/dist/{runtimeCapabilities-DSA_dH1_.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
  172. package/dist/{schedules-BktJb3Y3.mjs → schedules-C-OpmHBq.mjs} +46 -37
  173. package/dist/{schemaConverter-BLCSmNbr.mjs → schemaConverter-BORek3O-.mjs} +2 -2
  174. package/dist/{schemaIR-K16Bb5BX.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-jSxuBTFM.mjs → scope-DQthe88b.mjs} +4 -4
  181. package/dist/shutdown-MqA81z_P.mjs +59 -0
  182. package/dist/{simpleEqualityMatcher-BiOnyflP.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
  183. package/dist/{sse-CH7Slxc1.mjs → sse-CYzQBVFe.mjs} +11 -15
  184. package/dist/{sseStream-CN-Lh1Tu.mjs → sseStream-B5_Afiqn.mjs} +2 -2
  185. package/dist/{store-helpers-9DX9-1j_.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
  186. package/dist/{streaming-dvppnxsr.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-DCWMI6hV.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
  199. package/dist/{typeGuards-D7tHFdH6.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-CDy3-zw1.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-DDMOZ_Pm.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-B-zj_b__.mjs → utils-ec1oTo1n.mjs} +315 -404
  212. package/dist/{validate-R0G_JrnK.mjs → validate-9yzHC1e2.mjs} +6 -14
  213. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  214. package/dist/{versioning-DuVWcu-F.mjs → versioning-fhhWHV74.mjs} +4 -4
  215. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  216. package/dist/{workScope-BhsWde4k.mjs → workScope-zpPyge4t.mjs} +2 -2
  217. package/package.json +578 -574
  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-DPwPWPOV.mjs +0 -350
  225. package/dist/index-CFfLKMH4.d.mts +0 -290
  226. package/dist/interface-BIpgzbZl.mjs +0 -17
  227. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  228. package/dist/registerRoute-DRsSHPSM.mjs +0 -364
  229. package/dist/sync/changelog.d.mts +0 -3
  230. package/dist/sync/changelog.mjs +0 -3
@@ -1,5 +1,6 @@
1
- /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-DjDuyglE.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-D1w9Y1VJ.mjs";
3
+ import { n as evictToSize } from "./boundedMap-BULzfXYU.mjs";
3
4
  //#region src/cache/memory.ts
4
5
  var memory_exports = /* @__PURE__ */ __exportAll({ MemoryCacheStore: () => MemoryCacheStore });
5
6
  /**
@@ -24,13 +25,13 @@ var MemoryCacheStore = class {
24
25
  _misses = 0;
25
26
  _evictions = 0;
26
27
  constructor(options = {}) {
27
- this.defaultTtlSeconds = options.defaultTtlSeconds ?? 60;
28
- this.maxEntries = clamp(options.maxEntries ?? 1e3, 1, 1e5);
29
- this.maxEntryBytes = clamp(options.maxEntryBytes ?? 262144, 1024, 10485760);
30
- this.maxMemoryBytes = options.maxMemoryBytes ?? 52428800;
31
- this.evictionWatermark = clamp(options.evictionWatermark ?? .9, .5, 1);
28
+ this.defaultTtlSeconds = finite("defaultTtlSeconds", options.defaultTtlSeconds ?? 60);
29
+ this.maxEntries = clamp("maxEntries", options.maxEntries ?? 1e3, 1, 1e5);
30
+ this.maxEntryBytes = clamp("maxEntryBytes", options.maxEntryBytes ?? 262144, 1024, 10485760);
31
+ this.maxMemoryBytes = clamp("maxMemoryBytes", options.maxMemoryBytes ?? 52428800, 0, Number.MAX_SAFE_INTEGER);
32
+ this.evictionWatermark = clamp("evictionWatermark", options.evictionWatermark ?? .9, .5, 1);
32
33
  this.logger = options.logger ?? console;
33
- const cleanupIntervalMs = clamp(options.cleanupIntervalMs ?? 3e4, 1e3, 6e5);
34
+ const cleanupIntervalMs = clamp("cleanupIntervalMs", options.cleanupIntervalMs ?? 3e4, 1e3, 6e5);
34
35
  this.cleanupTimer = setInterval(() => this.cleanupExpired(), cleanupIntervalMs);
35
36
  if (this.cleanupTimer.unref) this.cleanupTimer.unref();
36
37
  }
@@ -54,11 +55,12 @@ var MemoryCacheStore = class {
54
55
  const effectiveTtlSeconds = ttlSeconds ?? this.defaultTtlSeconds;
55
56
  if (!Number.isFinite(effectiveTtlSeconds) || effectiveTtlSeconds <= 0) return;
56
57
  const ttlMs = effectiveTtlSeconds * 1e3;
57
- const size = this.estimateSize(value);
58
- if (size > this.maxEntryBytes) {
59
- this.logger.warn(`[MemoryCacheStore] Skipping oversized entry for key '${key}' (${size} bytes > ${this.maxEntryBytes} bytes)`);
58
+ const valueBytes = this.estimateSize(value);
59
+ if (valueBytes > this.maxEntryBytes) {
60
+ this.logger.warn(`[MemoryCacheStore] Skipping oversized entry for key '${key}' (${valueBytes} bytes > ${this.maxEntryBytes} bytes)`);
60
61
  return;
61
62
  }
63
+ const size = valueBytes + keyBytes(key);
62
64
  const existing = this.cache.get(key);
63
65
  if (existing) {
64
66
  this.currentBytes -= existing.size;
@@ -70,8 +72,7 @@ var MemoryCacheStore = class {
70
72
  expiresAt: Date.now() + ttlMs
71
73
  });
72
74
  this.currentBytes += size;
73
- this.evictToLimit();
74
- if (this.maxMemoryBytes > 0) this.evictToMemoryLimit();
75
+ this.enforceLimits();
75
76
  }
76
77
  async delete(key) {
77
78
  const entry = this.cache.get(key);
@@ -96,14 +97,14 @@ var MemoryCacheStore = class {
96
97
  const effectiveTtlSeconds = ttlSeconds ?? this.defaultTtlSeconds;
97
98
  if (entry) this.currentBytes -= entry.size;
98
99
  this.cache.delete(key);
99
- const size = 8;
100
+ const size = 8 + keyBytes(key);
100
101
  this.cache.set(key, {
101
102
  value: next,
102
103
  size,
103
104
  expiresAt: live ? entry.expiresAt : now + Math.max(1, effectiveTtlSeconds) * 1e3
104
105
  });
105
106
  this.currentBytes += size;
106
- this.evictToLimit();
107
+ this.enforceLimits();
107
108
  return next;
108
109
  }
109
110
  async clear(pattern) {
@@ -133,26 +134,19 @@ var MemoryCacheStore = class {
133
134
  this.cache.delete(key);
134
135
  this.currentBytes -= entry.size;
135
136
  }
136
- evictToLimit() {
137
- while (this.cache.size > this.maxEntries) {
138
- const oldestKey = this.cache.keys().next().value;
139
- if (!oldestKey) break;
140
- const entry = this.cache.get(oldestKey);
141
- if (!entry) break;
142
- this.removeEntry(oldestKey, entry);
143
- this._evictions++;
144
- }
145
- }
146
- evictToMemoryLimit() {
137
+ /** Every write path ends here, so no path can skip a limit. */
138
+ enforceLimits() {
139
+ this._evictions += evictToSize(this.cache, this.maxEntries, (_key, entry) => {
140
+ this.currentBytes -= entry.size;
141
+ });
142
+ if (this.maxMemoryBytes <= 0 || this.currentBytes <= this.maxMemoryBytes) return;
147
143
  const target = this.maxMemoryBytes * this.evictionWatermark;
148
- while (this.currentBytes > this.maxMemoryBytes && this.cache.size > 0) {
149
- const oldestKey = this.cache.keys().next().value;
150
- if (!oldestKey) break;
151
- const entry = this.cache.get(oldestKey);
152
- if (!entry) break;
144
+ while (this.currentBytes > target) {
145
+ const oldest = this.cache.entries().next();
146
+ if (oldest.done) break;
147
+ const [oldestKey, entry] = oldest.value;
153
148
  this.removeEntry(oldestKey, entry);
154
149
  this._evictions++;
155
- if (this.currentBytes <= target) break;
156
150
  }
157
151
  }
158
152
  cleanupExpired() {
@@ -169,8 +163,16 @@ var MemoryCacheStore = class {
169
163
  }
170
164
  }
171
165
  };
172
- function clamp(value, min, max) {
173
- return Math.min(max, Math.max(min, value));
166
+ /** A non-finite option is a configuration bug — `Math.max(min, NaN)` is NaN, which silently disabled the cap. */
167
+ function finite(name, value) {
168
+ if (!Number.isFinite(value)) throw new Error(`[arc] MemoryCacheStore ${name} must be a finite number, got ${String(value)}.`);
169
+ return value;
170
+ }
171
+ function clamp(name, value, min, max) {
172
+ return Math.min(max, Math.max(min, finite(name, value)));
173
+ }
174
+ function keyBytes(key) {
175
+ return Buffer.byteLength(key, "utf8");
174
176
  }
175
177
  /**
176
178
  * Translate a glob pattern (`prefix:*`, `*:tag:v`) into a regex. Only `*`
@@ -1,5 +1,6 @@
1
- /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-DjDuyglE.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-D1w9Y1VJ.mjs";
3
+ import { a as MetricHistogram, i as DURATION_BUCKETS_SECONDS, o as MetricSeries, s as renderMetricSample } from "./health-DqbsG-_w.mjs";
3
4
  import fp from "fastify-plugin";
4
5
  //#region src/plugins/metrics.ts
5
6
  var metrics_exports = /* @__PURE__ */ __exportAll({
@@ -7,99 +8,64 @@ var metrics_exports = /* @__PURE__ */ __exportAll({
7
8
  metricsPlugin: () => metricsPlugin
8
9
  });
9
10
  var Counter = class {
10
- data = /* @__PURE__ */ new Map();
11
+ series;
12
+ constructor(maxSeries) {
13
+ this.series = new MetricSeries(maxSeries);
14
+ }
11
15
  inc(labels, value = 1) {
12
- const key = labelKey(labels);
13
- const existing = this.data.get(key);
14
- if (existing) existing.value += value;
15
- else this.data.set(key, {
16
- labels: { ...labels },
17
- value
18
- });
16
+ this.series.get(labels, () => ({ value: 0 })).value += value;
19
17
  }
20
18
  values() {
21
- return [...this.data.values()];
22
- }
23
- reset() {
24
- this.data.clear();
25
- }
26
- };
27
- var Histogram = class {
28
- data = /* @__PURE__ */ new Map();
29
- observe(labels, value) {
30
- const key = labelKey(labels);
31
- const existing = this.data.get(key);
32
- if (existing) {
33
- existing.sum += value;
34
- existing.count += 1;
35
- } else this.data.set(key, {
19
+ return this.series.entries().map(({ labels, value }) => ({
36
20
  labels: { ...labels },
37
- sum: value,
38
- count: 1
39
- });
40
- }
41
- values() {
42
- return [...this.data.values()].flatMap(({ labels, sum, count }) => [{
43
- labels: {
44
- ...labels,
45
- le: "sum"
46
- },
47
- value: sum
48
- }, {
49
- labels: {
50
- ...labels,
51
- le: "count"
52
- },
53
- value: count
54
- }]);
21
+ value: value.value
22
+ }));
55
23
  }
56
24
  reset() {
57
- this.data.clear();
25
+ this.series.reset();
58
26
  }
59
27
  };
60
28
  var Gauge = class {
61
- data = /* @__PURE__ */ new Map();
29
+ series;
30
+ constructor(maxSeries) {
31
+ this.series = new MetricSeries(maxSeries);
32
+ }
62
33
  set(labels, value) {
63
- const key = labelKey(labels);
64
- this.data.set(key, {
65
- labels: { ...labels },
66
- value
67
- });
34
+ this.series.get(labels, () => ({ value: 0 })).value = value;
68
35
  }
69
36
  values() {
70
- return [...this.data.values()];
37
+ return this.series.entries().map(({ labels, value }) => ({
38
+ labels: { ...labels },
39
+ value: value.value
40
+ }));
71
41
  }
72
42
  reset() {
73
- this.data.clear();
43
+ this.series.reset();
74
44
  }
75
45
  };
76
- function labelKey(labels) {
77
- return Object.entries(labels).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",");
78
- }
79
46
  function toPrometheusText(metrics) {
80
47
  const lines = [];
81
48
  for (const m of metrics) {
82
49
  lines.push(`# HELP ${m.name} ${m.help}`);
83
50
  lines.push(`# TYPE ${m.name} ${m.type}`);
84
- for (const v of m.values) {
85
- const labelStr = Object.entries(v.labels).map(([k, val]) => `${k}="${val}"`).join(",");
86
- lines.push(`${m.name}{${labelStr}} ${v.value}`);
87
- }
51
+ for (const v of m.values) lines.push(renderMetricSample(m.name, v));
88
52
  }
89
53
  return `${lines.join("\n")}\n`;
90
54
  }
91
55
  const metricsPlugin = async (fastify, opts = {}) => {
92
56
  const path = opts.path ?? "/_metrics";
93
57
  const prefix = opts.prefix ?? "arc";
94
- const httpRequestsTotal = new Counter();
95
- const crudOpsTotal = new Counter();
96
- const cacheHitsTotal = new Counter();
97
- const cacheMissesTotal = new Counter();
98
- const eventsPublishedTotal = new Counter();
99
- const eventsConsumedTotal = new Counter();
100
- const httpDuration = new Histogram();
101
- const crudDuration = new Histogram();
102
- const circuitBreakerState = new Gauge();
58
+ const maxSeries = opts.maxSeries ?? 1e3;
59
+ if (!/^[a-zA-Z_:][a-zA-Z0-9_:]*$/.test(prefix)) throw new Error("metrics.prefix must be a valid Prometheus metric prefix");
60
+ const httpRequestsTotal = new Counter(maxSeries);
61
+ const crudOpsTotal = new Counter(maxSeries);
62
+ const cacheHitsTotal = new Counter(maxSeries);
63
+ const cacheMissesTotal = new Counter(maxSeries);
64
+ const eventsPublishedTotal = new Counter(maxSeries);
65
+ const eventsConsumedTotal = new Counter(maxSeries);
66
+ const httpDuration = new MetricHistogram(DURATION_BUCKETS_SECONDS, maxSeries);
67
+ const crudDuration = new MetricHistogram(DURATION_BUCKETS_SECONDS, maxSeries);
68
+ const circuitBreakerState = new Gauge(maxSeries);
103
69
  const collector = {
104
70
  collect() {
105
71
  const metrics = [
@@ -196,17 +162,18 @@ const metricsPlugin = async (fastify, opts = {}) => {
196
162
  eventsConsumedTotal.inc({ event_type: eventType });
197
163
  },
198
164
  recordCircuitBreakerState(service, state) {
199
- const stateValue = state === "open" ? 1 : state === "half-open" ? 2 : 0;
165
+ const normalized = state.toLowerCase().replaceAll("_", "-");
166
+ const stateValue = normalized === "open" ? 1 : normalized === "half-open" ? 2 : 0;
200
167
  circuitBreakerState.set({ service }, stateValue);
201
168
  }
202
169
  };
203
170
  fastify.decorate("metrics", collector);
204
171
  fastify.addHook("onResponse", async (request, reply) => {
205
- if (request.url === path) return;
172
+ if (request.routeOptions.url === path) return;
206
173
  const duration = reply.elapsedTime / 1e3;
207
174
  const labels = {
208
175
  method: request.method,
209
- route: request.routeOptions?.url ?? request.url,
176
+ route: request.routeOptions.url ?? "__unmatched__",
210
177
  status: String(reply.statusCode)
211
178
  };
212
179
  httpRequestsTotal.inc(labels);
@@ -222,4 +189,4 @@ var metrics_default = fp(metricsPlugin, {
222
189
  fastify: "5.x"
223
190
  });
224
191
  //#endregion
225
- export { metrics_default as n, metrics_exports as r, metricsPlugin as t };
192
+ export { metrics_exports as n, metrics_default as t };
@@ -1,5 +1,5 @@
1
1
 
2
- import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-DHbyPo5j.mjs";
2
+ import { A as RequestWithExtras, J as MiddlewareConfig, k as MiddlewareHandler } from "../index-mqaqZNpe.mjs";
3
3
  import { RouteHandlerMethod } from "fastify";
4
4
  //#region src/middleware/middleware.d.ts
5
5
  interface NamedMiddleware {
@@ -122,4 +122,4 @@ interface MultipartBodyOptions {
122
122
  */
123
123
  declare function multipartBody(options?: MultipartBodyOptions): RouteHandlerMethod;
124
124
  //#endregion
125
- export { type MultipartBodyOptions, type NamedMiddleware, type ParsedFile, middleware, multipartBody, sortMiddlewares };
125
+ export { type MiddlewareOptions, type MultipartBodyOptions, type NamedMiddleware, type ParsedFile, middleware, multipartBody, sortMiddlewares };
@@ -1,6 +1,6 @@
1
- /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
- import { t as CRUD_OPERATIONS } from "../constants-BxLgNFF7.mjs";
3
- import { t as multipartBody } from "../multipartBody-C92aJ8am.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as CRUD_OPERATIONS } from "../constants-Cu7Mx7_U.mjs";
3
+ import { t as multipartBody } from "../multipartBody-CXM2a-Dy.mjs";
4
4
  //#region src/middleware/middleware.ts
5
5
  /**
6
6
  * Named Middleware — Priority-based, conditional middleware execution.
@@ -1,5 +1,5 @@
1
- /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
- import { t as startRenewingLease } from "../renewingLease-B3VYh5aU.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as startRenewingLease } from "../renewingLease-qODa7KJQ.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  //#region src/migrations/index.ts
5
5
  /**
@@ -1,18 +1,18 @@
1
- /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
2
  //#region src/factory/module/index.ts
3
3
  /**
4
4
  * Identity helper for authoring a typed module — mirrors `defineResource`.
5
5
  * Gives inference + a single obvious construction site; does no work.
6
6
  *
7
- * `TExports` is inferred from the `bootstrap` return value, so a module
8
- * author gets a typed public export for free:
7
+ * Everything is inferred: the export from `bootstrap`'s return, the port types from
8
+ * `requires` (declare `requires` BEFORE the phases that read `ports`):
9
9
  *
10
10
  * ```ts
11
- * export const accountingModule = (deps: Deps) =>
12
- * defineModule({
13
- * name: "accounting",
14
- * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
15
- * });
11
+ * defineModule({
12
+ * name: "accounting",
13
+ * requires: { ports: ["outbox"], optionalPorts: ["calendar"] },
14
+ * bootstrap: (f, { ports }) => createAccountingEngine(ports.outbox, ports.calendar),
15
+ * });
16
16
  * ```
17
17
  */
18
18
  function defineModule(module) {
@@ -0,0 +1,104 @@
1
+
2
+ import { G as CrudRouteKey, W as PresetResult } from "./index-mqaqZNpe.mjs";
3
+ //#region src/permissions/tenantPreparation.d.ts
4
+ type TenantFieldSpec = {
5
+ field: string;
6
+ type: "org";
7
+ } | {
8
+ field: string;
9
+ type: "team";
10
+ } | {
11
+ field: string;
12
+ contextKey: string;
13
+ };
14
+ //#endregion
15
+ //#region src/presets/multiTenant.d.ts
16
+ interface MultiTenantOptions {
17
+ /**
18
+ * Single-field form: name of the database field to filter by.
19
+ * Reads `getOrgId(scope)` as the value source.
20
+ *
21
+ * Mutually exclusive with `tenantFields` — pass one or the other, not both.
22
+ *
23
+ * @default 'organizationId'
24
+ */
25
+ tenantField?: string;
26
+ /**
27
+ * Multi-field form (2.7.1+): list of tenant dimensions to filter by in
28
+ * lockstep. Use this when a resource is scoped by more than just
29
+ * organization — e.g. organization + branch, organization + project,
30
+ * or organization + team + workspace.
31
+ *
32
+ * Each entry is a discriminated `TenantFieldSpec` declaring where the
33
+ * value comes from. Use `type: 'org'` / `type: 'team'` for built-in scope
34
+ * fields, or `contextKey: '...'` to read from `scope.context` (set by
35
+ * your auth function).
36
+ *
37
+ * Fail-closed: if any required dimension is missing, the request is
38
+ * rejected. Elevated scopes apply whatever resolves and skip the rest.
39
+ *
40
+ * Mutually exclusive with `tenantField`.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * multiTenantPreset({
45
+ * tenantFields: [
46
+ * { field: 'organizationId', type: 'org' },
47
+ * { field: 'branchId', contextKey: 'branchId' },
48
+ * ],
49
+ * })
50
+ * ```
51
+ */
52
+ tenantFields?: readonly TenantFieldSpec[];
53
+ /**
54
+ * Routes that allow public access (no auth required)
55
+ * When a route is in this array:
56
+ * - If no org context: allow through without filtering (public data)
57
+ * - If org context present: require auth and apply filter
58
+ *
59
+ * @default [] (strict mode - all routes require auth)
60
+ * @example
61
+ * multiTenantPreset({ allowPublic: ['list', 'get'] })
62
+ */
63
+ allowPublic?: CrudRouteKey[];
64
+ /**
65
+ * Reads that are NOT scoped to a tenant at all — a marketplace listing.
66
+ *
67
+ * `allowPublic` answers "may an anonymous caller reach this route?". That is a different
68
+ * question from "is this listing scoped to a tenant?", and until this option there was no way
69
+ * to answer the second one with "no". An `allowPublic` route still filters by the caller's
70
+ * organization whenever they have one, and a signed-in user of a multi-org product ALWAYS has
71
+ * one, because choosing an organization writes it onto the session. So a cross-tenant catalog
72
+ * silently became single-tenant the moment one of its own sellers browsed it: the same URL,
73
+ * with no header and no query parameter to explain the difference, returned a different world
74
+ * depending on who was asking.
75
+ *
76
+ * The only escape was to drop this preset from the resource entirely, which also drops tenant
77
+ * INJECTION on create/update — the thing that stops a member moving a document into another
78
+ * organization. Trading a write guarantee for a read behaviour is not a real choice.
79
+ *
80
+ * Listed routes get no tenant middleware: no filter, and nothing stashed for the repo layer.
81
+ * Everything else keeps its filter, so writes are unaffected.
82
+ *
83
+ * READS ONLY, by type. A cross-tenant `create` or `update` is not a listing, it is an escape
84
+ * from tenancy, and it should never be one option away by accident.
85
+ *
86
+ * THIS DOES NOT AUTHORIZE ANYTHING. Removing the tenant filter widens what the query can
87
+ * return, so the resource owes a row-level `policy` from its permission check saying what a
88
+ * stranger may see (`{ status: 'active' }`, `{ visibility: 'public' }`). Declaring this without
89
+ * one publishes every row in every tenant.
90
+ *
91
+ * @default []
92
+ * @example
93
+ * // A public catalog: anyone may browse every seller's LIVE products,
94
+ * // while create/update/delete stay pinned to the caller's own shop.
95
+ * multiTenantPreset({
96
+ * allowPublic: ['list', 'get'],
97
+ * crossTenant: ['list', 'get'],
98
+ * })
99
+ */
100
+ crossTenant?: readonly Extract<CrudRouteKey, "list" | "get">[];
101
+ }
102
+ declare function multiTenantPreset(options?: MultiTenantOptions): PresetResult;
103
+ //#endregion
104
+ export { multiTenantPreset as n, TenantFieldSpec as r, MultiTenantOptions as t };
@@ -1,6 +1,41 @@
1
- /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { ARC_ERROR_CODES } from "@classytic/repo-core/errors";
2
3
  import sjson from "secure-json-parse";
3
4
  //#region src/middleware/multipartBody.ts
5
+ /**
6
+ * Multipart Body Middleware — Opt-in multipart/form-data parsing for CRUD routes.
7
+ *
8
+ * Parses multipart form fields into `req.body` and attaches files to `req.body._files`.
9
+ * Use with a `before:create` or `before:update` hook to process files (upload to S3, etc.)
10
+ * before BaseController persists the record.
11
+ *
12
+ * Requires `@fastify/multipart` to be registered on the Fastify instance
13
+ * (Arc registers it by default via `createApp()` unless `multipart: false`).
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { defineResource } from '@classytic/arc';
18
+ * import { multipartBody } from '@classytic/arc/middleware';
19
+ *
20
+ * const productResource = defineResource({
21
+ * name: 'product',
22
+ * adapter,
23
+ * middlewares: {
24
+ * create: [multipartBody()],
25
+ * update: [multipartBody()],
26
+ * },
27
+ * hooks: {
28
+ * 'before:create': async (data) => {
29
+ * if (data._files?.image) {
30
+ * data.imageUrl = await uploadToS3(data._files.image);
31
+ * delete data._files;
32
+ * }
33
+ * return data;
34
+ * },
35
+ * },
36
+ * });
37
+ * ```
38
+ */
4
39
  const DEFAULT_MAX_FILE_SIZE = 10485760;
5
40
  const DEFAULT_MAX_FILES = 5;
6
41
  const DEFAULT_MAX_TOTAL_BYTES = 26214400;
@@ -78,19 +113,19 @@ function multipartBody(options = {}) {
78
113
  for await (const part of parts) if (part.type === "file") {
79
114
  if (fileCount >= maxFiles) continue;
80
115
  if (mimeMatcher && !mimeMatcher.matches(part.mimetype)) return reply.code(415).send({
81
- code: "arc.unsupported_media_type",
116
+ code: ARC_ERROR_CODES.UNSUPPORTED_MEDIA_TYPE,
82
117
  message: `File type '${part.mimetype}' not allowed. Accepted: ${mimeMatcher.describe()}`,
83
118
  status: 415
84
119
  });
85
120
  const buffer = await part.toBuffer();
86
121
  if (buffer.length > maxFileSize) return reply.code(413).send({
87
- code: "arc.payload_too_large",
122
+ code: ARC_ERROR_CODES.PAYLOAD_TOO_LARGE,
88
123
  message: `File '${part.filename}' exceeds maximum size of ${Math.round(maxFileSize / 1024 / 1024)}MB`,
89
124
  status: 413
90
125
  });
91
126
  totalBytes += buffer.length;
92
127
  if (totalBytes > maxTotalBytes) return reply.code(413).send({
93
- code: "arc.payload_too_large",
128
+ code: ARC_ERROR_CODES.PAYLOAD_TOO_LARGE,
94
129
  message: `Combined upload size exceeds ${Math.round(maxTotalBytes / 1024 / 1024)}MB per request`,
95
130
  status: 413
96
131
  });
@@ -106,7 +141,7 @@ function multipartBody(options = {}) {
106
141
  } catch (err) {
107
142
  request.log.error({ err }, "multipartBody: failed to parse multipart form");
108
143
  return reply.code(400).send({
109
- code: "arc.bad_request",
144
+ code: ARC_ERROR_CODES.BAD_REQUEST,
110
145
  message: "Failed to parse multipart form data",
111
146
  status: 400
112
147
  });
@@ -114,10 +149,14 @@ function multipartBody(options = {}) {
114
149
  if (requiredFields) {
115
150
  const missing = requiredFields.filter((name) => !(name in files));
116
151
  if (missing.length > 0) return reply.code(400).send({
117
- code: "MISSING_FILE_FIELDS",
152
+ code: ARC_ERROR_CODES.VALIDATION_ERROR,
118
153
  message: `Missing required file field${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`,
119
154
  status: 400,
120
- details: { missing }
155
+ details: missing.map((name) => ({
156
+ path: name,
157
+ code: "required",
158
+ message: `File field "${name}" is required`
159
+ }))
121
160
  });
122
161
  }
123
162
  if (fileCount > 0) body[filesKey] = files;