@classytic/arc 2.38.0 → 2.41.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 (196) hide show
  1. package/dist/{BaseController-CYaDvmOB.mjs → BaseController-DYJRZ0EJ.mjs} +114 -24
  2. package/dist/{EventTransport-Cx60_s-6.d.mts → EventTransport-CbUKDzZZ.d.mts} +33 -0
  3. package/dist/{EventTransport-Gq7JWbWw.mjs → EventTransport-n7qO5Cz3.mjs} +22 -2
  4. package/dist/{HookSystem-C0ikzqQf.mjs → HookSystem-CxvZBo77.mjs} +2 -2
  5. package/dist/{ResourceRegistry-DPcX--zk.mjs → ResourceRegistry-Cu7uTJp9.mjs} +2 -2
  6. package/dist/{actionPermissions-jlFZ1it4.mjs → actionPermissions-DPwPWPOV.mjs} +7 -6
  7. package/dist/audit/index.d.mts +2 -2
  8. package/dist/audit/index.mjs +10 -3
  9. package/dist/auth/audit.mjs +1 -1
  10. package/dist/auth/index.d.mts +2 -2
  11. package/dist/auth/index.mjs +12 -6
  12. package/dist/auth/redis-session.mjs +1 -1
  13. package/dist/{authorizationDecision-BP8GHtjc.mjs → authorizationDecision-2YY_Kaj1.mjs} +2 -2
  14. package/dist/{base-C-NL2fTi.d.mts → base-CxrV1LmG.d.mts} +10 -0
  15. package/dist/{buildHandler-DjXOqIvX.mjs → buildHandler-CUhjGHGZ.mjs} +4 -4
  16. package/dist/cache/index.d.mts +4 -2
  17. package/dist/cache/index.mjs +4 -4
  18. package/dist/{caching-a3ZEcJJs.mjs → caching-DsWDjIxx.mjs} +2 -2
  19. package/dist/cleanup/index.d.mts +2 -2
  20. package/dist/cleanup/index.mjs +21 -5
  21. package/dist/cli/commands/describe.d.mts +1 -1
  22. package/dist/cli/commands/describe.mjs +3 -3
  23. package/dist/cli/commands/docs.mjs +3 -3
  24. package/dist/cli/commands/doctor.mjs +1 -1
  25. package/dist/cli/commands/generate.mjs +2 -2
  26. package/dist/cli/commands/init.mjs +2 -2
  27. package/dist/cli/commands/introspect.mjs +2 -2
  28. package/dist/cli/index.mjs +3 -3
  29. package/dist/{constants-BONghE1Y.mjs → constants-BxLgNFF7.mjs} +1 -1
  30. package/dist/context/index.d.mts +34 -6
  31. package/dist/context/index.mjs +42 -24
  32. package/dist/core/index.d.mts +2 -2
  33. package/dist/core/index.mjs +7 -7
  34. package/dist/{core-8She7FgF.mjs → core-iNGqYerO.mjs} +3 -3
  35. package/dist/{createActionRouter-Bqi6Bhhx.mjs → createActionRouter-BBlbPs3k.mjs} +7 -7
  36. package/dist/{createAggregationRouter-BsYO7ktd.mjs → createAggregationRouter-Dv5SuK2x.mjs} +7 -7
  37. package/dist/createMcpServer-CbDWkZ1T.d.mts +48 -0
  38. package/dist/{defineAggregation-0KBvqEEC.mjs → defineAggregation-UyeurGkp.mjs} +1 -1
  39. package/dist/{defineResource-BMdyRelW.mjs → defineResource-DzGT-2LP.mjs} +20 -22
  40. package/dist/discovery/index.mjs +1 -1
  41. package/dist/docs/index.d.mts +1 -1
  42. package/dist/docs/index.mjs +3 -3
  43. package/dist/{elevation-C6woysGC.mjs → elevation-BF-vhGxY.mjs} +4 -4
  44. package/dist/encryption/index.mjs +3 -3
  45. package/dist/{errorHandler-DI5AN2b5.d.mts → errorHandler--4rSMMvq.d.mts} +7 -1
  46. package/dist/{errorHandler-BdmIWPba.mjs → errorHandler-CIMkCOWm.mjs} +3 -3
  47. package/dist/{errors-2ExmOgIJ.mjs → errors-C_YhELhe.mjs} +1 -1
  48. package/dist/{eventPlugin-DFvHa4Nf.d.mts → eventPlugin-B52abJ8W.d.mts} +1 -1
  49. package/dist/{eventPlugin-DJwpGMzx.mjs → eventPlugin-CmXaPWLv.mjs} +5 -5
  50. package/dist/events/index.d.mts +4 -4
  51. package/dist/events/index.mjs +5 -5
  52. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  53. package/dist/events/transports/redis-stream-entry.mjs +34 -24
  54. package/dist/events/transports/redis.d.mts +1 -1
  55. package/dist/events/transports/redis.mjs +11 -8
  56. package/dist/factory/index.d.mts +5 -5
  57. package/dist/factory/index.mjs +5 -5
  58. package/dist/{fastify-augmentation-jYA2oHyL.d.mts → fastify-augmentation-Cq2nHVaW.d.mts} +3 -3
  59. package/dist/{fields-Cha2QCKH.mjs → fields-BTyPLHK0.mjs} +1 -1
  60. package/dist/{generate-BUP4Oydg.mjs → generate-D_YmiCNf.mjs} +4 -3
  61. package/dist/{headers-DeXNifVj.mjs → headers-CNECDE6q.mjs} +2 -2
  62. package/dist/{health-Bh0N_FLk.mjs → health-CxQzbJmr.mjs} +10 -2
  63. package/dist/hooks/index.d.mts +1 -1
  64. package/dist/hooks/index.mjs +2 -2
  65. package/dist/idempotency/index.d.mts +1 -1
  66. package/dist/idempotency/index.mjs +4 -4
  67. package/dist/idempotency/redis.d.mts +1 -1
  68. package/dist/idempotency/redis.mjs +1 -1
  69. package/dist/{index-BtYoMip1.d.mts → index-BHdZD8zK.d.mts} +10 -2
  70. package/dist/{index-D2uIUSQk.d.mts → index-CFfLKMH4.d.mts} +2 -2
  71. package/dist/{index-eyg5WEcy.d.mts → index-CbJiYrfv.d.mts} +1 -1
  72. package/dist/{index-BRoDtjek.d.mts → index-D4u76eQW.d.mts} +3 -3
  73. package/dist/{index-B78WZom8.d.mts → index-DHbyPo5j.d.mts} +52 -12
  74. package/dist/index.d.mts +5 -5
  75. package/dist/index.mjs +11 -11
  76. package/dist/{init-Dg_Am2o_.mjs → init-Y2MlSN9n.mjs} +9 -9
  77. package/dist/integrations/event-gateway.d.mts +1 -1
  78. package/dist/integrations/event-gateway.mjs +2 -2
  79. package/dist/integrations/index.d.mts +2 -2
  80. package/dist/integrations/index.mjs +1 -1
  81. package/dist/integrations/jobs/index.d.mts +1 -1
  82. package/dist/integrations/jobs/index.mjs +2 -2
  83. package/dist/integrations/mcp/index.d.mts +4 -47
  84. package/dist/integrations/mcp/index.mjs +2 -2
  85. package/dist/integrations/mcp/testing.d.mts +22 -2
  86. package/dist/integrations/mcp/testing.mjs +55 -33
  87. package/dist/integrations/streamline.mjs +3 -3
  88. package/dist/integrations/webhooks.d.mts +2 -2
  89. package/dist/integrations/webhooks.mjs +3 -3
  90. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  91. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  92. package/dist/integrations/websocket-redis.mjs +1 -1
  93. package/dist/integrations/websocket.mjs +24 -6
  94. package/dist/{interface-DYPVscCU.mjs → interface-BIpgzbZl.mjs} +1 -1
  95. package/dist/{jsonBody-DVui0uJl.mjs → jsonBody-Cq9KYl2Z.mjs} +2 -2
  96. package/dist/{keys-BSuxGS-B.mjs → keys-Cmv4cBRa.mjs} +1 -1
  97. package/dist/{loadResources-CNd1DWnK.mjs → loadResources-CblF7xt2.mjs} +2 -2
  98. package/dist/{loadResourcesFromEntry-DNj8cpiY.mjs → loadResourcesFromEntry-99fIMC3p.mjs} +2 -2
  99. package/dist/logger/index.mjs +1 -1
  100. package/dist/{mcpPlugin-KWsezMx7.mjs → mcpPlugin-xF3Gw-HW.mjs} +75 -31
  101. package/dist/{memory-BiPVaCPg.mjs → memory-CpDZ9NPw.mjs} +2 -2
  102. package/dist/{metrics-CsWaIVTa.mjs → metrics-BaKnq8ag.mjs} +2 -2
  103. package/dist/middleware/index.d.mts +1 -1
  104. package/dist/middleware/index.mjs +3 -3
  105. package/dist/migrations/index.mjs +2 -2
  106. package/dist/{module-BQXHxMEk.mjs → module-BkQEesNQ.mjs} +1 -1
  107. package/dist/{multipartBody-u6VrS_iH.mjs → multipartBody-C92aJ8am.mjs} +1 -1
  108. package/dist/{openapi-Dxc8RLy_.mjs → openapi-C3LFRzPq.mjs} +6 -6
  109. package/dist/outbox-admin/index.d.mts +2 -2
  110. package/dist/outbox-admin/index.mjs +4 -4
  111. package/dist/{outbox-module-CQXu_O6o.d.mts → outbox-module-BHdUnuuK.d.mts} +15 -2
  112. package/dist/{outbox-module-DyIqA5vo.mjs → outbox-module-dsCyLxXH.mjs} +43 -21
  113. package/dist/permissions/index.d.mts +2 -2
  114. package/dist/permissions/index.mjs +5 -5
  115. package/dist/{permissions-H7Zcb82Z.mjs → permissions-DiHXy8Ck.mjs} +12 -6
  116. package/dist/{pipe-SunhEaZ6.mjs → pipe-CBVmUJ-j.mjs} +2 -2
  117. package/dist/pipeline/index.d.mts +1 -1
  118. package/dist/pipeline/index.mjs +2 -2
  119. package/dist/plugins/index.d.mts +7 -37
  120. package/dist/plugins/index.mjs +34 -22
  121. package/dist/plugins/response-cache.mjs +9 -2
  122. package/dist/plugins/tracing-entry.mjs +2 -2
  123. package/dist/{pluralize-Da7IT_qJ.mjs → pluralize-2_U6QZQg.mjs} +1 -1
  124. package/dist/{preloadResources-DlhH81py.mjs → preloadResources-Bbp4QJ8V.mjs} +82 -26
  125. package/dist/{preloadResources-BbBkNkqK.d.mts → preloadResources-CjS2OfpU.d.mts} +14 -9
  126. package/dist/presets/filesUpload.d.mts +2 -2
  127. package/dist/presets/filesUpload.mjs +5 -5
  128. package/dist/presets/index.d.mts +2 -2
  129. package/dist/presets/index.mjs +2 -2
  130. package/dist/presets/multiTenant.d.mts +38 -1
  131. package/dist/presets/multiTenant.mjs +24 -6
  132. package/dist/presets/search.d.mts +2 -2
  133. package/dist/presets/search.mjs +2 -2
  134. package/dist/{presets-CNyfaxGL.mjs → presets-BdlugYTc.mjs} +4 -4
  135. package/dist/{projection-DSfEoooA.mjs → projection-Uvgp-EEj.mjs} +2 -2
  136. package/dist/{queryCachePlugin-oTsmVyEQ.mjs → queryCachePlugin-B5F09bYr.mjs} +11 -5
  137. package/dist/rateLimitKey-CK-EU2L8.mjs +41 -0
  138. package/dist/{realtime-CPmjjgqd.mjs → realtime-FnnD0n3Z.mjs} +9 -9
  139. package/dist/{redis-CduhKsJi.d.mts → redis-Bzi1y3F9.d.mts} +10 -2
  140. package/dist/{redis-stream-eh0eOtjh.d.mts → redis-stream-BPKscsTB.d.mts} +12 -7
  141. package/dist/{registerRoute-XvcI_3sh.mjs → registerRoute-DRsSHPSM.mjs} +6 -6
  142. package/dist/registry/index.d.mts +2 -2
  143. package/dist/registry/index.mjs +3 -3
  144. package/dist/{registry-UFEJQN8R.mjs → registry-D3y8VEJ1.mjs} +2 -2
  145. package/dist/{renewingLease-DuRGVOMe.mjs → renewingLease-B3VYh5aU.mjs} +1 -1
  146. package/dist/{replyHelpers-BHebarf0.mjs → replyHelpers-DPHsdw-5.mjs} +2 -2
  147. package/dist/{requestContext-DpmntRZy.mjs → requestContext-cSVKnYPk.mjs} +1 -1
  148. package/dist/{rolldown-runtime-Add2L50I.mjs → rolldown-runtime-DjDuyglE.mjs} +1 -1
  149. package/dist/{runtimeCapabilities-D793Pxci.mjs → runtimeCapabilities-DSA_dH1_.mjs} +31 -5
  150. package/dist/{schedules-DthM8Qag.mjs → schedules-BktJb3Y3.mjs} +8 -4
  151. package/dist/{schemaConverter-Db-MMsH6.mjs → schemaConverter-BLCSmNbr.mjs} +1 -1
  152. package/dist/{schemaIR-CIXShch7.mjs → schemaIR-K16Bb5BX.mjs} +2 -2
  153. package/dist/schemas/index.mjs +1 -1
  154. package/dist/scim/index.mjs +2 -2
  155. package/dist/scope/index.mjs +5 -5
  156. package/dist/{scope-D03sk17n.mjs → scope-jSxuBTFM.mjs} +4 -4
  157. package/dist/{simpleEqualityMatcher-B7qVjQBv.mjs → simpleEqualityMatcher-BiOnyflP.mjs} +1 -1
  158. package/dist/{sse-D4MYqkic.mjs → sse-CH7Slxc1.mjs} +5 -5
  159. package/dist/{sseStream-BuMNPR6r.mjs → sseStream-CN-Lh1Tu.mjs} +2 -2
  160. package/dist/{store-helpers-CIF4RG8s.mjs → store-helpers-9DX9-1j_.mjs} +1 -1
  161. package/dist/{streaming-DkpW1HwJ.mjs → streaming-dvppnxsr.mjs} +1 -1
  162. package/dist/{subscribe-helpers-CBZOi6pk.mjs → subscribe-helpers-AVYPgqTl.mjs} +2 -2
  163. package/dist/sync/changelog.mjs +1 -1
  164. package/dist/testing/cleanupStoreContract.mjs +1 -1
  165. package/dist/testing/index.d.mts +5 -5
  166. package/dist/testing/index.mjs +10 -10
  167. package/dist/testing/outboxStoreContract.mjs +14 -3
  168. package/dist/testing/storageContract.mjs +1 -1
  169. package/dist/{transactionContext-DMmvcfdo.mjs → transactionContext-DCWMI6hV.mjs} +1 -1
  170. package/dist/{typeGuards-DHTnEs5X.mjs → typeGuards-D7tHFdH6.mjs} +1 -1
  171. package/dist/types/index.d.mts +2 -2
  172. package/dist/types/index.mjs +1 -1
  173. package/dist/types/storage.mjs +1 -1
  174. package/dist/{types-BqLyyH0o.mjs → types-CDy3-zw1.mjs} +4 -4
  175. package/dist/{types-LQFwC37d.d.mts → types-CWQn9-IC.d.mts} +46 -1
  176. package/dist/{types-BRU-Kd3t.d.mts → types-DfVDvkdP.d.mts} +20 -2
  177. package/dist/usage/index.d.mts +58 -2
  178. package/dist/usage/index.mjs +0 -0
  179. package/dist/{userHelpers-CXySQFRg.mjs → userHelpers-DDMOZ_Pm.mjs} +1 -1
  180. package/dist/utils/index.d.mts +110 -8
  181. package/dist/utils/index.mjs +12 -12
  182. package/dist/{utils-BbGG8XNT.mjs → utils-B-zj_b__.mjs} +61 -12
  183. package/dist/{validate-CidSeUrx.mjs → validate-R0G_JrnK.mjs} +2 -2
  184. package/dist/{versioning-B843jOYX.d.mts → versioning-D6JIK8Tk.d.mts} +58 -2
  185. package/dist/{versioning-C2r0eOZ4.mjs → versioning-DuVWcu-F.mjs} +3 -3
  186. package/dist/workScope-BhsWde4k.mjs +41 -0
  187. package/package.json +574 -565
  188. package/skills/arc/SKILL.md +8 -4
  189. package/skills/arc/references/mcp.md +20 -0
  190. package/skills/arc/references/production.md +94 -9
  191. package/skills/arc/references/testing.md +0 -12
  192. package/skills/arc-code-review/references/anti-patterns.md +1 -1
  193. package/skills/arc-code-review/references/migration-recipes.md +0 -60
  194. package/skills/arc-code-review/references/mongokit-migration.md +0 -27
  195. package/skills/arc-module-publishing/references/package-manifest.md +3 -1
  196. package/dist/rateLimitKey-CewdR-m6.mjs +0 -15
