@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
@@ -0,0 +1,385 @@
1
+ # Arc Integrations
2
+
3
+ Pluggable adapters for BullMQ jobs, WebSocket real-time, Streamline workflows, and MCP tools.
4
+ All are separate subpath imports — only loaded when explicitly used.
5
+
6
+ > **MCP** has its own dedicated reference: [mcp.md](mcp.md) — auto-generate tools from resources, custom tools, Better Auth OAuth 2.1.
7
+
8
+ ## Job Queue (BullMQ)
9
+
10
+ ```typescript
11
+ import { jobsPlugin, defineJob } from '@classytic/arc/integrations/jobs';
12
+ ```
13
+
14
+ **Requires:** `bullmq` (peer dependency), Redis
15
+
16
+ ### Define Jobs
17
+
18
+ ```typescript
19
+ const sendEmail = defineJob({
20
+ name: 'send-email',
21
+ handler: async (data: { to: string; subject: string; body: string }, meta: JobMeta) => {
22
+ await emailService.send(data.to, data.subject, data.body);
23
+ return { sent: true };
24
+ },
25
+ retries: 3,
26
+ backoff: { type: 'exponential', delay: 1000 },
27
+ timeout: 30000,
28
+ concurrency: 5,
29
+ rateLimit: { max: 100, duration: 60000 }, // 100/min
30
+ deadLetterQueue: 'send-email:dead',
31
+ });
32
+
33
+ const processImage = defineJob({
34
+ name: 'process-image',
35
+ handler: async (data: { url: string; width: number }) => {
36
+ return await sharp(data.url).resize(data.width).toBuffer();
37
+ },
38
+ retries: 2,
39
+ timeout: 60000,
40
+ });
41
+ ```
42
+
43
+ ### Register Plugin
44
+
45
+ ```typescript
46
+ await fastify.register(jobsPlugin, {
47
+ connection: { host: 'localhost', port: 6379, password: '...' },
48
+ jobs: [sendEmail, processImage],
49
+ prefix: '/jobs', // Stats endpoint: GET /jobs/stats
50
+ bridgeEvents: true, // Emit job.{name}.completed / job.{name}.failed
51
+ defaults: {
52
+ retries: 3,
53
+ backoff: { type: 'exponential', delay: 1000 },
54
+ removeOnComplete: 100, // Keep last 100 completed
55
+ removeOnFail: 500, // Keep last 500 failed
56
+ },
57
+ });
58
+ ```
59
+
60
+ ### Dispatch Jobs
61
+
62
+ ```typescript
63
+ // Basic dispatch
64
+ await fastify.jobs.dispatch('send-email', { to: 'user@example.com', subject: 'Hi', body: 'Hello' });
65
+
66
+ // With options
67
+ await fastify.jobs.dispatch('process-image', { url: '...', width: 800 }, {
68
+ delay: 5000, // Delay 5s
69
+ priority: 1, // Lower = higher priority
70
+ jobId: 'unique-123', // Deduplication
71
+ removeOnComplete: true,
72
+ });
73
+
74
+ // Get stats
75
+ const stats = await fastify.jobs.getStats();
76
+ // { 'send-email': { waiting: 5, active: 2, completed: 100, failed: 3, delayed: 0 } }
77
+ ```
78
+
79
+ ### Timeout & DLQ
80
+
81
+ Job timeout via `Promise.race` (timer always cleaned up). DLQ queues tracked and closed on shutdown:
82
+
83
+ ```typescript
84
+ defineJob({ name: 'x', handler, timeout: 60000, deadLetterQueue: 'x:dead', retries: 3 });
85
+ ```
86
+
87
+ ### Event Bridge
88
+
89
+ When `bridgeEvents: true` (default), job events fire-and-forget (never fail the worker):
90
+ - `job.send-email.completed` — `{ jobId, data, result }`
91
+ - `job.send-email.failed` — `{ jobId, data, error, attemptsMade }`
92
+
93
+ ### Types
94
+
95
+ ```typescript
96
+ interface JobMeta { jobId: string; attemptsMade: number; timestamp: number; }
97
+ interface JobDispatchOptions { delay?; priority?; jobId?; removeOnComplete?; removeOnFail?; }
98
+ interface QueueStats { waiting; active; completed; failed; delayed; }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## WebSocket
104
+
105
+ ```typescript
106
+ import { websocketPlugin } from '@classytic/arc/integrations/websocket';
107
+ ```
108
+
109
+ **Requires:** `@fastify/websocket` (peer dependency), persistent runtime (not serverless)
110
+
111
+ ### Setup
112
+
113
+ ```typescript
114
+ import fastifyWebsocket from '@fastify/websocket';
115
+
116
+ await fastify.register(fastifyWebsocket);
117
+ await fastify.register(websocketPlugin, {
118
+ path: '/ws',
119
+ auth: true, // Fail-closed: throws if authenticate not registered
120
+ resources: ['product', 'order'], // Auto-broadcast CRUD events
121
+ heartbeatInterval: 30000, // Ping every 30s (0 to disable)
122
+ maxClientsPerRoom: 10000,
123
+
124
+ // Security controls
125
+ roomPolicy: (client, room) => { // Authorize room subscriptions (default: allow all)
126
+ return ['product', 'order'].includes(room);
127
+ },
128
+ maxMessageBytes: 16384, // Max message size from client (default: 16KB)
129
+ maxSubscriptionsPerClient: 100, // Max rooms per client (default: 100)
130
+ exposeStats: 'authenticated', // Stats at /ws/stats (false | true | 'authenticated')
131
+
132
+ // Lifecycle hooks
133
+ authenticate: async (request) => { // Custom auth (optional)
134
+ const { getOrgId } = await import('@classytic/arc/scope');
135
+ return { userId: request.user?.id, organizationId: getOrgId(request.scope) };
136
+ },
137
+ onConnect: async (client) => { console.log('Connected:', client.id); },
138
+ onDisconnect: async (client) => { console.log('Disconnected:', client.id); },
139
+ onMessage: async (client, msg) => { /* custom message handler */ },
140
+ });
141
+ ```
142
+
143
+ **Fail-closed auth:** When `auth: true` (default), registration throws if `fastify.authenticate` is not available and no custom `authenticate` function is provided. This prevents accidentally exposing WebSocket without auth.
144
+
145
+ ### Client Protocol
146
+
147
+ ```javascript
148
+ const ws = new WebSocket('ws://localhost:3000/ws');
149
+
150
+ // Server sends on connect:
151
+ // { type: 'connected', clientId: 'ws_1_...', resources: ['product', 'order'] }
152
+
153
+ // Subscribe to resource events
154
+ ws.send(JSON.stringify({ type: 'subscribe', resource: 'product' }));
155
+ // → { type: 'subscribed', channel: 'product' }
156
+
157
+ // Server pushes CRUD events:
158
+ // { type: 'product.created', data: { ... }, meta: { timestamp, userId, organizationId } }
159
+
160
+ // Unsubscribe
161
+ ws.send(JSON.stringify({ type: 'unsubscribe', resource: 'product' }));
162
+
163
+ // Heartbeat: server sends { type: 'ping' }, client responds { type: 'pong' }
164
+ ```
165
+
166
+ ### Server-Side Broadcasting
167
+
168
+ ```typescript
169
+ // Broadcast to room
170
+ fastify.ws.broadcast('product', { action: 'price-updated', productId: '123' });
171
+
172
+ // Org-scoped broadcast (only clients in same org)
173
+ fastify.ws.broadcastToOrg('org-456', 'product', { ... });
174
+
175
+ // Stats
176
+ fastify.ws.getStats(); // { clients: 150, rooms: 5, subscriptions: { product: 80, order: 70 } }
177
+ ```
178
+
179
+ ### RoomManager
180
+
181
+ ```typescript
182
+ // Access room manager directly
183
+ const rooms = fastify.ws.rooms;
184
+ rooms.subscribe(clientId, 'custom-room');
185
+ rooms.broadcast('custom-room', JSON.stringify({ type: 'custom', data: {} }));
186
+ rooms.broadcastToOrg(orgId, 'custom-room', JSON.stringify({ ... }));
187
+ ```
188
+
189
+ ### Multi-Tenant Auto-Scoping
190
+
191
+ When Arc events include `organizationId`, WebSocket broadcasts are automatically scoped:
192
+ - Client with `organizationId: 'org-A'` only receives events for org-A
193
+ - No cross-tenant data leakage
194
+
195
+ ---
196
+
197
+ ## EventGateway (Unified SSE + WebSocket)
198
+
199
+ ```typescript
200
+ import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';
201
+ ```
202
+
203
+ Single configuration point for both SSE and WebSocket with shared auth, org-scoping, and room policy:
204
+
205
+ ```typescript
206
+ await fastify.register(eventGatewayPlugin, {
207
+ auth: true, // Fail-closed for both SSE and WebSocket
208
+ orgScoped: true, // Filter events by org
209
+ roomPolicy: (client, room) => {
210
+ return ['product', 'order', 'invoice'].includes(room);
211
+ },
212
+ maxMessageBytes: 8192, // WS message size cap
213
+ maxSubscriptionsPerClient: 50, // WS subscription limit
214
+
215
+ sse: { // false to disable SSE
216
+ path: '/api/events',
217
+ patterns: ['order.*', 'product.*'],
218
+ },
219
+ ws: { // false to disable WebSocket
220
+ path: '/ws',
221
+ resources: ['product', 'order'],
222
+ exposeStats: 'authenticated',
223
+ },
224
+ });
225
+ ```
226
+
227
+ **`@fastify/websocket` auto-registration:** EventGateway auto-registers `@fastify/websocket` if not present. Throws with install instructions if package missing (or use `ws: false`).
228
+
229
+ **When to use:** Prefer EventGateway over separate SSE + WebSocket registration when you want consistent auth, org-scoping, and security policy across both transports.
230
+
231
+ ---
232
+
233
+ ## Streamline Workflows
234
+
235
+ ```typescript
236
+ import { streamlinePlugin } from '@classytic/arc/integrations/streamline';
237
+ ```
238
+
239
+ **Requires:** `@classytic/streamline` (peer dependency)
240
+
241
+ ### Setup
242
+
243
+ ```typescript
244
+ import { createWorkflow } from '@classytic/streamline';
245
+
246
+ const orderWorkflow = createWorkflow({ id: 'order', name: 'Order Processing', steps: { ... } });
247
+
248
+ await fastify.register(streamlinePlugin, {
249
+ workflows: [orderWorkflow],
250
+ prefix: '/api/workflows',
251
+ auth: true, // Require authentication (default, gracefully degrades)
252
+ bridgeEvents: true, // Publish workflow.{id}.started/resumed/cancelled
253
+ permissions: { // Per-operation permissions (all optional, default: allow)
254
+ start: (request) => request.user?.role === 'admin',
255
+ cancel: (request) => request.user?.role === 'admin',
256
+ },
257
+ });
258
+ ```
259
+
260
+ ### Auto-Generated Routes
261
+
262
+ | Route | Description |
263
+ |-------|-------------|
264
+ | `GET /api/workflows` | List all registered workflows |
265
+ | `POST /api/workflows/:id/start` | Start a new run (`{ input, meta }`) |
266
+ | `GET /api/workflows/:id/runs/:runId` | Get run status |
267
+ | `POST /api/workflows/:id/runs/:runId/resume` | Resume waiting run (`{ payload }`) |
268
+ | `POST /api/workflows/:id/runs/:runId/cancel` | Cancel a run |
269
+ | `POST /api/workflows/:id/runs/:runId/pause` | Pause (if engine supports) |
270
+ | `POST /api/workflows/:id/runs/:runId/rewind` | Rewind to step (`{ stepId }`) |
271
+
272
+ ### Fastify Decorators
273
+
274
+ ```typescript
275
+ fastify.workflows; // Map<string, WorkflowLike>
276
+ fastify.getWorkflow('order'); // Get specific workflow
277
+ ```
278
+
279
+ ### Event Bridge
280
+
281
+ - `workflow.order.started` — `{ runId, workflowId, status }`
282
+ - `workflow.order.resumed` — `{ runId, workflowId, status }`
283
+ - `workflow.order.cancelled` — `{ runId, workflowId }`
284
+
285
+ ### Auth & Permissions
286
+
287
+ All optional, gracefully degrade:
288
+ - `auth: false` — No authentication required
289
+ - If `fastify.authenticate` is not registered, auth middleware is skipped
290
+ - If no permission check defined for an operation, defaults to allow
291
+
292
+ ---
293
+
294
+ ## Webhooks (Outbound)
295
+
296
+ ```typescript
297
+ import { webhookPlugin } from '@classytic/arc/integrations/webhooks';
298
+ ```
299
+
300
+ Fastify plugin that auto-dispatches Arc events to customer webhook endpoints with HMAC-SHA256 signing, delivery logging, and pluggable persistence.
301
+
302
+ ### Setup
303
+
304
+ ```typescript
305
+ await fastify.register(webhookPlugin);
306
+
307
+ // With custom store (MongoDB, Redis, etc.)
308
+ await fastify.register(webhookPlugin, {
309
+ store: myMongoWebhookStore, // implements WebhookStore { getAll, save, remove }
310
+ timeout: 5000, // delivery timeout (default: 10000ms)
311
+ maxLogEntries: 500, // ring buffer cap (default: 1000)
312
+ });
313
+ ```
314
+
315
+ **Requires:** `arc-events` plugin (auto-registered by `createApp`).
316
+
317
+ ### Register Webhooks
318
+
319
+ ```typescript
320
+ await app.webhooks.register({
321
+ id: 'wh-1',
322
+ url: 'https://customer.com/webhook',
323
+ events: ['order.created', 'order.shipped'],
324
+ secret: 'whsec_abc123',
325
+ });
326
+
327
+ // Patterns: exact ('order.created'), prefix ('order.*'), global ('*')
328
+ ```
329
+
330
+ ### Auto-Dispatch
331
+
332
+ Events published via `fastify.events.publish()` auto-deliver to matching webhooks — no manual wiring:
333
+
334
+ ```typescript
335
+ await app.events.publish('order.created', { orderId: '123' });
336
+ // → POST https://customer.com/webhook
337
+ // Headers: x-webhook-signature, x-webhook-id, x-webhook-event
338
+ // Body: { type, payload, meta }
339
+ ```
340
+
341
+ ### HMAC Signing
342
+
343
+ Every delivery is signed with the subscription's secret using HMAC-SHA256:
344
+
345
+ ```
346
+ x-webhook-signature: sha256=a1b2c3...
347
+ ```
348
+
349
+ Verify on the receiving end:
350
+ ```typescript
351
+ import { createHmac } from 'node:crypto';
352
+ const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
353
+ if (expected !== req.headers['x-webhook-signature']) throw new Error('Invalid signature');
354
+ ```
355
+
356
+ ### Delivery Log
357
+
358
+ ```typescript
359
+ const log = app.webhooks.deliveryLog(); // all entries
360
+ const recent = app.webhooks.deliveryLog(10); // last 10
361
+
362
+ // Each entry: { subscriptionId, eventType, success, status?, error?, timestamp }
363
+ ```
364
+
365
+ ### WebhookStore Interface
366
+
367
+ Implement for persistent subscriptions (default: in-memory):
368
+
369
+ ```typescript
370
+ interface WebhookStore {
371
+ readonly name: string;
372
+ getAll(): Promise<WebhookSubscription[]>;
373
+ save(sub: WebhookSubscription): Promise<void>;
374
+ remove(id: string): Promise<void>;
375
+ }
376
+ ```
377
+
378
+ ### Fastify Decorators
379
+
380
+ ```typescript
381
+ app.webhooks.register(sub) // Add/replace subscription
382
+ app.webhooks.unregister(id) // Remove subscription
383
+ app.webhooks.list() // All subscriptions (copy)
384
+ app.webhooks.deliveryLog(n?) // Delivery history (ring buffer)
385
+ ```