@classytic/arc 2.23.0 → 2.25.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 (132) hide show
  1. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController--M8EMzt0.mjs} +90 -21
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  4. package/dist/audit/index.d.mts +32 -3
  5. package/dist/audit/index.mjs +7 -4
  6. package/dist/auth/index.d.mts +2 -2
  7. package/dist/auth/index.mjs +263 -259
  8. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  9. package/dist/cache/index.d.mts +41 -3
  10. package/dist/cache/index.mjs +33 -4
  11. package/dist/cli/commands/describe.d.mts +1 -1
  12. package/dist/cli/commands/docs.mjs +1 -1
  13. package/dist/cli/commands/generate.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +2 -2
  16. package/dist/context/index.mjs +2 -64
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-DQ6rDTyH.mjs → core-EUNu0u07.mjs} +296 -11
  20. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-B6GvAuAX.mjs} +3 -2
  21. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  25. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  26. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  27. package/dist/events/index.d.mts +57 -314
  28. package/dist/events/index.mjs +163 -91
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  31. package/dist/factory/index.d.mts +4 -5
  32. package/dist/factory/index.mjs +28 -8
  33. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  34. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  35. package/dist/headers-BvlQSDMB.mjs +45 -0
  36. package/dist/health-CB_m4S32.mjs +377 -0
  37. package/dist/hooks/index.d.mts +1 -1
  38. package/dist/idempotency/index.mjs +27 -3
  39. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  40. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  41. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  42. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  43. package/dist/{index-CC5ruDGh.d.mts → index-f4PTrk3B.d.mts} +37 -5
  44. package/dist/index.d.mts +6 -6
  45. package/dist/index.mjs +5 -5
  46. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  47. package/dist/integrations/event-gateway.d.mts +1 -1
  48. package/dist/integrations/event-gateway.mjs +1 -1
  49. package/dist/integrations/index.d.mts +3 -3
  50. package/dist/integrations/jobs/index.d.mts +2 -0
  51. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  52. package/dist/integrations/mcp/index.d.mts +4 -4
  53. package/dist/integrations/mcp/index.mjs +2 -2
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/integrations/webhooks.d.mts +37 -30
  57. package/dist/integrations/webhooks.mjs +51 -62
  58. package/dist/integrations/websocket-redis.d.mts +1 -1
  59. package/dist/integrations/websocket.d.mts +1 -1
  60. package/dist/integrations/websocket.mjs +41 -14
  61. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  62. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cb7olw0j.mjs} +11 -9
  63. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  64. package/dist/middleware/index.d.mts +17 -1
  65. package/dist/middleware/index.mjs +1 -1
  66. package/dist/migrations/index.d.mts +94 -44
  67. package/dist/migrations/index.mjs +196 -44
  68. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  69. package/dist/{openapi-jSDx-VDv.mjs → openapi-DrLtog_B.mjs} +3 -3
  70. package/dist/permissions/index.d.mts +2 -2
  71. package/dist/permissions/index.mjs +2 -2
  72. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  73. package/dist/pipeline/index.d.mts +1 -1
  74. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BN5G05rd.mjs} +3 -26
  75. package/dist/plugins/index.d.mts +6 -77
  76. package/dist/plugins/index.mjs +11 -95
  77. package/dist/plugins/response-cache.mjs +1 -0
  78. package/dist/plugins/tracing-entry.mjs +1 -1
  79. package/dist/{index-Ba10fdKp.d.mts → preloadResources-0oe-JSSS.d.mts} +50 -50
  80. package/dist/{createApp-CjOeWJ9_.mjs → preloadResources-DfyngIN9.mjs} +233 -70
  81. package/dist/presets/filesUpload.d.mts +3 -3
  82. package/dist/presets/filesUpload.mjs +3 -3
  83. package/dist/presets/index.d.mts +2 -1
  84. package/dist/presets/index.mjs +1 -1
  85. package/dist/presets/multiTenant.d.mts +1 -1
  86. package/dist/presets/multiTenant.mjs +1 -1
  87. package/dist/presets/search.d.mts +2 -2
  88. package/dist/presets/search.mjs +1 -1
  89. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  90. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  91. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  92. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  93. package/dist/{realtime-I8OrfnMR.mjs → realtime-Cuw_deA-.mjs} +2 -2
  94. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  95. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  96. package/dist/registry/index.d.mts +2 -2
  97. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  98. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  99. package/dist/schedules-CjOd2MG9.mjs +113 -0
  100. package/dist/schemas/index.d.mts +67 -67
  101. package/dist/schemas/index.mjs +2 -3
  102. package/dist/scim/index.mjs +1 -2
  103. package/dist/scope/index.d.mts +2 -2
  104. package/dist/scope/index.mjs +4 -4
  105. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  106. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  107. package/dist/testing/index.d.mts +4 -3
  108. package/dist/testing/index.mjs +2 -2
  109. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  110. package/dist/types/index.d.mts +4 -4
  111. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  112. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  113. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  114. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  115. package/dist/usage/index.d.mts +3 -52
  116. package/dist/usage/index.mjs +0 -0
  117. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  118. package/dist/utils/index.d.mts +58 -85
  119. package/dist/utils/index.mjs +4 -2
  120. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  121. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  122. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  123. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  124. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  125. package/package.json +526 -513
  126. package/skills/arc/references/api-reference.md +3 -1
  127. package/skills/arc/references/integrations.md +20 -11
  128. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  129. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  130. package/dist/health-ScyjIVo5.mjs +0 -202
  131. package/dist/requestId-5BhTVzBv.mjs +0 -113
  132. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,6 +1,4 @@
