@classytic/arc 2.8.0 → 2.8.3

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 (117) hide show
  1. package/README.md +10 -1
  2. package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
  3. package/dist/{EventTransport-n1KBxC_N.d.mts → EventTransport-CinyO7zQ.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-BOtJuRCs.mjs → ResourceRegistry-Dq3_zBQP.mjs} +17 -5
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BxGgSHjj.mjs → adapters-BBqAVvPK.mjs} +11 -0
  8. package/dist/audit/index.d.mts +1 -1
  9. package/dist/audit/index.mjs +1 -1
  10. package/dist/audit/mongodb.d.mts +1 -1
  11. package/dist/audit/mongodb.mjs +1 -1
  12. package/dist/auth/index.d.mts +4 -4
  13. package/dist/auth/index.mjs +3 -3
  14. package/dist/auth/redis-session.d.mts +1 -1
  15. package/dist/{betterAuthOpenApi-CHCIuA-p.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  16. package/dist/cache/index.d.mts +2 -2
  17. package/dist/cli/commands/describe.mjs +1 -1
  18. package/dist/cli/commands/docs.mjs +2 -2
  19. package/dist/cli/commands/generate.mjs +1 -1
  20. package/dist/cli/commands/init.mjs +10 -10
  21. package/dist/cli/commands/introspect.mjs +3 -3
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +5 -5
  24. package/dist/{core-BfrfxNqO.mjs → core-DKSwNSXf.mjs} +1 -1
  25. package/dist/{createActionRouter-CbkIAaGh.mjs → createActionRouter-Df1BuawX.mjs} +87 -21
  26. package/dist/{createApp-Cy8eUNKQ.mjs → createApp-BOYjBgdI.mjs} +16 -7
  27. package/dist/{defineResource-CovBXvTB.mjs → defineResource-Bb_Bdhtw.mjs} +60 -33
  28. package/dist/docs/index.d.mts +2 -2
  29. package/dist/docs/index.mjs +1 -1
  30. package/dist/dynamic/index.d.mts +2 -2
  31. package/dist/dynamic/index.mjs +1 -1
  32. package/dist/{errorHandler-BeN-ERN7.d.mts → errorHandler-CdZDavNH.d.mts} +2 -2
  33. package/dist/{errorHandler-BW08lEiy.mjs → errorHandler-mzqk4cGl.mjs} +1 -1
  34. package/dist/{eventPlugin-CAOWMQS8.d.mts → eventPlugin-CVxlE6De.d.mts} +1 -1
  35. package/dist/{eventPlugin-x4jo3sG0.mjs → eventPlugin-D91S2YF4.mjs} +19 -1
  36. package/dist/events/index.d.mts +399 -28
  37. package/dist/events/index.mjs +345 -29
  38. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  39. package/dist/events/transports/redis-stream-entry.mjs +3 -1
  40. package/dist/events/transports/redis.d.mts +1 -1
  41. package/dist/factory/index.d.mts +1 -1
  42. package/dist/factory/index.mjs +2 -152
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.d.mts +3 -3
  45. package/dist/idempotency/mongodb.d.mts +1 -1
  46. package/dist/idempotency/mongodb.mjs +18 -6
  47. package/dist/idempotency/redis.d.mts +1 -1
  48. package/dist/idempotency/redis.mjs +10 -1
  49. package/dist/{index-BpMhrFgn.d.mts → index-BgmMdpm8.d.mts} +1 -1
  50. package/dist/{index-CBru2y5Y.d.mts → index-CSkeivBx.d.mts} +3 -3
  51. package/dist/{index-qct60lnl.d.mts → index-CpTSDqmD.d.mts} +60 -6
  52. package/dist/index.d.mts +8 -8
  53. package/dist/index.mjs +7 -7
  54. package/dist/integrations/event-gateway.d.mts +1 -1
  55. package/dist/integrations/event-gateway.mjs +1 -1
  56. package/dist/integrations/index.d.mts +1 -1
  57. package/dist/integrations/mcp/index.d.mts +2 -2
  58. package/dist/integrations/mcp/index.mjs +1 -1
  59. package/dist/integrations/mcp/testing.d.mts +1 -1
  60. package/dist/integrations/mcp/testing.mjs +1 -1
  61. package/dist/{interface-IJqN3pXK.d.mts → interface-BVuMfeVv.d.mts} +596 -125
  62. package/dist/loadResources-Bksk8ydA.mjs +154 -0
  63. package/dist/{mongodb-B1eVtFhw.d.mts → mongodb-B8U2xaLj.d.mts} +1 -1
  64. package/dist/{mongodb-NShVZDMr.d.mts → mongodb-X7LbEjTN.d.mts} +10 -1
  65. package/dist/{openapi-AYLVjqVe.mjs → openapi-CYCuekCn.mjs} +50 -3
  66. package/dist/org/index.d.mts +2 -2
  67. package/dist/permissions/index.d.mts +3 -3
  68. package/dist/plugins/index.d.mts +5 -5
  69. package/dist/plugins/index.mjs +8 -8
  70. package/dist/plugins/tracing-entry.d.mts +1 -1
  71. package/dist/plugins/tracing-entry.mjs +1 -1
  72. package/dist/policies/index.d.mts +1 -1
  73. package/dist/presets/index.d.mts +3 -3
  74. package/dist/presets/index.mjs +1 -1
  75. package/dist/presets/multiTenant.d.mts +1 -1
  76. package/dist/{presets-BFrGvvjL.mjs → presets-C2xgzW6x.mjs} +10 -18
  77. package/dist/{queryCachePlugin-BCFVXnxK.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
  78. package/dist/{redis-stream-CF1lrKVk.d.mts → redis-stream-D54N5oXs.d.mts} +1 -1
  79. package/dist/{redis-Bunu3qWg.d.mts → redis-z3sFr1UP.d.mts} +1 -1
  80. package/dist/registry/index.d.mts +1 -1
  81. package/dist/registry/index.mjs +1 -1
  82. package/dist/{resourceToTools-C_1SMiCz.mjs → resourceToTools-O_HwWXFa.mjs} +194 -64
  83. package/dist/rpc/index.d.mts +1 -1
  84. package/dist/rpc/index.mjs +1 -1
  85. package/dist/scope/index.d.mts +2 -2
  86. package/dist/testing/index.d.mts +2 -2
  87. package/dist/testing/index.mjs +1 -1
  88. package/dist/types/index.d.mts +5 -5
  89. package/dist/{types-gUxAIZHp.d.mts → types-Bg2X42_m.d.mts} +30 -9
  90. package/dist/{types-BoaZHr-2.d.mts → types-CVC4HOKi.d.mts} +1 -1
  91. package/dist/{types-Ct0PUUSp.d.mts → types-CcG4avic.d.mts} +1 -1
  92. package/dist/utils/index.d.mts +43 -17
  93. package/dist/utils/index.mjs +5 -5
  94. package/dist/{utils-B-l6410F.mjs → utils-yYT3HDXt.mjs} +65 -13
  95. package/package.json +10 -9
  96. package/skills/arc/SKILL.md +79 -6
  97. /package/dist/{caching-CHH-iHs3.mjs → caching-CjybdRwx.mjs} +0 -0
  98. /package/dist/{circuitBreaker-BGVoB1hD.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
  99. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  100. /package/dist/{elevation-UJO3-NvX.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  101. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  102. /package/dist/{errors-BI8kEKsO.d.mts → errors-Bmn3eZT6.d.mts} +0 -0
  103. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  104. /package/dist/{fields-DoeDgh2b.d.mts → fields-DC4So2M2.d.mts} +0 -0
  105. /package/dist/{interface-CkkWm5uR.d.mts → interface-B-pe8fhj.d.mts} +0 -0
  106. /package/dist/{interface-bpoLKKqx.d.mts → interface-DplgQO2e.d.mts} +0 -0
  107. /package/dist/{metrics-DuhiSEZI.mjs → metrics-TuOmguhi.mjs} +0 -0
  108. /package/dist/{mongodb-5Ff3w8jy.mjs → mongodb-B5O6xaW1.mjs} +0 -0
  109. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  110. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
  111. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  112. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
  113. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  114. /package/dist/{sse-CD5Hghpu.mjs → sse-CJpt7LGI.mjs} +0 -0
  115. /package/dist/{tracing-xqXzWeaf.d.mts → tracing-DxjKk7eW.d.mts} +0 -0
  116. /package/dist/{types-CN6JvmYz.d.mts → types-C72d3NDn.d.mts} +0 -0
  117. /package/dist/{versioning-CPU_5Xfs.mjs → versioning-Cm8qoFDg.mjs} +0 -0
@@ -1,7 +1,11 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-D6GPMsvh.mjs";
3
+ import { a as toJsonSchema } from "./schemaConverter-OxfCshus.mjs";
3
4
  //#region src/core/createActionRouter.ts
4
- var createActionRouter_exports = /* @__PURE__ */ __exportAll({ createActionRouter: () => createActionRouter });
5
+ var createActionRouter_exports = /* @__PURE__ */ __exportAll({
6
+ buildActionBodySchema: () => buildActionBodySchema,
7
+ createActionRouter: () => createActionRouter
8
+ });
5
9
  /**
6
10
  * Create action-based state transition endpoint
7
11
  *
@@ -18,20 +22,7 @@ function createActionRouter(fastify, config) {
18
22
  fastify.log.warn("[createActionRouter] No actions defined, skipping route creation");
19
23
  return;
20
24
  }
21
- const bodyProperties = { action: {
22
- type: "string",
23
- enum: actionEnum,
24
- description: `Action to perform: ${actionEnum.join(" | ")}`
25
- } };
26
- Object.entries(actionSchemas).forEach(([actionName, schema]) => {
27
- if (schema && typeof schema === "object") Object.entries(schema).forEach(([propName, propSchema]) => {
28
- const schemaObj = propSchema;
29
- bodyProperties[propName] = {
30
- ...schemaObj,
31
- description: `${schemaObj.description || ""} (for ${actionName} action)`.trim()
32
- };
33
- });
34
- });
25
+ const bodySchema = buildActionBodySchema(actionEnum, actionSchemas);
35
26
  const routeSchema = {
36
27
  tags: tag ? [tag] : void 0,
37
28
  summary: `Perform action (${actionEnum.join("/")})`,
@@ -44,11 +35,7 @@ function createActionRouter(fastify, config) {
44
35
  } },
45
36
  required: ["id"]
46
37
  },
47
- body: {
48
- type: "object",
49
- properties: bodyProperties,
50
- required: ["action"]
51
- }
38
+ body: bodySchema
52
39
  };
53
40
  const preHandler = [];
54
41
  const hasPublicActions = Object.entries(actionPermissions).some(([, p]) => p?._isPublic) || globalAuth && globalAuth?._isPublic;
@@ -167,6 +154,85 @@ function createActionRouter(fastify, config) {
167
154
  }, "[createActionRouter] Registered action endpoint: POST /:id/action");
168
155
  }
169
156
  /**
157
+ * Build a discriminated body schema for the unified action endpoint.
158
+ *
159
+ * Produces a schema of the form:
160
+ * ```json
161
+ * {
162
+ * "type": "object",
163
+ * "required": ["action"],
164
+ * "oneOf": [
165
+ * { "properties": { "action": { "const": "dispatch" }, "carrier": {...} }, "required": ["action", "carrier"] },
166
+ * { "properties": { "action": { "const": "approve" } }, "required": ["action"] }
167
+ * ]
168
+ * }
169
+ * ```
170
+ *
171
+ * AJV validates this natively, so an action call missing required fields is
172
+ * rejected with HTTP 400 before the handler ever runs.
173
+ *
174
+ * Exported so OpenAPI generation and MCP tool generation can reuse the same
175
+ * schema shape (single source of truth).
176
+ */
177
+ function buildActionBodySchema(actionEnum, actionSchemas = {}) {
178
+ const branches = [];
179
+ for (const actionName of actionEnum) {
180
+ const raw = actionSchemas[actionName];
181
+ const { properties, required } = normalizeActionSchema(raw);
182
+ const branchProperties = {
183
+ action: {
184
+ type: "string",
185
+ const: actionName
186
+ },
187
+ ...properties
188
+ };
189
+ const branchRequired = ["action", ...required.filter((r) => r !== "action")];
190
+ branches.push({
191
+ type: "object",
192
+ properties: branchProperties,
193
+ required: branchRequired
194
+ });
195
+ }
196
+ return {
197
+ type: "object",
198
+ required: ["action"],
199
+ oneOf: branches
200
+ };
201
+ }
202
+ /**
203
+ * Normalize the accepted schema shapes into `{ properties, required }`.
204
+ *
205
+ * Handles:
206
+ * 1. Full JSON Schema object (has `type: 'object'` + `properties`)
207
+ * 2. Zod v4 schema (has `_zod` marker) — converted via `toJsonSchema`
208
+ * 3. Legacy field map (`{ fieldName: { type: 'string' } }`) — every field required
209
+ * unless its schema has `nullable: true` or sentinel `required: false`
210
+ */
211
+ function normalizeActionSchema(raw) {
212
+ if (!raw || typeof raw !== "object") return {
213
+ properties: {},
214
+ required: []
215
+ };
216
+ const converted = toJsonSchema(raw);
217
+ if (converted && typeof converted === "object" && (converted.type === "object" || "properties" in converted)) return {
218
+ properties: converted.properties ?? {},
219
+ required: Array.isArray(converted.required) ? converted.required : []
220
+ };
221
+ const properties = {};
222
+ const required = [];
223
+ for (const [fieldName, fieldSchema] of Object.entries(raw)) {
224
+ if (fieldName === "type" || fieldName === "properties" || fieldName === "required") continue;
225
+ if (!fieldSchema || typeof fieldSchema !== "object") continue;
226
+ const fs = fieldSchema;
227
+ properties[fieldName] = fs;
228
+ if (fs.required !== false) required.push(fieldName);
229
+ }
230
+ return {
231
+ properties,
232
+ required
233
+ };
234
+ }
235
+ /**
170
236
  * Build description with action details
171
237
  * Uses _roles metadata from PermissionCheck functions for OpenAPI docs
172
238
  */
@@ -180,4 +246,4 @@ function buildActionDescription(actions, actionPermissions) {
180
246
  return lines.join("\n");
181
247
  }
182
248
  //#endregion
183
- export { createActionRouter_exports as n, createActionRouter as t };
249
+ export { createActionRouter as n, createActionRouter_exports as r, buildActionBodySchema as t };
@@ -207,7 +207,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
207
207
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
208
208
  trackPlugin("arc-core");
209
209
  if (config.arcPlugins?.events !== false) {
210
- const { default: eventPlugin } = await import("./eventPlugin-x4jo3sG0.mjs").then((n) => n.n);
210
+ const { default: eventPlugin } = await import("./eventPlugin-D91S2YF4.mjs").then((n) => n.n);
211
211
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
212
212
  await fastify.register(eventPlugin, {
213
213
  ...eventOpts,
@@ -243,7 +243,7 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
243
243
  trackPlugin("arc-graceful-shutdown");
244
244
  }
245
245
  if (config.arcPlugins?.caching) {
246
- const { default: cachingPlugin } = await import("./caching-CHH-iHs3.mjs").then((n) => n.r);
246
+ const { default: cachingPlugin } = await import("./caching-CjybdRwx.mjs").then((n) => n.r);
247
247
  const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
248
248
  await fastify.register(cachingPlugin, opts);
249
249
  trackPlugin("arc-caching", opts);
@@ -260,19 +260,19 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
260
260
  }
261
261
  if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
262
262
  else {
263
- const { default: ssePlugin } = await import("./sse-CD5Hghpu.mjs").then((n) => n.r);
263
+ const { default: ssePlugin } = await import("./sse-CJpt7LGI.mjs").then((n) => n.r);
264
264
  const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
265
265
  await fastify.register(ssePlugin, opts);
266
266
  trackPlugin("arc-sse", opts);
267
267
  }
268
268
  if (config.arcPlugins?.metrics) {
269
- const { default: metricsPlugin } = await import("./metrics-DuhiSEZI.mjs").then((n) => n.r);
269
+ const { default: metricsPlugin } = await import("./metrics-TuOmguhi.mjs").then((n) => n.r);
270
270
  const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
271
271
  await fastify.register(metricsPlugin, opts);
272
272
  trackPlugin("arc-metrics", opts);
273
273
  }
274
274
  if (config.arcPlugins?.versioning) {
275
- const { default: versioningPlugin } = await import("./versioning-CPU_5Xfs.mjs").then((n) => n.r);
275
+ const { default: versioningPlugin } = await import("./versioning-Cm8qoFDg.mjs").then((n) => n.r);
276
276
  await fastify.register(versioningPlugin, config.arcPlugins.versioning);
277
277
  trackPlugin("arc-versioning", config.arcPlugins.versioning);
278
278
  }
@@ -350,7 +350,7 @@ async function registerElevation(fastify, config, trackPlugin) {
350
350
  */
351
351
  async function registerErrorHandler(fastify, config, trackPlugin) {
352
352
  if (config.errorHandler === false) return;
353
- const { errorHandlerPlugin } = await import("./errorHandler-BW08lEiy.mjs").then((n) => n.n);
353
+ const { errorHandlerPlugin } = await import("./errorHandler-mzqk4cGl.mjs").then((n) => n.n);
354
354
  const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
355
355
  await fastify.register(errorHandlerPlugin, errorOpts);
356
356
  trackPlugin("arc-error-handler", errorOpts);
@@ -416,6 +416,15 @@ async function registerResources(fastify, config) {
416
416
  for (const init of config.bootstrap) await init(fastify);
417
417
  fastify.log.debug(`${config.bootstrap.length} bootstrap function(s) executed`);
418
418
  }
419
+ if (!config.resources?.length && config.resourceDir) {
420
+ const { loadResources } = await import("./loadResources-Bksk8ydA.mjs").then((n) => n.n);
421
+ const { resolve } = await import("node:path");
422
+ const dir = resolve(config.resourceDir);
423
+ config = {
424
+ ...config,
425
+ resources: await loadResources(dir, { logger: fastify.log })
426
+ };
427
+ }
419
428
  if (config.resources?.length) {
420
429
  const seen = /* @__PURE__ */ new Set();
421
430
  for (const resource of config.resources) if (resource.name) {
@@ -720,7 +729,7 @@ async function createApp(options) {
720
729
  await registerErrorHandler(fastify, config, trackPlugin);
721
730
  await registerResources(fastify, config);
722
731
  if (config.replyHelpers) {
723
- const { replyHelpersPlugin } = await import("./replyHelpers-CXtJDAZ0.mjs").then((n) => n.n);
732
+ const { replyHelpersPlugin } = await import("./replyHelpers-BLojtuvR.mjs").then((n) => n.n);
724
733
  await fastify.register(replyHelpersPlugin);
725
734
  }
726
735
  if (config.serializeBigInt) fastify.addHook("preSerialization", async (_request, _reply, payload) => {
@@ -1,15 +1,15 @@
1
1
  import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
2
2
  import { _ as isElevated, n as PUBLIC_SCOPE, v as isMember } from "./types-AOD8fxIw.mjs";
3
- import { t as BaseController } from "./BaseController-CpMfCXdn.mjs";
3
+ import { t as BaseController } from "./BaseController-DAGGc5Xn.mjs";
4
4
  import { i as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-ipsbIRPK.mjs";
5
5
  import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
6
6
  import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-D6GPMsvh.mjs";
7
- import { t as requestContext } from "./requestContext-xHIKedG6.mjs";
8
- import { i as getDefaultCrudSchemas } from "./utils-B-l6410F.mjs";
9
- import { r as ForbiddenError } from "./errors-Cg58SLNi.mjs";
10
- import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-Y5EejTnJ.mjs";
7
+ import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-OxfCshus.mjs";
8
+ import { t as requestContext } from "./requestContext-DYvHl113.mjs";
9
+ import { i as getDefaultCrudSchemas } from "./utils-yYT3HDXt.mjs";
10
+ import { r as ForbiddenError } from "./errors-BF2bIOIS.mjs";
11
11
  import { t as hasEvents } from "./typeGuards-CcFZXgU7.mjs";
12
- import { r as getAvailablePresets, t as applyPresets } from "./presets-BFrGvvjL.mjs";
12
+ import { r as getAvailablePresets, t as applyPresets } from "./presets-C2xgzW6x.mjs";
13
13
  //#region src/pipeline/pipe.ts
14
14
  /**
15
15
  * Compose pipeline steps into an ordered array.
@@ -390,7 +390,7 @@ function buildPermissionMiddleware(permissionCheck, resourceName, action) {
390
390
  * Create additional routes from preset/custom definitions
391
391
  */
392
392
  function createAdditionalRoutes(fastify, routes, controller, options) {
393
- const { tag, resourceName, arcDecorator, rateLimitConfig, cacheMw, idempotencyMw, pipeline } = options;
393
+ const { tag, resourceName, arcDecorator, rateLimitConfig, cacheMw, idempotencyMw, pipeline, routeGuards } = options;
394
394
  for (const route of routes) {
395
395
  const opName = route.operation ?? (typeof route.handler === "string" ? route.handler : `${route.method.toLowerCase()}${route.path.replace(/[/:]/g, "_")}`);
396
396
  let handler;
@@ -431,6 +431,7 @@ function createAdditionalRoutes(fastify, routes, controller, options) {
431
431
  authMw,
432
432
  permissionMw,
433
433
  pluginMw,
434
+ ...routeGuards,
434
435
  ...customPreHandlers
435
436
  ].filter(Boolean);
436
437
  const isStream = route.streamResponse === true;
@@ -479,7 +480,7 @@ function createPipelineHandler(controllerMethod, steps, operation, resourceName)
479
480
  * @param options - Router configuration
480
481
  */
481
482
  function createCrudRouter(fastify, controller, options = {}) {
482
- const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, additionalRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, updateMethod = DEFAULT_UPDATE_METHOD } = options;
483
+ const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], additionalRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, updateMethod = DEFAULT_UPDATE_METHOD } = options;
483
484
  const rateLimitConfig = buildRateLimitConfig(rateLimit);
484
485
  const cacheMw = !(fastify.hasDecorator("queryCache") && controller && typeof controller._cacheConfig !== "undefined" && controller._cacheConfig !== void 0) && fastify.hasDecorator("responseCache") ? fastify.responseCache.middleware : null;
485
486
  const idempotencyMw = fastify.hasDecorator("idempotency") ? fastify.idempotency.middleware : null;
@@ -542,6 +543,7 @@ function createCrudRouter(fastify, controller, options = {}) {
542
543
  buildAuthMiddleware(fastify, permissions.list),
543
544
  buildPermissionMiddleware(permissions.list, resourceName, "list"),
544
545
  cacheMw,
546
+ ...routeGuards,
545
547
  ...mw.list
546
548
  ].filter(Boolean);
547
549
  fastify.route({
@@ -562,6 +564,7 @@ function createCrudRouter(fastify, controller, options = {}) {
562
564
  buildAuthMiddleware(fastify, permissions.get),
563
565
  buildPermissionMiddleware(permissions.get, resourceName, "get"),
564
566
  cacheMw,
567
+ ...routeGuards,
565
568
  ...mw.get
566
569
  ].filter(Boolean);
567
570
  fastify.route({
@@ -583,6 +586,7 @@ function createCrudRouter(fastify, controller, options = {}) {
583
586
  buildAuthMiddleware(fastify, permissions.create),
584
587
  buildPermissionMiddleware(permissions.create, resourceName, "create"),
585
588
  idempotencyMw,
589
+ ...routeGuards,
586
590
  ...mw.create
587
591
  ].filter(Boolean);
588
592
  fastify.route({
@@ -604,6 +608,7 @@ function createCrudRouter(fastify, controller, options = {}) {
604
608
  buildAuthMiddleware(fastify, permissions.update),
605
609
  buildPermissionMiddleware(permissions.update, resourceName, "update"),
606
610
  idempotencyMw,
611
+ ...routeGuards,
607
612
  ...mw.update
608
613
  ].filter(Boolean);
609
614
  for (const method of updateMethods) fastify.route({
@@ -624,6 +629,7 @@ function createCrudRouter(fastify, controller, options = {}) {
624
629
  arcDecorator,
625
630
  buildAuthMiddleware(fastify, permissions.delete),
626
631
  buildPermissionMiddleware(permissions.delete, resourceName, "delete"),
632
+ ...routeGuards,
627
633
  ...mw.delete
628
634
  ].filter(Boolean);
629
635
  fastify.route({
@@ -647,7 +653,8 @@ function createCrudRouter(fastify, controller, options = {}) {
647
653
  rateLimitConfig,
648
654
  cacheMw,
649
655
  idempotencyMw,
650
- pipeline
656
+ pipeline,
657
+ routeGuards
651
658
  });
652
659
  }
653
660
  /**
@@ -701,10 +708,10 @@ function validateResourceConfig(config, options = {}) {
701
708
  message: "Adapter must provide a repository",
702
709
  suggestion: "Ensure your adapter returns a valid CrudRepository"
703
710
  });
704
- } else if (!config.adapter && !config.additionalRoutes?.length) warnings.push({
711
+ } else if (!config.adapter && !config.routes?.length) warnings.push({
705
712
  field: "config",
706
- message: "Resource has no adapter and no additionalRoutes",
707
- suggestion: "Provide either adapter for CRUD or additionalRoutes for custom logic"
713
+ message: "Resource has no adapter and no routes",
714
+ suggestion: "Provide either adapter for CRUD or routes for custom logic"
708
715
  });
709
716
  if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
710
717
  const ctrl = config.controller;
@@ -715,7 +722,7 @@ function validateResourceConfig(config, options = {}) {
715
722
  suggestion: "Extend BaseController which implements IController interface"
716
723
  });
717
724
  }
718
- if (config.controller && config.additionalRoutes) validateAdditionalRouteHandlers(config.controller, config.additionalRoutes, errors);
725
+ if (config.controller && config.routes) validateRouteHandlers(config.controller, config.routes, errors);
719
726
  if (config.permissions) validatePermissionKeys(config, options, errors, warnings);
720
727
  if (config.presets && !options.allowUnknownPresets) validatePresets(config.presets, errors, warnings);
721
728
  if (config.prefix) {
@@ -730,18 +737,18 @@ function validateResourceConfig(config, options = {}) {
730
737
  suggestion: `Change to "${config.prefix.slice(0, -1)}"`
731
738
  });
732
739
  }
733
- if (config.additionalRoutes) validateAdditionalRoutes(config.additionalRoutes, errors);
740
+ if (config.routes) validateRoutes(config.routes, errors);
734
741
  return {
735
742
  valid: errors.length === 0,
736
743
  errors,
737
744
  warnings
738
745
  };
739
746
  }
740
- function validateAdditionalRouteHandlers(controller, routes, errors) {
747
+ function validateRouteHandlers(controller, routes, errors) {
741
748
  const ctrl = controller;
742
749
  for (const route of routes) if (typeof route.handler === "string") {
743
750
  if (typeof ctrl[route.handler] !== "function") errors.push({
744
- field: `additionalRoutes[${route.method} ${route.path}]`,
751
+ field: `routes[${route.method} ${route.path}]`,
745
752
  message: `Handler "${route.handler}" not found on controller`,
746
753
  suggestion: `Add method "${route.handler}" to controller or use a function handler`
747
754
  });
@@ -749,7 +756,7 @@ function validateAdditionalRouteHandlers(controller, routes, errors) {
749
756
  }
750
757
  function validatePermissionKeys(config, options, _errors, warnings) {
751
758
  const validKeys = new Set([...CRUD_OPERATIONS, ...options.additionalPermissionKeys ?? []]);
752
- for (const route of config.additionalRoutes ?? []) if (typeof route.handler === "string") validKeys.add(route.handler);
759
+ for (const route of config.routes ?? []) if (typeof route.handler === "string") validKeys.add(route.handler);
753
760
  for (const preset of config.presets ?? []) {
754
761
  const presetName = typeof preset === "string" ? preset : preset.name;
755
762
  if (presetName === "softDelete") {
@@ -773,7 +780,7 @@ function validatePermissionKeys(config, options, _errors, warnings) {
773
780
  function validatePresets(presets, errors, warnings) {
774
781
  const availablePresets = getAvailablePresets();
775
782
  for (const preset of presets) {
776
- if (typeof preset === "object" && ("middlewares" in preset || "additionalRoutes" in preset)) continue;
783
+ if (typeof preset === "object" && ("middlewares" in preset || "routes" in preset)) continue;
777
784
  const presetName = typeof preset === "string" ? preset : preset.name;
778
785
  if (!availablePresets.includes(presetName)) errors.push({
779
786
  field: "presets",
@@ -798,7 +805,7 @@ function validatePresetOptions(preset, warnings) {
798
805
  suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
799
806
  });
800
807
  }
801
- function validateAdditionalRoutes(routes, errors) {
808
+ function validateRoutes(routes, errors) {
802
809
  const validMethods = [
803
810
  "GET",
804
811
  "POST",
@@ -811,26 +818,26 @@ function validateAdditionalRoutes(routes, errors) {
811
818
  const seenRoutes = /* @__PURE__ */ new Set();
812
819
  for (const [i, route] of routes.entries()) {
813
820
  if (!validMethods.includes(route.method)) errors.push({
814
- field: `additionalRoutes[${i}].method`,
821
+ field: `routes[${i}].method`,
815
822
  message: `Invalid HTTP method "${route.method}"`,
816
823
  suggestion: `Valid methods: ${validMethods.join(", ")}`
817
824
  });
818
825
  if (!route.path) errors.push({
819
- field: `additionalRoutes[${i}].path`,
826
+ field: `routes[${i}].path`,
820
827
  message: "Route path is required"
821
828
  });
822
829
  else if (!route.path.startsWith("/")) errors.push({
823
- field: `additionalRoutes[${i}].path`,
830
+ field: `routes[${i}].path`,
824
831
  message: `Route path must start with "/" (got "${route.path}")`,
825
832
  suggestion: `Change to "/${route.path}"`
826
833
  });
827
834
  if (!route.handler) errors.push({
828
- field: `additionalRoutes[${i}].handler`,
835
+ field: `routes[${i}].handler`,
829
836
  message: "Route handler is required"
830
837
  });
831
838
  const routeKey = `${route.method} ${route.path}`;
832
839
  if (seenRoutes.has(routeKey)) errors.push({
833
- field: `additionalRoutes[${i}]`,
840
+ field: `routes[${i}]`,
834
841
  message: `Duplicate route "${routeKey}"`
835
842
  });
836
843
  seenRoutes.add(routeKey);
@@ -884,11 +891,6 @@ function defineResource(config) {
884
891
  if (config.permissions) {
885
892
  for (const [key, value] of Object.entries(config.permissions)) if (value !== void 0 && typeof value !== "function") throw new Error(`[Arc] Resource '${config.name}': permissions.${key} must be a PermissionCheck function.\nUse allowPublic(), requireAuth(), or requireRoles(['role']) from @classytic/arc/permissions.`);
886
893
  }
887
- for (const route of config.additionalRoutes ?? []) {
888
- if (typeof route.permissions !== "function") throw new Error(`[Arc] Resource '${config.name}' route ${route.method} ${route.path}: permissions is required and must be a PermissionCheck function.\nUse allowPublic() or requireAuth() from @classytic/arc/permissions.`);
889
- if (typeof route.wrapHandler !== "boolean") throw new Error(`[Arc] Resource '${config.name}' route ${route.method} ${route.path}: wrapHandler is required.\nSet true for ControllerHandler (context object) or false for FastifyHandler (req, reply).`);
890
- }
891
- if (config.routes && config.additionalRoutes) throw new Error(`[Arc] Resource '${config.name}': Cannot use both 'routes' and 'additionalRoutes'.\nUse 'routes' (v2.8) — it replaces 'additionalRoutes'.`);
892
894
  for (const route of config.routes ?? []) if (typeof route.permissions !== "function") throw new Error(`[Arc] Resource '${config.name}' route ${route.method} ${route.path}: permissions is required and must be a PermissionCheck function.`);
893
895
  if (config.actions) {
894
896
  const CRUD_OPS = new Set([
@@ -1087,7 +1089,18 @@ var ResourceDefinition = class {
1087
1089
  customSchemas;
1088
1090
  permissions;
1089
1091
  additionalRoutes;
1092
+ /**
1093
+ * Original v2.8 `routes` declaration — retained for downstream consumers
1094
+ * (OpenAPI, MCP, registry, CLI introspect). Preserves fields dropped during
1095
+ * normalization to `additionalRoutes` (notably `mcp`, `description`,
1096
+ * `annotations`). Undefined when the resource was defined with the legacy
1097
+ * `additionalRoutes` shape.
1098
+ *
1099
+ * Added in 2.8.1 — the source-of-truth fix for "canonical resource manifest".
1100
+ */
1101
+ routes;
1090
1102
  middlewares;
1103
+ routeGuards;
1091
1104
  disableDefaultRoutes;
1092
1105
  disabledRoutes;
1093
1106
  actions;
@@ -1117,8 +1130,10 @@ var ResourceDefinition = class {
1117
1130
  this.schemaOptions = config.schemaOptions ?? {};
1118
1131
  this.customSchemas = config.customSchemas ?? {};
1119
1132
  this.permissions = config.permissions ?? {};
1120
- this.additionalRoutes = config.routes ? convertRoutesToAdditionalRoutes(config.routes) : config.additionalRoutes ?? [];
1133
+ this.routes = config.routes;
1134
+ this.additionalRoutes = config.routes ? convertRoutesToAdditionalRoutes(config.routes) : [];
1121
1135
  this.middlewares = config.middlewares ?? {};
1136
+ this.routeGuards = config.routeGuards;
1122
1137
  this.disableDefaultRoutes = config.disableDefaultRoutes ?? false;
1123
1138
  this.disabledRoutes = config.disabledRoutes ?? [];
1124
1139
  this.actions = config.actions;
@@ -1272,6 +1287,7 @@ var ResourceDefinition = class {
1272
1287
  schemas: schemas ?? void 0,
1273
1288
  permissions: self.permissions,
1274
1289
  middlewares: self.middlewares,
1290
+ routeGuards: self.routeGuards,
1275
1291
  additionalRoutes: resolvedRoutes,
1276
1292
  disableDefaultRoutes: self.disableDefaultRoutes,
1277
1293
  disabledRoutes: self.disabledRoutes,
@@ -1283,7 +1299,7 @@ var ResourceDefinition = class {
1283
1299
  fields: self.fields
1284
1300
  });
1285
1301
  if (self.actions && Object.keys(self.actions).length > 0) {
1286
- const { createActionRouter } = await import("./createActionRouter-CbkIAaGh.mjs").then((n) => n.n);
1302
+ const { createActionRouter } = await import("./createActionRouter-Df1BuawX.mjs").then((n) => n.r);
1287
1303
  createActionRouter(instance, normalizeActionsToRouterConfig(self.actions, self.actionPermissions, self.tag));
1288
1304
  }
1289
1305
  if (self.events && Object.keys(self.events).length > 0) typedInstance.log?.debug?.(`Resource '${self.name}' defined ${Object.keys(self.events).length} events`);
@@ -1320,7 +1336,17 @@ var ResourceDefinition = class {
1320
1336
  prefix: this.prefix,
1321
1337
  presets: this._appliedPresets,
1322
1338
  permissions: this.permissions,
1323
- additionalRoutes: this.additionalRoutes,
1339
+ customRoutes: (this.routes ?? []).map((r) => ({
1340
+ method: r.method,
1341
+ path: r.path,
1342
+ handler: typeof r.handler === "string" ? r.handler : r.handler.name || "anonymous",
1343
+ operation: r.operation,
1344
+ summary: r.summary,
1345
+ description: r.description,
1346
+ permissions: r.permissions,
1347
+ raw: r.raw,
1348
+ schema: r.schema
1349
+ })),
1324
1350
  routes: [],
1325
1351
  events: Object.keys(this.events)
1326
1352
  };
@@ -1358,7 +1384,8 @@ function convertRoutesToAdditionalRoutes(routes) {
1358
1384
  preAuth: route.preAuth,
1359
1385
  streamResponse: route.streamResponse,
1360
1386
  schema: route.schema,
1361
- mcpHandler: route.mcpHandler
1387
+ mcpHandler: route.mcpHandler,
1388
+ mcp: route.mcp
1362
1389
  }));
1363
1390
  }
1364
1391
  /**
@@ -1,5 +1,5 @@
1
- import { it as RegistryEntry } from "../interface-IJqN3pXK.mjs";
2
- import { t as ExternalOpenApiPaths } from "../externalPaths-BQ8QijNH.mjs";
1
+ import { it as RegistryEntry } from "../interface-BVuMfeVv.mjs";
2
+ import { t as ExternalOpenApiPaths } from "../externalPaths-Bapitwvd.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
5
5
  //#region src/docs/openapi.d.ts
@@ -1,5 +1,5 @@
1
1
  import { t as getUserRoles } from "../types-ZUu_h0jp.mjs";
2
- import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-AYLVjqVe.mjs";
2
+ import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-CYCuekCn.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/docs/scalar.ts
5
5
  const scalarPlugin = async (fastify, opts = {}) => {
@@ -1,5 +1,5 @@
1
- import { qt as ResourceDefinition, r as DataAdapter } from "../interface-IJqN3pXK.mjs";
2
- import { t as PermissionCheck } from "../types-BoaZHr-2.mjs";
1
+ import { qt as ResourceDefinition, r as DataAdapter } from "../interface-BVuMfeVv.mjs";
2
+ import { t as PermissionCheck } from "../types-CVC4HOKi.mjs";
3
3
 
4
4
  //#region src/dynamic/ArcDynamicLoader.d.ts
5
5
  interface ArcArchitectureSchema {
@@ -1,5 +1,5 @@
1
1
  import { t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
2
- import { n as defineResource } from "../defineResource-CovBXvTB.mjs";
2
+ import { n as defineResource } from "../defineResource-Bb_Bdhtw.mjs";
3
3
  import { C as publicRead, T as readOnly, b as fullPublic, v as adminOnly, w as publicReadAdminWrite, x as ownerWithAdminBypass, y as authenticated } from "../permissions-CH4cNwJi.mjs";
4
4
  //#region src/dynamic/ArcDynamicLoader.ts
5
5
  const VALID_FIELD_TYPES = new Set([
@@ -1,4 +1,4 @@
1
- import { t as DomainEvent } from "./EventTransport-n1KBxC_N.mjs";
1
+ import { t as DomainEvent } from "./EventTransport-CinyO7zQ.mjs";
2
2
  import { FastifyInstance, FastifyPluginAsync, FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/plugins/caching.d.ts
@@ -180,4 +180,4 @@ interface ErrorHandlerOptions {
180
180
  declare function errorHandlerPluginFn(fastify: FastifyInstance, options?: ErrorHandlerOptions): Promise<void>;
181
181
  declare const errorHandlerPlugin: typeof errorHandlerPluginFn;
182
182
  //#endregion
183
- export { cachingPlugin as _, versioningPlugin as a, MetricsOptions as c, SSEOptions as d, _default$2 as f, _default$3 as g, CachingRule as h, _default as i, _default$1 as l, CachingOptions as m, errorHandlerPlugin as n, MetricEntry as o, ssePlugin as p, VersioningOptions as r, MetricsCollector as s, ErrorHandlerOptions as t, metricsPlugin as u };
183
+ export { _default$3 as _, _default as a, MetricsCollector as c, metricsPlugin as d, SSEOptions as f, CachingRule as g, CachingOptions as h, VersioningOptions as i, MetricsOptions as l, ssePlugin as m, ErrorMapper as n, versioningPlugin as o, _default$2 as p, errorHandlerPlugin as r, MetricEntry as s, ErrorHandlerOptions as t, _default$1 as u, cachingPlugin as v };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { p as isArcError } from "./errors-Cg58SLNi.mjs";
2
+ import { p as isArcError } from "./errors-BF2bIOIS.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/plugins/errorHandler.ts
5
5
  var errorHandler_exports = /* @__PURE__ */ __exportAll({ errorHandlerPlugin: () => errorHandlerPlugin });
@@ -1,4 +1,4 @@
1
- import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-n1KBxC_N.mjs";
1
+ import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-CinyO7zQ.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/events/defineEvent.d.ts
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { t as requestContext } from "./requestContext-xHIKedG6.mjs";
2
+ import { t as requestContext } from "./requestContext-DYvHl113.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/events/EventTransport.ts
5
5
  /**
@@ -33,6 +33,24 @@ var MemoryEventTransport = class {
33
33
  this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
34
34
  }
35
35
  }
36
+ /**
37
+ * Reference `publishMany` implementation — delegates to `publish()` in order.
38
+ *
39
+ * Production transports (Kafka, Redis pipeline, SQS batch) should override
40
+ * this with a single batched network call. Memory transport has nothing to
41
+ * batch, so we just loop — the loop still returns a proper result map so
42
+ * `EventOutbox.relay` can exercise the batched code path in tests.
43
+ */
44
+ async publishMany(events) {
45
+ const results = /* @__PURE__ */ new Map();
46
+ for (const event of events) try {
47
+ await this.publish(event);
48
+ results.set(event.meta.id, null);
49
+ } catch (err) {
50
+ results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
51
+ }
52
+ return results;
53
+ }
36
54
  async subscribe(pattern, handler) {
37
55
  if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
38
56
  this.handlers.get(pattern)?.add(handler);