@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
@@ -0,0 +1,377 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fp from "fastify-plugin";
3
+ //#region src/plugins/requestId.ts
4
+ /**
5
+ * Request ID Plugin
6
+ *
7
+ * Propagates request IDs for distributed tracing.
8
+ * - Accepts incoming x-request-id header (sanitized)
9
+ * - Generates UUID if not provided
10
+ * - Echoes the id on the response header + W3C trace-context propagation
11
+ *
12
+ * ID RESOLUTION LIVES AT THE SERVER LEVEL, NOT HERE. Fastify binds
13
+ * `request.log = logger.child({ reqId: request.id })` at request
14
+ * construction — before any hook runs — so an id assigned inside a hook
15
+ * never reaches the logs. `createApp` therefore wires
16
+ * {@link createRequestIdGenerator} into Fastify's `genReqId` option, and
17
+ * this plugin simply adopts `request.id`. Standalone registration on a
18
+ * bare Fastify instance falls back to the legacy in-hook resolution
19
+ * (header echo / UUID), with the documented limitation that
20
+ * `request.log`'s `reqId` binding predates the overwrite — pass
21
+ * `genReqId: createRequestIdGenerator()` to `Fastify()` to fix that.
22
+ *
23
+ * @example
24
+ * import { requestIdPlugin, createRequestIdGenerator } from '@classytic/arc';
25
+ *
26
+ * const fastify = Fastify({ genReqId: createRequestIdGenerator() });
27
+ * await fastify.register(requestIdPlugin);
28
+ *
29
+ * fastify.get('/', async (request) => {
30
+ * request.log.info('traced'); // carries the same reqId as the header
31
+ * return { id: request.id };
32
+ * });
33
+ */
34
+ /**
35
+ * Sanitize an incoming request-id header value: max 128 chars, alphanumeric
36
+ * plus dashes/underscores/dots/colons only. Returns `undefined` for anything
37
+ * else (crafted values could pollute logs or response headers). Array values
38
+ * (repeated headers) are rejected outright — a client sending multiple
39
+ * request-id headers is not tracing in good faith.
40
+ */
41
+ function sanitizeRequestId(value) {
42
+ if (typeof value !== "string") return void 0;
43
+ const trimmed = value.trim();
44
+ return trimmed.length > 0 && trimmed.length <= 128 && /^[\w.:-]+$/.test(trimmed) ? trimmed : void 0;
45
+ }
46
+ /**
47
+ * Build a Fastify `genReqId` function: adopt a sanitized incoming request-id
48
+ * header, or generate one. Wire this into the `Fastify()` constructor so
49
+ * `request.id`, `request.log`'s `reqId` binding, and the echoed response
50
+ * header all agree — `createApp` does this automatically.
51
+ */
52
+ function createRequestIdGenerator(opts = {}) {
53
+ const { header = "x-request-id", generator = randomUUID } = opts;
54
+ const headerName = header.toLowerCase();
55
+ return (req) => sanitizeRequestId(req.headers[headerName]) ?? generator();
56
+ }
57
+ /** Fastify's default request ids are `req-<base36 counter>`. */
58
+ const FASTIFY_DEFAULT_ID = /^req-[0-9a-z]+$/;
59
+ /** W3C traceparent format: `version-traceid-parentid-flags` */
60
+ const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
61
+ /** W3C spec: version ff is reserved and invalid; trace/parent IDs must not be all zeros */
62
+ const TRACEID_ZEROS = "0".repeat(32);
63
+ const PARENTID_ZEROS = "0".repeat(16);
64
+ /** tracestate: printable ASCII, <= 512 bytes */
65
+ const TRACESTATE_MAX = 512;
66
+ function isValidTraceparent(tp) {
67
+ const m = TRACEPARENT_RE.exec(tp);
68
+ if (!m) return false;
69
+ const version = m[1] ?? "";
70
+ const traceId = m[2] ?? "";
71
+ const parentId = m[3] ?? "";
72
+ if (version.toLowerCase() === "ff") return false;
73
+ if (traceId.toLowerCase() === TRACEID_ZEROS) return false;
74
+ if (parentId.toLowerCase() === PARENTID_ZEROS) return false;
75
+ return true;
76
+ }
77
+ const requestIdPlugin = async (fastify, opts = {}) => {
78
+ const { header = "x-request-id", generator = randomUUID, setResponseHeader = true, propagateTraceContext = true } = opts;
79
+ if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
80
+ if (!fastify.hasRequestDecorator("traceContext")) fastify.decorateRequest("traceContext", void 0);
81
+ fastify.addHook("onRequest", async (request, reply) => {
82
+ const incoming = sanitizeRequestId(request.headers[header]);
83
+ let requestId = String(request.id);
84
+ if (incoming && incoming !== requestId) requestId = incoming;
85
+ else if (!incoming && FASTIFY_DEFAULT_ID.test(requestId)) requestId = generator();
86
+ if (requestId !== request.id) request.id = requestId;
87
+ request.requestId = requestId;
88
+ if (setResponseHeader) reply.header(header, requestId);
89
+ if (propagateTraceContext) {
90
+ const rawTraceparent = request.headers.traceparent;
91
+ const rawTracestate = request.headers.tracestate;
92
+ if (typeof rawTraceparent === "string") {
93
+ const tp = rawTraceparent.trim();
94
+ if (isValidTraceparent(tp)) {
95
+ const ts = typeof rawTracestate === "string" && rawTracestate.length > 0 && rawTracestate.length <= TRACESTATE_MAX ? rawTracestate : void 0;
96
+ request.traceContext = {
97
+ traceparent: tp,
98
+ ...ts ? { tracestate: ts } : {}
99
+ };
100
+ reply.header("traceparent", tp);
101
+ if (ts) reply.header("tracestate", ts);
102
+ }
103
+ }
104
+ }
105
+ });
106
+ fastify.log?.debug?.("Request ID plugin registered");
107
+ };
108
+ var requestId_default = fp(requestIdPlugin, {
109
+ name: "arc-request-id",
110
+ fastify: "5.x"
111
+ });
112
+ //#endregion
113
+ //#region src/plugins/health.ts
114
+ /**
115
+ * Merge health checks without losing ownership information.
116
+ *
117
+ * Check names are the identity used in readiness responses and criticality
118
+ * lookup, so duplicates are ambiguous and fail at boot instead of silently
119
+ * picking the first declaration. Input order is preserved.
120
+ */
121
+ function mergeHealthChecks(groups) {
122
+ const owners = /* @__PURE__ */ new Map();
123
+ const merged = [];
124
+ for (const group of groups) for (const check of group.checks ?? []) {
125
+ const prior = owners.get(check.name);
126
+ if (prior !== void 0) throw new Error(`[arc] duplicate health-check name "${check.name}" — declared by ${prior} and ${group.owner}. Health-check names must be unique.`);
127
+ owners.set(check.name, group.owner);
128
+ merged.push(check);
129
+ }
130
+ return merged;
131
+ }
132
+ /**
133
+ * Fixed histogram bucket upper bounds (ms). Recording is O(#buckets) per
134
+ * request; the scrape derives quantile UPPER BOUNDS from cumulative counts
135
+ * in O(#buckets) — no sample retention, no per-scrape sort (the previous
136
+ * 10k-sample ring buffer copied + sorted on EVERY scrape), and bucket
137
+ * counts aggregate correctly across instances.
138
+ */
139
+ const DURATION_BUCKETS_MS = [
140
+ 1,
141
+ 2,
142
+ 5,
143
+ 10,
144
+ 25,
145
+ 50,
146
+ 100,
147
+ 250,
148
+ 500,
149
+ 1e3,
150
+ 2500,
151
+ 5e3,
152
+ 1e4
153
+ ];
154
+ function createHttpMetrics() {
155
+ return {
156
+ requestsTotal: {},
157
+ durationBuckets: new Array(DURATION_BUCKETS_MS.length + 1).fill(0),
158
+ durationCount: 0,
159
+ durationSumMs: 0,
160
+ startTime: Date.now()
161
+ };
162
+ }
163
+ /** Smallest bucket upper bound whose cumulative count reaches `q × total`. */
164
+ function bucketQuantile(metrics, q) {
165
+ const target = Math.ceil(metrics.durationCount * q);
166
+ let cumulative = 0;
167
+ for (let i = 0; i < metrics.durationBuckets.length; i++) {
168
+ cumulative += metrics.durationBuckets[i] ?? 0;
169
+ if (cumulative >= target) return DURATION_BUCKETS_MS[i] ?? DURATION_BUCKETS_MS[DURATION_BUCKETS_MS.length - 1] ?? 0;
170
+ }
171
+ return DURATION_BUCKETS_MS[DURATION_BUCKETS_MS.length - 1] ?? 0;
172
+ }
173
+ const healthPlugin = async (fastify, opts = {}) => {
174
+ const { prefix = "/_health", checks = [], metrics = false, metricsCollector, version, collectHttpMetrics = metrics } = opts;
175
+ const readinessChecks = mergeHealthChecks([{
176
+ owner: "healthPlugin options",
177
+ checks
178
+ }]);
179
+ const httpMetrics = createHttpMetrics();
180
+ fastify.get(prefix, { schema: {
181
+ tags: ["Health"],
182
+ summary: "Liveness probe (alias for /live)",
183
+ description: "Returns 200 if the process is alive",
184
+ response: { 200: {
185
+ type: "object",
186
+ properties: {
187
+ status: {
188
+ type: "string",
189
+ enum: ["ok"]
190
+ },
191
+ timestamp: { type: "string" },
192
+ version: { type: "string" }
193
+ }
194
+ } }
195
+ } }, async () => ({
196
+ status: "ok",
197
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
198
+ ...version ? { version } : {}
199
+ }));
200
+ fastify.get(`${prefix}/live`, { schema: {
201
+ tags: ["Health"],
202
+ summary: "Liveness probe",
203
+ description: "Returns 200 if the process is alive",
204
+ response: { 200: {
205
+ type: "object",
206
+ properties: {
207
+ status: {
208
+ type: "string",
209
+ enum: ["ok"]
210
+ },
211
+ timestamp: { type: "string" },
212
+ version: { type: "string" }
213
+ }
214
+ } }
215
+ } }, async () => {
216
+ return {
217
+ status: "ok",
218
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
219
+ ...version ? { version } : {}
220
+ };
221
+ });
222
+ fastify.get(`${prefix}/ready`, { schema: {
223
+ tags: ["Health"],
224
+ summary: "Readiness probe",
225
+ description: "Returns 200 if all dependencies are healthy",
226
+ response: {
227
+ 200: {
228
+ type: "object",
229
+ properties: {
230
+ status: {
231
+ type: "string",
232
+ enum: ["ready", "not_ready"]
233
+ },
234
+ timestamp: { type: "string" },
235
+ checks: {
236
+ type: "array",
237
+ items: {
238
+ type: "object",
239
+ properties: {
240
+ name: { type: "string" },
241
+ healthy: { type: "boolean" },
242
+ duration: { type: "number" },
243
+ error: { type: "string" }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ },
249
+ 503: {
250
+ type: "object",
251
+ properties: {
252
+ status: {
253
+ type: "string",
254
+ enum: ["not_ready"]
255
+ },
256
+ timestamp: { type: "string" },
257
+ checks: { type: "array" }
258
+ }
259
+ }
260
+ }
261
+ } }, async (_, reply) => {
262
+ const results = await runChecks(readinessChecks);
263
+ const criticalFailed = results.some((result, index) => !result.healthy && (readinessChecks[index]?.critical ?? true));
264
+ const response = {
265
+ status: criticalFailed ? "not_ready" : "ready",
266
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
267
+ checks: results
268
+ };
269
+ if (criticalFailed) reply.code(503);
270
+ return response;
271
+ });
272
+ if (metrics) fastify.get(`${prefix}/metrics`, async (_, reply) => {
273
+ reply.type("text/plain; charset=utf-8");
274
+ if (metricsCollector) return await metricsCollector();
275
+ const uptime = process.uptime();
276
+ const memory = process.memoryUsage();
277
+ const cpu = process.cpuUsage();
278
+ const lines = [
279
+ "# HELP process_uptime_seconds Process uptime in seconds",
280
+ "# TYPE process_uptime_seconds gauge",
281
+ `process_uptime_seconds ${uptime.toFixed(2)}`,
282
+ "",
283
+ "# HELP process_memory_heap_bytes Heap memory usage in bytes",
284
+ "# TYPE process_memory_heap_bytes gauge",
285
+ `process_memory_heap_bytes{type="used"} ${memory.heapUsed}`,
286
+ `process_memory_heap_bytes{type="total"} ${memory.heapTotal}`,
287
+ "",
288
+ "# HELP process_memory_rss_bytes RSS memory in bytes",
289
+ "# TYPE process_memory_rss_bytes gauge",
290
+ `process_memory_rss_bytes ${memory.rss}`,
291
+ "",
292
+ "# HELP process_memory_external_bytes External memory in bytes",
293
+ "# TYPE process_memory_external_bytes gauge",
294
+ `process_memory_external_bytes ${memory.external}`,
295
+ "",
296
+ "# HELP process_cpu_user_microseconds User CPU time in microseconds",
297
+ "# TYPE process_cpu_user_microseconds counter",
298
+ `process_cpu_user_microseconds ${cpu.user}`,
299
+ "",
300
+ "# HELP process_cpu_system_microseconds System CPU time in microseconds",
301
+ "# TYPE process_cpu_system_microseconds counter",
302
+ `process_cpu_system_microseconds ${cpu.system}`,
303
+ ""
304
+ ];
305
+ if (collectHttpMetrics && Object.keys(httpMetrics.requestsTotal).length > 0) {
306
+ lines.push("# HELP http_requests_total Total HTTP requests by status code", "# TYPE http_requests_total counter");
307
+ for (const [status, count] of Object.entries(httpMetrics.requestsTotal)) lines.push(`http_requests_total{status="${status}"} ${count}`);
308
+ lines.push("");
309
+ if (httpMetrics.durationCount > 0) {
310
+ const p50 = bucketQuantile(httpMetrics, .5);
311
+ const p95 = bucketQuantile(httpMetrics, .95);
312
+ const p99 = bucketQuantile(httpMetrics, .99);
313
+ lines.push("# HELP http_request_duration_milliseconds HTTP request duration (bucket upper bounds)", "# TYPE http_request_duration_milliseconds summary", `http_request_duration_milliseconds{quantile="0.5"} ${p50.toFixed(2)}`, `http_request_duration_milliseconds{quantile="0.95"} ${p95.toFixed(2)}`, `http_request_duration_milliseconds{quantile="0.99"} ${p99.toFixed(2)}`, `http_request_duration_milliseconds_sum ${httpMetrics.durationSumMs.toFixed(2)}`, `http_request_duration_milliseconds_count ${httpMetrics.durationCount}`, "");
314
+ }
315
+ }
316
+ return lines.join("\n");
317
+ });
318
+ if (collectHttpMetrics) fastify.addHook("onResponse", async (_request, reply) => {
319
+ const duration = reply.elapsedTime;
320
+ const statusBucket = `${Math.floor(reply.statusCode / 100)}xx`;
321
+ httpMetrics.requestsTotal[statusBucket] = (httpMetrics.requestsTotal[statusBucket] || 0) + 1;
322
+ let bucket = DURATION_BUCKETS_MS.length;
323
+ for (let i = 0; i < DURATION_BUCKETS_MS.length; i++) {
324
+ const bound = DURATION_BUCKETS_MS[i];
325
+ if (bound !== void 0 && duration <= bound) {
326
+ bucket = i;
327
+ break;
328
+ }
329
+ }
330
+ httpMetrics.durationBuckets[bucket] = (httpMetrics.durationBuckets[bucket] ?? 0) + 1;
331
+ httpMetrics.durationCount++;
332
+ httpMetrics.durationSumMs += duration;
333
+ });
334
+ fastify.log?.debug?.(`Health plugin registered at ${prefix} (alias), ${prefix}/live, ${prefix}/ready${metrics ? `, ${prefix}/metrics` : ""}`);
335
+ };
336
+ /**
337
+ * Run all health checks with timeout
338
+ */
339
+ async function runChecks(checks) {
340
+ return Promise.all(checks.map(async (check) => {
341
+ const start = Date.now();
342
+ const timeout = check.timeout ?? 5e3;
343
+ let timer;
344
+ const abort = new AbortController();
345
+ try {
346
+ const checkPromise = Promise.resolve(check.check(abort.signal));
347
+ const timeoutPromise = new Promise((_, reject) => {
348
+ timer = setTimeout(() => {
349
+ const err = /* @__PURE__ */ new Error("Health check timeout");
350
+ abort.abort(err);
351
+ reject(err);
352
+ }, timeout);
353
+ });
354
+ const healthy = await Promise.race([checkPromise, timeoutPromise]);
355
+ return {
356
+ name: check.name,
357
+ healthy: Boolean(healthy),
358
+ duration: Date.now() - start
359
+ };
360
+ } catch (err) {
361
+ return {
362
+ name: check.name,
363
+ healthy: false,
364
+ duration: Date.now() - start,
365
+ error: err.message
366
+ };
367
+ } finally {
368
+ if (timer) clearTimeout(timer);
369
+ }
370
+ }));
371
+ }
372
+ var health_default = fp(healthPlugin, {
373
+ name: "arc-health",
374
+ fastify: "5.x"
375
+ });
376
+ //#endregion
377
+ export { requestIdPlugin as a, createRequestIdGenerator as i, health_default as n, requestId_default as o, mergeHealthChecks as r, sanitizeRequestId as s, healthPlugin as t };
@@ -1,2 +1,2 @@
1
- import { An as defineHook, Cn as afterCreate, Dn as beforeDelete, En as beforeCreate, On as beforeUpdate, Sn as HookSystemOptions, Tn as afterUpdate, _n as HookHandler, bn as HookRegistration, gn as HookContext, hn as DefineHookOptions, kn as createHookSystem, vn as HookOperation, wn as afterDelete, xn as HookSystem, yn as HookPhase } from "../index-BBsvQt7U.mjs";
1
+ import { Cn as defineHook, Sn as createHookSystem, _n as afterDelete, bn as beforeDelete, cn as DefineHookOptions, dn as HookOperation, fn as HookPhase, gn as afterCreate, hn as HookSystemOptions, ln as HookContext, mn as HookSystem, pn as HookRegistration, un as HookHandler, vn as afterUpdate, xn as beforeUpdate, yn as beforeCreate } from "../index-DPRH0Cme.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,3 +1,4 @@
1
+ import { n as requireSingleHeaderValue } from "../headers-BvlQSDMB.mjs";
1
2
  import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DY6xq4sM.mjs";
2
3
  import { t as createIdempotencyResult } from "../interface-n5hpKg1d.mjs";
3
4
  import { createHash } from "node:crypto";
@@ -249,6 +250,26 @@ var MemoryIdempotencyStore = class {
249
250
  * // Idempotency-Key: order-123-abc
250
251
  *
251
252
  * // If same key sent again within TTL, returns cached response
253
+ *
254
+ * ## Guarantee: best-effort effectively-once, NOT exactly-once
255
+ *
256
+ * The plugin gives strong request replay + overlap protection: a
257
+ * distributed lock guards execution (concurrent same-key requests get
258
+ * 409), the store key includes a body/caller fingerprint (a different
259
+ * body under the same key executes as its own operation instead of
260
+ * replaying someone else's response), and a completed result replays on
261
+ * hit. It does NOT make the business effect exactly-once, because the
262
+ * domain write and the idempotency record live in different systems:
263
+ *
264
+ * - a crash after the DB mutation but before the result is recorded
265
+ * (preSerialization) lets a retry re-execute the mutation;
266
+ * - a handler that outruns `lockTimeoutMs` can overlap its own retry —
267
+ * size the lock timeout above the slowest protected handler.
268
+ *
269
+ * For mutations where re-execution is unacceptable, write the idempotency
270
+ * record in the SAME transaction as the business write inside the handler
271
+ * (the store contract is public — `@classytic/arc/idempotency` exports it),
272
+ * or make the handler naturally idempotent (upserts, compare-and-set).
252
273
  */
253
274
  const HEADER_IDEMPOTENCY_REPLAYED = "x-idempotency-replayed";
254
275
  const HEADER_IDEMPOTENCY_KEY = "x-idempotency-key";
@@ -306,7 +327,11 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
306
327
  */
307
328
  function normalizeBody(obj, depth = 0) {
308
329
  if (obj === null || typeof obj !== "object") return obj;
309
- if (depth >= MAX_FINGERPRINT_DEPTH) return "<truncated>";
330
+ if (depth >= MAX_FINGERPRINT_DEPTH) try {
331
+ return `<deep:${createHash("sha256").update(JSON.stringify(obj)).digest("hex").slice(0, 16)}>`;
332
+ } catch {
333
+ return "<truncated>";
334
+ }
310
335
  if (Array.isArray(obj)) return obj.map((item) => normalizeBody(item, depth + 1));
311
336
  const sorted = {};
312
337
  const keys = Object.keys(obj).sort();
@@ -335,8 +360,7 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
335
360
  }
336
361
  const idempotencyMiddleware = async (request, reply) => {
337
362
  if (!shouldApplyIdempotency(request)) return;
338
- const keyHeader = request.headers[headerName.toLowerCase()];
339
- const idempotencyKey = typeof keyHeader === "string" ? keyHeader.trim() : void 0;
363
+ const idempotencyKey = requireSingleHeaderValue(request.headers, headerName)?.trim();
340
364
  if (!idempotencyKey) return;
341
365
  request.idempotencyKey = idempotencyKey;
342
366
  const fullKey = `${idempotencyKey}:${getRequestFingerprint(request)}`;
@@ -1,6 +1,6 @@
1
- import { r as CacheStore, t as CacheLogger } from "./interface-C1_WLTMK.mjs";
2
- import { i as RequestScope, n as Mandate } from "./types-DH0GupI7.mjs";
3
- import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-Csxga7d3.mjs";
1
+ import { r as CacheStore, t as CacheLogger } from "./interface-BGZCzOGL.mjs";
2
+ import { i as RequestScope, n as Mandate } from "./types-DjC-KUCz.mjs";
3
+ import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-Dmw-6Qeo.mjs";
4
4
  import { FastifyReply, FastifyRequest } from "fastify";
5
5
  //#region src/permissions/agent.d.ts
6
6
  /**
@@ -1,5 +1,59 @@
1
1
  import { FastifyPluginAsync } from "fastify";
2
- //#region src/integrations/jobs.d.ts
2
+ //#region src/integrations/jobs/execution.d.ts
3
+ /**
4
+ * Timed handler execution — the concurrency core of the jobs worker,
5
+ * extracted from the BullMQ registration so the algorithm is testable
6
+ * without queue mocks.
7
+ *
8
+ * Contract:
9
+ * - The handler receives an `AbortSignal`, aborted when `timeoutMs`
10
+ * elapses. The timeout rejects THIS call; the handler itself is only
11
+ * signalled — cooperative cancellation.
12
+ * - `releaseSlot` (the bulkhead semaphore) fires when the handler
13
+ * promise SETTLES, not when the timeout rejects — a timed-out handler
14
+ * that keeps running still occupies its concurrency slot, so retries
15
+ * can't stack live executions past the limit.
16
+ * - `cancelGraceMs` bounds that hold: a handler ignoring its signal past
17
+ * timeout + grace has its slot force-released (logged loudly — beyond
18
+ * this point the bulkhead can be exceeded by the abandoned execution).
19
+ * - A timed-out handler's eventual settle is logged for observability;
20
+ * its result is discarded.
21
+ * - `releaseSlot` is invoked exactly once, on every path (success,
22
+ * failure, timeout-then-settle, grace expiry). Synchronous throws from
23
+ * the handler are converted to rejections so release always fires.
24
+ */
25
+ interface TimedHandlerLogger {
26
+ warn(obj: Record<string, unknown>, msg: string): void;
27
+ }
28
+ interface ExecuteTimedHandlerOptions<T> {
29
+ /** The work. Must treat the signal as its cancellation channel. */
30
+ run: (signal: AbortSignal) => Promise<T>;
31
+ /** Job/queue label for timeout errors and logs. */
32
+ label: string;
33
+ /** Correlation id for logs (e.g. the BullMQ job id). */
34
+ jobId?: string;
35
+ /** Reject after this many ms and abort the signal. Omit = no timeout. */
36
+ timeoutMs?: number;
37
+ /** Slot hold-bound after timeout (default: 30_000). */
38
+ cancelGraceMs?: number;
39
+ /** Bulkhead slot release — called exactly once. Omit when unguarded. */
40
+ releaseSlot?: () => void;
41
+ logger?: TimedHandlerLogger;
42
+ }
43
+ declare function executeTimedHandler<T>(options: ExecuteTimedHandlerOptions<T>): Promise<T>;
44
+ //#endregion
45
+ //#region src/integrations/jobs/types.d.ts
46
+ /**
47
+ * Job queue integration — public contract (definitions, meta, dispatcher).
48
+ *
49
+ * Directory map (mechanical split of the former single file):
50
+ * types.ts — this contract + `defineJob`
51
+ * execution.ts — `executeTimedHandler` (timeout / cancellation / bulkhead)
52
+ * repeat.ts — repeatable-schedule reconciliation (boot hygiene)
53
+ * plugin.ts — BullMQ wiring (queues, workers, DLQ, routes, shutdown)
54
+ * index.ts — public barrel; `src/integrations/jobs.ts` re-exports it
55
+ * so the package subpath is unchanged
56
+ */
3
57
  /** Repeat schedule — cron pattern or fixed interval. Explicit timezone is required. */
4
58
  interface JobRepeatOptions {
5
59
  /** Cron pattern (e.g. '0 9 * * *' = every day 09:00). Mutually exclusive with `every`. */
@@ -44,13 +98,37 @@ interface JobDefinition<TData = unknown, TResult = unknown> {
44
98
  * and appear as `throttled: true` in `GET /jobs/:id/status`.
45
99
  * Distinct from BullMQ `concurrency` — use this when the constraint is a
46
100
  * downstream resource (AI model rate limit, DB connection pool, external API).
101
+ *
102
+ * The slot is held until the handler actually SETTLES — a timed-out
103
+ * handler that keeps running (ignoring `meta.signal`) still counts
104
+ * against `maxConcurrent`, so retries can't stack live executions past
105
+ * the limit. `cancelGraceMs` bounds how long a stuck handler may hold
106
+ * its slot after timeout.
47
107
  */
48
108
  maxConcurrent?: number;
109
+ /**
110
+ * How long after a TIMEOUT a still-running handler may keep its
111
+ * `maxConcurrent` slot before the slot is force-released (default:
112
+ * 30_000). The bulkhead limits LIVE handlers, not worker awaits — but a
113
+ * handler that ignores `meta.signal` forever must not wedge the queue.
114
+ * A force-release is logged loudly: past that point the bulkhead can be
115
+ * exceeded by the abandoned execution.
116
+ */
117
+ cancelGraceMs?: number;
49
118
  }
50
119
  interface JobMeta {
51
120
  jobId: string;
52
121
  attemptsMade: number;
53
122
  timestamp: number;
123
+ /**
124
+ * Aborted when the job's `timeout` elapses. Pass it to fetch/DB calls so
125
+ * timed-out work actually STOPS — the timeout alone only rejects the
126
+ * worker's await (releasing the concurrency slot) while the handler keeps
127
+ * running; a BullMQ retry can then overlap the original execution.
128
+ * Handlers that ignore the signal must be idempotent / compare-and-set
129
+ * safe under that overlap.
130
+ */
131
+ signal: AbortSignal;
54
132
  }
55
133
  interface JobDispatchOptions {
56
134
  /** Delay job execution by ms */
@@ -148,6 +226,8 @@ interface QueueStats {
148
226
  * });
149
227
  */
150
228
  declare function defineJob<TData = unknown, TResult = unknown>(definition: JobDefinition<TData, TResult>): JobDefinition<TData, TResult>;
229
+ //#endregion
230
+ //#region src/integrations/jobs/plugin.d.ts
151
231
  /**
152
232
  * Pluggable BullMQ job queue integration for Arc.
153
233
  *
@@ -156,4 +236,4 @@ declare function defineJob<TData = unknown, TResult = unknown>(definition: JobDe
156
236
  */
157
237
  declare const jobsPlugin: FastifyPluginAsync<JobsPluginOptions>;
158
238
  //#endregion
159
- export { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobRepeatOptions, JobStatus, JobsPluginOptions, QueueStats, defineJob, jobsPlugin };
239
+ export { JobMeta as a, JobsPluginOptions as c, ExecuteTimedHandlerOptions as d, TimedHandlerLogger as f, JobDispatcher as i, QueueStats as l, JobDefinition as n, JobRepeatOptions as o, executeTimedHandler as p, JobDispatchOptions as r, JobStatus as s, jobsPlugin as t, defineJob as u };
@@ -0,0 +1,107 @@
1
+ import { a as ModuleContribution, h as HealthCheck, n as ArcModuleInput, r as ArcModuleRegistry, t as ArcModule, u as ScheduleDefinition } from "./types-Cu6vVJmh.mjs";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/factory/module/contributions.d.ts
4
+ /**
5
+ * Collect every module's `healthChecks` in the caller's order. Fails at boot
6
+ * on a duplicate check `name` across modules, naming BOTH owning modules.
7
+ * The host's app-level checks are appended AFTER this (modules-first,
8
+ * host-last) by the caller.
9
+ */
10
+ declare function collectModuleHealthChecks(modules: readonly ArcModule[]): HealthCheck[];
11
+ /**
12
+ * Collect every module's `workflows`. Resolves factory contributions (so a
13
+ * factory can close over a container the caller has already decorated on
14
+ * `fastify`) and flattens to a single opaque array. Arc core deliberately
15
+ * does NOT validate or interpret the values — the streamline integration
16
+ * owns name/shape checks and registration. Called by the integration at init
17
+ * time, not by arc's boot pipeline (workflows depend on the integration's
18
+ * container).
19
+ */
20
+ declare function collectModuleWorkflows(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<unknown[]>;
21
+ /**
22
+ * Collect every module's `scheduledJobs`. Resolves factory contributions
23
+ * (after bootstraps) and fails at boot on a duplicate job `name` across
24
+ * modules. The caller passes the returned definitions to the canonical
25
+ * schedules plugin; factories must therefore be resolved only once.
26
+ */
27
+ declare function collectModuleScheduledJobs(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<ScheduleDefinition[]>;
28
+ //#endregion
29
+ //#region src/factory/module/lifecycle.d.ts
30
+ /**
31
+ * Subscribe every module's `eventHandlers` in dependency order (pass the
32
+ * `orderModules`-sorted list). Returns the ordered list of unsubscribe
33
+ * functions so the caller can tear them down at shutdown BEFORE module
34
+ * `onClose` (while module deps are still alive). Fails at boot on:
35
+ * - a duplicate NAMED handler across modules (attributing both owners);
36
+ * - a module declaring handlers while the event subsystem is unavailable.
37
+ * Resolves factory contributions (after bootstraps) so a handler can close over
38
+ * booted engines rather than a global getter.
39
+ */
40
+ declare function subscribeModuleEventHandlers(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<Array<() => void | Promise<void>>>;
41
+ //#endregion
42
+ //#region src/factory/module/order.d.ts
43
+ /**
44
+ * Order modules for composition by their `dependsOn` edges — a STABLE
45
+ * topological sort. Called once at the start of the bootstrap phase; every
46
+ * subsequent phase (bootstrap, resources, afterResources, and reverse-order
47
+ * onClose) iterates the returned list, so a module's declared dependencies are
48
+ * always composed before it.
49
+ *
50
+ * "Stable" = modules with no edge between them keep their original list order,
51
+ * so declaring `dependsOn` on one module never silently reorders an unrelated
52
+ * one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
53
+ * returned unchanged.
54
+ *
55
+ * Fail-fast — throws (never reorders past a broken contract) on:
56
+ * - duplicate module names (the name is the graph key)
57
+ * - a `dependsOn` name not present in the composed set
58
+ * - a self-reference (`dependsOn` includes the module's own name)
59
+ * - a dependency cycle (reports the concrete `a → b → … → a` path)
60
+ */
61
+ declare function orderModules(modules: readonly ArcModule[]): ArcModule[];
62
+ //#endregion
63
+ //#region src/factory/module/resolve.d.ts
64
+ /**
65
+ * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
66
+ * (this is where a dynamic `import()` fires); a promise (or plain module) is
67
+ * awaited. Called once per module at the start of the bootstrap phase.
68
+ */
69
+ declare function resolveModule(input: ArcModuleInput): Promise<ArcModule>;
70
+ /** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
71
+ declare function resolveContribution<T>(contribution: ModuleContribution<T> | undefined, fastify: FastifyInstance): Promise<readonly T[]>;
72
+ //#endregion
73
+ //#region src/factory/module/index.d.ts
74
+ /**
75
+ * Identity helper for authoring a typed module — mirrors `defineResource`.
76
+ * Gives inference + a single obvious construction site; does no work.
77
+ *
78
+ * `TExports` is inferred from the `bootstrap` return value, so a module
79
+ * author gets a typed public export for free:
80
+ *
81
+ * ```ts
82
+ * export const accountingModule = (deps: Deps) =>
83
+ * defineModule({
84
+ * name: "accounting",
85
+ * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
86
+ * });
87
+ * ```
88
+ */
89
+ declare function defineModule<TExports = unknown>(module: ArcModule<TExports>): ArcModule<TExports>;
90
+ /**
91
+ * Typed accessor for a module's public export (its `bootstrap` return value),
92
+ * recorded at `fastify.arc.modules[name]`.
93
+ *
94
+ * Throwing, in line with the fail-fast contract (and `requireOrgId`-style
95
+ * accessors): a missing entry means the module either isn't composed, is
96
+ * composed AFTER the caller (list order = init order), or returned nothing —
97
+ * all wiring bugs that must surface at boot, not as `undefined` downstream.
98
+ *
99
+ * The type parameter is an assertion, not a proof. Two ways to type it:
100
+ * - augment `ArcModuleRegistry` once — then the name alone infers the type:
101
+ * `getModuleExports(f, "accounting")` → `AccountingEngine`.
102
+ * - or pass it inline: `getModuleExports<AccountingEngine>(f, "accounting")`.
103
+ */
104
+ declare function getModuleExports<K extends keyof ArcModuleRegistry>(fastify: FastifyInstance, name: K): ArcModuleRegistry[K];
105
+ declare function getModuleExports<TExports = unknown>(fastify: FastifyInstance, name: string): TExports;
106
+ //#endregion
107
+ export { orderModules as a, collectModuleScheduledJobs as c, resolveModule as i, collectModuleWorkflows as l, getModuleExports as n, subscribeModuleEventHandlers as o, resolveContribution as r, collectModuleHealthChecks as s, defineModule as t };