@classytic/arc 2.23.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController--M8EMzt0.mjs} +90 -21
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  4. package/dist/audit/index.d.mts +32 -3
  5. package/dist/audit/index.mjs +7 -4
  6. package/dist/auth/index.d.mts +2 -2
  7. package/dist/auth/index.mjs +263 -259
  8. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  9. package/dist/cache/index.d.mts +41 -3
  10. package/dist/cache/index.mjs +33 -4
  11. package/dist/cli/commands/describe.d.mts +1 -1
  12. package/dist/cli/commands/docs.mjs +1 -1
  13. package/dist/cli/commands/generate.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +2 -2
  16. package/dist/context/index.mjs +2 -64
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-DQ6rDTyH.mjs → core-EUNu0u07.mjs} +296 -11
  20. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-B6GvAuAX.mjs} +3 -2
  21. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  25. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  26. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  27. package/dist/events/index.d.mts +57 -314
  28. package/dist/events/index.mjs +163 -91
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  31. package/dist/factory/index.d.mts +4 -5
  32. package/dist/factory/index.mjs +28 -8
  33. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  34. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  35. package/dist/headers-BvlQSDMB.mjs +45 -0
  36. package/dist/health-CB_m4S32.mjs +377 -0
  37. package/dist/hooks/index.d.mts +1 -1
  38. package/dist/idempotency/index.mjs +27 -3
  39. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  40. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  41. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  42. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  43. package/dist/{index-CC5ruDGh.d.mts → index-f4PTrk3B.d.mts} +37 -5
  44. package/dist/index.d.mts +6 -6
  45. package/dist/index.mjs +5 -5
  46. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  47. package/dist/integrations/event-gateway.d.mts +1 -1
  48. package/dist/integrations/event-gateway.mjs +1 -1
  49. package/dist/integrations/index.d.mts +3 -3
  50. package/dist/integrations/jobs/index.d.mts +2 -0
  51. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  52. package/dist/integrations/mcp/index.d.mts +4 -4
  53. package/dist/integrations/mcp/index.mjs +2 -2
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/integrations/webhooks.d.mts +37 -30
  57. package/dist/integrations/webhooks.mjs +51 -62
  58. package/dist/integrations/websocket-redis.d.mts +1 -1
  59. package/dist/integrations/websocket.d.mts +1 -1
  60. package/dist/integrations/websocket.mjs +41 -14
  61. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  62. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cb7olw0j.mjs} +11 -9
  63. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  64. package/dist/middleware/index.d.mts +17 -1
  65. package/dist/middleware/index.mjs +1 -1
  66. package/dist/migrations/index.d.mts +94 -44
  67. package/dist/migrations/index.mjs +196 -44
  68. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  69. package/dist/{openapi-jSDx-VDv.mjs → openapi-DrLtog_B.mjs} +3 -3
  70. package/dist/permissions/index.d.mts +2 -2
  71. package/dist/permissions/index.mjs +2 -2
  72. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  73. package/dist/pipeline/index.d.mts +1 -1
  74. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BN5G05rd.mjs} +3 -26
  75. package/dist/plugins/index.d.mts +6 -77
  76. package/dist/plugins/index.mjs +11 -95
  77. package/dist/plugins/response-cache.mjs +1 -0
  78. package/dist/plugins/tracing-entry.mjs +1 -1
  79. package/dist/{index-Ba10fdKp.d.mts → preloadResources-0oe-JSSS.d.mts} +50 -50
  80. package/dist/{createApp-CjOeWJ9_.mjs → preloadResources-DfyngIN9.mjs} +233 -70
  81. package/dist/presets/filesUpload.d.mts +3 -3
  82. package/dist/presets/filesUpload.mjs +3 -3
  83. package/dist/presets/index.d.mts +2 -1
  84. package/dist/presets/index.mjs +1 -1
  85. package/dist/presets/multiTenant.d.mts +1 -1
  86. package/dist/presets/multiTenant.mjs +1 -1
  87. package/dist/presets/search.d.mts +2 -2
  88. package/dist/presets/search.mjs +1 -1
  89. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  90. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  91. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  92. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  93. package/dist/{realtime-I8OrfnMR.mjs → realtime-Cuw_deA-.mjs} +2 -2
  94. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  95. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  96. package/dist/registry/index.d.mts +2 -2
  97. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  98. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  99. package/dist/schedules-CjOd2MG9.mjs +113 -0
  100. package/dist/schemas/index.d.mts +67 -67
  101. package/dist/schemas/index.mjs +2 -3
  102. package/dist/scim/index.mjs +1 -2
  103. package/dist/scope/index.d.mts +2 -2
  104. package/dist/scope/index.mjs +4 -4
  105. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  106. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  107. package/dist/testing/index.d.mts +4 -3
  108. package/dist/testing/index.mjs +2 -2
  109. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  110. package/dist/types/index.d.mts +4 -4
  111. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  112. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  113. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  114. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  115. package/dist/usage/index.d.mts +3 -52
  116. package/dist/usage/index.mjs +0 -0
  117. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  118. package/dist/utils/index.d.mts +58 -85
  119. package/dist/utils/index.mjs +4 -2
  120. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  121. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  122. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  123. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  124. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  125. package/package.json +526 -513
  126. package/skills/arc/references/api-reference.md +3 -1
  127. package/skills/arc/references/integrations.md +20 -11
  128. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  129. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  130. package/dist/health-ScyjIVo5.mjs +0 -202
  131. package/dist/requestId-5BhTVzBv.mjs +0 -113
  132. package/dist/types-XTApa8b6.mjs +0 -24