1
- import { f as createError } from "./errors-DkO8Nk6-.mjs";
2
- import { n as requestContext } from "./requestContext-BZKOr13H.mjs";
3
- import { n as evaluateAndApplyPermission } from "./applyPermissionResult-Tw4iXtlf.mjs";
1
+ import { n as requestContext } from "./requestContext-MrlJwECf.mjs";
4
2
  //#region src/core/middlewares/arcDecorator.ts
5
3
  /**
6
4
  * Build the `arcDecorator` preHandler for a resource.
@@ -179,76 +177,6 @@ function describeValue(v) {
179
177
  return `${typeof v} (${JSON.stringify(v).slice(0, 80)})`;
180
178
  }
181
179
  //#endregion
182
- //#region src/core/middlewares/permissions.ts
183
- /**
184
- * Build a PermissionContext from a Fastify request. Extracted so the CRUD
185
- * permission middleware and the dynamic action-permission check use the same
186
- * field layout — divergence here silently broke policy filters for actions.
187
- */
188
- function buildPermissionContext(req, opts) {
189
- const reqWithExtras = req;
190
- const params = req.params;
191
- return {
192
- user: reqWithExtras.user ?? null,
193
- request: req,
194
- resource: opts.resource,
195
- action: opts.action,
196
- resourceId: opts.resourceId ?? params?.id,
197
- params,
198
- data: opts.data ?? req.body
199
- };
200
- }
201
- /**
202
- * Static per-route CRUD permission gate. The permission and action are known
203
- * at route-registration time, so the gate is a plain preHandler.
204
- *
205
- * Actions use the dynamic counterpart `buildActionPermissionMw` — their
206
- * permission is resolved from `body.action` at request time.
207
- */
208
- function buildCrudPermissionMw(permissionCheck, resourceName, action) {
209
- if (!permissionCheck) return null;
210
- return async (req, reply) => {
211
- await evaluateAndApplyPermission(permissionCheck, buildPermissionContext(req, {
212
- resource: resourceName,
213
- action
214
- }), req, reply);
215
- };
216
- }
217
- /**
218
- * Dynamic per-action permission gate for the action router.
219
- *
220
- * Resolves the permission from `body.action` at request time and runs
221
- * `evaluateAndApplyPermission` from the canonical `permissionMw` slot — so
222
- * `_policyFilters` and `request.scope` are installed BEFORE `pluginMw`
223
- * (idempotency) and `routeGuards` run. Previously this check lived inside
224
- * the main action handler, which meant idempotency recorded unauthorized
225
- * requests and route guards saw unfiltered scope — the very divergence
226
- * routerShared exists to prevent.
227
- *
228
- * Also acts as a defensive fallback for invalid action names — the
229
- * `oneOf` body schema normally rejects these at AJV validation, but
230
- * hosts that disable schema validation still get a 400 here.
231
- */
232
- function buildActionPermissionMw(actionEnum, actionPermissions, globalAuth, resourceName) {
233
- const enumSet = new Set(actionEnum);
234
- const validActions = [...actionEnum];
235
- return async (req, reply) => {
236
- const body = req.body ?? {};
237
- const action = body.action;
238
- if (!action || !enumSet.has(action)) throw createError(400, `Invalid action '${action ?? ""}'. Valid actions: ${validActions.join(", ")}`, { validActions });
239
- const permissionCheck = actionPermissions[action] ?? globalAuth;
240
- if (!permissionCheck) return;
241
- const { action: _discard, ...data } = body;
242
- const params = req.params;
243
- await evaluateAndApplyPermission(permissionCheck, buildPermissionContext(req, {
244
- resource: resourceName,
245
- action,
246
- resourceId: params?.id,
247
- data
248
- }), req, reply, { defaultDenialMessage: (user) => user ? `Permission denied for '${action}'` : "Authentication required" });
249
- };
250
- }
251
- //#endregion
252
180
  //#region src/core/middlewares/pluginMw.ts
