@classytic/arc 1.1.0 → 2.1.3

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 (200) hide show
  1. package/README.md +247 -794
  2. package/bin/arc.js +91 -52
  3. package/dist/EventTransport-BkUDYZEb.d.mts +99 -0
  4. package/dist/HookSystem-BsGV-j2l.mjs +404 -0
  5. package/dist/ResourceRegistry-7Ic20ZMw.mjs +249 -0
  6. package/dist/adapters/index.d.mts +5 -0
  7. package/dist/adapters/index.mjs +3 -0
  8. package/dist/audit/index.d.mts +81 -0
  9. package/dist/audit/index.mjs +275 -0
  10. package/dist/audit/mongodb.d.mts +5 -0
  11. package/dist/audit/mongodb.mjs +3 -0
  12. package/dist/audited-CGdLiSlE.mjs +140 -0
  13. package/dist/auth/index.d.mts +188 -0
  14. package/dist/auth/index.mjs +1096 -0
  15. package/dist/auth/redis-session.d.mts +43 -0
  16. package/dist/auth/redis-session.mjs +75 -0
  17. package/dist/betterAuthOpenApi-DjWDddNc.mjs +249 -0
  18. package/dist/cache/index.d.mts +145 -0
  19. package/dist/cache/index.mjs +91 -0
  20. package/dist/caching-GSDJcA6-.mjs +93 -0
  21. package/dist/chunk-C7Uep-_p.mjs +20 -0
  22. package/dist/circuitBreaker-DYhWBW_D.mjs +1096 -0
  23. package/dist/cli/commands/describe.d.mts +18 -0
  24. package/dist/cli/commands/describe.mjs +238 -0
  25. package/dist/cli/commands/docs.d.mts +13 -0
  26. package/dist/cli/commands/docs.mjs +52 -0
  27. package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -2
  28. package/dist/cli/commands/generate.mjs +357 -0
  29. package/dist/cli/commands/{init.d.ts → init.d.mts} +11 -8
  30. package/dist/cli/commands/{init.js → init.mjs} +807 -617
  31. package/dist/cli/commands/introspect.d.mts +10 -0
  32. package/dist/cli/commands/introspect.mjs +75 -0
  33. package/dist/cli/index.d.mts +16 -0
  34. package/dist/cli/index.mjs +156 -0
  35. package/dist/constants-DdXFXQtN.mjs +84 -0
  36. package/dist/core/index.d.mts +5 -0
  37. package/dist/core/index.mjs +4 -0
  38. package/dist/createApp-D2D5XXaV.mjs +559 -0
  39. package/dist/defineResource-PXzSJ15_.mjs +2197 -0
  40. package/dist/discovery/index.d.mts +46 -0
  41. package/dist/discovery/index.mjs +109 -0
  42. package/dist/docs/index.d.mts +162 -0
  43. package/dist/docs/index.mjs +74 -0
  44. package/dist/elevation-DGo5shaX.d.mts +87 -0
  45. package/dist/elevation-DSTbVvYj.mjs +113 -0
  46. package/dist/errorHandler-C3GY3_ow.mjs +108 -0
  47. package/dist/errorHandler-CW3OOeYq.d.mts +72 -0
  48. package/dist/errors-DAWRdiYP.d.mts +124 -0
  49. package/dist/errors-DBANPbGr.mjs +211 -0
  50. package/dist/eventPlugin-BEOvaDqo.mjs +229 -0
  51. package/dist/eventPlugin-H6wDDjGO.d.mts +124 -0
  52. package/dist/events/index.d.mts +53 -0
  53. package/dist/events/index.mjs +51 -0
  54. package/dist/events/transports/redis-stream-entry.d.mts +2 -0
  55. package/dist/events/transports/redis-stream-entry.mjs +177 -0
  56. package/dist/events/transports/redis.d.mts +76 -0
  57. package/dist/events/transports/redis.mjs +124 -0
  58. package/dist/externalPaths-SyPF2tgK.d.mts +50 -0
  59. package/dist/factory/index.d.mts +63 -0
  60. package/dist/factory/index.mjs +3 -0
  61. package/dist/fastifyAdapter-C8DlE0YH.d.mts +216 -0
  62. package/dist/fields-Bi_AVKSo.d.mts +109 -0
  63. package/dist/fields-CTd_CrKr.mjs +114 -0
  64. package/dist/hooks/index.d.mts +4 -0
  65. package/dist/hooks/index.mjs +3 -0
  66. package/dist/idempotency/index.d.mts +96 -0
  67. package/dist/idempotency/index.mjs +319 -0
  68. package/dist/idempotency/mongodb.d.mts +2 -0
  69. package/dist/idempotency/mongodb.mjs +114 -0
  70. package/dist/idempotency/redis.d.mts +2 -0
  71. package/dist/idempotency/redis.mjs +103 -0
  72. package/dist/index.d.mts +260 -0
  73. package/dist/index.mjs +104 -0
  74. package/dist/integrations/event-gateway.d.mts +46 -0
  75. package/dist/integrations/event-gateway.mjs +43 -0
  76. package/dist/integrations/index.d.mts +5 -0
  77. package/dist/integrations/index.mjs +1 -0
  78. package/dist/integrations/jobs.d.mts +103 -0
  79. package/dist/integrations/jobs.mjs +123 -0
  80. package/dist/integrations/streamline.d.mts +60 -0
  81. package/dist/integrations/streamline.mjs +125 -0
  82. package/dist/integrations/websocket.d.mts +82 -0
  83. package/dist/integrations/websocket.mjs +288 -0
  84. package/dist/interface-CSNjltAc.d.mts +77 -0
  85. package/dist/interface-DTbsvIWe.d.mts +54 -0
  86. package/dist/interface-e9XfSsUV.d.mts +1097 -0
  87. package/dist/introspectionPlugin-B3JkrjwU.mjs +53 -0
  88. package/dist/keys-DhqDRxv3.mjs +42 -0
  89. package/dist/logger-ByrvQWZO.mjs +78 -0
  90. package/dist/memory-B2v7KrCB.mjs +143 -0
  91. package/dist/migrations/index.d.mts +156 -0
  92. package/dist/migrations/index.mjs +260 -0
  93. package/dist/mongodb-ClykrfGo.d.mts +118 -0
  94. package/dist/mongodb-DNKEExbf.mjs +93 -0
  95. package/dist/mongodb-Dg8O_gvd.d.mts +71 -0
  96. package/dist/openapi-9nB_kiuR.mjs +525 -0
  97. package/dist/org/index.d.mts +68 -0
  98. package/dist/org/index.mjs +513 -0
  99. package/dist/org/types.d.mts +82 -0
  100. package/dist/org/types.mjs +1 -0
  101. package/dist/permissions/index.d.mts +278 -0
  102. package/dist/permissions/index.mjs +579 -0
  103. package/dist/plugins/index.d.mts +172 -0
  104. package/dist/plugins/index.mjs +522 -0
  105. package/dist/plugins/response-cache.d.mts +87 -0
  106. package/dist/plugins/response-cache.mjs +283 -0
  107. package/dist/plugins/tracing-entry.d.mts +2 -0
  108. package/dist/plugins/tracing-entry.mjs +185 -0
  109. package/dist/pluralize-CM-jZg7p.mjs +86 -0
  110. package/dist/policies/{index.d.ts → index.d.mts} +204 -170
  111. package/dist/policies/index.mjs +321 -0
  112. package/dist/presets/{index.d.ts → index.d.mts} +62 -131
  113. package/dist/presets/index.mjs +143 -0
  114. package/dist/presets/multiTenant.d.mts +24 -0
  115. package/dist/presets/multiTenant.mjs +113 -0
  116. package/dist/presets-BTeYbw7h.d.mts +57 -0
  117. package/dist/presets-CeFtfDR8.mjs +119 -0
  118. package/dist/prisma-C3iornoK.d.mts +274 -0
  119. package/dist/prisma-DJbMt3yf.mjs +627 -0
  120. package/dist/queryCachePlugin-B6R0d4av.mjs +138 -0
  121. package/dist/queryCachePlugin-Q6SYuHZ6.d.mts +71 -0
  122. package/dist/redis-UwjEp8Ea.d.mts +49 -0
  123. package/dist/redis-stream-CBg0upHI.d.mts +103 -0
  124. package/dist/registry/index.d.mts +11 -0
  125. package/dist/registry/index.mjs +4 -0
  126. package/dist/requestContext-xi6OKBL-.mjs +55 -0
  127. package/dist/schemaConverter-Dtg0Kt9T.mjs +98 -0
  128. package/dist/schemas/index.d.mts +63 -0
  129. package/dist/schemas/index.mjs +82 -0
  130. package/dist/scope/index.d.mts +21 -0
  131. package/dist/scope/index.mjs +65 -0
  132. package/dist/sessionManager-D_iEHjQl.d.mts +186 -0
  133. package/dist/sse-DkqQ1uxb.mjs +123 -0
  134. package/dist/testing/index.d.mts +907 -0
  135. package/dist/testing/index.mjs +1976 -0
  136. package/dist/tracing-8CEbhF0w.d.mts +70 -0
  137. package/dist/typeGuards-DwxA1t_L.mjs +9 -0
  138. package/dist/types/index.d.mts +946 -0
  139. package/dist/types/index.mjs +14 -0
  140. package/dist/types-B0dhNrnd.d.mts +445 -0
  141. package/dist/types-Beqn1Un7.mjs +38 -0
  142. package/dist/types-DelU6kln.mjs +25 -0
  143. package/dist/types-RLkFVgaw.d.mts +101 -0
  144. package/dist/utils/index.d.mts +747 -0
  145. package/dist/utils/index.mjs +6 -0
  146. package/package.json +194 -68
  147. package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
  148. package/dist/adapters/index.d.ts +0 -237
  149. package/dist/adapters/index.js +0 -668
  150. package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
  151. package/dist/audit/index.d.ts +0 -195
  152. package/dist/audit/index.js +0 -319
  153. package/dist/auth/index.d.ts +0 -47
  154. package/dist/auth/index.js +0 -174
  155. package/dist/cli/commands/docs.d.ts +0 -11
  156. package/dist/cli/commands/docs.js +0 -474
  157. package/dist/cli/commands/generate.js +0 -334
  158. package/dist/cli/commands/introspect.d.ts +0 -8
  159. package/dist/cli/commands/introspect.js +0 -338
  160. package/dist/cli/index.d.ts +0 -4
  161. package/dist/cli/index.js +0 -3269
  162. package/dist/core/index.d.ts +0 -220
  163. package/dist/core/index.js +0 -2786
  164. package/dist/createApp-Ce9wl8W9.d.ts +0 -77
  165. package/dist/docs/index.d.ts +0 -166
  166. package/dist/docs/index.js +0 -658
  167. package/dist/errors-8WIxGS_6.d.ts +0 -122
  168. package/dist/events/index.d.ts +0 -117
  169. package/dist/events/index.js +0 -89
  170. package/dist/factory/index.d.ts +0 -38
  171. package/dist/factory/index.js +0 -1652
  172. package/dist/hooks/index.d.ts +0 -4
  173. package/dist/hooks/index.js +0 -199
  174. package/dist/idempotency/index.d.ts +0 -323
  175. package/dist/idempotency/index.js +0 -500
  176. package/dist/index-B4t03KQ0.d.ts +0 -1366
  177. package/dist/index.d.ts +0 -135
  178. package/dist/index.js +0 -4756
  179. package/dist/migrations/index.d.ts +0 -185
  180. package/dist/migrations/index.js +0 -274
  181. package/dist/org/index.d.ts +0 -129
  182. package/dist/org/index.js +0 -220
  183. package/dist/permissions/index.d.ts +0 -144
  184. package/dist/permissions/index.js +0 -103
  185. package/dist/plugins/index.d.ts +0 -46
  186. package/dist/plugins/index.js +0 -1069
  187. package/dist/policies/index.js +0 -196
  188. package/dist/presets/index.js +0 -384
  189. package/dist/presets/multiTenant.d.ts +0 -39
  190. package/dist/presets/multiTenant.js +0 -112
  191. package/dist/registry/index.d.ts +0 -16
  192. package/dist/registry/index.js +0 -253
  193. package/dist/testing/index.d.ts +0 -618
  194. package/dist/testing/index.js +0 -48020
  195. package/dist/types/index.d.ts +0 -4
  196. package/dist/types/index.js +0 -8
  197. package/dist/types-B99TBmFV.d.ts +0 -76
  198. package/dist/types-BvckRbs2.d.ts +0 -143
  199. package/dist/utils/index.d.ts +0 -679
  200. package/dist/utils/index.js +0 -931
