@classytic/arc 2.21.0 → 2.22.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 (94) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-C6FlRWxF.mjs} +1 -1
  2. package/dist/audit/index.d.mts +2 -2
  3. package/dist/auth/index.d.mts +30 -2
  4. package/dist/auth/index.mjs +5 -2
  5. package/dist/cli/commands/describe.d.mts +1 -1
  6. package/dist/cli/commands/docs.mjs +1 -1
  7. package/dist/cli/commands/init.mjs +1 -1
  8. package/dist/cli/index.mjs +1 -1
  9. package/dist/core/index.d.mts +2 -2
  10. package/dist/core/index.mjs +2 -2
  11. package/dist/{core-DZ6Xe1jR.mjs → core-b6fgFgnd.mjs} +123 -66
  12. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-h1cEEaEX.mjs} +9 -5
  13. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
  14. package/dist/{createApp-Bi61XxqU.mjs → createApp-Ci7yG5gv.mjs} +242 -56
  15. package/dist/docs/index.d.mts +1 -1
  16. package/dist/docs/index.mjs +1 -1
  17. package/dist/encryption/index.mjs +9 -5
  18. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
  19. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  20. package/dist/factory/index.d.mts +29 -8
  21. package/dist/factory/index.mjs +34 -3
  22. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -2
  23. package/dist/fields-D3lhgzhD.d.mts +121 -0
  24. package/dist/health-ScyjIVo5.mjs +202 -0
  25. package/dist/hooks/index.d.mts +1 -1
  26. package/dist/idempotency/index.mjs +2 -0
  27. package/dist/{types-DpxoNX5v.d.mts → index-BEu4wjbL.d.mts} +187 -66
  28. package/dist/{index-B5N-NaHs.d.mts → index-D8MKYGDa.d.mts} +3 -3
  29. package/dist/{index-Dtrj6XZk.d.mts → index-DbtgCOCj.d.mts} +1287 -1225
  30. package/dist/{index-B5PoNbfu.d.mts → index-ogIHvOjo.d.mts} +1 -1
  31. package/dist/index.d.mts +6 -5
  32. package/dist/index.mjs +3 -3
  33. package/dist/{init-0YhHkXGk.mjs → init-DleK3p9f.mjs} +118 -2
  34. package/dist/integrations/index.d.mts +1 -1
  35. package/dist/integrations/mcp/index.d.mts +3 -3
  36. package/dist/integrations/mcp/index.mjs +1 -1
  37. package/dist/integrations/mcp/testing.d.mts +1 -1
  38. package/dist/integrations/mcp/testing.mjs +1 -1
  39. package/dist/integrations/webhooks.d.mts +8 -4
  40. package/dist/integrations/webhooks.mjs +9 -5
  41. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  42. package/dist/logger/index.d.mts +26 -1
  43. package/dist/logger/index.mjs +39 -1
  44. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-ByA2tf6D.mjs} +6 -2
  45. package/dist/middleware/index.d.mts +1 -1
  46. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  47. package/dist/{openapi-Kyod_di6.mjs → openapi-BRpMZD7C.mjs} +2 -2
  48. package/dist/permissions/index.d.mts +3 -2
  49. package/dist/pipeline/index.d.mts +1 -1
  50. package/dist/plugins/index.d.mts +22 -5
  51. package/dist/plugins/index.mjs +11 -280
  52. package/dist/plugins/response-cache.mjs +1 -0
  53. package/dist/plugins/tracing-entry.mjs +7 -4
  54. package/dist/presets/filesUpload.d.mts +2 -2
  55. package/dist/presets/index.d.mts +1 -1
  56. package/dist/presets/multiTenant.d.mts +1 -1
  57. package/dist/presets/search.d.mts +2 -2
  58. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  59. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-xHc42fwB.mjs} +37 -17
  60. package/dist/registry/index.d.mts +2 -2
  61. package/dist/requestId-5BhTVzBv.mjs +113 -0
  62. package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
  63. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
  64. package/dist/scim/index.mjs +6 -7
  65. package/dist/scope/index.mjs +2 -76
  66. package/dist/scope-k15RXfcq.mjs +64 -0
  67. package/dist/testing/index.d.mts +83 -3
  68. package/dist/testing/index.mjs +70 -3
  69. package/dist/types/index.d.mts +2 -2
  70. package/dist/{types-DRj-Ut4n.d.mts → types-Bc42CUyk.d.mts} +1 -1
  71. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  72. package/dist/usage/index.d.mts +142 -0
  73. package/dist/usage/index.mjs +0 -0
  74. package/dist/utils/index.d.mts +40 -18
  75. package/dist/utils/index.mjs +2 -2
  76. package/dist/{utils-CO8a3B_1.mjs → utils-FhJ88TJE.mjs} +59 -13
  77. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-DLwklPlG.d.mts} +0 -5
  78. package/llms.txt +39 -0
  79. package/package.json +453 -447
  80. package/skills/arc/SKILL.md +14 -8
  81. package/skills/arc/references/api-reference.md +34 -0
  82. package/skills/arc/references/auth.md +21 -4
  83. package/skills/arc/references/mcp.md +1 -1
  84. package/skills/arc/references/production.md +44 -2
  85. package/skills/arc-code-review/SKILL.md +1 -1
  86. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  87. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  88. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  89. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  90. package/skills/arc-module-publishing/SKILL.md +2 -1
  91. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  92. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  93. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  94. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -1,7 +1,7 @@
