@classytic/arc 2.3.0 → 2.4.2

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 (175) hide show
  1. package/README.md +187 -18
  2. package/bin/arc.js +11 -3
  3. package/dist/BaseController-CkM5dUh_.mjs +1031 -0
  4. package/dist/{EventTransport-BkUDYZEb.d.mts → EventTransport-wc5hSLik.d.mts} +1 -1
  5. package/dist/{HookSystem-BsGV-j2l.mjs → HookSystem-COkyWztM.mjs} +2 -3
  6. package/dist/{ResourceRegistry-7Ic20ZMw.mjs → ResourceRegistry-DeCIFlix.mjs} +8 -5
  7. package/dist/adapters/index.d.mts +3 -5
  8. package/dist/adapters/index.mjs +2 -3
  9. package/dist/{prisma-DJbMt3yf.mjs → adapters-DTC4Ug66.mjs} +45 -12
  10. package/dist/audit/index.d.mts +4 -7
  11. package/dist/audit/index.mjs +2 -29
  12. package/dist/audit/mongodb.d.mts +1 -4
  13. package/dist/audit/mongodb.mjs +2 -3
  14. package/dist/auth/index.d.mts +7 -9
  15. package/dist/auth/index.mjs +65 -63
  16. package/dist/auth/redis-session.d.mts +1 -1
  17. package/dist/auth/redis-session.mjs +1 -2
  18. package/dist/{betterAuthOpenApi-DjWDddNc.mjs → betterAuthOpenApi-lz0IRbXJ.mjs} +4 -6
  19. package/dist/cache/index.d.mts +23 -23
  20. package/dist/cache/index.mjs +4 -6
  21. package/dist/{caching-GSDJcA6-.mjs → caching-BSXB-Xr7.mjs} +2 -24
  22. package/dist/chunk-BpYLSNr0.mjs +14 -0
  23. package/dist/circuitBreaker-BOBOpN2w.mjs +284 -0
  24. package/dist/circuitBreaker-JP2GdJ4b.d.mts +206 -0
  25. package/dist/cli/commands/describe.mjs +24 -7
  26. package/dist/cli/commands/docs.mjs +6 -7
  27. package/dist/cli/commands/doctor.d.mts +10 -0
  28. package/dist/cli/commands/doctor.mjs +156 -0
  29. package/dist/cli/commands/generate.mjs +66 -17
  30. package/dist/cli/commands/init.mjs +315 -45
  31. package/dist/cli/commands/introspect.mjs +2 -4
  32. package/dist/cli/index.d.mts +1 -10
  33. package/dist/cli/index.mjs +4 -153
  34. package/dist/{constants-DdXFXQtN.mjs → constants-Cxde4rpC.mjs} +1 -2
  35. package/dist/core/index.d.mts +3 -5
  36. package/dist/core/index.mjs +5 -4
  37. package/dist/core-C1XCMtqM.mjs +185 -0
  38. package/dist/{createApp-CgKOPhA4.mjs → createApp-ByWNRsZj.mjs} +64 -35
  39. package/dist/{defineResource-DWbpJYtm.mjs → defineResource-D9aY5Cy6.mjs} +108 -1157
  40. package/dist/discovery/index.mjs +37 -5
  41. package/dist/docs/index.d.mts +6 -9
  42. package/dist/docs/index.mjs +3 -21
  43. package/dist/dynamic/index.d.mts +93 -0
  44. package/dist/dynamic/index.mjs +122 -0
  45. package/dist/{elevation-DSTbVvYj.mjs → elevation-BEdACOLB.mjs} +5 -36
  46. package/dist/{elevation-DGo5shaX.d.mts → elevation-Ca_yveIO.d.mts} +41 -7
  47. package/dist/{errorHandler-C3GY3_ow.mjs → errorHandler--zp54tGc.mjs} +3 -5
  48. package/dist/errorHandler-Do4vVQ1f.d.mts +139 -0
  49. package/dist/{errors-DBANPbGr.mjs → errors-rxhfP7Hf.mjs} +1 -2
  50. package/dist/{eventPlugin-BEOvaDqo.mjs → eventPlugin-Ba00swHF.mjs} +25 -27
  51. package/dist/{eventPlugin-H6wDDjGO.d.mts → eventPlugin-iGrSEmwJ.d.mts} +105 -5
  52. package/dist/events/index.d.mts +72 -7
  53. package/dist/events/index.mjs +216 -4
  54. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  55. package/dist/events/transports/redis-stream-entry.mjs +19 -7
  56. package/dist/events/transports/redis.d.mts +1 -1
  57. package/dist/events/transports/redis.mjs +3 -4
  58. package/dist/factory/index.d.mts +23 -9
  59. package/dist/factory/index.mjs +48 -3
  60. package/dist/{fields-Bi_AVKSo.d.mts → fields-DFwdaWCq.d.mts} +1 -1
  61. package/dist/{fields-CTd_CrKr.mjs → fields-ipsbIRPK.mjs} +1 -2
  62. package/dist/hooks/index.d.mts +1 -3
  63. package/dist/hooks/index.mjs +2 -3
  64. package/dist/idempotency/index.d.mts +5 -5
  65. package/dist/idempotency/index.mjs +3 -7
  66. package/dist/idempotency/mongodb.d.mts +1 -1
  67. package/dist/idempotency/mongodb.mjs +4 -5
  68. package/dist/idempotency/redis.d.mts +1 -1
  69. package/dist/idempotency/redis.mjs +2 -5
  70. package/dist/{fastifyAdapter-6b_eRDBw.d.mts → index-BL8CaQih.d.mts} +56 -57
  71. package/dist/index-Diqcm14c.d.mts +369 -0
  72. package/dist/{prisma-Dy5S5F5i.d.mts → index-yhxyjqNb.d.mts} +4 -5
  73. package/dist/index.d.mts +100 -105
  74. package/dist/index.mjs +85 -58
  75. package/dist/integrations/event-gateway.d.mts +1 -1
  76. package/dist/integrations/event-gateway.mjs +8 -4
  77. package/dist/integrations/index.d.mts +4 -2
  78. package/dist/integrations/index.mjs +1 -1
  79. package/dist/integrations/jobs.d.mts +2 -2
  80. package/dist/integrations/jobs.mjs +63 -14
  81. package/dist/integrations/mcp/index.d.mts +219 -0
  82. package/dist/integrations/mcp/index.mjs +572 -0
  83. package/dist/integrations/mcp/testing.d.mts +53 -0
  84. package/dist/integrations/mcp/testing.mjs +104 -0
  85. package/dist/integrations/streamline.mjs +39 -19
  86. package/dist/integrations/webhooks.d.mts +56 -0
  87. package/dist/integrations/webhooks.mjs +139 -0
  88. package/dist/integrations/websocket-redis.d.mts +46 -0
  89. package/dist/integrations/websocket-redis.mjs +50 -0
  90. package/dist/integrations/websocket.d.mts +68 -2
  91. package/dist/integrations/websocket.mjs +96 -13
  92. package/dist/{interface-CSNjltAc.d.mts → interface-B4awm1RJ.d.mts} +2 -2
  93. package/dist/interface-DGmPxakH.d.mts +2213 -0
  94. package/dist/{keys-DhqDRxv3.mjs → keys-qcD-TVJl.mjs} +3 -4
  95. package/dist/{logger-ByrvQWZO.mjs → logger-Dz3j1ItV.mjs} +2 -4
  96. package/dist/{memory-B2v7KrCB.mjs → memory-Cb_7iy9e.mjs} +2 -4
  97. package/dist/metrics-Csh4nsvv.mjs +224 -0
  98. package/dist/migrations/index.d.mts +113 -44
  99. package/dist/migrations/index.mjs +84 -102
  100. package/dist/{mongodb-DNKEExbf.mjs → mongodb-BuQ7fNTg.mjs} +1 -4
  101. package/dist/{mongodb-ClykrfGo.d.mts → mongodb-CUpYfxfD.d.mts} +2 -3
  102. package/dist/{mongodb-Dg8O_gvd.d.mts → mongodb-bga9AbkD.d.mts} +2 -2
  103. package/dist/{openapi-9nB_kiuR.mjs → openapi-CBmZ6EQN.mjs} +4 -21
  104. package/dist/org/index.d.mts +12 -14
  105. package/dist/org/index.mjs +92 -119
  106. package/dist/org/types.d.mts +2 -2
  107. package/dist/org/types.mjs +1 -1
  108. package/dist/permissions/index.d.mts +4 -278
  109. package/dist/permissions/index.mjs +4 -579
  110. package/dist/permissions-CA5zg0yK.mjs +751 -0
  111. package/dist/plugins/index.d.mts +104 -107
  112. package/dist/plugins/index.mjs +203 -313
  113. package/dist/plugins/response-cache.mjs +4 -69
  114. package/dist/plugins/tracing-entry.d.mts +1 -1
  115. package/dist/plugins/tracing-entry.mjs +24 -11
  116. package/dist/{pluralize-CM-jZg7p.mjs → pluralize-CcT6qF0a.mjs} +12 -13
  117. package/dist/policies/index.d.mts +2 -2
  118. package/dist/policies/index.mjs +80 -83
  119. package/dist/presets/index.d.mts +26 -19
  120. package/dist/presets/index.mjs +2 -142
  121. package/dist/presets/multiTenant.d.mts +1 -4
  122. package/dist/presets/multiTenant.mjs +4 -6
  123. package/dist/presets-C9QXJV1u.mjs +422 -0
  124. package/dist/{queryCachePlugin-B6R0d4av.mjs → queryCachePlugin-ClosZdNS.mjs} +6 -27
  125. package/dist/{queryCachePlugin-Q6SYuHZ6.d.mts → queryCachePlugin-DcmETvcB.d.mts} +3 -3
  126. package/dist/queryParser-CgCtsjti.mjs +352 -0
  127. package/dist/{redis-UwjEp8Ea.d.mts → redis-CQ5YxMC5.d.mts} +2 -2
  128. package/dist/{redis-stream-CBg0upHI.d.mts → redis-stream-BW9UKLZM.d.mts} +9 -2
  129. package/dist/registry/index.d.mts +1 -4
  130. package/dist/registry/index.mjs +3 -4
  131. package/dist/{introspectionPlugin-B3JkrjwU.mjs → registry-I-ogLgL9.mjs} +1 -8
  132. package/dist/{requestContext-xi6OKBL-.mjs → requestContext-DYtmNpm5.mjs} +1 -3
  133. package/dist/resourceToTools-PMFE8HIv.mjs +533 -0
  134. package/dist/rpc/index.d.mts +90 -0
  135. package/dist/rpc/index.mjs +248 -0
  136. package/dist/{schemaConverter-Dtg0Kt9T.mjs → schemaConverter-DjzHpFam.mjs} +1 -2
  137. package/dist/schemas/index.d.mts +30 -30
  138. package/dist/schemas/index.mjs +2 -4
  139. package/dist/scope/index.d.mts +13 -2
  140. package/dist/scope/index.mjs +18 -5
  141. package/dist/{sessionManager-D_iEHjQl.d.mts → sessionManager-wbkYj2HL.d.mts} +2 -2
  142. package/dist/{sse-DkqQ1uxb.mjs → sse-BkViJPlT.mjs} +4 -25
  143. package/dist/testing/index.d.mts +551 -567
  144. package/dist/testing/index.mjs +1744 -1799
  145. package/dist/{tracing-8CEbhF0w.d.mts → tracing-bz_U4EM1.d.mts} +6 -1
  146. package/dist/{typeGuards-DwxA1t_L.mjs → typeGuards-Cj5Rgvlg.mjs} +1 -2
  147. package/dist/types/index.d.mts +4 -946
  148. package/dist/types/index.mjs +2 -4
  149. package/dist/types-BJmgxNbF.d.mts +275 -0
  150. package/dist/{types-RLkFVgaw.d.mts → types-BNUccdcf.d.mts} +2 -2
  151. package/dist/{types-Beqn1Un7.mjs → types-C6TQjtdi.mjs} +30 -2
  152. package/dist/{types-tKwaViYB.d.mts → types-Dt0-AI6E.d.mts} +68 -27
  153. package/dist/{types-DelU6kln.mjs → types-ZUu_h0jp.mjs} +1 -2
  154. package/dist/utils/index.d.mts +254 -351
  155. package/dist/utils/index.mjs +7 -6
  156. package/dist/utils-Dc0WhlIl.mjs +594 -0
  157. package/dist/versioning-BzfeHmhj.mjs +37 -0
  158. package/package.json +44 -10
  159. package/skills/arc/SKILL.md +518 -0
  160. package/skills/arc/references/auth.md +250 -0
  161. package/skills/arc/references/events.md +272 -0
  162. package/skills/arc/references/integrations.md +385 -0
  163. package/skills/arc/references/mcp.md +431 -0
  164. package/skills/arc/references/production.md +610 -0
  165. package/skills/arc/references/testing.md +183 -0
  166. package/dist/audited-CGdLiSlE.mjs +0 -140
  167. package/dist/chunk-C7Uep-_p.mjs +0 -20
  168. package/dist/circuitBreaker-CSS2VvL6.mjs +0 -1109
  169. package/dist/errorHandler-CW3OOeYq.d.mts +0 -72
  170. package/dist/interface-BtdYtQUA.d.mts +0 -1114
  171. package/dist/presets-BTeYbw7h.d.mts +0 -57
  172. package/dist/presets-CeFtfDR8.mjs +0 -119
  173. /package/dist/{errors-DAWRdiYP.d.mts → errors-CPpvPHT0.d.mts} +0 -0
  174. /package/dist/{externalPaths-SyPF2tgK.d.mts → externalPaths-DpO-s7r8.d.mts} +0 -0
  175. /package/dist/{interface-DTbsvIWe.d.mts → interface-D_BWALyZ.d.mts} +0 -0