@@ -115,8 +115,10 @@ import {
115
115
  healthPlugin, gracefulShutdownPlugin, ssePlugin,
116
116
  metricsPlugin, versioningPlugin,
117
117
  schedulesPlugin, // 2.21: interval jobs, optional LockAdapter leader lease, no Redis
118
- type ScheduleLockLike, type ScheduleDefinition,
118
+ type ScheduleDefinition,
119
119
  } from '@classytic/arc/plugins';
120
+ // Lock contract (schedules.lock / MigrationRunner.lock) — one ecosystem standard:
121
+ // import type { LockAdapter } from '@classytic/repo-core/lock';
120
122
  import { tracingPlugin } from '@classytic/arc/plugins/tracing';
121
123
  import { auditPlugin } from '@classytic/arc/audit';
122
124
  import { idempotencyPlugin } from '@classytic/arc/idempotency';
@@ -335,7 +335,8 @@ Events published via `fastify.events.publish()` auto-deliver to matching webhook
335
335
  ```typescript
336
336
  await app.events.publish('order.created', { orderId: '123' });
337
337
  // → POST https://customer.com/webhook
338
- // Headers: x-webhook-signature, x-webhook-id, x-webhook-event
338
+ // Headers: x-arc-webhook-signature (v1=<hmac>), x-arc-webhook-timestamp,
339
+ // x-arc-webhook-delivery (= event meta.id, stable across retries)
339
340
  // Body: { type, payload, meta }
340
341
  ```
341
342
 
@@ -343,23 +344,31 @@ Deliveries run with bounded concurrency (default: 5) — one slow endpoint won't
343
344
 
344
345
  ### HMAC Signing & Verification
345
346
 
346
- **Outbound** — every delivery is signed with the subscription's secret:
347
+ **Outbound** — every delivery is signed with the subscription's secret using the v1 contract (timestamp + delivery id are INSIDE the signed string — replay-bounded, dedup on an authenticated id):
347
348
 
348
349
  ```
349
- x-webhook-signature: sha256=a1b2c3...
350
+ x-arc-webhook-signature: v1=a1b2c3...
351
+ x-arc-webhook-timestamp: 1784640000000
352
+ x-arc-webhook-delivery: evt_...
350
353
  ```
351
354
 
352
- **Inbound** — verify with `verifySignature()` (timing-safe, never throws):
355
+ **Inbound from arc** — verify with `verifyWebhook()` (signature + tolerance window, typed failure reasons):
353
356
 