@@ -1,5 +1,5 @@
1
1
 
2
- import { V as ResourceDefinition } from "./index-B78WZom8.mjs";
2
+ import { V as ResourceDefinition } from "./index-DHbyPo5j.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/integrations/mcp/types.d.ts
5
5
  /** Behavioral hints for MCP clients (tool annotations per MCP spec) */
@@ -275,6 +275,51 @@ interface McpPluginOptions {
275
275
  * - `McpAuthResolver` — custom function that resolves identity from headers
276
276
  */
277
277
  auth?: BetterAuthHandler | McpAuthResolver | false;
278
+ /**
279
+ * DNS-rebinding protection for the MCP routes — `Host` / `Origin` validation.
280
+ *
281
+ * The attack this closes: a page the developer visits can POST to
282
+ * `127.0.0.1:<port>`. If a DNS name the attacker controls resolves to
283
+ * loopback, the browser treats the response as same-origin and the page can
284
+ * READ it — every registered tool, from any website. Validating that `Host`
285
+ * names a host you expect is what breaks it, which is why the MCP SDK ships
286
+ * this as a default for localhost servers.
287
+ *
288
+ * **Default: on when `auth: false`, off otherwise.** Not a guess — it follows
289
+ * the threat:
290
+ * - `auth: false` is the local/dev shape the attack targets, and nothing else
291
+ * is standing in the way. Defaults to the SDK's localhost set
292
+ * (`localhost`, `127.0.0.1`, `[::1]`).
293
+ * - With auth configured, the endpoint is normally a real domain whose `Host`
294
+ * is not localhost — defaulting protection ON would 403 every production
295
+ * deployment. There, credentials are the gate.
296
+ *
297
+ * Set explicitly whenever the default's assumption is wrong for you: a
298
+ * localhost server that DOES use auth, or a deliberately-public `auth: false`
299
+ * endpoint that must keep answering (`false` to disable).
300
+ *
301
+ * Validation itself is `@modelcontextprotocol/server`'s `validateHostHeader` /
302
+ * `validateOriginHeader` — arc does not reimplement the matching rules.
303
+ * Hostnames only, no scheme or port; IPv6 in brackets (`[::1]`). A request
304
+ * with no `Origin` header passes (non-browser MCP clients send none); a
305
+ * present-but-unlisted `Origin` is rejected.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * // localhost server that uses auth — opt in explicitly
310
+ * mcpPlugin({ resources, auth, dnsRebindingProtection: {} });
311
+ * // reachable under a known public name
312
+ * mcpPlugin({ resources, dnsRebindingProtection: { allowedHosts: ['mcp.example.com'] } });
313
+ * // deliberately public, no Host constraint
314
+ * mcpPlugin({ resources, auth: false, dnsRebindingProtection: false });
315
+ * ```
316
+ */
317
+ dnsRebindingProtection?: false | {
318
+ /** Allowed `Host` hostnames. Default: `localhost`, `127.0.0.1`, `[::1]`. */
319
+ allowedHosts?: string[];
320
+ /** Allowed `Origin` hostnames. Default: same localhost set. */
321
+ allowedOrigins?: string[];
322
+ };
278
323
  /** MCP endpoint path (default: '/mcp') */
279
324
  prefix?: string;
280
325
  /** Server identity */
@@ -1,6 +1,6 @@
1
1
 
2
- import { n as DomainEvent, r as EventHandler } from "./EventTransport-Cx60_s-6.mjs";
3
- import { n as ErrorMapper } from "./errorHandler-DI5AN2b5.mjs";
2
+ import { n as DomainEvent, r as EventHandler } from "./EventTransport-CbUKDzZZ.mjs";
3
+ import { n as ErrorMapper } from "./errorHandler--4rSMMvq.mjs";
4
4
  import { FastifyInstance, FastifyPluginAsync } from "fastify";
5
5
  import { LockAdapter } from "@classytic/repo-core/lock";
6
6
  //#region src/plugins/health.d.ts
@@ -90,6 +90,24 @@ interface SchedulesPluginOptions {
90
90
  * environments), same pattern as `idempotencyPlugin`.
91
91
  */
92
92
  enabled?: boolean;
93
+ /**
94
+ * States that exactly ONE replica of this deployment arms schedules, so no
95
+ * lock is needed. Same idiom as `eventPlugin, { singleProcess: true }`.
96
+ *
97
+ * Under `runtime: 'distributed'`, schedules with neither `lock` nor this
98
+ * flag FAIL the boot. Without one of them arc cannot tell a deliberately
99
+ * single-armed deployment — the `role: 'scheduler'` process pinned to one
100
+ * replica, which is the recommended topology — from the accidental default,
101
+ * where every replica in a fleet fires every tick. The accidental case is
102
+ * duplicate side effects: two invoices, two dunning emails, two charges.
103
+ * That is a worse failure than the replica-local stores arc already refuses
104
+ * to boot on, so it is held to the same standard rather than warned about.
105
+ *
106
+ * Set it only when the replica count for the process that arms schedules is
107
+ * genuinely one. It is a claim about your deployment, not a silencer — if
108
+ * you scale that deployment past one replica, remove it and pass a `lock`.
109
+ */
110
+ singleReplica?: boolean;
93
111
  /**
94
112
  * How long `onClose` waits for in-flight runs before abandoning them.
95
113
  * Default: 5000.
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { i as RequestScope } from "../types-Ddl9OjmE.mjs";
3
- import { l as PermissionCheck, u as PermissionContext } from "../base-C-NL2fTi.mjs";
3
+ import { l as PermissionCheck, u as PermissionContext } from "../base-CxrV1LmG.mjs";
4
4
  import { UsageBucket, UsageStore, usagePeriod } from "@classytic/repo-core/usage";
5
5
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
6
6
  //#region src/usage/requireQuota.d.ts
@@ -40,6 +40,62 @@ declare class MemoryUsageStore implements UsageStore {
40
40
  clear(): void;
41
41
  }
42
42
  //#endregion
43
+ //#region src/usage/stores/redis.d.ts
44
+ /**
45
+ * The three commands this store needs. Fixed arity with an `unknown[]` tail
46
+ * where ioredis overloads one — see the note in `idempotency/stores/redis.ts`
47
+ * for why a narrower tail makes a real `Redis` unassignable.
48
+ */
49
+ interface UsageRedisLike {
50
+ hincrby(key: string, field: string, increment: number): Promise<number>;
51
+ hgetall(key: string): Promise<Record<string, string>>;
52
+ expire(key: string, seconds: number, ...args: unknown[]): Promise<number>;
53
+ /**
54
+ * Optional. When present, a retention-enabled increment sends `HINCRBY` and
55
+ * `EXPIRE` as ONE round trip instead of two. ioredis has it; a minimal
56
+ * wrapper may not, and the store falls back to two sequential calls.
57
+ */
58
+ pipeline?(): {
59
+ hincrby(key: string, field: string, increment: number): unknown;
60
+ expire(key: string, seconds: number): unknown;
61
+ exec(): Promise<unknown>;
62
+ };
63
+ }
64
+ interface RedisUsageStoreOptions {
65
+ /** Redis client (ioredis or compatible). */
66
+ redis: UsageRedisLike;
67
+ /**
68
+ * Key prefix (default: `'arc:usage:'`).
69
+ *
70
+ * Keys are `{prefix}{actor}:{period}`. `usagePeriod()` never emits a `:`,
71
+ * so the split is unambiguous for every canonical period; a host that
72
+ * passes its own period key should keep `:` out of it.
73
+ */
74
+ prefix?: string;
75
+ /**
76
+ * Seconds a bucket survives after its LAST write, or `undefined` (the
77
+ * default) to keep counters forever.
78
+ *
79
+ * The default is deliberate and it is not the usual Redis default. These
80
+ * counters are what quota decisions and invoices are computed from —
81
+ * expiring them by default would delete billing history on a schedule
82
+ * nobody chose, and the loss is silent because `summary()` answers `{}` for
83
+ * a bucket that never existed and one that expired alike. Set it once you
84
+ * know your retention window (e.g. `86400 * 400` to keep a year plus a
85
+ * grace period), and only if the counters here are not your system of
86
+ * record.
87
+ */
88
+ retentionSeconds?: number;
89
+ }
90
+ /** Period-bucketed usage counters in Redis hashes. See the module header. */
91
+ declare class RedisUsageStore implements UsageStore {
92
+ #private;
93
+ readonly name = "redis";
94
+ constructor(options: RedisUsageStoreOptions);
95
+ increment(bucket: UsageBucket, amount: number): Promise<void>;
96
+ summary(actor: string, period: string): Promise<Record<string, number>>;
97
+ }
98
+ //#endregion
43
99
  //#region src/usage/usagePlugin.d.ts
44
100
  interface UsageTrackOptions {
45
101
  /** Count every completed request as `api.requests` (default: true). */
@@ -91,4 +147,4 @@ declare module "fastify" {
91
147
  declare const usagePluginFn: FastifyPluginAsync<UsagePluginOptions>;
92
148
  declare const _default: FastifyPluginAsync<UsagePluginOptions>;
93
149
  //#endregion
94
- export { MemoryUsageStore, type QuotaOptions, type UsageBucket, type UsageMeter, type UsagePluginOptions, type UsageStore, type UsageTrackOptions, _default as default, requireQuota, usagePeriod, usagePluginFn as usagePlugin };
150
+ export { MemoryUsageStore, type QuotaOptions, RedisUsageStore, type RedisUsageStoreOptions, type UsageBucket, type UsageMeter, type UsagePluginOptions, type UsageRedisLike, type UsageStore, type UsageTrackOptions, _default as default, requireQuota, usagePeriod, usagePluginFn as usagePlugin };
Binary file
@@ -1,4 +1,4 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
2
  //#region src/utils/userHelpers.ts
3
3
  /**
4
4
  * Extract a user ID from a user object. Accepts `id` or `_id` — returns
@@ -1,8 +1,8 @@
1
1
 
2
2
  import { F as getHeader, I as requireSingleHeaderValue, P as ArcHeaders } from "../types-Ddl9OjmE.mjs";
3
- import { f as getUserId, m as normalizeRoles, p as getUserRoles, t as AnyRecord } from "../base-C-NL2fTi.mjs";
4
- import { $ as OpenApiSchemas, S as QueryParserInterface, b as ParsedQuery } from "../index-B78WZom8.mjs";
5
- import { n as ErrorMapper } from "../errorHandler-DI5AN2b5.mjs";
3
+ import { f as getUserId, m as normalizeRoles, p as getUserRoles, t as AnyRecord } from "../base-CxrV1LmG.mjs";
4
+ import { $ as OpenApiSchemas, S as QueryParserInterface, b as ParsedQuery } from "../index-DHbyPo5j.mjs";
5
+ import { n as ErrorMapper } from "../errorHandler--4rSMMvq.mjs";
6
6
  import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createDomainError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorOptions, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-BGdVwP25.mjs";
7
7
  import { scheduleBackground } from "@classytic/repo-core/cache";
8
8
  import { errorContractSchema, errorDetailSchema } from "@classytic/repo-core/errors";
@@ -54,6 +54,53 @@ type CircuitState = (typeof CircuitState)[keyof typeof CircuitState];
54
54
  * Promise<any>` would silently disable type checking everywhere `T` flows.
55
55
  */
56
56
  type AnyAsyncFn = (...args: never[]) => Promise<unknown>;
57
+ /**
58
+ * Cluster-wide failure counting for a circuit.
59
+ *
60
+ * ## What this shares, and what it deliberately does not
61
+ *
62
+ * It shares the failure SIGNAL, not the state machine. Each replica still owns
63
+ * its own CLOSED/OPEN/HALF_OPEN transitions and its own probe scheduling; what
64
+ * becomes cluster-wide is the count that decides when to trip.
65
+ *
66
+ * That is the whole of the problem worth solving. Per-replica counting means a
67
+ * downstream outage costs `failureThreshold × replicas` real requests before
68
+ * anything opens — at 5 and 50 that is 250 errors a failing payment gateway
69
+ * absorbs while every pod independently rediscovers the same outage. Sharing
70
+ * the count collapses it: the first replica trips at the threshold, and every
71
+ * other replica trips on its NEXT failure, because the shared window is
72
+ * already over the line.
73
+ *
74
+ * Sharing the full state machine instead would mean electing which replica
75
+ * probes in HALF_OPEN — consensus, plus a lock, plus a read on the hot path.
76
+ * The remaining gap without it is bounded and small: one extra failure per
77
+ * replica, once, versus `failureThreshold` per replica every time.
78
+ *
79
+ * ## Cost
80
+ *
81
+ * ONLY the failure path calls this. A successful call never touches the
82
+ * network, which matters because success is the common case and this wraps
83
+ * things like a payment charge. `reset` is called when a circuit closes.
84
+ *
85
+ * ## Failure of the store itself
86
+ *
87
+ * Never propagates. If the shared counter is unreachable the breaker falls
88
+ * back to its local count — degraded to the old behaviour, which is the
89
+ * correct direction: a Redis blip must not disable the protection that exists
90
+ * for a downstream outage, and the two often arrive together.
91
+ */
92
+ interface CircuitBreakerSharedState {
93
+ /** Store name for diagnostics. */
94
+ readonly name: string;
95
+ /**
96
+ * Record one failure for `circuit` and return the CLUSTER-wide count within
97
+ * the current window. Implementations expire the window so a long-ago
98
+ * outage does not trip a healthy circuit.
99
+ */
100
+ recordFailure(circuit: string): Promise<number>;
101
+ /** Clear the window — called when this replica closes the circuit. */
102
+ reset(circuit: string): Promise<void>;
103
+ }
57
104
  interface CircuitBreakerOptions {
58
105
  /**
59
106
  * Number of failures before opening circuit
@@ -117,6 +164,19 @@ interface CircuitBreakerOptions {
117
164
  * Name for logging/monitoring
118
165
  */
119
166
  name?: string;
167
+ /**
168
+ * Cluster-wide failure counting — see {@link CircuitBreakerSharedState}.
169
+ *
170
+ * Omit for a single process, where the local count already IS the cluster
171
+ * count. Under a multi-replica deployment this is what stops every replica
172
+ * absorbing the full threshold before anything trips.
173
+ *
174
+ * Requires a stable {@link CircuitBreakerOptions.name}: it is the key every
175
+ * replica must agree on. A breaker with `sharedState` and no `name` throws
176
+ * at construction rather than silently counting under a generated id that no
177
+ * other replica shares.
178
+ */
179
+ sharedState?: CircuitBreakerSharedState;
120
180
  }
121
181
  interface CircuitBreakerStats {
122
182
  name?: string;
@@ -150,6 +210,7 @@ declare class CircuitBreaker<T extends AnyAsyncFn> {
150
210
  private readonly onStateChange?;
151
211
  private readonly onError?;
152
212
  private readonly name;
213
+ private readonly sharedState?;
153
214
  private readonly fn;
154
215
  constructor(fn: T, options?: CircuitBreakerOptions);
155
216
  /**
@@ -171,6 +232,10 @@ declare class CircuitBreaker<T extends AnyAsyncFn> {
171
232
  * Handle failed call
172
233
  */
173
234
  private onFailure;
235
+ /** Open the circuit and schedule the next probe. */
236
+ private trip;
237
+ /** Best-effort clear of the cluster failure window. Never throws. */
238
+ private clearSharedWindow;
174
239
  /**
175
240
  * Change circuit state
176
241
  */
@@ -254,6 +319,42 @@ declare class CircuitBreakerRegistry {
254
319
  */
255
320
  declare function createCircuitBreakerRegistry(): CircuitBreakerRegistry;
256
321
  //#endregion
322
+ //#region src/utils/circuitBreakerRedis.d.ts
323
+ /**
324
+ * The two commands this needs. Variadic tail is `unknown[]` for the same
325
+ * reason as every other `*Like` shape in arc — ioredis types its commands as
326
+ * overloads mixing literal tokens and an optional callback, which a narrower
327
+ * tail matches none of. Pinned by `tests/types/ioredis-assignability.test.ts`.
328
+ */
329
+ interface CircuitBreakerRedisLike {
330
+ incr(key: string): Promise<number>;
331
+ pexpire(key: string, milliseconds: number, ...args: unknown[]): Promise<number>;
332
+ del(...keys: string[]): Promise<number>;
333
+ }
334
+ interface RedisCircuitBreakerStateOptions {
335
+ /** Redis client (ioredis or compatible). */
336
+ redis: CircuitBreakerRedisLike;
337
+ /**
338
+ * Key prefix (default: `'arc:circuit:'`). Keys are `{prefix}{circuitName}`,
339
+ * so the circuit's `name` is what every replica must agree on.
340
+ */
341
+ prefix?: string;
342
+ /**
343
+ * Counting window in ms (default: `60_000`, matching the breaker's default
344
+ * `resetTimeout`). The period over which `failureThreshold` failures mean
345
+ * the downstream is down.
346
+ */
347
+ windowMs?: number;
348
+ }
349
+ /** Fixed-window cluster failure counter. See the module header. */
350
+ declare class RedisCircuitBreakerState implements CircuitBreakerSharedState {
351
+ #private;
352
+ readonly name = "redis";
353
+ constructor(options: RedisCircuitBreakerStateOptions);
354
+ recordFailure(circuit: string): Promise<number>;
355
+ reset(circuit: string): Promise<void>;
356
+ }
357
+ //#endregion
257
358
  //#region src/utils/compensation.d.ts
258
359
  /**
259
360
  * Compensating Transaction — In-Process Rollback Primitive
@@ -691,8 +792,8 @@ declare const paginationSchema: JsonSchema;
691
792
  * validation PER RESPONSE to pick the branch, on the hottest route of
692
793
  * every arc app. The merge keeps the exact bytes (pinned by
693
794
  * `tests/utils/list-response-serialization.test.ts`) and drops the
694
- * per-response branch validation. Only `data` is required — the one field
695
- * all four variants share.
795
+ * per-response branch validation. The list route also serves count, exists
796
+ * and distinct dispatch verbs, so no field is common to every valid shape.
696
797
  */
697
798
  declare function listResponse(itemSchema: JsonSchema): JsonSchema;
698
799
  /** Offset variant — `{ method: 'offset', data, page, limit, total, pages, hasNext, hasPrev }`. */
@@ -1152,8 +1253,9 @@ interface RuntimeCapabilityDeclaration {
1152
1253
  }
1153
1254
  /**
1154
1255
  * Declare a runtime capability. Callable from ANY registration context —
1155
- * arc's own plugins, host `plugins()` callbacks, module `plugins` phases —
1156
- * which is the point: the audit sees what the constructor-time guard cannot.
1256
+ * arc's own plugins, host `plugins()` callbacks, module `plugins` phases,
1257
+ * and encapsulated children of any of them — which is the point: the audit
1258
+ * sees what the constructor-time guard cannot.
1157
1259
  */
1158
1260
  declare function declareRuntimeCapability(fastify: FastifyInstance, decl: RuntimeCapabilityDeclaration): void;
1159
1261
  //#endregion
@@ -1540,4 +1642,4 @@ declare function hasEvents(instance: FastifyInstance): instance is FastifyInstan
1540
1642
  events: EventsDecorator;
1541
1643
  };
1542
1644
  //#endregion
1543
- export { ArcError, type ArcHeaders, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, CircuitBreakerRegistry, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, ConflictError, type ErrorOptions, type EventsDecorator, type FilterDialect, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, type JsonSchemaTarget, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, type PipeUIMessageStreamOptions, RateLimitError, type RuntimeCapabilityDeclaration, type SchemaIo, ServiceUnavailableError, type SseStream, type SseStreamOptions, type StateMachine, type TransitionConfig, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, declareRuntimeCapability, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, forwardedStreamHeaders, getDefaultCrudSchemas, getHeader, getListQueryParams, getUserId, getUserRoles, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, normalizeRoles, offsetListResponse, openSseStream, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, requireSingleHeaderValue, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, withCompensation };
1645
+ export { ArcError, type ArcHeaders, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, type CircuitBreakerRedisLike, CircuitBreakerRegistry, type CircuitBreakerSharedState, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, ConflictError, type ErrorOptions, type EventsDecorator, type FilterDialect, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, type JsonSchemaTarget, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, type PipeUIMessageStreamOptions, RateLimitError, RedisCircuitBreakerState, type RedisCircuitBreakerStateOptions, type RuntimeCapabilityDeclaration, type SchemaIo, ServiceUnavailableError, type SseStream, type SseStreamOptions, type StateMachine, type TransitionConfig, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, declareRuntimeCapability, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, forwardedStreamHeaders, getDefaultCrudSchemas, getHeader, getListQueryParams, getUserId, getUserRoles, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, normalizeRoles, offsetListResponse, openSseStream, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, requireSingleHeaderValue, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, withCompensation };
@@ -1,12 +1,12 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-2ExmOgIJ.mjs";
3
- import { n as requireSingleHeaderValue, t as getHeader } from "../headers-DeXNifVj.mjs";
4
- import { n as getUserRoles, r as normalizeRoles, t as getUserId } from "../userHelpers-CXySQFRg.mjs";
5
- import { t as simpleEqualityMatcher } from "../simpleEqualityMatcher-B7qVjQBv.mjs";
6
- import { A as scheduleBackground, C as paginationSchema, D as createQueryParser, E as ArcQueryParser, O as MONGO_DIALECT, S as offsetListResponse, T as responses, _ as errorDetailSchema, a as defineCompensation, b as keysetListResponse, c as CircuitBreakerError, d as createCircuitBreaker, f as createCircuitBreakerRegistry, g as errorContractSchema, h as deleteResponse, i as defineErrorMapper, k as NEUTRAL_DIALECT, l as CircuitBreakerRegistry, m as bareListResponse, n as handleRaw, o as withCompensation, p as aggregateListResponse, r as defineGuard, s as CircuitBreaker, t as createStateMachine, u as CircuitState, v as getDefaultCrudSchemas, w as queryParams, x as listResponse, y as getListQueryParams } from "../utils-BbGG8XNT.mjs";
7
- import { a as isZodSchema, i as isJsonSchema, n as convertRouteSchema, o as toJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-Db-MMsH6.mjs";
8
- import { a as promoteStreamTokenToHeader, i as pipeUIMessageStreamToReply, n as forwardedStreamHeaders, r as isReadableStream, t as UI_MESSAGE_STREAM_HEADERS } from "../streaming-DkpW1HwJ.mjs";
9
- import { n as declareRuntimeCapability } from "../runtimeCapabilities-D793Pxci.mjs";
10
- import { t as openSseStream } from "../sseStream-BuMNPR6r.mjs";
11
- import { t as hasEvents } from "../typeGuards-DHTnEs5X.mjs";
12
- export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, declareRuntimeCapability, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, forwardedStreamHeaders, getDefaultCrudSchemas, getHeader, getListQueryParams, getUserId, getUserRoles, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, normalizeRoles, offsetListResponse, openSseStream, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, requireSingleHeaderValue, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, withCompensation };
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-C_YhELhe.mjs";
3
+ import { n as requireSingleHeaderValue, t as getHeader } from "../headers-CNECDE6q.mjs";
4
+ import { n as getUserRoles, r as normalizeRoles, t as getUserId } from "../userHelpers-DDMOZ_Pm.mjs";
5
+ import { t as simpleEqualityMatcher } from "../simpleEqualityMatcher-BiOnyflP.mjs";
6
+ import { A as NEUTRAL_DIALECT, C as offsetListResponse, D as ArcQueryParser, E as responses, O as createQueryParser, S as listResponse, T as queryParams, _ as errorContractSchema, a as defineCompensation, b as getListQueryParams, c as CircuitBreaker, d as CircuitState, f as createCircuitBreaker, g as deleteResponse, h as bareListResponse, i as defineErrorMapper, j as scheduleBackground, k as MONGO_DIALECT, l as CircuitBreakerError, m as aggregateListResponse, n as handleRaw, o as withCompensation, p as createCircuitBreakerRegistry, r as defineGuard, s as RedisCircuitBreakerState, t as createStateMachine, u as CircuitBreakerRegistry, v as errorDetailSchema, w as paginationSchema, x as keysetListResponse, y as getDefaultCrudSchemas } from "../utils-B-zj_b__.mjs";
7
+ import { a as isZodSchema, i as isJsonSchema, n as convertRouteSchema, o as toJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-BLCSmNbr.mjs";
8
+ import { a as promoteStreamTokenToHeader, i as pipeUIMessageStreamToReply, n as forwardedStreamHeaders, r as isReadableStream, t as UI_MESSAGE_STREAM_HEADERS } from "../streaming-dvppnxsr.mjs";
9
+ import { n as declareRuntimeCapability } from "../runtimeCapabilities-DSA_dH1_.mjs";
10
+ import { t as openSseStream } from "../sseStream-CN-Lh1Tu.mjs";
11
+ import { t as hasEvents } from "../typeGuards-D7tHFdH6.mjs";
12
+ export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, RedisCircuitBreakerState, ServiceUnavailableError, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, declareRuntimeCapability, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, forwardedStreamHeaders, getDefaultCrudSchemas, getHeader, getListQueryParams, getUserId, getUserRoles, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, normalizeRoles, offsetListResponse, openSseStream, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, requireSingleHeaderValue, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, withCompensation };
@@ -1,7 +1,7 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { m as RESERVED_QUERY_PARAMS } from "./constants-BONghE1Y.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { m as RESERVED_QUERY_PARAMS } from "./constants-BxLgNFF7.mjs";
3
3
  import { arcLog } from "./logger/index.mjs";
4
- import { t as ArcError, u as ValidationError } from "./errors-2ExmOgIJ.mjs";
4
+ import { t as ArcError, u as ValidationError } from "./errors-C_YhELhe.mjs";
5
5
  import { scheduleBackground } from "@classytic/repo-core/cache";
6
6
  import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
7
7
  //#region src/utils/filter-dialect.ts
@@ -561,8 +561,8 @@ const paginationSchema = {
561
561
  * validation PER RESPONSE to pick the branch, on the hottest route of
562
562
  * every arc app. The merge keeps the exact bytes (pinned by
563
563
  * `tests/utils/list-response-serialization.test.ts`) and drops the
564
- * per-response branch validation. Only `data` is required — the one field
565
- * all four variants share.
564
+ * per-response branch validation. The list route also serves count, exists
565
+ * and distinct dispatch verbs, so no field is common to every valid shape.
566
566
  */
567
567
  function listResponse(itemSchema) {
568
568
  return {
@@ -577,6 +577,15 @@ function listResponse(itemSchema) {
577
577
  type: "array",
578
578
  items: itemSchema
579
579
  },
580
+ count: {
581
+ type: "integer",
582
+ minimum: 0
583
+ },
584
+ exists: { type: "boolean" },
585
+ values: {
586
+ type: "array",
587
+ items: {}
588
+ },
580
589
  page: {
581
590
  type: "integer",
582
591
  example: 1
@@ -607,7 +616,6 @@ function listResponse(itemSchema) {
607
616
  description: "Cursor token for the next page, or null."
608
617
  }
609
618
  },
610
- required: ["data"],
611
619
  additionalProperties: true
612
620
  };
613
621
  }
@@ -963,9 +971,12 @@ var CircuitBreaker = class {
963
971
  onStateChange;
964
972
  onError;
965
973
  name;
974
+ sharedState;
966
975
  fn;
967
976
  constructor(fn, options = {}) {
968
977
  this.fn = fn;
978
+ if (options.sharedState && !options.name) throw new Error("[arc] CircuitBreaker: `sharedState` requires an explicit `name` — it is the key every replica counts under. Without it each replica would count under its own key.");
979
+ this.sharedState = options.sharedState;
969
980
  this.failureThreshold = options.failureThreshold ?? 5;
970
981
  this.resetTimeout = options.resetTimeout ?? 6e4;
971
982
  this.timeout = options.timeout ?? 1e4;
@@ -1043,6 +1054,7 @@ var CircuitBreaker = class {
1043
1054
  if (this.successes >= this.successThreshold) {
1044
1055
  this.setState(CircuitState.CLOSED);
1045
1056
  this.successes = 0;
1057
+ this.clearSharedWindow();
1046
1058
  }
1047
1059
  }
1048
1060
  }
@@ -1053,11 +1065,23 @@ var CircuitBreaker = class {
1053
1065
  this.failures++;
1054
1066
  this.successes = 0;
1055
1067
  if (this.onError) this.onError(error);
1056
- if (this.state === CircuitState.HALF_OPEN || this.failures >= this.failureThreshold) {
1057
- this.setState(CircuitState.OPEN);
1058
- this.nextAttempt = Date.now() + this.resetTimeout;
1059
- this.openedAt = Date.now();
1060
- }
1068
+ const trippedLocally = this.state === CircuitState.HALF_OPEN || this.failures >= this.failureThreshold;
1069
+ if (trippedLocally) this.trip();
1070
+ if (!this.sharedState) return;
1071
+ this.sharedState.recordFailure(this.name).then((clusterFailures) => {
1072
+ if (!trippedLocally && this.state === CircuitState.CLOSED && clusterFailures >= this.failureThreshold) this.trip();
1073
+ }).catch(() => {});
1074
+ }
1075
+ /** Open the circuit and schedule the next probe. */
1076
+ trip() {
1077
+ this.setState(CircuitState.OPEN);
1078
+ this.nextAttempt = Date.now() + this.resetTimeout;
1079
+ this.openedAt = Date.now();
1080
+ }
1081
+ /** Best-effort clear of the cluster failure window. Never throws. */
1082
+ clearSharedWindow() {
1083
+ if (!this.sharedState) return;
1084
+ this.sharedState.reset(this.name).catch(() => {});
1061
1085
  }
1062
1086
  /**
1063
1087
  * Change circuit state
@@ -1205,6 +1229,31 @@ function createCircuitBreakerRegistry() {
1205
1229
  return new CircuitBreakerRegistry();
1206
1230
  }
1207
1231
  //#endregion
1232
+ //#region src/utils/circuitBreakerRedis.ts
1233
+ /** Fixed-window cluster failure counter. See the module header. */
1234
+ var RedisCircuitBreakerState = class {
1235
+ name = "redis";
1236
+ #redis;
1237
+ #prefix;
1238
+ #windowMs;
1239
+ constructor(options) {
1240
+ this.#redis = options.redis;
1241
+ this.#prefix = options.prefix ?? "arc:circuit:";
1242
+ const windowMs = options.windowMs ?? 6e4;
1243
+ if (!Number.isFinite(windowMs) || windowMs <= 0) throw new Error(`[arc] RedisCircuitBreakerState: windowMs must be a positive number, got ${String(options.windowMs)}.`);
1244
+ this.#windowMs = windowMs;
1245
+ }
1246
+ async recordFailure(circuit) {
1247
+ const key = this.#prefix + circuit;
1248
+ const count = await this.#redis.incr(key);
1249
+ if (count === 1) await this.#redis.pexpire(key, this.#windowMs);
1250
+ return count;
1251
+ }
1252
+ async reset(circuit) {
1253
+ await this.#redis.del(this.#prefix + circuit);
1254
+ }
1255
+ };
1256
+ //#endregion
1208
1257
  //#region src/utils/compensation.ts
1209
1258
  /**
1210
1259
  * Observability hooks must never alter the transaction: a throwing metrics
@@ -1472,4 +1521,4 @@ function createStateMachine(name, transitions = {}, options = {}) {
1472
1521
  };
1473
1522
  }
1474
1523
  //#endregion
1475
- export { scheduleBackground as A, paginationSchema as C, createQueryParser as D, ArcQueryParser as E, MONGO_DIALECT as O, offsetListResponse as S, responses as T, errorDetailSchema$1 as _, defineCompensation as a, keysetListResponse as b, CircuitBreakerError as c, createCircuitBreaker as d, createCircuitBreakerRegistry as f, errorContractSchema$1 as g, deleteResponse as h, defineErrorMapper as i, NEUTRAL_DIALECT as k, CircuitBreakerRegistry as l, bareListResponse as m, handleRaw as n, withCompensation as o, aggregateListResponse as p, defineGuard as r, CircuitBreaker as s, createStateMachine as t, CircuitState as u, getDefaultCrudSchemas as v, queryParams as w, listResponse as x, getListQueryParams as y };
1524
+ export { NEUTRAL_DIALECT as A, offsetListResponse as C, ArcQueryParser as D, responses as E, createQueryParser as O, listResponse as S, queryParams as T, errorContractSchema$1 as _, defineCompensation as a, getListQueryParams as b, CircuitBreaker as c, CircuitState as d, createCircuitBreaker as f, deleteResponse as g, bareListResponse as h, defineErrorMapper as i, scheduleBackground as j, MONGO_DIALECT as k, CircuitBreakerError as l, aggregateListResponse as m, handleRaw as n, withCompensation as o, createCircuitBreakerRegistry as p, defineGuard as r, RedisCircuitBreakerState as s, createStateMachine as t, CircuitBreakerRegistry as u, errorDetailSchema$1 as v, paginationSchema as w, keysetListResponse as x, getDefaultCrudSchemas as y };
@@ -1,5 +1,5 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-Add2L50I.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-DjDuyglE.mjs";
3
3
  //#region src/core/aggregation/normalize.ts
4
4
  /**
5
5
  * Framework cap on URL-driven aggregation limits when the host opts
@@ -1,6 +1,62 @@
1
1
 
2
- import { n as DomainEvent } from "./EventTransport-Cx60_s-6.mjs";
2
+ import { n as DomainEvent } from "./EventTransport-CbUKDzZZ.mjs";
3
3
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
4
+ //#region src/plugins/gracefulShutdown.d.ts
5
+ interface GracefulShutdownOptions {
6
+ /** Maximum time to wait for graceful shutdown in ms (default: 30000) */
7
+ timeout?: number;
8
+ /** Custom cleanup function called before exit */
9
+ onShutdown?: () => Promise<void> | void;
10
+ /** Signals to handle (default: ['SIGTERM', 'SIGINT']) */
11
+ signals?: NodeJS.Signals[];
12
+ /** Whether to log shutdown events (default: true) */
13
+ logEvents?: boolean;
14
+ /**
15
+ * Lame-duck window in ms between flipping readiness and closing the
16
+ * server (default: 0 — close immediately, the pre-2.41 behaviour).
17
+ *
18
+ * A load balancer keeps routing to an instance until its readiness
19
+ * probe has failed for a few intervals; closing the moment SIGTERM
20
+ * arrives is the classic rolling-deploy 502. Set this to exceed the LB's
21
+ * deregistration window (typically 5000–15000) so `/ready` reports
22
+ * `draining` while the instance still answers everything routed to it.
23
+ * Counts against `timeout`.
24
+ */
25
+ drainDelayMs?: number;
26
+ /**
27
+ * Called when shutdown times out or encounters an error.
28
+ * Defaults to `process.exit(1)` — appropriate for production but dangerous in:
29
+ * - **Tests**: kills the test runner. Pass `() => {}` or `() => { throw … }`.
30
+ * - **Shared runtimes** (e.g., serverless): may kill unrelated handlers.
31
+ *
32
+ * @param reason - `'timeout'` if shutdown exceeded `timeout` ms,
33
+ * `'error'` if `onShutdown` or `fastify.close()` threw.
34
+ */
35
+ onForceExit?: (reason: "timeout" | "error") => void;
36
+ }
37
+ /**
38
+ * Readable shutdown state — `fastify.shutdownState`. Set the moment a signal
39
+ * (or `fastify.shutdown()`) arrives, BEFORE the server closes, so readiness
40
+ * can start failing while the instance is still serving. `/live` ignores it:
41
+ * a draining process is alive, that is the whole point of the window.
42
+ */
43
+ interface ShutdownState {
44
+ /** True from the shutdown signal onward — never resets. */
45
+ readonly draining: boolean;
46
+ /** When draining began. */
47
+ readonly since?: Date;
48
+ }
49
+ declare const gracefulShutdownPlugin: FastifyPluginAsync<GracefulShutdownOptions>;
50
+ declare module "fastify" {
51
+ interface FastifyInstance {
52
+ /** Trigger graceful shutdown manually */
53
+ shutdown: () => Promise<void>;
54
+ /** Draining flag + start time; read by the health plugin's `/ready`. */
55
+ shutdownState: ShutdownState;
56
+ }
57
+ }
58
+ declare const _default$5: FastifyPluginAsync<GracefulShutdownOptions>;
59
+ //#endregion
4
60
  //#region src/plugins/caching.d.ts
5
61
  interface CachingRule {
6
62
  /** Path prefix to match (e.g., '/api/products') */
@@ -172,4 +228,4 @@ declare module "fastify" {
172
228
  declare const versioningPlugin: FastifyPluginAsync<VersioningOptions>;
173
229
  declare const _default: FastifyPluginAsync<VersioningOptions>;
174
230
  //#endregion
175
- export { CachingRule as _, MetricsCollector as a, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, cachingPlugin as y };
231
+ export { gracefulShutdownPlugin as C, _default$5 as S, CachingRule as _, MetricsCollector as a, GracefulShutdownOptions as b, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, ShutdownState as x, cachingPlugin as y };
@@ -1,6 +1,6 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-Add2L50I.mjs";
3
- import { d as createDomainError } from "./errors-2ExmOgIJ.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-DjDuyglE.mjs";
3
+ import { d as createDomainError } from "./errors-C_YhELhe.mjs";
4
4
  import fp from "fastify-plugin";
5
5
  //#region src/plugins/versioning.ts
6
6
  var versioning_exports = /* @__PURE__ */ __exportAll({