253
181
  /**
254
182
  * Pick the request-lifecycle plugin middleware for an HTTP method:
@@ -314,39 +242,4 @@ function buildRateLimitConfig(rateLimit) {
314
242
  } };
315
243
  }
316
244
  //#endregion
317
- //#region src/core/middlewares/registerRoute.ts
318
- /**
319
- * Register a route with friendly handling of `FST_ERR_DUPLICATED_ROUTE`.
320
- *
321
- * Boot-time `validateRouteCrudCollisions()` catches custom-route → auto-CRUD
322
- * overlaps before Fastify ever runs, but a few cases still slip through to
323
- * Fastify itself: two presets contributing the same `routes` entry, hosts
324
- * mounting two resources at the same prefix, custom routes that share a
325
- * URL with another plugin. Fastify's default message ("Method 'GET'
326
- * already declared for route '/'") gives no hint at the resource-shaped
327
- * fix — `disabledRoutes: ['list']` — so wrap it with one.
328
- */
329
- function tryRegisterRoute(fastify, opts, context) {
330
- try {
331
- fastify.route(opts);
332
- } catch (err) {
333
- const fastifyErr = err;
334
- if (fastifyErr?.code !== "FST_ERR_DUPLICATED_ROUTE") throw err;
335
- const method = opts.method;
336
- const url = opts.url;
337
- const opHint = context.op ? ` (op: ${context.op})` : "";
338
- const fixHint = context.op && [
339
- "list",
340
- "get",
341
- "create",
342
- "update",
343
- "delete"
344
- ].includes(context.op) ? ` Suppress the auto-CRUD route with \`disabledRoutes: ['${context.op}']\`.` : " If a custom route in `routes:` collides with auto-CRUD, add `disabledRoutes: ['list' | 'get' | 'create' | 'update' | 'delete']` to `defineResource()` to suppress the matching auto-CRUD route.";
345
- const enhanced = /* @__PURE__ */ new Error(`[arc] Duplicate route ${method} ${url} on resource "${context.resourceName}"${opHint}.${fixHint} Original: ${fastifyErr.message ?? "FST_ERR_DUPLICATED_ROUTE"}`);
346
- enhanced.code = "FST_ERR_DUPLICATED_ROUTE";
347
- enhanced.cause = err;
348
- throw enhanced;
349
- }
350
- }
351
- //#endregion
352
- export { selectPluginMw as a, buildRouteHooks as c, buildAuthMiddleware as d, buildAuthMiddlewareForPermissions as f, resolveRouterPluginMw as i, resolveRoutePreHandlers as l, buildRateLimitConfig as n, buildActionPermissionMw as o, buildArcDecorator as p, buildRouteConfig as r, buildCrudPermissionMw as s, tryRegisterRoute as t, routeHookOptions as u };
245
+ export { buildRouteHooks as a, buildAuthMiddleware as c, selectPluginMw as i, buildAuthMiddlewareForPermissions as l, buildRouteConfig as n, resolveRoutePreHandlers as o, resolveRouterPluginMw as r, routeHookOptions as s, buildRateLimitConfig as t, buildArcDecorator as u };
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
- import { c as getOrgId } from "./types-bHblZ9YN.mjs";
3
+ import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
4
+ import { c as getOrgId } from "./types-CSyCxl2V.mjs";
4
5
  import { t as simpleEqualityMatcher } from "./simpleEqualityMatcher-Cg05HPsn.mjs";
5
6
  import { i as applyFieldReadPermissions, n as evaluateAndApplyPermission } from "./applyPermissionResult-Tw4iXtlf.mjs";
6
- import { t as getUserRoles } from "./types-XTApa8b6.mjs";
7
7
  import { a as promoteStreamTokenToHeader } from "./streaming-BQcW6_Kq.mjs";
8
8
  import { t as openSseStream } from "./sseStream-BQ7Qaynp.mjs";
9
9
  import fp from "fastify-plugin";