@@ -0,0 +1,123 @@
1
+ //#region src/integrations/jobs.ts
2
+ /**
3
+ * Define a background job with typed data and configuration.
4
+ *
5
+ * @example
6
+ * const processImage = defineJob({
7
+ * name: 'process-image',
8
+ * handler: async (data: { url: string; width: number }) => {
9
+ * return await sharp(data.url).resize(data.width).toBuffer();
10
+ * },
11
+ * retries: 3,
12
+ * timeout: 60000,
13
+ * });
14
+ */
15
+ function defineJob(definition) {
16
+ return definition;
17
+ }
18
+ const jobsPluginImpl = async (fastify, options) => {
19
+ const { connection, jobs, prefix = "/jobs", bridgeEvents = true, defaults = {} } = options;
20
+ let Queue;
21
+ let Worker;
22
+ try {
23
+ const bullmq = await import("bullmq");
24
+ Queue = bullmq.Queue;
25
+ Worker = bullmq.Worker;
26
+ } catch {
27
+ throw new Error("@classytic/arc/integrations/jobs requires \"bullmq\" package.\nInstall it: npm install bullmq");
28
+ }
29
+ const queues = /* @__PURE__ */ new Map();
30
+ const workers = /* @__PURE__ */ new Map();
31
+ for (const job of jobs) {
32
+ const queueName = job.name;
33
+ const queue = new Queue(queueName, { connection });
34
+ queues.set(queueName, queue);
35
+ const worker = new Worker(queueName, async (bullJob) => {
36
+ const meta = {
37
+ jobId: bullJob.id,
38
+ attemptsMade: bullJob.attemptsMade,
39
+ timestamp: Date.now()
40
+ };
41
+ const result = await job.handler(bullJob.data, meta);
42
+ if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`job.${queueName}.completed`, {
43
+ jobId: bullJob.id,
44
+ data: bullJob.data,
45
+ result
46
+ });
47
+ return result;
48
+ }, {
49
+ connection,
50
+ concurrency: job.concurrency ?? 1,
51
+ limiter: job.rateLimit ? {
52
+ max: job.rateLimit.max,
53
+ duration: job.rateLimit.duration
54
+ } : void 0
55
+ });
56
+ worker.on("failed", async (bullJob, error) => {
57
+ if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`job.${queueName}.failed`, {
58
+ jobId: bullJob?.id,
59
+ data: bullJob?.data,
60
+ error: error.message,
61
+ attemptsMade: bullJob?.attemptsMade
62
+ });
63
+ });
64
+ workers.set(queueName, worker);
65
+ }
66
+ const dispatcher = {
67
+ async dispatch(name, data, opts = {}) {
68
+ const queue = queues.get(name);
69
+ if (!queue) throw new Error(`Job queue '${name}' not registered. Available: ${Array.from(queues.keys()).join(", ")}`);
70
+ const jobDef = jobs.find((j) => j.name === name);
71
+ return { jobId: (await queue.add(name, data, {
72
+ delay: opts.delay,
73
+ priority: opts.priority,
74
+ jobId: opts.jobId,
75
+ removeOnComplete: opts.removeOnComplete ?? defaults.removeOnComplete ?? 100,
76
+ removeOnFail: opts.removeOnFail ?? defaults.removeOnFail ?? 500,
77
+ attempts: jobDef?.retries ?? defaults.retries ?? 3,
78
+ backoff: jobDef?.backoff ?? defaults.backoff ?? {
79
+ type: "exponential",
80
+ delay: 1e3
81
+ }
82
+ })).id };
83
+ },
84
+ getQueue(name) {
85
+ return queues.get(name) ?? null;
86
+ },
87
+ async getStats() {
88
+ const stats = {};
89
+ for (const [name, queue] of queues) {
90
+ const counts = await queue.getJobCounts();
91
+ stats[name] = {
92
+ waiting: counts.waiting ?? 0,
93
+ active: counts.active ?? 0,
94
+ completed: counts.completed ?? 0,
95
+ failed: counts.failed ?? 0,
96
+ delayed: counts.delayed ?? 0
97
+ };
98
+ }
99
+ return stats;
100
+ },
101
+ async close() {
102
+ const closePromises = [];
103
+ for (const worker of workers.values()) closePromises.push(worker.close());
104
+ for (const queue of queues.values()) closePromises.push(queue.close());
105
+ await Promise.all(closePromises);
106
+ }
107
+ };
108
+ if (!fastify.hasDecorator("jobs")) fastify.decorate("jobs", dispatcher);
109
+ fastify.get(`${prefix}/stats`, async () => {
110
+ return {
111
+ success: true,
112
+ data: await dispatcher.getStats()
113
+ };
114
+ });
115
+ fastify.addHook("onClose", async () => {
116
+ await dispatcher.close();
117
+ });
118
+ };
119
+ /** Pluggable BullMQ job queue integration for Arc */
120
+ const jobsPlugin = jobsPluginImpl;
121
+
122
+ //#endregion
123
+ export { jobsPlugin as default, jobsPlugin, defineJob };
@@ -0,0 +1,60 @@
1
+ import { FastifyPluginAsync } from "fastify";
2
+
3
+ //#region src/integrations/streamline.d.ts
4
+ /** Minimal workflow interface — matches @classytic/streamline's createWorkflow() return */
5
+ interface WorkflowLike {
6
+ definition: {
7
+ id: string;
8
+ name?: string;
9
+ steps: Record<string, unknown>;
10
+ };
11
+ engine: {
12
+ start(input: unknown, meta?: unknown): Promise<WorkflowRunLike>;
13
+ execute(runId: string): Promise<WorkflowRunLike>;
14
+ resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;
15
+ cancel(runId: string): Promise<WorkflowRunLike>;
16
+ pause?(runId: string): Promise<WorkflowRunLike>;
17
+ rewindTo?(runId: string, stepId: string): Promise<WorkflowRunLike>;
18
+ get(runId: string): Promise<WorkflowRunLike | null>;
19
+ shutdown?(): void;
20
+ };
21
+ start(input: unknown, meta?: unknown): Promise<WorkflowRunLike>;
22
+ resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;
23
+ cancel(runId: string): Promise<WorkflowRunLike>;
24
+ get(runId: string): Promise<WorkflowRunLike | null>;
25
+ shutdown?(): void;
26
+ }
27
+ interface WorkflowRunLike {
28
+ _id: string;
29
+ workflowId: string;
30
+ status: string;
31
+ context?: unknown;
32
+ input?: unknown;
33
+ steps?: Record<string, unknown>;
34
+ error?: unknown;
35
+ createdAt?: Date;
36
+ updatedAt?: Date;
37
+ [key: string]: unknown;
38
+ }
39
+ interface StreamlinePluginOptions {
40
+ /** Array of workflows created with createWorkflow() */
41
+ workflows: WorkflowLike[];
42
+ /** URL prefix for workflow endpoints (default: '/workflows') */
43
+ prefix?: string;
44
+ /** Require authentication for all workflow endpoints (default: true) */
45
+ auth?: boolean;
46
+ /** Connect workflow events to Arc's event bus (default: true) */
47
+ bridgeEvents?: boolean;
48
+ /** Custom permission check for workflow operations */
49
+ permissions?: {
50
+ start?: (request: unknown) => boolean | Promise<boolean>;
51
+ resume?: (request: unknown) => boolean | Promise<boolean>;
52
+ cancel?: (request: unknown) => boolean | Promise<boolean>;
53
+ list?: (request: unknown) => boolean | Promise<boolean>;
54
+ get?: (request: unknown) => boolean | Promise<boolean>;
55
+ };
56
+ }
57
+ /** Pluggable streamline integration for Arc */
58
+ declare const streamlinePlugin: FastifyPluginAsync<StreamlinePluginOptions>;
59
+ //#endregion
60
+ export { StreamlinePluginOptions, WorkflowLike, WorkflowRunLike, streamlinePlugin as default, streamlinePlugin };
@@ -0,0 +1,125 @@
1
+ //#region src/integrations/streamline.ts
2
+ const streamlinePluginImpl = async (fastify, options) => {
3
+ const { workflows, prefix = "/workflows", auth = true, bridgeEvents = true, permissions: perms } = options;
4
+ const registry = /* @__PURE__ */ new Map();
5
+ for (const wf of workflows) {
6
+ const id = wf.definition.id;
7
+ if (registry.has(id)) throw new Error(`Duplicate workflow ID: '${id}'`);
8
+ registry.set(id, wf);
9
+ }
10
+ if (!fastify.hasDecorator("workflows")) fastify.decorate("workflows", registry);
11
+ if (!fastify.hasDecorator("getWorkflow")) fastify.decorate("getWorkflow", (id) => registry.get(id) ?? null);
12
+ const authPreHandler = auth && typeof fastify.authenticate === "function" ? [fastify.authenticate] : [];
13
+ const checkPerm = async (op, request) => {
14
+ const check = perms?.[op];
15
+ if (!check) return true;
16
+ return check(request);
17
+ };
18
+ for (const [id, wf] of registry) {
19
+ const routePrefix = `${prefix}/${id}`;
20
+ fastify.post(`${routePrefix}/start`, { preHandler: authPreHandler }, async (request, reply) => {
21
+ if (!await checkPerm("start", request)) return reply.status(403).send({
22
+ success: false,
23
+ error: "Forbidden"
24
+ });
25
+ const { input, meta } = request.body ?? {};
26
+ const run = await wf.start(input, meta);
27
+ if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.started`, {
28
+ runId: run._id,
29
+ workflowId: id,
30
+ status: run.status
31
+ });
32
+ return reply.status(201).send({
33
+ success: true,
34
+ data: run
35
+ });
36
+ });
37
+ fastify.get(`${routePrefix}/runs/:runId`, { preHandler: authPreHandler }, async (request, reply) => {
38
+ if (!await checkPerm("get", request)) return reply.status(403).send({
39
+ success: false,
40
+ error: "Forbidden"
41
+ });
42
+ const { runId } = request.params;
43
+ const run = await wf.get(runId);
44
+ if (!run) return reply.status(404).send({
45
+ success: false,
46
+ error: "Workflow run not found"
47
+ });
48
+ return {
49
+ success: true,
50
+ data: run
51
+ };
52
+ });
53
+ fastify.post(`${routePrefix}/runs/:runId/resume`, { preHandler: authPreHandler }, async (request, reply) => {
54
+ if (!await checkPerm("resume", request)) return reply.status(403).send({
55
+ success: false,
56
+ error: "Forbidden"
57
+ });
58
+ const { runId } = request.params;
59
+ const { payload } = request.body ?? {};
60
+ const run = await wf.resume(runId, payload);
61
+ if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.resumed`, {
62
+ runId: run._id,
63
+ workflowId: id,
64
+ status: run.status
65
+ });
66
+ return {
67
+ success: true,
68
+ data: run
69
+ };
70
+ });
71
+ fastify.post(`${routePrefix}/runs/:runId/cancel`, { preHandler: authPreHandler }, async (request, reply) => {
72
+ if (!await checkPerm("cancel", request)) return reply.status(403).send({
73
+ success: false,
74
+ error: "Forbidden"
75
+ });
76
+ const { runId } = request.params;
77
+ const run = await wf.cancel(runId);
78
+ if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.cancelled`, {
79
+ runId: run._id,
80
+ workflowId: id
81
+ });
82
+ return {
83
+ success: true,
84
+ data: run
85
+ };
86
+ });
87
+ if (wf.engine.pause) fastify.post(`${routePrefix}/runs/:runId/pause`, { preHandler: authPreHandler }, async (request, reply) => {
88
+ const { runId } = request.params;
89
+ return {
90
+ success: true,
91
+ data: await wf.engine.pause(runId)
92
+ };
93
+ });
94
+ if (wf.engine.rewindTo) fastify.post(`${routePrefix}/runs/:runId/rewind`, { preHandler: authPreHandler }, async (request, reply) => {
95
+ const { runId } = request.params;
96
+ const { stepId } = request.body ?? {};
97
+ if (!stepId) return reply.status(400).send({
98
+ success: false,
99
+ error: "stepId is required"
100
+ });
101
+ return {
102
+ success: true,
103
+ data: await wf.engine.rewindTo(runId, stepId)
104
+ };
105
+ });
106
+ }
107
+ fastify.get(prefix, { preHandler: authPreHandler }, async () => {
108
+ return {
109
+ success: true,
110
+ data: Array.from(registry.entries()).map(([id, wf]) => ({
111
+ id,
112
+ name: wf.definition.name ?? id,
113
+ steps: Object.keys(wf.definition.steps)
114
+ }))
115
+ };
116
+ });
117
+ fastify.addHook("onClose", async () => {
118
+ for (const wf of registry.values()) wf.shutdown?.();
119
+ });
120
+ };
121
+ /** Pluggable streamline integration for Arc */
122
+ const streamlinePlugin = streamlinePluginImpl;
123
+
124
+ //#endregion
125
+ export { streamlinePlugin as default, streamlinePlugin };
@@ -0,0 +1,82 @@
1
+ import { FastifyPluginAsync } from "fastify";
2
+
3
+ //#region src/integrations/websocket.d.ts
4
+ interface WebSocketClient {
5
+ id: string;
6
+ socket: {
7
+ send(data: string): void;
8
+ close(): void;
9
+ readyState: number;
10
+ };
11
+ subscriptions: Set<string>;
12
+ userId?: string;
13
+ organizationId?: string;
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+ interface WebSocketMessage {
17
+ type: string;
18
+ resource?: string;
19
+ channel?: string;
20
+ data?: unknown;
21
+ }
22
+ interface WebSocketPluginOptions {
23
+ /** WebSocket endpoint path (default: '/ws') */
24
+ path?: string;
25
+ /** Require authentication for WebSocket connections (default: true) */
26
+ auth?: boolean;
27
+ /** Resources to auto-broadcast CRUD events for */
28
+ resources?: string[];
29
+ /** Heartbeat interval in ms (default: 30000). Set 0 to disable. */
30
+ heartbeatInterval?: number;
31
+ /** Custom authentication function for WebSocket upgrade */
32
+ authenticate?: (request: unknown) => Promise<{
33
+ userId?: string;
34
+ organizationId?: string;
35
+ } | null>;
36
+ /** Max clients per resource subscription (default: 10000) */
37
+ maxClientsPerRoom?: number;
38
+ /**
39
+ * Expose a stats endpoint at `{path}/stats`.
40
+ * - `false` (default): stats endpoint is not registered
41
+ * - `true`: registered without auth
42
+ * - `'authenticated'`: guarded by `fastify.authenticate` if available
43
+ */
44
+ exposeStats?: boolean | "authenticated";
45
+ /**
46
+ * Authorize room subscriptions. Return true to allow, false to deny.
47
+ * Called before every subscribe. If not provided, all rooms are allowed.
48
+ */
49
+ roomPolicy?: (client: WebSocketClient, room: string) => boolean | Promise<boolean>;
50
+ /** Maximum message size in bytes from client (default: 16384 = 16KB). Messages exceeding this are dropped. */
51
+ maxMessageBytes?: number;
52
+ /** Maximum subscriptions per client (default: 100). Prevents resource exhaustion. */
53
+ maxSubscriptionsPerClient?: number;
54
+ /** Custom message handler */
55
+ onMessage?: (client: WebSocketClient, message: WebSocketMessage) => void | Promise<void>;
56
+ /** Called when a client connects */
57
+ onConnect?: (client: WebSocketClient) => void | Promise<void>;
58
+ /** Called when a client disconnects */
59
+ onDisconnect?: (client: WebSocketClient) => void | Promise<void>;
60
+ }
61
+ declare class RoomManager {
62
+ private rooms;
63
+ private clients;
64
+ private maxPerRoom;
65
+ constructor(maxPerRoom?: number);
66
+ addClient(client: WebSocketClient): void;
67
+ removeClient(clientId: string): void;
68
+ subscribe(clientId: string, room: string): boolean;
69
+ unsubscribe(clientId: string, room: string): void;
70
+ broadcast(room: string, message: string, excludeClientId?: string): void;
71
+ broadcastToOrg(organizationId: string, room: string, message: string): void;
72
+ getClient(clientId: string): WebSocketClient | undefined;
73
+ getStats(): {
74
+ clients: number;
75
+ rooms: number;
76
+ subscriptions: Record<string, number>;
77
+ };
78
+ }
79
+ /** Pluggable WebSocket integration for Arc */
80
+ declare const websocketPlugin: FastifyPluginAsync<WebSocketPluginOptions>;
81
+ //#endregion
82
+ export { RoomManager, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, websocketPlugin as default, websocketPlugin };
@@ -0,0 +1,288 @@
1
+ import fp from "fastify-plugin";
2
+
3
+ //#region src/integrations/websocket.ts
4
+ var RoomManager = class {
5
+ rooms = /* @__PURE__ */ new Map();
6
+ clients = /* @__PURE__ */ new Map();
7
+ maxPerRoom;
8
+ constructor(maxPerRoom = 1e4) {
9
+ this.maxPerRoom = maxPerRoom;
10
+ }
11
+ addClient(client) {
12
+ this.clients.set(client.id, client);
13
+ }
14
+ removeClient(clientId) {
15
+ const client = this.clients.get(clientId);
16
+ if (!client) return;
17
+ for (const room of client.subscriptions) {
18
+ const members = this.rooms.get(room);
19
+ if (members) {
20
+ members.delete(clientId);
21
+ if (members.size === 0) this.rooms.delete(room);
22
+ }
23
+ }
24
+ client.subscriptions.clear();
25
+ this.clients.delete(clientId);
26
+ }
27
+ subscribe(clientId, room) {
28
+ const client = this.clients.get(clientId);
29
+ if (!client) return false;
30
+ const members = this.rooms.get(room);
31
+ if (members && members.size >= this.maxPerRoom) return false;
32
+ if (!this.rooms.has(room)) this.rooms.set(room, /* @__PURE__ */ new Set());
33
+ this.rooms.get(room).add(clientId);
34
+ client.subscriptions.add(room);
35
+ return true;
36
+ }
37
+ unsubscribe(clientId, room) {
38
+ const client = this.clients.get(clientId);
39
+ if (!client) return;
40
+ const members = this.rooms.get(room);
41
+ if (members) {
42
+ members.delete(clientId);
43
+ if (members.size === 0) this.rooms.delete(room);
44
+ }
45
+ client.subscriptions.delete(room);
46
+ }
47
+ broadcast(room, message, excludeClientId) {
48
+ const members = this.rooms.get(room);
49
+ if (!members) return;
50
+ for (const clientId of members) {
51
+ if (clientId === excludeClientId) continue;
52
+ const client = this.clients.get(clientId);
53
+ if (client && client.socket.readyState === 1) try {
54
+ client.socket.send(message);
55
+ } catch {}
56
+ }
57
+ }
58
+ broadcastToOrg(organizationId, room, message) {
59
+ const members = this.rooms.get(room);
60
+ if (!members) return;
61
+ for (const clientId of members) {
62
+ const client = this.clients.get(clientId);
63
+ if (client && client.organizationId === organizationId && client.socket.readyState === 1) try {
64
+ client.socket.send(message);
65
+ } catch {}
66
+ }
67
+ }
68
+ getClient(clientId) {
69
+ return this.clients.get(clientId);
70
+ }
71
+ getStats() {
72
+ const subscriptions = {};
73
+ for (const [room, members] of this.rooms) subscriptions[room] = members.size;
74
+ return {
75
+ clients: this.clients.size,
76
+ rooms: this.rooms.size,
77
+ subscriptions
78
+ };
79
+ }
80
+ };
81
+ const websocketPluginImpl = async (fastify, options) => {
82
+ let clientCounter = 0;
83
+ const { path = "/ws", auth = true, resources = [], heartbeatInterval = 3e4, authenticate: customAuth, maxClientsPerRoom = 1e4, roomPolicy, maxMessageBytes = 16384, maxSubscriptionsPerClient = 100, exposeStats = false, onMessage, onConnect, onDisconnect } = options;
84
+ if (auth && !customAuth && !fastify.hasDecorator("authenticate")) throw new Error("[arc-websocket] auth is true but fastify.authenticate is not registered. Register an auth plugin before WebSocket, provide a custom authenticate function, or set auth: false.");
85
+ const rooms = new RoomManager(maxClientsPerRoom);
86
+ if (!fastify.hasDecorator("ws")) fastify.decorate("ws", {
87
+ rooms,
88
+ broadcast: (room, data) => {
89
+ rooms.broadcast(room, JSON.stringify({
90
+ type: "broadcast",
91
+ channel: room,
92
+ data
93
+ }));
94
+ },
95
+ broadcastToOrg: (orgId, room, data) => {
96
+ rooms.broadcastToOrg(orgId, room, JSON.stringify({
97
+ type: "broadcast",
98
+ channel: room,
99
+ data
100
+ }));
101
+ },
102
+ getStats: () => rooms.getStats()
103
+ });
104
+ const eventUnsubscribers = [];
105
+ if (resources.length > 0 && fastify.events?.subscribe) for (const resourceName of resources) for (const op of [
106
+ "created",
107
+ "updated",
108
+ "deleted"
109
+ ]) {
110
+ const unsub = await fastify.events.subscribe(`${resourceName}.${op}`, async (event) => {
111
+ const room = resourceName;
112
+ const payload = JSON.stringify({
113
+ type: `${resourceName}.${op}`,
114
+ data: event.payload,
115
+ meta: {
116
+ timestamp: event.meta?.timestamp,
117
+ userId: event.meta?.userId,
118
+ organizationId: event.meta?.organizationId
119
+ }
120
+ });
121
+ if (event.meta?.organizationId) rooms.broadcastToOrg(event.meta.organizationId, room, payload);
122
+ else rooms.broadcast(room, payload);
123
+ });
124
+ eventUnsubscribers.push(unsub);
125
+ }
126
+ fastify.get(path, { websocket: true }, async (socket, request) => {
127
+ const clientId = `ws_${++clientCounter}_${Date.now()}`;
128
+ let userId;
129
+ let organizationId;
130
+ if (auth) if (customAuth) {
131
+ const result = await customAuth(request);
132
+ if (!result) {
133
+ socket.close(4001, "Unauthorized");
134
+ return;
135
+ }
136
+ userId = result.userId;
137
+ organizationId = result.organizationId;
138
+ } else {
139
+ if (fastify.authenticate) try {
140
+ let rejected = false;
141
+ const fakeReply = {
142
+ code(_statusCode) {
143
+ rejected = true;
144
+ return fakeReply;
145
+ },
146
+ send() {
147
+ return fakeReply;
148
+ },
149
+ sent: false
150
+ };
151
+ await fastify.authenticate(request, fakeReply);
152
+ if (rejected) {
153
+ socket.close(4001, "Unauthorized");
154
+ return;
155
+ }
156
+ } catch {
157
+ socket.close(4001, "Unauthorized");
158
+ return;
159
+ }
160
+ if (request.user) {
161
+ userId = request.user.id ?? request.user.sub;
162
+ organizationId = request.scope?.organizationId;
163
+ } else {
164
+ socket.close(4001, "Unauthorized");
165
+ return;
166
+ }
167
+ }
168
+ const client = {
169
+ id: clientId,
170
+ socket,
171
+ subscriptions: /* @__PURE__ */ new Set(),
172
+ userId,
173
+ organizationId
174
+ };
175
+ rooms.addClient(client);
176
+ await onConnect?.(client);
177
+ socket.send(JSON.stringify({
178
+ type: "connected",
179
+ clientId,
180
+ resources
181
+ }));
182
+ let heartbeatTimer;
183
+ if (heartbeatInterval > 0) heartbeatTimer = setInterval(() => {
184
+ if (socket.readyState === 1) socket.send(JSON.stringify({
185
+ type: "ping",
186
+ timestamp: Date.now()
187
+ }));
188
+ }, heartbeatInterval);
189
+ socket.on("message", async (raw) => {
190
+ if ((typeof raw === "string" ? Buffer.byteLength(raw) : raw.length) > maxMessageBytes) {
191
+ socket.send(JSON.stringify({
192
+ type: "error",
193
+ error: "Message too large"
194
+ }));
195
+ return;
196
+ }
197
+ try {
198
+ const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString());
199
+ switch (msg.type) {
200
+ case "subscribe": {
201
+ const room = msg.resource ?? msg.channel;
202
+ if (room) {
203
+ if (client.subscriptions.size >= maxSubscriptionsPerClient) {
204
+ socket.send(JSON.stringify({
205
+ type: "error",
206
+ channel: room,
207
+ error: "Subscription limit reached"
208
+ }));
209
+ break;
210
+ }
211
+ if (roomPolicy) {
212
+ if (!await roomPolicy(client, room)) {
213
+ socket.send(JSON.stringify({
214
+ type: "error",
215
+ channel: room,
216
+ error: "Subscription denied"
217
+ }));
218
+ break;
219
+ }
220
+ }
221
+ const ok = rooms.subscribe(clientId, room);
222
+ socket.send(JSON.stringify({
223
+ type: ok ? "subscribed" : "error",
224
+ channel: room,
225
+ ...ok ? {} : { error: "Room at capacity" }
226
+ }));
227
+ }
228
+ break;
229
+ }
230
+ case "unsubscribe": {
231
+ const room = msg.resource ?? msg.channel;
232
+ if (room) {
233
+ rooms.unsubscribe(clientId, room);
234
+ socket.send(JSON.stringify({
235
+ type: "unsubscribed",
236
+ channel: room
237
+ }));
238
+ }
239
+ break;
240
+ }
241
+ case "pong": break;
242
+ default:
243
+ await onMessage?.(client, msg);
244
+ break;
245
+ }
246
+ } catch {
247
+ socket.send(JSON.stringify({
248
+ type: "error",
249
+ error: "Invalid message format"
250
+ }));
251
+ }
252
+ });
253
+ socket.on("close", async () => {
254
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
255
+ await onDisconnect?.(client);
256
+ rooms.removeClient(clientId);
257
+ });
258
+ socket.on("error", () => {
259
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
260
+ rooms.removeClient(clientId);
261
+ });
262
+ });
263
+ if (exposeStats === true) fastify.get(`${path}/stats`, async () => {
264
+ return {
265
+ success: true,
266
+ data: rooms.getStats()
267
+ };
268
+ });
269
+ else if (exposeStats === "authenticated") if (fastify.hasDecorator("authenticate")) fastify.get(`${path}/stats`, { preHandler: fastify.authenticate }, async () => {
270
+ return {
271
+ success: true,
272
+ data: rooms.getStats()
273
+ };
274
+ });
275
+ else fastify.log.warn("arc-websocket: exposeStats is \"authenticated\" but fastify.authenticate is not registered — stats endpoint skipped");
276
+ fastify.addHook("onClose", async () => {
277
+ for (const unsub of eventUnsubscribers) unsub();
278
+ eventUnsubscribers.length = 0;
279
+ });
280
+ };
281
+ /** Pluggable WebSocket integration for Arc */
282
+ const websocketPlugin = fp(websocketPluginImpl, {
283
+ name: "arc-websocket",
284
+ fastify: "5.x"
285
+ });
286
+
287
+ //#endregion
288
+ export { RoomManager, websocketPlugin as default, websocketPlugin };