@@ -1,75 +1,191 @@
1
- import { p as MUTATION_OPERATIONS } from "../constants-DdXFXQtN.mjs";
2
- import { r as getOrgId } from "../types-Beqn1Un7.mjs";
3
- import { t as HookSystem } from "../HookSystem-BsGV-j2l.mjs";
4
- import { t as hasEvents } from "../typeGuards-DwxA1t_L.mjs";
5
- import { t as requestContext } from "../requestContext-xi6OKBL-.mjs";
6
- import { t as ResourceRegistry } from "../ResourceRegistry-7Ic20ZMw.mjs";
7
- import { t as errorHandlerPlugin } from "../errorHandler-C3GY3_ow.mjs";
8
- import { n as caching_default, t as cachingPlugin } from "../caching-GSDJcA6-.mjs";
9
- import { n as sse_default, t as ssePlugin } from "../sse-DkqQ1uxb.mjs";
1
+ import { p as MUTATION_OPERATIONS } from "../constants-Cxde4rpC.mjs";
2
+ import { r as getOrgId } from "../types-C6TQjtdi.mjs";
3
+ import { t as requestContext } from "../requestContext-DYtmNpm5.mjs";
4
+ import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
5
+ import { t as HookSystem } from "../HookSystem-COkyWztM.mjs";
6
+ import { t as ResourceRegistry } from "../ResourceRegistry-DeCIFlix.mjs";
7
+ import { n as caching_default, t as cachingPlugin } from "../caching-BSXB-Xr7.mjs";
8
+ import { t as errorHandlerPlugin } from "../errorHandler--zp54tGc.mjs";
9
+ import { n as metrics_default, t as metricsPlugin } from "../metrics-Csh4nsvv.mjs";
10
+ import { n as sse_default, t as ssePlugin } from "../sse-BkViJPlT.mjs";
11
+ import { n as versioning_default, t as versioningPlugin } from "../versioning-BzfeHmhj.mjs";
12
+ import { randomUUID } from "node:crypto";
10
13
  import fp from "fastify-plugin";
