@classytic/arc 2.23.0 → 2.28.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 (147) hide show
  1. package/README.md +2 -0
  2. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController-B65zbz9m.mjs} +94 -23
  3. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -0
  6. package/dist/audit/index.d.mts +32 -3
  7. package/dist/audit/index.mjs +7 -4
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +263 -259
  10. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  11. package/dist/cache/index.d.mts +41 -3
  12. package/dist/cache/index.mjs +33 -4
  13. package/dist/cleanup/index.d.mts +313 -0
  14. package/dist/cleanup/index.mjs +1267 -0
  15. package/dist/cli/commands/describe.d.mts +1 -1
  16. package/dist/cli/commands/docs.mjs +1 -1
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.mjs +1 -1
  19. package/dist/cli/index.mjs +2 -2
  20. package/dist/context/index.mjs +2 -64
  21. package/dist/core/index.d.mts +3 -3
  22. package/dist/core/index.mjs +5 -4
  23. package/dist/core-BGa8TXJR.mjs +221 -0
  24. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-C-N7z2EL.mjs} +3 -2
  25. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/{createApp-CjOeWJ9_.mjs → createApp-Du5G54TM.mjs} +187 -96
  27. package/dist/{core-DQ6rDTyH.mjs → defineResource-2zvVVpxg.mjs} +297 -230
  28. package/dist/docs/index.d.mts +1 -1
  29. package/dist/docs/index.mjs +2 -2
  30. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  31. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  32. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  33. package/dist/events/index.d.mts +83 -314
  34. package/dist/events/index.mjs +183 -95
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  37. package/dist/factory/index.d.mts +5 -5
  38. package/dist/factory/index.mjs +30 -8
  39. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +26 -2
  40. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  41. package/dist/headers-BvlQSDMB.mjs +45 -0
  42. package/dist/health-CB_m4S32.mjs +377 -0
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.mjs +27 -3
  45. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/index-CZY4EGga.d.mts +107 -0
  48. package/dist/{index-CC5ruDGh.d.mts → index-CjVg-Epg.d.mts} +37 -5
  49. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +7 -6
  52. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  53. package/dist/integrations/event-gateway.d.mts +1 -1
  54. package/dist/integrations/event-gateway.mjs +1 -1
  55. package/dist/integrations/index.d.mts +3 -3
  56. package/dist/integrations/jobs/index.d.mts +2 -0
  57. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  58. package/dist/integrations/mcp/index.d.mts +4 -4
  59. package/dist/integrations/mcp/index.mjs +2 -2
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/webhooks.d.mts +37 -30
  63. package/dist/integrations/webhooks.mjs +51 -62
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +41 -14
  67. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  68. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cs6nRGCt.mjs} +12 -10
  69. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  70. package/dist/middleware/index.d.mts +17 -1
  71. package/dist/middleware/index.mjs +1 -1
  72. package/dist/migrations/index.d.mts +94 -44
  73. package/dist/migrations/index.mjs +196 -44
  74. package/dist/module-_-M7C5ES.mjs +29 -0
  75. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  76. package/dist/{openapi-jSDx-VDv.mjs → openapi-BLV4OMXb.mjs} +3 -3
  77. package/dist/permissions/index.d.mts +2 -2
  78. package/dist/permissions/index.mjs +3 -3
  79. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  80. package/dist/pipeline/index.d.mts +1 -1
  81. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BGRnV-To.mjs} +4 -27
  82. package/dist/plugins/index.d.mts +6 -77
  83. package/dist/plugins/index.mjs +11 -95
  84. package/dist/plugins/response-cache.mjs +1 -0
  85. package/dist/plugins/tracing-entry.mjs +1 -1
  86. package/dist/{index-Ba10fdKp.d.mts → preloadResources-CM4uUaFv.d.mts} +51 -50
  87. package/dist/preloadResources-DSY5HGpS.mjs +45 -0
  88. package/dist/presets/filesUpload.d.mts +3 -3
  89. package/dist/presets/filesUpload.mjs +3 -3
  90. package/dist/presets/index.d.mts +2 -1
  91. package/dist/presets/index.mjs +1 -1
  92. package/dist/presets/multiTenant.d.mts +1 -1
  93. package/dist/presets/multiTenant.mjs +1 -1
  94. package/dist/presets/search.d.mts +2 -2
  95. package/dist/presets/search.mjs +1 -1
  96. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  97. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  98. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  99. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  100. package/dist/{realtime-I8OrfnMR.mjs → realtime-DEuDu9io.mjs} +3 -3
  101. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  102. package/dist/registerRoute-DTp18Ah_.mjs +108 -0
  103. package/dist/registry/index.d.mts +2 -2
  104. package/dist/registry/index.mjs +1 -1
  105. package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
  106. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  107. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  108. package/dist/schedules-CjOd2MG9.mjs +113 -0
  109. package/dist/schemas/index.d.mts +67 -67
  110. package/dist/schemas/index.mjs +2 -3
  111. package/dist/scim/index.mjs +1 -2
  112. package/dist/scope/index.d.mts +2 -2
  113. package/dist/scope/index.mjs +4 -4
  114. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  115. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  116. package/dist/testing/cleanupStoreContract.d.mts +10 -0
  117. package/dist/testing/cleanupStoreContract.mjs +375 -0
  118. package/dist/testing/index.d.mts +5 -3
  119. package/dist/testing/index.mjs +4 -2
  120. package/dist/testing/outboxStoreContract.d.mts +13 -0
  121. package/dist/testing/outboxStoreContract.mjs +247 -0
  122. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  123. package/dist/types/index.d.mts +4 -4
  124. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  125. package/dist/types-C_ZZRcD6.d.mts +450 -0
  126. package/dist/{module-B4u10iOJ.d.mts → types-Cu6vVJmh.d.mts} +196 -60
  127. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  128. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  129. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  130. package/dist/usage/index.d.mts +3 -52
  131. package/dist/usage/index.mjs +0 -0
  132. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  133. package/dist/utils/index.d.mts +58 -85
  134. package/dist/utils/index.mjs +4 -2
  135. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  136. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  137. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  138. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  139. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  140. package/package.json +541 -513
  141. package/skills/arc/references/api-reference.md +3 -1
  142. package/skills/arc/references/integrations.md +20 -11
  143. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  144. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  145. package/dist/health-ScyjIVo5.mjs +0 -202
  146. package/dist/requestId-5BhTVzBv.mjs +0 -113
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,291 +1,8 @@
1
- import { m as RESERVED_QUERY_PARAMS, t as CRUD_OPERATIONS } from "./constants-DNre0wRg.mjs";
1
+ import { m as RESERVED_QUERY_PARAMS } from "./constants-DNre0wRg.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { t as ArcError } from "./errors-DkO8Nk6-.mjs";
4
- import { r as getAvailablePresets } from "./presets-BOe4Ugkl.mjs";
5
4
  import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