@@ -59,6 +59,20 @@ interface RedisStreamTransportOptions {
59
59
  * @default 10
60
60
  */
61
61
  batchSize?: number;
62
+ /**
63
+ * Bounded concurrency for processing a batch's ENTRIES (default: 1 —
64
+ * strict stream order, the safe default). With one slow handler,
65
+ * throughput per consumer is ~`1 / total handler latency`; raising this
66
+ * processes up to N entries of a batch concurrently.
67
+ *
68
+ * Trade-off: entries within a batch may complete OUT OF ORDER — only
69
+ * raise it when handlers are order-independent (idempotent, keyed
70
+ * upserts). Handlers on this transport should be short and I/O-bound
71
+ * regardless; heavy work belongs in jobs. Matching handlers for a single
72
+ * event always run sequentially.
73
+ * @default 1
74
+ */
75
+ processingConcurrency?: number;
62
76
  /**
63
77
  * Max delivery attempts before moving to dead letter stream.
64
78
  * @default 5
@@ -133,6 +147,7 @@ declare class RedisStreamTransport implements EventTransport {
133
147
  private consumer;
134
148
  private blockTimeMs;
135
149
  private batchSize;
150
+ private processingConcurrency;
136
151
  private maxRetries;
137
152
  private claimTimeoutMs;
138
153
  private deadLetterStream;
@@ -207,6 +222,13 @@ declare class RedisStreamTransport implements EventTransport {
207
222
  private ensureGroup;
208
223
  private pollLoop;
209
224
  private readNewMessages;
225
+ /**
226
+ * Process a batch's entries with bounded concurrency
227
+ * (`processingConcurrency`, default 1 = strict order). Worker-pool over a
228
+ * shared index — at most N entries in flight, each entry's own handlers
229
+ * still sequential inside `processEntry`.
230
+ */
231
+ private processEntries;
210
232
  private claimPending;
211
233
  private processEntry;
212
234
  private getMatchingHandlers;
@@ -0,0 +1,108 @@
1
+ import { f as createError } from "./errors-DkO8Nk6-.mjs";
2
+ import { n as evaluateAndApplyPermission } from "./applyPermissionResult-Tw4iXtlf.mjs";
3
+ //#region src/core/middlewares/permissions.ts
4
+ /**
5
+ * Build a PermissionContext from a Fastify request. Extracted so the CRUD
6
+ * permission middleware and the dynamic action-permission check use the same
7
+ * field layout — divergence here silently broke policy filters for actions.
8
+ */
9
+ function buildPermissionContext(req, opts) {
10
+ const reqWithExtras = req;
11
+ const params = req.params;
12
+ return {
13
+ user: reqWithExtras.user ?? null,
14
+ request: req,
15
+ resource: opts.resource,
16
+ action: opts.action,
17
+ resourceId: opts.resourceId ?? params?.id,
18
+ params,
19
+ data: opts.data ?? req.body
20
+ };
21
+ }
22
+ /**
23
+ * Static per-route CRUD permission gate. The permission and action are known
24
+ * at route-registration time, so the gate is a plain preHandler.
25
+ *
26
+ * Actions use the dynamic counterpart `buildActionPermissionMw` — their
27
+ * permission is resolved from `body.action` at request time.
28
+ */
29
+ function buildCrudPermissionMw(permissionCheck, resourceName, action) {
30
+ if (!permissionCheck) return null;
31
+ return async (req, reply) => {
32
+ await evaluateAndApplyPermission(permissionCheck, buildPermissionContext(req, {
33
+ resource: resourceName,
34
+ action
35
+ }), req, reply);
36
+ };
37
+ }
38
+ /**
39
+ * Dynamic per-action permission gate for the action router.
40
+ *
41
+ * Resolves the permission from `body.action` at request time and runs
42
+ * `evaluateAndApplyPermission` from the canonical `permissionMw` slot — so
43
+ * `_policyFilters` and `request.scope` are installed BEFORE `pluginMw`
44
+ * (idempotency) and `routeGuards` run. Previously this check lived inside
45
+ * the main action handler, which meant idempotency recorded unauthorized
46
+ * requests and route guards saw unfiltered scope — the very divergence
47
+ * routerShared exists to prevent.
48
+ *
49
+ * Also acts as a defensive fallback for invalid action names — the
50
+ * `oneOf` body schema normally rejects these at AJV validation, but
51
+ * hosts that disable schema validation still get a 400 here.
52
+ */
53
+ function buildActionPermissionMw(actionEnum, actionPermissions, globalAuth, resourceName) {
54
+ const enumSet = new Set(actionEnum);
55
+ const validActions = [...actionEnum];
56
+ return async (req, reply) => {
57
+ const body = req.body ?? {};
58
+ const action = body.action;
59
+ if (!action || !enumSet.has(action)) throw createError(400, `Invalid action '${action ?? ""}'. Valid actions: ${validActions.join(", ")}`, { validActions });
60
+ const permissionCheck = actionPermissions[action] ?? globalAuth;
61
+ if (!permissionCheck) return;
62
+ const { action: _discard, ...data } = body;
63
+ const params = req.params;
64
+ await evaluateAndApplyPermission(permissionCheck, buildPermissionContext(req, {
65
+ resource: resourceName,
66
+ action,
67
+ resourceId: params?.id,
68
+ data
69
+ }), req, reply, { defaultDenialMessage: (user) => user ? `Permission denied for '${action}'` : "Authentication required" });
70
+ };
71
+ }
72
+ //#endregion
73
+ //#region src/core/middlewares/registerRoute.ts
74
+ /**
75
+ * Register a route with friendly handling of `FST_ERR_DUPLICATED_ROUTE`.
76
+ *
77
+ * Boot-time `validateRouteCrudCollisions()` catches custom-route → auto-CRUD
78
+ * overlaps before Fastify ever runs, but a few cases still slip through to
79
+ * Fastify itself: two presets contributing the same `routes` entry, hosts
80
+ * mounting two resources at the same prefix, custom routes that share a
81
+ * URL with another plugin. Fastify's default message ("Method 'GET'
82
+ * already declared for route '/'") gives no hint at the resource-shaped
83
+ * fix — `disabledRoutes: ['list']` — so wrap it with one.
84
+ */
85
+ function tryRegisterRoute(fastify, opts, context) {
86
+ try {
87
+ fastify.route(opts);
88
+ } catch (err) {
89
+ const fastifyErr = err;
90
+ if (fastifyErr?.code !== "FST_ERR_DUPLICATED_ROUTE") throw err;
91
+ const method = opts.method;
92
+ const url = opts.url;
93
+ const opHint = context.op ? ` (op: ${context.op})` : "";
94
+ const fixHint = context.op && [
95
+ "list",
96
+ "get",
97
+ "create",
98
+ "update",
99
+ "delete"
100
+ ].includes(context.op) ? ` Suppress the auto-CRUD route with \`disabledRoutes: ['${context.op}']\`.` : " If a custom route in `routes:` collides with auto-CRUD, add `disabledRoutes: ['list' | 'get' | 'create' | 'update' | 'delete']` to `defineResource()` to suppress the matching auto-CRUD route.";
101
+ const enhanced = /* @__PURE__ */ new Error(`[arc] Duplicate route ${method} ${url} on resource "${context.resourceName}"${opHint}.${fixHint} Original: ${fastifyErr.message ?? "FST_ERR_DUPLICATED_ROUTE"}`);
102
+ enhanced.code = "FST_ERR_DUPLICATED_ROUTE";
103
+ enhanced.cause = err;
104
+ throw enhanced;
105
+ }
106
+ }
107
+ //#endregion
108
+ export { buildActionPermissionMw as n, buildCrudPermissionMw as r, tryRegisterRoute as t };
@@ -1,5 +1,5 @@
1
- import { K as ResolvedTenantPurge, R as RegisterOptions, V as ResourceDefinition, X as CrudRouteKey, k as IntrospectionPluginOptions, p as RegistryEntry, z as ResourceRegistry } from "../index-BBsvQt7U.mjs";
2
- import "../fastify-augmentation-g_f9omSc.mjs";
1
+ import { K as ResolvedTenantPurge, R as RegisterOptions, V as ResourceDefinition, X as CrudRouteKey, k as IntrospectionPluginOptions, p as RegistryEntry, z as ResourceRegistry } from "../index-DPRH0Cme.mjs";
2
+ import "../fastify-augmentation-dPNBVeIj.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { TenantPurgeProgress, TenantPurgeStrategy } from "@classytic/repo-core/repository";
5
5
  //#region src/registry/assertNoTenantData.d.ts
@@ -0,0 +1,44 @@
1
+ //#region src/lock/renewingLease.ts
2
+ /**
3
+ * Start renewing an ALREADY-ACQUIRED lease. The caller performed the
4
+ * initial `tryAcquire` (its contention policy differs per consumer); this
5
+ * keeps the lease alive until `stop()`.
6
+ */
7
+ function startRenewingLease(options) {
8
+ const { lock, name, holderId, leaseMs, onLost, onError } = options;
9
+ const renewEveryMs = options.renewEveryMs ?? Math.max(10, Math.floor(leaseMs / 2));
10
+ let stopped = false;
11
+ let lost = false;
12
+ let timer;
13
+ let inFlight;
14
+ const scheduleNext = () => {
15
+ if (stopped) return;
16
+ timer = setTimeout(() => {
17
+ inFlight = Promise.resolve(lock.tryAcquire(name, holderId, leaseMs)).then((renewed) => {
18
+ if (!renewed && !lost) {
19
+ lost = true;
20
+ onLost?.();
21
+ }
22
+ }).catch((error) => {
23
+ onError?.(error);
24
+ }).finally(() => {
25
+ inFlight = void 0;
26
+ scheduleNext();
27
+ });
28
+ }, renewEveryMs);
29
+ timer.unref?.();
30
+ };
31
+ scheduleNext();
32
+ return {
33
+ get lost() {
34
+ return lost;
35
+ },
36
+ async stop() {
37
+ stopped = true;
38
+ if (timer) clearTimeout(timer);
39
+ await inFlight;
40
+ }
41
+ };
42
+ }
43
+ //#endregion
44
+ export { startRenewingLease as t };
@@ -85,8 +85,8 @@ function getTraceHeaders() {
85
85
  const store = requestContext.get();
86
86
  if (!store) return void 0;
87
87
  const headers = {};
88
- if (store.traceparent) headers["traceparent"] = store.traceparent;
89
- if (store.tracestate) headers["tracestate"] = store.tracestate;
88
+ if (store.traceparent) headers.traceparent = store.traceparent;
89
+ if (store.tracestate) headers.tracestate = store.tracestate;
90
90
  return Object.keys(headers).length > 0 ? headers : void 0;
91
91
  }
92
92
  //#endregion
@@ -0,0 +1,113 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
+ import { t as startRenewingLease } from "./renewingLease-3cuQH7h8.mjs";
3
+ import fp from "fastify-plugin";
4
+ //#region src/plugins/schedules.ts
5
+ var schedules_exports = /* @__PURE__ */ __exportAll({
6
+ default: () => schedules_default,
7
+ schedulesPlugin: () => schedulesPlugin
8
+ });
9
+ const MIN_SAFE_INTERVAL_MS = 1e3;
10
+ const MAX_DEFAULT_LEASE_MS = 3e5;
11
+ const schedulesPlugin = async (fastify, opts) => {
12
+ const { schedules, lock, enabled = true } = opts;
13
+ const holderId = opts.holderId ?? `arc-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
14
+ const stats = /* @__PURE__ */ new Map();
15
+ fastify.decorate("getScheduleStats", () => [...stats.values()].map((s) => ({ ...s })));
16
+ if (!enabled) {
17
+ fastify.log.debug("schedulesPlugin disabled — registered as no-op");
18
+ return;
19
+ }
20
+ const seen = /* @__PURE__ */ new Set();
21
+ for (const s of schedules) {
22
+ if (!s.name.trim()) throw new TypeError("[arc-schedules] every schedule needs a name");
23
+ if (seen.has(s.name)) throw new TypeError(`[arc-schedules] duplicate schedule name "${s.name}"`);
24
+ seen.add(s.name);
25
+ if (!Number.isFinite(s.every) || s.every <= 0) throw new TypeError(`[arc-schedules] "${s.name}": \`every\` must be a positive number of ms`);
26
+ if (typeof s.handler !== "function") throw new TypeError(`[arc-schedules] "${s.name}": \`handler\` must be a function`);
27
+ if (s.jitterMs !== void 0 && (!Number.isFinite(s.jitterMs) || s.jitterMs < 0)) throw new TypeError(`[arc-schedules] "${s.name}": \`jitterMs\` must be a non-negative number of ms`);
28
+ if (s.leaseMs !== void 0 && (!Number.isFinite(s.leaseMs) || s.leaseMs <= 0)) throw new TypeError(`[arc-schedules] "${s.name}": \`leaseMs\` must be a positive number of ms`);
29
+ if (s.every < MIN_SAFE_INTERVAL_MS) fastify.log.warn(`[arc-schedules] "${s.name}" runs every ${s.every}ms — sub-second schedules are almost always a bug in production (DB pressure, lock churn). Intended for tests only.`);
30
+ }
31
+ const timers = /* @__PURE__ */ new Map();
32
+ const inFlight = /* @__PURE__ */ new Map();
33
+ let closing = false;
34
+ async function tick(s) {
35
+ const stat = stats.get(s.name);
36
+ if (!stat) return;
37
+ let lease;
38
+ try {
39
+ if (lock) {
40
+ const lockName = `arc:schedule:${s.name}`;
41
+ const leaseMs = s.leaseMs ?? Math.min(Math.floor(s.every * .9), MAX_DEFAULT_LEASE_MS);
42
+ if (!await lock.tryAcquire(lockName, holderId, leaseMs)) {
43
+ stat.skippedByLock++;
44
+ return;
45
+ }
46
+ lease = startRenewingLease({
47
+ lock,
48
+ name: lockName,
49
+ holderId,
50
+ leaseMs,
51
+ onLost: () => fastify.log.warn({ schedule: s.name }, "[arc-schedules] lease renewal lost — another holder took the lock; this run may now overlap. Handlers should be idempotent."),
52
+ onError: (err) => fastify.log.warn({
53
+ err,
54
+ schedule: s.name
55
+ }, "[arc-schedules] lease renewal errored (lease may lapse mid-run)")
56
+ });
57
+ }
58
+ stat.runs++;
59
+ stat.lastRunAt = (/* @__PURE__ */ new Date()).toISOString();
60
+ await s.handler(fastify);
61
+ stat.lastError = null;
62
+ } catch (err) {
63
+ stat.failures++;
64
+ stat.lastError = err instanceof Error ? err.message : String(err);
65
+ fastify.log.error({
66
+ err,
67
+ schedule: s.name
68
+ }, "[arc-schedules] schedule run failed");
69
+ } finally {
70
+ await lease?.stop();
71
+ }
72
+ }
73
+ function scheduleNext(s, delay) {
74
+ if (closing) return;
75
+ const timer = setTimeout(() => {
76
+ const run = tick(s).finally(() => {
77
+ inFlight.delete(s.name);
78
+ scheduleNext(s, s.every);
79
+ });
80
+ inFlight.set(s.name, run);
81
+ }, delay);
82
+ timer.unref?.();
83
+ timers.set(s.name, timer);
84
+ }
85
+ fastify.addHook("onReady", async () => {
86
+ for (const s of schedules) {
87
+ if (s.enabled === false) continue;
88
+ stats.set(s.name, {
89
+ name: s.name,
90
+ runs: 0,
91
+ failures: 0,
92
+ skippedByLock: 0,
93
+ lastRunAt: null,
94
+ lastError: null
95
+ });
96
+ const jitter = !s.runOnStart && s.jitterMs ? Math.floor(Math.random() * s.jitterMs) : 0;
97
+ scheduleNext(s, s.runOnStart ? 0 : s.every + jitter);
98
+ }
99
+ if (stats.size > 0) fastify.log.debug(`[arc-schedules] ${stats.size} schedule(s) armed${lock ? ` (leader-safe, holder ${holderId})` : ""}`);
100
+ });
101
+ fastify.addHook("onClose", async () => {
102
+ closing = true;
103
+ for (const timer of timers.values()) clearTimeout(timer);
104
+ timers.clear();
105
+ await Promise.allSettled([...inFlight.values()]);
106
+ });
107
+ };
108
+ var schedules_default = fp(schedulesPlugin, {
109
+ name: "arc-schedules",
110
+ fastify: "5.x"
111
+ });
112
+ //#endregion
113
+ export { schedules_default as n, schedules_exports as r, schedulesPlugin as t };
@@ -1,6 +1,6 @@
1
1
  import { ErrorContract, ErrorDetail } from "@classytic/repo-core/errors";