11
- import { randomUUID } from "crypto";
12
-
13
- //#region src/plugins/requestId.ts
14
+ //#region src/core/arcCorePlugin.ts
15
+ const arcCorePlugin = async (fastify, opts = {}) => {
16
+ const { emitEvents = true, hookSystem, registry } = opts;
17
+ const actualHookSystem = hookSystem ?? new HookSystem();
18
+ const actualRegistry = registry ?? new ResourceRegistry();
19
+ fastify.decorate("arc", {
20
+ hooks: actualHookSystem,
21
+ registry: actualRegistry,
22
+ emitEvents,
23
+ externalOpenApiPaths: [],
24
+ plugins: /* @__PURE__ */ new Map()
25
+ });
26
+ fastify.addHook("onRequest", (request, _reply, done) => {
27
+ const store = {
28
+ requestId: request.id,
29
+ startTime: performance.now()
30
+ };
31
+ requestContext.storage.run(store, done);
32
+ });
33
+ fastify.addHook("preHandler", (request, _reply, done) => {
34
+ const store = requestContext.get();
35
+ if (store) {
36
+ store.user = request.user ?? null;
37
+ store.organizationId = request.scope?.kind === "member" ? request.scope.organizationId : request.scope?.kind === "elevated" ? request.scope.organizationId : void 0;
38
+ }
39
+ done();
40
+ });
41
+ if (emitEvents) {
42
+ const eventOperations = MUTATION_OPERATIONS;
43
+ for (const operation of eventOperations) actualHookSystem.after("*", operation, async (ctx) => {
44
+ if (!hasEvents(fastify)) return;
45
+ const store = requestContext.get();
46
+ const eventType = `${ctx.resource}.${operation}d`;
47
+ const userId = ctx.user?.id ?? ctx.user?._id;
48
+ const organizationId = ctx.context?._scope ? getOrgId(ctx.context._scope) : void 0;
49
+ const payload = {
50
+ resource: ctx.resource,
51
+ operation: ctx.operation,
52
+ data: ctx.result,
53
+ userId,
54
+ organizationId,
55
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
56
+ };
57
+ try {
58
+ await fastify.events.publish(eventType, payload, {
59
+ correlationId: store?.requestId,
60
+ resource: ctx.resource,
61
+ resourceId: extractId(ctx.result),
62
+ userId: userId ? String(userId) : void 0,
63
+ organizationId
64
+ });
65
+ } catch (error) {
66
+ fastify.log?.warn?.({
67
+ eventType,
68
+ error
69
+ }, "Failed to emit event");
70
+ }
71
+ });
72
+ }
73
+ fastify.addHook("onReady", async () => {
74
+ if (!hasEvents(fastify)) return;
75
+ try {
76
+ await fastify.events.publish("arc.ready", {
77
+ resources: actualRegistry.getAll().length,
78
+ hooks: actualHookSystem.getAll().length,
79
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
80
+ });
81
+ } catch {}
82
+ });
83
+ fastify.addHook("onClose", async () => {
84
+ actualHookSystem.clear();
85
+ actualRegistry._clear();
86
+ });
87
+ fastify.log?.debug?.("Arc core plugin enabled (instance-scoped hooks & registry)");
88
+ };
89
+ /** Extract document ID from a result (handles Mongoose docs and plain objects) */
90
+ function extractId(doc) {
91
+ if (!doc || typeof doc !== "object") return void 0;
92
+ const d = doc;
93
+ const rawId = d._id ?? d.id;
94
+ return rawId ? String(rawId) : void 0;
95
+ }
96
+ var arcCorePlugin_default = fp(arcCorePlugin, {
97
+ name: "arc-core",
98
+ fastify: "5.x"
99
+ });
100
+ //#endregion
101
+ //#region src/plugins/createPlugin.ts
14
102
  /**
15
- * Request ID Plugin
16
- *
17
- * Propagates request IDs for distributed tracing.
18
- * - Accepts incoming x-request-id header
19
- * - Generates UUID if not provided
20
- * - Attaches to request.id and response header
21
- *
22
- * @example
23
- * import { requestIdPlugin } from '@classytic/arc';
24
- *
25
- * await fastify.register(requestIdPlugin);
103
+ * Create a structured plugin with forRoot (global) and forResource (per-resource) support.
26
104
  *
27
- * // In handlers, access via request.id
28
- * fastify.get('/', async (request) => {
29
- * console.log(request.id); // UUID
30
- * });
105
+ * @param name - Plugin name (used for Fastify registration and debugging)
106
+ * @param definition - Plugin setup functions
107
+ * @returns ArcPlugin with forRoot() and forResource() methods
31
108
  */