354
357
  ```typescript
355
- import { verifySignature } from '@classytic/arc/integrations/webhooks';
356
-
357
- fastify.post('/webhooks/incoming', async (req, reply) => {
358
- const sig = req.headers['x-webhook-signature'] as string;
359
- if (!verifySignature(req.rawBody, secret, sig)) {
360
- return reply.status(401).send({ error: 'Invalid signature' });
358
+ import { verifyWebhook } from '@classytic/arc/integrations/webhooks';
359
+
360
+ fastify.post('/webhooks/incoming', { config: { rawBody: true } }, async (req, reply) => {
361
+ const result = verifyWebhook({
362
+ body: req.rawBody as string,
363
+ secret,
364
+ signature: req.headers['x-arc-webhook-signature'] as string,
365
+ timestamp: req.headers['x-arc-webhook-timestamp'] as string,
366
+ deliveryId: req.headers['x-arc-webhook-delivery'] as string,
367
+ });
368
+ if (!result.valid) {
369
+ return reply.status(401).send({ error: result.reason });
361
370
  }
362
- // handle event via req.headers['x-webhook-event']
371
+ // dedup on result.deliveryId, then handle the event
363
372
  });
364
373
  ```
365
374
 
@@ -1,40 +0,0 @@
1
- import { r as CacheStore } from "./interface-C1_WLTMK.mjs";
2
- //#region src/cache/QueryCache.d.ts
3
- /** Metadata wrapper stored in CacheStore */
4
- interface CacheEnvelope<T = unknown> {
5
- data: T;
6
- createdAt: number;
7
- staleAfter: number;
8
- expiresAt: number;
9
- tags: string[];
10
- }
11
- interface QueryCacheConfig {
12
- /** Seconds data is "fresh" (no revalidation). Default: 0 */
13
- staleTime?: number;
14
- /** Seconds stale data stays cached (SWR window). Default: 60 */
15
- gcTime?: number;
16
- /** Tags for group invalidation */
17
- tags?: string[];
18
- }
19
- type CacheStatus = "fresh" | "stale" | "miss";
20
- interface CacheResult<T> {
21
- data: T;
22
- status: CacheStatus;
23
- }
24
- declare class QueryCache {
25
- private readonly store;
26
- constructor(store: CacheStore);
27
- get<T>(key: string): Promise<CacheResult<T>>;
28
- set<T>(key: string, data: T, config: QueryCacheConfig): Promise<void>;
29
- invalidate(key: string): Promise<void>;
30
- /** Get current version for a resource (defaults to 0 if not set) */
31
- getResourceVersion(resource: string): Promise<number>;
32
- /** Bump resource version — orphans all cached queries for this resource */
33
- bumpResourceVersion(resource: string): Promise<void>;
34
- /** Get current version for a tag */
35
- getTagVersion(tag: string): Promise<number>;
36
- /** Bump tag version — orphans all cached queries tagged with this tag */
37
- bumpTagVersion(tag: string): Promise<void>;
38
- }
39
- //#endregion
40
- export { QueryCacheConfig as a, QueryCache as i, CacheResult as n, CacheStatus as r, CacheEnvelope as t };
@@ -1,2 +0,0 @@
1
- import { extractBetterAuthOpenApi } from "./auth/index.mjs";
2
- export { extractBetterAuthOpenApi };
@@ -1,202 +0,0 @@
1
- import fp from "fastify-plugin";
2
- //#region src/plugins/health.ts
3
- function createHttpMetrics() {
4
- return {
5
- requestsTotal: {},
6
- requestDurations: [],
7
- _ringIndex: 0,
8
- startTime: Date.now()
9
- };
10
- }
11
- const healthPlugin = async (fastify, opts = {}) => {
12
- const { prefix = "/_health", checks = [], metrics = false, metricsCollector, version, collectHttpMetrics = metrics } = opts;
13
- const httpMetrics = createHttpMetrics();
14
- fastify.get(prefix, { schema: {
15
- tags: ["Health"],
16
- summary: "Liveness probe (alias for /live)",
17
- description: "Returns 200 if the process is alive",
18
- response: { 200: {
19
- type: "object",
20
- properties: {
21
- status: {
22
- type: "string",
23
- enum: ["ok"]
24
- },
25
- timestamp: { type: "string" },
26
- version: { type: "string" }
27
- }
28
- } }
29
- } }, async () => ({
30
- status: "ok",
31
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
32
- ...version ? { version } : {}
33
- }));
34
- fastify.get(`${prefix}/live`, { schema: {
35
- tags: ["Health"],
36
- summary: "Liveness probe",
37
- description: "Returns 200 if the process is alive",
38
- response: { 200: {
39
- type: "object",
40
- properties: {
41
- status: {
42
- type: "string",
43
- enum: ["ok"]
44
- },
45
- timestamp: { type: "string" },
46
- version: { type: "string" }
47
- }
48
- } }
49
- } }, async () => {
50
- return {
51
- status: "ok",
52
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
53
- ...version ? { version } : {}
54
- };
55
- });
56
- fastify.get(`${prefix}/ready`, { schema: {
57
- tags: ["Health"],
58
- summary: "Readiness probe",
59
- description: "Returns 200 if all dependencies are healthy",
60
- response: {
61
- 200: {
62
- type: "object",
63
- properties: {
64
- status: {
65
- type: "string",
66
- enum: ["ready", "not_ready"]
67
- },
68
- timestamp: { type: "string" },
69
- checks: {
70
- type: "array",
71
- items: {
72
- type: "object",
73
- properties: {
74
- name: { type: "string" },
75
- healthy: { type: "boolean" },
76
- duration: { type: "number" },
77
- error: { type: "string" }
78
- }
79
- }
80
- }
81
- }
82
- },
83
- 503: {
84
- type: "object",
85
- properties: {
86
- status: {
87
- type: "string",
88
- enum: ["not_ready"]
89
- },
90
- timestamp: { type: "string" },
91
- checks: { type: "array" }
92
- }
93
- }
94
- }
95
- } }, async (_, reply) => {
96
- const results = await runChecks(checks);
97
- const criticalFailed = results.some((r) => !r.healthy && (checks.find((c) => c.name === r.name)?.critical ?? true));
98
- const response = {
99
- status: criticalFailed ? "not_ready" : "ready",
100
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
101
- checks: results
102
- };
103
- if (criticalFailed) reply.code(503);
104
- return response;
105
- });
106
- if (metrics) fastify.get(`${prefix}/metrics`, async (_, reply) => {
107
- reply.type("text/plain; charset=utf-8");
108
- if (metricsCollector) return await metricsCollector();
109
- const uptime = process.uptime();
110
- const memory = process.memoryUsage();
111
- const cpu = process.cpuUsage();
112
- const lines = [
113
- "# HELP process_uptime_seconds Process uptime in seconds",
114
- "# TYPE process_uptime_seconds gauge",
115
- `process_uptime_seconds ${uptime.toFixed(2)}`,
116
- "",
117
- "# HELP process_memory_heap_bytes Heap memory usage in bytes",
118
- "# TYPE process_memory_heap_bytes gauge",
119
- `process_memory_heap_bytes{type="used"} ${memory.heapUsed}`,
120
- `process_memory_heap_bytes{type="total"} ${memory.heapTotal}`,
121
- "",
122
- "# HELP process_memory_rss_bytes RSS memory in bytes",
123
- "# TYPE process_memory_rss_bytes gauge",
124
- `process_memory_rss_bytes ${memory.rss}`,
125
- "",
126
- "# HELP process_memory_external_bytes External memory in bytes",
127
- "# TYPE process_memory_external_bytes gauge",
128
- `process_memory_external_bytes ${memory.external}`,
129
- "",
130
- "# HELP process_cpu_user_microseconds User CPU time in microseconds",
131
- "# TYPE process_cpu_user_microseconds counter",
132
- `process_cpu_user_microseconds ${cpu.user}`,
133
- "",
134
- "# HELP process_cpu_system_microseconds System CPU time in microseconds",
135
- "# TYPE process_cpu_system_microseconds counter",
136
- `process_cpu_system_microseconds ${cpu.system}`,
137
- ""
138
- ];
139
- if (collectHttpMetrics && Object.keys(httpMetrics.requestsTotal).length > 0) {
140
- lines.push("# HELP http_requests_total Total HTTP requests by status code", "# TYPE http_requests_total counter");
141
- for (const [status, count] of Object.entries(httpMetrics.requestsTotal)) lines.push(`http_requests_total{status="${status}"} ${count}`);
142
- lines.push("");
143
- if (httpMetrics.requestDurations.length > 0) {
144
- const sorted = [...httpMetrics.requestDurations].sort((a, b) => a - b);
145
- const p50 = sorted[Math.floor(sorted.length * .5)] || 0;
146
- const p95 = sorted[Math.floor(sorted.length * .95)] || 0;
147
- const p99 = sorted[Math.floor(sorted.length * .99)] || 0;
148
- const sum = sorted.reduce((a, b) => a + b, 0);
149
- 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}`, "");
150
- }
151
- }
152
- return lines.join("\n");
153
- });
154
- if (collectHttpMetrics) fastify.addHook("onResponse", async (_request, reply) => {
155
- const duration = reply.elapsedTime;
156
- const statusBucket = `${Math.floor(reply.statusCode / 100)}xx`;
157
- httpMetrics.requestsTotal[statusBucket] = (httpMetrics.requestsTotal[statusBucket] || 0) + 1;
158
- if (httpMetrics.requestDurations.length < 1e4) httpMetrics.requestDurations.push(duration);
159
- else httpMetrics.requestDurations[httpMetrics._ringIndex % 1e4] = duration;
160
- httpMetrics._ringIndex = httpMetrics._ringIndex + 1;
161
- });
162
- fastify.log?.debug?.(`Health plugin registered at ${prefix} (alias), ${prefix}/live, ${prefix}/ready${metrics ? `, ${prefix}/metrics` : ""}`);
163
- };
164
- /**
165
- * Run all health checks with timeout
166
- */
167
- async function runChecks(checks) {
168
- const results = [];
169
- for (const check of checks) {
170
- const start = Date.now();
171
- const timeout = check.timeout ?? 5e3;
172
- let timer;
173
- try {
174
- const checkPromise = Promise.resolve(check.check());
175
- const timeoutPromise = new Promise((_, reject) => {
176
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Health check timeout")), timeout);
177
- });
178
- const healthy = await Promise.race([checkPromise, timeoutPromise]);
179
- results.push({
180
- name: check.name,
181
- healthy: Boolean(healthy),
182
- duration: Date.now() - start
183
- });
184
- } catch (err) {
185
- results.push({
186
- name: check.name,
187
- healthy: false,
188
- duration: Date.now() - start,
189
- error: err.message
190
- });
191
- } finally {
192
- if (timer) clearTimeout(timer);
193
- }
194
- }
195
- return results;
196
- }
197
- var health_default = fp(healthPlugin, {
198
- name: "arc-health",
199
- fastify: "5.x"
200
- });
201
- //#endregion
202
- export { health_default as n, healthPlugin as t };
@@ -1,113 +0,0 @@
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 };
@@ -1,24 +0,0 @@
1
- //#region src/permissions/types.ts
2
- /**
3
- * Extract normalized roles from a user object.
4
- *
5
- * Reads `user.role` which can be:
6
- * - A comma-separated string: `"superadmin,user"` (Better Auth admin plugin)
7
- * - A string array: `["admin", "user"]` (JWT / custom auth)
8
- * - A single string: `"admin"`
9
- */
10
- /**
11
- * Normalize a raw role value (string, comma-separated string, or array) into a string[].
12
- * Shared low-level helper used by both getUserRoles() and the Better Auth adapter.
13
- */
14
- function normalizeRoles(value) {
15
- if (Array.isArray(value)) return value.map((r) => String(r).trim()).filter(Boolean);
16
- if (typeof value === "string" && value.length > 0) return value.split(",").map((r) => r.trim()).filter(Boolean);
17
- return [];
18
- }
19
- function getUserRoles(user) {
20
- if (!user) return [];
21
- return normalizeRoles(user.role);
22
- }
23
- //#endregion
24
- export { normalizeRoles as n, getUserRoles as t };