@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.
- package/README.md +187 -18
- package/bin/arc.js +11 -3
- package/dist/BaseController-CkM5dUh_.mjs +1031 -0
- package/dist/{EventTransport-BkUDYZEb.d.mts → EventTransport-wc5hSLik.d.mts} +1 -1
- package/dist/{HookSystem-BsGV-j2l.mjs → HookSystem-COkyWztM.mjs} +2 -3
- package/dist/{ResourceRegistry-7Ic20ZMw.mjs → ResourceRegistry-DeCIFlix.mjs} +8 -5
- package/dist/adapters/index.d.mts +3 -5
- package/dist/adapters/index.mjs +2 -3
- package/dist/{prisma-DJbMt3yf.mjs → adapters-DTC4Ug66.mjs} +45 -12
- package/dist/audit/index.d.mts +4 -7
- package/dist/audit/index.mjs +2 -29
- package/dist/audit/mongodb.d.mts +1 -4
- package/dist/audit/mongodb.mjs +2 -3
- package/dist/auth/index.d.mts +7 -9
- package/dist/auth/index.mjs +65 -63
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/auth/redis-session.mjs +1 -2
- package/dist/{betterAuthOpenApi-DjWDddNc.mjs → betterAuthOpenApi-lz0IRbXJ.mjs} +4 -6
- package/dist/cache/index.d.mts +23 -23
- package/dist/cache/index.mjs +4 -6
- package/dist/{caching-GSDJcA6-.mjs → caching-BSXB-Xr7.mjs} +2 -24
- package/dist/chunk-BpYLSNr0.mjs +14 -0
- package/dist/circuitBreaker-BOBOpN2w.mjs +284 -0
- package/dist/circuitBreaker-JP2GdJ4b.d.mts +206 -0
- package/dist/cli/commands/describe.mjs +24 -7
- package/dist/cli/commands/docs.mjs +6 -7
- package/dist/cli/commands/doctor.d.mts +10 -0
- package/dist/cli/commands/doctor.mjs +156 -0
- package/dist/cli/commands/generate.mjs +66 -17
- package/dist/cli/commands/init.mjs +315 -45
- package/dist/cli/commands/introspect.mjs +2 -4
- package/dist/cli/index.d.mts +1 -10
- package/dist/cli/index.mjs +4 -153
- package/dist/{constants-DdXFXQtN.mjs → constants-Cxde4rpC.mjs} +1 -2
- package/dist/core/index.d.mts +3 -5
- package/dist/core/index.mjs +5 -4
- package/dist/core-C1XCMtqM.mjs +185 -0
- package/dist/{createApp-CgKOPhA4.mjs → createApp-ByWNRsZj.mjs} +64 -35
- package/dist/{defineResource-DWbpJYtm.mjs → defineResource-D9aY5Cy6.mjs} +108 -1157
- package/dist/discovery/index.mjs +37 -5
- package/dist/docs/index.d.mts +6 -9
- package/dist/docs/index.mjs +3 -21
- package/dist/dynamic/index.d.mts +93 -0
- package/dist/dynamic/index.mjs +122 -0
- package/dist/{elevation-DSTbVvYj.mjs → elevation-BEdACOLB.mjs} +5 -36
- package/dist/{elevation-DGo5shaX.d.mts → elevation-Ca_yveIO.d.mts} +41 -7
- package/dist/{errorHandler-C3GY3_ow.mjs → errorHandler--zp54tGc.mjs} +3 -5
- package/dist/errorHandler-Do4vVQ1f.d.mts +139 -0
- package/dist/{errors-DBANPbGr.mjs → errors-rxhfP7Hf.mjs} +1 -2
- package/dist/{eventPlugin-BEOvaDqo.mjs → eventPlugin-Ba00swHF.mjs} +25 -27
- package/dist/{eventPlugin-H6wDDjGO.d.mts → eventPlugin-iGrSEmwJ.d.mts} +105 -5
- package/dist/events/index.d.mts +72 -7
- package/dist/events/index.mjs +216 -4
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +19 -7
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +3 -4
- package/dist/factory/index.d.mts +23 -9
- package/dist/factory/index.mjs +48 -3
- package/dist/{fields-Bi_AVKSo.d.mts → fields-DFwdaWCq.d.mts} +1 -1
- package/dist/{fields-CTd_CrKr.mjs → fields-ipsbIRPK.mjs} +1 -2
- package/dist/hooks/index.d.mts +1 -3
- package/dist/hooks/index.mjs +2 -3
- package/dist/idempotency/index.d.mts +5 -5
- package/dist/idempotency/index.mjs +3 -7
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/mongodb.mjs +4 -5
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +2 -5
- package/dist/{fastifyAdapter-6b_eRDBw.d.mts → index-BL8CaQih.d.mts} +56 -57
- package/dist/index-Diqcm14c.d.mts +369 -0
- package/dist/{prisma-Dy5S5F5i.d.mts → index-yhxyjqNb.d.mts} +4 -5
- package/dist/index.d.mts +100 -105
- package/dist/index.mjs +85 -58
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +8 -4
- package/dist/integrations/index.d.mts +4 -2
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs.d.mts +2 -2
- package/dist/integrations/jobs.mjs +63 -14
- package/dist/integrations/mcp/index.d.mts +219 -0
- package/dist/integrations/mcp/index.mjs +572 -0
- package/dist/integrations/mcp/testing.d.mts +53 -0
- package/dist/integrations/mcp/testing.mjs +104 -0
- package/dist/integrations/streamline.mjs +39 -19
- package/dist/integrations/webhooks.d.mts +56 -0
- package/dist/integrations/webhooks.mjs +139 -0
- package/dist/integrations/websocket-redis.d.mts +46 -0
- package/dist/integrations/websocket-redis.mjs +50 -0
- package/dist/integrations/websocket.d.mts +68 -2
- package/dist/integrations/websocket.mjs +96 -13
- package/dist/{interface-CSNjltAc.d.mts → interface-B4awm1RJ.d.mts} +2 -2
- package/dist/interface-DGmPxakH.d.mts +2213 -0
- package/dist/{keys-DhqDRxv3.mjs → keys-qcD-TVJl.mjs} +3 -4
- package/dist/{logger-ByrvQWZO.mjs → logger-Dz3j1ItV.mjs} +2 -4
- package/dist/{memory-B2v7KrCB.mjs → memory-Cb_7iy9e.mjs} +2 -4
- package/dist/metrics-Csh4nsvv.mjs +224 -0
- package/dist/migrations/index.d.mts +113 -44
- package/dist/migrations/index.mjs +84 -102
- package/dist/{mongodb-DNKEExbf.mjs → mongodb-BuQ7fNTg.mjs} +1 -4
- package/dist/{mongodb-ClykrfGo.d.mts → mongodb-CUpYfxfD.d.mts} +2 -3
- package/dist/{mongodb-Dg8O_gvd.d.mts → mongodb-bga9AbkD.d.mts} +2 -2
- package/dist/{openapi-9nB_kiuR.mjs → openapi-CBmZ6EQN.mjs} +4 -21
- package/dist/org/index.d.mts +12 -14
- package/dist/org/index.mjs +92 -119
- package/dist/org/types.d.mts +2 -2
- package/dist/org/types.mjs +1 -1
- package/dist/permissions/index.d.mts +4 -278
- package/dist/permissions/index.mjs +4 -579
- package/dist/permissions-CA5zg0yK.mjs +751 -0
- package/dist/plugins/index.d.mts +104 -107
- package/dist/plugins/index.mjs +203 -313
- package/dist/plugins/response-cache.mjs +4 -69
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +24 -11
- package/dist/{pluralize-CM-jZg7p.mjs → pluralize-CcT6qF0a.mjs} +12 -13
- package/dist/policies/index.d.mts +2 -2
- package/dist/policies/index.mjs +80 -83
- package/dist/presets/index.d.mts +26 -19
- package/dist/presets/index.mjs +2 -142
- package/dist/presets/multiTenant.d.mts +1 -4
- package/dist/presets/multiTenant.mjs +4 -6
- package/dist/presets-C9QXJV1u.mjs +422 -0
- package/dist/{queryCachePlugin-B6R0d4av.mjs → queryCachePlugin-ClosZdNS.mjs} +6 -27
- package/dist/{queryCachePlugin-Q6SYuHZ6.d.mts → queryCachePlugin-DcmETvcB.d.mts} +3 -3
- package/dist/queryParser-CgCtsjti.mjs +352 -0
- package/dist/{redis-UwjEp8Ea.d.mts → redis-CQ5YxMC5.d.mts} +2 -2
- package/dist/{redis-stream-CBg0upHI.d.mts → redis-stream-BW9UKLZM.d.mts} +9 -2
- package/dist/registry/index.d.mts +1 -4
- package/dist/registry/index.mjs +3 -4
- package/dist/{introspectionPlugin-B3JkrjwU.mjs → registry-I-ogLgL9.mjs} +1 -8
- package/dist/{requestContext-xi6OKBL-.mjs → requestContext-DYtmNpm5.mjs} +1 -3
- package/dist/resourceToTools-PMFE8HIv.mjs +533 -0
- package/dist/rpc/index.d.mts +90 -0
- package/dist/rpc/index.mjs +248 -0
- package/dist/{schemaConverter-Dtg0Kt9T.mjs → schemaConverter-DjzHpFam.mjs} +1 -2
- package/dist/schemas/index.d.mts +30 -30
- package/dist/schemas/index.mjs +2 -4
- package/dist/scope/index.d.mts +13 -2
- package/dist/scope/index.mjs +18 -5
- package/dist/{sessionManager-D_iEHjQl.d.mts → sessionManager-wbkYj2HL.d.mts} +2 -2
- package/dist/{sse-DkqQ1uxb.mjs → sse-BkViJPlT.mjs} +4 -25
- package/dist/testing/index.d.mts +551 -567
- package/dist/testing/index.mjs +1744 -1799
- package/dist/{tracing-8CEbhF0w.d.mts → tracing-bz_U4EM1.d.mts} +6 -1
- package/dist/{typeGuards-DwxA1t_L.mjs → typeGuards-Cj5Rgvlg.mjs} +1 -2
- package/dist/types/index.d.mts +4 -946
- package/dist/types/index.mjs +2 -4
- package/dist/types-BJmgxNbF.d.mts +275 -0
- package/dist/{types-RLkFVgaw.d.mts → types-BNUccdcf.d.mts} +2 -2
- package/dist/{types-Beqn1Un7.mjs → types-C6TQjtdi.mjs} +30 -2
- package/dist/{types-tKwaViYB.d.mts → types-Dt0-AI6E.d.mts} +68 -27
- package/dist/{types-DelU6kln.mjs → types-ZUu_h0jp.mjs} +1 -2
- package/dist/utils/index.d.mts +254 -351
- package/dist/utils/index.mjs +7 -6
- package/dist/utils-Dc0WhlIl.mjs +594 -0
- package/dist/versioning-BzfeHmhj.mjs +37 -0
- package/package.json +44 -10
- package/skills/arc/SKILL.md +518 -0
- package/skills/arc/references/auth.md +250 -0
- package/skills/arc/references/events.md +272 -0
- package/skills/arc/references/integrations.md +385 -0
- package/skills/arc/references/mcp.md +431 -0
- package/skills/arc/references/production.md +610 -0
- package/skills/arc/references/testing.md +183 -0
- package/dist/audited-CGdLiSlE.mjs +0 -140
- package/dist/chunk-C7Uep-_p.mjs +0 -20
- package/dist/circuitBreaker-CSS2VvL6.mjs +0 -1109
- package/dist/errorHandler-CW3OOeYq.d.mts +0 -72
- package/dist/interface-BtdYtQUA.d.mts +0 -1114
- package/dist/presets-BTeYbw7h.d.mts +0 -57
- package/dist/presets-CeFtfDR8.mjs +0 -119
- /package/dist/{errors-DAWRdiYP.d.mts → errors-CPpvPHT0.d.mts} +0 -0
- /package/dist/{externalPaths-SyPF2tgK.d.mts → externalPaths-DpO-s7r8.d.mts} +0 -0
- /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
|
+
```
|