32
- const requestIdPlugin = async (fastify, opts = {}) => {
33
- const { header = "x-request-id", generator = randomUUID, setResponseHeader = true } = opts;
34
- if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
35
- fastify.addHook("onRequest", async (request) => {
36
- const incomingId = request.headers[header];
37
- const sanitized = typeof incomingId === "string" ? incomingId.trim() : "";
38
- const requestId = sanitized.length > 0 && sanitized.length <= 128 && /^[\w.:-]+$/.test(sanitized) ? sanitized : generator();
39
- request.id = requestId;
40
- request.requestId = requestId;
109
+ function createPlugin(name, definition) {
110
+ return {
111
+ name,
112
+ forRoot(opts) {
113
+ const plugin = async (fastify, pluginOpts) => {
114
+ const mergedOpts = {
115
+ ...opts,
116
+ ...pluginOpts
117
+ };
118
+ if (definition.forRoot) await definition.forRoot(fastify, mergedOpts);
119
+ };
120
+ return fp(plugin, {
121
+ name: `arc-plugin-${name}`,
122
+ fastify: "5.x"
123
+ });
124
+ },
125
+ forResource(opts) {
126
+ if (!definition.forResource) return {};
127
+ return definition.forResource({}, opts ?? {});
128
+ }
129
+ };
130
+ }
131
+ //#endregion
132
+ //#region src/plugins/gracefulShutdown.ts
133
+ const gracefulShutdownPlugin = async (fastify, opts = {}) => {
134
+ const { timeout = 3e4, onShutdown, signals = ["SIGTERM", "SIGINT"], logEvents = true, onForceExit = () => process.exit(1) } = opts;
135
+ let isShuttingDown = false;
136
+ const signalHandlers = /* @__PURE__ */ new Map();
137
+ const shutdown = async (signal) => {
138
+ if (isShuttingDown) {
139
+ if (logEvents) fastify.log?.warn?.({ signal }, "Shutdown already in progress, ignoring signal");
140
+ return;
141
+ }
142
+ isShuttingDown = true;
143
+ if (logEvents) fastify.log?.info?.({
144
+ signal,
145
+ timeout
146
+ }, "Shutdown signal received, starting graceful shutdown");
147
+ const forceExitTimer = setTimeout(() => {
148
+ if (logEvents) fastify.log?.error?.("Graceful shutdown timeout exceeded, forcing exit");
149
+ onForceExit("timeout");
150
+ }, timeout);
151
+ forceExitTimer.unref();
152
+ try {
153
+ if (logEvents) fastify.log?.info?.("Closing server to new connections");
154
+ await fastify.close();
155
+ if (onShutdown) {
156
+ if (logEvents) fastify.log?.info?.("Running custom shutdown handler");
157
+ await onShutdown();
158
+ }
159
+ if (logEvents) fastify.log?.info?.("Graceful shutdown complete");
160
+ clearTimeout(forceExitTimer);
161
+ } catch (err) {
162
+ if (logEvents) fastify.log?.error?.({ error: err.message }, "Error during shutdown");
163
+ clearTimeout(forceExitTimer);
164
+ onForceExit("error");
165
+ }
166
+ };
167
+ for (const signal of signals) {
168
+ const handler = () => {
169
+ shutdown(signal);
170
+ };
171
+ signalHandlers.set(signal, handler);
172
+ process.on(signal, handler);
173
+ }
174
+ fastify.addHook("onClose", async () => {
175
+ for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
176
+ signalHandlers.clear();
41
177
  });
42
- if (setResponseHeader) fastify.addHook("onSend", async (request, reply) => {
43
- reply.header(header, request.requestId);
178
+ fastify.decorate("shutdown", async () => {
179
+ await shutdown("MANUAL");
44
180
  });
45
- fastify.log?.debug?.("Request ID plugin registered");
181
+ if (logEvents) fastify.log?.debug?.({ signals }, "Graceful shutdown plugin registered");
46
182
  };
47
- var requestId_default = fp(requestIdPlugin, {
48
- name: "arc-request-id",
183
+ var gracefulShutdown_default = fp(gracefulShutdownPlugin, {
184
+ name: "arc-graceful-shutdown",
49
185
  fastify: "5.x"
50
186
  });
51
-
52
187
  //#endregion
53
188
  //#region src/plugins/health.ts
54
- /**
55
- * Health Check Plugin
56
- *
57
- * Kubernetes-ready health endpoints:
58
- * - /health/live - Liveness probe (is the process alive?)
59
- * - /health/ready - Readiness probe (can we serve traffic?)
60
- * - /health/metrics - Prometheus metrics (optional)
61
- *
62
- * @example
63
- * import { healthPlugin } from '@classytic/arc';
64
- *
65
- * await fastify.register(healthPlugin, {
66
- * prefix: '/_health',
67
- * checks: [
68
- * { name: 'mongodb', check: async () => mongoose.connection.readyState === 1 },
69
- * { name: 'redis', check: async () => redis.ping() === 'PONG' },
70
- * ],
71
- * });
72
- */
73
189
  function createHttpMetrics() {
74
190
  return {
75
191
  requestsTotal: {},
@@ -253,270 +369,44 @@ var health_default = fp(healthPlugin, {
253
369
  name: "arc-health",
254
370
  fastify: "5.x"
255
371
  });
256
-
257
372
  //#endregion
258
- //#region src/plugins/gracefulShutdown.ts
259
- /**
260
- * Graceful Shutdown Plugin
261
- *
262
- * Handles SIGTERM and SIGINT signals for clean shutdown:
263
- * - Stops accepting new connections
264
- * - Waits for in-flight requests to complete
265
- * - Closes database connections
266
- * - Exits cleanly
267
- *
268
- * Essential for Kubernetes deployments.
269
- *
270
- * @example
271
- * import { gracefulShutdownPlugin } from '@classytic/arc';
272
- *
273
- * // Production
274
- * await fastify.register(gracefulShutdownPlugin, {
275
- * timeout: 30000, // 30 seconds max
276
- * onShutdown: async () => {
277
- * await mongoose.disconnect();
278
- * await redis.quit();
279
- * },
280
- * });
281
- *
282
- * // Tests — prevent process.exit from killing the runner
283
- * await fastify.register(gracefulShutdownPlugin, {
284
- * onForceExit: () => {},
285
- * });
286
- */
287
- const gracefulShutdownPlugin = async (fastify, opts = {}) => {
288
- const { timeout = 3e4, onShutdown, signals = ["SIGTERM", "SIGINT"], logEvents = true, onForceExit = () => process.exit(1) } = opts;
289
- let isShuttingDown = false;
290
- const signalHandlers = /* @__PURE__ */ new Map();
291
- const shutdown = async (signal) => {
292
- if (isShuttingDown) {
293
- if (logEvents) fastify.log?.warn?.({ signal }, "Shutdown already in progress, ignoring signal");
294
- return;
295
- }
296
- isShuttingDown = true;
297
- if (logEvents) fastify.log?.info?.({
298
- signal,
299
- timeout
300
- }, "Shutdown signal received, starting graceful shutdown");
301
- const forceExitTimer = setTimeout(() => {
302
- if (logEvents) fastify.log?.error?.("Graceful shutdown timeout exceeded, forcing exit");
303
- onForceExit("timeout");
304
- }, timeout);
305
- forceExitTimer.unref();
306
- try {
307
- if (logEvents) fastify.log?.info?.("Closing server to new connections");
308
- await fastify.close();
309
- if (onShutdown) {
310
- if (logEvents) fastify.log?.info?.("Running custom shutdown handler");
311
- await onShutdown();
312
- }
313
- if (logEvents) fastify.log?.info?.("Graceful shutdown complete");
314
- clearTimeout(forceExitTimer);
315
- } catch (err) {
316
- if (logEvents) fastify.log?.error?.({ error: err.message }, "Error during shutdown");
317
- clearTimeout(forceExitTimer);
318
- onForceExit("error");
319
- }
320
- };
321
- for (const signal of signals) {
322
- const handler = () => {
323
- shutdown(signal);
324
- };
325
- signalHandlers.set(signal, handler);
326
- process.on(signal, handler);
327
- }
328
- fastify.addHook("onClose", async () => {
329
- for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
330
- signalHandlers.clear();
331
- });
332
- fastify.decorate("shutdown", async () => {
333
- await shutdown("MANUAL");
334
- });
335
- if (logEvents) fastify.log?.debug?.({ signals }, "Graceful shutdown plugin registered");
336
- };
337
- var gracefulShutdown_default = fp(gracefulShutdownPlugin, {
338
- name: "arc-graceful-shutdown",
339
- fastify: "5.x"
340
- });
341
-
342
- //#endregion
343
- //#region src/plugins/createPlugin.ts
344
- /**
345
- * createPlugin() — forRoot/forFeature Pattern
346
- *
347
- * Standard pattern for plugins that need both global setup and per-resource configuration.
348
- * Inspired by NestJS forRoot/forFeature but simpler — plain functions, no decorators.
349
- *
350
- * @example
351
- * ```typescript
352
- * // Define a plugin with global + per-resource config
353
- * const analytics = createPlugin('analytics', {
354
- * forRoot: async (fastify, opts) => {
355
- * // Global setup: connect to analytics service, add decorators
356
- * const client = new AnalyticsClient(opts.apiKey);
357
- * fastify.decorate('analytics', client);
358
- * },
359
- * forResource: (resourceConfig, opts) => {
360
- * // Per-resource: return hooks, middleware, or routes
361
- * return {
362
- * hooks: [{
363
- * operation: 'create', phase: 'after', priority: 100,
364
- * handler: (ctx) => client.track('created', ctx.result),
365
- * }],
366
- * };
367
- * },
368
- * });
369
- *
370
- * // Usage — register globally once
371
- * await app.register(analytics.forRoot({ apiKey: 'xxx' }));
372
- *
373
- * // Then apply per-resource
374
- * const productResource = defineResource({
375
- * name: 'product',
376
- * adapter: productAdapter,
377
- * ...analytics.forResource({ trackEvents: true }),
378
- * });
379
- * ```
380
- */
381
- /**
382
- * Create a structured plugin with forRoot (global) and forResource (per-resource) support.
383
- *
384
- * @param name - Plugin name (used for Fastify registration and debugging)
385
- * @param definition - Plugin setup functions
386
- * @returns ArcPlugin with forRoot() and forResource() methods
387
- */
388
- function createPlugin(name, definition) {
389
- return {
390
- name,
391
- forRoot(opts) {
392
- const plugin = async (fastify, pluginOpts) => {
393
- const mergedOpts = {
394
- ...opts,
395
- ...pluginOpts
396
- };
397
- if (definition.forRoot) await definition.forRoot(fastify, mergedOpts);
398
- };
399
- return fp(plugin, {
400
- name: `arc-plugin-${name}`,
401
- fastify: "5.x"
402
- });
403
- },
404
- forResource(opts) {
405
- if (!definition.forResource) return {};
406
- return definition.forResource({}, opts ?? {});
407
- }
408
- };
409
- }
410
-
411
- //#endregion
412
- //#region src/core/arcCorePlugin.ts
373
+ //#region src/plugins/requestId.ts
413
374
  /**
414
- * Arc Core Plugin
415
- *
416
- * Sets up instance-scoped Arc systems:
417
- * - HookSystem: Lifecycle hooks per app instance
418
- * - ResourceRegistry: Resource tracking per app instance
419
- * - Event integration: Wires CRUD operations to fastify.events
375
+ * Request ID Plugin
420
376
  *
421
- * This solves the global singleton leak problem where multiple
422
- * app instances (e.g., in tests) would share state.
377
+ * Propagates request IDs for distributed tracing.
378
+ * - Accepts incoming x-request-id header
379
+ * - Generates UUID if not provided
380
+ * - Attaches to request.id and response header
423
381
  *
424
382
  * @example
425
- * import { arcCorePlugin } from '@classytic/arc';
383
+ * import { requestIdPlugin } from '@classytic/arc';
426
384
  *
427
- * const app = Fastify();
428
- * await app.register(arcCorePlugin);
385
+ * await fastify.register(requestIdPlugin);
429
386
  *
430
- * // Now use instance-scoped hooks
431
- * app.arc.hooks.before('product', 'create', async (ctx) => {
432
- * ctx.data.slug = slugify(ctx.data.name);
387
+ * // In handlers, access via request.id
388
+ * fastify.get('/', async (request) => {
389
+ * console.log(request.id); // UUID
433
390
  * });
434
391
  */
435
- const arcCorePlugin = async (fastify, opts = {}) => {
436
- const { emitEvents = true, hookSystem, registry } = opts;
437
- const actualHookSystem = hookSystem ?? new HookSystem();
438
- const actualRegistry = registry ?? new ResourceRegistry();
439
- fastify.decorate("arc", {
440
- hooks: actualHookSystem,
441
- registry: actualRegistry,
442
- emitEvents,
443
- externalOpenApiPaths: [],
444
- plugins: /* @__PURE__ */ new Map()
445
- });
446
- fastify.addHook("onRequest", (request, _reply, done) => {
447
- const store = {
448
- requestId: request.id,
449
- startTime: performance.now()
450
- };
451
- requestContext.storage.run(store, done);
452
- });
453
- fastify.addHook("preHandler", (request, _reply, done) => {
454
- const store = requestContext.get();
455
- if (store) {
456
- store.user = request.user ?? null;
457
- store.organizationId = request.scope?.kind === "member" ? request.scope.organizationId : request.scope?.kind === "elevated" ? request.scope.organizationId : void 0;
458
- }
459
- done();
460
- });
461
- if (emitEvents) {
462
- const eventOperations = MUTATION_OPERATIONS;
463
- for (const operation of eventOperations) actualHookSystem.after("*", operation, async (ctx) => {
464
- if (!hasEvents(fastify)) return;
465
- const store = requestContext.get();
466
- const eventType = `${ctx.resource}.${operation}d`;
467
- const userId = ctx.user?.id ?? ctx.user?._id;
468
- const organizationId = ctx.context?._scope ? getOrgId(ctx.context._scope) : void 0;
469
- const payload = {
470
- resource: ctx.resource,
471
- operation: ctx.operation,
472
- data: ctx.result,
473
- userId,
474
- organizationId,
475
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
476
- };
477
- try {
478
- await fastify.events.publish(eventType, payload, {
479
- correlationId: store?.requestId,
480
- resource: ctx.resource,
481
- resourceId: extractId(ctx.result),
482
- userId: userId ? String(userId) : void 0,
483
- organizationId
484
- });
485
- } catch (error) {
486
- fastify.log?.warn?.({
487
- eventType,
488
- error
489
- }, "Failed to emit event");
490
- }
491
- });
492
- }
493
- fastify.addHook("onReady", async () => {
494
- if (!hasEvents(fastify)) return;
495
- try {
496
- await fastify.events.publish("arc.ready", {
497
- resources: actualRegistry.getAll().length,
498
- hooks: actualHookSystem.getAll().length,
499
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
500
- });
501
- } catch {}
392
+ const requestIdPlugin = async (fastify, opts = {}) => {
393
+ const { header = "x-request-id", generator = randomUUID, setResponseHeader = true } = opts;
394
+ if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
395
+ fastify.addHook("onRequest", async (request) => {
396
+ const incomingId = request.headers[header];
397
+ const sanitized = typeof incomingId === "string" ? incomingId.trim() : "";
398
+ const requestId = sanitized.length > 0 && sanitized.length <= 128 && /^[\w.:-]+$/.test(sanitized) ? sanitized : generator();
399
+ request.id = requestId;
400
+ request.requestId = requestId;
502
401
  });
503
- fastify.addHook("onClose", async () => {
504
- actualHookSystem.clear();
505
- actualRegistry._clear();
402
+ if (setResponseHeader) fastify.addHook("onSend", async (request, reply) => {
403
+ reply.header(header, request.requestId);
506
404
  });
507
- fastify.log?.debug?.("Arc core plugin enabled (instance-scoped hooks & registry)");
405
+ fastify.log?.debug?.("Request ID plugin registered");
508
406
  };
509
- /** Extract document ID from a result (handles Mongoose docs and plain objects) */
510
- function extractId(doc) {
511
- if (!doc || typeof doc !== "object") return void 0;
512
- const d = doc;
513
- const rawId = d._id ?? d.id;
514
- return rawId ? String(rawId) : void 0;
515
- }
516
- var arcCorePlugin_default = fp(arcCorePlugin, {
517
- name: "arc-core",
407
+ var requestId_default = fp(requestIdPlugin, {
408
+ name: "arc-request-id",
518
409
  fastify: "5.x"
519
410
  });
520
-
521
411
  //#endregion
522
- export { arcCorePlugin_default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, caching_default as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, gracefulShutdown_default as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, health_default as healthPlugin, healthPlugin as healthPluginFn, requestId_default as requestIdPlugin, requestIdPlugin as requestIdPluginFn, sse_default as ssePlugin, ssePlugin as ssePluginFn };
412
+ export { arcCorePlugin_default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, caching_default as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, 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, requestId_default as requestIdPlugin, requestIdPlugin as requestIdPluginFn, sse_default as ssePlugin, ssePlugin as ssePluginFn, versioning_default as versioningPlugin, versioningPlugin as versioningPluginFn };
@@ -1,71 +1,7 @@
1
- import { t as hasEvents } from "../typeGuards-DwxA1t_L.mjs";
1
+ import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
2
2
  import fp from "fastify-plugin";
3
-
4
3
  //#region src/plugins/response-cache.ts
5
4
  /**
6
- * Response Cache Plugin for Arc
7
- *
8
- * In-memory LRU/TTL response cache that sits in front of your database.
9
- * Caches serialized responses for GET requests, dramatically reducing DB load
10
- * for frequently accessed resources.
11
- *
12
- * Features:
13
- * - LRU eviction with configurable max entries
14
- * - Per-route TTL configuration
15
- * - Automatic invalidation on mutations (POST/PUT/PATCH/DELETE)
16
- * - Manual invalidation via `fastify.responseCache.invalidate()`
17
- * - Cache stats endpoint for monitoring
18
- * - Zero external deps — pure in-memory, serverless-safe
19
- *
20
- * NOTE: This cache is per-instance (in-memory). In multi-instance deployments,
21
- * each instance maintains its own cache. For cross-instance invalidation,
22
- * wire `fastify.responseCache.invalidate()` to your event bus manually.
23
- *
24
- * ## Auth Safety
25
- *
26
- * The cache check runs as a **route-level middleware** (`responseCache.middleware`)
27
- * that must be wired AFTER authentication in the preHandler chain. Arc's
28
- * `createCrudRouter` does this automatically. For custom routes, wire it
29
- * manually:
30
- *
31
- * ```typescript
32
- * fastify.get('/data', {
33
- * preHandler: [fastify.authenticate, fastify.responseCache.middleware],
34
- * }, handler);
35
- * ```
36
- *
37
- * This ensures cached responses are never served before auth validates the
38
- * caller's identity. The default cache key includes `userId` and `orgId`
39
- * to prevent cross-caller data leaks.
40
- *
41
- * This is a SEPARATE subpath import — only loaded when explicitly used:
42
- * import { responseCachePlugin } from '@classytic/arc/plugins/response-cache';
43
- *
44
- * @example
45
- * ```typescript
46
- * import { responseCachePlugin } from '@classytic/arc/plugins/response-cache';
47
- *
48
- * await fastify.register(responseCachePlugin, {
49
- * maxEntries: 1000,
50
- * defaultTTL: 30, // 30 seconds
51
- * rules: [
52
- * { match: '/api/products', ttl: 120 }, // 2 min for products
53
- * { match: '/api/categories', ttl: 300 }, // 5 min for categories
54
- * { match: '/api/users', ttl: 0 }, // never cache users
55
- * ],
56
- * invalidateOn: ['POST', 'PUT', 'PATCH', 'DELETE'],
57
- * });
58
- *
59
- * // Manual invalidation
60
- * fastify.responseCache.invalidate('/api/products');
61
- * fastify.responseCache.invalidateAll();
62
- *
63
- * // Stats
64
- * const stats = fastify.responseCache.stats();
65
- * // { entries: 42, hits: 1250, misses: 180, hitRate: 0.87, evictions: 5 }
66
- * ```
67
- */
68
- /**
69
5
  * Simple LRU cache using Map iteration order.
70
6
  * Map in JS preserves insertion order — we re-insert on access to make it LRU.
71
7
  */
@@ -191,7 +127,7 @@ const responseCachePluginImpl = async (fastify, opts = {}) => {
191
127
  const segments = path.split("/").filter(Boolean);
192
128
  const lastSegment = segments[segments.length - 1];
193
129
  if (segments.length >= 2 && lastSegment != null && /^[0-9a-f]{8,}$|^\d+$/.test(lastSegment)) {
194
- const resourceRoot = "/" + segments.slice(0, -1).join("/");
130
+ const resourceRoot = `/${segments.slice(0, -1).join("/")}`;
195
131
  cache.invalidatePrefix(resourceRoot);
196
132
  cache.invalidatePrefix(path);
197
133
  } else cache.invalidatePrefix(path);
@@ -227,7 +163,7 @@ const responseCachePluginImpl = async (fastify, opts = {}) => {
227
163
  const contentType = reply.getHeader("content-type");
228
164
  if (contentType) headers["content-type"] = String(contentType);
229
165
  const etag = reply.getHeader("etag");
230
- if (etag) headers["etag"] = String(etag);
166
+ if (etag) headers.etag = String(etag);
231
167
  cache.set(key, {
232
168
  body,
233
169
  statusCode,
@@ -278,6 +214,5 @@ const responseCachePlugin = fp(responseCachePluginImpl, {
278
214
  name: "arc-response-cache",
279
215
  fastify: "5.x"
280
216
  });
281
-
282
217
  //#endregion
283
- export { responseCachePlugin as default, responseCachePlugin };
218
+ export { responseCachePlugin as default, responseCachePlugin };
@@ -1,2 +1,2 @@
1
- import { a as traced, i as isTracingAvailable, n as _default, r as createSpan, t as TracingOptions } from "../tracing-8CEbhF0w.mjs";
1
+ import { a as traced, i as isTracingAvailable, n as _default, r as createSpan, t as TracingOptions } from "../tracing-bz_U4EM1.mjs";
2
2
  export { type TracingOptions, createSpan, isTracingAvailable, traced, _default as tracingPlugin };