@classytic/arc 2.10.8 → 2.11.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 (136) hide show
  1. package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
  2. package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +5 -5
  9. package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  10. package/dist/cache/index.d.mts +3 -2
  11. package/dist/cache/index.mjs +3 -3
  12. package/dist/cli/commands/docs.mjs +2 -2
  13. package/dist/cli/commands/generate.mjs +37 -27
  14. package/dist/cli/commands/init.mjs +46 -33
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/context/index.mjs +1 -1
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -3
  19. package/dist/core-DXdSSFW-.mjs +1037 -0
  20. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  21. package/dist/{createApp-BwnEAO2h.mjs → createApp-DvNYEhpb.mjs} +75 -27
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
  25. package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
  26. package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  27. package/dist/events/index.d.mts +3 -3
  28. package/dist/events/index.mjs +2 -2
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/factory/index.d.mts +1 -1
  31. package/dist/factory/index.mjs +2 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.d.mts +3 -3
  35. package/dist/idempotency/index.mjs +1 -1
  36. package/dist/idempotency/redis.d.mts +1 -1
  37. package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
  38. package/dist/{index-BGbpGVyM.d.mts → index-Cm0vUrr_.d.mts} +699 -494
  39. package/dist/{index-BziRPS4H.d.mts → index-DAushRTt.d.mts} +29 -10
  40. package/dist/index-DsJ1MNfC.d.mts +1179 -0
  41. package/dist/{index-EqQN6p0W.d.mts → index-t8pLpPFW.d.mts} +11 -8
  42. package/dist/index.d.mts +6 -38
  43. package/dist/index.mjs +9 -9
  44. package/dist/integrations/event-gateway.d.mts +1 -1
  45. package/dist/integrations/event-gateway.mjs +1 -1
  46. package/dist/integrations/index.d.mts +2 -2
  47. package/dist/integrations/mcp/index.d.mts +2 -2
  48. package/dist/integrations/mcp/index.mjs +1 -1
  49. package/dist/integrations/mcp/testing.d.mts +1 -1
  50. package/dist/integrations/mcp/testing.mjs +1 -1
  51. package/dist/integrations/streamline.d.mts +46 -5
  52. package/dist/integrations/streamline.mjs +50 -21
  53. package/dist/integrations/websocket-redis.d.mts +1 -1
  54. package/dist/integrations/websocket.d.mts +2 -154
  55. package/dist/integrations/websocket.mjs +292 -224
  56. package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
  57. package/dist/{loadResources-Bksk8ydA.mjs → loadResources-YNwKHvRA.mjs} +3 -1
  58. package/dist/middleware/index.d.mts +1 -1
  59. package/dist/middleware/index.mjs +1 -1
  60. package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
  61. package/dist/org/index.d.mts +1 -1
  62. package/dist/permissions/index.d.mts +1 -1
  63. package/dist/permissions/index.mjs +2 -4
  64. package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
  65. package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
  66. package/dist/pipeline/index.d.mts +1 -1
  67. package/dist/pipeline/index.mjs +1 -1
  68. package/dist/plugins/index.d.mts +4 -4
  69. package/dist/plugins/index.mjs +10 -10
  70. package/dist/plugins/response-cache.mjs +1 -1
  71. package/dist/plugins/tracing-entry.d.mts +1 -1
  72. package/dist/plugins/tracing-entry.mjs +42 -24
  73. package/dist/presets/filesUpload.d.mts +1 -1
  74. package/dist/presets/filesUpload.mjs +3 -3
  75. package/dist/presets/index.d.mts +1 -1
  76. package/dist/presets/index.mjs +1 -1
  77. package/dist/presets/multiTenant.d.mts +1 -1
  78. package/dist/presets/multiTenant.mjs +6 -0
  79. package/dist/presets/search.d.mts +1 -1
  80. package/dist/presets/search.mjs +1 -1
  81. package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
  82. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  83. package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  84. package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
  88. package/dist/routerShared-DeESFp4a.mjs +515 -0
  89. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/testing/index.d.mts +367 -711
  92. package/dist/testing/index.mjs +637 -1434
  93. package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +1 -3
  96. package/dist/{types-CVdgPXBW.d.mts → types-CgikqKAj.d.mts} +118 -19
  97. package/dist/{types-CVKBssX5.d.mts → types-D9NqiYIw.d.mts} +1 -1
  98. package/dist/utils/index.d.mts +2 -968
  99. package/dist/utils/index.mjs +5 -6
  100. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  101. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  102. package/package.json +7 -5
  103. package/skills/arc/SKILL.md +123 -38
  104. package/skills/arc/references/testing.md +212 -183
  105. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  106. package/dist/core-3MWJosCH.mjs +0 -1459
  107. package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
  108. package/dist/errors-BI8kEKsO.d.mts +0 -140
  109. package/dist/fields-CTMWOUDt.mjs +0 -126
  110. package/dist/queryParser-NR__Qiju.mjs +0 -419
  111. package/dist/types-CDnTEpga.mjs +0 -27
  112. package/dist/utils-LMwVidKy.mjs +0 -947
  113. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  114. /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  115. /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
  116. /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
  117. /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
  118. /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
  119. /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
  120. /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  121. /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  122. /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
  123. /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
  124. /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
  125. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
  126. /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
  127. /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
  128. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  129. /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
  130. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  131. /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
  132. /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
  133. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  134. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  135. /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
  136. /package/dist/{versioning-CeUXHfjw.d.mts → versioning-M9lNLhO8.d.mts} +0 -0