1
- import { Q as MiddlewareConfig, ft as RouteDefinition, ht as RouteSchemaOptions, nt as PresetHook, qt as AnyRecord } from "../index-Dtrj6XZk.mjs";
2
- import { i as errorHandlerPlugin, n as ErrorMapper, r as defaultIsDuplicateKeyError, t as ErrorHandlerOptions } from "../errorHandler-B0e-bq-O.mjs";
3
- import { a as arcCorePlugin, i as _default, n as ArcCorePluginOptions, r as PluginMeta, t as ArcCore } from "../fastify-augmentation-DafigGQp.mjs";
4
- import { _ as HealthOptions, a as MetricsCollector, c as metricsPlugin, d as ssePlugin, f as CachingOptions, g as HealthCheck, h as cachingPlugin, i as MetricEntry, l as SSEOptions, m as _default$1, n as _default$8, o as MetricsOptions, p as CachingRule, r as versioningPlugin, s as _default$4, t as VersioningOptions, u as _default$7, v as _default$3, y as healthPlugin } from "../versioning-DQ4mCvg0.mjs";
1
+ import { At as RouteDefinition, J as PresetHook, Q as MiddlewareConfig, et as RouteSchemaOptions, qt as AnyRecord } from "../index-DbtgCOCj.mjs";
2
+ import { i as errorHandlerPlugin, n as ErrorMapper, r as defaultIsDuplicateKeyError, t as ErrorHandlerOptions } from "../errorHandler-C6yv4ZTW.mjs";
3
+ import { a as arcCorePlugin, i as _default, n as ArcCorePluginOptions, r as PluginMeta, t as ArcCore } from "../fastify-augmentation-9_fuetB4.mjs";
4
+ import { _ as HealthOptions, a as MetricsCollector, c as metricsPlugin, d as ssePlugin, f as CachingOptions, g as HealthCheck, h as cachingPlugin, i as MetricEntry, l as SSEOptions, m as _default$1, n as _default$8, o as MetricsOptions, p as CachingRule, r as versioningPlugin, s as _default$4, t as VersioningOptions, u as _default$7, v as _default$3, y as healthPlugin } from "../versioning-DLwklPlG.mjs";
5
5
  import { t as TracingOptions } from "../tracing-CsGKQJGg.mjs";
6
6
  import { PaginatedResult } from "@classytic/repo-core/pagination";
7
7
  import { FastifyInstance, FastifyPluginAsync } from "fastify";
@@ -145,6 +145,23 @@ declare module "fastify" {
145
145
  };
146
146
  }
147
147
  }
148
+ /**
149
+ * Sanitize an incoming request-id header value: max 128 chars, alphanumeric
150
+ * plus dashes/underscores/dots/colons only. Returns `undefined` for anything
151
+ * else (crafted values could pollute logs or response headers). Array values
152
+ * (repeated headers) are rejected outright — a client sending multiple
153
+ * request-id headers is not tracing in good faith.
154
+ */
155
+ declare function sanitizeRequestId(value: unknown): string | undefined;
156
+ /**
157
+ * Build a Fastify `genReqId` function: adopt a sanitized incoming request-id
158
+ * header, or generate one. Wire this into the `Fastify()` constructor so
159
+ * `request.id`, `request.log`'s `reqId` binding, and the echoed response
160
+ * header all agree — `createApp` does this automatically.
161
+ */
162
+ declare function createRequestIdGenerator(opts?: Pick<RequestIdOptions, "header" | "generator">): (req: {
163
+ headers: Record<string, string | string[] | undefined>;
164
+ }) => string;
148
165
  declare const requestIdPlugin: FastifyPluginAsync<RequestIdOptions>;
149
166
  declare const _default$5: FastifyPluginAsync<RequestIdOptions>;
150
167
  //#endregion