6
- //#region src/core/validateResourceConfig.ts
7
- /**
8
- * Resource Configuration Validator
9
- *
10
- * Fail-fast validation at definition time.
11
- * Invalid configs throw immediately with clear, actionable errors.
12
- *
13
- * @example
14
- * const result = validateResourceConfig(config);
15
- * if (!result.valid) {
16
- * console.error(formatValidationErrors(result.errors));
17
- * }
18
- */
19
- /**
20
- * Validate a resource configuration
21
- */
22
- function validateResourceConfig(config, options = {}) {
23
- const errors = [];
24
- const warnings = [];
25
- if (!config.name) errors.push({
26
- field: "name",
27
- message: "Resource name is required",
28
- suggestion: "Add a unique resource name (e.g., \"product\", \"user\")"
29
- });
30
- else if (!/^[a-z][a-z0-9-]*$/i.test(config.name)) errors.push({
31
- field: "name",
32
- message: `Invalid resource name "${config.name}"`,
33
- suggestion: "Use alphanumeric characters and hyphens, starting with a letter"
34
- });
35
- const crudRoutes = CRUD_OPERATIONS;
36
- const disabledRoutes = new Set(config.disabledRoutes ?? []);
37
- const enabledCrudRoutes = crudRoutes.filter((route) => !disabledRoutes.has(route));
38
- if (!config.disableDefaultRoutes && enabledCrudRoutes.length > 0) {
39
- if (!config.adapter) errors.push({
40
- field: "adapter",
41
- message: "Data adapter is required when CRUD routes are enabled",
42
- suggestion: "Provide an adapter: createMongooseAdapter({ model, repository })"
43
- });
44
- else if (!config.adapter.repository) errors.push({
45
- field: "adapter.repository",
46
- message: "Adapter must provide a repository",
47
- suggestion: "Ensure your adapter returns a valid StandardRepo (see @classytic/repo-core)"
48
- });
49
- } else if (!config.adapter && !config.routes?.length) warnings.push({
50
- field: "config",
51
- message: "Resource has no adapter and no routes",
52
- suggestion: "Provide either adapter for CRUD or routes for custom logic"
53
- });
54
- if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
55
- const ctrl = config.controller;
56
- const requiredMethods = CRUD_OPERATIONS;
57
- for (const method of requiredMethods) if (typeof ctrl[method] !== "function") errors.push({
58
- field: `controller.${method}`,
59
- message: `Missing required CRUD method "${method}"`,
60
- suggestion: "Extend BaseController which implements IController interface"
61
- });
62
- }
63
- if (config.controller && config.routes) validateRouteHandlers(config.controller, config.routes, errors);
64
- if (config.permissions) validatePermissionKeys(config, options, errors, warnings);
65
- if (config.presets && !options.allowUnknownPresets) validatePresets(config.presets, errors, warnings);
66
- if (config.prefix) {
67
- if (!config.prefix.startsWith("/")) errors.push({
68
- field: "prefix",
69
- message: `Prefix must start with "/" (got "${config.prefix}")`,
70
- suggestion: `Change to "/${config.prefix}"`
71
- });
72
- if (config.prefix.endsWith("/") && config.prefix !== "/") warnings.push({
73
- field: "prefix",
74
- message: `Prefix should not end with "/" (got "${config.prefix}")`,
75
- suggestion: `Change to "${config.prefix.slice(0, -1)}"`
76
- });
77
- }
78
- if (config.routes) {
79
- validateRoutes(config.routes, errors);
80
- validateRouteCrudCollisions(config, errors);
81
- }
82
- return {
83
- valid: errors.length === 0,
84
- errors,
85
- warnings
86
- };
87
- }
88
- function validateRouteHandlers(controller, routes, errors) {
89
- const ctrl = controller;
90
- for (const route of routes) if (typeof route.handler === "string") {
91
- if (typeof ctrl[route.handler] !== "function") errors.push({
92
- field: `routes[${route.method} ${route.path}]`,
93
- message: `Handler "${route.handler}" not found on controller`,
94
- suggestion: `Add method "${route.handler}" to controller or use a function handler`
95
- });
96
- }
97
- }
98
- function validatePermissionKeys(config, options, _errors, warnings) {
99
- const validKeys = /* @__PURE__ */ new Set([...CRUD_OPERATIONS, ...options.additionalPermissionKeys ?? []]);
100
- for (const route of config.routes ?? []) if (typeof route.handler === "string") validKeys.add(route.handler);
101
- for (const preset of config.presets ?? []) {
102
- const presetName = typeof preset === "string" ? preset : preset.name;
103
- if (presetName === "softDelete") {
104
- validKeys.add("deleted");
105
- validKeys.add("restore");
106
- }
107
- if (presetName === "slugLookup") validKeys.add("getBySlug");
108
- if (presetName === "tree") {
109
- validKeys.add("tree");
110
- validKeys.add("children");
111
- validKeys.add("getTree");
112
- validKeys.add("getChildren");
113
- }
114
- }
115
- for (const key of Object.keys(config.permissions ?? {})) if (!validKeys.has(key)) warnings.push({
116
- field: `permissions.${key}`,
117
- message: `Unknown permission key "${key}"`,
118
- suggestion: `Valid keys: ${Array.from(validKeys).join(", ")}`
119
- });
120
- }
121
- function validatePresets(presets, errors, warnings) {
122
- const availablePresets = getAvailablePresets();
123
- for (const preset of presets) {
124
- if (typeof preset === "object" && ("middlewares" in preset || "routes" in preset)) continue;
125
- const presetName = typeof preset === "string" ? preset : preset.name;
126
- if (!availablePresets.includes(presetName)) errors.push({
127
- field: "presets",
128
- message: `Unknown preset "${presetName}"`,
129
- suggestion: `Available presets: ${availablePresets.join(", ")}`
130
- });
131
- if (typeof preset === "object") validatePresetOptions(preset, warnings);
132
- }
133
- }
134
- function validatePresetOptions(preset, warnings) {
135
- const validOptions = {
136
- slugLookup: ["slugField"],
137
- tree: ["parentField"],
138
- softDelete: ["deletedField"],
139
- ownedByUser: ["ownerField"],
140
- multiTenant: ["tenantField", "allowPublic"]
141
- }[preset.name] ?? [];
142
- const providedOptions = Object.keys(preset).filter((k) => k !== "name");
143
- for (const opt of providedOptions) if (!validOptions.includes(opt)) warnings.push({
144
- field: `presets[${preset.name}].${opt}`,
145
- message: `Unknown option "${opt}" for preset "${preset.name}"`,
146
- suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
147
- });
148
- }
149
- /**
150
- * Detect collisions between user-declared `routes:` entries and the
151
- * auto-CRUD route table that `createCrudRouter` will register.
152
- *
153
- * Without this check, a custom `POST /` on the same prefix as auto-CRUD's
154
- * `create` boots fine through `defineResource()` and only blows up at
155
- * `app.register()` time with Fastify's opaque `FST_ERR_DUPLICATED_ROUTE`.
156
- * That error doesn't mention `disabledRoutes` and doesn't distinguish
157
- * "you collided with auto-CRUD" from "you collided with yourself" — so
158
- * the consumer has to grep arc's source to discover the fix.
159
- *
160
- * We surface the fix in the error message, naming the conflicting op
161
- * and the literal line to add.
162
- *
163
- * Honored config:
164
- * - `disableDefaultRoutes: true` → no auto-CRUD, no collisions possible
165
- * - `disabledRoutes: ['create']` → that op's path is free for custom use
166
- * - `updateMethod: 'PUT' | 'both'` → update slot covers PUT/PATCH accordingly
167
- */
168
- function validateRouteCrudCollisions(config, errors) {
169
- if (config.disableDefaultRoutes) return;
170
- if (!config.routes || config.routes.length === 0) return;
171
- const updateMethod = config.updateMethod ?? "PATCH";
172
- const updateMethods = updateMethod === "both" ? ["PUT", "PATCH"] : [updateMethod];
173
- const disabled = new Set(config.disabledRoutes ?? []);
174
- const crudTable = [
175
- {
176
- op: "list",
177
- method: "GET",
178
- path: "/"
179
- },
180
- {
181
- op: "get",
182
- method: "GET",
183
- path: "/:id"
184
- },
185
- {
186
- op: "create",
187
- method: "POST",
188
- path: "/"
189
- },
190
- ...updateMethods.map((m) => ({
191
- op: "update",
192
- method: m,
193
- path: "/:id"
194
- })),
195
- {
196
- op: "delete",
197
- method: "DELETE",
198
- path: "/:id"
199
- }
200
- ];
201
- for (const [i, route] of config.routes.entries()) {
202
- if (!route.method || !route.path) continue;
203
- for (const crud of crudTable) {
204
- if (disabled.has(crud.op)) continue;
205
- if (route.method !== crud.method || route.path !== crud.path) continue;
206
- errors.push({
207
- field: `routes[${i}]`,
208
- message: `Route ${route.method} ${route.path} collides with auto-CRUD "${crud.op}"`,
209
- suggestion: `Add \`disabledRoutes: ['${crud.op}']\` to suppress the auto-CRUD route, or move your custom handler to a different path. Set \`disableDefaultRoutes: true\` to disable all auto-CRUD on this resource.`
210
- });
211
- }
212
- }
213
- }
214
- function validateRoutes(routes, errors) {
215
- const validMethods = [
216
- "GET",
217
- "POST",
218
- "PUT",
219
- "PATCH",
220
- "DELETE",
221
- "OPTIONS",
222
- "HEAD"
223
- ];
224
- const seenRoutes = /* @__PURE__ */ new Set();
225
- for (const [i, route] of routes.entries()) {
226
- if (!validMethods.includes(route.method)) errors.push({
227
- field: `routes[${i}].method`,
228
- message: `Invalid HTTP method "${route.method}"`,
229
- suggestion: `Valid methods: ${validMethods.join(", ")}`
230
- });
231
- if (!route.path) errors.push({
232
- field: `routes[${i}].path`,
233
- message: "Route path is required"
234
- });
235
- else if (!route.path.startsWith("/")) errors.push({
236
- field: `routes[${i}].path`,
237
- message: `Route path must start with "/" (got "${route.path}")`,
238
- suggestion: `Change to "/${route.path}"`
239
- });
240
- const routeWithRefs = route;
241
- if (!route.handler && typeof routeWithRefs.controllerMethod !== "function") errors.push({
242
- field: `routes[${i}].handler`,
243
- message: "Route must declare either `handler` (string / function) or `controllerMethod`",
244
- suggestion: "Prefer `controllerMethod: (c: MyController) => c.method` for typed handler refs."
245
- });
246
- const routeKey = `${route.method} ${route.path}`;
247
- if (seenRoutes.has(routeKey)) errors.push({
248
- field: `routes[${i}]`,
249
- message: `Duplicate route "${routeKey}"`
250
- });
251
- seenRoutes.add(routeKey);
252
- }
253
- }
254
- /**
255
- * Format validation errors for display
256
- */
257
- function formatValidationErrors(resourceName, result) {
258
- const lines = [];
259
- if (result.errors.length > 0) {
260
- lines.push(`Resource "${resourceName}" validation failed:`);
261
- lines.push("");
262
- lines.push("ERRORS:");
263
- for (const err of result.errors) {
264
- lines.push(` ✗ ${err.field}: ${err.message}`);
265
- if (err.suggestion) lines.push(` → ${err.suggestion}`);
266
- }
267
- }
268
- if (result.warnings.length > 0) {
269
- if (lines.length > 0) lines.push("");
270
- lines.push("WARNINGS:");
271
- for (const warn of result.warnings) {
272
- lines.push(` ⚠ ${warn.field}: ${warn.message}`);
273
- if (warn.suggestion) lines.push(` → ${warn.suggestion}`);
274
- }
275
- }
276
- return lines.join("\n");
277
- }
278
- /**
279
- * Validate and throw if invalid
280
- */
281
- function assertValidConfig(config, options) {
282
- const result = validateResourceConfig(config, options);
283
- if (!result.valid) {
284
- const errorMsg = formatValidationErrors(config.name ?? "unknown", result);
285
- throw new Error(errorMsg);
286
- }
287
- }
288
- //#endregion
5
+ import { scheduleBackground } from "@classytic/repo-core/cache";
289
6
  //#region src/utils/circuitBreaker.ts