@@ -0,0 +1,166 @@
1
+ import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
+ import { a as buildAuthMiddlewareForPermissions, c as buildPreHandlerChain, d as resolveRouterPluginMw, f as selectPluginMw, l as buildRateLimitConfig, n as buildActionPipelineHandler, r as buildArcDecorator, t as buildActionPermissionMw, u as resolvePipelineSteps, v as sendControllerResponse } from "./routerShared-DeESFp4a.mjs";
3
+ import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-BlG9bY7v.mjs";
4
+ //#region src/core/createActionRouter.ts
5
+ var createActionRouter_exports = /* @__PURE__ */ __exportAll({
6
+ buildActionBodySchema: () => buildActionBodySchema,
7
+ createActionRouter: () => createActionRouter
8
+ });
9
+ /**
10
+ * Register the unified action endpoint: `POST /:id/action`.
11
+ *
12
+ * Shares every lifecycle primitive with the CRUD router — the preHandler
13
+ * chain, the arc decorator, idempotency, rate-limit, and the response
14
+ * shaper. The only thing that stays local is the dynamic permission check
15
+ * (keyed by `body.action` at request time).
16
+ */
17
+ function createActionRouter(fastify, config) {
18
+ const { tag, resourceName = tag ?? "action", actions, actionPermissions = {}, actionSchemas = {}, globalAuth, onError, fields: fieldPermissions, schemaOptions, permissions: resourcePermissions, routeGuards = [], pipeline, rateLimit } = config;
19
+ const actionEnum = Object.keys(actions);
20
+ if (actionEnum.length === 0) {
21
+ fastify.log.warn("[createActionRouter] No actions defined, skipping route creation");
22
+ return;
23
+ }
24
+ const bodySchema = buildActionBodySchema(actionEnum, actionSchemas);
25
+ const routeSchema = {
26
+ tags: tag ? [tag] : void 0,
27
+ summary: `Perform action (${actionEnum.join("/")})`,
28
+ description: buildActionDescription(actions, actionPermissions),
29
+ params: {
30
+ type: "object",
31
+ properties: { id: {
32
+ type: "string",
33
+ description: "Resource ID"
34
+ } },
35
+ required: ["id"]
36
+ },
37
+ body: bodySchema
38
+ };
39
+ const arcDecorator = buildArcDecorator({
40
+ resourceName,
41
+ schemaOptions,
42
+ permissions: resourcePermissions,
43
+ hooks: fastify.arc?.hooks,
44
+ events: fastify.events,
45
+ fields: fieldPermissions
46
+ });
47
+ const authMw = buildAuthMiddlewareForPermissions(fastify, actionEnum.map((name) => actionPermissions[name] ?? globalAuth));
48
+ const pluginMw = resolveRouterPluginMw(fastify, false);
49
+ const wrappedHandlers = /* @__PURE__ */ new Map();
50
+ for (const [name, handler] of Object.entries(actions)) {
51
+ const steps = resolvePipelineSteps(pipeline, name);
52
+ wrappedHandlers.set(name, buildActionPipelineHandler(handler, steps, name, resourceName));
53
+ }
54
+ const preHandler = buildPreHandlerChain({
55
+ arcDecorator,
56
+ authMw,
57
+ permissionMw: buildActionPermissionMw(actionEnum, actionPermissions, globalAuth, resourceName),
58
+ pluginMw: selectPluginMw("POST", pluginMw),
59
+ routeGuards
60
+ });
61
+ const rateLimitConfig = buildRateLimitConfig(rateLimit);
62
+ fastify.route({
63
+ method: "POST",
64
+ url: "/:id/action",
65
+ schema: routeSchema,
66
+ preHandler: preHandler.length > 0 ? preHandler : void 0,
67
+ ...rateLimitConfig ? { config: rateLimitConfig } : {},
68
+ handler: async (req, reply) => {
69
+ const { action, ...data } = req.body;
70
+ const { id } = req.params;
71
+ const handler = wrappedHandlers.get(action);
72
+ if (!handler) return sendControllerResponse(reply, {
73
+ success: false,
74
+ status: 400,
75
+ error: `Invalid action '${action}'. Valid actions: ${actionEnum.join(", ")}`,
76
+ meta: { validActions: actionEnum }
77
+ }, req);
78
+ try {
79
+ return sendControllerResponse(reply, await handler(id, data, req), req);
80
+ } catch (error) {
81
+ if (onError) {
82
+ const { statusCode, error: errorMsg, code } = onError(error, action, id);
83
+ return sendControllerResponse(reply, {
84
+ success: false,
85
+ status: statusCode,
86
+ error: errorMsg,
87
+ ...code ? { meta: { code } } : {}
88
+ }, req);
89
+ }
90
+ const err = error;
91
+ const statusCode = err.statusCode || err.status || 500;
92
+ const errorCode = err.code || "ACTION_FAILED";
93
+ if (statusCode >= 500) req.log.error({
94
+ err: error,
95
+ action,
96
+ id
97
+ }, "Action handler error");
98
+ return sendControllerResponse(reply, {
99
+ success: false,
100
+ status: statusCode,
101
+ error: err.message || `Failed to execute '${action}' action`,
102
+ meta: { code: errorCode }
103
+ }, req);
104
+ }
105
+ }
106
+ });
107
+ fastify.log.debug({
108
+ actions: actionEnum,
109
+ tag,
110
+ resourceName
111
+ }, "[createActionRouter] Registered action endpoint: POST /:id/action");
112
+ }
113
+ /**
114
+ * Build a discriminated body schema for the unified action endpoint.
115
+ *
116
+ * Produces a schema of the form:
117
+ * ```json
118
+ * {
119
+ * "type": "object",
120
+ * "required": ["action"],
121
+ * "oneOf": [
122
+ * { "properties": { "action": { "const": "dispatch" }, "carrier": {...} }, "required": ["action", "carrier"] },
123
+ * { "properties": { "action": { "const": "approve" } }, "required": ["action"] }
124
+ * ]
125
+ * }
126
+ * ```
127
+ *
128
+ * AJV validates this natively, so an action call missing required fields is
129
+ * rejected with HTTP 400 before the handler ever runs.
130
+ *
131
+ * Exported so OpenAPI generation and MCP tool generation can reuse the same
132
+ * schema shape (single source of truth).
133
+ */
134
+ function buildActionBodySchema(actionEnum, actionSchemas = {}) {
135
+ const branches = [];
136
+ for (const actionName of actionEnum) {
137
+ const ir = normalizeSchemaIR(actionSchemas[actionName]);
138
+ branches.push(schemaIRToJsonSchemaBranch(ir, {
139
+ properties: { action: {
140
+ type: "string",
141
+ const: actionName
142
+ } },
143
+ required: ["action"]
144
+ }));
145
+ }
146
+ return {
147
+ type: "object",
148
+ required: ["action"],
149
+ oneOf: branches
150
+ };
151
+ }
152
+ /**
153
+ * Build OpenAPI description with action list + role hints.
154
+ * Reads `_roles` metadata from permission checks for docs.
155
+ */
156
+ function buildActionDescription(actions, actionPermissions) {
157
+ const lines = ["Unified action endpoint for state transitions.\n\n**Available actions:**"];
158
+ Object.keys(actions).forEach((action) => {
159
+ const roles = actionPermissions[action]?._roles;
160
+ const roleStr = roles?.length ? ` (requires: ${roles.join(" or ")})` : "";
161
+ lines.push(`- \`${action}\`${roleStr}`);
162
+ });
163
+ return lines.join("\n");
164
+ }
165
+ //#endregion
166
+ export { createActionRouter_exports as n, buildActionBodySchema as t };
@@ -204,7 +204,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
204
204
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
205
205
  trackPlugin("arc-core");
206
206
  if (config.arcPlugins?.events !== false) {
207
- const { default: eventPlugin } = await import("./eventPlugin-ByU4Cv0e.mjs").then((n) => n.n);
207
+ const { default: eventPlugin } = await import("./eventPlugin--5HIkdPU.mjs").then((n) => n.n);
208
208
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
209
209
  await fastify.register(eventPlugin, {
210
210
  ...eventOpts,
@@ -240,15 +240,15 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
240
240
  trackPlugin("arc-graceful-shutdown");
241
241
  }
242
242
  if (config.arcPlugins?.caching) {
243
- const { default: cachingPlugin } = await import("./caching-3h93rkJM.mjs").then((n) => n.r);
243
+ const { default: cachingPlugin } = await import("./caching-CheW3m-S.mjs").then((n) => n.r);
244
244
  const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
245
245
  await fastify.register(cachingPlugin, opts);
246
246
  trackPlugin("arc-caching", opts);
247
247
  }
248
248
  if (config.arcPlugins?.queryCache) {
249
- const { queryCachePlugin } = await import("./queryCachePlugin-ChLNZvFT.mjs").then((n) => n.n);
249
+ const { queryCachePlugin } = await import("./queryCachePlugin-Bq6bO6vc.mjs").then((n) => n.n);
250
250
  const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
251
- const store = config.stores?.queryCache ?? new (await (import("./memory-DqI-449b.mjs").then((n) => n.n))).MemoryCacheStore();
251
+ const store = config.stores?.queryCache ?? new (await (import("./memory-DikHSvWa.mjs").then((n) => n.n))).MemoryCacheStore();
252
252
  await fastify.register(queryCachePlugin, {
253
253
  store,
254
254
  ...opts
@@ -257,19 +257,19 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
257
257
  }
258
258
  if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
259
259
  else {
260
- const { default: ssePlugin } = await import("./sse-D8UeDwis.mjs").then((n) => n.r);
260
+ const { default: ssePlugin } = await import("./sse-V7aXc3bW.mjs").then((n) => n.r);
261
261
  const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
262
262
  await fastify.register(ssePlugin, opts);
263
263
  trackPlugin("arc-sse", opts);
264
264
  }
265
265
  if (config.arcPlugins?.metrics) {
266
- const { default: metricsPlugin } = await import("./metrics-TuOmguhi.mjs").then((n) => n.r);
266
+ const { default: metricsPlugin } = await import("./metrics-Csh4nsvv.mjs").then((n) => n.r);
267
267
  const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
268
268
  await fastify.register(metricsPlugin, opts);
269
269
  trackPlugin("arc-metrics", opts);
270
270
  }
271
271
  if (config.arcPlugins?.versioning) {
272
- const { default: versioningPlugin } = await import("./versioning-B6mimogM.mjs").then((n) => n.r);
272
+ const { default: versioningPlugin } = await import("./versioning-CGPjkqAg.mjs").then((n) => n.r);
273
273
  await fastify.register(versioningPlugin, config.arcPlugins.versioning);
274
274
  trackPlugin("arc-versioning", config.arcPlugins.versioning);
275
275
  }
@@ -338,7 +338,7 @@ async function registerAuth(fastify, config, trackPlugin) {
338
338
  */
339
339
  async function registerElevation(fastify, config, trackPlugin) {
340
340
  if (!config.elevation) return;
341
- const { elevationPlugin } = await import("./elevation-Dci0AYLT.mjs").then((n) => n.r);
341
+ const { elevationPlugin } = await import("./elevation-DOFoxoDs.mjs").then((n) => n.r);
342
342
  await fastify.register(elevationPlugin, config.elevation);
343
343
  trackPlugin("arc-elevation", config.elevation);
344
344
  fastify.log.debug("Elevation plugin enabled");
@@ -348,7 +348,7 @@ async function registerElevation(fastify, config, trackPlugin) {
348
348
  */
349
349
  async function registerErrorHandler(fastify, config, trackPlugin) {
350
350
  if (config.errorHandler === false) return;
351
- const { errorHandlerPlugin } = await import("./errorHandler-CSxe7KIM.mjs").then((n) => n.r);
351
+ const { errorHandlerPlugin } = await import("./errorHandler-BQm8ZxTK.mjs").then((n) => n.r);
352
352
  const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
353
353
  await fastify.register(errorHandlerPlugin, errorOpts);
354
354
  trackPlugin("arc-error-handler", errorOpts);
@@ -386,6 +386,9 @@ function createOptionalAuthenticate(authenticate) {
386
386
  }
387
387
  //#endregion
388
388
  //#region src/factory/registerResources.ts
389
+ function isResourcesFactory(value) {
390
+ return typeof value === "function";
391
+ }
389
392
  /** Register a single resource with descriptive error on failure. */
390
393
  async function registerOne(parent, resource) {
391
394
  const name = resource.name ?? "unknown";
@@ -394,18 +397,31 @@ async function registerOne(parent, resource) {
394
397
  } catch (err) {
395
398
  const msg = err instanceof Error ? err.message : String(err);
396
399
  parent.log.error(`Failed to register resource "${name}": ${msg}`);
397
- throw new Error(`Resource "${name}" failed to register: ${msg}. Check the resource definition, adapter, and permissions.`);
400
+ throw new Error(`Resource "${name}" failed to register: ${msg}. Check the resource definition, adapter, and permissions.`, { cause: err });
398
401
  }
399
402
  }
400
403
  /**
401
404
  * Execute the full resource lifecycle:
402
- * 1. plugins() — infra (DB, docs, webhooks)
403
- * 2. bootstrap[] — domain init (singletons, event handlers)
404
- * 3. resources[] auto-discovered routes (split by prefix)
405
- * 4. afterResources() post-registration wiring
406
- * 5. onReady/onClose — lifecycle hooks
405
+ * 1. plugins() — infra (DB, docs, webhooks)
406
+ * 2. bootstrap[] — domain init (singletons, event handlers)
407
+ * 3. resources factory (if any) resolved AFTER bootstrap, so engine-backed
408
+ * adapters can `await ensureEngine()` and pass
409
+ * live models/repos into `defineResource(...)`
410
+ * 4. resources[] — register each (split by prefix)
411
+ * 5. afterResources() — post-registration wiring
412
+ * 6. onReady/onClose — lifecycle hooks
407
413
  */
408
414
  async function registerResources(fastify, config) {
415
+ if (config.preset === "production") {
416
+ if (config.strictResources === void 0) config = {
417
+ ...config,
418
+ strictResources: true
419
+ };
420
+ if (config.strictResourceDir === void 0) config = {
421
+ ...config,
422
+ strictResourceDir: true
423
+ };
424
+ }
409
425
  if (config.plugins) {
410
426
  await config.plugins(fastify);
411
427
  fastify.log.debug("Custom plugins registered");
@@ -414,33 +430,65 @@ async function registerResources(fastify, config) {
414
430
  for (const init of config.bootstrap) await init(fastify);
415
431
  fastify.log.debug(`${config.bootstrap.length} bootstrap function(s) executed`);
416
432
  }
417
- if (!config.resources?.length && config.resourceDir) {
418
- const { loadResources } = await import("./loadResources-Bksk8ydA.mjs").then((n) => n.n);
419
- const { resolve } = await import("node:path");
420
- const dir = resolve(config.resourceDir);
433
+ let resolvedResources;
434
+ if (isResourcesFactory(config.resources)) {
435
+ try {
436
+ resolvedResources = await config.resources(fastify);
437
+ } catch (err) {
438
+ const msg = err instanceof Error ? err.message : String(err);
439
+ fastify.log.error(`Resources factory threw during boot: ${msg}`);
440
+ throw new Error(`[arc] resources factory threw: ${msg}. Check engine bootstrap order (did you forget a bootstrap step?) and that \`defineResource(...)\` calls inside the factory receive fully-booted adapters / repositories.`, { cause: err });
441
+ }
421
442
  config = {
422
443
  ...config,
423
- resources: await loadResources(dir, { logger: fastify.log })
444
+ resources: resolvedResources
424
445
  };
446
+ } else resolvedResources = config.resources;
447
+ let discoveryRawDir;
448
+ let discoveryPath;
449
+ let discoveryYieldedZero = false;
450
+ if (resolvedResources === void 0 && config.resourceDir) {
451
+ const { loadResources } = await import("./loadResources-YNwKHvRA.mjs").then((n) => n.n);
452
+ const { resolve, dirname } = await import("node:path");
453
+ const { fileURLToPath } = await import("node:url");
454
+ const rawDir = config.resourceDir;
455
+ const dir = rawDir.startsWith("file://") ? dirname(fileURLToPath(rawDir)) : resolve(rawDir);
456
+ discoveryRawDir = rawDir;
457
+ discoveryPath = dir;
458
+ const discovered = await loadResources(dir, { logger: fastify.log });
459
+ if (discovered.length === 0) {
460
+ if (config.strictResourceDir) throw new Error(`[arc] loadResources: resourceDir "${rawDir}" resolved to "${dir}" but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.`);
461
+ discoveryYieldedZero = true;
462
+ }
463
+ resolvedResources = discovered;
425
464
  }
426
- if (config.resources?.length) {
465
+ if (resolvedResources && resolvedResources.length > 0) {
427
466
  const seen = /* @__PURE__ */ new Set();
428
- for (const resource of config.resources) if (resource.name) {
429
- if (seen.has(resource.name)) fastify.log.warn(`Duplicate resource name "${resource.name}" detected. This will cause route conflicts. Check your resources array and loadResources() output.`);
467
+ for (const resource of resolvedResources) if (resource.name) {
468
+ if (seen.has(resource.name)) {
469
+ const msg = `Duplicate resource name "${resource.name}" detected. This will cause route conflicts. Check your resources array and loadResources() output. Common cause: stale compiled files in dist/ alongside src/. Use \`strictResources: true\` to fail boot.`;
470
+ if (config.strictResources) throw new Error(msg);
471
+ fastify.log.warn(msg);
472
+ }
430
473
  seen.add(resource.name);
431
474
  }
432
475
  const prefixed = [];
433
476
  const root = [];
434
- for (const resource of config.resources) if (resource.skipGlobalPrefix) root.push(resource);
477
+ for (const resource of resolvedResources) if (resource.skipGlobalPrefix) root.push(resource);
435
478
  else prefixed.push(resource);
436
479
  for (const resource of root) await registerOne(fastify, resource);
437
480
  if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
438
481
  for (const resource of prefixed) await registerOne(scoped, resource);
439
482
  }, { prefix: config.resourcePrefix });
440
483
  else for (const resource of prefixed) await registerOne(fastify, resource);
441
- const names = config.resources.map((r) => r.name ?? "?").join(", ");
484
+ const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
485
+ const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
486
+ fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
487
+ } else {
442
488
  const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
443
- fastify.log.info(`${config.resources.length} resource(s) registered${prefix}: ${names}`);
489
+ const scanned = discoveryPath ? ` — resourceDir "${discoveryRawDir}" resolved to "${discoveryPath}"` : "";
490
+ const hints = discoveryYieldedZero ? ` but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.` : "";
491
+ fastify.log.warn(`0 resources registered${prefix}${scanned}${hints}`);
444
492
  }
445
493
  if (config.afterResources) {
446
494
  await config.afterResources(fastify);
@@ -764,7 +812,7 @@ async function createApp(options) {
764
812
  await registerErrorHandler(fastify, config, trackPlugin);
765
813
  await registerResources(fastify, config);
766
814
  if (config.replyHelpers) {
767
- const { replyHelpersPlugin } = await import("./replyHelpers-BLojtuvR.mjs").then((n) => n.n);
815
+ const { replyHelpersPlugin } = await import("./replyHelpers-ByllIXXV.mjs").then((n) => n.n);
768
816
  await fastify.register(replyHelpersPlugin);
769
817
  }
770
818
  if (config.serializeBigInt) fastify.addHook("preSerialization", async (_request, _reply, payload) => {
@@ -1,4 +1,4 @@
1
- import { p as RegistryEntry } from "../index-BGbpGVyM.mjs";
1
+ import { p as RegistryEntry } from "../index-Cm0vUrr_.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-Bapitwvd.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
@@ -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-DpNpqBmo.mjs";
1
+ import { t as getUserRoles } from "../types-DV9WDfeg.mjs";
2
+ import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-C0L9ar7m.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({
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { p as isArcError } from "./errors-BqdUDja_.mjs";
2
+ import { p as isArcError } from "./errors-D5c-5BJL.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/plugins/errorHandler.ts
5
5
  var errorHandler_exports = /* @__PURE__ */ __exportAll({
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { t as requestContext } from "./requestContext-C38GskNt.mjs";
2
+ import { t as requestContext } from "./requestContext-CfRkaxwf.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/events/EventTransport.ts
5
5
  /**
@@ -1,8 +1,8 @@
1
- import { _n as RepositoryLike } from "../index-BGbpGVyM.mjs";
1
+ import { jn as RepositoryLike } from "../index-Cm0vUrr_.mjs";
2
2
  import { a as EventMeta, c as MemoryEventTransportOptions, d as createEvent, i as EventLogger, l as PublishManyResult, n as DomainEvent, o as EventTransport, r as EventHandler, s as MemoryEventTransport, t as DeadLetteredEvent, u as createChildEvent } from "../EventTransport-CfVEGaEl.mjs";
3
- 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-D1ThQ1Pp.mjs";
3
+ 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-CUNjYYRY.mjs";
4
4
  import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
5
- import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-bkO88VHx.mjs";
5
+ import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-CM8TXTix.mjs";
6
6
 
7
7
  //#region src/events/eventTypes.d.ts
8
8
  /**
@@ -1,5 +1,5 @@
1
- import { a as MemoryEventTransport, i as withRetry, o as createChildEvent, r as createDeadLetterPublisher, s as createEvent, t as eventPlugin } from "../eventPlugin-ByU4Cv0e.mjs";
2
- import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DYYUQbQN.mjs";
1
+ import { a as MemoryEventTransport, i as withRetry, o as createChildEvent, r as createDeadLetterPublisher, s as createEvent, t as eventPlugin } from "../eventPlugin--5HIkdPU.mjs";
2
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-BhrzxvyQ.mjs";
3
3
  import { and, anyOf, eq, lte, ne, or } from "@classytic/repo-core/filter";
4
4
  import { update } from "@classytic/repo-core/update";
5
5
  //#region src/events/defineEvent.ts
@@ -1,2 +1,2 @@
1
- import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-bkO88VHx.mjs";
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-CM8TXTix.mjs";
2
2
  export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
@@ -1,4 +1,4 @@
1
- import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-CVdgPXBW.mjs";
1
+ import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-CgikqKAj.mjs";
2
2
  import { FastifyInstance } from "fastify";
3
3
 
4
4
  //#region src/factory/createApp.d.ts
@@ -1,5 +1,5 @@
1
- import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-BwnEAO2h.mjs";
2
- import { t as loadResources } from "../loadResources-Bksk8ydA.mjs";
1
+ import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-DvNYEhpb.mjs";
2
+ import { t as loadResources } from "../loadResources-YNwKHvRA.mjs";
3
3
  //#region src/factory/edge.ts
4
4
  /**
5
5
  * Convert a Fastify app into a Web Standards fetch handler.
@@ -1,2 +1,2 @@
1
- import { $ as beforeCreate, G as HookOperation, H as DefineHookOptions, J as HookSystem, K as HookPhase, Q as afterUpdate, U as HookContext, W as HookHandler, X as afterCreate, Y as HookSystemOptions, Z as afterDelete, et as beforeDelete, nt as createHookSystem, q as HookRegistration, rt as defineHook, tt as beforeUpdate } from "../index-BGbpGVyM.mjs";
1
+ import { Cn as beforeUpdate, Sn as beforeDelete, Tn as defineHook, _n as HookSystemOptions, bn as afterUpdate, dn as HookContext, fn as HookHandler, gn as HookSystem, hn as HookRegistration, mn as HookPhase, pn as HookOperation, un as DefineHookOptions, vn as afterCreate, wn as createHookSystem, xn as beforeCreate, yn as afterDelete } from "../index-Cm0vUrr_.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,2 +1,2 @@
1
- import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-BjFu7zf1.mjs";
1
+ import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-CGsMd6oK.mjs";
2
2
  export { HookSystem, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,6 +1,6 @@
1
- import { _n as RepositoryLike } from "../index-BGbpGVyM.mjs";
2
- import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-B-pe8fhj.mjs";
3
- import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-MXLp1oOf.mjs";
1
+ import { jn as RepositoryLike } from "../index-Cm0vUrr_.mjs";
2
+ import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-CkkWm5uR.mjs";
3
+ import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-Cm1gnRDf.mjs";
4
4
  import { FastifyPluginAsync } from "fastify";
5
5
 
6
6
  //#region src/idempotency/idempotencyPlugin.d.ts
@@ -1,4 +1,4 @@
1
- import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DYYUQbQN.mjs";
1
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-BhrzxvyQ.mjs";
2
2
  import { createHash } from "node:crypto";
3
3
  import fp from "fastify-plugin";
4
4
  import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
@@ -1,2 +1,2 @@
1
- import { a as UpstashRedisLike, i as RedisIdempotencyStoreOptions, n as RedisClient, o as ioredisAsIdempotencyClient, r as RedisIdempotencyStore, s as upstashAsIdempotencyClient, t as IoredisLike } from "../redis-MXLp1oOf.mjs";
1
+ import { a as UpstashRedisLike, i as RedisIdempotencyStoreOptions, n as RedisClient, o as ioredisAsIdempotencyClient, r as RedisIdempotencyStore, s as upstashAsIdempotencyClient, t as IoredisLike } from "../redis-Cm1gnRDf.mjs";
2
2
  export { type IoredisLike, type RedisClient, RedisIdempotencyStore, type RedisIdempotencyStoreOptions, type UpstashRedisLike, ioredisAsIdempotencyClient, upstashAsIdempotencyClient };
@@ -1,7 +1,7 @@
1
+ import { r as CacheStore, t as CacheLogger } from "./interface-Da0r7Lna.mjs";
1
2
  import { r as RequestScope } from "./types-tgR4Pt8F.mjs";
2
3
  import { c as PermissionCheck, l as PermissionContext, u as PermissionResult } from "./fields-C8Y0XLAu.mjs";
3
- import { r as CacheStore, t as CacheLogger } from "./interface-yhyb_pLY.mjs";
4
- import { FastifyRequest } from "fastify";
4
+ import { FastifyReply, FastifyRequest } from "fastify";
5
5
 
6
6
  //#region src/permissions/applyPermissionResult.d.ts
7
7
  /**