@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.
- package/dist/{BaseController-BnLYgE9A.mjs → BaseController-C6FlRWxF.mjs} +1 -1
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +30 -2
- package/dist/auth/index.mjs +5 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{core-DZ6Xe1jR.mjs → core-b6fgFgnd.mjs} +123 -66
- package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-h1cEEaEX.mjs} +9 -5
- package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
- package/dist/{createApp-Bi61XxqU.mjs → createApp-Ci7yG5gv.mjs} +242 -56
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
- package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
- package/dist/factory/index.d.mts +29 -8
- package/dist/factory/index.mjs +34 -3
- package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -2
- package/dist/fields-D3lhgzhD.d.mts +121 -0
- package/dist/health-ScyjIVo5.mjs +202 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +2 -0
- package/dist/{types-DpxoNX5v.d.mts → index-BEu4wjbL.d.mts} +187 -66
- package/dist/{index-B5N-NaHs.d.mts → index-D8MKYGDa.d.mts} +3 -3
- package/dist/{index-Dtrj6XZk.d.mts → index-DbtgCOCj.d.mts} +1287 -1225
- package/dist/{index-B5PoNbfu.d.mts → index-ogIHvOjo.d.mts} +1 -1
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +3 -3
- package/dist/{init-0YhHkXGk.mjs → init-DleK3p9f.mjs} +118 -2
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +3 -3
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +8 -4
- package/dist/integrations/webhooks.mjs +9 -5
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-ByA2tf6D.mjs} +6 -2
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
- package/dist/{openapi-Kyod_di6.mjs → openapi-BRpMZD7C.mjs} +2 -2
- package/dist/permissions/index.d.mts +3 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +22 -5
- package/dist/plugins/index.mjs +11 -280
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-xHc42fwB.mjs} +37 -17
- package/dist/registry/index.d.mts +2 -2
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-k15RXfcq.mjs +64 -0
- package/dist/testing/index.d.mts +83 -3
- package/dist/testing/index.mjs +70 -3
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-DRj-Ut4n.d.mts → types-Bc42CUyk.d.mts} +1 -1
- package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +40 -18
- package/dist/utils/index.mjs +2 -2
- package/dist/{utils-CO8a3B_1.mjs → utils-FhJ88TJE.mjs} +59 -13
- package/dist/{versioning-DQ4mCvg0.d.mts → versioning-DLwklPlG.d.mts} +0 -5
- package/llms.txt +39 -0
- package/package.json +453 -447
- package/skills/arc/SKILL.md +14 -8
- package/skills/arc/references/api-reference.md +34 -0
- package/skills/arc/references/auth.md +21 -4
- package/skills/arc/references/mcp.md +1 -1
- package/skills/arc/references/production.md +44 -2
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/anti-patterns.md +71 -18
- package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
- package/skills/arc-code-review/references/migration-recipes.md +7 -7
- package/skills/arc-code-review/references/mongokit-migration.md +4 -3
- package/skills/arc-module-publishing/SKILL.md +2 -1
- package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
- package/skills/arc-module-publishing/references/package-manifest.md +4 -2
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
package/dist/plugins/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as errorHandlerPlugin, n as ErrorMapper, r as defaultIsDuplicateKeyError, t as ErrorHandlerOptions } from "../errorHandler-
|
|
3
|
-
import { a as arcCorePlugin, i as _default, n as ArcCorePluginOptions, r as PluginMeta, t as ArcCore } from "../fastify-augmentation-
|
|
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-
|
|
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 };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -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-
|
|
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.
|
|
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",
|
|
96
|
-
if (Math.random() > sampleRate)
|
|
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 {
|
|
1
|
+
import { Y as PresetResult } from "../index-DbtgCOCj.mjs";
|
|
2
2
|
import { i as RequestScope } from "../types-DH0GupI7.mjs";
|
|
3
|
-
import {
|
|
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 {
|
package/dist/presets/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
|
85
|
-
*
|
|
86
|
-
* ingredients differently (regression risk:
|
|
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
|
-
*
|
|
90
|
-
*
|
|
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
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,
|
|
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 {
|
|
2
|
-
import "../fastify-augmentation-
|
|
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 };
|