@classytic/arc 2.20.0 → 2.22.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 (143) hide show
  1. package/dist/{BaseController-D-MHZ27_.mjs → BaseController-C6FlRWxF.mjs} +2 -2
  2. package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
  3. package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
  4. package/dist/{actionPermissions-C9RMiIpc.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
  5. package/dist/audit/index.d.mts +9 -5
  6. package/dist/auth/audit.d.mts +7 -4
  7. package/dist/auth/audit.mjs +5 -0
  8. package/dist/auth/index.d.mts +35 -14
  9. package/dist/auth/index.mjs +6 -3
  10. package/dist/auth/redis-session.d.mts +1 -2
  11. package/dist/cache/index.d.mts +2 -3
  12. package/dist/cli/commands/describe.d.mts +1 -2
  13. package/dist/cli/commands/docs.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/context/index.d.mts +2 -2
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-CWhyGkiM.mjs → core-b6fgFgnd.mjs} +129 -17
  20. package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-h1cEEaEX.mjs} +10 -6
  21. package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
  22. package/dist/{createApp-D6uwzCKu.mjs → createApp-Ci7yG5gv.mjs} +390 -192
  23. package/dist/discovery/index.d.mts +0 -1
  24. package/dist/docs/index.d.mts +1 -2
  25. package/dist/docs/index.mjs +1 -1
  26. package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
  27. package/dist/encryption/index.d.mts +2 -144
  28. package/dist/encryption/index.mjs +9 -5
  29. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
  30. package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -1
  31. package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
  32. package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
  33. package/dist/events/index.d.mts +9 -6
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis.d.mts +1 -2
  36. package/dist/factory/index.d.mts +30 -9
  37. package/dist/factory/index.mjs +35 -4
  38. package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -3
  39. package/dist/fields-D3lhgzhD.d.mts +121 -0
  40. package/dist/health-ScyjIVo5.mjs +202 -0
  41. package/dist/hooks/index.d.mts +1 -1
  42. package/dist/idempotency/index.d.mts +4 -3
  43. package/dist/idempotency/index.mjs +2 -0
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{types-BHMKlMBU.d.mts → index-BEu4wjbL.d.mts} +241 -80
  46. package/dist/{index-DUa8WHz3.d.mts → index-D8MKYGDa.d.mts} +88 -7
  47. package/dist/{index-_--GyzcO.d.mts → index-DbtgCOCj.d.mts} +1324 -1228
  48. package/dist/{index-CdFkIMEd.d.mts → index-ogIHvOjo.d.mts} +36 -7
  49. package/dist/index.d.mts +8 -7
  50. package/dist/index.mjs +6 -6
  51. package/dist/{init-YH_nslQR.mjs → init-DleK3p9f.mjs} +272 -139
  52. package/dist/integrations/event-gateway.d.mts +2 -3
  53. package/dist/integrations/index.d.mts +2 -2
  54. package/dist/integrations/jobs.d.mts +0 -1
  55. package/dist/integrations/mcp/index.d.mts +6 -7
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -2
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/integrations/streamline.d.mts +2 -2
  60. package/dist/integrations/webhooks.d.mts +8 -5
  61. package/dist/integrations/webhooks.mjs +9 -5
  62. package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
  63. package/dist/integrations/websocket-redis.d.mts +1 -2
  64. package/dist/integrations/websocket.d.mts +2 -2
  65. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  66. package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
  67. package/dist/logger/index.d.mts +26 -1
  68. package/dist/logger/index.mjs +39 -1
  69. package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-ByA2tf6D.mjs} +8 -4
  70. package/dist/middleware/index.d.mts +1 -2
  71. package/dist/{module-JYH9V0I8.d.mts → module-B4u10iOJ.d.mts} +71 -1
  72. package/dist/{openapi-Dq6RbEGK.mjs → openapi-BRpMZD7C.mjs} +3 -3
  73. package/dist/permissions/index.d.mts +4 -3
  74. package/dist/permissions/index.mjs +2 -2
  75. package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
  76. package/dist/pipeline/index.d.mts +1 -2
  77. package/dist/plugins/index.d.mts +96 -7
  78. package/dist/plugins/index.mjs +86 -268
  79. package/dist/plugins/response-cache.d.mts +5 -3
  80. package/dist/plugins/response-cache.mjs +1 -0
  81. package/dist/plugins/tracing-entry.d.mts +1 -1
  82. package/dist/plugins/tracing-entry.mjs +7 -4
  83. package/dist/presets/filesUpload.d.mts +4 -5
  84. package/dist/presets/filesUpload.mjs +1 -1
  85. package/dist/presets/index.d.mts +1 -2
  86. package/dist/presets/index.mjs +1 -1
  87. package/dist/presets/multiTenant.d.mts +1 -2
  88. package/dist/presets/search.d.mts +2 -3
  89. package/dist/presets/search.mjs +1 -1
  90. package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
  91. package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
  92. package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
  93. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  94. package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
  95. package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
  96. package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-xHc42fwB.mjs} +38 -18
  97. package/dist/registry/index.d.mts +2 -2
  98. package/dist/requestId-5BhTVzBv.mjs +113 -0
  99. package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
  100. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
  101. package/dist/schemas/index.d.mts +0 -1
  102. package/dist/scim/index.d.mts +0 -1
  103. package/dist/scim/index.mjs +6 -7
  104. package/dist/scope/index.d.mts +2 -3
  105. package/dist/scope/index.mjs +2 -76
  106. package/dist/scope-k15RXfcq.mjs +64 -0
  107. package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
  108. package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
  109. package/dist/testing/index.d.mts +83 -4
  110. package/dist/testing/index.mjs +74 -4
  111. package/dist/testing/storageContract.d.mts +1 -2
  112. package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
  113. package/dist/types/index.d.mts +4 -4
  114. package/dist/types/storage.d.mts +1 -1
  115. package/dist/{types-C9b2HR5W.d.mts → types-Bc42CUyk.d.mts} +1 -2
  116. package/dist/types-Cc91nXuA.d.mts +132 -0
  117. package/dist/{fields-C7ahdGYf.d.mts → types-Csxga7d3.d.mts} +2 -123
  118. package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
  119. package/dist/usage/index.d.mts +142 -0
  120. package/dist/usage/index.mjs +0 -0
  121. package/dist/utils/index.d.mts +220 -144
  122. package/dist/utils/index.mjs +3 -3
  123. package/dist/{utils-ChjYQpjn.mjs → utils-FhJ88TJE.mjs} +129 -39
  124. package/dist/{versioning-D_eBCfNr.d.mts → versioning-DLwklPlG.d.mts} +1 -7
  125. package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
  126. package/llms.txt +39 -0
  127. package/package.json +453 -447
  128. package/skills/arc/SKILL.md +14 -8
  129. package/skills/arc/references/api-reference.md +34 -0
  130. package/skills/arc/references/auth.md +21 -4
  131. package/skills/arc/references/mcp.md +1 -1
  132. package/skills/arc/references/production.md +44 -2
  133. package/skills/arc-code-review/SKILL.md +1 -1
  134. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  135. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  136. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  137. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  138. package/skills/arc-module-publishing/SKILL.md +243 -0
  139. package/skills/arc-module-publishing/references/authoring-modules.md +327 -0
  140. package/skills/arc-module-publishing/references/package-manifest.md +157 -0
  141. package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
  142. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  143. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -2,8 +2,8 @@ import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-DNre0
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { d as createDomainError, f as createError, i as NotFoundError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
4
4
  import { b as isElevated, c as getOrgId, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
5
- import { N as simpleEqualityMatcher, _ as ArcQueryParser, r as scheduleBackground, t as getUserId } from "./utils-ChjYQpjn.mjs";
6
- import { F as applyFieldWritePermissions, L as resolveEffectiveRoles } from "./permissions-CgbOLJZG.mjs";
5
+ import { F as simpleEqualityMatcher, _ as ArcQueryParser, r as scheduleBackground, t as getUserId } from "./utils-FhJ88TJE.mjs";
6
+ import { I as applyFieldWritePermissions, R as resolveEffectiveRoles } from "./permissions-BJMuKMl7.mjs";
7
7
  import { t as getUserRoles } from "./types-XTApa8b6.mjs";
8
8
  import { t as buildQueryKey } from "./keys-Cd316u_y.mjs";
9
9
  //#region src/core/AccessControl.ts
@@ -1,5 +1,4 @@
1
1
  import { DeadLetteredEvent, DomainEvent, DomainEvent as DomainEvent$1, EventHandler, EventHandler as EventHandler$1, EventLogger, EventLogger as EventLogger$1, EventTransport, EventTransport as EventTransport$1, PublishManyResult } from "@classytic/primitives/events";
2
-
3
2
  //#region src/events/EventTransport.d.ts
4
3
  interface MemoryEventTransportOptions {
5
4
  /** Logger for error/warning messages (default: console) */
@@ -1,5 +1,4 @@
1
1
  import { r as CacheStore } from "./interface-C1_WLTMK.mjs";
2
-
3
2
  //#region src/cache/QueryCache.d.ts
4
3
  /** Metadata wrapper stored in CacheStore */
5
4
  interface CacheEnvelope<T = unknown> {
@@ -1,5 +1,5 @@
1
1
  import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
2
- import { L as resolveEffectiveRoles, P as applyFieldReadPermissions } from "./permissions-CgbOLJZG.mjs";
2
+ import { F as applyFieldReadPermissions, R as resolveEffectiveRoles } from "./permissions-BJMuKMl7.mjs";
3
3
  import { t as getUserRoles } from "./types-XTApa8b6.mjs";
4
4
  import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
5
5
  import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
@@ -1,7 +1,7 @@
1
- import { d as UserBase } from "../fields-C7ahdGYf.mjs";
1
+ import "../index-DbtgCOCj.mjs";
2
+ import { i as UserBase } from "../types-Csxga7d3.mjs";
2
3
  import { FastifyPluginAsync } from "fastify";
3
4
  import { RepositoryLike } from "@classytic/repo-core/adapter";
4
-
5
5
  //#region src/audit/stores/interface.d.ts
6
6
  type AuditAction = "create" | "update" | "delete" | "restore" | "custom";
7
7
  interface AuditEntry {
@@ -126,7 +126,9 @@ interface AuditPluginOptions {
126
126
  * Set `purgeIntervalMs: 0` to skip the timer (manual purge only).
127
127
  */
128
128
  retention?: {
129
- /** Max entry age in ms. Entries with `timestamp < now - maxAgeMs` are purged. */maxAgeMs: number; /** Interval between purges in ms. Default 86_400_000 (24h). 0 disables the timer. */
129
+ /** Max entry age in ms. Entries with `timestamp < now - maxAgeMs` are purged. */
130
+ maxAgeMs: number;
131
+ /** Interval between purges in ms. Default 86_400_000 (24h). 0 disables the timer. */
130
132
  purgeIntervalMs?: number;
131
133
  };
132
134
  /**
@@ -159,8 +161,10 @@ interface AuditPluginOptions {
159
161
  * - `false`: Disable auto-audit (manual `fastify.audit.*()` calls only)
160
162
  */
161
163
  autoAudit?: boolean | {
162
- operations?: ("create" | "update" | "delete")[]; /** Allowlist — only listed resources are audited (mutually exclusive with exclude) */
163
- include?: string[]; /** Denylist — audit everything except listed resources */
164
+ operations?: ("create" | "update" | "delete")[];
165
+ /** Allowlist — only listed resources are audited (mutually exclusive with exclude) */
166
+ include?: string[];
167
+ /** Denylist — audit everything except listed resources */
164
168
  exclude?: string[];
165
169
  /**
166
170
  * Per-resource opt-in mode: only audit resources with `audit: true` in their
@@ -1,5 +1,4 @@
1
1
  import { FastifyInstance } from "fastify";
2
-
3
2
  //#region src/auth/audit.d.ts
4
3
  /**
5
4
  * Canonical auth event names emitted by the bridge. Hosts pattern-match
@@ -143,9 +142,13 @@ interface BetterAuthAuditBridge {
143
142
  * is called again. Useful for Prometheus exporters that scrape periodically.
144
143
  */
145
144
  getStats(): {
146
- /** Events buffered during pre-`attach` phase that were dropped due to `bufferSize` overflow. */droppedFromBuffer: number; /** Events that reached `app.audit.custom(...)` but threw (audit store failure). */
147
- dispatchFailures: number; /** Total `dispatch` calls — useful to verify BA is actually invoking the bridge's hooks. */
148
- dispatchAttempts: number; /** Events currently buffered awaiting `attach`. */
145
+ /** Events buffered during pre-`attach` phase that were dropped due to `bufferSize` overflow. */
146
+ droppedFromBuffer: number;
147
+ /** Events that reached `app.audit.custom(...)` but threw (audit store failure). */
148
+ dispatchFailures: number;
149
+ /** Total `dispatch` calls — useful to verify BA is actually invoking the bridge's hooks. */
150
+ dispatchAttempts: number;
151
+ /** Events currently buffered awaiting `attach`. */
149
152
  pendingBuffered: number;
150
153
  };
151
154
  }
@@ -100,6 +100,11 @@ const ENDPOINT_TO_EVENT = [
100
100
  test: /\/api-key\/delete\b/,
101
101
  phase: "after",
102
102
  name: "apikey.delete"
103
+ },
104
+ {
105
+ test: /\/sign-out\b/,
106
+ phase: "after",
107
+ name: "session.delete"
103
108
  }
104
109
  ];
105
110
  function classifyEndpoint(phase, ctx) {
@@ -1,9 +1,8 @@
1
- import { Ht as AuthPluginOptions, Vt as AuthHelpers } from "../index-_--GyzcO.mjs";
2
- import { c as PermissionCheck } from "../fields-C7ahdGYf.mjs";
1
+ import { Ht as AuthPluginOptions, Vt as AuthHelpers } from "../index-DbtgCOCj.mjs";
2
+ import { t as PermissionCheck } from "../types-Csxga7d3.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
4
- import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-BXG-HGwG.mjs";
4
+ import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-BHOM3N_0.mjs";
5
5
  import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest } from "fastify";
6
-
7
6
  //#region src/auth/authPlugin.d.ts
8
7
  declare module "fastify" {
9
8
  interface FastifyInstance {
@@ -17,14 +16,6 @@ declare module "fastify" {
17
16
  auth: AuthHelpers;
18
17
  }
19
18
  }
20
- /**
21
- * Extract Bearer token from Authorization header.
22
- *
23
- * Exported for property-based test coverage — the contract is:
24
- * - header must start with exactly `"Bearer "` (case-sensitive, one space)
25
- * - everything after that prefix is returned verbatim (no trim, no parse)
26
- * - missing header → `null`; any other shape → `null`
27
- */
28
19
  declare const authPlugin: FastifyPluginAsync<AuthPluginOptions>;
29
20
  declare const _default: FastifyPluginAsync<AuthPluginOptions>;
30
21
  //#endregion
@@ -93,6 +84,34 @@ interface BetterAuthAdapterOptions {
93
84
  * When true, includes the actual error message for debugging.
94
85
  */
95
86
  exposeAuthErrors?: boolean;
87
+ /**
88
+ * Per-route rate limit for the Better Auth catch-all (`/api/auth/*`),
89
+ * applied via `@fastify/rate-limit`'s route config (requires the
90
+ * limiter to be registered, which createApp does by default).
91
+ *
92
+ * Credential endpoints deserve a tighter bucket than the global limit
93
+ * (brute-force / credential-stuffing surface), BUT the same catch-all
94
+ * also serves Better Auth's chatty `get-session` heartbeat — so arc
95
+ * ships NO default here; a forced strict limit (the classic "5/min on
96
+ * /login") would 429 every SPA that polls sessions. Pick a ceiling that
97
+ * covers your session-poll cadence, or key the bucket smarter:
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * createBetterAuthAdapter({
102
+ * auth,
103
+ * // simple ceiling over the whole auth surface
104
+ * rateLimit: { max: 60, timeWindow: '1 minute' },
105
+ * });
106
+ *
107
+ * // OR keep the global limiter but exempt the heartbeat:
108
+ * createApp({ rateLimit: { max: 100, skipPaths: ['/api/auth/get-session'] } });
109
+ * ```
110
+ */
111
+ rateLimit?: {
112
+ max: number;
113
+ timeWindow: string | number;
114
+ } | false;
96
115
  }
97
116
  interface BetterAuthAdapterResult {
98
117
  /** Fastify plugin that registers catch-all auth routes */
@@ -177,8 +196,10 @@ interface BetterAuthOpenApiOptions {
177
196
  */
178
197
  userFields?: Record<string, {
179
198
  type: string;
180
- description?: string; /** Whether this field is required in sign-up (default: false) */
181
- required?: boolean; /** Whether this field is accepted in request body (default: true). Set false for output-only fields. */
199
+ description?: string;
200
+ /** Whether this field is required in sign-up (default: false) */
201
+ required?: boolean;
202
+ /** Whether this field is accepted in request body (default: true). Set false for output-only fields. */
182
203
  input?: boolean;
183
204
  }>;
184
205
  }
@@ -1,7 +1,7 @@
1
1
  import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-DkO8Nk6-.mjs";
2
- import { _ as requireTeamMembership, m as requireOrgRole, p as requireOrgMembership } from "../permissions-CgbOLJZG.mjs";
2
+ import { h as requireOrgRole, m as requireOrgMembership, v as requireTeamMembership } from "../permissions-BJMuKMl7.mjs";
3
3
  import { n as normalizeRoles, t as getUserRoles } from "../types-XTApa8b6.mjs";
4
- import { a as toJsonSchema } from "../schemaConverter-U0MnO9vd.mjs";
4
+ import { o as toJsonSchema } from "../schemaConverter-Ds5reSGs.mjs";
5
5
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
6
6
  import fp from "fastify-plugin";
7
7
  //#region src/auth/authPlugin.ts
@@ -605,7 +605,8 @@ function createBetterAuthAdapter(options) {
605
605
  if (openapiOpt === false) extractedOpenApi = void 0;
606
606
  else if (typeof openapiOpt === "object") extractedOpenApi = openapiOpt;
607
607
  const betterAuthPlugin = async (fastify) => {
608
- fastify.all(`${normalizedBase}/*`, async (request, reply) => {
608
+ const routeOpts = options.rateLimit && typeof options.rateLimit === "object" ? { config: { rateLimit: options.rateLimit } } : {};
609
+ fastify.all(`${normalizedBase}/*`, routeOpts, async (request, reply) => {
609
610
  try {
610
611
  const fetchRequest = toFetchRequest(request);
611
612
  await sendFetchResponse(await auth.handler(fetchRequest), reply);
@@ -619,6 +620,8 @@ function createBetterAuthAdapter(options) {
619
620
  });
620
621
  if (!fastify.hasDecorator("authenticate")) fastify.decorate("authenticate", authenticate);
621
622
  if (!fastify.hasDecorator("optionalAuthenticate")) fastify.decorate("optionalAuthenticate", optionalAuthenticate);
623
+ if (!fastify.hasRequestDecorator("user")) fastify.decorateRequest("user", void 0);
624
+ if (!fastify.hasRequestDecorator("session")) fastify.decorateRequest("session", void 0);
622
625
  if (!extractedOpenApi && openapiOpt !== false && auth.api && typeof auth.api === "object") {
623
626
  const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-CV1lxpRX.mjs");
624
627
  extractedOpenApi = extractBetterAuthOpenApi(auth.api, {
@@ -1,5 +1,4 @@
1
- import { i as SessionData, s as SessionStore } from "../sessionManager-BXG-HGwG.mjs";
2
-
1
+ import { i as SessionData, s as SessionStore } from "../sessionManager-BHOM3N_0.mjs";
3
2
  //#region src/auth/redis-session.d.ts
4
3
  /** Minimal Redis client interface — compatible with ioredis */
5
4
  interface RedisLike {
@@ -1,7 +1,6 @@
1
1
  import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-C1_WLTMK.mjs";
2
- import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-TRfXau3W.mjs";
3
- import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-xzFyKAjT.mjs";
4
-
2
+ import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-Z1B38koh.mjs";
3
+ import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-CfYWq-Ak.mjs";
5
4
  //#region src/cache/keys.d.ts
6
5
  /**
7
6
  * Cache Key Utilities
@@ -1,5 +1,4 @@
1
- import { V as ResourceDefinition, ht as RouteSchemaOptions, it as RateLimitConfig } from "../../index-_--GyzcO.mjs";
2
-
1
+ import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-DbtgCOCj.mjs";
3
2
  //#region src/cli/commands/describe.d.ts
4
3
  interface DescribedResource {
5
4
  name: string;
@@ -1,4 +1,4 @@
1
- import { t as buildOpenApiSpec } from "../../openapi-Dq6RbEGK.mjs";
1
+ import { t as buildOpenApiSpec } from "../../openapi-BRpMZD7C.mjs";
2
2
  import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-OMCvIEE7.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { mkdirSync, writeFileSync } from "node:fs";
@@ -1,2 +1,2 @@
1
- import { t as init } from "../../init-YH_nslQR.mjs";
1
+ import { t as init } from "../../init-DleK3p9f.mjs";
2
2
  export { init };
@@ -2,6 +2,6 @@ import { describe } from "./commands/describe.mjs";
2
2
  import { exportDocs } from "./commands/docs.mjs";
3
3
  import { doctor } from "./commands/doctor.mjs";
4
4
  import { t as generate } from "../generate-usRSdQmP.mjs";
5
- import { t as init } from "../init-YH_nslQR.mjs";
5
+ import { t as init } from "../init-DleK3p9f.mjs";
6
6
  import { introspect } from "./commands/introspect.mjs";
7
7
  export { describe, doctor, exportDocs, generate, init, introspect };
@@ -1,5 +1,4 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
-
3
2
  //#region src/context/requestContext.d.ts
4
3
  /**
5
4
  * Shape of the request-scoped context store.
@@ -104,7 +103,8 @@ declare const transactionContext: {
104
103
  * Get the current DB session, or `undefined` if not inside a
105
104
  * `transactionContext.run(...)` scope.
106
105
  */
107
- get(): DbSession | undefined; /** The underlying AsyncLocalStorage instance. Exposed for advanced integrations. */
106
+ get(): DbSession | undefined;
107
+ /** The underlying AsyncLocalStorage instance. Exposed for advanced integrations. */
108
108
  storage: AsyncLocalStorage<unknown>;
109
109
  };
110
110
  //#endregion
@@ -1,3 +1,3 @@
1
- import { At as AggregationMaterializedResult, B as defineResource, Bn as BodySanitizerConfig, Ct as AggMeasureInput, Dt as AggregationDateRangeRequirement, Et as AggregationConfig, Fn as BaseControllerOptions, In as ControllerConfigurableOptions, Ln as ControllerConstructionOptions, Mt as AggregationsMap, Ot as AggregationIndexHint, Rn as ListResult, Tt as AggregationCacheConfig, V as ResourceDefinition, an as SlugExt, cn as BulkMixin, dn as QueryResolverConfig, en as BaseController, fn as AccessControl, in as TreeMixin, jt as AggregationRateLimit, kt as AggregationMaterializedContext, ln as BaseCrudController, nn as SoftDeleteMixin, on as SlugMixin, pn as AccessControlConfig, rn as TreeExt, sn as BulkExt, tn as SoftDeleteExt, un as QueryResolver, wt as AggMeasureShorthand, zn as BodySanitizer } from "../index-_--GyzcO.mjs";
2
- import { A as MAX_FILTER_DEPTH, C as DEFAULT_SORT, D as HOOK_PHASES, E as HOOK_OPERATIONS, F as RESERVED_QUERY_PARAMS, I as SYSTEM_FIELDS, M as MAX_SEARCH_LENGTH, N as MUTATION_OPERATIONS, O as HookOperation, P as MutationOperation, S as DEFAULT_MAX_LIMIT, T as DEFAULT_UPDATE_METHOD, _ as defineAggregation, a as createRequestContext, b as DEFAULT_ID_FIELD, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as createPermissionMiddleware, h as createCrudRouter, i as createFastifyHandler, j as MAX_REGEX_LENGTH, k as HookPhase, l as getEntityId, m as defineAction, n as isFieldReadable, o as getControllerContext, p as DefineActionConfig, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as CRUD_OPERATIONS, w as DEFAULT_TENANT_FIELD, x as DEFAULT_LIMIT, y as CrudOperation } from "../index-DUa8WHz3.mjs";
3
- export { AccessControl, type AccessControlConfig, type AggMeasureInput, type AggMeasureShorthand, type AggregationCacheConfig, type AggregationConfig, type AggregationDateRangeRequirement, type AggregationIndexHint, type AggregationMaterializedContext, type AggregationMaterializedResult, type AggregationRateLimit, type AggregationsMap, BaseController, type BaseControllerOptions, BaseCrudController, BodySanitizer, type BodySanitizerConfig, type BulkExt, BulkMixin, CRUD_OPERATIONS, type ControllerConfigurableOptions, type ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, type DefineActionConfig, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, type ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, type QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, type SlugExt, SlugMixin, type SoftDeleteExt, SoftDeleteMixin, type TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
1
+ import { B as defineResource, Bn as BodySanitizerConfig, Fn as BaseControllerOptions, In as ControllerConfigurableOptions, Ln as ControllerConstructionOptions, Rn as ListResult, V as ResourceDefinition, an as SlugExt, cn as BulkMixin, ct as AggMeasureShorthand, dn as QueryResolverConfig, dt as AggregationDateRangeRequirement, en as BaseController, fn as AccessControl, ft as AggregationIndexHint, gt as AggregationsMap, ht as AggregationRateLimit, in as TreeMixin, ln as BaseCrudController, lt as AggregationCacheConfig, mt as AggregationMaterializedResult, nn as SoftDeleteMixin, on as SlugMixin, pn as AccessControlConfig, pt as AggregationMaterializedContext, rn as TreeExt, sn as BulkExt, st as AggMeasureInput, tn as SoftDeleteExt, un as QueryResolver, ut as AggregationConfig, zn as BodySanitizer } from "../index-DbtgCOCj.mjs";
2
+ import { A as HOOK_OPERATIONS, B as SYSTEM_FIELDS, C as CrudOperation, D as DEFAULT_SORT, E as DEFAULT_MAX_LIMIT, F as MAX_REGEX_LENGTH, I as MAX_SEARCH_LENGTH, L as MUTATION_OPERATIONS, M as HookOperation, N as HookPhase, O as DEFAULT_TENANT_FIELD, P as MAX_FILTER_DEPTH, R as MutationOperation, S as CRUD_OPERATIONS, T as DEFAULT_LIMIT, _ as DefineActionConfig, a as createRequestContext, b as createPermissionMiddleware, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as mergeResourceConfig, h as SeamedResourceConfig, i as createFastifyHandler, j as HOOK_PHASES, k as DEFAULT_UPDATE_METHOD, l as getEntityId, m as ResourceSeams, n as isFieldReadable, o as getControllerContext, p as AdapterLike, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as defineAction, w as DEFAULT_ID_FIELD, x as defineAggregation, y as createCrudRouter, z as RESERVED_QUERY_PARAMS } from "../index-D8MKYGDa.mjs";
3
+ export { AccessControl, type AccessControlConfig, type AdapterLike, type AggMeasureInput, type AggMeasureShorthand, type AggregationCacheConfig, type AggregationConfig, type AggregationDateRangeRequirement, type AggregationIndexHint, type AggregationMaterializedContext, type AggregationMaterializedResult, type AggregationRateLimit, type AggregationsMap, BaseController, type BaseControllerOptions, BaseCrudController, BodySanitizer, type BodySanitizerConfig, type BulkExt, BulkMixin, CRUD_OPERATIONS, type ControllerConfigurableOptions, type ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, type DefineActionConfig, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, type ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, type QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, type ResourceSeams, SYSTEM_FIELDS, type SeamedResourceConfig, type SlugExt, SlugMixin, type SoftDeleteExt, SoftDeleteMixin, type TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse };
@@ -1,5 +1,5 @@
1
1
  import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-DNre0wRg.mjs";
2
- import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-D-MHZ27_.mjs";
3
- import { a as defineResource, c as createCrudRouter, i as defineResourceVariants, l as createPermissionMiddleware, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as defineAction, t as getEntityId, u as defineAggregation } from "../core-CWhyGkiM.mjs";
4
- import { a as createCrudHandlers, c as getControllerContext, l as getControllerScope, o as createFastifyHandler, s as createRequestContext, u as sendControllerResponse } from "../actionPermissions-C9RMiIpc.mjs";
5
- export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
2
+ import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-C6FlRWxF.mjs";
3
+ import { a as defineResource, c as defineAction, d as defineAggregation, i as defineResourceVariants, l as createCrudRouter, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as mergeResourceConfig, t as getEntityId, u as createPermissionMiddleware } from "../core-b6fgFgnd.mjs";
4
+ import { a as createCrudHandlers, c as getControllerContext, l as getControllerScope, o as createFastifyHandler, s as createRequestContext, u as sendControllerResponse } from "../actionPermissions-ze70xXJi.mjs";
5
+ export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse };
@@ -2,16 +2,16 @@ import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-DN
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
4
4
  import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
5
- import { A as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-ChjYQpjn.mjs";
6
- import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-D-MHZ27_.mjs";
7
- import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, l as resolveRoutePreHandlers, n as buildRateLimitConfig, r as buildRouteConfig, s as buildCrudPermissionMw, t as tryRegisterRoute, u as buildAuthMiddleware } from "./registerRoute-DGSREwPw.mjs";
8
- import { F as applyFieldWritePermissions, L as resolveEffectiveRoles } from "./permissions-CgbOLJZG.mjs";
5
+ import { M as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-FhJ88TJE.mjs";
6
+ import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-C6FlRWxF.mjs";
7
+ import { a as selectPluginMw, c as buildRouteHooks, d as buildAuthMiddleware, i as resolveRouterPluginMw, l as resolveRoutePreHandlers, n as buildRateLimitConfig, p as buildArcDecorator, r as buildRouteConfig, s as buildCrudPermissionMw, t as tryRegisterRoute, u as routeHookOptions } from "./registerRoute-xHc42fwB.mjs";
8
+ import { I as applyFieldWritePermissions, R as resolveEffectiveRoles, w as requireAuth } from "./permissions-BJMuKMl7.mjs";
9
9
  import { t as getUserRoles } from "./types-XTApa8b6.mjs";
10
- import { t as applyPresets } from "./presets-DWsYQdbV.mjs";
11
- import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-U0MnO9vd.mjs";
10
+ import { t as applyPresets } from "./presets-gw0ciIUD.mjs";
11
+ import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-Ds5reSGs.mjs";
12
12
  import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
13
13
  import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
14
- import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-C9RMiIpc.mjs";
14
+ import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-ze70xXJi.mjs";
15
15
  import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
16
16
  //#region src/core/middlewares/fieldWrite.ts
17
17
  /**
@@ -151,7 +151,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
151
151
  const fieldWriteMw = buildFieldWritePreHandler(fieldPermissions, onFieldWriteDenied);
152
152
  if (fieldWriteMw) customPreHandlers.push(fieldWriteMw);
153
153
  }
154
- const preHandler = buildPreHandlerChain({
154
+ const hooks = buildRouteHooks({
155
155
  preAuth: route.preAuth ?? [],
156
156
  arcDecorator,
157
157
  authMw: buildAuthMiddleware(fastify, route.permissions),
@@ -165,7 +165,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
165
165
  method: route.method,
166
166
  url: route.path,
167
167
  schema,
168
- preHandler: preHandler.length > 0 ? preHandler : void 0,
168
+ ...routeHookOptions(hooks),
169
169
  handler: isStream ? async (request, reply) => {
170
170
  reply.raw.setHeader("Content-Type", "text/event-stream");
171
171
  reply.raw.setHeader("Cache-Control", "no-cache");
@@ -274,7 +274,7 @@ function createCrudRouter(fastify, controller, options = {}) {
274
274
  for (const spec of crudTable) {
275
275
  if (disabledRoutes.includes(spec.op)) continue;
276
276
  const permission = permissions[spec.op];
277
- const preHandler = buildPreHandlerChain({
277
+ const hooks = buildRouteHooks({
278
278
  arcDecorator,
279
279
  authMw: buildAuthMiddleware(fastify, permission),
280
280
  permissionMw: buildCrudPermissionMw(permission, resourceName, spec.op),
@@ -293,7 +293,7 @@ function createCrudRouter(fastify, controller, options = {}) {
293
293
  summary,
294
294
  ...spec.hasIdParams ? { params: idParamsSchema } : {}
295
295
  }, defaultSchemas[spec.op], schemas[spec.op]),
296
- preHandler: preHandler.length > 0 ? preHandler : void 0,
296
+ ...routeHookOptions(hooks),
297
297
  handler: handlers[spec.op],
298
298
  ...buildRouteConfig(rateLimitConfig, extensions)
299
299
  }, {
@@ -359,6 +359,58 @@ function defineAction(config) {
359
359
  };
360
360
  }
361
361
  //#endregion
362
+ //#region src/core/defineResource/seams.ts
363
+ /** Plain object test — literals only; class instances must replace, not merge. */
364
+ function isPlainObject(value) {
365
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
366
+ const proto = Object.getPrototypeOf(value);
367
+ return proto === Object.prototype || proto === null;
368
+ }
369
+ /** Recursive plain-object merge; nested arrays REPLACE (they're value lists). */
370
+ function mergePlain(base, next) {
371
+ const out = { ...base };
372
+ for (const [key, value] of Object.entries(next)) {
373
+ if (value === void 0) continue;
374
+ const existing = out[key];
375
+ out[key] = isPlainObject(existing) && isPlainObject(value) ? mergePlain(existing, value) : value;
376
+ }
377
+ return out;
378
+ }
379
+ /**
380
+ * Canonical slot-aware merge of a base resource config with host seams.
381
+ * Later seams win over earlier ones; `undefined` values never clobber.
382
+ *
383
+ * Merge semantics (value-driven — no per-slot registry to drift):
384
+ *
385
+ * - **Top-level arrays CONCAT** — `routes`, `middlewares`, `presets`,
386
+ * `routeGuards`, `disabledRoutes`: additive collections, base first.
387
+ * - **Plain objects merge recursively** — `permissions`, `actions`,
388
+ * `schemaOptions` (incl. `fieldRules`), `aggregations`, `customSchemas`,
389
+ * `fields`, `cache`, `events`: per-key composition. Arrays NESTED inside
390
+ * them replace (they're value lists — enums, sort specs), not concat.
391
+ * - **Everything else last-wins** — class instances (`adapter`,
392
+ * `controller`, `queryParser`), functions, scalars.
393
+ *
394
+ * The single `as` at the return is the one documented erasure for the
395
+ * {@link AdapterLike}-widened seam slots — inside the helper so no module
396
+ * author ever writes one (mirrors mongokit's one-cast-at-the-boundary rule).
397
+ */
398
+ function mergeResourceConfig(base, ...seams) {
399
+ const out = { ...base };
400
+ for (const seam of seams) {
401
+ if (!seam) continue;
402
+ for (const [key, value] of Object.entries(seam)) {
403
+ if (value === void 0) continue;
404
+ const existing = out[key];
405
+ if (Array.isArray(existing) && Array.isArray(value)) out[key] = [...existing, ...value];
406
+ else if (isPlainObject(existing) && isPlainObject(value)) out[key] = mergePlain(existing, value);
407
+ else if (Array.isArray(value)) out[key] = [...value];
408
+ else out[key] = value;
409
+ }
410
+ }
411
+ return out;
412
+ }
413
+ //#endregion
362
414
  //#region src/core/defineResource/controller.ts
363
415
  /**
364
416
  * Phase 4 — pick (or auto-create) the resource's controller.
@@ -652,7 +704,7 @@ function resolveIdField(config, repository) {
652
704
  * enabled but no adapter" (a confusing false-positive error).
653
705
  */
654
706
  function normalizeResourceConfig(config) {
655
- return resolveCrudAllowList(expandCustomRoutesOnly(expandReferenceData(config)));
707
+ return resolveCrudAllowList(expandHistory(expandCustomRoutesOnly(expandReferenceData(config))));
656
708
  }
657
709
  /**
658
710
  * Expand the `referenceData: true` shorthand. Reference data is the
@@ -706,6 +758,61 @@ function expandCustomRoutesOnly(config) {
706
758
  };
707
759
  }
708
760
  /**
761
+ * Expand the 2.22 `history: true` shorthand — the per-record change
762
+ * timeline. Injects `GET /:id/history` (audit-entry page for one
763
+ * document, newest-first per the audit store's contract) and implies
764
+ * `audit: true` so the rows the route reads actually get written.
765
+ *
766
+ * The gate defaults STRICTER than reads (history exposes before/after
767
+ * snapshots): resource `update` permission → `get` → route auth.
768
+ * `history: { permissions }` overrides. The flag is CONSUMED here
769
+ * (set to undefined) — later phases and the registry see only its
770
+ * expansion, same contract as `customRoutesOnly`.
771
+ */
772
+ function expandHistory(config) {
773
+ if (!config.history) return config;
774
+ const opts = config.history === true ? {} : config.history;
775
+ const permissions = opts.permissions ?? config.permissions?.update ?? config.permissions?.get ?? requireAuth();
776
+ const defaultLimit = opts.limit ?? 50;
777
+ const resourceName = config.name;
778
+ const historyRoute = {
779
+ method: "GET",
780
+ path: "/:id/history",
781
+ summary: `Change history for a ${resourceName} record (audit-backed timeline)`,
782
+ raw: true,
783
+ handler: async (request, reply) => {
784
+ const audit = request.server.audit;
785
+ if (!audit?.query || audit._noop === true) return reply.code(503).send({
786
+ code: "history.audit_unavailable",
787
+ message: `history for '${resourceName}' requires auditPlugin to be registered (and enabled)`,
788
+ status: 503
789
+ });
790
+ const { id } = request.params;
791
+ const q = request.query;
792
+ const limit = Math.min(Number(q.limit) > 0 ? Number(q.limit) : defaultLimit, 200);
793
+ const offset = Number(q.offset) > 0 ? Number(q.offset) : 0;
794
+ const entries = await audit.query({
795
+ resource: resourceName,
796
+ documentId: id,
797
+ limit,
798
+ offset
799
+ });
800
+ return reply.send({
801
+ data: entries,
802
+ limit,
803
+ offset
804
+ });
805
+ },
806
+ permissions
807
+ };
808
+ return {
809
+ ...config,
810
+ history: void 0,
811
+ audit: config.audit ?? true,
812
+ routes: [...config.routes ?? [], historyRoute]
813
+ };
814
+ }
815
+ /**
709
816
  * Normalise the 2.16 `crud:` positive-form allow-list into the canonical
710
817
  * `{ disabledRoutes, disableDefaultRoutes }` pair the rest of arc reads.
711
818
  *
@@ -1068,7 +1175,10 @@ function buildGeneratedCrudSchemas(openApi, customSchemas) {
1068
1175
  for (const [op, customSchema] of Object.entries(customSchemas)) {
1069
1176
  const key = op;
1070
1177
  const converted = convertRouteSchema(customSchema);
1071
- schemas[key] = schemas[key] ? deepMergeSchemas(schemas[key], converted) : converted;
1178
+ schemas[key] = schemas[key] ? {
1179
+ ...schemas[key],
1180
+ ...converted
1181
+ } : converted;
1072
1182
  }
1073
1183
  }
1074
1184
  return schemas;
@@ -1218,7 +1328,8 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
1218
1328
  * Fastify instance via `resource._sharedStateRegisteredOn`.
1219
1329
  */
1220
1330
  function buildResourcePlugin(resource) {
1221
- return async function resourcePlugin(fastify, _opts) {
1331
+ return async function resourcePlugin(fastify, opts) {
1332
+ await ensureSchemaConverter();
1222
1333
  const sharedRoot = fastify.server;
1223
1334
  const isFirstMount = !resource._sharedStateRegisteredOn.has(sharedRoot);
1224
1335
  if (isFirstMount) resource._sharedStateRegisteredOn.add(sharedRoot);
@@ -1254,6 +1365,7 @@ function buildResourcePlugin(resource) {
1254
1365
  fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares events (${eventNames}) but \`eventPlugin\` is not registered. The events will silently NOT be emitted on CRUD mutations. Either register the plugin (\`await fastify.register(eventPlugin, { transport: ... })\`) or remove the \`events\` declaration from the resource.`);
1255
1366
  }
1256
1367
  }
1368
+ if (opts?.arcMountRoutes === false) return;
1257
1369
  await fastify.register(async (instance) => {
1258
1370
  const typedInstance = instance;
1259
1371
  let schemas = buildGeneratedCrudSchemas(resource._registryMeta?.openApiSchemas, resource.customSchemas);
@@ -1283,7 +1395,7 @@ function buildResourcePlugin(resource) {
1283
1395
  extensions: resource.extensions
1284
1396
  });
1285
1397
  if (resource.actions && Object.keys(resource.actions).length > 0) {
1286
- const { createActionRouter } = await import("./createActionRouter-HlQso0jO.mjs").then((n) => n.n);
1398
+ const { createActionRouter } = await import("./createActionRouter-h1cEEaEX.mjs").then((n) => n.n);
1287
1399
  createActionRouter(typedInstance, {
1288
1400
  ...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
1289
1401
  resourceName: resource.name,
@@ -1298,7 +1410,7 @@ function buildResourcePlugin(resource) {
1298
1410
  });
1299
1411
  }
1300
1412
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
1301
- const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-5LzPzgw1.mjs"), import("./validate-C2mGgSXg.mjs").then((n) => n.r)]);
1413
+ const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-B1C0kvTL.mjs"), import("./validate-C2mGgSXg.mjs").then((n) => n.r)]);
1302
1414
  const repoForAgg = resource.controller?.repository ?? resource.repository;
1303
1415
  if (!adapterSupportsAggregate(repoForAgg)) {
1304
1416
  const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
@@ -2008,4 +2120,4 @@ function getEntityQuery(req) {
2008
2120
  return { [getEntityIdField(req)]: id };
2009
2121
  }
2010
2122
  //#endregion
2011
- export { defineResource as a, createCrudRouter as c, defineResourceVariants as i, createPermissionMiddleware as l, getEntityIdField as n, ResourceDefinition as o, getEntityQuery as r, defineAction as s, getEntityId as t, defineAggregation as u };
2123
+ export { defineResource as a, defineAction as c, defineAggregation as d, defineResourceVariants as i, createCrudRouter as l, getEntityIdField as n, ResourceDefinition as o, getEntityQuery as r, mergeResourceConfig as s, getEntityId as t, createPermissionMiddleware as u };
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import "./constants-DNre0wRg.mjs";
3
3
  import { f as createError } from "./errors-DkO8Nk6-.mjs";
4
- import { a as selectPluginMw, c as buildPreHandlerChain, d as buildAuthMiddlewareForPermissions, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, o as buildActionPermissionMw, r as buildRouteConfig, t as tryRegisterRoute } from "./registerRoute-DGSREwPw.mjs";
5
- import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-C9RMiIpc.mjs";
6
- import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-ajetw3Fv.mjs";
4
+ import { a as selectPluginMw, c as buildRouteHooks, f as buildAuthMiddlewareForPermissions, i as resolveRouterPluginMw, n as buildRateLimitConfig, o as buildActionPermissionMw, p as buildArcDecorator, r as buildRouteConfig, t as tryRegisterRoute, u as routeHookOptions } from "./registerRoute-xHc42fwB.mjs";
5
+ import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-ze70xXJi.mjs";
6
+ import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-B-6SPe7g.mjs";
7
7
  //#region src/core/createActionRouter.ts
8
8
  var createActionRouter_exports = /* @__PURE__ */ __exportAll({
9
9
  buildActionBodySchema: () => buildActionBodySchema,
@@ -108,19 +108,23 @@ function createActionRouter(fastify, config) {
108
108
  scopedError.meta = { action: submitted };
109
109
  throw scopedError;
110
110
  };
111
- const preHandler = [actionValidationFormatter, ...buildPreHandlerChain({
111
+ const hooks = buildRouteHooks({
112
112
  arcDecorator,
113
113
  authMw,
114
114
  permissionMw,
115
115
  pluginMw: selectPluginMw("POST", pluginMw),
116
116
  routeGuards
117
- })];
117
+ });
118
+ const preHandler = [actionValidationFormatter, ...hooks.preHandler];
118
119
  tryRegisterRoute(fastify, {
119
120
  method: "POST",
120
121
  url: urlPath,
121
122
  schema: routeSchema,
122
123
  attachValidation: true,
123
- preHandler: preHandler.length > 0 ? preHandler : void 0,
124
+ ...routeHookOptions({
125
+ onRequest: hooks.onRequest,
126
+ preHandler
127
+ }),
124
128
  ...buildRouteConfig(rateLimitConfig, extensions),
125
129
  handler: async (req, reply) => {
126
130
  const { action, ...data } = req.body;
@@ -1,5 +1,5 @@
1
1
  import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
2
- import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, u as buildAuthMiddleware } from "./registerRoute-DGSREwPw.mjs";
2
+ import { a as selectPluginMw, c as buildRouteHooks, d as buildAuthMiddleware, i as resolveRouterPluginMw, n as buildRateLimitConfig, p as buildArcDecorator, u as routeHookOptions } from "./registerRoute-xHc42fwB.mjs";
3
3
  import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
4
4
  import { t as buildAggregationHandler } from "./buildHandler-D_2ULWxZ.mjs";
5
5
  //#region src/core/aggregation/createAggregationRouter.ts
@@ -48,7 +48,7 @@ function registerOne(fastify, normalized, ctx) {
48
48
  throw new ForbiddenError(reason ?? "You do not have permission to access this aggregation.");
49
49
  }
50
50
  };
51
- const preHandler = buildPreHandlerChain({
51
+ const hooks = buildRouteHooks({
52
52
  arcDecorator,
53
53
  authMw,
54
54
  permissionMw,
@@ -73,7 +73,7 @@ function registerOne(fastify, normalized, ctx) {
73
73
  method: "GET",
74
74
  url: `/aggregations/${name}`,
75
75
  schema: routeSchema,
76
- preHandler: preHandler.length > 0 ? preHandler : void 0,
76
+ ...routeHookOptions(hooks),
77
77
  ...rateLimitConfig ? { config: rateLimitConfig } : {},
78
78
  handler: async (req, reply) => {
79
79
  try {