@classytic/arc 2.15.3 → 2.16.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 (159) hide show
  1. package/README.md +1 -0
  2. package/bin/arc.js +12 -0
  3. package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
  4. package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
  5. package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
  7. package/dist/audit/index.d.mts +1 -1
  8. package/dist/audit/index.mjs +4 -2
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +4 -4
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
  13. package/dist/buildHandler-BZX6zzDM.mjs +300 -0
  14. package/dist/cache/index.d.mts +3 -3
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
  17. package/dist/cli/commands/describe.d.mts +35 -1
  18. package/dist/cli/commands/describe.mjs +52 -12
  19. package/dist/cli/commands/docs.d.mts +1 -4
  20. package/dist/cli/commands/docs.mjs +4 -16
  21. package/dist/cli/commands/generate.d.mts +2 -20
  22. package/dist/cli/commands/generate.mjs +1 -546
  23. package/dist/cli/commands/init.d.mts +2 -40
  24. package/dist/cli/commands/init.mjs +1 -3036
  25. package/dist/cli/commands/introspect.mjs +53 -64
  26. package/dist/cli/index.d.mts +2 -2
  27. package/dist/cli/index.mjs +2 -2
  28. package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
  29. package/dist/core/index.d.mts +3 -3
  30. package/dist/core/index.mjs +5 -5
  31. package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
  32. package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
  33. package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
  34. package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
  35. package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
  36. package/dist/docs/index.d.mts +2 -2
  37. package/dist/docs/index.mjs +1 -1
  38. package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
  39. package/dist/errors-C1lX_jlm.d.mts +91 -0
  40. package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
  41. package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
  42. package/dist/events/index.d.mts +3 -3
  43. package/dist/events/index.mjs +5 -5
  44. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +1 -1
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
  49. package/dist/generate-BWFwgcCM.d.mts +38 -0
  50. package/dist/generate-CYac-OLv.mjs +654 -0
  51. package/dist/hooks/index.d.mts +1 -1
  52. package/dist/hooks/index.mjs +1 -1
  53. package/dist/idempotency/index.d.mts +2 -2
  54. package/dist/idempotency/index.mjs +1 -1
  55. package/dist/idempotency/redis.d.mts +1 -1
  56. package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
  57. package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
  58. package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
  59. package/dist/index.d.mts +6 -6
  60. package/dist/index.mjs +7 -8
  61. package/dist/init-Dv71MsJr.d.mts +71 -0
  62. package/dist/init-HDvoO9L5.mjs +3098 -0
  63. package/dist/integrations/event-gateway.d.mts +2 -2
  64. package/dist/integrations/event-gateway.mjs +1 -1
  65. package/dist/integrations/index.d.mts +2 -2
  66. package/dist/integrations/jobs.mjs +3 -3
  67. package/dist/integrations/mcp/index.d.mts +239 -7
  68. package/dist/integrations/mcp/index.mjs +2 -528
  69. package/dist/integrations/mcp/testing.d.mts +2 -2
  70. package/dist/integrations/mcp/testing.mjs +6 -10
  71. package/dist/integrations/streamline.d.mts +71 -2
  72. package/dist/integrations/streamline.mjs +81 -8
  73. package/dist/integrations/websocket-redis.d.mts +1 -1
  74. package/dist/integrations/websocket.d.mts +1 -1
  75. package/dist/integrations/websocket.mjs +1 -0
  76. package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
  77. package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
  78. package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
  79. package/dist/middleware/index.d.mts +1 -1
  80. package/dist/middleware/index.mjs +1 -1
  81. package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
  82. package/dist/permissions/index.d.mts +2 -2
  83. package/dist/permissions/index.mjs +1 -1
  84. package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
  85. package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
  86. package/dist/pipeline/index.d.mts +1 -1
  87. package/dist/pipeline/index.mjs +1 -1
  88. package/dist/plugins/index.d.mts +5 -5
  89. package/dist/plugins/index.mjs +10 -10
  90. package/dist/plugins/response-cache.mjs +5 -5
  91. package/dist/plugins/tracing-entry.d.mts +1 -1
  92. package/dist/plugins/tracing-entry.mjs +1 -1
  93. package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
  94. package/dist/presets/filesUpload.d.mts +4 -4
  95. package/dist/presets/filesUpload.mjs +2 -2
  96. package/dist/presets/index.d.mts +1 -1
  97. package/dist/presets/index.mjs +1 -1
  98. package/dist/presets/multiTenant.d.mts +1 -1
  99. package/dist/presets/multiTenant.mjs +4 -3
  100. package/dist/presets/search.d.mts +2 -2
  101. package/dist/presets/search.mjs +1 -1
  102. package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
  103. package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
  104. package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
  105. package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
  106. package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
  107. package/dist/registry/index.d.mts +319 -2
  108. package/dist/registry/index.mjs +3 -3
  109. package/dist/registry-BBE23CDj.mjs +576 -0
  110. package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
  111. package/dist/scope/index.d.mts +3 -3
  112. package/dist/scope/index.mjs +3 -3
  113. package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
  114. package/dist/testing/index.d.mts +2 -2
  115. package/dist/testing/index.mjs +16 -7
  116. package/dist/testing/storageContract.d.mts +1 -1
  117. package/dist/types/index.d.mts +5 -5
  118. package/dist/types/storage.d.mts +1 -1
  119. package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
  120. package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
  121. package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
  122. package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
  123. package/dist/utils/index.d.mts +1286 -2
  124. package/dist/utils/index.mjs +1 -1
  125. package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
  126. package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
  127. package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
  128. package/package.json +22 -29
  129. package/skills/arc/SKILL.md +299 -689
  130. package/skills/arc/references/auth.md +19 -7
  131. package/skills/arc-code-review/SKILL.md +1 -1
  132. package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
  133. package/dist/createActionRouter-S3MLVYot.mjs +0 -220
  134. package/dist/index-bRjYu21O.d.mts +0 -1320
  135. package/dist/org/index.d.mts +0 -66
  136. package/dist/org/index.mjs +0 -486
  137. package/dist/org/types.d.mts +0 -82
  138. package/dist/org/types.mjs +0 -1
  139. package/dist/registry-I-ogLgL9.mjs +0 -46
  140. /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
  141. /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
  142. /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
  143. /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
  144. /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
  145. /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
  146. /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
  147. /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
  148. /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
  149. /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
  150. /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
  151. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
  152. /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
  153. /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
  154. /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
  155. /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
  156. /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
  157. /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
  158. /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
  159. /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