@@ -221,4 +238,4 @@ declare module "fastify" {
221
238
  declare const schedulesPlugin: FastifyPluginAsync<SchedulesPluginOptions>;
222
239
  declare const _default$6: FastifyPluginAsync<SchedulesPluginOptions>;
223
240
  //#endregion
224
- export { type ArcCore, type ArcCorePluginOptions, type ArcPlugin, type CachingOptions, type CachingRule, type CreatePluginDefinition, type ErrorHandlerOptions, type ErrorMapper, type GracefulShutdownOptions, type HealthCheck, type HealthOptions, type MetricEntry, type MetricsCollector, type MetricsOptions, type PluginMeta, type PluginResourceResult, type RequestIdOptions, type SSEOptions, type ScheduleDefinition, type ScheduleLockLike, type ScheduleStats, type SchedulesPluginOptions, type TracingOptions, type VersioningOptions, _default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, _default$1 as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, defaultIsDuplicateKeyError, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, _default$2 as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, _default$3 as healthPlugin, healthPlugin as healthPluginFn, _default$4 as metricsPlugin, metricsPlugin as metricsPluginFn, replyHelpersPlugin, _default$5 as requestIdPlugin, requestIdPlugin as requestIdPluginFn, _default$6 as schedulesPlugin, schedulesPlugin as schedulesPluginFn, _default$7 as ssePlugin, ssePlugin as ssePluginFn, _default$8 as versioningPlugin, versioningPlugin as versioningPluginFn };
241
+ export { type ArcCore, type ArcCorePluginOptions, type ArcPlugin, type CachingOptions, type CachingRule, type CreatePluginDefinition, type ErrorHandlerOptions, type ErrorMapper, type GracefulShutdownOptions, type HealthCheck, type HealthOptions, type MetricEntry, type MetricsCollector, type MetricsOptions, type PluginMeta, type PluginResourceResult, type RequestIdOptions, type SSEOptions, type ScheduleDefinition, type ScheduleLockLike, type ScheduleStats, type SchedulesPluginOptions, type TracingOptions, type VersioningOptions, _default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, _default$1 as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, createRequestIdGenerator, defaultIsDuplicateKeyError, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, _default$2 as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, _default$3 as healthPlugin, healthPlugin as healthPluginFn, _default$4 as metricsPlugin, metricsPlugin as metricsPluginFn, replyHelpersPlugin, _default$5 as requestIdPlugin, requestIdPlugin as requestIdPluginFn, sanitizeRequestId, _default$6 as schedulesPlugin, schedulesPlugin as schedulesPluginFn, _default$7 as ssePlugin, ssePlugin as ssePluginFn, _default$8 as versioningPlugin, versioningPlugin as versioningPluginFn };
@@ -4,13 +4,14 @@ import { n as requestContext } from "../requestContext-BZKOr13H.mjs";
4
4
  import { t as hasEvents } from "../typeGuards-C9KXUly6.mjs";
5
5
  import { t as HookSystem } from "../HookSystem-C1vB_Gc9.mjs";
6
6
  import { t as ResourceRegistry } from "../ResourceRegistry-7RnrEWNP.mjs";
7
+ import { i as sanitizeRequestId, n as requestIdPlugin, r as requestId_default, t as createRequestIdGenerator } from "../requestId-5BhTVzBv.mjs";
8
+ import { n as health_default, t as healthPlugin } from "../health-ScyjIVo5.mjs";
7
9
  import { n as caching_default, t as cachingPlugin } from "../caching-_NJeDd1_.mjs";
8
- import { n as errorHandlerPlugin, t as defaultIsDuplicateKeyError } from "../errorHandler-Dxf6P_kV.mjs";
10
+ import { n as errorHandlerPlugin, t as defaultIsDuplicateKeyError } from "../errorHandler-BTzXSwIg.mjs";
9
11
  import { n as metrics_default, t as metricsPlugin } from "../metrics-fdxI5mn_.mjs";
10
12
  import { t as replyHelpersPlugin } from "../replyHelpers-67tRawQZ.mjs";
11
13
  import { n as sse_default, t as ssePlugin } from "../sse-CfUjwjDk.mjs";
12
14
  import { n as versioning_default, t as versioningPlugin } from "../versioning-Dk-71iIz.mjs";
13
- import { randomUUID } from "node:crypto";
14
15
  import fp from "fastify-plugin";
15
16
  //#region src/core/arcCorePlugin.ts
16
17
  const arcCorePlugin = async (fastify, opts = {}) => {
@@ -24,6 +25,13 @@ const arcCorePlugin = async (fastify, opts = {}) => {
24
25
  externalOpenApiPaths: [],
25
26
  plugins: /* @__PURE__ */ new Map()
26
27
  });
28
+ for (const field of [
29
+ "_policyFilters",
30
+ "_ownershipCheck",
31
+ "fieldMask",
32
+ "policyMetadata",
33
+ "document"
34
+ ]) if (!fastify.hasRequestDecorator(field)) fastify.decorateRequest(field, void 0);
27
35
  fastify.addHook("onRequest", (request, _reply, done) => {
28
36
  const store = {
29
37
  requestId: request.id,
@@ -192,283 +200,6 @@ var gracefulShutdown_default = fp(gracefulShutdownPlugin, {
192
200
  fastify: "5.x"
193
201
  });
194
202
  //#endregion
195
- //#region src/plugins/health.ts
196
- function createHttpMetrics() {
197
- return {
198
- requestsTotal: {},
199
- requestDurations: [],
200
- _ringIndex: 0,
201
- startTime: Date.now()
202
- };
203
- }
204
- const healthPlugin = async (fastify, opts = {}) => {
205
- const { prefix = "/_health", checks = [], metrics = false, metricsCollector, version, collectHttpMetrics = metrics } = opts;
206
- const httpMetrics = createHttpMetrics();
207
- fastify.get(prefix, { schema: {
208
- tags: ["Health"],
209
- summary: "Liveness probe (alias for /live)",
210
- description: "Returns 200 if the process is alive",
211
- response: { 200: {
212
- type: "object",
213
- properties: {
214
- status: {
215
- type: "string",
216
- enum: ["ok"]
217
- },
218
- timestamp: { type: "string" },
219
- version: { type: "string" }
220
- }
221
- } }
222
- } }, async () => ({
223
- status: "ok",
224
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
225
- ...version ? { version } : {}
226
- }));
227
- fastify.get(`${prefix}/live`, { schema: {
228
- tags: ["Health"],
229
- summary: "Liveness probe",
230
- description: "Returns 200 if the process is alive",
231
- response: { 200: {
232
- type: "object",
233
- properties: {
234
- status: {
235
- type: "string",
236
- enum: ["ok"]
237
- },
238
- timestamp: { type: "string" },
239
- version: { type: "string" }
240
- }
241
- } }
242
- } }, async () => {
243
- return {
244
- status: "ok",
245
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
246
- ...version ? { version } : {}
247
- };
248
- });
249
- fastify.get(`${prefix}/ready`, { schema: {
250
- tags: ["Health"],
251
- summary: "Readiness probe",
252
- description: "Returns 200 if all dependencies are healthy",
253
- response: {
254
- 200: {
255
- type: "object",
256
- properties: {
257
- status: {
258
- type: "string",
259
- enum: ["ready", "not_ready"]
260
- },
261
- timestamp: { type: "string" },
262
- checks: {
263
- type: "array",
264
- items: {
265
- type: "object",
266
- properties: {
267
- name: { type: "string" },
268
- healthy: { type: "boolean" },
269
- duration: { type: "number" },
270
- error: { type: "string" }
271
- }
272
- }
273
- }
274
- }
275
- },
276
- 503: {
277
- type: "object",
278
- properties: {
279
- status: {
280
- type: "string",
281
- enum: ["not_ready"]
282
- },
283
- timestamp: { type: "string" },
284
- checks: { type: "array" }
285
- }
286
- }
287
- }
288
- } }, async (_, reply) => {
289
- const results = await runChecks(checks);
290
- const criticalFailed = results.some((r) => !r.healthy && (checks.find((c) => c.name === r.name)?.critical ?? true));
291
- const response = {
292
- status: criticalFailed ? "not_ready" : "ready",
293
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
294
- checks: results
295
- };
296
- if (criticalFailed) reply.code(503);
297
- return response;
298
- });
299
- if (metrics) fastify.get(`${prefix}/metrics`, async (_, reply) => {
300
- reply.type("text/plain; charset=utf-8");
301
- if (metricsCollector) return await metricsCollector();
302
- const uptime = process.uptime();
303
- const memory = process.memoryUsage();
304
- const cpu = process.cpuUsage();
305
- const lines = [
306
- "# HELP process_uptime_seconds Process uptime in seconds",
307
- "# TYPE process_uptime_seconds gauge",
308
- `process_uptime_seconds ${uptime.toFixed(2)}`,
309
- "",
310
- "# HELP process_memory_heap_bytes Heap memory usage in bytes",
311
- "# TYPE process_memory_heap_bytes gauge",
312
- `process_memory_heap_bytes{type="used"} ${memory.heapUsed}`,
313
- `process_memory_heap_bytes{type="total"} ${memory.heapTotal}`,
314
- "",
315
- "# HELP process_memory_rss_bytes RSS memory in bytes",
316
- "# TYPE process_memory_rss_bytes gauge",
317
- `process_memory_rss_bytes ${memory.rss}`,
318
- "",
319
- "# HELP process_memory_external_bytes External memory in bytes",
320
- "# TYPE process_memory_external_bytes gauge",
321
- `process_memory_external_bytes ${memory.external}`,
322
- "",
323
- "# HELP process_cpu_user_microseconds User CPU time in microseconds",
324
- "# TYPE process_cpu_user_microseconds counter",
325
- `process_cpu_user_microseconds ${cpu.user}`,
326
- "",
327
- "# HELP process_cpu_system_microseconds System CPU time in microseconds",
328
- "# TYPE process_cpu_system_microseconds counter",
329
- `process_cpu_system_microseconds ${cpu.system}`,
330
- ""
331
- ];
332
- if (collectHttpMetrics && Object.keys(httpMetrics.requestsTotal).length > 0) {
333
- lines.push("# HELP http_requests_total Total HTTP requests by status code", "# TYPE http_requests_total counter");
334
- for (const [status, count] of Object.entries(httpMetrics.requestsTotal)) lines.push(`http_requests_total{status="${status}"} ${count}`);
335
- lines.push("");
336
- if (httpMetrics.requestDurations.length > 0) {
337
- const sorted = [...httpMetrics.requestDurations].sort((a, b) => a - b);
338
- const p50 = sorted[Math.floor(sorted.length * .5)] || 0;
339
- const p95 = sorted[Math.floor(sorted.length * .95)] || 0;
340
- const p99 = sorted[Math.floor(sorted.length * .99)] || 0;
341
- const sum = sorted.reduce((a, b) => a + b, 0);
342
- lines.push("# HELP http_request_duration_milliseconds HTTP request duration", "# 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 ${sum.toFixed(2)}`, `http_request_duration_milliseconds_count ${sorted.length}`, "");
343
- }
344
- }
345
- return lines.join("\n");
346
- });
347
- if (collectHttpMetrics) {
348
- fastify.addHook("onRequest", async (request) => {
349
- request._startTime = Date.now();
350
- });
351
- fastify.addHook("onResponse", async (request, reply) => {
352
- const duration = Date.now() - (request._startTime ?? Date.now());
353
- const statusBucket = `${Math.floor(reply.statusCode / 100)}xx`;
354
- httpMetrics.requestsTotal[statusBucket] = (httpMetrics.requestsTotal[statusBucket] || 0) + 1;
355
- if (httpMetrics.requestDurations.length < 1e4) httpMetrics.requestDurations.push(duration);
356
- else httpMetrics.requestDurations[httpMetrics._ringIndex % 1e4] = duration;
357
- httpMetrics._ringIndex = httpMetrics._ringIndex + 1;
358
- });
359
- }
360
- fastify.log?.debug?.(`Health plugin registered at ${prefix} (alias), ${prefix}/live, ${prefix}/ready${metrics ? `, ${prefix}/metrics` : ""}`);
361
- };
362
- /**
363
- * Run all health checks with timeout
364
- */
365
- async function runChecks(checks) {
366
- const results = [];
367
- for (const check of checks) {
368
- const start = Date.now();
369
- const timeout = check.timeout ?? 5e3;
370
- let timer;
371
- try {
372
- const checkPromise = Promise.resolve(check.check());
373
- const timeoutPromise = new Promise((_, reject) => {
374
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Health check timeout")), timeout);
375
- });
376
- const healthy = await Promise.race([checkPromise, timeoutPromise]);
377
- results.push({
378
- name: check.name,
379
- healthy: Boolean(healthy),
380
- duration: Date.now() - start
381
- });
382
- } catch (err) {
383
- results.push({
384
- name: check.name,
385
- healthy: false,
386
- duration: Date.now() - start,
387
- error: err.message
388
- });
389
- } finally {
390
- if (timer) clearTimeout(timer);
391
- }
392
- }
393
- return results;
394
- }
395
- var health_default = fp(healthPlugin, {
396
- name: "arc-health",
397
- fastify: "5.x"
398
- });
399
- //#endregion
400
- //#region src/plugins/requestId.ts
401
- /**
402
- * Request ID Plugin
403
- *
404
- * Propagates request IDs for distributed tracing.
405
- * - Accepts incoming x-request-id header
406
- * - Generates UUID if not provided
407
- * - Attaches to request.id and response header
408
- *
409
- * @example
410
- * import { requestIdPlugin } from '@classytic/arc';
411
- *
412
- * await fastify.register(requestIdPlugin);
413
- *
414
- * // In handlers, access via request.id
415
- * fastify.get('/', async (request) => {
416
- * console.log(request.id); // UUID
417
- * });
418
- */
419
- /** W3C traceparent format: `version-traceid-parentid-flags` */
420
- const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
421
- /** W3C spec: version ff is reserved and invalid; trace/parent IDs must not be all zeros */
422
- const TRACEID_ZEROS = "0".repeat(32);
423
- const PARENTID_ZEROS = "0".repeat(16);
424
- /** tracestate: printable ASCII, <= 512 bytes */
425
- const TRACESTATE_MAX = 512;
426
- function isValidTraceparent(tp) {
427
- const m = TRACEPARENT_RE.exec(tp);
428
- if (!m) return false;
429
- const version = m[1] ?? "";
430
- const traceId = m[2] ?? "";
431
- const parentId = m[3] ?? "";
432
- if (version.toLowerCase() === "ff") return false;
433
- if (traceId.toLowerCase() === TRACEID_ZEROS) return false;
434
- if (parentId.toLowerCase() === PARENTID_ZEROS) return false;
435
- return true;
436
- }
437
- const requestIdPlugin = async (fastify, opts = {}) => {
438
- const { header = "x-request-id", generator = randomUUID, setResponseHeader = true, propagateTraceContext = true } = opts;
439
- if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
440
- if (!fastify.hasRequestDecorator("traceContext")) fastify.decorateRequest("traceContext", void 0);
441
- fastify.addHook("onRequest", async (request, reply) => {
442
- const incomingId = request.headers[header];
443
- const sanitized = typeof incomingId === "string" ? incomingId.trim() : "";
444
- const requestId = sanitized.length > 0 && sanitized.length <= 128 && /^[\w.:-]+$/.test(sanitized) ? sanitized : generator();
445
- request.id = requestId;
446
- request.requestId = requestId;
447
- if (setResponseHeader) reply.header(header, requestId);
448
- if (propagateTraceContext) {
449
- const rawTraceparent = request.headers["traceparent"];
450
- const rawTracestate = request.headers["tracestate"];
451
- if (typeof rawTraceparent === "string") {
452
- const tp = rawTraceparent.trim();
453
- if (isValidTraceparent(tp)) {
454
- const ts = typeof rawTracestate === "string" && rawTracestate.length > 0 && rawTracestate.length <= TRACESTATE_MAX ? rawTracestate : void 0;
455
- request.traceContext = {
456
- traceparent: tp,
457
- ...ts ? { tracestate: ts } : {}
458
- };
459
- reply.header("traceparent", tp);
460
- if (ts) reply.header("tracestate", ts);
461
- }
462
- }
463
- }
464
- });
465
- fastify.log?.debug?.("Request ID plugin registered");
466
- };
467
- var requestId_default = fp(requestIdPlugin, {
468
- name: "arc-request-id",
469
- fastify: "5.x"
470
- });
471
- //#endregion
472
203
  //#region src/plugins/schedules.ts
473
204
  const MIN_SAFE_INTERVAL_MS = 1e3;
474
205
  const MAX_DEFAULT_LEASE_MS = 3e5;
@@ -556,4 +287,4 @@ var schedules_default = fp(schedulesPlugin, {
556
287
  fastify: "5.x"
557
288
  });
558
289
  //#endregion
559
- export { arcCorePlugin_default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, caching_default as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, defaultIsDuplicateKeyError, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, gracefulShutdown_default as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, health_default as healthPlugin, healthPlugin as healthPluginFn, metrics_default as metricsPlugin, metricsPlugin as metricsPluginFn, replyHelpersPlugin, requestId_default as requestIdPlugin, requestIdPlugin as requestIdPluginFn, schedules_default as schedulesPlugin, schedulesPlugin as schedulesPluginFn, sse_default as ssePlugin, ssePlugin as ssePluginFn, versioning_default as versioningPlugin, versioningPlugin as versioningPluginFn };
290
+ export { arcCorePlugin_default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, caching_default as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, createRequestIdGenerator, defaultIsDuplicateKeyError, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, gracefulShutdown_default as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, health_default as healthPlugin, healthPlugin as healthPluginFn, metrics_default as metricsPlugin, metricsPlugin as metricsPluginFn, replyHelpersPlugin, requestId_default as requestIdPlugin, requestIdPlugin as requestIdPluginFn, sanitizeRequestId, schedules_default as schedulesPlugin, schedulesPlugin as schedulesPluginFn, sse_default as ssePlugin, ssePlugin as ssePluginFn, versioning_default as versioningPlugin, versioningPlugin as versioningPluginFn };
@@ -94,6 +94,7 @@ const responseCachePluginImpl = async (fastify, opts = {}) => {
94
94
  ], xCacheHeader = true, statsPath = null, keyFn } = opts;
95
95
  const cache = new LRUCache(maxEntries);
96
96
  const invalidateMethods = new Set(invalidateOn.map((m) => m.toUpperCase()));
97
+ if (!fastify.hasRequestDecorator("__arcCacheTTL")) fastify.decorateRequest("__arcCacheTTL", void 0);
97
98
  /** Find TTL for a given URL path (seconds) */
98
99
  function getTTL(url) {
99
100
  const path = url.split("?")[0] ?? url;
@@ -58,7 +58,7 @@ try {
58
58
  function createTracerProvider(options) {
59
59
  if (!isAvailable || !NodeTracerProvider || !BatchSpanProcessor || !OTLPTraceExporter) return null;
60
60
  const { serviceName = "@classytic/arc", serviceVersion, exporterUrl = "http://localhost:4318/v1/traces" } = options;
61
- const resolvedVersion = serviceVersion ?? "2.21.0";
61
+ const resolvedVersion = serviceVersion ?? "2.22.0";
62
62
  const exporter = new OTLPTraceExporter({ url: exporterUrl });
63
63
  const provider = new NodeTracerProvider({ resource: { attributes: {
64
64
  "service.name": serviceName,
@@ -92,8 +92,11 @@ async function tracingPlugin(fastify, options = {}) {
92
92
  }
93
93
  const tracer = otelTrace.getTracer(serviceName);
94
94
  fastify.decorateRequest("tracer", void 0);
95
- fastify.addHook("onRequest", async (request, _reply) => {
96
- if (Math.random() > sampleRate) return;
95
+ fastify.addHook("onRequest", (request, _reply, done) => {
96
+ if (Math.random() > sampleRate) {
97
+ done();
98
+ return;
99
+ }
97
100
  const span = tracer.startSpan(`HTTP ${request.method} ${request.url}`, {
98
101
  kind: 1,
99
102
  attributes: {
@@ -109,7 +112,7 @@ async function tracingPlugin(fastify, options = {}) {
109
112
  tracer,
110
113
  currentSpan: span
111
114
  };
112
- otelContext.with(otelTrace.setSpan(otelContext.active(), span), () => {});
115
+ otelContext.with(otelTrace.setSpan(otelContext.active(), span), done);
113
116
  });
114
117
  fastify.addHook("onResponse", async (request, reply) => {
115
118
  if (!request.tracer?.currentSpan) return;
@@ -1,6 +1,6 @@
1
- import { rt as PresetResult } from "../index-Dtrj6XZk.mjs";
1
+ import { Y as PresetResult } from "../index-DbtgCOCj.mjs";
2
2
  import { i as RequestScope } from "../types-DH0GupI7.mjs";
3
- import { c as PermissionCheck } from "../fields-BGqrAa_u.mjs";
3
+ import { t as PermissionCheck } from "../types-Csxga7d3.mjs";
4
4
  import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-DLUZ04kK.mjs";
5
5
  //#region src/presets/filesUpload.d.ts
6
6
  interface FilesUploadPresetRoutes {
@@ -1,4 +1,4 @@
1
- import { bt as IControllerResponse, d as PaginationResult, qt as AnyRecord, rt as PresetResult, st as ResourceConfig, xt as IRequestContext } from "../index-Dtrj6XZk.mjs";
1
+ import { Rt as IControllerResponse, U as ResourceConfig, Y as PresetResult, d as PaginationResult, qt as AnyRecord, zt as IRequestContext } from "../index-DbtgCOCj.mjs";
2
2
  import { FilesUploadPresetOptions, FilesUploadPresetPermissions, FilesUploadPresetRoutes, filesUploadPreset } from "./filesUpload.mjs";
3
3
  import { MultiTenantOptions, TenantFieldSpec, multiTenantPreset } from "./multiTenant.mjs";
4
4
  import { SearchHandler, SearchPresetOptions, SearchRouteConfig, searchPreset } from "./search.mjs";
@@ -1,4 +1,4 @@
1
- import { J as CrudRouteKey, rt as PresetResult } from "../index-Dtrj6XZk.mjs";
1
+ import { X as CrudRouteKey, Y as PresetResult } from "../index-DbtgCOCj.mjs";
2
2
  //#region src/presets/multiTenant.d.ts
3
3
  /**
4
4
  * One tenant dimension for multi-field filtering. Discriminated by source:
@@ -1,5 +1,5 @@
1
- import { ft as RouteDefinition, gt as ControllerHandler, pt as RouteMcpConfig, rt as PresetResult } from "../index-Dtrj6XZk.mjs";
2
- import { c as PermissionCheck } from "../fields-BGqrAa_u.mjs";
1
+ import { At as RouteDefinition, Pt as ControllerHandler, Y as PresetResult, jt as RouteMcpConfig } from "../index-DbtgCOCj.mjs";
2
+ import { t as PermissionCheck } from "../types-Csxga7d3.mjs";
3
3
  //#region src/presets/search.d.ts
4
4
  /**
5
5
  * Search Preset — backend-agnostic search / vector / embed routes
@@ -0,0 +1,14 @@
1
+ //#region src/scope/rateLimitKey.ts
2
+ function createTenantKeyGenerator(opts) {
3
+ if (opts?.strategy) return opts.strategy;
4
+ return (ctx) => {
5
+ const scope = ctx.scope;
6
+ if (!scope || scope.kind === "public") return ctx.ip;
7
+ if (scope.kind === "member") return scope.organizationId;
8
+ if (scope.kind === "service") return scope.organizationId;
9
+ if (scope.kind === "elevated") return scope.organizationId ?? scope.userId ?? ctx.ip;
10
+ return scope.userId ?? ctx.ip;
11
+ };
12
+ }
13
+ //#endregion
14
+ export { createTenantKeyGenerator as t };
@@ -81,24 +81,44 @@ function buildAuthMiddlewareForPermissions(fastify, permissions) {
81
81
  //#endregion
82
82
  //#region src/core/middlewares/chain.ts
83
83
  /**
84
- * Compose preHandler[] in the canonical order. Every null/undefined entry is
85
- * dropped. Keeps CRUD and Action routers from accidentally ordering the same
86
- * ingredients differently (regression risk: cache before auth → user-scoped
87
- * cache keys leak across users).
84
+ * Compose the route-level `onRequest` + `preHandler` arrays in the canonical
85
+ * order. Every null/undefined entry is dropped. Keeps the three routers from
86
+ * accidentally ordering the same ingredients differently (regression risk:
87
+ * cache before auth → user-scoped cache keys leak across users; auth after
88
+ * body parse → anonymous schema probing).
88
89
  *
89
- * Canonical order:
90
- * preAuth → arcDecorator → authMw → permissionMw → pluginMw → routeGuards → customMws
90
+ * onRequest constraints (Fastify lifecycle): `request.body` is undefined —
91
+ * everything placed there must be header/cookie/param/query-driven. arc's
92
+ * `authenticate` / `optionalAuthenticate` qualify by contract (Bearer
93
+ * header, session cookie, api-key header); `preAuth` handlers exist
94
+ * precisely to promote query tokens into headers (SSE `?token=`); the arc
95
+ * decorator stamps static route metadata.
91
96
  */
92
- function buildPreHandlerChain(parts) {
93
- return [
94
- ...parts.preAuth ?? [],
95
- parts.arcDecorator,
96
- parts.authMw ?? null,
97
- parts.permissionMw ?? null,
98
- parts.pluginMw ?? null,
99
- ...parts.routeGuards ?? [],
100
- ...parts.customMws ?? []
101
- ].filter(Boolean);
97
+ function buildRouteHooks(parts) {
98
+ return {
99
+ onRequest: [
100
+ ...parts.preAuth ?? [],
101
+ parts.arcDecorator,
102
+ parts.authMw ?? null
103
+ ].filter(Boolean),
104
+ preHandler: [
105
+ parts.permissionMw ?? null,
106
+ parts.pluginMw ?? null,
107
+ ...parts.routeGuards ?? [],
108
+ ...parts.customMws ?? []
109
+ ].filter(Boolean)
110
+ };
111
+ }
112
+ /**
113
+ * Spread helper — turns `RouteHookChains` into the two Fastify route-option
114
+ * fields, omitting empty arrays so routes without middleware stay
115
+ * option-free (Fastify skips absent hooks faster than empty arrays).
116
+ */
117
+ function routeHookOptions(hooks) {
118
+ return {
119
+ ...hooks.onRequest.length > 0 ? { onRequest: hooks.onRequest } : {},
120
+ ...hooks.preHandler.length > 0 ? { preHandler: hooks.preHandler } : {}
121
+ };
102
122
  }
103
123
  /**
104
124
  * `RouteDefinition.preHandler` accepts two shapes:
@@ -329,4 +349,4 @@ function tryRegisterRoute(fastify, opts, context) {
329
349
  }
330
350
  }
331
351
  //#endregion
332
- export { selectPluginMw as a, buildPreHandlerChain as c, buildAuthMiddlewareForPermissions as d, buildArcDecorator as f, resolveRouterPluginMw as i, resolveRoutePreHandlers as l, buildRateLimitConfig as n, buildActionPermissionMw as o, buildRouteConfig as r, buildCrudPermissionMw as s, tryRegisterRoute as t, buildAuthMiddleware as u };
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 };
@@ -1,5 +1,5 @@
1
- import { J as CrudRouteKey, R as RegisterOptions, V as ResourceDefinition, at as ResolvedTenantPurge, k as IntrospectionPluginOptions, p as RegistryEntry, z as ResourceRegistry } from "../index-Dtrj6XZk.mjs";
2
- import "../fastify-augmentation-DafigGQp.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-DbtgCOCj.mjs";
2
+ import "../fastify-augmentation-9_fuetB4.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,113 @@
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
+ export { sanitizeRequestId as i, requestIdPlugin as n, requestId_default as r, createRequestIdGenerator as t };