2
- import { FastifyPluginAsyncTypebox, FastifyPluginCallbackTypebox, TypeBoxTypeProvider, TypeBoxValidatorCompiler } from "@fastify/type-provider-typebox";
3
- import { Static, TObject, TSchema, TSchema as TSchema$1, Type } from "@sinclair/typebox";
2
+ import { Static, TObject, TSchema, TSchema as TSchema$1, Type, Type as Type$1 } from "typebox";
3
+ import { FastifyPluginAsyncTypebox, FastifyPluginCallbackTypebox, TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
4
4
  //#region src/schemas/index.d.ts
5
5
  /**
6
6
  * Paginated list response — full union of every wire shape `toCanonicalList`
@@ -16,74 +16,74 @@ import { Static, TObject, TSchema, TSchema as TSchema$1, Type } from "@sinclair/
16
16
  * variant when your endpoint never emits the others. HTTP status
17
17
  * discriminates success vs error — no `success` field.
18
18
  */
19
- declare function ArcListResponse<T extends TSchema$1>(itemSchema: T): import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
20
- method: import("@sinclair/typebox").TLiteral<"offset">;
21
- data: import("@sinclair/typebox").TArray<T>;
22
- page: import("@sinclair/typebox").TInteger;
23
- limit: import("@sinclair/typebox").TInteger;
24
- total: import("@sinclair/typebox").TInteger;
25
- pages: import("@sinclair/typebox").TInteger;
26
- hasNext: import("@sinclair/typebox").TBoolean;
27
- hasPrev: import("@sinclair/typebox").TBoolean;
28
- }>, import("@sinclair/typebox").TObject<{
29
- method: import("@sinclair/typebox").TLiteral<"keyset">;
30
- data: import("@sinclair/typebox").TArray<T>;
31
- limit: import("@sinclair/typebox").TInteger;
32
- hasMore: import("@sinclair/typebox").TBoolean;
33
- next: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>;
34
- }>, import("@sinclair/typebox").TObject<{
35
- method: import("@sinclair/typebox").TLiteral<"aggregate">;
36
- data: import("@sinclair/typebox").TArray<T>;
37
- page: import("@sinclair/typebox").TInteger;
38
- limit: import("@sinclair/typebox").TInteger;
39
- total: import("@sinclair/typebox").TInteger;
40
- pages: import("@sinclair/typebox").TInteger;
41
- hasNext: import("@sinclair/typebox").TBoolean;
42
- hasPrev: import("@sinclair/typebox").TBoolean;
43
- }>, import("@sinclair/typebox").TObject<{
44
- data: import("@sinclair/typebox").TArray<T>;
19
+ declare function ArcListResponse<T extends TSchema$1>(itemSchema: T): Type$1.TUnion<[Type$1.TObject<{
20
+ method: Type$1.TLiteral<"offset">;
21
+ data: Type$1.TArray<T>;
22
+ page: Type$1.TInteger;
23
+ limit: Type$1.TInteger;
24
+ total: Type$1.TInteger;
25
+ pages: Type$1.TInteger;
26
+ hasNext: Type$1.TBoolean;
27
+ hasPrev: Type$1.TBoolean;
28
+ }>, Type$1.TObject<{
29
+ method: Type$1.TLiteral<"keyset">;
30
+ data: Type$1.TArray<T>;
31
+ limit: Type$1.TInteger;
32
+ hasMore: Type$1.TBoolean;
33
+ next: Type$1.TUnion<[Type$1.TString, Type$1.TNull]>;
34
+ }>, Type$1.TObject<{
35
+ method: Type$1.TLiteral<"aggregate">;
36
+ data: Type$1.TArray<T>;
37
+ page: Type$1.TInteger;
38
+ limit: Type$1.TInteger;
39
+ total: Type$1.TInteger;
40
+ pages: Type$1.TInteger;
41
+ hasNext: Type$1.TBoolean;
42
+ hasPrev: Type$1.TBoolean;
43
+ }>, Type$1.TObject<{
44
+ data: Type$1.TArray<T>;
45
45
  }>]>;
46
46
  /** Offset variant — `{ method: 'offset', data, page, limit, total, pages, hasNext, hasPrev }`. */
47
- declare function ArcOffsetListResponse<T extends TSchema$1>(itemSchema: T): import("@sinclair/typebox").TObject<{
48
- method: import("@sinclair/typebox").TLiteral<"offset">;
49
- data: import("@sinclair/typebox").TArray<T>;
50
- page: import("@sinclair/typebox").TInteger;
51
- limit: import("@sinclair/typebox").TInteger;
52
- total: import("@sinclair/typebox").TInteger;
53
- pages: import("@sinclair/typebox").TInteger;
54
- hasNext: import("@sinclair/typebox").TBoolean;
55
- hasPrev: import("@sinclair/typebox").TBoolean;
47
+ declare function ArcOffsetListResponse<T extends TSchema$1>(itemSchema: T): Type$1.TObject<{
48
+ method: Type$1.TLiteral<"offset">;
49
+ data: Type$1.TArray<T>;
50
+ page: Type$1.TInteger;
51
+ limit: Type$1.TInteger;
52
+ total: Type$1.TInteger;
53
+ pages: Type$1.TInteger;
54
+ hasNext: Type$1.TBoolean;
55
+ hasPrev: Type$1.TBoolean;
56
56
  }>;
57
57
  /** Keyset variant — `{ method: 'keyset', data, limit, hasMore, next: string | null }`. */
58
- declare function ArcKeysetListResponse<T extends TSchema$1>(itemSchema: T): import("@sinclair/typebox").TObject<{
59
- method: import("@sinclair/typebox").TLiteral<"keyset">;
60
- data: import("@sinclair/typebox").TArray<T>;
61
- limit: import("@sinclair/typebox").TInteger;
62
- hasMore: import("@sinclair/typebox").TBoolean;
63
- next: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>;
58
+ declare function ArcKeysetListResponse<T extends TSchema$1>(itemSchema: T): Type$1.TObject<{
59
+ method: Type$1.TLiteral<"keyset">;
60
+ data: Type$1.TArray<T>;
61
+ limit: Type$1.TInteger;
62
+ hasMore: Type$1.TBoolean;
63
+ next: Type$1.TUnion<[Type$1.TString, Type$1.TNull]>;
64
64
  }>;
65
65
  /** Aggregate variant — `{ method: 'aggregate', ...same as offset }`. */
66
- declare function ArcAggregateListResponse<T extends TSchema$1>(itemSchema: T): import("@sinclair/typebox").TObject<{
67
- method: import("@sinclair/typebox").TLiteral<"aggregate">;
68
- data: import("@sinclair/typebox").TArray<T>;
69
- page: import("@sinclair/typebox").TInteger;
70
- limit: import("@sinclair/typebox").TInteger;
71
- total: import("@sinclair/typebox").TInteger;
72
- pages: import("@sinclair/typebox").TInteger;
73
- hasNext: import("@sinclair/typebox").TBoolean;
74
- hasPrev: import("@sinclair/typebox").TBoolean;
66
+ declare function ArcAggregateListResponse<T extends TSchema$1>(itemSchema: T): Type$1.TObject<{
67
+ method: Type$1.TLiteral<"aggregate">;
68
+ data: Type$1.TArray<T>;
69
+ page: Type$1.TInteger;
70
+ limit: Type$1.TInteger;
71
+ total: Type$1.TInteger;
72
+ pages: Type$1.TInteger;
73
+ hasNext: Type$1.TBoolean;
74
+ hasPrev: Type$1.TBoolean;
75
75
  }>;
76
76
  /** Bare variant — `{ data }`, no `method` discriminant. */
77
- declare function ArcBareListResponse<T extends TSchema$1>(itemSchema: T): import("@sinclair/typebox").TObject<{
78
- data: import("@sinclair/typebox").TArray<T>;
77
+ declare function ArcBareListResponse<T extends TSchema$1>(itemSchema: T): Type$1.TObject<{
78
+ data: Type$1.TArray<T>;
79
79
  }>;
80
80
  /**
81
81
  * Delete response — `{ message, id?, soft? }` raw at the top level.
82
82
  */
83
- declare function ArcDeleteResponse(): import("@sinclair/typebox").TObject<{
84
- message: import("@sinclair/typebox").TString;
85
- id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
86
- soft: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
83
+ declare function ArcDeleteResponse(): Type$1.TObject<{
84
+ message: Type$1.TString;
85
+ id: Type$1.TOptional<Type$1.TString>;
86
+ soft: Type$1.TOptional<Type$1.TBoolean>;
87
87
  }>;
88
88
  /**
89
89
  * Single field-scoped error detail — `Type.Unsafe<ErrorDetail>` over the
@@ -95,7 +95,7 @@ declare function ArcDeleteResponse(): import("@sinclair/typebox").TObject<{
95
95
  * Exported standalone so hosts can embed it in custom 422 / 409 response
96
96
  * schemas.
97
97
  */
98
- declare function ArcErrorDetail(): import("@sinclair/typebox").TUnsafe<ErrorDetail>;
98
+ declare function ArcErrorDetail(): Type$1.TUnsafe<ErrorDetail>;
99
99
  /**
100
100
  * Error response schema — `Type.Unsafe<ErrorContract>` over the canonical
101
101
  * JSON Schema `errorContractSchema` from `@classytic/repo-core/errors`.
@@ -104,7 +104,7 @@ declare function ArcErrorDetail(): import("@sinclair/typebox").TUnsafe<ErrorDeta
104
104
  * (`errorContractSchema`) and this TypeBox helper cannot drift —
105
105
  * literally one source.
106
106
  */
107
- declare function ArcErrorResponse(): import("@sinclair/typebox").TUnsafe<ErrorContract>;
107
+ declare function ArcErrorResponse(): Type$1.TUnsafe<ErrorContract>;
108
108
  /**
109
109
  * Standard pagination + sorting + filtering query parameters.
110
110
  * Matches Arc's list endpoint conventions.
@@ -120,12 +120,12 @@ declare function ArcErrorResponse(): import("@sinclair/typebox").TUnsafe<ErrorCo
120
120
  * projection forms at the request-validation gate even though arc passes
121
121
  * them through unchanged.
122
122
  */
123
- declare function ArcPaginationQuery(): import("@sinclair/typebox").TObject<{
124
- page: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
125
- limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
126
- sort: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
127
- select: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>, import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<0>, import("@sinclair/typebox").TLiteral<1>]>>]>>;
128
- populate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
123
+ declare function ArcPaginationQuery(): Type$1.TObject<{
124
+ page: Type$1.TOptional<Type$1.TInteger>;
125
+ limit: Type$1.TOptional<Type$1.TInteger>;
126
+ sort: Type$1.TOptional<Type$1.TString>;
127
+ select: Type$1.TOptional<Type$1.TUnion<[Type$1.TString, Type$1.TArray<Type$1.TString>, Type$1.TRecord<"^.*$", Type$1.TUnion<[Type$1.TLiteral<0>, Type$1.TLiteral<1>]>>]>>;
128
+ populate: Type$1.TOptional<Type$1.TAny>;
129
129
  }>;
130
130
  //#endregion
131
- export { ArcAggregateListResponse, ArcBareListResponse, ArcDeleteResponse, ArcErrorDetail, ArcErrorResponse, ArcKeysetListResponse, ArcListResponse, ArcOffsetListResponse, ArcPaginationQuery, type FastifyPluginAsyncTypebox, type FastifyPluginCallbackTypebox, type Static, type TObject, type TSchema, Type, type TypeBoxTypeProvider, TypeBoxValidatorCompiler };
131
+ export { ArcAggregateListResponse, ArcBareListResponse, ArcDeleteResponse, ArcErrorDetail, ArcErrorResponse, ArcKeysetListResponse, ArcListResponse, ArcOffsetListResponse, ArcPaginationQuery, type FastifyPluginAsyncTypebox, type FastifyPluginCallbackTypebox, type Static, type TObject, type TSchema, Type, type TypeBoxTypeProvider };