@classytic/arc 2.16.0 → 2.17.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 (140) hide show
  1. package/README.md +22 -2
  2. package/dist/{BaseController-DlCCTIxJ.mjs → BaseController-Dmeh4Y_y.mjs} +5 -47
  3. package/dist/{QueryCache-SvmT_9ti.d.mts → QueryCache-8YmAQ-zW.d.mts} +1 -1
  4. package/dist/{ResourceRegistry-f48hFk3m.mjs → ResourceRegistry-CzSzVK1m.mjs} +3 -0
  5. package/dist/audit/index.d.mts +1 -1
  6. package/dist/audit/index.mjs +2 -1
  7. package/dist/auth/index.d.mts +4 -4
  8. package/dist/auth/index.mjs +2 -2
  9. package/dist/auth/redis-session.d.mts +1 -1
  10. package/dist/auth/redis-session.mjs +28 -2
  11. package/dist/{buildHandler-BZX6zzDM.mjs → buildHandler-C_SkBNF3.mjs} +6 -4
  12. package/dist/cache/index.d.mts +3 -3
  13. package/dist/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/describe.mjs +1 -1
  15. package/dist/cli/commands/docs.mjs +2 -2
  16. package/dist/cli/commands/generate.d.mts +1 -1
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.d.mts +1 -1
  19. package/dist/cli/commands/init.mjs +1 -1
  20. package/dist/cli/commands/introspect.mjs +1 -1
  21. package/dist/cli/index.d.mts +2 -2
  22. package/dist/cli/index.mjs +2 -2
  23. package/dist/core/index.d.mts +2 -2
  24. package/dist/core/index.mjs +3 -3
  25. package/dist/{core-DBJ_j6rX.mjs → core-DYJvu7aI.mjs} +174 -31
  26. package/dist/{createActionRouter-DUpN3Dd1.mjs → createActionRouter-BHUMb1-y.mjs} +6 -3
  27. package/dist/{createAggregationRouter-Dq-TUCuY.mjs → createAggregationRouter-BYVvhfc5.mjs} +3 -3
  28. package/dist/{createApp-DNccuhyI.mjs → createApp-CzCEEsEm.mjs} +9 -9
  29. package/dist/{defineEvent-DRwY0fYm.mjs → defineEvent-NPKUJRt8.mjs} +1 -1
  30. package/dist/docs/index.d.mts +2 -2
  31. package/dist/docs/index.mjs +2 -2
  32. package/dist/{elevation-Dci0AYLT.mjs → elevation-6mfTLBC-.mjs} +1 -1
  33. package/dist/{errorHandler-DpoXQHZ9.mjs → errorHandler-wQejMBT3.mjs} +45 -4
  34. package/dist/{eventPlugin-C2cGqtRO.mjs → eventPlugin-CnJCUyM0.mjs} +1 -1
  35. package/dist/{eventPlugin-CtHC_av1.d.mts → eventPlugin-DjEx_J3j.d.mts} +1 -1
  36. package/dist/events/index.d.mts +3 -3
  37. package/dist/events/index.mjs +5 -5
  38. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  39. package/dist/events/transports/redis-stream-entry.mjs +34 -1
  40. package/dist/events/transports/redis.d.mts +1 -1
  41. package/dist/events/transports/redis.mjs +17 -1
  42. package/dist/factory/index.d.mts +2 -2
  43. package/dist/factory/index.mjs +4 -3
  44. package/dist/fastify-augmentation-7LIR0nDv.d.mts +42 -0
  45. package/dist/{fields-Anj0xdih.d.mts → fields-DqZLp5d6.d.mts} +1 -1
  46. package/dist/hooks/index.d.mts +1 -1
  47. package/dist/idempotency/index.d.mts +2 -2
  48. package/dist/idempotency/index.mjs +21 -5
  49. package/dist/idempotency/redis.d.mts +1 -1
  50. package/dist/{index-3oIimXQn.d.mts → index-3L-adFJO.d.mts} +3 -3
  51. package/dist/{index-CkW0flkU.d.mts → index-C1JSBan-.d.mts} +256 -8
  52. package/dist/{index-B-ulKx5P.d.mts → index-CruZir1o.d.mts} +36 -16
  53. package/dist/index.d.mts +5 -5
  54. package/dist/index.mjs +5 -5
  55. package/dist/integrations/event-gateway.d.mts +2 -2
  56. package/dist/integrations/event-gateway.mjs +1 -1
  57. package/dist/integrations/index.d.mts +2 -2
  58. package/dist/integrations/jobs.d.mts +36 -1
  59. package/dist/integrations/jobs.mjs +82 -9
  60. package/dist/integrations/mcp/index.d.mts +67 -5
  61. package/dist/integrations/mcp/index.mjs +93 -2
  62. package/dist/integrations/mcp/testing.d.mts +1 -1
  63. package/dist/integrations/mcp/testing.mjs +1 -1
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +2 -1
  67. package/dist/{loadResourcesFromEntry-BLMEI2Xa.mjs → loadResourcesFromEntry-DCjYMgfC.mjs} +1 -1
  68. package/dist/{mcpPlugin-7vGV51ED.mjs → mcpPlugin-hZE00H74.mjs} +114 -11
  69. package/dist/middleware/index.d.mts +1 -1
  70. package/dist/middleware/index.mjs +1 -1
  71. package/dist/{multipartBody-BOvVSVCD.mjs → multipartBody-VjVWmZl8.mjs} +2 -1
  72. package/dist/{openapi-34T9yNwd.mjs → openapi-DKDs9NfD.mjs} +37 -10
  73. package/dist/permissions/index.d.mts +2 -2
  74. package/dist/permissions/index.mjs +2 -2
  75. package/dist/{permissions-CTxMrreC.mjs → permissions-B1Qy55fg.mjs} +169 -169
  76. package/dist/pipeline/index.d.mts +1 -1
  77. package/dist/plugins/index.d.mts +5 -59
  78. package/dist/plugins/index.mjs +7 -7
  79. package/dist/plugins/tracing-entry.d.mts +1 -1
  80. package/dist/plugins/tracing-entry.mjs +1 -1
  81. package/dist/preloadResources-BOXzPhKU.mjs +45 -0
  82. package/dist/presets/filesUpload.d.mts +4 -4
  83. package/dist/presets/filesUpload.mjs +2 -2
  84. package/dist/presets/index.d.mts +1 -1
  85. package/dist/presets/index.mjs +1 -1
  86. package/dist/presets/multiTenant.d.mts +1 -1
  87. package/dist/presets/search.d.mts +2 -2
  88. package/dist/presets/search.mjs +1 -1
  89. package/dist/{presets-C9BE6WaZ.mjs → presets-BYbPobbF.mjs} +5 -3
  90. package/dist/{queryCachePlugin-Biqzfbi5.d.mts → queryCachePlugin-Do4P_WBC.d.mts} +2 -2
  91. package/dist/{redis-Cyzrz6SX.d.mts → redis-cpqVb85w.d.mts} +1 -1
  92. package/dist/{redis-stream-DT-YjzrB.d.mts → redis-stream-BuiAS3aE.d.mts} +1 -1
  93. package/dist/registry/index.d.mts +30 -8
  94. package/dist/registry/index.mjs +2 -2
  95. package/dist/{registry-BBE23CDj.mjs → registry-BrlHkrRS.mjs} +14 -8
  96. package/dist/{routerShared-CZV5aabX.mjs → routerShared-nCyJGOKX.mjs} +164 -17
  97. package/dist/scope/index.d.mts +2 -2
  98. package/dist/scope/index.mjs +2 -2
  99. package/dist/testing/index.d.mts +2 -25
  100. package/dist/testing/index.mjs +2 -45
  101. package/dist/testing/storageContract.d.mts +1 -1
  102. package/dist/types/index.d.mts +4 -4
  103. package/dist/types/storage.d.mts +1 -1
  104. package/dist/{types-D-fYtKjb.d.mts → types-BRmS8TNa.d.mts} +34 -11
  105. package/dist/{types-BsJMEQ4D.d.mts → types-CoMjNmkF.d.mts} +12 -1
  106. package/dist/utils/index.d.mts +83 -5
  107. package/dist/utils/index.mjs +2 -2
  108. package/dist/{utils-DC5ycPfr.mjs → utils-CvD3glpM.mjs} +168 -3
  109. package/dist/{validate-By96rH0r.mjs → validate-DFIP2VrM.mjs} +65 -3
  110. package/dist/{versioning-ZwX9tmbS.d.mts → versioning-5H4930tj.d.mts} +1 -1
  111. package/package.json +3 -3
  112. package/skills/arc/SKILL.md +43 -1
  113. package/skills/arc/references/api-reference.md +2 -0
  114. package/skills/arc/references/mcp.md +30 -1
  115. /package/dist/{EventTransport-C-2oAHtw.d.mts → EventTransport-B1GFiYUy.d.mts} +0 -0
  116. /package/dist/{EventTransport-Hxvv5QQz.mjs → EventTransport-ClEDfiMS.mjs} +0 -0
  117. /package/dist/{caching-TeHE8G-v.mjs → caching-8MfrrUYU.mjs} +0 -0
  118. /package/dist/{elevation-0YBpa663.d.mts → elevation-DvwtjDwu.d.mts} +0 -0
  119. /package/dist/{errorHandler-mHuyWzZE.d.mts → errorHandler-DuqTqeqn.d.mts} +0 -0
  120. /package/dist/{errors-C1lX_jlm.d.mts → errors-4ZloYFvm.d.mts} +0 -0
  121. /package/dist/{externalPaths-DFg-2KTp.d.mts → externalPaths-ClykOQqa.d.mts} +0 -0
  122. /package/dist/{generate-CYac-OLv.mjs → generate-CcM24hoq.mjs} +0 -0
  123. /package/dist/{generate-BWFwgcCM.d.mts → generate-cRnsafMS.d.mts} +0 -0
  124. /package/dist/{init-Dv71MsJr.d.mts → init-D-vrTZ1G.d.mts} +0 -0
  125. /package/dist/{init-HDvoO9L5.mjs → init-DYzfniuX.mjs} +0 -0
  126. /package/dist/{interface-CH0OQudo.d.mts → interface-BVkZsvEe.d.mts} +0 -0
  127. /package/dist/{interface-NwJ_qPlY.d.mts → interface-DoE6UoC2.d.mts} +0 -0
  128. /package/dist/{loadResources-ChQEj8ih.mjs → loadResources-fol6JuvW.mjs} +0 -0
  129. /package/dist/{metrics-TuOmguhi.mjs → metrics-B-PU4-Yu.mjs} +0 -0
  130. /package/dist/{replyHelpers-C-gD32oF.mjs → replyHelpers-boS5Rx26.mjs} +0 -0
  131. /package/dist/{schemaIR-Ctc89DSn.mjs → schemaIR-DAOvBWgT.mjs} +0 -0
  132. /package/dist/{sessionManager-BqFegc0W.d.mts → sessionManager-DlmaHUAl.d.mts} +0 -0
  133. /package/dist/{sse-BY6sTy4P.mjs → sse-jh5OqFjR.mjs} +0 -0
  134. /package/dist/{storage-D2KZJAmn.d.mts → storage-HcAgP5Ni.d.mts} +0 -0
  135. /package/dist/{store-helpers-B0sunfZZ.mjs → store-helpers-5JgaxieH.mjs} +0 -0
  136. /package/dist/{tracing-Dm8n7Cnn.d.mts → tracing-bRqqUxFO.d.mts} +0 -0
  137. /package/dist/{types-DVfpSfx2.d.mts → types-CfapW2t8.d.mts} +0 -0
  138. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  139. /package/dist/{versioning-B6mimogM.mjs → versioning-CuIwUQZ2.mjs} +0 -0
  140. /package/dist/{websocket-BkjeGZRn.d.mts → websocket-BJ0w-3Dt.d.mts} +0 -0
