@classytic/arc 2.16.0 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +22 -2
  2. package/dist/{BaseController-DlCCTIxJ.mjs → BaseController-Dmeh4Y_y.mjs} +5 -47
  3. package/dist/{QueryCache-SvmT_9ti.d.mts → QueryCache-8YmAQ-zW.d.mts} +1 -1
  4. package/dist/{ResourceRegistry-f48hFk3m.mjs → ResourceRegistry-CzSzVK1m.mjs} +3 -0
  5. package/dist/audit/index.d.mts +1 -1
  6. package/dist/audit/index.mjs +2 -1
  7. package/dist/auth/index.d.mts +4 -4
  8. package/dist/auth/index.mjs +2 -2
  9. package/dist/auth/redis-session.d.mts +1 -1
  10. package/dist/auth/redis-session.mjs +28 -2
  11. package/dist/{buildHandler-BZX6zzDM.mjs → buildHandler-C_SkBNF3.mjs} +6 -4
  12. package/dist/cache/index.d.mts +3 -3
  13. package/dist/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/describe.mjs +1 -1
  15. package/dist/cli/commands/docs.mjs +2 -2
  16. package/dist/cli/commands/generate.d.mts +1 -1
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.d.mts +1 -1
  19. package/dist/cli/commands/init.mjs +1 -1
  20. package/dist/cli/commands/introspect.mjs +1 -1
  21. package/dist/cli/index.d.mts +2 -2
  22. package/dist/cli/index.mjs +2 -2
  23. package/dist/core/index.d.mts +2 -2
  24. package/dist/core/index.mjs +3 -3
  25. package/dist/{core-DBJ_j6rX.mjs → core-DYJvu7aI.mjs} +174 -31
  26. package/dist/{createActionRouter-DUpN3Dd1.mjs → createActionRouter-BHUMb1-y.mjs} +6 -3
  27. package/dist/{createAggregationRouter-Dq-TUCuY.mjs → createAggregationRouter-BYVvhfc5.mjs} +3 -3
  28. package/dist/{createApp-DNccuhyI.mjs → createApp-CzCEEsEm.mjs} +9 -9
  29. package/dist/{defineEvent-DRwY0fYm.mjs → defineEvent-NPKUJRt8.mjs} +1 -1
  30. package/dist/docs/index.d.mts +2 -2
  31. package/dist/docs/index.mjs +2 -2
  32. package/dist/{elevation-Dci0AYLT.mjs → elevation-6mfTLBC-.mjs} +1 -1
  33. package/dist/{errorHandler-DpoXQHZ9.mjs → errorHandler-wQejMBT3.mjs} +45 -4
  34. package/dist/{eventPlugin-C2cGqtRO.mjs → eventPlugin-CnJCUyM0.mjs} +1 -1
  35. package/dist/{eventPlugin-CtHC_av1.d.mts → eventPlugin-DjEx_J3j.d.mts} +1 -1
  36. package/dist/events/index.d.mts +3 -3
  37. package/dist/events/index.mjs +5 -5
  38. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  39. package/dist/events/transports/redis-stream-entry.mjs +34 -1
  40. package/dist/events/transports/redis.d.mts +1 -1
  41. package/dist/events/transports/redis.mjs +17 -1
  42. package/dist/factory/index.d.mts +2 -2
  43. package/dist/factory/index.mjs +4 -3
  44. package/dist/fastify-augmentation-7LIR0nDv.d.mts +42 -0
  45. package/dist/{fields-Anj0xdih.d.mts → fields-DqZLp5d6.d.mts} +1 -1
  46. package/dist/hooks/index.d.mts +1 -1
  47. package/dist/idempotency/index.d.mts +2 -2
  48. package/dist/idempotency/index.mjs +21 -5
  49. package/dist/idempotency/redis.d.mts +1 -1
  50. package/dist/{index-3oIimXQn.d.mts → index-3L-adFJO.d.mts} +3 -3
  51. package/dist/{index-CkW0flkU.d.mts → index-C1JSBan-.d.mts} +256 -8
  52. package/dist/{index-B-ulKx5P.d.mts → index-CruZir1o.d.mts} +36 -16
  53. package/dist/index.d.mts +5 -5
  54. package/dist/index.mjs +5 -5
  55. package/dist/integrations/event-gateway.d.mts +2 -2
  56. package/dist/integrations/event-gateway.mjs +1 -1
  57. package/dist/integrations/index.d.mts +2 -2
  58. package/dist/integrations/jobs.d.mts +36 -1
  59. package/dist/integrations/jobs.mjs +82 -9
  60. package/dist/integrations/mcp/index.d.mts +67 -5
  61. package/dist/integrations/mcp/index.mjs +93 -2
  62. package/dist/integrations/mcp/testing.d.mts +1 -1
  63. package/dist/integrations/mcp/testing.mjs +1 -1
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +2 -1
  67. package/dist/{loadResourcesFromEntry-BLMEI2Xa.mjs → loadResourcesFromEntry-DCjYMgfC.mjs} +1 -1
  68. package/dist/{mcpPlugin-7vGV51ED.mjs → mcpPlugin-hZE00H74.mjs} +114 -11
  69. package/dist/middleware/index.d.mts +1 -1
  70. package/dist/middleware/index.mjs +1 -1
  71. package/dist/{multipartBody-BOvVSVCD.mjs → multipartBody-VjVWmZl8.mjs} +2 -1
  72. package/dist/{openapi-34T9yNwd.mjs → openapi-DKDs9NfD.mjs} +37 -10
  73. package/dist/permissions/index.d.mts +2 -2
  74. package/dist/permissions/index.mjs +2 -2
  75. package/dist/{permissions-CTxMrreC.mjs → permissions-B1Qy55fg.mjs} +169 -169
  76. package/dist/pipeline/index.d.mts +1 -1
  77. package/dist/plugins/index.d.mts +5 -59
  78. package/dist/plugins/index.mjs +7 -7
  79. package/dist/plugins/tracing-entry.d.mts +1 -1
  80. package/dist/plugins/tracing-entry.mjs +1 -1
  81. package/dist/preloadResources-BOXzPhKU.mjs +45 -0
  82. package/dist/presets/filesUpload.d.mts +4 -4
  83. package/dist/presets/filesUpload.mjs +2 -2
  84. package/dist/presets/index.d.mts +1 -1
  85. package/dist/presets/index.mjs +1 -1
  86. package/dist/presets/multiTenant.d.mts +1 -1
  87. package/dist/presets/search.d.mts +2 -2
  88. package/dist/presets/search.mjs +1 -1
  89. package/dist/{presets-C9BE6WaZ.mjs → presets-BYbPobbF.mjs} +5 -3
  90. package/dist/{queryCachePlugin-Biqzfbi5.d.mts → queryCachePlugin-Do4P_WBC.d.mts} +2 -2
  91. package/dist/{redis-Cyzrz6SX.d.mts → redis-cpqVb85w.d.mts} +1 -1
  92. package/dist/{redis-stream-DT-YjzrB.d.mts → redis-stream-BuiAS3aE.d.mts} +1 -1
  93. package/dist/registry/index.d.mts +30 -8
  94. package/dist/registry/index.mjs +2 -2
  95. package/dist/{registry-BBE23CDj.mjs → registry-BrlHkrRS.mjs} +14 -8
  96. package/dist/{routerShared-CZV5aabX.mjs → routerShared-nCyJGOKX.mjs} +164 -17
  97. package/dist/scope/index.d.mts +2 -2
  98. package/dist/scope/index.mjs +2 -2
  99. package/dist/testing/index.d.mts +2 -25
  100. package/dist/testing/index.mjs +2 -45
  101. package/dist/testing/storageContract.d.mts +1 -1
  102. package/dist/types/index.d.mts +4 -4
  103. package/dist/types/storage.d.mts +1 -1
  104. package/dist/{types-D-fYtKjb.d.mts → types-BRmS8TNa.d.mts} +34 -11
  105. package/dist/{types-BsJMEQ4D.d.mts → types-CoMjNmkF.d.mts} +12 -1
  106. package/dist/utils/index.d.mts +83 -5
  107. package/dist/utils/index.mjs +2 -2
  108. package/dist/{utils-DC5ycPfr.mjs → utils-CvD3glpM.mjs} +168 -3
  109. package/dist/{validate-By96rH0r.mjs → validate-DFIP2VrM.mjs} +65 -3
  110. package/dist/{versioning-ZwX9tmbS.d.mts → versioning-5H4930tj.d.mts} +1 -1
  111. package/package.json +3 -3
  112. package/skills/arc/SKILL.md +43 -1
  113. package/skills/arc/references/api-reference.md +2 -0
  114. package/skills/arc/references/mcp.md +30 -1
  115. /package/dist/{EventTransport-C-2oAHtw.d.mts → EventTransport-B1GFiYUy.d.mts} +0 -0
  116. /package/dist/{EventTransport-Hxvv5QQz.mjs → EventTransport-ClEDfiMS.mjs} +0 -0
  117. /package/dist/{caching-TeHE8G-v.mjs → caching-8MfrrUYU.mjs} +0 -0
  118. /package/dist/{elevation-0YBpa663.d.mts → elevation-DvwtjDwu.d.mts} +0 -0
  119. /package/dist/{errorHandler-mHuyWzZE.d.mts → errorHandler-DuqTqeqn.d.mts} +0 -0
  120. /package/dist/{errors-C1lX_jlm.d.mts → errors-4ZloYFvm.d.mts} +0 -0
  121. /package/dist/{externalPaths-DFg-2KTp.d.mts → externalPaths-ClykOQqa.d.mts} +0 -0
  122. /package/dist/{generate-CYac-OLv.mjs → generate-CcM24hoq.mjs} +0 -0
  123. /package/dist/{generate-BWFwgcCM.d.mts → generate-cRnsafMS.d.mts} +0 -0
  124. /package/dist/{init-Dv71MsJr.d.mts → init-D-vrTZ1G.d.mts} +0 -0
  125. /package/dist/{init-HDvoO9L5.mjs → init-DYzfniuX.mjs} +0 -0
  126. /package/dist/{interface-CH0OQudo.d.mts → interface-BVkZsvEe.d.mts} +0 -0
  127. /package/dist/{interface-NwJ_qPlY.d.mts → interface-DoE6UoC2.d.mts} +0 -0
  128. /package/dist/{loadResources-ChQEj8ih.mjs → loadResources-fol6JuvW.mjs} +0 -0
  129. /package/dist/{metrics-TuOmguhi.mjs → metrics-B-PU4-Yu.mjs} +0 -0
  130. /package/dist/{replyHelpers-C-gD32oF.mjs → replyHelpers-boS5Rx26.mjs} +0 -0
  131. /package/dist/{schemaIR-Ctc89DSn.mjs → schemaIR-DAOvBWgT.mjs} +0 -0
  132. /package/dist/{sessionManager-BqFegc0W.d.mts → sessionManager-DlmaHUAl.d.mts} +0 -0
  133. /package/dist/{sse-BY6sTy4P.mjs → sse-jh5OqFjR.mjs} +0 -0
  134. /package/dist/{storage-D2KZJAmn.d.mts → storage-HcAgP5Ni.d.mts} +0 -0
  135. /package/dist/{store-helpers-B0sunfZZ.mjs → store-helpers-5JgaxieH.mjs} +0 -0
  136. /package/dist/{tracing-Dm8n7Cnn.d.mts → tracing-bRqqUxFO.d.mts} +0 -0
  137. /package/dist/{types-DVfpSfx2.d.mts → types-CfapW2t8.d.mts} +0 -0
  138. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  139. /package/dist/{versioning-B6mimogM.mjs → versioning-CuIwUQZ2.mjs} +0 -0
  140. /package/dist/{websocket-BkjeGZRn.d.mts → websocket-BJ0w-3Dt.d.mts} +0 -0