290
7
  /**
291
8
  * Circuit Breaker Pattern
@@ -337,10 +54,13 @@ var CircuitBreaker = class {
337
54
  nextAttempt = 0;
338
55
  lastCallAt = null;
339
56
  openedAt = null;
57
+ halfOpenInFlight = 0;
340
58
  failureThreshold;
341
59
  resetTimeout;
342
60
  timeout;
343
61
  successThreshold;
62
+ halfOpenMaxProbes;
63
+ propagateAbort;
344
64
  fallback;
345
65
  onStateChange;
346
66
  onError;
@@ -352,6 +72,8 @@ var CircuitBreaker = class {
352
72
  this.resetTimeout = options.resetTimeout ?? 6e4;
353
73
  this.timeout = options.timeout ?? 1e4;
354
74
  this.successThreshold = options.successThreshold ?? 1;
75
+ this.halfOpenMaxProbes = options.halfOpenMaxProbes ?? 1;
76
+ this.propagateAbort = options.propagateAbort ?? false;
355
77
  this.fallback = options.fallback;
356
78
  this.onStateChange = options.onStateChange;
357
79
  this.onError = options.onError;
@@ -371,6 +93,15 @@ var CircuitBreaker = class {
371
93
  }
372
94
  this.setState(CircuitState.HALF_OPEN);
373
95
  }
96
+ let isProbe = false;
97
+ if (this.state === CircuitState.HALF_OPEN) {
98
+ if (this.halfOpenInFlight >= this.halfOpenMaxProbes) {
99
+ if (this.fallback) return this.fallback(...args);
100
+ throw new CircuitBreakerError(`Circuit breaker is HALF_OPEN for ${this.name} — probe slots full`, CircuitState.HALF_OPEN);
101
+ }
102
+ this.halfOpenInFlight++;
103
+ isProbe = true;
104
+ }
374
105
  try {
375
106
  const result = await this.executeWithTimeout(args);
376
107
  this.onSuccess();
@@ -378,17 +109,24 @@ var CircuitBreaker = class {
378
109
  } catch (err) {
379
110
  this.onFailure(err instanceof Error ? err : new Error(String(err)));
380
111
  throw err;
112
+ } finally {
113
+ if (isProbe) this.halfOpenInFlight--;
381
114
  }
382
115
  }
383
116
  /**
384
- * Execute function with timeout
117
+ * Execute function with timeout. With `propagateAbort`, the wrapped fn
118
+ * receives a trailing AbortSignal that fires on timeout — the breaker's
119
+ * timeout otherwise only rejects the CALLER while the underlying work
120
+ * keeps consuming sockets/pool slots to completion.
385
121
  */
