@classytic/arc 2.21.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 (94) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-C6FlRWxF.mjs} +1 -1
  2. package/dist/audit/index.d.mts +2 -2
  3. package/dist/auth/index.d.mts +30 -2
  4. package/dist/auth/index.mjs +5 -2
  5. package/dist/cli/commands/describe.d.mts +1 -1
  6. package/dist/cli/commands/docs.mjs +1 -1
  7. package/dist/cli/commands/init.mjs +1 -1
  8. package/dist/cli/index.mjs +1 -1
  9. package/dist/core/index.d.mts +2 -2
  10. package/dist/core/index.mjs +2 -2
  11. package/dist/{core-DZ6Xe1jR.mjs → core-b6fgFgnd.mjs} +123 -66
  12. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-h1cEEaEX.mjs} +9 -5
  13. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
  14. package/dist/{createApp-Bi61XxqU.mjs → createApp-Ci7yG5gv.mjs} +242 -56
  15. package/dist/docs/index.d.mts +1 -1
  16. package/dist/docs/index.mjs +1 -1
  17. package/dist/encryption/index.mjs +9 -5
  18. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
  19. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  20. package/dist/factory/index.d.mts +29 -8
  21. package/dist/factory/index.mjs +34 -3
  22. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -2
  23. package/dist/fields-D3lhgzhD.d.mts +121 -0
  24. package/dist/health-ScyjIVo5.mjs +202 -0
  25. package/dist/hooks/index.d.mts +1 -1
  26. package/dist/idempotency/index.mjs +2 -0
  27. package/dist/{types-DpxoNX5v.d.mts → index-BEu4wjbL.d.mts} +187 -66
  28. package/dist/{index-B5N-NaHs.d.mts → index-D8MKYGDa.d.mts} +3 -3
  29. package/dist/{index-Dtrj6XZk.d.mts → index-DbtgCOCj.d.mts} +1287 -1225
  30. package/dist/{index-B5PoNbfu.d.mts → index-ogIHvOjo.d.mts} +1 -1
  31. package/dist/index.d.mts +6 -5
  32. package/dist/index.mjs +3 -3
  33. package/dist/{init-0YhHkXGk.mjs → init-DleK3p9f.mjs} +118 -2
  34. package/dist/integrations/index.d.mts +1 -1
  35. package/dist/integrations/mcp/index.d.mts +3 -3
  36. package/dist/integrations/mcp/index.mjs +1 -1
  37. package/dist/integrations/mcp/testing.d.mts +1 -1
  38. package/dist/integrations/mcp/testing.mjs +1 -1
  39. package/dist/integrations/webhooks.d.mts +8 -4
  40. package/dist/integrations/webhooks.mjs +9 -5
  41. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  42. package/dist/logger/index.d.mts +26 -1
  43. package/dist/logger/index.mjs +39 -1
  44. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-ByA2tf6D.mjs} +6 -2
  45. package/dist/middleware/index.d.mts +1 -1
  46. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  47. package/dist/{openapi-Kyod_di6.mjs → openapi-BRpMZD7C.mjs} +2 -2
  48. package/dist/permissions/index.d.mts +3 -2
  49. package/dist/pipeline/index.d.mts +1 -1
  50. package/dist/plugins/index.d.mts +22 -5
  51. package/dist/plugins/index.mjs +11 -280
  52. package/dist/plugins/response-cache.mjs +1 -0
  53. package/dist/plugins/tracing-entry.mjs +7 -4
  54. package/dist/presets/filesUpload.d.mts +2 -2
  55. package/dist/presets/index.d.mts +1 -1
  56. package/dist/presets/multiTenant.d.mts +1 -1
  57. package/dist/presets/search.d.mts +2 -2
  58. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  59. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-xHc42fwB.mjs} +37 -17
  60. package/dist/registry/index.d.mts +2 -2
  61. package/dist/requestId-5BhTVzBv.mjs +113 -0
  62. package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
  63. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
  64. package/dist/scim/index.mjs +6 -7
  65. package/dist/scope/index.mjs +2 -76
  66. package/dist/scope-k15RXfcq.mjs +64 -0
  67. package/dist/testing/index.d.mts +83 -3
  68. package/dist/testing/index.mjs +70 -3
  69. package/dist/types/index.d.mts +2 -2
  70. package/dist/{types-DRj-Ut4n.d.mts → types-Bc42CUyk.d.mts} +1 -1
  71. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  72. package/dist/usage/index.d.mts +142 -0
  73. package/dist/usage/index.mjs +0 -0
  74. package/dist/utils/index.d.mts +40 -18
  75. package/dist/utils/index.mjs +2 -2
  76. package/dist/{utils-CO8a3B_1.mjs → utils-FhJ88TJE.mjs} +59 -13
  77. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-DLwklPlG.d.mts} +0 -5
  78. package/llms.txt +39 -0
  79. package/package.json +453 -447
  80. package/skills/arc/SKILL.md +14 -8
  81. package/skills/arc/references/api-reference.md +34 -0
  82. package/skills/arc/references/auth.md +21 -4
  83. package/skills/arc/references/mcp.md +1 -1
  84. package/skills/arc/references/production.md +44 -2
  85. package/skills/arc-code-review/SKILL.md +1 -1
  86. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  87. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  88. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  89. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  90. package/skills/arc-module-publishing/SKILL.md +2 -1
  91. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  92. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  93. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  94. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -2,7 +2,7 @@ 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 { F as simpleEqualityMatcher, _ as ArcQueryParser, r as scheduleBackground, t as getUserId } from "./utils-CO8a3B_1.mjs";