@@ -1,12 +1,13 @@
1
- import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
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-_h9B3c57.mjs";
4
- import { t as BaseController } from "./BaseController-dx3m2J8V.mjs";
5
- import { t as applyPresets } from "./presets-BbkjdPeH.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";
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-DrOa-26E.mjs";
9
- import { t as resolveActionPermission } from "./actionPermissions-CyUkQu6O.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";
9
+ import { t as pluralize } from "./pluralize-B9M8xvy-.mjs";
10
+ import { t as resolveActionPermission } from "./actionPermissions-Bjmvn7Eb.mjs";
10
11
  //#region src/core/aggregation/defineAggregation.ts
11
12
  /**
12
13
  * Declare a single resource aggregation. Exported configs flow into
@@ -59,15 +60,27 @@ function defineAggregation(config) {
59
60
  * `wrapHandler` is derived inline from `!route.raw`.
60
61
  */
61
62
  function createCustomRoutes(fastify, routes, controller, options) {
62
- const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards } = options;
63
+ const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw } = options;
63
64
  for (const route of routes) {
64
- const opName = route.operation ?? (typeof route.handler === "string" ? route.handler : `${route.method.toLowerCase()}${route.path.replace(/[/:]/g, "_")}`);
65
+ const routeWithRefs = route;
66
+ const hasHandler = route.handler !== void 0;
67
+ const hasControllerMethod = typeof routeWithRefs.controllerMethod === "function";
68
+ if (hasHandler && hasControllerMethod) throw new Error(`Route ${route.method} ${route.path}: pass either \`handler\` or \`controllerMethod\`, not both. Prefer \`controllerMethod: (c: MyController) => c.method\` for typed handler refs (TS catches typos).`);
69
+ if (!hasHandler && !hasControllerMethod) throw new Error(`Route ${route.method} ${route.path}: must declare either \`handler\` (string / function) or \`controllerMethod: (c) => c.method\` (typed function-ref form).`);
70
+ let resolvedHandler;
71
+ if (hasControllerMethod) {
72
+ if (!controller) throw new Error(`Route ${route.method} ${route.path}: \`controllerMethod\` requires a controller. Provide one via \`defineResource({ controller, … })\`, or use \`defineResource\` with an \`adapter\` so arc auto-creates a BaseController.`);
73
+ const referenced = routeWithRefs.controllerMethod?.(controller);
74
+ if (typeof referenced !== "function") throw new Error(`Route ${route.method} ${route.path}: \`controllerMethod\` did not return a function. Return the method itself: \`controllerMethod: (c) => c.myMethod\`.`);
75
+ resolvedHandler = referenced.bind(controller);
76
+ } else resolvedHandler = route.handler;
77
+ const opName = route.operation ?? (typeof resolvedHandler === "string" ? resolvedHandler : `${route.method.toLowerCase()}${route.path.replace(/[/:]/g, "_")}`);
65
78
  const wrapHandler = !route.raw;
66
79
  let handler;
67
- if (typeof route.handler === "string") {
68
- if (!controller) throw new Error(`Route ${route.method} ${route.path}: string handler '${route.handler}' requires a controller. Either provide a controller or use a function handler instead.`);
69
- const method = controller[route.handler];
70
- if (typeof method !== "function") throw new Error(`Handler '${route.handler}' not found on controller`);
80
+ if (typeof resolvedHandler === "string") {
81
+ if (!controller) throw new Error(`Route ${route.method} ${route.path}: string handler '${resolvedHandler}' requires a controller. Either provide a controller or use a function handler instead.`);
82
+ const method = controller[resolvedHandler];
83
+ if (typeof method !== "function") throw new Error(`Handler '${resolvedHandler}' not found on controller`);
71
84
  const boundMethod = method.bind(controller);
72
85
  if (wrapHandler) {
73
86
  const steps = resolvePipelineSteps(pipeline, opName);
@@ -75,8 +88,8 @@ function createCustomRoutes(fastify, routes, controller, options) {
75
88
  } else handler = boundMethod;
76
89
  } else if (wrapHandler) {
77
90
  const steps = resolvePipelineSteps(pipeline, opName);
78
- handler = steps.length > 0 ? buildPipelineHandler(route.handler, steps, opName, resourceName) : createFastifyHandler(route.handler);
79
- } else handler = route.handler;
91
+ handler = steps.length > 0 ? buildPipelineHandler(resolvedHandler, steps, opName, resourceName) : createFastifyHandler(resolvedHandler);
92
+ } else handler = resolvedHandler;
80
93
  const routeTags = route.tags ?? (tag ? [tag] : void 0);
81
94
  const convertedSchema = route.schema ? convertRouteSchema(route.schema) : void 0;
82
95
  const schema = {
@@ -86,6 +99,10 @@ function createCustomRoutes(fastify, routes, controller, options) {
86
99
  ...convertedSchema ?? {}
87
100
  };
88
101
  const customPreHandlers = resolveRoutePreHandlers(route.preHandler, fastify, `${route.method} ${route.path}`);
102
+ if (route.tenantScope === true) {
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
+ customPreHandlers.unshift(...tenantScopeMw);
105
+ }
89
106
  const preHandler = buildPreHandlerChain({
90
107
  preAuth: route.preAuth ?? [],
91
108
  arcDecorator,
@@ -141,6 +158,7 @@ function createCrudRouter(fastify, controller, options = {}) {
141
158
  update: middlewares.update ?? [],
142
159
  delete: middlewares.delete ?? []
143
160
  };
161
+ const tenantScopeMw = middlewares.tenantScope;
144
162
  const idParamsSchema = {
145
163
  type: "object",
146
164
  properties: { id: { type: "string" } },
@@ -233,7 +251,8 @@ function createCrudRouter(fastify, controller, options = {}) {
233
251
  rateLimitConfig,
234
252
  pluginMw,
235
253
  pipeline,
236
- routeGuards
254
+ routeGuards,
255
+ tenantScopeMw
237
256
  });
238
257
  }
239
258
  function buildCrudHandlers(ctrl, pipeline, resourceName) {
@@ -256,6 +275,28 @@ function createPermissionMiddleware(permission, resourceName, action) {
256
275
  return buildCrudPermissionMw(permission, resourceName, action);
257
276
  }
258
277
  //#endregion
278
+ //#region src/core/defineAction.ts
279
+ /**
280
+ * Build an `ActionDefinition` with a typed handler. The literal schema
281
+ * type captured here flows into `data`, so `defineAction({ schema:
282
+ * z.object({...}), handler })` produces fully-typed code with no
283
+ * `as MyShape` cast.
284
+ *
285
+ * Behaviorally identical to a bare `ActionDefinition` object — same
286
+ * validation path (AJV), same permission resolution, same MCP wiring.
287
+ * The runtime shape is unchanged; only the type-level inference is new.
288
+ */
289
+ function defineAction(config) {
290
+ return {
291
+ handler: config.handler,
292
+ permissions: config.permissions,
293
+ schema: config.schema,
294
+ description: config.description,
295
+ id: config.id,
296
+ mcp: config.mcp
297
+ };
298
+ }
299
+ //#endregion
259
300
  //#region src/core/defineResource/controller.ts
260
301
  /**
261
302
  * Phase 4 — pick (or auto-create) the resource's controller.
@@ -739,6 +780,39 @@ function computeHasCrudRoutes(config) {
739
780
  return !config.disableDefaultRoutes && CRUD_OPERATIONS.some((op) => !disabled.has(op));
740
781
  }
741
782
  //#endregion
783
+ //#region src/registry/resolveTenantPurge.ts
784
+ const DEFAULT_PRIORITY = 100;
785
+ function resolveTenantPurge(input) {
786
+ const { resourceName, tenantField, onTenantDelete } = input;
787
+ if (onTenantDelete) {
788
+ assertTenantFieldUsable(resourceName, tenantField, onTenantDelete.strategy.type);
789
+ return {
790
+ strategy: onTenantDelete.strategy,
791
+ priority: onTenantDelete.priority ?? DEFAULT_PRIORITY,
792
+ batchSize: onTenantDelete.batchSize,
793
+ source: "declared"
794
+ };
795
+ }
796
+ return {
797
+ strategy: {
798
+ type: "skip",
799
+ reason: "no `onTenantDelete` declared"
800
+ },
801
+ priority: DEFAULT_PRIORITY,
802
+ source: "disabled"
803
+ };
804
+ }
805
+ /**
806
+ * Boot-time invariant: any non-skip strategy requires a real
807
+ * `tenantField`. Company-wide tables (`tenantField: false`) can't be
808
+ * cascaded by org. Throw rather than silently skip so the misconfig
809
+ * surfaces in CI / the auth-event path instead of leaking org data
810
+ * on the next delete.
811
+ */
812
+ function assertTenantFieldUsable(resourceName, tenantField, strategyType) {
813
+ if (tenantField === false) throw new Error(`[Arc/Cascade] Resource '${resourceName}' declares onTenantDelete (strategy: ${strategyType}) but \`tenantField: false\`. Company-wide resources can't be cascaded by org — set a real \`tenantField\` or remove the \`onTenantDelete\` declaration.`);
814
+ }
815
+ //#endregion
742
816
  //#region src/core/defineResource/plugin.ts
743
817
  /**
744
818
  * Build the CRUD schema map from the adapter's `OpenApiSchemas` plus
@@ -896,6 +970,7 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
896
970
  const handlers = {};
897
971
  const permissions = {};
898
972
  const schemas = {};
973
+ const idLessActionNames = [];
899
974
  for (const [name, entry] of Object.entries(actions)) {
900
975
  const explicit = typeof entry !== "function" && entry.permissions ? entry.permissions : void 0;
901
976
  if (typeof entry === "function") handlers[name] = entry;
@@ -903,6 +978,7 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
903
978
  const def = entry;
904
979
  handlers[name] = def.handler;
905
980
  if (def.schema) schemas[name] = def.schema;
981
+ if (def.id === false) idLessActionNames.push(name);
906
982
  }
907
983
  const effective = resolveActionPermission({
908
984
  action: entry,
@@ -922,7 +998,8 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
922
998
  tag,
923
999
  actions: handlers,
924
1000
  actionPermissions: permissions,
925
- actionSchemas: schemas
1001
+ actionSchemas: schemas,
1002
+ idLessActionNames
926
1003
  };
927
1004
  }
928
1005
  /**
@@ -936,9 +1013,13 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
936
1013
  */
937
1014
  function buildResourcePlugin(resource) {
938
1015
  return async function resourcePlugin(fastify, _opts) {
939
- const sharedRoot = fastify.server ?? fastify;
1016
+ const sharedRoot = fastify.server;
940
1017
  const isFirstMount = !resource._sharedStateRegisteredOn.has(sharedRoot);
941
1018
  if (isFirstMount) resource._sharedStateRegisteredOn.add(sharedRoot);
1019
+ if (isFirstMount && resource._diagnostics?.length) for (const diagnostic of resource._diagnostics) {
1020
+ const level = diagnostic.severity === "info" ? "info" : "warn";
1021
+ fastify.log?.[level]?.(diagnostic.message);
1022
+ }
942
1023
  const arc = fastify.arc;
943
1024
  if (isFirstMount && arc?.registry && resource._registryMeta) try {
944
1025
  arc.registry.register(resource, resource._registryMeta);
@@ -952,8 +1033,7 @@ function buildResourcePlugin(resource) {
952
1033
  handler: hook.handler,
953
1034
  priority: hook.priority
954
1035
  });
955
- const registerRule = fastify.registerCacheInvalidationRule;
956
- if (isFirstMount && resource.cache?.invalidateOn && typeof registerRule === "function") for (const [pattern, tags] of Object.entries(resource.cache.invalidateOn)) registerRule({
1036
+ if (isFirstMount && resource.cache?.invalidateOn && fastify.registerCacheInvalidationRule) for (const [pattern, tags] of Object.entries(resource.cache.invalidateOn)) fastify.registerCacheInvalidationRule({
957
1037
  pattern,
958
1038
  tags
959
1039
  });
@@ -984,7 +1064,7 @@ function buildResourcePlugin(resource) {
984
1064
  idField: resource.idField
985
1065
  });
986
1066
  if (resource.actions && Object.keys(resource.actions).length > 0) {
987
- const { createActionRouter } = await import("./createActionRouter-S3MLVYot.mjs").then((n) => n.n);
1067
+ const { createActionRouter } = await import("./createActionRouter-DUpN3Dd1.mjs").then((n) => n.n);
988
1068
  createActionRouter(typedInstance, {
989
1069
  ...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
990
1070
  resourceName: resource.name,
@@ -998,8 +1078,12 @@ function buildResourcePlugin(resource) {
998
1078
  });
999
1079
  }
1000
1080
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
1001
- const { createAggregationRouter } = await import("./createAggregationRouter-B0bPDf5b.mjs");
1002
- const repoForAgg = resource.controller?.repository;
1081
+ const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-Dq-TUCuY.mjs"), import("./validate-By96rH0r.mjs").then((n) => n.i)]);
1082
+ const repoForAgg = resource.controller?.repository ?? resource.repository;
1083
+ if (!adapterSupportsAggregate(repoForAgg)) {
1084
+ const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
1085
+ if (undispatchable.length > 0) throw new ArcAggregationConfigError(`Resource '${resource.name}' declares aggregations [${undispatchable.join(", ")}] but no repository implementing 'aggregate(req, options?)' is wired. Either (a) attach an adapter whose repo ships StandardRepo.aggregate (mongokit >= 3.13 / sqlitekit >= 0.3), (b) pass a custom controller exposing such a repository, or (c) declare 'materialized' on each aggregation so it dispatches through your own hook.`);
1086
+ }
1003
1087
  const buildOptions = (req) => {
1004
1088
  const ctrl = resource.controller;
1005
1089
  if (!ctrl?.tenantRepoOptions) return {};
@@ -1059,13 +1143,40 @@ var ResourceDefinition = class {
1059
1143
  pipe;
1060
1144
  fields;
1061
1145
  cache;
1146
+ /**
1147
+ * Per-resource MCP opt-out. `false` keeps the resource out of every
1148
+ * `mcpPlugin` registration regardless of the plugin's `expose` /
1149
+ * `include` allowlist — local opt-out is authoritative. See
1150
+ * `ResourceConfig.mcp` for the host-facing surface.
1151
+ */
1152
+ mcp;
1062
1153
  tenantField;
1154
+ /** Tenant-cleanup strategy on org delete — see `OnTenantDeleteConfig`. */
1155
+ onTenantDelete;
1156
+ /**
1157
+ * Resolved tenant-purge strategy — what `cascadeDeleteForOrganization`
1158
+ * actually runs. Computed once at boot from `onTenantDelete`. Audit /
1159
+ * introspection tooling reads this instead of re-running the rule at
1160
+ * the call site.
1161
+ */
1162
+ resolvedTenantPurge;
1063
1163
  idField;
1064
1164
  queryParser;
1065
1165
  _appliedPresets;
1066
1166
  _pendingHooks;
1067
1167
  _registryMeta;
1068
1168
  /**
1169
+ * Boot-time validation diagnostics (non-fatal — hard errors throw
1170
+ * synchronously in `validateDefineResourceConfig`). Populated when
1171
+ * the host's config contains redundant / ambiguous flags that
1172
+ * shouldn't crash the boot but the host should clean up. Flushed
1173
+ * through `fastify.log.warn` on first mount inside
1174
+ * `buildResourcePlugin` so the host's configured logger owns the
1175
+ * output — the framework never speaks to `console.*` from `src/`
1176
+ * outside of the CLI.
1177
+ */
1178
+ _diagnostics;
1179
+ /**
1069
1180
  * Per-host idempotency guard used by `buildResourcePlugin` to
1070
1181
  * skip duplicate shared-state writes when the same resource is
1071
1182
  * mounted at multiple prefixes (`/v1`, `/v2`). See the plugin
@@ -1075,8 +1186,8 @@ var ResourceDefinition = class {
1075
1186
  _sharedStateRegisteredOn = /* @__PURE__ */ new WeakSet();
1076
1187
  constructor(config) {
1077
1188
  this.name = config.name;
1078
- this.displayName = config.displayName ?? `${capitalize(config.name)}s`;
1079
- this.tag = config.tag ?? this.displayName;
1189
+ this.displayName = config.displayName ?? capitalize(config.name);
1190
+ this.tag = config.tag ?? pluralize(this.displayName);
1080
1191
  this.prefix = config.prefix ?? `/${config.name}s`;
1081
1192
  this.skipGlobalPrefix = config.skipGlobalPrefix ?? false;
1082
1193
  this.adapter = config.adapter;
@@ -1099,7 +1210,14 @@ var ResourceDefinition = class {
1099
1210
  this.pipe = config.pipe;
1100
1211
  this.fields = config.fields;
1101
1212
  this.cache = config.cache;
1213
+ this.mcp = config.mcp !== false;
1102
1214
  this.tenantField = config.tenantField;
1215
+ this.onTenantDelete = config.onTenantDelete;
1216
+ this.resolvedTenantPurge = resolveTenantPurge({
1217
+ resourceName: this.name,
1218
+ tenantField: this.tenantField,
1219
+ onTenantDelete: this.onTenantDelete
1220
+ });
1103
1221
  this.idField = config.idField;
1104
1222
  this.queryParser = config.queryParser;
1105
1223
  this._appliedPresets = config._appliedPresets ?? [];
@@ -1343,17 +1461,19 @@ const CRUD_OP_NAMES = new Set([
1343
1461
  "get"
1344
1462
  ]);
1345
1463
  /**
1346
- * Run the structural validation pipeline. Throws an `Error` with a
1347
- * resource-named message on the first failure — `defineResource()`
1348
- * surfaces it verbatim so hosts get a clear "fix this resource"
1349
- * pointer.
1464
+ * Run the structural validation pipeline.
1465
+ *
1466
+ * Throws synchronously on hard errors. Returns the (possibly empty)
1467
+ * list of non-fatal diagnostics — defineResource attaches them to the
1468
+ * resulting `ResourceDefinition` for the plugin layer to flush through
1469
+ * `fastify.log.warn` on first mount.
1350
1470
  */
1351
1471
  function validateDefineResourceConfig(config) {
1352
1472
  assertValidConfig(config, { skipControllerCheck: true });
1353
1473
  validatePermissionsShape(config);
1354
1474
  validateCustomRoutePermissions(config);
1355
1475
  validateActionsShape(config);
1356
- warnRedundantFieldRules(config);
1476
+ return collectRedundantFieldRuleDiagnostics(config);
1357
1477
  }
1358
1478
  /** Permissions must be `PermissionCheck` functions, not arbitrary values. */
1359
1479
  function validatePermissionsShape(config) {
@@ -1369,8 +1489,7 @@ function validateCustomRoutePermissions(config) {
1369
1489
  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.`);
1370
1490
  }
1371
1491
  /**
1372
- * Surface common field-rule misconfigurations at boot — non-fatal,
1373
- * just a `console.warn` so hosts notice and clean up.
1492
+ * Surface common field-rule misconfigurations as boot-time diagnostics.
1374
1493
  *
1375
1494
  * Catches:
1376
1495
  * 1. `immutable: true` + `immutableAfterCreate: true` — `immutable`
@@ -1381,19 +1500,34 @@ function validateCustomRoutePermissions(config) {
1381
1500
  * 3. `hidden: true` + `aggregable: false` — `hidden` already blocks
1382
1501
  * aggregation; `aggregable: false` is redundant.
1383
1502
  *
1384
- * NOT a hard error — write-rule overlap is harmless at runtime, just
1385
- * noisy in code review.
1503
+ * NOT hard errors — write-rule overlap is harmless at runtime, just
1504
+ * noisy in code review. Returned as `ResourceDiagnostic[]` so the
1505
+ * caller can route them through the host logger; never logged here.
1386
1506
  */
1387
- function warnRedundantFieldRules(config) {
1507
+ function collectRedundantFieldRuleDiagnostics(config) {
1388
1508
  const fieldRules = config.schemaOptions?.fieldRules;
1389
- if (!fieldRules) return;
1509
+ if (!fieldRules) return [];
1510
+ const diagnostics = [];
1390
1511
  for (const [field, rule] of Object.entries(fieldRules)) {
1391
1512
  if (!rule) continue;
1392
1513
  const r = rule;
1393
- if (r.immutable === true && r.immutableAfterCreate === true) console.warn(`[Arc] Resource '${config.name}' fieldRules.${field}: \`immutable: true\` already implies \`immutableAfterCreate: true\` — drop the second flag.`);
1394
- if (r.systemManaged === true && r.readonly === true) console.warn(`[Arc] Resource '${config.name}' fieldRules.${field}: \`systemManaged\` and \`readonly\` both strip writes — pick one (\`systemManaged\` is the canonical name).`);
1395
- if (r.hidden === true && r.aggregable === false) console.warn(`[Arc] Resource '${config.name}' fieldRules.${field}: \`hidden: true\` already blocks aggregation — \`aggregable: false\` is redundant.`);
1514
+ if (r.immutable === true && r.immutableAfterCreate === true) diagnostics.push({
1515
+ severity: "warn",
1516
+ code: "field-rule-redundant-immutable",
1517
+ message: `[Arc] Resource '${config.name}' fieldRules.${field}: \`immutable: true\` already implies \`immutableAfterCreate: true\` — drop the second flag.`
1518
+ });
1519
+ if (r.systemManaged === true && r.readonly === true) diagnostics.push({
1520
+ severity: "warn",
1521
+ code: "field-rule-redundant-system-managed",
1522
+ message: `[Arc] Resource '${config.name}' fieldRules.${field}: \`systemManaged\` and \`readonly\` both strip writes — pick one (\`systemManaged\` is the canonical name).`
1523
+ });
1524
+ if (r.hidden === true && r.aggregable === false) diagnostics.push({
1525
+ severity: "warn",
1526
+ code: "field-rule-redundant-hidden",
1527
+ message: `[Arc] Resource '${config.name}' fieldRules.${field}: \`hidden: true\` already blocks aggregation — \`aggregable: false\` is redundant.`
1528
+ });
1396
1529
  }
1530
+ return diagnostics;
1397
1531
  }
1398
1532
  /**
1399
1533
  * Actions (v2.8) — name must not collide with CRUD ops; handler +
@@ -1430,9 +1564,11 @@ function validateActionsShape(config) {
1430
1564
  * One internal boundary cast replaces N host-side casts.
1431
1565
  */
1432
1566
  function defineResource(config) {
1433
- if (!config.skipValidation) validateDefineResourceConfig(config);
1434
- const repository = config.adapter?.repository;
1435
- const configWithId = resolveIdField(config, repository);
1567
+ const normalisedConfig = resolveCrudAllowList(config);
1568
+ let diagnostics = [];
1569
+ if (!normalisedConfig.skipValidation) diagnostics = validateDefineResourceConfig(normalisedConfig);
1570
+ const repository = normalisedConfig.adapter?.repository;
1571
+ const configWithId = resolveIdField(normalisedConfig, repository);
1436
1572
  const resolvedConfig = applyPresetsAndAutoInject(configWithId);
1437
1573
  const hasCrudRoutes = computeHasCrudRoutes(resolvedConfig);
1438
1574
  const narrowedConfig = resolvedConfig;
@@ -1443,14 +1579,56 @@ function defineResource(config) {
1443
1579
  adapter: configWithId.adapter,
1444
1580
  controller
1445
1581
  });
1446
- if (!config.skipValidation && controller) resource._validateControllerMethods();
1582
+ if (!normalisedConfig.skipValidation && controller) resource._validateControllerMethods();
1447
1583
  wireHooks(resource, narrowedConfig, configWithId.hooks);
1448
- if (!config.skipRegistry) {
1584
+ if (!normalisedConfig.skipRegistry) {
1449
1585
  const registryMeta = resolveOpenApiSchemas(narrowedConfig);
1450
1586
  if (registryMeta) resource._registryMeta = registryMeta;
1451
1587
  }
1588
+ if (diagnostics.length > 0) resource._diagnostics = diagnostics;
1452
1589
  return resource;
1453
1590
  }
1591
+ /**
1592
+ * Normalise the 2.16 `crud:` positive-form allow-list into the canonical
1593
+ * `{ disabledRoutes, disableDefaultRoutes }` pair the rest of arc reads.
1594
+ *
1595
+ * Three input forms collapse to one output:
1596
+ * - `crud: false` → `disableDefaultRoutes: true`
1597
+ * - `crud: { list: true }` → `disabledRoutes: [get,create,update,delete]`
1598
+ * - legacy `disabledRoutes` → passed through unchanged
1599
+ *
1600
+ * Mutually exclusive: `crud` + `disabledRoutes` together is a config bug
1601
+ * (the host meant ONE of two intents) — throw rather than pick.
1602
+ *
1603
+ * Lifted out of the `ResourceDefinition` constructor in 2.16 so the
1604
+ * validator (Phase 1) observes the post-resolve shape — `crud: false`
1605
+ * now looks like `disableDefaultRoutes: true` to the validator, so it
1606
+ * doesn't false-positive "Data adapter required when CRUD routes are
1607
+ * enabled" on a host that explicitly opted CRUD out.
1608
+ */
1609
+ function resolveCrudAllowList(config) {
1610
+ const { crud, disabledRoutes: legacyDisabled, disableDefaultRoutes: legacyDisableAll } = config;
1611
+ if (crud === void 0) return config;
1612
+ if (legacyDisabled !== void 0) throw new Error(`[Arc] Resource '${config.name}': pass either \`crud\` (positive allow-list) or \`disabledRoutes\` (negative opt-out), not both. The positive form is the documented default going forward; drop \`disabledRoutes\` when both are set.`);
1613
+ if (crud === false) return {
1614
+ ...config,
1615
+ crud: void 0,
1616
+ disableDefaultRoutes: true
1617
+ };
1618
+ const disabled = [
1619
+ "list",
1620
+ "get",
1621
+ "create",
1622
+ "update",
1623
+ "delete"
1624
+ ].filter((op) => crud[op] !== true);
1625
+ return {
1626
+ ...config,
1627
+ crud: void 0,
1628
+ disabledRoutes: disabled,
1629
+ disableDefaultRoutes: legacyDisableAll ?? false
1630
+ };
1631
+ }
1454
1632
  //#endregion
1455
1633
  //#region src/core/defineResourceVariants.ts
1456
1634
  /**
@@ -1550,4 +1728,4 @@ function getEntityQuery(req) {
1550
1728
  return { [getEntityIdField(req)]: id };
1551
1729
  }
1552
1730
  //#endregion
1553
- export { defineResource as a, createPermissionMiddleware as c, defineResourceVariants as i, defineAggregation as l, getEntityIdField as n, ResourceDefinition as o, getEntityQuery as r, createCrudRouter as s, getEntityId as t };
1731
+ export { defineResource as a, createCrudRouter as c, defineResourceVariants as i, createPermissionMiddleware as l, getEntityIdField as n, ResourceDefinition as o, getEntityQuery as r, defineAction as s, getEntityId as t, defineAggregation as u };