386
122
  async executeWithTimeout(args) {
123
+ const controller = this.propagateAbort ? new AbortController() : void 0;
387
124
  return new Promise((resolve, reject) => {
388
125
  const timeoutId = setTimeout(() => {
126
+ controller?.abort(/* @__PURE__ */ new Error(`Request timeout after ${this.timeout}ms`));
389
127
  reject(/* @__PURE__ */ new Error(`Request timeout after ${this.timeout}ms`));
390
128
  }, this.timeout);
391
- this.fn(...args).then((result) => {
129
+ (controller ? this.fn(...args, controller.signal) : this.fn(...args)).then((result) => {
392
130
  clearTimeout(timeoutId);
393
131
  resolve(result);
394
132
  }).catch((error) => {
@@ -571,6 +309,21 @@ function createCircuitBreakerRegistry() {
571
309
  //#endregion
572
310
  //#region src/utils/compensation.ts
573
311
  /**
312
+ * Observability hooks must never alter the transaction: a throwing metrics
313
+ * sink must not fail a step that succeeded, trigger compensation, or become
314
+ * an unhandled rejection from a detached fire-and-forget chain. Containment
315
+ * is reported via `hooks.onHookError` so broken sinks stay visible.
316
+ */
317
+ function safeHook(name, invoke, hooks) {
318
+ try {
319
+ invoke();
320
+ } catch (err) {
321
+ try {
322
+ hooks?.onHookError?.(name, err instanceof Error ? err : new Error(String(err)));
323
+ } catch {}
324
+ }
325
+ }
326
+ /**
574
327
  * Run steps in order with automatic compensation on failure.
575
328
  *
576
329
  * @typeParam TCtx - Context type shared across steps (defaults to Record<string, unknown>)
@@ -582,8 +335,7 @@ async function withCompensation(_name, steps, initialContext, hooks) {
582
335
  const completed = [];
583
336
  for (const step of steps) {
584
337
  if (step.fireAndForget) {
585
- completedSteps.push(step.name);
586
- step.execute(ctx).then((result) => hooks?.onStepComplete?.(step.name, result), () => {});
338
+ step.execute(ctx).then((result) => safeHook("onStepComplete", () => hooks?.onStepComplete?.(step.name, result), hooks), (err) => safeHook("onStepFailed", () => hooks?.onStepFailed?.(step.name, err instanceof Error ? err : new Error(String(err))), hooks)).catch(() => {});
587
339
  continue;
588
340
  }
589
341
  try {
@@ -594,10 +346,10 @@ async function withCompensation(_name, steps, initialContext, hooks) {
594
346
  step,
595
347
  result
596
348
  });
597
- hooks?.onStepComplete?.(step.name, result);
349
+ safeHook("onStepComplete", () => hooks?.onStepComplete?.(step.name, result), hooks);
598
350
  } catch (err) {
599
351
  const error = err instanceof Error ? err : new Error(String(err));
600
- hooks?.onStepFailed?.(step.name, error);
352
+ safeHook("onStepFailed", () => hooks?.onStepFailed?.(step.name, error), hooks);
601
353
  const compensationErrors = await rollback(ctx, completed, hooks);
602
354
  return {
603
355
  success: false,
@@ -623,7 +375,7 @@ async function rollback(ctx, completed, hooks) {
623
375
  const compensateFn = entry.step.compensate;
624
376
  try {
625
377
  await compensateFn(ctx, entry.result);
626
- hooks?.onCompensate?.(entry.step.name);
378
+ safeHook("onCompensate", () => hooks?.onCompensate?.(entry.step.name), hooks);
627
379
  } catch (err) {
628
380
  errors.push({
629
381
  step: entry.step.name,
@@ -770,25 +522,6 @@ function handleRaw(handler, statusCode = 200) {
770
522
  };
771
523
  }
772
524
  //#endregion
773
- //#region src/utils/headers.ts
774
- /**
775
- * Read a single header value. Lowercases the name (Node normalizes incoming
776
- * header names to lowercase) and returns the FIRST value when the header
777
- * was repeated.
778
- *
779
- * Duplicate policy: first-value is deterministic, but for security-sensitive
780
- * headers (authorization material, forwarded-for chains, content-length) a
781
- * duplicate usually signals smuggling or a broken proxy — consumers that
782
- * care should check `Array.isArray(headers[name])` and REJECT instead of
783
- * picking a winner.
784
- */
785
- function getHeader(headers, name) {
786
- if (!headers) return void 0;
787
- const value = headers[name.toLowerCase()];
788
- if (Array.isArray(value)) return value[0];
789
- return value;
790
- }
791
- //#endregion
792
525
  //#region src/utils/queryParser.ts
793
526
  /**
794
527
  * Arc Query Parser - Default URL-to-Query Parser
@@ -802,6 +535,8 @@ function getHeader(headers, name) {
802
535
  * defineResource({ name: 'product', adapter: ... });
803
536
  *
804
537
  * // Use MongoKit's QueryParser (recommended for MongoDB - has $lookup, aggregations, etc.)
538
+ * // Fail-closed by default (mongokit >=3.25): invalid/blocked query input
539
+ * // becomes HTTP 400 instead of a silently broadened result set.
805
540
  * import { QueryParser } from '@classytic/mongokit';
806
541
  * defineResource({
807
542
  * name: 'product',
@@ -1604,13 +1339,6 @@ function getDefaultCrudSchemas() {
1604
1339
  });
1605
1340
  }
1606
1341
  //#endregion
1607
- //#region src/utils/runtime.ts
1608
- /**
1609
- * Portable "run on next tick" scheduler. `setImmediate` is Node-only — not
1610
- * available in Bun workers, Deno, Cloudflare Workers, or edge runtimes.
1611
- */
1612
- const scheduleBackground = typeof setImmediate === "function" ? (cb) => void setImmediate(cb) : (cb) => queueMicrotask(cb);
1613
- //#endregion
1614
1342
  //#region src/utils/stateMachine.ts
1615
1343
  /**
1616
1344
  * Create a state machine for validating transitions
@@ -1712,22 +1440,4 @@ function createStateMachine(name, transitions = {}, options = {}) {
1712
1440
  };
1713
1441
  }
1714
1442
  //#endregion
1715
- //#region src/utils/userHelpers.ts
1716
- /**
1717
- * Extract a user ID from a user object. Accepts `id` or `_id` — returns
1718
- * `undefined` when neither is present. Used by arc's controllers to
1719
- * populate `createdBy` / `updatedBy` fields and for cache scoping.
1720
- *
1721
- * @example
1722
- * ```ts
1723
- * import { getUserId } from '@classytic/arc/utils';
1724
- * const uid = getUserId(request.user);
1725
- * ```
1726
- */
1727
- function getUserId(user) {
1728
- if (!user) return void 0;
1729
- const id = user.id ?? user._id;
1730
- return id ? String(id) : void 0;
1731
- }
1732
- //#endregion
1733
- export { CircuitState as A, defineGuard as C, CircuitBreaker as D, withCompensation as E, validateResourceConfig as F, createCircuitBreakerRegistry as M, assertValidConfig as N, CircuitBreakerError as O, formatValidationErrors as P, NEUTRAL_DIALECT as S, defineCompensation as T, ArcQueryParser as _, bareListResponse as a, handleRaw as b, errorDetailSchema$1 as c, keysetListResponse as d, listResponse as f, responses as g, queryParams as h, aggregateListResponse as i, createCircuitBreaker as j, CircuitBreakerRegistry as k, getDefaultCrudSchemas as l, paginationSchema as m, createStateMachine as n, deleteResponse as o, offsetListResponse as p, scheduleBackground as r, errorContractSchema$1 as s, getUserId as t, getListQueryParams as u, createQueryParser as v, defineErrorMapper as w, MONGO_DIALECT as x, getHeader as y };
1443
+ export { createCircuitBreakerRegistry as A, defineCompensation as C, CircuitBreakerRegistry as D, CircuitBreakerError as E, CircuitState as O, defineErrorMapper as S, CircuitBreaker as T, createQueryParser as _, deleteResponse as a, NEUTRAL_DIALECT as b, getDefaultCrudSchemas as c, listResponse as d, offsetListResponse as f, ArcQueryParser as g, responses as h, bareListResponse as i, createCircuitBreaker as k, getListQueryParams as l, queryParams as m, scheduleBackground as n, errorContractSchema$1 as o, paginationSchema as p, aggregateListResponse as r, errorDetailSchema$1 as s, createStateMachine as t, keysetListResponse as u, handleRaw as v, withCompensation as w, defineGuard as x, MONGO_DIALECT as y };
@@ -123,7 +123,7 @@ function resolveLimitPolicy(config) {
123
123
  function resolveAggRequestLimit(normalized, query) {
124
124
  const policy = normalized.compiled.limitPolicy;
125
125
  if (!policy) return normalized.compiled.limit;
126
- const raw = query["limit"];
126
+ const raw = query.limit;
127
127
  if (raw === void 0 || raw === "") return policy.defaultLimit;
128
128
  const parsed = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
129
129
  if (!Number.isFinite(parsed) || parsed <= 0) return policy.defaultLimit;
@@ -1,33 +1,5 @@
1
1
  import { n as DomainEvent } from "./EventTransport-QmY7A258.mjs";
2
2
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
3
- //#region src/plugins/health.d.ts
4
- interface HealthCheck {
5
- /** Name of the dependency */
6
- name: string;
7
- /** Function that returns true if healthy, false otherwise */
8
- check: () => Promise<boolean> | boolean;
9
- /** Optional timeout in ms (default: 5000) */
10
- timeout?: number;
11
- /** Whether this check is critical for readiness (default: true) */
12
- critical?: boolean;
13
- }
14
- interface HealthOptions {
15
- /** Route prefix (default: '/_health') */
16
- prefix?: string;
17
- /** Health check dependencies */
18
- checks?: HealthCheck[];
19
- /** Enable metrics endpoint (default: false) */
20
- metrics?: boolean;
21
- /** Custom metrics collector function */
22
- metricsCollector?: () => Promise<string> | string;
23
- /** Version info to include in responses */
24
- version?: string;
25
- /** Collect HTTP request metrics (default: true if metrics enabled) */
26
- collectHttpMetrics?: boolean;
27
- }
28
- declare const healthPlugin: FastifyPluginAsync<HealthOptions>;
29
- declare const _default$5: FastifyPluginAsync<HealthOptions>;
30
- //#endregion
31
3
  //#region src/plugins/caching.d.ts
32
4
  interface CachingRule {
33
5
  /** Path prefix to match (e.g., '/api/products') */
@@ -174,6 +146,16 @@ interface VersioningOptions {
174
146
  headerName?: string;
175
147
  /** Response header name (default: 'x-api-version') */
176
148
  responseHeader?: string;
149
+ /**
150
+ * Supported versions. When set, a request for any OTHER version is
151
+ * rejected with `400 arc.unsupported_api_version` (details list the
152
+ * supported set). When omitted, every requested version is accepted
153
+ * and merely annotated — detection-only mode.
154
+ *
155
+ * The `defaultVersion` must be in this list (boot-time check) so a
156
+ * versionless request can never be rejected by its own default.
157
+ */
158
+ versions?: string[];
177
159
  /** Deprecated versions — adds Deprecation + Sunset headers */
178
160
  deprecated?: string[];
179
161
  /** Sunset date for deprecated versions (ISO 8601) */
@@ -187,4 +169,4 @@ declare module "fastify" {
187
169
  declare const versioningPlugin: FastifyPluginAsync<VersioningOptions>;
188
170
  declare const _default: FastifyPluginAsync<VersioningOptions>;
189
171
  //#endregion
190
- export { healthPlugin as C, _default$5 as S, CachingRule as _, MetricsCollector as a, HealthCheck as b, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, HealthOptions as x, cachingPlugin as y };
172
+ export { CachingRule as _, MetricsCollector as a, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, cachingPlugin as y };
@@ -1,4 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
+ import { d as createDomainError } from "./errors-DkO8Nk6-.mjs";
2
3
  import fp from "fastify-plugin";
3
4
  //#region src/plugins/versioning.ts
4
5
  var versioning_exports = /* @__PURE__ */ __exportAll({
@@ -7,8 +8,10 @@ var versioning_exports = /* @__PURE__ */ __exportAll({
7
8
  });
8
9
  const PREFIX_REGEX = /^\/v(\d+)\//;
9
10
  const versioningPlugin = async (fastify, opts) => {
10
- const { type, defaultVersion = "1", headerName = "accept-version", responseHeader = "x-api-version", deprecated = [], sunset } = opts;
11
+ const { type, defaultVersion = "1", headerName = "accept-version", responseHeader = "x-api-version", versions, deprecated = [], sunset } = opts;
11
12
  const deprecatedSet = new Set(deprecated);
13
+ const supportedSet = versions ? new Set(versions) : void 0;
14
+ if (supportedSet && !supportedSet.has(defaultVersion)) throw new Error(`versioningPlugin: defaultVersion '${defaultVersion}' is not in versions [${[...supportedSet].join(", ")}]`);
12
15
  fastify.decorateRequest("apiVersion", defaultVersion);
13
16
  fastify.addHook("onRequest", async (request, reply) => {
14
17
  let version = defaultVersion;
@@ -19,6 +22,10 @@ const versioningPlugin = async (fastify, opts) => {
19
22
  const match = request.url.match(PREFIX_REGEX);
20
23
  if (match) version = match[1] ?? defaultVersion;
21
24
  }
25
+ if (supportedSet && !supportedSet.has(version)) throw createDomainError("arc.unsupported_api_version", `API version '${version}' is not supported`, 400, {
26
+ requested: version,
27
+ supported: [...supportedSet]
28
+ });
22
29
  request.apiVersion = version;
23
30
  reply.header(responseHeader, version);
24
31
  if (deprecatedSet.has(version)) {
@@ -66,7 +66,15 @@ interface SendQueueOptions {
66
66
  */
67
67
  declare class SendQueue {
68
68
  private entries;
69
- /** `coalesceKey → entries[] index` for O(1) replacement. */
69
+ /**
70
+ * Logical queue head — `entries[head..]` is the live queue. Draining
71
+ * advances the pointer instead of `shift()`ing (which moves every
72
+ * trailing element AND invalidated every tracked coalesce index,
73
+ * making a full drain O(n²)). The prefix is garbage-collected by
74
+ * {@link compactIfNeeded} — amortized O(1) per drained frame.
75
+ */
76
+ private head;
77
+ /** `coalesceKey → ABSOLUTE entries[] index` for O(1) replacement. */
70
78
  private coalesceIndex;
71
79
  private socket;
72
80
  private capacity;
@@ -92,6 +100,12 @@ declare class SendQueue {
92
100
  */
93
101
  flush(): void;
94
102
  size(): number;
103
+ /**
104
+ * Drop the drained prefix once it dominates the array. Rebuilding the
105
+ * (small) coalesce index here is the amortized cost that replaces the
106
+ * per-frame O(n) reindex the old `shift()` drain paid.
107
+ */
108
+ private compactIfNeeded;
95
109
  dispose(): void;
96
110
  /**
97
111
  * One-shot retry timer. Called when `flush()` hits backpressure and