@@ -1,11 +1,11 @@
1
1
  import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-TrJVIJl0.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
- import { A as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-DC5ycPfr.mjs";
4
- import { t as BaseController } from "./BaseController-DlCCTIxJ.mjs";
5
- import { t as applyPresets } from "./presets-C9BE6WaZ.mjs";
3
+ import { N as assertValidConfig, f as getDefaultCrudSchemas, i as pipeUIMessageStreamToReply, r as isReadableStream } from "./utils-CvD3glpM.mjs";
4
+ import { t as BaseController } from "./BaseController-Dmeh4Y_y.mjs";
5
+ import { t as applyPresets } from "./presets-BYbPobbF.mjs";
6
6
  import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-De34B1ZG.mjs";
7
7
  import { t as hasEvents } from "./typeGuards-BzkXkvVv.mjs";
8
- import { b as buildRequestScopeProjection, c as buildPreHandlerChain, d as resolveRoutePreHandlers, f as resolveRouterPluginMw, g as createRequestContext, h as createFastifyHandler, i as buildAuthMiddleware, l as buildRateLimitConfig, m as createCrudHandlers, o as buildCrudPermissionMw, p as selectPluginMw, r as buildArcDecorator, s as buildPipelineHandler, u as resolvePipelineSteps } from "./routerShared-CZV5aabX.mjs";
8
+ import { C as buildRequestScopeProjection, _ as createCrudHandlers, c as buildPipelineHandler, d as methodCarriesBody, f as resolvePipelineSteps, g as tryRegisterRoute, h as selectPluginMw, i as buildAuthMiddleware, l as buildPreHandlerChain, m as resolveRouterPluginMw, o as buildCrudPermissionMw, p as resolveRoutePreHandlers, r as buildArcDecorator, s as buildFieldWritePreHandler, u as buildRateLimitConfig, v as createFastifyHandler, y as createRequestContext } from "./routerShared-nCyJGOKX.mjs";
9
9
  import { t as pluralize } from "./pluralize-B9M8xvy-.mjs";
10
10
  import { t as resolveActionPermission } from "./actionPermissions-Bjmvn7Eb.mjs";
11
11
  //#region src/core/aggregation/defineAggregation.ts
@@ -60,7 +60,7 @@ function defineAggregation(config) {
60
60
  * `wrapHandler` is derived inline from `!route.raw`.
61
61
  */
62
62
  function createCustomRoutes(fastify, routes, controller, options) {
63
- const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw } = options;
63
+ const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw, fieldPermissions, onFieldWriteDenied } = options;
64
64
  for (const route of routes) {
65
65
  const routeWithRefs = route;
66
66
  const hasHandler = route.handler !== void 0;
@@ -103,6 +103,10 @@ function createCustomRoutes(fastify, routes, controller, options) {
103
103
  if (!tenantScopeMw || tenantScopeMw.length === 0) throw new Error(`Route ${route.method} ${route.path}: \`tenantScope: true\` requires a multi-tenant preset. Add \`multiTenantPreset()\` (or \`flexibleMultiTenantPreset()\`) to the resource's \`presets\`, or remove the \`tenantScope\` flag from this route.`);
104
104
  customPreHandlers.unshift(...tenantScopeMw);
105
105
  }
106
+ if (route.raw !== true && route.fieldWrite !== false && methodCarriesBody(route.method)) {
107
+ const fieldWriteMw = buildFieldWritePreHandler(fieldPermissions, onFieldWriteDenied);
108
+ if (fieldWriteMw) customPreHandlers.push(fieldWriteMw);
109
+ }
106
110
  const preHandler = buildPreHandlerChain({
107
111
  preAuth: route.preAuth ?? [],
108
112
  arcDecorator,
@@ -113,7 +117,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
113
117
  customMws: customPreHandlers
114
118
  });
115
119
  const isStream = route.streamResponse === true;
116
- fastify.route({
120
+ tryRegisterRoute(fastify, {
117
121
  method: route.method,
118
122
  url: route.path,
119
123
  schema,
@@ -122,9 +126,14 @@ function createCustomRoutes(fastify, routes, controller, options) {
122
126
  reply.raw.setHeader("Content-Type", "text/event-stream");
123
127
  reply.raw.setHeader("Cache-Control", "no-cache");
124
128
  reply.raw.setHeader("Connection", "keep-alive");
125
- return handler(request, reply);
129
+ const result = await handler(request, reply);
130
+ if (isReadableStream(result)) await pipeUIMessageStreamToReply(reply, result);
131
+ return result;
126
132
  } : handler,
127
133
  ...rateLimitConfig ? { config: rateLimitConfig } : {}
134
+ }, {
135
+ resourceName,
136
+ op: opName
128
137
  });
129
138
  }
130
139
  }
@@ -138,7 +147,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
138
147
  * @param options - Router configuration
139
148
  */
140
149
  function createCrudRouter(fastify, controller, options = {}) {
141
- const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, updateMethod = DEFAULT_UPDATE_METHOD, idField } = options;
150
+ const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, onFieldWriteDenied, updateMethod = DEFAULT_UPDATE_METHOD, idField } = options;
142
151
  const rateLimitConfig = buildRateLimitConfig(rateLimit);
143
152
  const resourceHasQueryCache = fastify.hasDecorator("queryCache") && controller && typeof controller._cacheConfig !== "undefined" && controller._cacheConfig !== void 0;
144
153
  const pluginMw = resolveRouterPluginMw(fastify, Boolean(resourceHasQueryCache));
@@ -229,7 +238,7 @@ function createCrudRouter(fastify, controller, options = {}) {
229
238
  const methodsToRegister = spec.op === "update" ? updateMethod === "both" ? ["PUT", "PATCH"] : [updateMethod] : [spec.method];
230
239
  for (const method of methodsToRegister) {
231
240
  const summary = spec.op === "update" ? `${method === "PUT" ? "Replace" : "Update"} ${tag}` : spec.summary;
232
- fastify.route({
241
+ tryRegisterRoute(fastify, {
233
242
  method,
234
243
  url: spec.url,
235
244
  schema: buildSchema({
@@ -240,6 +249,9 @@ function createCrudRouter(fastify, controller, options = {}) {
240
249
  preHandler: preHandler.length > 0 ? preHandler : void 0,
241
250
  handler: handlers[spec.op],
242
251
  ...rateLimitConfig ? { config: rateLimitConfig } : {}
252
+ }, {
253
+ resourceName,
254
+ op: spec.op
243
255
  });
244
256
  }
245
257
  }
@@ -252,7 +264,9 @@ function createCrudRouter(fastify, controller, options = {}) {
252
264
  pluginMw,
253
265
  pipeline,
254
266
  routeGuards,
255
- tenantScopeMw
267
+ tenantScopeMw,
268
+ fieldPermissions,
269
+ onFieldWriteDenied
256
270
  });
257
271
  }
258
272
  function buildCrudHandlers(ctrl, pipeline, resourceName) {
@@ -442,11 +456,14 @@ function buildBaseController(resolvedConfig, adapter, repository) {
442
456
  const limitProp = qp.getQuerySchema()?.properties?.limit;
443
457
  if (limitProp?.maximum) maxLimitFromParser = limitProp.maximum;
444
458
  }
459
+ const resourceConfigMaxLimit = resolvedConfig.maxLimit;
460
+ const resourceConfigDefaultLimit = resolvedConfig.defaultLimit;
445
461
  return new BaseController(repository, {
446
462
  resourceName: resolvedConfig.name,
447
463
  schemaOptions: resolvedConfig.schemaOptions,
448
464
  queryParser: qp,
449
- maxLimit: maxLimitFromParser,
465
+ maxLimit: resourceConfigMaxLimit ?? maxLimitFromParser,
466
+ ...resourceConfigDefaultLimit !== void 0 ? { defaultLimit: resourceConfigDefaultLimit } : {},
450
467
  tenantField: resolvedConfig.tenantField,
451
468
  idField: resolvedConfig.idField,
452
469
  ...resolvedConfig.defaultSort !== void 0 ? { defaultSort: resolvedConfig.defaultSort } : {},
@@ -1033,10 +1050,21 @@ function buildResourcePlugin(resource) {
1033
1050
  handler: hook.handler,
1034
1051
  priority: hook.priority
1035
1052
  });
1036
- if (isFirstMount && resource.cache?.invalidateOn && fastify.registerCacheInvalidationRule) for (const [pattern, tags] of Object.entries(resource.cache.invalidateOn)) fastify.registerCacheInvalidationRule({
1053
+ if (isFirstMount && resource.cache?.invalidateOn) if (fastify.registerCacheInvalidationRule) for (const [pattern, tags] of Object.entries(resource.cache.invalidateOn)) fastify.registerCacheInvalidationRule({
1037
1054
  pattern,
1038
1055
  tags
1039
1056
  });
1057
+ else fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares \`cache.invalidateOn\` but \`queryCachePlugin\` is not registered. Cache-invalidation rules are silently ignored. Either register the plugin (\`await fastify.register(queryCachePlugin, ...)\`) or remove the \`cache.invalidateOn\` config to clean up the resource definition.`);
1058
+ if (isFirstMount && resource.audit) {
1059
+ if (!(fastify.hasDecorator("audit") && typeof fastify.audit?.create === "function" && fastify.audit?._noop !== true)) fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares \`audit: ${JSON.stringify(resource.audit)}\` but \`auditPlugin\` is not registered (or is registered with a noop logger). Audit events for this resource will NOT be recorded. Either register \`auditPlugin\` (\`await fastify.register(auditPlugin, { store: ... })\`) or remove the \`audit\` flag from the resource definition.`);
1060
+ }
1061
+ if (isFirstMount && resource.events && Object.keys(resource.events).length > 0) {
1062
+ const eventsDecor = fastify.events;
1063
+ if (!(eventsDecor != null && typeof eventsDecor.publish === "function")) {
1064
+ const eventNames = Object.keys(resource.events).join(", ");
1065
+ 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.`);
1066
+ }
1067
+ }
1040
1068
  await fastify.register(async (instance) => {
1041
1069
  const typedInstance = instance;
1042
1070
  let schemas = buildGeneratedCrudSchemas(resource._registryMeta?.openApiSchemas, resource.customSchemas);
@@ -1061,10 +1089,11 @@ function buildResourcePlugin(resource) {
1061
1089
  updateMethod: resource.updateMethod,
1062
1090
  pipe: resource.pipe,
1063
1091
  fields: resource.fields,
1092
+ onFieldWriteDenied: resource.onFieldWriteDenied,
1064
1093
  idField: resource.idField
1065
1094
  });
1066
1095
  if (resource.actions && Object.keys(resource.actions).length > 0) {
1067
- const { createActionRouter } = await import("./createActionRouter-DUpN3Dd1.mjs").then((n) => n.n);
1096
+ const { createActionRouter } = await import("./createActionRouter-BHUMb1-y.mjs").then((n) => n.n);
1068
1097
  createActionRouter(typedInstance, {
1069
1098
  ...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
1070
1099
  resourceName: resource.name,
@@ -1078,7 +1107,7 @@ function buildResourcePlugin(resource) {
1078
1107
  });
1079
1108
  }
1080
1109
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
1081
- const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-Dq-TUCuY.mjs"), import("./validate-By96rH0r.mjs").then((n) => n.i)]);
1110
+ const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-BYVvhfc5.mjs"), import("./validate-DFIP2VrM.mjs").then((n) => n.i)]);
1082
1111
  const repoForAgg = resource.controller?.repository ?? resource.repository;
1083
1112
  if (!adapterSupportsAggregate(repoForAgg)) {
1084
1113
  const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
@@ -1142,7 +1171,16 @@ var ResourceDefinition = class {
1142
1171
  updateMethod;
1143
1172
  pipe;
1144
1173
  fields;
1174
+ /**
1175
+ * Policy for field-write denials — `'reject'` (default, secure) raises 403
1176
+ * listing the denied fields; `'strip'` silently drops them. Honored by
1177
+ * both `BodySanitizer` (auto-CRUD) and `buildFieldWritePreHandler`
1178
+ * (custom routes) so the policy is consistent across all body-bearing
1179
+ * routes for the resource.
1180
+ */
1181
+ onFieldWriteDenied;
1145
1182
  cache;
1183
+ referenceData;
1146
1184
  /**
1147
1185
  * Per-resource MCP opt-out. `false` keeps the resource out of every
1148
1186
  * `mcpPlugin` registration regardless of the plugin's `expose` /
@@ -1174,9 +1212,37 @@ var ResourceDefinition = class {
1174
1212
  * `buildResourcePlugin` so the host's configured logger owns the
1175
1213
  * output — the framework never speaks to `console.*` from `src/`
1176
1214
  * outside of the CLI.
1215
+ *
1216
+ * Prefer the public {@link warnings} getter for programmatic access
1217
+ * (CI fail-on-warnings, lint integrations) — it returns a frozen
1218
+ * snapshot so callers can't accidentally mutate the diagnostic list.
1177
1219
  */
1178
1220
  _diagnostics;
1179
1221
  /**
1222
+ * Public view of {@link _diagnostics} for programmatic consumption.
1223
+ *
1224
+ * Returns the collected define-time diagnostics for this resource.
1225
+ * Empty when the resource validated cleanly. Useful for CI gates that
1226
+ * want to fail the build on any non-fatal warning:
1227
+ *
1228
+ * @example
1229
+ * ```ts
1230
+ * import { orderResource } from './resources/order.resource.js';
1231
+ * if (orderResource.warnings.length > 0) {
1232
+ * for (const w of orderResource.warnings) console.error(w.message);
1233
+ * process.exit(1);
1234
+ * }
1235
+ * ```
1236
+ *
1237
+ * **Scope.** Covers define-time only — diagnostics emitted at
1238
+ * first-mount (e.g. `cache.invalidateOn` without `queryCachePlugin`)
1239
+ * surface through `fastify.log.warn` instead and are not collected
1240
+ * here; they require a Fastify instance to detect.
1241
+ */
1242
+ get warnings() {
1243
+ return this._diagnostics ?? [];
1244
+ }
1245
+ /**
1180
1246
  * Per-host idempotency guard used by `buildResourcePlugin` to
1181
1247
  * skip duplicate shared-state writes when the same resource is
1182
1248
  * mounted at multiple prefixes (`/v1`, `/v2`). See the plugin
@@ -1209,8 +1275,10 @@ var ResourceDefinition = class {
1209
1275
  this.updateMethod = config.updateMethod;
1210
1276
  this.pipe = config.pipe;
1211
1277
  this.fields = config.fields;
1278
+ this.onFieldWriteDenied = config.onFieldWriteDenied;
1212
1279
  this.cache = config.cache;
1213
1280
  this.mcp = config.mcp !== false;
1281
+ this.referenceData = config.referenceData === true;
1214
1282
  this.tenantField = config.tenantField;
1215
1283
  this.onTenantDelete = config.onTenantDelete;
1216
1284
  this.resolvedTenantPurge = resolveTenantPurge({
@@ -1364,6 +1432,7 @@ function resolveOpenApiSchemas(resolvedConfig) {
1364
1432
  openApiSchemas = stripSystemManagedFromBodyRequired(openApiSchemas, resolvedConfig.schemaOptions);
1365
1433
  openApiSchemas = cleanLegacyObjectIdParams(openApiSchemas, resolvedConfig.idField);
1366
1434
  openApiSchemas = layerQueryParserListQuery(openApiSchemas, resolvedConfig.queryParser);
1435
+ openApiSchemas = applyResourcePaginationCaps(openApiSchemas, resolvedConfig);
1367
1436
  openApiSchemas = mergeUserOpenApiOverrides(openApiSchemas, resolvedConfig.openApiSchemas);
1368
1437
  if (openApiSchemas) openApiSchemas = convertOpenApiSchemas(openApiSchemas);
1369
1438
  return {
@@ -1435,6 +1504,45 @@ function layerQueryParserListQuery(openApiSchemas, queryParser) {
1435
1504
  };
1436
1505
  }
1437
1506
  /**
1507
+ * Apply resource-level `defaultLimit` / `maxLimit` (2.17.0) onto the
1508
+ * listQuery schema. Resource-level caps win over both the framework
1509
+ * default and the parser-derived value because they're the most-explicit
1510
+ * declaration — a host that wrote `defaultLimit: 1000` on the resource
1511
+ * means "this resource specifically returns up to 1000", regardless of
1512
+ * what the parser's default would have been. The `referenceData: true`
1513
+ * shorthand lifts both to 1000 so reference-data resources don't need
1514
+ * a custom queryParser just to get the caps right.
1515
+ *
1516
+ * Bootstraps a minimal listQuery slot when none exists (no queryParser
1517
+ * registered, no adapter-generated schema) so the AJV layer still gets
1518
+ * a `maximum:` and surfaces the cap in the 400 envelope via
1519
+ * `errorHandler.describePaginationCap()` instead of producing the
1520
+ * generic "Bad Request" we ate pre-2.17.0.
1521
+ */
1522
+ function applyResourcePaginationCaps(openApiSchemas, resolvedConfig) {
1523
+ const cfg = resolvedConfig;
1524
+ if (cfg.defaultLimit === void 0 && cfg.maxLimit === void 0) return openApiSchemas;
1525
+ const existing = openApiSchemas?.listQuery;
1526
+ const existingProps = existing?.properties ?? {};
1527
+ const patchedLimit = { ...existingProps.limit ?? {
1528
+ type: "integer",
1529
+ minimum: 1
1530
+ } };
1531
+ if (cfg.maxLimit !== void 0) patchedLimit.maximum = cfg.maxLimit;
1532
+ if (cfg.defaultLimit !== void 0) patchedLimit.default = cfg.defaultLimit;
1533
+ return {
1534
+ ...openApiSchemas,
1535
+ listQuery: {
1536
+ type: "object",
1537
+ ...existing ?? {},
1538
+ properties: {
1539
+ ...existingProps,
1540
+ limit: patchedLimit
1541
+ }
1542
+ }
1543
+ };
1544
+ }
1545
+ /**
1438
1546
  * Apply per-resource `openApiSchemas` overrides on top of the kit's
1439
1547
  * generated schemas. Shallow merge by slot — users who want field-level
1440
1548
  * surgery should compose at the schema-options layer before this point.
@@ -1547,24 +1655,8 @@ function validateActionsShape(config) {
1547
1655
  }
1548
1656
  //#endregion
1549
1657
  //#region src/core/defineResource.ts
1550
- /**
1551
- * `TDoc` is **unconstrained** at this layer. The previous `TDoc
1552
- * extends AnyRecord` bound leaked out of `BaseController`'s
1553
- * mixin-composition requirement into every host's adapter boundary:
1554
- * Mongoose's `HydratedDocument<T>`, Prisma's generated row types,
1555
- * and any domain interface without an explicit index signature all
1556
- * failed to satisfy `Record<string, unknown>` even though at runtime
1557
- * they ARE string-keyed objects. Hosts were forced to cast at every
1558
- * adapter (`as RepositoryLike<Record<string, unknown>>`) — a type
1559
- * escape with no runtime purpose, since arc's pipeline only reads
1560
- * known envelope fields.
1561
- *
1562
- * The cast moved inside `resolveOrAutoCreateController` where
1563
- * `BaseController<TDoc extends AnyRecord>` actually requires it.
1564
- * One internal boundary cast replaces N host-side casts.
1565
- */
1566
1658
  function defineResource(config) {
1567
- const normalisedConfig = resolveCrudAllowList(config);
1659
+ const normalisedConfig = resolveCrudAllowList(expandCustomRoutesOnly(expandReferenceData(config)));
1568
1660
  let diagnostics = [];
1569
1661
  if (!normalisedConfig.skipValidation) diagnostics = validateDefineResourceConfig(normalisedConfig);
1570
1662
  const repository = normalisedConfig.adapter?.repository;
@@ -1606,6 +1698,57 @@ function defineResource(config) {
1606
1698
  * doesn't false-positive "Data adapter required when CRUD routes are
1607
1699
  * enabled" on a host that explicitly opted CRUD out.
1608
1700
  */
1701
+ /**
1702
+ * Expand the `referenceData: true` shorthand. Reference data is the
1703
+ * recurring "small static catalogue" shape (currencies, plans,
1704
+ * pipeline stages, credential types) where callers want "fetch all"
1705
+ * semantics and aggressive caching — pre-2.17.0 hosts hand-wired
1706
+ * `crud` + `defaultLimit` + `maxLimit` + `cache` on every such
1707
+ * resource. The shorthand pins canonical defaults; explicit narrow
1708
+ * settings always win so a host can opt INTO mutations
1709
+ * (`referenceData: true, crud: { list: true, get: true, create: true }`)
1710
+ * or override the cache window without giving up the read-only +
1711
+ * fetch-all defaults.
1712
+ */
1713
+ function expandReferenceData(config) {
1714
+ if (config.referenceData !== true) return config;
1715
+ return {
1716
+ ...config,
1717
+ crud: config.crud ?? {
1718
+ list: true,
1719
+ get: true
1720
+ },
1721
+ defaultLimit: config.defaultLimit ?? 1e3,
1722
+ maxLimit: config.maxLimit ?? 1e3,
1723
+ cache: config.cache ?? {
1724
+ staleTime: 300,
1725
+ gcTime: 600
1726
+ }
1727
+ };
1728
+ }
1729
+ /**
1730
+ * Expand the `customRoutesOnly: true` shorthand into its three primitive
1731
+ * flags. The shorthand exists because hosts wiring a "service resource"
1732
+ * (custom routes only, no adapter, no auto-CRUD) had to remember to set
1733
+ * `disableDefaultRoutes` + `skipValidation` + `skipRegistry` in lockstep —
1734
+ * forgetting any one of them produced confusing errors ("controller
1735
+ * required when CRUD routes are enabled" for a resource that ships none).
1736
+ *
1737
+ * Explicit narrow settings always win — the shorthand only fills in
1738
+ * flags that were not set so power users can opt back into a specific
1739
+ * primitive (`customRoutesOnly: true, skipRegistry: false` keeps OpenAPI
1740
+ * docs while still skipping CRUD + validation).
1741
+ */
1742
+ function expandCustomRoutesOnly(config) {
1743
+ if (config.customRoutesOnly !== true) return config;
1744
+ return {
1745
+ ...config,
1746
+ customRoutesOnly: void 0,
1747
+ disableDefaultRoutes: config.disableDefaultRoutes ?? true,
1748
+ skipValidation: config.skipValidation ?? true,
1749
+ skipRegistry: config.skipRegistry ?? true
1750
+ };
1751
+ }
1609
1752
  function resolveCrudAllowList(config) {
1610
1753
  const { crud, disabledRoutes: legacyDisabled, disableDefaultRoutes: legacyDisableAll } = config;
1611
1754
  if (crud === void 0) return config;
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  import "./constants-TrJVIJl0.mjs";
3
3
  import { f as createError } from "./errors-j4aJm1Wg.mjs";
4
- import { a as buildAuthMiddlewareForPermissions, c as buildPreHandlerChain, f as resolveRouterPluginMw, l as buildRateLimitConfig, n as buildActionPipelineHandler, p as selectPluginMw, r as buildArcDecorator, t as buildActionPermissionMw, u as resolvePipelineSteps, y as sendControllerResponse } from "./routerShared-CZV5aabX.mjs";
5
- import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-Ctc89DSn.mjs";
4
+ import { S as sendControllerResponse, a as buildAuthMiddlewareForPermissions, f as resolvePipelineSteps, g as tryRegisterRoute, h as selectPluginMw, l as buildPreHandlerChain, m as resolveRouterPluginMw, n as buildActionPipelineHandler, r as buildArcDecorator, t as buildActionPermissionMw, u as buildRateLimitConfig } from "./routerShared-nCyJGOKX.mjs";
5
+ import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-DAOvBWgT.mjs";
6
6
  //#region src/core/createActionRouter.ts
7
7
  var createActionRouter_exports = /* @__PURE__ */ __exportAll({
8
8
  buildActionBodySchema: () => buildActionBodySchema,
@@ -114,7 +114,7 @@ function createActionRouter(fastify, config) {
114
114
  pluginMw: selectPluginMw("POST", pluginMw),
115
115
  routeGuards
116
116
  })];
117
- fastify.route({
117
+ tryRegisterRoute(fastify, {
118
118
  method: "POST",
119
119
  url: urlPath,
120
120
  schema: routeSchema,
@@ -152,6 +152,9 @@ function createActionRouter(fastify, config) {
152
152
  throw createError(statusCode, err.message || `Failed to execute '${action}' action`, { code: errorCode });
153
153
  }
154
154
  }
155
+ }, {
156
+ resourceName,
157
+ op: `action${urlPath}`
155
158
  });
156
159
  fastify.log.debug({
157
160
  actions: actionSubset,
@@ -1,7 +1,7 @@
1
1
  import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
2
- import { c as buildPreHandlerChain, f as resolveRouterPluginMw, i as buildAuthMiddleware, l as buildRateLimitConfig, p as selectPluginMw, r as buildArcDecorator } from "./routerShared-CZV5aabX.mjs";
3
- import { r as validateAggregations } from "./validate-By96rH0r.mjs";
4
- import { t as buildAggregationHandler } from "./buildHandler-BZX6zzDM.mjs";
2
+ import { h as selectPluginMw, i as buildAuthMiddleware, l as buildPreHandlerChain, m as resolveRouterPluginMw, r as buildArcDecorator, u as buildRateLimitConfig } from "./routerShared-nCyJGOKX.mjs";
3
+ import { r as validateAggregations } from "./validate-DFIP2VrM.mjs";
4
+ import { t as buildAggregationHandler } from "./buildHandler-C_SkBNF3.mjs";
5
5
  //#region src/core/aggregation/createAggregationRouter.ts
6
6
  /**
7
7
  * Register one Fastify route per aggregation. No-op when the map is
@@ -202,7 +202,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
202
202
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
203
203
  trackPlugin("arc-core");
204
204
  if (config.arcPlugins?.events !== false) {
205
- const { default: eventPlugin } = await import("./eventPlugin-C2cGqtRO.mjs").then((n) => n.n);
205
+ const { default: eventPlugin } = await import("./eventPlugin-CnJCUyM0.mjs").then((n) => n.n);
206
206
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
207
207
  await fastify.register(eventPlugin, {
208
208
  ...eventOpts,
@@ -239,7 +239,7 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
239
239
  trackPlugin("arc-graceful-shutdown");
240
240
  }
241
241
  if (config.arcPlugins?.caching) {
242
- const { default: cachingPlugin } = await import("./caching-TeHE8G-v.mjs").then((n) => n.r);
242
+ const { default: cachingPlugin } = await import("./caching-8MfrrUYU.mjs").then((n) => n.r);
243
243
  const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
244
244
  await fastify.register(cachingPlugin, opts);
245
245
  trackPlugin("arc-caching", opts);
@@ -256,19 +256,19 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
256
256
  }
257
257
  if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
258
258
  else {
259
- const { default: ssePlugin } = await import("./sse-BY6sTy4P.mjs").then((n) => n.r);
259
+ const { default: ssePlugin } = await import("./sse-jh5OqFjR.mjs").then((n) => n.r);
260
260
  const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
261
261
  await fastify.register(ssePlugin, opts);
262
262
  trackPlugin("arc-sse", opts);
263
263
  }
264
264
  if (config.arcPlugins?.metrics) {
265
- const { default: metricsPlugin } = await import("./metrics-TuOmguhi.mjs").then((n) => n.r);
265
+ const { default: metricsPlugin } = await import("./metrics-B-PU4-Yu.mjs").then((n) => n.r);
266
266
  const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
267
267
  await fastify.register(metricsPlugin, opts);
268
268
  trackPlugin("arc-metrics", opts);
269
269
  }
270
270
  if (config.arcPlugins?.versioning) {
271
- const { default: versioningPlugin } = await import("./versioning-B6mimogM.mjs").then((n) => n.r);
271
+ const { default: versioningPlugin } = await import("./versioning-CuIwUQZ2.mjs").then((n) => n.r);
272
272
  await fastify.register(versioningPlugin, config.arcPlugins.versioning);
273
273
  trackPlugin("arc-versioning", config.arcPlugins.versioning);
274
274
  }
@@ -337,7 +337,7 @@ async function registerAuth(fastify, config, trackPlugin) {
337
337
  */
338
338
  async function registerElevation(fastify, config, trackPlugin) {
339
339
  if (!config.elevation) return;
340
- const { elevationPlugin } = await import("./elevation-Dci0AYLT.mjs").then((n) => n.r);
340
+ const { elevationPlugin } = await import("./elevation-6mfTLBC-.mjs").then((n) => n.r);
341
341
  await fastify.register(elevationPlugin, config.elevation);
342
342
  trackPlugin("arc-elevation", config.elevation);
343
343
  fastify.log.debug("Elevation plugin enabled");
@@ -347,7 +347,7 @@ async function registerElevation(fastify, config, trackPlugin) {
347
347
  */
348
348
  async function registerErrorHandler(fastify, config, trackPlugin) {
349
349
  if (config.errorHandler === false) return;
350
- const { errorHandlerPlugin } = await import("./errorHandler-DpoXQHZ9.mjs").then((n) => n.r);
350
+ const { errorHandlerPlugin } = await import("./errorHandler-wQejMBT3.mjs").then((n) => n.r);
351
351
  const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
352
352
  await fastify.register(errorHandlerPlugin, errorOpts);
353
353
  trackPlugin("arc-error-handler", errorOpts);
@@ -448,7 +448,7 @@ async function registerResources(fastify, config) {
448
448
  let discoveryPath;
449
449
  let discoveryYieldedZero = false;
450
450
  if (resolvedResources === void 0 && config.resourceDir) {
451
- const { loadResources } = await import("./loadResources-ChQEj8ih.mjs").then((n) => n.n);
451
+ const { loadResources } = await import("./loadResources-fol6JuvW.mjs").then((n) => n.n);
452
452
  const { resolve, dirname } = await import("node:path");
453
453
  const { fileURLToPath } = await import("node:url");
454
454
  const rawDir = config.resourceDir;
@@ -874,7 +874,7 @@ async function createApp(options) {
874
874
  await registerErrorHandler(fastify, config, trackPlugin);
875
875
  await registerResources(fastify, config);
876
876
  if (config.replyHelpers) {
877
- const { replyHelpersPlugin } = await import("./replyHelpers-C-gD32oF.mjs").then((n) => n.n);
877
+ const { replyHelpersPlugin } = await import("./replyHelpers-boS5Rx26.mjs").then((n) => n.n);
878
878
  await fastify.register(replyHelpersPlugin);
879
879
  }
880
880
  if (config.serializeBigInt) {
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { n as createEvent } from "./EventTransport-Hxvv5QQz.mjs";
2
+ import { n as createEvent } from "./EventTransport-ClEDfiMS.mjs";
3
3
  //#region src/events/defineEvent.ts
4
4
  /**
5
5
  * defineEvent — Typed Event Definitions with Optional Schema Validation
@@ -1,5 +1,5 @@
1
- import { p as RegistryEntry } from "../index-CkW0flkU.mjs";
2
- import { t as ExternalOpenApiPaths } from "../externalPaths-DFg-2KTp.mjs";
1
+ import { p as RegistryEntry } from "../index-C1JSBan-.mjs";
2
+ import { t as ExternalOpenApiPaths } from "../externalPaths-ClykOQqa.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
5
5
  //#region src/docs/openapi/types.d.ts
@@ -1,5 +1,5 @@
1
- import { t as getUserRoles } from "../types-D57iXYb8.mjs";
2
- import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-34T9yNwd.mjs";
1
+ import { t as getUserRoles } from "../types-DV9WDfeg.mjs";
2
+ import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-DKDs9NfD.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/docs/scalar.ts
5
5
  const scalarPlugin = async (fastify, opts = {}) => {
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
- import { t as getUserRoles } from "./types-D57iXYb8.mjs";
3
+ import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
4
4
  import fp from "fastify-plugin";
5
5
  //#region src/scope/elevation.ts
6
6
  var elevation_exports = /* @__PURE__ */ __exportAll({
@@ -39,13 +39,48 @@ function fastifyValidationDetails(error) {
39
39
  return (error.validation ?? []).map((v) => {
40
40
  const missingProperty = v.params?.missingProperty;
41
41
  const path = v.instancePath?.replace(/^\//, "") || (typeof missingProperty === "string" ? missingProperty : void 0);
42
+ const meta = {};
43
+ if (v.keyword === "maximum" || v.keyword === "minimum" || v.keyword === "exclusiveMaximum" || v.keyword === "exclusiveMinimum") {
44
+ const bound = v.params?.limit;
45
+ if (typeof bound === "number") meta.bound = bound;
46
+ }
47
+ if (v.keyword === "enum") {
48
+ const allowed = v.params?.allowedValues;
49
+ if (Array.isArray(allowed)) meta.allowedValues = allowed;
50
+ }
42
51
  return {
43
52
  ...path ? { path } : {},
44
53
  code: v.keyword ?? "invalid",
45
- message: v.message || "Invalid value"
54
+ message: v.message || "Invalid value",
55
+ ...Object.keys(meta).length > 0 ? { meta } : {}
46
56
  };
47
57
  });
48
58
  }
59
+ /**
60
+ * Hoist a context-aware message + machine-readable `meta` for the
61
+ * common pagination-cap miss (`?limit=200` against a maxLimit=100
62
+ * resource). Pre-2.17.0 the wire envelope carried "Validation failed"
63
+ * + a detail with the AJV string; hosts had to grep the detail's
64
+ * message to surface the cap. Now the top-level message is
65
+ * "limit must be <= 100 (got 200)" AND `meta.cap` / `meta.received`
66
+ * are stamped on the contract for programmatic callers.
67
+ */
68
+ function describePaginationCap(error, details) {
69
+ if (!Array.isArray(error.validation) || error.validation.length === 0) return null;
70
+ const v = error.validation.find((entry) => (entry.instancePath === "/limit" || entry.instancePath === "/page") && (entry.keyword === "maximum" || entry.keyword === "minimum"));
71
+ if (!v) return null;
72
+ const field = v.instancePath === "/limit" ? "limit" : "page";
73
+ const bound = v.params?.limit;
74
+ if (typeof bound !== "number") return null;
75
+ const detail = details.find((d) => d.path === field);
76
+ return {
77
+ message: detail?.message ? `Query parameter '${field}' ${detail.message} (cap is ${bound})` : `Query parameter '${field}' violates bound ${bound}`,
78
+ meta: {
79
+ field,
80
+ cap: bound
81
+ }
82
+ };
83
+ }
49
84
  /** Map Mongoose `ValidationError.errors` → canonical `ErrorDetail[]`. */
50
85
  function mongooseValidationDetails(errors) {
51
86
  return Object.entries(errors).map(([field, e]) => ({
@@ -116,13 +151,19 @@ function classify(error, ctx) {
116
151
  const fastifyErr = error;
117
152
  if (Array.isArray(fastifyErr.validation)) {
118
153
  const arcInternalCode = typeof fastifyErr.code === "string" && fastifyErr.code.startsWith("arc.") ? fastifyErr.code : void 0;
119
- const message = arcInternalCode ? error.message || "Validation failed" : "Validation failed";
154
+ const details = fastifyValidationDetails(fastifyErr);
155
+ const paginationCap = arcInternalCode ? null : describePaginationCap(fastifyErr, details);
156
+ const message = paginationCap?.message ?? (arcInternalCode ? error.message || "Validation failed" : "Validation failed");
157
+ const mergedMeta = {
158
+ ...fastifyErr.meta ?? {},
159
+ ...paginationCap?.meta ?? {}
160
+ };
120
161
  return {
121
162
  code: arcInternalCode ?? "arc.validation_error",
122
163
  message,
123
164
  status: typeof fastifyErr.statusCode === "number" ? fastifyErr.statusCode : 400,
124
- details: fastifyValidationDetails(fastifyErr),
125
- ...fastifyErr.meta ? { meta: fastifyErr.meta } : {}
165
+ details,
166
+ ...Object.keys(mergedMeta).length > 0 ? { meta: mergedMeta } : {}
126
167
  };
127
168
  }
128
169
  if (typeof fastifyErr.statusCode === "number") return {
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { d as createDomainError } from "./errors-j4aJm1Wg.mjs";
4
4
  import { t as requestContext } from "./requestContext-SSaaTgW8.mjs";
5
- import { n as createEvent, t as MemoryEventTransport } from "./EventTransport-Hxvv5QQz.mjs";
5
+ import { n as createEvent, t as MemoryEventTransport } from "./EventTransport-ClEDfiMS.mjs";
6
6
  import fp from "fastify-plugin";
7
7
  //#region src/events/retry.ts
8
8
  /**
@@ -1,4 +1,4 @@
1
- import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-C-2oAHtw.mjs";
1
+ import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-B1GFiYUy.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/events/defineEvent.d.ts
@@ -1,7 +1,7 @@
1
- import { a as EventTransport, i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions, t as DeadLetteredEvent } from "../EventTransport-C-2oAHtw.mjs";
2
- import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-CtHC_av1.mjs";
1
+ import { a as EventTransport, i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions, t as DeadLetteredEvent } from "../EventTransport-B1GFiYUy.mjs";
2
+ import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-DjEx_J3j.mjs";
3
3
  import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
4
- import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-DT-YjzrB.mjs";
4
+ import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-BuiAS3aE.mjs";
5
5
  import { RepositoryLike } from "@classytic/repo-core/adapter";
6
6
 
7
7
  //#region src/events/eventTypes.d.ts
@@ -1,7 +1,7 @@
1
- import { t as MemoryEventTransport } from "../EventTransport-Hxvv5QQz.mjs";
2
- import { n as defineEvent, t as createEventRegistry } from "../defineEvent-DRwY0fYm.mjs";
3
- import { i as withRetry, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-C2cGqtRO.mjs";
4
- import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-B0sunfZZ.mjs";
1
+ import { t as MemoryEventTransport } from "../EventTransport-ClEDfiMS.mjs";
2
+ import { n as defineEvent, t as createEventRegistry } from "../defineEvent-NPKUJRt8.mjs";
3
+ import { i as withRetry, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-CnJCUyM0.mjs";
4
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-5JgaxieH.mjs";
5
5
  import { and, anyOf, eq, lte, ne, or } from "@classytic/repo-core/filter";
6
6
  import { update } from "@classytic/repo-core/update";
7
7
  //#region src/events/eventTypes.ts
@@ -716,7 +716,7 @@ function wrapWithSchema(definition, handler, options = {}) {
716
716
  if (validate && definition.schema) result = validate(definition.schema, event.payload);
717
717
  else if (registry) result = registry.validate(definition.name, event.payload, eventVersion);
718
718
  else if (definition.schema) {
719
- const { createEventRegistry } = await import("../defineEvent-DRwY0fYm.mjs").then((n) => n.r);
719
+ const { createEventRegistry } = await import("../defineEvent-NPKUJRt8.mjs").then((n) => n.r);
720
720
  const adhoc = createEventRegistry();
721
721
  adhoc.register(definition);
722
722
  result = adhoc.validate(definition.name, event.payload);
@@ -1,2 +1,2 @@
1
- import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-DT-YjzrB.mjs";
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-BuiAS3aE.mjs";
2
2
  export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };