@classytic/arc 2.10.3 → 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 (153) hide show
  1. package/README.md +1 -1
  2. package/dist/{BaseController-CbKKIflT.mjs → BaseController-JNV08qOT.mjs} +595 -537
  3. package/dist/{queryCachePlugin-BKbWjgDG.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  4. package/dist/actionPermissions-C8YYU92K.mjs +22 -0
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  8. package/dist/audit/index.d.mts +2 -2
  9. package/dist/audit/index.mjs +15 -17
  10. package/dist/auth/index.d.mts +4 -4
  11. package/dist/auth/index.mjs +3 -3
  12. package/dist/auth/redis-session.d.mts +1 -1
  13. package/dist/{betterAuthOpenApi-BBRVhjQN.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  14. package/dist/cache/index.d.mts +3 -2
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/generate.mjs +37 -27
  18. package/dist/cli/commands/init.mjs +47 -34
  19. package/dist/cli/commands/introspect.mjs +1 -1
  20. package/dist/context/index.d.mts +58 -0
  21. package/dist/context/index.mjs +2 -0
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +4 -3
  24. package/dist/core-DXdSSFW-.mjs +1037 -0
  25. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  26. package/dist/{createApp-BuvPma24.mjs → createApp-DvNYEhpb.mjs} +118 -36
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +1 -1
  29. package/dist/{elevation-C7hgL_aI.mjs → elevation-DOFoxoDs.mjs} +1 -1
  30. package/dist/errorHandler-Co3lnVmJ.d.mts +114 -0
  31. package/dist/{eventPlugin-DCUjuiQT.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  32. package/dist/{eventPlugin-CxWgpd6K.d.mts → eventPlugin-CUNjYYRY.d.mts} +1 -1
  33. package/dist/events/index.d.mts +4 -4
  34. package/dist/events/index.mjs +69 -51
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis.d.mts +1 -1
  37. package/dist/factory/index.d.mts +1 -1
  38. package/dist/factory/index.mjs +2 -2
  39. package/dist/{fields-Lo1VUDpt.d.mts → fields-C8Y0XLAu.d.mts} +1 -1
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/hooks/index.mjs +1 -1
  42. package/dist/idempotency/index.d.mts +3 -3
  43. package/dist/idempotency/index.mjs +38 -27
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{index-ChIw3776.d.mts → index-BYCqHCVu.d.mts} +4 -4
  46. package/dist/{index-Cl0uoKd5.d.mts → index-Cm0vUrr_.d.mts} +2100 -1688
  47. package/dist/{index-DStwgFUK.d.mts → index-DAushRTt.d.mts} +29 -10
  48. package/dist/index-DsJ1MNfC.d.mts +1179 -0
  49. package/dist/{index-8qw4y6ff.d.mts → index-t8pLpPFW.d.mts} +13 -10
  50. package/dist/index.d.mts +7 -251
  51. package/dist/index.mjs +8 -128
  52. package/dist/integrations/event-gateway.d.mts +2 -2
  53. package/dist/integrations/event-gateway.mjs +1 -1
  54. package/dist/integrations/index.d.mts +2 -2
  55. package/dist/integrations/mcp/index.d.mts +2 -2
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -1
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/integrations/streamline.d.mts +46 -5
  60. package/dist/integrations/streamline.mjs +50 -21
  61. package/dist/integrations/websocket-redis.d.mts +1 -1
  62. package/dist/integrations/websocket.d.mts +2 -154
  63. package/dist/integrations/websocket.mjs +292 -224
  64. package/dist/{keys-qcD-TVJl.mjs → keys-CARyUjiR.mjs} +2 -0
  65. package/dist/{loadResources-BAzJItAJ.mjs → loadResources-YNwKHvRA.mjs} +3 -1
  66. package/dist/logger/index.d.mts +81 -0
  67. package/dist/{logger-DLg8-Ueg.mjs → logger/index.mjs} +1 -6
  68. package/dist/middleware/index.d.mts +109 -0
  69. package/dist/middleware/index.mjs +70 -0
  70. package/dist/multipartBody-CvTR1Un6.mjs +123 -0
  71. package/dist/{openapi-B5F8AddX.mjs → openapi-C0L9ar7m.mjs} +9 -7
  72. package/dist/org/index.d.mts +2 -2
  73. package/dist/permissions/index.d.mts +2 -2
  74. package/dist/permissions/index.mjs +1 -3
  75. package/dist/{permissions-Dk6mshja.mjs → permissions-B4vU9L0Q.mjs} +220 -2
  76. package/dist/pipe-DVoIheVC.mjs +62 -0
  77. package/dist/pipeline/index.d.mts +62 -0
  78. package/dist/pipeline/index.mjs +53 -0
  79. package/dist/plugins/index.d.mts +25 -5
  80. package/dist/plugins/index.mjs +10 -10
  81. package/dist/plugins/response-cache.mjs +1 -1
  82. package/dist/plugins/tracing-entry.d.mts +1 -1
  83. package/dist/plugins/tracing-entry.mjs +42 -24
  84. package/dist/presets/filesUpload.d.mts +4 -4
  85. package/dist/presets/filesUpload.mjs +255 -1
  86. package/dist/presets/index.d.mts +1 -1
  87. package/dist/presets/index.mjs +2 -2
  88. package/dist/presets/multiTenant.d.mts +1 -1
  89. package/dist/presets/multiTenant.mjs +48 -8
  90. package/dist/presets/search.d.mts +2 -2
  91. package/dist/presets/search.mjs +1 -1
  92. package/dist/{presets-fLJVXdVn.mjs → presets-k604Lj99.mjs} +1 -1
  93. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  94. package/dist/{queryCachePlugin-DQCEfJis.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  95. package/dist/{redis-DqyeggCa.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  96. package/dist/{redis-stream-CakIQmwR.d.mts → redis-stream-CM8TXTix.d.mts} +1 -1
  97. package/dist/registry/index.d.mts +1 -1
  98. package/dist/registry/index.mjs +2 -2
  99. package/dist/{requestContext-xHIKedG6.mjs → requestContext-CfRkaxwf.mjs} +1 -1
  100. package/dist/{resourceToTools-BElv3xPT.mjs → resourceToTools--okX6QBr.mjs} +534 -415
  101. package/dist/routerShared-DeESFp4a.mjs +515 -0
  102. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  103. package/dist/scope/index.d.mts +2 -2
  104. package/dist/scope/index.mjs +1 -1
  105. package/dist/{sse-yBCgOLGu.mjs → sse-V7aXc3bW.mjs} +1 -1
  106. package/dist/{store-helpers-ZCSMJJAX.mjs → store-helpers-BhrzxvyQ.mjs} +4 -0
  107. package/dist/testing/index.d.mts +367 -711
  108. package/dist/testing/index.mjs +646 -1434
  109. package/dist/testing/storageContract.d.mts +1 -1
  110. package/dist/{tracing-65B51Dw3.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  111. package/dist/types/index.d.mts +5 -5
  112. package/dist/types/index.mjs +1 -3
  113. package/dist/types/storage.d.mts +1 -1
  114. package/dist/{types-Co8k3NyS.d.mts → types-CgikqKAj.d.mts} +133 -21
  115. package/dist/{types-Btdda02s.d.mts → types-D9NqiYIw.d.mts} +1 -1
  116. package/dist/utils/index.d.mts +2 -898
  117. package/dist/utils/index.mjs +4 -5
  118. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  119. package/dist/versioning-M9lNLhO8.d.mts +117 -0
  120. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  121. package/package.json +26 -8
  122. package/skills/arc/SKILL.md +124 -39
  123. package/skills/arc/references/testing.md +212 -183
  124. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  125. package/dist/core-CcR01lup.mjs +0 -1411
  126. package/dist/createActionRouter-Bp_5c_2b.mjs +0 -249
  127. package/dist/errorHandler-DRQ3EqfL.d.mts +0 -218
  128. package/dist/errors-CCSsMpXE.d.mts +0 -140
  129. package/dist/fields-bxkeltzz.mjs +0 -126
  130. package/dist/filesUpload-t21LS-py.mjs +0 -377
  131. package/dist/queryParser-DBqBB6AC.mjs +0 -352
  132. package/dist/types-Csi3FLfq.mjs +0 -27
  133. package/dist/utils-B2fNOD_i.mjs +0 -929
  134. /package/dist/{EventTransport-CUw5NNWe.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
  135. /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  136. /package/dist/{ResourceRegistry-BPd6NQDm.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  137. /package/dist/{caching-CBpK_SCM.mjs → caching-CheW3m-S.mjs} +0 -0
  138. /package/dist/{elevation-C5SwtkAn.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  139. /package/dist/{errorHandler-Bb49BvPD.mjs → errorHandler-BQm8ZxTK.mjs} +0 -0
  140. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  141. /package/dist/{interface-CSbZdv_3.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  142. /package/dist/{interface-D218ikEo.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  143. /package/dist/{memory-B5Amv9A1.mjs → memory-DikHSvWa.mjs} +0 -0
  144. /package/dist/{metrics-DuhiSEZI.mjs → metrics-Csh4nsvv.mjs} +0 -0
  145. /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-BneOJkpi.mjs} +0 -0
  146. /package/dist/{registry-B3lRFBWo.mjs → registry-D63ee7fl.mjs} +0 -0
  147. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  148. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  149. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  150. /package/dist/{storage-CVk_SEn2.d.mts → storage-BwGQXUpd.d.mts} +0 -0
  151. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  152. /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
  153. /package/dist/{versioning-C2U_bLY0.mjs → versioning-CGPjkqAg.mjs} +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 };
@@ -116,10 +116,7 @@ const developmentPreset = {
116
116
  "x-request-id"
117
117
  ]
118
118
  },
119
- rateLimit: {
120
- max: 1e3,
121
- timeWindow: "1 minute"
122
- },
119
+ rateLimit: false,
123
120
  underPressure: {
124
121
  exposeStatusRoute: true,
125
122
  maxEventLoopDelay: 5e3
@@ -207,7 +204,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
207
204
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
208
205
  trackPlugin("arc-core");
209
206
  if (config.arcPlugins?.events !== false) {
210
- const { default: eventPlugin } = await import("./eventPlugin-DCUjuiQT.mjs").then((n) => n.n);
207
+ const { default: eventPlugin } = await import("./eventPlugin--5HIkdPU.mjs").then((n) => n.n);
211
208
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
212
209
  await fastify.register(eventPlugin, {
213
210
  ...eventOpts,
@@ -243,15 +240,15 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
243
240
  trackPlugin("arc-graceful-shutdown");
244
241
  }
245
242
  if (config.arcPlugins?.caching) {
246
- const { default: cachingPlugin } = await import("./caching-CBpK_SCM.mjs").then((n) => n.r);
243
+ const { default: cachingPlugin } = await import("./caching-CheW3m-S.mjs").then((n) => n.r);
247
244
  const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
248
245
  await fastify.register(cachingPlugin, opts);
249
246
  trackPlugin("arc-caching", opts);
250
247
  }
251
248
  if (config.arcPlugins?.queryCache) {
252
- const { queryCachePlugin } = await import("./queryCachePlugin-DQCEfJis.mjs").then((n) => n.n);
249
+ const { queryCachePlugin } = await import("./queryCachePlugin-Bq6bO6vc.mjs").then((n) => n.n);
253
250
  const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
254
- const store = config.stores?.queryCache ?? new (await (import("./memory-B5Amv9A1.mjs").then((n) => n.n))).MemoryCacheStore();
251
+ const store = config.stores?.queryCache ?? new (await (import("./memory-DikHSvWa.mjs").then((n) => n.n))).MemoryCacheStore();
255
252
  await fastify.register(queryCachePlugin, {
256
253
  store,
257
254
  ...opts
@@ -260,19 +257,19 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
260
257
  }
261
258
  if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
262
259
  else {
263
- const { default: ssePlugin } = await import("./sse-yBCgOLGu.mjs").then((n) => n.r);
260
+ const { default: ssePlugin } = await import("./sse-V7aXc3bW.mjs").then((n) => n.r);
264
261
  const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
265
262
  await fastify.register(ssePlugin, opts);
266
263
  trackPlugin("arc-sse", opts);
267
264
  }
268
265
  if (config.arcPlugins?.metrics) {
269
- const { default: metricsPlugin } = await import("./metrics-DuhiSEZI.mjs").then((n) => n.r);
266
+ const { default: metricsPlugin } = await import("./metrics-Csh4nsvv.mjs").then((n) => n.r);
270
267
  const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
271
268
  await fastify.register(metricsPlugin, opts);
272
269
  trackPlugin("arc-metrics", opts);
273
270
  }
274
271
  if (config.arcPlugins?.versioning) {
275
- const { default: versioningPlugin } = await import("./versioning-C2U_bLY0.mjs").then((n) => n.r);
272
+ const { default: versioningPlugin } = await import("./versioning-CGPjkqAg.mjs").then((n) => n.r);
276
273
  await fastify.register(versioningPlugin, config.arcPlugins.versioning);
277
274
  trackPlugin("arc-versioning", config.arcPlugins.versioning);
278
275
  }
@@ -303,7 +300,8 @@ async function registerAuth(fastify, config, trackPlugin) {
303
300
  const { plugin, openapi } = authConfig.betterAuth;
304
301
  await fastify.register(plugin);
305
302
  trackPlugin("auth-better-auth");
306
- if (openapi && !fastify.arc.externalOpenApiPaths.includes(openapi)) fastify.arc.externalOpenApiPaths.push(openapi);
303
+ const arc = fastify.arc;
304
+ if (arc && openapi && !arc.externalOpenApiPaths.includes(openapi)) arc.externalOpenApiPaths.push(openapi);
307
305
  fastify.log.debug("Better Auth authentication enabled");
308
306
  break;
309
307
  }
@@ -340,7 +338,7 @@ async function registerAuth(fastify, config, trackPlugin) {
340
338
  */
341
339
  async function registerElevation(fastify, config, trackPlugin) {
342
340
  if (!config.elevation) return;
343
- const { elevationPlugin } = await import("./elevation-C7hgL_aI.mjs").then((n) => n.r);
341
+ const { elevationPlugin } = await import("./elevation-DOFoxoDs.mjs").then((n) => n.r);
344
342
  await fastify.register(elevationPlugin, config.elevation);
345
343
  trackPlugin("arc-elevation", config.elevation);
346
344
  fastify.log.debug("Elevation plugin enabled");
@@ -350,7 +348,7 @@ async function registerElevation(fastify, config, trackPlugin) {
350
348
  */
351
349
  async function registerErrorHandler(fastify, config, trackPlugin) {
352
350
  if (config.errorHandler === false) return;
353
- const { errorHandlerPlugin } = await import("./errorHandler-Bb49BvPD.mjs").then((n) => n.r);
351
+ const { errorHandlerPlugin } = await import("./errorHandler-BQm8ZxTK.mjs").then((n) => n.r);
354
352
  const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
355
353
  await fastify.register(errorHandlerPlugin, errorOpts);
356
354
  trackPlugin("arc-error-handler", errorOpts);
@@ -388,6 +386,9 @@ function createOptionalAuthenticate(authenticate) {
388
386
  }
389
387
  //#endregion
390
388
  //#region src/factory/registerResources.ts
389
+ function isResourcesFactory(value) {
390
+ return typeof value === "function";
391
+ }
391
392
  /** Register a single resource with descriptive error on failure. */
392
393
  async function registerOne(parent, resource) {
393
394
  const name = resource.name ?? "unknown";
@@ -396,18 +397,31 @@ async function registerOne(parent, resource) {
396
397
  } catch (err) {
397
398
  const msg = err instanceof Error ? err.message : String(err);
398
399
  parent.log.error(`Failed to register resource "${name}": ${msg}`);
399
- 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 });
400
401
  }
401
402
  }
402
403
  /**
403
404
  * Execute the full resource lifecycle:
404
- * 1. plugins() — infra (DB, docs, webhooks)
405
- * 2. bootstrap[] — domain init (singletons, event handlers)
406
- * 3. resources[] auto-discovered routes (split by prefix)
407
- * 4. afterResources() post-registration wiring
408
- * 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
409
413
  */
410
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
+ }
411
425
  if (config.plugins) {
412
426
  await config.plugins(fastify);
413
427
  fastify.log.debug("Custom plugins registered");
@@ -416,33 +430,65 @@ async function registerResources(fastify, config) {
416
430
  for (const init of config.bootstrap) await init(fastify);
417
431
  fastify.log.debug(`${config.bootstrap.length} bootstrap function(s) executed`);
418
432
  }
419
- if (!config.resources?.length && config.resourceDir) {
420
- const { loadResources } = await import("./loadResources-BAzJItAJ.mjs").then((n) => n.n);
421
- const { resolve } = await import("node:path");
422
- 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
+ }
423
442
  config = {
424
443
  ...config,
425
- resources: await loadResources(dir, { logger: fastify.log })
444
+ resources: resolvedResources
426
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;
427
464
  }
428
- if (config.resources?.length) {
465
+ if (resolvedResources && resolvedResources.length > 0) {
429
466
  const seen = /* @__PURE__ */ new Set();
430
- for (const resource of config.resources) if (resource.name) {
431
- 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
+ }
432
473
  seen.add(resource.name);
433
474
  }
434
475
  const prefixed = [];
435
476
  const root = [];
436
- for (const resource of config.resources) if (resource.skipGlobalPrefix) root.push(resource);
477
+ for (const resource of resolvedResources) if (resource.skipGlobalPrefix) root.push(resource);
437
478
  else prefixed.push(resource);
438
479
  for (const resource of root) await registerOne(fastify, resource);
439
480
  if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
440
481
  for (const resource of prefixed) await registerOne(scoped, resource);
441
482
  }, { prefix: config.resourcePrefix });
442
483
  else for (const resource of prefixed) await registerOne(fastify, resource);
443
- 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 {
444
488
  const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
445
- 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}`);
446
492
  }
447
493
  if (config.afterResources) {
448
494
  await config.afterResources(fastify);
@@ -463,6 +509,40 @@ async function registerResources(fastify, config) {
463
509
  }
464
510
  //#endregion
465
511
  //#region src/factory/registerSecurity.ts
512
+ /**
513
+ * Translate `skipPaths` sugar into a `@fastify/rate-limit` `allowList`
514
+ * function. A user-supplied `allowList` (array of IPs or function) is
515
+ * preserved and OR-ed with the path match.
516
+ */
517
+ function buildRateLimitOpts(input) {
518
+ const { skipPaths, allowList, ...rest } = input;
519
+ if (!skipPaths || skipPaths.length === 0) return allowList === void 0 ? rest : {
520
+ ...rest,
521
+ allowList
522
+ };
523
+ const matchesPath = compilePathMatcher(skipPaths);
524
+ const combined = async (req, key) => {
525
+ if (matchesPath((req.url ?? "").split("?", 1)[0] ?? "")) return true;
526
+ if (typeof allowList === "function") return await allowList(req, key);
527
+ if (Array.isArray(allowList)) return allowList.includes(key);
528
+ return false;
529
+ };
530
+ return {
531
+ ...rest,
532
+ allowList: combined
533
+ };
534
+ }
535
+ function compilePathMatcher(patterns) {
536
+ const prefixes = [];
537
+ const exact = /* @__PURE__ */ new Set();
538
+ for (const p of patterns) if (p.endsWith("*")) prefixes.push(p.slice(0, -1));
539
+ else exact.add(p);
540
+ return (path) => {
541
+ if (exact.has(path)) return true;
542
+ for (const pre of prefixes) if (path.startsWith(pre)) return true;
543
+ return false;
544
+ };
545
+ }
466
546
  const PLUGIN_REGISTRY = {
467
547
  cors: {
468
548
  package: "@fastify/cors",
@@ -532,10 +612,10 @@ async function registerSecurityPlugins(fastify, config) {
532
612
  } else fastify.log.warn("CORS disabled");
533
613
  if (config.rateLimit !== false) {
534
614
  const rateLimit = await loadPlugin("rateLimit");
535
- const rateLimitOpts = config.rateLimit ?? {
615
+ const rateLimitOpts = buildRateLimitOpts(config.rateLimit ?? {
536
616
  max: 100,
537
617
  timeWindow: "1 minute"
538
- };
618
+ });
539
619
  await fastify.register(rateLimit, rateLimitOpts);
540
620
  if (!(typeof rateLimitOpts === "object" && "store" in rateLimitOpts)) {
541
621
  if (config.runtime === "distributed") throw new Error("[Arc] runtime: 'distributed' with rate limiting requires a shared store.\nProvide rateLimit: { store: new RedisStore({ ... }) } or disable rate limiting: rateLimit: false");
@@ -676,7 +756,7 @@ function validateDistributedRuntime(options) {
676
756
  */
677
757
  async function createApp(options) {
678
758
  if (options.debug !== void 0 && options.debug !== false) {
679
- const { configureArcLogger } = await import("./logger-DLg8-Ueg.mjs").then((n) => n.r);
759
+ const { configureArcLogger } = await import("./logger/index.mjs");
680
760
  configureArcLogger({ debug: options.debug });
681
761
  }
682
762
  validateAuthOptions(options);
@@ -717,7 +797,9 @@ async function createApp(options) {
717
797
  await registerSecurityPlugins(fastify, config);
718
798
  await registerUtilityPlugins(fastify, config);
719
799
  const trackPlugin = (name, opts) => {
720
- fastify.arc.plugins.set(name, {
800
+ const arc = fastify.arc;
801
+ if (!arc) return;
802
+ arc.plugins.set(name, {
721
803
  name,
722
804
  options: opts,
723
805
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -730,7 +812,7 @@ async function createApp(options) {
730
812
  await registerErrorHandler(fastify, config, trackPlugin);
731
813
  await registerResources(fastify, config);
732
814
  if (config.replyHelpers) {
733
- const { replyHelpersPlugin } = await import("./replyHelpers-CXtJDAZ0.mjs").then((n) => n.n);
815
+ const { replyHelpersPlugin } = await import("./replyHelpers-ByllIXXV.mjs").then((n) => n.n);
734
816
  await fastify.register(replyHelpersPlugin);
735
817
  }
736
818
  if (config.serializeBigInt) fastify.addHook("preSerialization", async (_request, _reply, payload) => {
@@ -1,5 +1,5 @@
1
- import { x as RegistryEntry } from "../index-Cl0uoKd5.mjs";
2
- import { t as ExternalOpenApiPaths } from "../externalPaths-BQ8QijNH.mjs";
1
+ import { p as RegistryEntry } from "../index-Cm0vUrr_.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-DV9WDfeg.mjs";
2
- import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-B5F8AddX.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
+ import { arcLog } from "./logger/index.mjs";
2
3
  import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
3
- import { t as arcLog } from "./logger-DLg8-Ueg.mjs";
4
4
  import fp from "fastify-plugin";
5
5
  //#region src/scope/elevation.ts
6
6
  var elevation_exports = /* @__PURE__ */ __exportAll({
@@ -0,0 +1,114 @@
1
+ import { FastifyInstance, FastifyRequest } from "fastify";
2
+
3
+ //#region src/plugins/errorHandler.d.ts
4
+ /** Class-based error mapper — maps thrown error instances to HTTP responses */
5
+ interface ErrorMapper<T extends Error = Error> {
6
+ /**
7
+ * Error class to match. Checked at runtime via `instanceof` — the constructor
8
+ * arity/signature is not called by the plugin, so the signature is typed
9
+ * permissively to accept real-world error classes:
10
+ *
11
+ * - **Abstract classes** (e.g. base domain errors) — `abstract new` is accepted.
12
+ * - **Specific constructor signatures** (e.g. `new InvalidTransitionError(from, to, id?)`)
13
+ * — `any[]` avoids forcing consumers to widen to `unknown[]` or cast.
14
+ *
15
+ * What matters for dispatch is the `instanceof` check, not the ctor shape.
16
+ */
17
+ type: abstract new (...args: any[]) => T;
18
+ /** Convert the error to an HTTP response shape */
19
+ toResponse: (error: T) => {
20
+ status: number;
21
+ code?: string;
22
+ message?: string;
23
+ details?: Record<string, unknown>;
24
+ };
25
+ }
26
+ interface ErrorHandlerOptions {
27
+ /**
28
+ * Include stack trace in error responses (default: false in production)
29
+ */
30
+ includeStack?: boolean;
31
+ /**
32
+ * Custom error callback for logging to external services
33
+ */
34
+ onError?: (error: Error, request: FastifyRequest) => void | Promise<void>;
35
+ /**
36
+ * Map specific error types to custom responses (by error.name string)
37
+ */
38
+ errorMap?: Record<string, {
39
+ statusCode: number;
40
+ code: string;
41
+ message?: string;
42
+ }>;
43
+ /**
44
+ * Class-based error mappers — checked via `instanceof`, highest priority.
45
+ *
46
+ * Register your domain error classes once; Arc auto-catches and maps them
47
+ * in every handler. Handlers just `throw` — no try/catch needed.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * class AccountingError extends Error {
52
+ * constructor(message: string, public status: number, public code: string) {
53
+ * super(message);
54
+ * }
55
+ * }
56
+ *
57
+ * const app = await createApp({
58
+ * errorHandler: {
59
+ * errorMappers: [
60
+ * {
61
+ * type: AccountingError,
62
+ * toResponse: (err) => ({ status: err.status, code: err.code, message: err.message }),
63
+ * },
64
+ * ],
65
+ * },
66
+ * });
67
+ *
68
+ * // Now handlers just throw:
69
+ * handler: async (req) => {
70
+ * await ledger.post(id); // throws AccountingError → Arc maps to proper HTTP response
71
+ * }
72
+ * ```
73
+ */
74
+ errorMappers?: ErrorMapper[];
75
+ /**
76
+ * Classify an error as a duplicate-key / unique-constraint violation →
77
+ * mapped to `409 Conflict` with `code: "DUPLICATE_KEY"`.
78
+ *
79
+ * Mirrors `RepositoryLike.isDuplicateKeyError` for the Fastify layer: errors
80
+ * that escape a controller (custom routes, user hooks, raw driver calls)
81
+ * still land here, so the classifier is duplicated at the edge. Defaults
82
+ * cover MongoDB (`code 11000` / `codeName "DuplicateKey"`), Prisma
83
+ * (`code "P2002"`), and Postgres (`code "23505"`). Override to add other
84
+ * backends (DynamoDB `ConditionalCheckFailedException`, etc.) or to disable
85
+ * the built-in detection.
86
+ */
87
+ isDuplicateKeyError?: (err: unknown) => boolean;
88
+ }
89
+ /**
90
+ * Default duplicate-key detector covering the mainstream drivers arc sees
91
+ * most. Detection is strictly by known driver codes — never by message
92
+ * string matching — because false positives on dup-key silently mask real
93
+ * errors (WriteConflict, NotWritablePrimary, etc.) as 409s. For long-tail
94
+ * drivers (Neo4j, MSSQL, DynamoDB, custom kits), compose rather than
95
+ * replace:
96
+ *
97
+ * ```ts
98
+ * import { defaultIsDuplicateKeyError } from '@classytic/arc/plugins';
99
+ *
100
+ * errorHandler: {
101
+ * isDuplicateKeyError: (err) =>
102
+ * defaultIsDuplicateKeyError(err) || isNeo4jDupKey(err),
103
+ * }
104
+ * ```
105
+ *
106
+ * Drizzle apps get coverage transitively (Drizzle doesn't wrap driver
107
+ * errors — pg/mysql2/better-sqlite3 codes propagate as-is). Neon is
108
+ * Postgres-wire-compatible → `23505` covers `@neondatabase/serverless`.
109
+ */
110
+ declare function defaultIsDuplicateKeyError(err: unknown): boolean;
111
+ declare function errorHandlerPluginFn(fastify: FastifyInstance, options?: ErrorHandlerOptions): Promise<void>;
112
+ declare const errorHandlerPlugin: typeof errorHandlerPluginFn;
113
+ //#endregion
114
+ export { errorHandlerPlugin as i, ErrorMapper as n, defaultIsDuplicateKeyError as r, ErrorHandlerOptions as t };
@@ -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-CfRkaxwf.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/events/EventTransport.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { i as EventLogger, n as DomainEvent, o as EventTransport, r as EventHandler } from "./EventTransport-CUw5NNWe.mjs";
1
+ import { i as EventLogger, n as DomainEvent, o as EventTransport, r as EventHandler } from "./EventTransport-CfVEGaEl.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/events/defineEvent.d.ts
@@ -1,8 +1,8 @@
1
- import { kt as RepositoryLike } from "../index-Cl0uoKd5.mjs";
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-CUw5NNWe.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-CxWgpd6K.mjs";
1
+ import { jn as RepositoryLike } from "../index-Cm0vUrr_.mjs";
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-CUNjYYRY.mjs";
4
4
  import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
5
- import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-CakIQmwR.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
  /**