5
+ import { F as simpleEqualityMatcher, _ as ArcQueryParser, r as scheduleBackground, t as getUserId } from "./utils-FhJ88TJE.mjs";
6
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";
@@ -1,5 +1,5 @@
1
- import "../index-Dtrj6XZk.mjs";
2
- import { d as UserBase } from "../fields-BGqrAa_u.mjs";
1
+ import "../index-DbtgCOCj.mjs";
2
+ import { i as UserBase } from "../types-Csxga7d3.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { RepositoryLike } from "@classytic/repo-core/adapter";
5
5
  //#region src/audit/stores/interface.d.ts
@@ -1,5 +1,5 @@
1
- import { Ht as AuthPluginOptions, Vt as AuthHelpers } from "../index-Dtrj6XZk.mjs";
2
- import { c as PermissionCheck } from "../fields-BGqrAa_u.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
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";
@@ -84,6 +84,34 @@ interface BetterAuthAdapterOptions {
84
84
  * When true, includes the actual error message for debugging.
85
85
  */
86
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;
87
115
  }
88
116
  interface BetterAuthAdapterResult {
89
117
  /** Fastify plugin that registers catch-all auth routes */
@@ -1,7 +1,7 @@
1
1
  import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-DkO8Nk6-.mjs";
2
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,4 +1,4 @@
1
- import { V as ResourceDefinition, ht as RouteSchemaOptions, it as RateLimitConfig } from "../../index-Dtrj6XZk.mjs";
1
+ import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-DbtgCOCj.mjs";
2
2
  //#region src/cli/commands/describe.d.ts
3
3
  interface DescribedResource {
4
4
  name: string;
@@ -1,4 +1,4 @@
1
- import { t as buildOpenApiSpec } from "../../openapi-Kyod_di6.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-0YhHkXGk.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-0YhHkXGk.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,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-Dtrj6XZk.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-B5N-NaHs.mjs";
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
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-BnLYgE9A.mjs";
3
- import { a as mergeResourceConfig, c as defineAction, d as defineAggregation, i as defineResourceVariants, l as createCrudRouter, n as getEntityIdField, o as defineResource, r as getEntityQuery, s as ResourceDefinition, t as getEntityId, u as createPermissionMiddleware } from "../core-DZ6Xe1jR.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-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
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
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,13 +2,13 @@ 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 { M as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-CO8a3B_1.mjs";
6
- import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-BnLYgE9A.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-oGAl0oA8.mjs";
8
- import { I as applyFieldWritePermissions, R as resolveEffectiveRoles } from "./permissions-BJMuKMl7.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
10
  import { t as applyPresets } from "./presets-gw0ciIUD.mjs";
11
- import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-U0MnO9vd.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
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";
@@ -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
  *
@@ -1221,7 +1328,8 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
1221
1328
  * Fastify instance via `resource._sharedStateRegisteredOn`.
1222
1329
  */
1223
1330
  function buildResourcePlugin(resource) {
1224
- return async function resourcePlugin(fastify, _opts) {
1331
+ return async function resourcePlugin(fastify, opts) {
1332
+ await ensureSchemaConverter();
1225
1333
  const sharedRoot = fastify.server;
1226
1334
  const isFirstMount = !resource._sharedStateRegisteredOn.has(sharedRoot);
1227
1335
  if (isFirstMount) resource._sharedStateRegisteredOn.add(sharedRoot);
@@ -1257,6 +1365,7 @@ function buildResourcePlugin(resource) {
1257
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.`);
1258
1366
  }
1259
1367
  }
1368
+ if (opts?.arcMountRoutes === false) return;
1260
1369
  await fastify.register(async (instance) => {
1261
1370
  const typedInstance = instance;
1262
1371
  let schemas = buildGeneratedCrudSchemas(resource._registryMeta?.openApiSchemas, resource.customSchemas);
@@ -1286,7 +1395,7 @@ function buildResourcePlugin(resource) {
1286
1395
  extensions: resource.extensions
1287
1396
  });
1288
1397
  if (resource.actions && Object.keys(resource.actions).length > 0) {
1289
- const { createActionRouter } = await import("./createActionRouter-BWMDW-dL.mjs").then((n) => n.n);
1398
+ const { createActionRouter } = await import("./createActionRouter-h1cEEaEX.mjs").then((n) => n.n);
1290
1399
  createActionRouter(typedInstance, {
1291
1400
  ...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
1292
1401
  resourceName: resource.name,
@@ -1301,7 +1410,7 @@ function buildResourcePlugin(resource) {
1301
1410
  });
1302
1411
  }
1303
1412
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
1304
- const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-l5mNdYrZ.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)]);
1305
1414
  const repoForAgg = resource.controller?.repository ?? resource.repository;
1306
1415
  if (!adapterSupportsAggregate(repoForAgg)) {
1307
1416
  const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
@@ -1913,58 +2022,6 @@ function defineResource(config) {
1913
2022
  return resource;
1914
2023
  }
1915
2024
  //#endregion
1916
- //#region src/core/defineResource/seams.ts
1917
- /** Plain object test — literals only; class instances must replace, not merge. */
1918
- function isPlainObject(value) {
1919
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1920
- const proto = Object.getPrototypeOf(value);
1921
- return proto === Object.prototype || proto === null;
1922
- }
1923
- /** Recursive plain-object merge; nested arrays REPLACE (they're value lists). */
1924
- function mergePlain(base, next) {
1925
- const out = { ...base };
1926
- for (const [key, value] of Object.entries(next)) {
1927
- if (value === void 0) continue;
1928
- const existing = out[key];
1929
- out[key] = isPlainObject(existing) && isPlainObject(value) ? mergePlain(existing, value) : value;
1930
- }
1931
- return out;
1932
- }
1933
- /**
1934
- * Canonical slot-aware merge of a base resource config with host seams.
1935
- * Later seams win over earlier ones; `undefined` values never clobber.
1936
- *
1937
- * Merge semantics (value-driven — no per-slot registry to drift):
1938
- *
1939
- * - **Top-level arrays CONCAT** — `routes`, `middlewares`, `presets`,
1940
- * `routeGuards`, `disabledRoutes`: additive collections, base first.
1941
- * - **Plain objects merge recursively** — `permissions`, `actions`,
1942
- * `schemaOptions` (incl. `fieldRules`), `aggregations`, `customSchemas`,
1943
- * `fields`, `cache`, `events`: per-key composition. Arrays NESTED inside
1944
- * them replace (they're value lists — enums, sort specs), not concat.
1945
- * - **Everything else last-wins** — class instances (`adapter`,
1946
- * `controller`, `queryParser`), functions, scalars.
1947
- *
1948
- * The single `as` at the return is the one documented erasure for the
1949
- * {@link AdapterLike}-widened seam slots — inside the helper so no module
1950
- * author ever writes one (mirrors mongokit's one-cast-at-the-boundary rule).
1951
- */
1952
- function mergeResourceConfig(base, ...seams) {
1953
- const out = { ...base };
1954
- for (const seam of seams) {
1955
- if (!seam) continue;
1956
- for (const [key, value] of Object.entries(seam)) {
1957
- if (value === void 0) continue;
1958
- const existing = out[key];
1959
- if (Array.isArray(existing) && Array.isArray(value)) out[key] = [...existing, ...value];
1960
- else if (isPlainObject(existing) && isPlainObject(value)) out[key] = mergePlain(existing, value);
1961
- else if (Array.isArray(value)) out[key] = [...value];
1962
- else out[key] = value;
1963
- }
1964
- }
1965
- return out;
1966
- }
1967
- //#endregion
1968
2025
  //#region src/core/defineResourceVariants.ts
1969
2026
  /**
1970
2027
  * Define multiple resources from a shared base config and per-variant overrides.
@@ -2063,4 +2120,4 @@ function getEntityQuery(req) {
2063
2120
  return { [getEntityIdField(req)]: id };
2064
2121
  }
2065
2122
  //#endregion
2066
- export { mergeResourceConfig as a, defineAction as c, defineAggregation as d, defineResourceVariants as i, createCrudRouter as l, getEntityIdField as n, defineResource as o, getEntityQuery as r, ResourceDefinition as s, getEntityId as t, createPermissionMiddleware 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-oGAl0oA8.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
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-ajetw3Fv.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-oGAl0oA8.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 {