@@ -1,4 +1,37 @@
1
+ import sjson from "secure-json-parse";
1
2
  //#region src/events/transports/redis-stream.ts
3
+ /**
4
+ * Redis Stream Event Transport — Durable Event Delivery
5
+ *
6
+ * Uses Redis Streams (`XADD`/`XREADGROUP`) for persistent, at-least-once event
7
+ * delivery across multiple service instances. Unlike Pub/Sub, events are stored
8
+ * in Redis and survive crashes/restarts.
9
+ *
10
+ * **Delivery guarantee:** at-least-once. Failed messages are left unacked and
11
+ * reclaimed after `claimTimeoutMs`, which can result in duplicate handler
12
+ * execution. Consumers must be idempotent (e.g. use `event.meta.id` as a
13
+ * deduplication key) to achieve effectively-once processing.
14
+ *
15
+ * Key features:
16
+ * - Consumer groups: each event delivered to exactly one consumer per group
17
+ * - Crash recovery: pending entries are auto-claimed after `claimTimeoutMs`
18
+ * - Dead letter stream: events exceeding `maxRetries` are moved to a DLQ
19
+ * - Backpressure: configurable block time and batch size
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { RedisStreamTransport } from '@classytic/arc/events';
24
+ * import Redis from 'ioredis';
25
+ *
26
+ * const transport = new RedisStreamTransport(new Redis(), {
27
+ * stream: 'arc-events',
28
+ * group: 'api-service',
29
+ * consumer: 'worker-1',
30
+ * });
31
+ *
32
+ * await app.register(eventPlugin, { transport });
33
+ * ```
34
+ */
2
35
  var RedisStreamTransport = class {
3
36
  name = "redis-stream";
4
37
  redis;
@@ -350,7 +383,7 @@ function parseStreamFields(fields) {
350
383
  }
351
384
  if (!eventType || !rawData) return null;
352
385
  try {
353
- const parsed = JSON.parse(rawData, (key, value) => {
386
+ const parsed = sjson.parse(rawData, (key, value) => {
354
387
  if (key === "timestamp" && typeof value === "string") return new Date(value);
355
388
  return value;
356
389
  });
@@ -1,4 +1,4 @@
1
- import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "../../EventTransport-C-2oAHtw.mjs";
1
+ import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "../../EventTransport-B1GFiYUy.mjs";
2
2
 
3
3
  //#region src/events/transports/redis.d.ts
4
4
  interface RedisLike {
@@ -1,5 +1,21 @@
1
+ import sjson from "secure-json-parse";
1
2
  //#region src/events/transports/redis.ts
2
3
  /**
4
+ * Redis Event Transport
5
+ *
6
+ * Uses Redis Pub/Sub for cross-process event delivery.
7
+ * Optional dependency — only imported when explicitly used.
8
+ *
9
+ * @example
10
+ * import { RedisEventTransport } from '@classytic/arc/events/redis';
11
+ * import Redis from 'ioredis';
12
+ *
13
+ * const redis = new Redis();
14
+ * const transport = new RedisEventTransport(redis, { channel: 'arc-events' });
15
+ *
16
+ * await app.register(eventPlugin, { transport });
17
+ */
18
+ /**
3
19
  * DomainEvent contains a `Date` in `meta.timestamp`. We serialize it as an
4
20
  * ISO string and revive it on the other end so subscribers always receive a
5
21
  * proper Date object.
@@ -11,7 +27,7 @@ function serialize(event) {
11
27
  });
12
28
  }
13
29
  function deserialize(raw) {
14
- return JSON.parse(raw, (key, value) => {
30
+ return sjson.parse(raw, (key, value) => {
15
31
  if (key === "timestamp" && typeof value === "string") return new Date(value);
16
32
  return value;
17
33
  });
@@ -1,4 +1,4 @@
1
- import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as ResourceModule, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, p as loadResources, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-D-fYtKjb.mjs";
1
+ import { a as CustomPluginAuthOption, c as RawBodyOptions, d as preloadResourcesAsync, f as LoadResourcesOptions, h as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, m as ResourceModule, n as BetterAuthOption, o as JwtAuthOption, p as ResourceLike, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as preloadResources } from "../types-BRmS8TNa.mjs";
2
2
  import { FastifyInstance, FastifyServerOptions } from "fastify";
3
3
 
4
4
  //#region src/factory/createApp.d.ts
@@ -95,4 +95,4 @@ declare const edgePreset: Partial<CreateAppOptions>;
95
95
  */
96
96
  declare function getPreset(name: "production" | "development" | "testing" | "edge"): Partial<CreateAppOptions>;
97
97
  //#endregion
98
- export { ArcFactory, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, createApp, developmentPreset, edgePreset, getPreset, loadResources, productionPreset, testingPreset, toFetchHandler };
98
+ export { ArcFactory, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, createApp, developmentPreset, edgePreset, getPreset, loadResources, preloadResources, preloadResourcesAsync, productionPreset, testingPreset, toFetchHandler };
@@ -1,5 +1,6 @@
1
- import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-DNccuhyI.mjs";
2
- import { t as loadResources } from "../loadResources-ChQEj8ih.mjs";
1
+ import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-BOXzPhKU.mjs";
2
+ import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-CzCEEsEm.mjs";
3
+ import { t as loadResources } from "../loadResources-fol6JuvW.mjs";
3
4
  //#region src/factory/edge.ts
4
5
  /**
5
6
  * Convert a Fastify app into a Web Standards fetch handler.
@@ -46,4 +47,4 @@ function toFetchHandler(app, options = {}) {
46
47
  };
47
48
  }
48
49
  //#endregion
49
- export { ArcFactory, createApp, developmentPreset, edgePreset, getPreset, loadResources, productionPreset, testingPreset, toFetchHandler };
50
+ export { ArcFactory, createApp, developmentPreset, edgePreset, getPreset, loadResources, preloadResources, preloadResourcesAsync, productionPreset, testingPreset, toFetchHandler };
@@ -0,0 +1,42 @@
1
+ import { An as HookSystem, z as ResourceRegistry } from "./index-C1JSBan-.mjs";
2
+ import { t as ExternalOpenApiPaths } from "./externalPaths-ClykOQqa.mjs";
3
+ import { FastifyPluginAsync } from "fastify";
4
+
5
+ //#region src/core/arcCorePlugin.d.ts
6
+ interface ArcCorePluginOptions {
7
+ /** Enable event emission for CRUD operations (requires eventPlugin) */
8
+ emitEvents?: boolean;
9
+ /** Hook system instance (for testing/custom setup) */
10
+ hookSystem?: HookSystem;
11
+ /** Resource registry instance (for testing/custom setup) */
12
+ registry?: ResourceRegistry;
13
+ }
14
+ interface PluginMeta {
15
+ name: string;
16
+ version?: string;
17
+ options?: Record<string, unknown>;
18
+ registeredAt: string;
19
+ }
20
+ interface ArcCore {
21
+ /** Instance-scoped hook system */
22
+ hooks: HookSystem;
23
+ /** Instance-scoped resource registry */
24
+ registry: ResourceRegistry;
25
+ /** Whether event emission is enabled */
26
+ emitEvents: boolean;
27
+ /** External OpenAPI paths contributed by auth adapters or third-party integrations */
28
+ externalOpenApiPaths: ExternalOpenApiPaths[];
29
+ /** Registered plugins for introspection */
30
+ plugins: Map<string, PluginMeta>;
31
+ }
32
+ declare const arcCorePlugin: FastifyPluginAsync<ArcCorePluginOptions>;
33
+ declare const _default: FastifyPluginAsync<ArcCorePluginOptions>;
34
+ //#endregion
35
+ //#region src/types/fastify-augmentation.d.ts
36
+ declare module "fastify" {
37
+ interface FastifyInstance {
38
+ arc?: ArcCore;
39
+ }
40
+ }
41
+ //#endregion
42
+ export { arcCorePlugin as a, _default as i, ArcCorePluginOptions as n, PluginMeta as r, ArcCore as t };
@@ -1,4 +1,4 @@
1
- import { i as RequestScope } from "./types-DVfpSfx2.mjs";
1
+ import { i as RequestScope } from "./types-CfapW2t8.mjs";
2
2
  import { FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/permissions/types.d.ts
@@ -1,2 +1,2 @@
1
- import { An as HookSystem, Dn as HookOperation, En as HookHandler, Fn as beforeCreate, In as beforeDelete, Ln as beforeUpdate, Mn as afterCreate, Nn as afterDelete, On as HookPhase, Pn as afterUpdate, Rn as createHookSystem, Tn as HookContext, jn as HookSystemOptions, kn as HookRegistration, wn as DefineHookOptions, zn as defineHook } from "../index-CkW0flkU.mjs";
1
+ import { An as HookSystem, Dn as HookOperation, En as HookHandler, Fn as beforeCreate, In as beforeDelete, Ln as beforeUpdate, Mn as afterCreate, Nn as afterDelete, On as HookPhase, Pn as afterUpdate, Rn as createHookSystem, Tn as HookContext, jn as HookSystemOptions, kn as HookRegistration, wn as DefineHookOptions, zn as defineHook } from "../index-C1JSBan-.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,5 +1,5 @@
1
- import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-NwJ_qPlY.mjs";
2
- import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-Cyzrz6SX.mjs";
1
+ import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-DoE6UoC2.mjs";
2
+ import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-cpqVb85w.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { RepositoryLike } from "@classytic/repo-core/adapter";
5
5
 
@@ -1,4 +1,4 @@
1
- import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-B0sunfZZ.mjs";
1
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-5JgaxieH.mjs";
2
2
  import { createHash } from "node:crypto";
3
3
  import fp from "fastify-plugin";
4
4
  import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
@@ -266,6 +266,12 @@ var MemoryIdempotencyStore = class {
266
266
  */
267
267
  const HEADER_IDEMPOTENCY_REPLAYED = "x-idempotency-replayed";
268
268
  const HEADER_IDEMPOTENCY_KEY = "x-idempotency-key";
269
+ /**
270
+ * Cap recursion depth in body fingerprinting. Real APIs almost never exceed
271
+ * ~10 levels; the cap is generous so well-formed payloads are unaffected,
272
+ * yet low enough to stop a `{"a":{"a":{...thousands deep...}}}` stack bomb.
273
+ */
274
+ const MAX_FINGERPRINT_DEPTH = 32;
269
275
  const idempotencyPlugin = async (fastify, opts = {}) => {
270
276
  const { enabled = false, headerName = "idempotency-key", ttlMs = 864e5, lockTimeoutMs = 3e4, methods = [
271
277
  "POST",
@@ -298,14 +304,24 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
298
304
  return true;
299
305
  }
300
306
  /**
301
- * Normalize body for consistent hashing (sort keys recursively)
307
+ * Normalize body for consistent hashing (sort keys recursively).
308
+ *
309
+ * Recursion is bounded by `MAX_FINGERPRINT_DEPTH` to defeat a deep-nesting
310
+ * DoS: an attacker who sends a 1 MB JSON body of nested `{"a":{"a":...}}`
311
+ * would otherwise blow the call stack inside this hot path. The cap
312
+ * exceeds any sensible API shape (real bodies are <10 levels deep) while
313
+ * still being well under V8's default stack budget. At the limit we
314
+ * substitute a sentinel string so the hash still differs from siblings
315
+ * but recursion stops — the request is fingerprinted as if everything
316
+ * beyond the cap were `"<truncated>"`.
302
317
  */
303
- function normalizeBody(obj) {
318
+ function normalizeBody(obj, depth = 0) {
304
319
  if (obj === null || typeof obj !== "object") return obj;
305
- if (Array.isArray(obj)) return obj.map(normalizeBody);
320
+ if (depth >= MAX_FINGERPRINT_DEPTH) return "<truncated>";
321
+ if (Array.isArray(obj)) return obj.map((item) => normalizeBody(item, depth + 1));
306
322
  const sorted = {};
307
323
  const keys = Object.keys(obj).sort();
308
- for (const key of keys) sorted[key] = normalizeBody(obj[key]);
324
+ for (const key of keys) sorted[key] = normalizeBody(obj[key], depth + 1);
309
325
  return sorted;
310
326
  }
311
327
  /**
@@ -1,2 +1,2 @@
1
- import { a as UpstashRedisLike, i as RedisIdempotencyStoreOptions, n as RedisClient, o as ioredisAsIdempotencyClient, r as RedisIdempotencyStore, s as upstashAsIdempotencyClient, t as IoredisLike } from "../redis-Cyzrz6SX.mjs";
1
+ import { a as UpstashRedisLike, i as RedisIdempotencyStoreOptions, n as RedisClient, o as ioredisAsIdempotencyClient, r as RedisIdempotencyStore, s as upstashAsIdempotencyClient, t as IoredisLike } from "../redis-cpqVb85w.mjs";
2
2
  export { type IoredisLike, type RedisClient, RedisIdempotencyStore, type RedisIdempotencyStoreOptions, type UpstashRedisLike, ioredisAsIdempotencyClient, upstashAsIdempotencyClient };
@@ -1,6 +1,6 @@
1
- import { r as CacheStore, t as CacheLogger } from "./interface-CH0OQudo.mjs";
2
- import { i as RequestScope, n as Mandate } from "./types-DVfpSfx2.mjs";
3
- import { c as PermissionCheck, l as PermissionContext, u as PermissionResult } from "./fields-Anj0xdih.mjs";
1
+ import { r as CacheStore, t as CacheLogger } from "./interface-BVkZsvEe.mjs";
2
+ import { i as RequestScope, n as Mandate } from "./types-CfapW2t8.mjs";
3
+ import { c as PermissionCheck, l as PermissionContext, u as PermissionResult } from "./fields-DqZLp5d6.mjs";
4
4
  import { FastifyReply, FastifyRequest } from "fastify";
5
5
 
6
6
  //#region src/permissions/agent.d.ts
@@ -1,7 +1,7 @@
1
- import { a as QueryCacheConfig } from "./QueryCache-SvmT_9ti.mjs";
2
- import { i as RequestScope } from "./types-DVfpSfx2.mjs";
3
- import { c as PermissionCheck, d as UserBase, n as FieldPermissionMap } from "./fields-Anj0xdih.mjs";
4
- import { n as DomainEvent } from "./EventTransport-C-2oAHtw.mjs";
1
+ import { a as QueryCacheConfig } from "./QueryCache-8YmAQ-zW.mjs";
2
+ import { i as RequestScope } from "./types-CfapW2t8.mjs";
3
+ import { c as PermissionCheck, d as UserBase, n as FieldPermissionMap } from "./fields-DqZLp5d6.mjs";
4
+ import { n as DomainEvent } from "./EventTransport-B1GFiYUy.mjs";
5
5
  import { KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
6
6
  import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
7
7
  import * as _$_classytic_repo_core_adapter0 from "@classytic/repo-core/adapter";
@@ -1569,8 +1569,39 @@ interface AggregationConfig {
1569
1569
  having?: AnyRecord;
1570
1570
  /** Order grouped rows by groupBy field, measure alias, or joined-alias path. */
1571
1571
  sort?: Record<string, 1 | -1>;
1572
- /** Hard cap on result rows. Applied at the IR level (LIMIT / `$limit`). */
1572
+ /**
1573
+ * Static hard cap on result rows. Always applied at the IR level
1574
+ * (LIMIT / `$limit`), regardless of URL params. Use when the
1575
+ * aggregation has a fixed-size shape ("top 10 categories"). Mutually
1576
+ * exclusive with `defaultLimit` / `maxLimit` — pick the static form
1577
+ * OR the URL-driven form, not both.
1578
+ */
1573
1579
  limit?: number;
1580
+ /**
1581
+ * URL-driven row limit — applied when the caller passes `?limit=N`.
1582
+ * When set, the auto-route reads `query.limit`, parses it as a
1583
+ * positive integer, caps it at `maxLimit` (or the framework default
1584
+ * if `maxLimit` is unset), and writes the result into `AggRequest.limit`.
1585
+ * When the caller omits `?limit`, this default applies.
1586
+ *
1587
+ * **DoS defense.** Auto-generated `/aggregations/:name` routes today
1588
+ * have no upper bound on response size. A `groupBy: 'userId'` on a
1589
+ * 10M-user table without `defaultLimit` returns a 10M-row JSON. Setting
1590
+ * `defaultLimit: 100, maxLimit: 1000` bounds the response shape while
1591
+ * letting power users request up to 1000 rows when they need them.
1592
+ *
1593
+ * Mutually exclusive with the static `limit` — boot error if both set.
1594
+ * Setting `defaultLimit` without `maxLimit` uses the framework
1595
+ * default cap (`AGG_FRAMEWORK_MAX_LIMIT = 1000`) — explicit is better.
1596
+ */
1597
+ defaultLimit?: number;
1598
+ /**
1599
+ * Hard ceiling for URL-driven `?limit=N`. Only meaningful when
1600
+ * `defaultLimit` is set; ignored otherwise. Defaults to
1601
+ * `AGG_FRAMEWORK_MAX_LIMIT` (1000) when omitted but `defaultLimit` is
1602
+ * present. Boot validation: must be ≥ `defaultLimit`.
1603
+ */
1604
+ maxLimit?: number;
1574
1605
  /**
1575
1606
  * Top-N-per-group filter. Keeps only the top `limit` rows per
1576
1607
  * partition, ranked by `sortBy`. The classic "top 3 products per
@@ -2427,8 +2458,42 @@ interface RouteDefinition {
2427
2458
  * @default false
2428
2459
  */
2429
2460
  readonly tenantScope?: boolean;
2430
- /** SSE streaming mode */
2461
+ /**
2462
+ * SSE streaming mode. Two handler shapes work:
2463
+ *
2464
+ * - Write to `reply.raw` directly (NDJSON, custom SSE — historical contract).
2465
+ * - Return a Web `ReadableStream` (Vercel AI SDK's `result.toUIMessageStream()`,
2466
+ * `fetch().body`, etc.). 2.17.1+ auto-pipes these through
2467
+ * `pipeUIMessageStreamToReply()` so each chunk is JSON-encoded into
2468
+ * an SSE `data:` frame and client-disconnect cancels the source.
2469
+ * Pre-2.17.1 this crashed with `chunk must be a string or Buffer`.
2470
+ */
2431
2471
  readonly streamResponse?: boolean;
2472
+ /**
2473
+ * INTERNAL — marker stamped by built-in presets (softDelete, tree, …)
2474
+ * onto routes they emit. Read by the MCP collision detector
2475
+ * (`createMcpServer.resolveToolCollisions`) to attribute the route
2476
+ * source ("preset:softDelete vs user `actions.restore`") and to
2477
+ * auto-namespace the preset side when a user-authored tool legitimately
2478
+ * shadows it. Not part of the public surface — hosts must not set this.
2479
+ *
2480
+ * @internal
2481
+ */
2482
+ readonly _presetSource?: string;
2483
+ /**
2484
+ * Apply the resource's `fields` (field-level write permissions) to this
2485
+ * custom route's request body.
2486
+ *
2487
+ * Default: `true` for body-bearing methods (POST/PUT/PATCH), `false`
2488
+ * otherwise. Set to `false` to opt out — handler accepts the body as
2489
+ * supplied with no field-write filtering. The resource-level
2490
+ * `onFieldWriteDenied: 'reject' | 'strip'` setting governs how denials
2491
+ * are surfaced.
2492
+ *
2493
+ * `raw: true` routes ALWAYS bypass this — they've opted out of the
2494
+ * pipeline entirely.
2495
+ */
2496
+ readonly fieldWrite?: boolean;
2432
2497
  /**
2433
2498
  * Fastify route schema. Each slot (`body`, `querystring`, `params`,
2434
2499
  * `headers`, `response[status]`) accepts a plain JSON Schema object
@@ -2707,13 +2772,37 @@ interface ResourceConfig<TDoc = AnyRecord> {
2707
2772
  /**
2708
2773
  * Custom routes beyond CRUD. Presets also merge their routes here.
2709
2774
  *
2710
- * @example
2775
+ * **Route handlers return `IControllerResponse<T>` shape** —
2776
+ * `{ data, status?, meta?, headers? }`. Returning a bare value (array,
2777
+ * object, primitive) is supported via auto-envelope (2.17+) but the
2778
+ * explicit envelope is the canonical contract — declared `fields:`
2779
+ * permissions, custom `status`, `meta`, and `headers` only work when
2780
+ * you return the envelope. Set `raw: true` on a route to opt out of
2781
+ * arc's pipeline entirely (custom streaming, SSE, manual `reply.send()`).
2782
+ *
2783
+ * **Path collisions with auto-CRUD are detected at `defineResource()` time.**
2784
+ * If you declare a route that shares method+path with an auto-CRUD op
2785
+ * (e.g. `POST /` collides with `create`), validation throws with the
2786
+ * exact `disabledRoutes` line to add. Use one of:
2787
+ * - `disabledRoutes: ['create']` — suppress the auto-CRUD op
2788
+ * - `crud: { list: true, get: true }` — opt-in allow-list (preferred)
2789
+ * - `disableDefaultRoutes: true` — turn off all auto-CRUD on this resource
2790
+ *
2791
+ * @example Custom route alongside default CRUD:
2711
2792
  * ```typescript
2712
2793
  * routes: [
2713
2794
  * { method: 'GET', path: '/stats', handler: 'getStats', permissions: auth() },
2714
2795
  * { method: 'POST', path: '/webhook', handler: webhookFn, raw: true, permissions: auth() },
2715
2796
  * ]
2716
2797
  * ```
2798
+ *
2799
+ * @example Custom POST replacing the auto-CRUD `create`:
2800
+ * ```typescript
2801
+ * disabledRoutes: ['create'],
2802
+ * routes: [
2803
+ * { method: 'POST', path: '/', handler: customCreate, permissions: requireAuth() },
2804
+ * ]
2805
+ * ```
2717
2806
  */
2718
2807
  routes?: RouteDefinition[];
2719
2808
  /**
@@ -2886,6 +2975,86 @@ interface ResourceConfig<TDoc = AnyRecord> {
2886
2975
  skipValidation?: boolean;
2887
2976
  /** Don't register in introspection */
2888
2977
  skipRegistry?: boolean;
2978
+ /**
2979
+ * Default `limit` value when the request omits `?limit=`. Default 20.
2980
+ * Surfaced at the resource level (rather than only via a custom
2981
+ * `queryParser`) so hosts can declare it inline next to the resource
2982
+ * shape. Threads into both the auto-built controller's QueryResolver
2983
+ * AND the OpenAPI listQuery schema.
2984
+ *
2985
+ * For "fetch all" reference data, prefer the `referenceData: true`
2986
+ * shorthand below — it pins this AND `maxLimit` AND aggressive cache
2987
+ * defaults in one declaration.
2988
+ */
2989
+ defaultLimit?: number;
2990
+ /**
2991
+ * URL-driven `limit` ceiling. Default 100. Requests with
2992
+ * `?limit=N` above this cap fail validation with a 400 that names
2993
+ * the cap (`Query parameter 'limit' must be <= 100 (got N)`,
2994
+ * `meta.cap: 100` on the wire envelope) — 2.17.0+ surfaces the cap
2995
+ * value programmatically so callers can self-correct.
2996
+ */
2997
+ maxLimit?: number;
2998
+ /**
2999
+ * Mark this resource as **reference data** — small, mostly-static rows
3000
+ * (currencies, countries, plans, credentials, pipeline stages) that
3001
+ * callers want to fetch all of at once and cache aggressively.
3002
+ *
3003
+ * Sets the following defaults (each only when the corresponding
3004
+ * narrower flag is not explicitly set, so this stays composable):
3005
+ *
3006
+ * - `crud: { list: true, get: true }` — read-only surface (reference
3007
+ * data is rarely mutated through the public API; mutate via
3008
+ * migrations / admin tools).
3009
+ * - `defaultLimit: 1000` — one request returns everything by default.
3010
+ * - `maxLimit: 1000` — same cap so explicit `?limit=` requests
3011
+ * don't have to know the resource is reference data.
3012
+ * - `cache: { staleTime: 300, gcTime: 600 }` — 5 min fresh / 10 min
3013
+ * GC window so reference data isn't refetched on every request
3014
+ * (values are seconds per `ResourceCacheConfig`). No-op without
3015
+ * `queryCachePlugin` — same contract as the manual `cache` field;
3016
+ * a first-mount diagnostic fires when the plugin is missing.
3017
+ *
3018
+ * Pre-2.17.0 hosts achieved this by sprinkling `defaultLimit: 1000` +
3019
+ * a custom queryParser + a `cache:` block across every reference
3020
+ * resource. The shorthand collapses that to one flag.
3021
+ *
3022
+ * @example
3023
+ * ```ts
3024
+ * defineResource({
3025
+ * name: 'credential-type',
3026
+ * adapter,
3027
+ * referenceData: true,
3028
+ * });
3029
+ * ```
3030
+ */
3031
+ referenceData?: boolean;
3032
+ /**
3033
+ * Shorthand for the "service resource" pattern — a resource that ships
3034
+ * ONLY custom routes (no auto-CRUD, no adapter, no validation, no
3035
+ * introspection registry entry). Setting `customRoutesOnly: true`
3036
+ * is equivalent to the three-flag combo:
3037
+ *
3038
+ * ```ts
3039
+ * { disableDefaultRoutes: true, skipValidation: true, skipRegistry: true }
3040
+ * ```
3041
+ *
3042
+ * Each of those flags stays public — power users that need to opt OUT
3043
+ * of one (e.g. `customRoutesOnly: true` + `skipRegistry: false` because
3044
+ * a custom-routes-only resource still wants OpenAPI docs) can pass the
3045
+ * narrow flag explicitly; explicit settings always win over the
3046
+ * shorthand. The shorthand only fills in flags that were not set.
3047
+ *
3048
+ * @example
3049
+ * ```ts
3050
+ * defineResource({
3051
+ * name: 'health',
3052
+ * customRoutesOnly: true,
3053
+ * routes: [{ method: 'GET', path: '/ping', handler: () => ({ ok: true }) }],
3054
+ * });
3055
+ * ```
3056
+ */
3057
+ customRoutesOnly?: boolean;
2889
3058
  /**
2890
3059
  * Per-resource MCP opt-out.
2891
3060
  *
@@ -3029,7 +3198,16 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
3029
3198
  readonly updateMethod?: "PUT" | "PATCH" | "both";
3030
3199
  readonly pipe?: PipelineConfig;
3031
3200
  readonly fields?: FieldPermissionMap;
3201
+ /**
3202
+ * Policy for field-write denials — `'reject'` (default, secure) raises 403
3203
+ * listing the denied fields; `'strip'` silently drops them. Honored by
3204
+ * both `BodySanitizer` (auto-CRUD) and `buildFieldWritePreHandler`
3205
+ * (custom routes) so the policy is consistent across all body-bearing
3206
+ * routes for the resource.
3207
+ */
3208
+ readonly onFieldWriteDenied?: "reject" | "strip";
3032
3209
  readonly cache?: ResourceCacheConfig;
3210
+ readonly referenceData: boolean;
3033
3211
  /**
3034
3212
  * Per-resource MCP opt-out. `false` keeps the resource out of every
3035
3213
  * `mcpPlugin` registration regardless of the plugin's `expose` /
@@ -3066,8 +3244,34 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
3066
3244
  * `buildResourcePlugin` so the host's configured logger owns the
3067
3245
  * output — the framework never speaks to `console.*` from `src/`
3068
3246
  * outside of the CLI.
3247
+ *
3248
+ * Prefer the public {@link warnings} getter for programmatic access
3249
+ * (CI fail-on-warnings, lint integrations) — it returns a frozen
3250
+ * snapshot so callers can't accidentally mutate the diagnostic list.
3069
3251
  */
3070
3252
  _diagnostics?: ResourceDiagnostic[];
3253
+ /**
3254
+ * Public view of {@link _diagnostics} for programmatic consumption.
3255
+ *
3256
+ * Returns the collected define-time diagnostics for this resource.
3257
+ * Empty when the resource validated cleanly. Useful for CI gates that
3258
+ * want to fail the build on any non-fatal warning:
3259
+ *
3260
+ * @example
3261
+ * ```ts
3262
+ * import { orderResource } from './resources/order.resource.js';
3263
+ * if (orderResource.warnings.length > 0) {
3264
+ * for (const w of orderResource.warnings) console.error(w.message);
3265
+ * process.exit(1);
3266
+ * }
3267
+ * ```
3268
+ *
3269
+ * **Scope.** Covers define-time only — diagnostics emitted at
3270
+ * first-mount (e.g. `cache.invalidateOn` without `queryCachePlugin`)
3271
+ * surface through `fastify.log.warn` instead and are not collected
3272
+ * here; they require a Fastify instance to detect.
3273
+ */
3274
+ get warnings(): readonly ResourceDiagnostic[];
3071
3275
  /**
3072
3276
  * Per-host idempotency guard used by `buildResourcePlugin` to
3073
3277
  * skip duplicate shared-state writes when the same resource is
@@ -3121,6 +3325,26 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
3121
3325
  * `BaseController<TDoc extends AnyRecord>` actually requires it.
3122
3326
  * One internal boundary cast replaces N host-side casts.
3123
3327
  */
3328
+ /**
3329
+ * Narrow overload: when the host passes an `actions: { ... }` literal, the
3330
+ * returned `ResourceDefinition.actions` is typed as the exact captured map
3331
+ * (not the wide `ActionsMap | undefined`). Without this overload, hosts that
3332
+ * wrote `defineResource({ actions: { send: ..., receive: ... } })` saw
3333
+ * `resource.actions` typed `ActionsMap | undefined` and got
3334
+ * "'resource.actions' is possibly 'undefined'" on every `resource.actions.send`
3335
+ * call. TS infers `TActions` from the object literal at the call site —
3336
+ * no explicit generic needed.
3337
+ */
3338
+ declare function defineResource<TDoc, TActions extends ActionsMap>(config: ResourceConfig<TDoc> & {
3339
+ actions: TActions;
3340
+ }): ResourceDefinition<TDoc> & {
3341
+ readonly actions: TActions;
3342
+ };
3343
+ /**
3344
+ * Wide overload: no `actions` declared. `ResourceDefinition<TDoc>.actions`
3345
+ * stays `ActionsMap | undefined` exactly as in pre-2.17.1 — no
3346
+ * back-compat breakage for hosts that never touched actions.
3347
+ */
3124
3348
  declare function defineResource<TDoc = AnyRecord>(config: ResourceConfig<TDoc>): ResourceDefinition<TDoc>;
3125
3349
  //#endregion
3126
3350
  //#region src/registry/ResourceRegistry.d.ts
@@ -3390,6 +3614,13 @@ interface CrudRouterOptions {
3390
3614
  schemaOptions?: RouteSchemaOptions;
3391
3615
  /** Field-level permissions (visibility, writability per role) */
3392
3616
  fields?: FieldPermissionMap;
3617
+ /**
3618
+ * Policy for handling fields the caller lacks write permission for.
3619
+ * Auto-CRUD reads this via `BodySanitizer`; custom routes' field-write
3620
+ * preHandler honors the same setting. Default: `'reject'` — silent
3621
+ * stripping hides misconfigurations and real attacks.
3622
+ */
3623
+ onFieldWriteDenied?: "reject" | "strip";
3393
3624
  /** HTTP method for update routes. Default: 'PATCH' */
3394
3625
  updateMethod?: "PUT" | "PATCH" | "both";
3395
3626
  /**
@@ -3713,7 +3944,24 @@ interface RegistryEntry extends ResourceMetadata {
3713
3944
  field: string;
3714
3945
  maxRangeDays?: number;
3715
3946
  }; /** Whether the aggregation requires named filters — surfaced in docs. */
3716
- readonly requireFilters?: readonly string[]; /** MCP tool generation flag — `false` to skip, object for overrides. */
3947
+ readonly requireFilters?: readonly string[];
3948
+ /**
3949
+ * Static result-row cap (no URL override). Mutually exclusive with
3950
+ * `defaultLimit` / `maxLimit`. Surfaced so OpenAPI docs can mention
3951
+ * the fixed cap in the description.
3952
+ */
3953
+ readonly limit?: number;
3954
+ /**
3955
+ * URL-driven default limit (caller may pass `?limit=N`). When set,
3956
+ * OpenAPI docs render `?limit` as a query parameter with this
3957
+ * default and `maxLimit` as the maximum.
3958
+ */
3959
+ readonly defaultLimit?: number;
3960
+ /**
3961
+ * URL-driven limit ceiling (only meaningful with `defaultLimit`).
3962
+ * OpenAPI docs render this as the `maximum` constraint on `?limit`.
3963
+ */
3964
+ readonly maxLimit?: number; /** MCP tool generation flag — `false` to skip, object for overrides. */
3717
3965
  readonly mcp?: boolean | {
3718
3966
  readonly description?: string;
3719
3967
  readonly annotations?: Record<string, unknown>;