@multiplier-labs/stepflow 0.3.1
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/.github/workflows/publish.yml +30 -0
- package/README.md +408 -0
- package/dist/chunk-FQCT3I2A.js +1512 -0
- package/dist/chunk-FQCT3I2A.js.map +1 -0
- package/dist/chunk-GXY66V7Q.js +111 -0
- package/dist/chunk-GXY66V7Q.js.map +1 -0
- package/dist/chunk-U5QGN3RT.js +715 -0
- package/dist/chunk-U5QGN3RT.js.map +1 -0
- package/dist/chunk-XCF4PLN7.js +575 -0
- package/dist/chunk-XCF4PLN7.js.map +1 -0
- package/dist/events/index.d.ts +346 -0
- package/dist/events/index.js +11 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index-CPfGsmrp.d.ts +567 -0
- package/dist/index.d.ts +748 -0
- package/dist/index.js +1707 -0
- package/dist/index.js.map +1 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/storage/index.d.ts +402 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types-BVShq7zT.d.ts +150 -0
- package/dist/types-BlwFEGzj.d.ts +276 -0
- package/dist/types-DzkEqnQJ.d.ts +75 -0
- package/docs/github-packages-setup.md +97 -0
- package/docs/stepflow-api-reference.md +1662 -0
- package/package.json +85 -0
- package/src/core/engine.test.ts +1421 -0
- package/src/core/engine.ts +717 -0
- package/src/core/index.ts +7 -0
- package/src/core/orchestrator.ts +590 -0
- package/src/core/types.ts +242 -0
- package/src/events/index.ts +8 -0
- package/src/events/memory.test.ts +109 -0
- package/src/events/memory.ts +95 -0
- package/src/events/socketio.test.ts +428 -0
- package/src/events/socketio.ts +269 -0
- package/src/events/types.ts +121 -0
- package/src/events/webhook.test.ts +695 -0
- package/src/events/webhook.ts +591 -0
- package/src/index.ts +270 -0
- package/src/planning/index.ts +52 -0
- package/src/planning/planner.ts +601 -0
- package/src/planning/planning.test.ts +790 -0
- package/src/planning/registry.test.ts +445 -0
- package/src/planning/registry.ts +369 -0
- package/src/planning/types.ts +570 -0
- package/src/scheduler/cron.test.ts +480 -0
- package/src/scheduler/cron.ts +449 -0
- package/src/scheduler/index.ts +18 -0
- package/src/scheduler/postgres-persistence.test.ts +906 -0
- package/src/scheduler/postgres-persistence.ts +546 -0
- package/src/scheduler/sqlite-persistence.test.ts +235 -0
- package/src/scheduler/sqlite-persistence.ts +268 -0
- package/src/scheduler/types.ts +78 -0
- package/src/storage/index.ts +27 -0
- package/src/storage/memory.test.ts +399 -0
- package/src/storage/memory.ts +248 -0
- package/src/storage/postgres.ts +1550 -0
- package/src/storage/sqlite.test.ts +593 -0
- package/src/storage/sqlite.ts +662 -0
- package/src/storage/types.ts +368 -0
- package/src/utils/errors.test.ts +152 -0
- package/src/utils/errors.ts +191 -0
- package/src/utils/id.ts +21 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.test.ts +127 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/postgres-deps.ts +63 -0
- package/src/utils/retry.test.ts +197 -0
- package/src/utils/retry.ts +121 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// src/events/memory.ts
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
var MemoryEventTransport = class {
|
|
4
|
+
emitter = new EventEmitter();
|
|
5
|
+
constructor() {
|
|
6
|
+
this.emitter.setMaxListeners(100);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Emit an event to all subscribers.
|
|
10
|
+
*/
|
|
11
|
+
emit(event) {
|
|
12
|
+
this.emitter.emit("event", event);
|
|
13
|
+
this.emitter.emit(`run:${event.runId}`, event);
|
|
14
|
+
this.emitter.emit(`type:${event.eventType}`, event);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Subscribe to events for a specific run.
|
|
18
|
+
*/
|
|
19
|
+
subscribe(runId, callback) {
|
|
20
|
+
const channel = `run:${runId}`;
|
|
21
|
+
this.emitter.on(channel, callback);
|
|
22
|
+
return () => {
|
|
23
|
+
this.emitter.off(channel, callback);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to all events.
|
|
28
|
+
*/
|
|
29
|
+
subscribeAll(callback) {
|
|
30
|
+
this.emitter.on("event", callback);
|
|
31
|
+
return () => {
|
|
32
|
+
this.emitter.off("event", callback);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Subscribe to events of a specific type.
|
|
37
|
+
*/
|
|
38
|
+
subscribeToType(eventType, callback) {
|
|
39
|
+
const channel = `type:${eventType}`;
|
|
40
|
+
this.emitter.on(channel, callback);
|
|
41
|
+
return () => {
|
|
42
|
+
this.emitter.off(channel, callback);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Close the transport and remove all listeners.
|
|
47
|
+
*/
|
|
48
|
+
close() {
|
|
49
|
+
this.emitter.removeAllListeners();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get the number of listeners for a channel (for testing).
|
|
53
|
+
*/
|
|
54
|
+
getListenerCount(channel) {
|
|
55
|
+
if (channel) {
|
|
56
|
+
return this.emitter.listenerCount(channel);
|
|
57
|
+
}
|
|
58
|
+
return this.emitter.listenerCount("event");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/events/socketio.ts
|
|
63
|
+
var SocketIOEventTransport = class {
|
|
64
|
+
io;
|
|
65
|
+
eventName;
|
|
66
|
+
roomPrefix;
|
|
67
|
+
broadcastGlobal;
|
|
68
|
+
globalRoom;
|
|
69
|
+
logger;
|
|
70
|
+
// Local subscribers for server-side subscriptions
|
|
71
|
+
runSubscribers = /* @__PURE__ */ new Map();
|
|
72
|
+
globalSubscribers = /* @__PURE__ */ new Set();
|
|
73
|
+
constructor(config) {
|
|
74
|
+
this.io = config.io;
|
|
75
|
+
this.eventName = config.eventName ?? "workflow:event";
|
|
76
|
+
this.roomPrefix = config.roomPrefix ?? "run:";
|
|
77
|
+
this.broadcastGlobal = config.broadcastGlobal ?? true;
|
|
78
|
+
this.globalRoom = config.globalRoom ?? "workflow:all";
|
|
79
|
+
this.logger = config.logger ?? {
|
|
80
|
+
debug() {
|
|
81
|
+
},
|
|
82
|
+
info() {
|
|
83
|
+
},
|
|
84
|
+
warn() {
|
|
85
|
+
},
|
|
86
|
+
error(message, ...args) {
|
|
87
|
+
console.error(message, ...args);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Emit an event to Socket.IO clients and local subscribers.
|
|
93
|
+
*/
|
|
94
|
+
emit(event) {
|
|
95
|
+
const serializedEvent = {
|
|
96
|
+
...event,
|
|
97
|
+
timestamp: event.timestamp.toISOString()
|
|
98
|
+
};
|
|
99
|
+
const room = `${this.roomPrefix}${event.runId}`;
|
|
100
|
+
this.io.to(room).emit(this.eventName, serializedEvent);
|
|
101
|
+
if (this.broadcastGlobal) {
|
|
102
|
+
this.io.to(this.globalRoom).emit(this.eventName, serializedEvent);
|
|
103
|
+
}
|
|
104
|
+
const runSubs = this.runSubscribers.get(event.runId);
|
|
105
|
+
if (runSubs) {
|
|
106
|
+
for (const callback of runSubs) {
|
|
107
|
+
try {
|
|
108
|
+
callback(event);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.logger.error("Event callback error:", error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const callback of this.globalSubscribers) {
|
|
115
|
+
try {
|
|
116
|
+
callback(event);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.logger.error("Event callback error:", error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Subscribe to events for a specific run (server-side).
|
|
124
|
+
*/
|
|
125
|
+
subscribe(runId, callback) {
|
|
126
|
+
let subs = this.runSubscribers.get(runId);
|
|
127
|
+
if (!subs) {
|
|
128
|
+
subs = /* @__PURE__ */ new Set();
|
|
129
|
+
this.runSubscribers.set(runId, subs);
|
|
130
|
+
}
|
|
131
|
+
subs.add(callback);
|
|
132
|
+
return () => {
|
|
133
|
+
subs.delete(callback);
|
|
134
|
+
if (subs.size === 0) {
|
|
135
|
+
this.runSubscribers.delete(runId);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Subscribe to all events (server-side).
|
|
141
|
+
*/
|
|
142
|
+
subscribeAll(callback) {
|
|
143
|
+
this.globalSubscribers.add(callback);
|
|
144
|
+
return () => {
|
|
145
|
+
this.globalSubscribers.delete(callback);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Set up client subscription handlers on a socket.
|
|
150
|
+
* Call this when a client connects to enable subscription commands.
|
|
151
|
+
*
|
|
152
|
+
* An `authorize` callback is required to prevent unauthorized access to run events.
|
|
153
|
+
* The callback receives the run ID (or `'*'` for global subscriptions) and the socket,
|
|
154
|
+
* and must return `true` to allow or `false` to deny the subscription.
|
|
155
|
+
*
|
|
156
|
+
* @param socket - The Socket.IO socket to set up handlers on
|
|
157
|
+
* @param authorize - Callback to check if a socket can access a run.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* io.on('connection', (socket) => {
|
|
162
|
+
* eventTransport.setupClientHandlers(socket, async (runId, sock) => {
|
|
163
|
+
* // Check if the authenticated user owns this run
|
|
164
|
+
* const userId = sock.data?.userId;
|
|
165
|
+
* return userId ? await canUserAccessRun(userId, runId) : false;
|
|
166
|
+
* });
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
setupClientHandlers(socket, authorize) {
|
|
171
|
+
socket.on("workflow:subscribe", async (...args) => {
|
|
172
|
+
const runId = args[0];
|
|
173
|
+
if (typeof runId !== "string") return;
|
|
174
|
+
try {
|
|
175
|
+
const allowed = await authorize(runId, socket);
|
|
176
|
+
if (!allowed) {
|
|
177
|
+
this.logger.warn(`Subscription denied for run ${runId}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.logger.error("Authorization check failed:", error);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
socket.join(`${this.roomPrefix}${runId}`);
|
|
185
|
+
});
|
|
186
|
+
socket.on("workflow:unsubscribe", (...args) => {
|
|
187
|
+
const runId = args[0];
|
|
188
|
+
if (typeof runId === "string") {
|
|
189
|
+
socket.leave(`${this.roomPrefix}${runId}`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
socket.on("workflow:subscribe:all", async () => {
|
|
193
|
+
try {
|
|
194
|
+
const allowed = await authorize("*", socket);
|
|
195
|
+
if (!allowed) {
|
|
196
|
+
this.logger.warn("Global subscription denied");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this.logger.error("Authorization check failed:", error);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
socket.join(this.globalRoom);
|
|
204
|
+
});
|
|
205
|
+
socket.on("workflow:unsubscribe:all", () => {
|
|
206
|
+
socket.leave(this.globalRoom);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Clear all in-process server-side subscribers.
|
|
211
|
+
* The underlying Socket.IO server socket is not closed here — manage its lifecycle externally.
|
|
212
|
+
*/
|
|
213
|
+
close() {
|
|
214
|
+
this.runSubscribers.clear();
|
|
215
|
+
this.globalSubscribers.clear();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/events/webhook.ts
|
|
220
|
+
import { promises as dns } from "dns";
|
|
221
|
+
var WebhookEventTransport = class {
|
|
222
|
+
endpoints = /* @__PURE__ */ new Map();
|
|
223
|
+
defaultTimeout;
|
|
224
|
+
defaultRetries;
|
|
225
|
+
retryDelay;
|
|
226
|
+
fetchFn;
|
|
227
|
+
allowInsecureUrls;
|
|
228
|
+
maxPayloadBytes;
|
|
229
|
+
maxConcurrentRequests;
|
|
230
|
+
activeRequests = 0;
|
|
231
|
+
requestQueue = [];
|
|
232
|
+
logger;
|
|
233
|
+
// Local subscribers for server-side subscriptions
|
|
234
|
+
runSubscribers = /* @__PURE__ */ new Map();
|
|
235
|
+
globalSubscribers = /* @__PURE__ */ new Set();
|
|
236
|
+
constructor(config = {}) {
|
|
237
|
+
this.defaultTimeout = config.defaultTimeout ?? 5e3;
|
|
238
|
+
this.defaultRetries = config.defaultRetries ?? 3;
|
|
239
|
+
this.retryDelay = config.retryDelay ?? 1e3;
|
|
240
|
+
this.fetchFn = config.fetchFn ?? globalThis.fetch;
|
|
241
|
+
this.allowInsecureUrls = config.allowInsecureUrls ?? false;
|
|
242
|
+
this.maxPayloadBytes = config.maxPayloadBytes ?? 1048576;
|
|
243
|
+
this.maxConcurrentRequests = config.maxConcurrentRequests ?? 50;
|
|
244
|
+
this.logger = config.logger ?? {
|
|
245
|
+
debug() {
|
|
246
|
+
},
|
|
247
|
+
info() {
|
|
248
|
+
},
|
|
249
|
+
warn() {
|
|
250
|
+
},
|
|
251
|
+
error(message, ...args) {
|
|
252
|
+
console.error(message, ...args);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
if (config.endpoints) {
|
|
256
|
+
for (const endpoint of config.endpoints) {
|
|
257
|
+
this.addEndpoint(endpoint);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Add a webhook endpoint.
|
|
263
|
+
* Validates the URL to prevent SSRF attacks.
|
|
264
|
+
* @throws Error if the URL scheme is not allowed
|
|
265
|
+
*/
|
|
266
|
+
addEndpoint(endpoint) {
|
|
267
|
+
this.validateWebhookUrl(endpoint.url);
|
|
268
|
+
this.endpoints.set(endpoint.id, { enabled: true, ...endpoint });
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Remove a webhook endpoint.
|
|
272
|
+
*/
|
|
273
|
+
removeEndpoint(id) {
|
|
274
|
+
return this.endpoints.delete(id);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get all registered endpoints.
|
|
278
|
+
*/
|
|
279
|
+
getEndpoints() {
|
|
280
|
+
return Array.from(this.endpoints.values());
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Enable or disable an endpoint.
|
|
284
|
+
*/
|
|
285
|
+
setEndpointEnabled(id, enabled) {
|
|
286
|
+
const endpoint = this.endpoints.get(id);
|
|
287
|
+
if (endpoint) {
|
|
288
|
+
endpoint.enabled = enabled;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Emit an event to all matching webhook endpoints.
|
|
293
|
+
*/
|
|
294
|
+
emit(event) {
|
|
295
|
+
const runSubs = this.runSubscribers.get(event.runId);
|
|
296
|
+
if (runSubs) {
|
|
297
|
+
for (const callback of runSubs) {
|
|
298
|
+
try {
|
|
299
|
+
callback(event);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
this.logger.error("Event callback error:", error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const callback of this.globalSubscribers) {
|
|
306
|
+
try {
|
|
307
|
+
callback(event);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
this.logger.error("Event callback error:", error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const endpoint of this.endpoints.values()) {
|
|
313
|
+
if (!endpoint.enabled) continue;
|
|
314
|
+
if (!this.matchesFilter(event, endpoint)) continue;
|
|
315
|
+
this.enqueueRequest(
|
|
316
|
+
() => this.sendWebhook(endpoint, event).catch((error) => {
|
|
317
|
+
this.logger.error(`Webhook ${endpoint.id} failed:`, error);
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Subscribe to events for a specific run (server-side).
|
|
324
|
+
*/
|
|
325
|
+
subscribe(runId, callback) {
|
|
326
|
+
let subs = this.runSubscribers.get(runId);
|
|
327
|
+
if (!subs) {
|
|
328
|
+
subs = /* @__PURE__ */ new Set();
|
|
329
|
+
this.runSubscribers.set(runId, subs);
|
|
330
|
+
}
|
|
331
|
+
subs.add(callback);
|
|
332
|
+
return () => {
|
|
333
|
+
subs.delete(callback);
|
|
334
|
+
if (subs.size === 0) {
|
|
335
|
+
this.runSubscribers.delete(runId);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Subscribe to all events (server-side).
|
|
341
|
+
*/
|
|
342
|
+
subscribeAll(callback) {
|
|
343
|
+
this.globalSubscribers.add(callback);
|
|
344
|
+
return () => {
|
|
345
|
+
this.globalSubscribers.delete(callback);
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Close the transport.
|
|
350
|
+
*/
|
|
351
|
+
close() {
|
|
352
|
+
this.runSubscribers.clear();
|
|
353
|
+
this.globalSubscribers.clear();
|
|
354
|
+
this.endpoints.clear();
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Check if an event matches an endpoint's filters.
|
|
358
|
+
*/
|
|
359
|
+
matchesFilter(event, endpoint) {
|
|
360
|
+
if (endpoint.eventTypes && endpoint.eventTypes.length > 0) {
|
|
361
|
+
if (!endpoint.eventTypes.includes(event.eventType)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (endpoint.workflowKinds && endpoint.workflowKinds.length > 0) {
|
|
366
|
+
if (!endpoint.workflowKinds.includes(event.kind)) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Send a webhook with retry support.
|
|
374
|
+
*/
|
|
375
|
+
async sendWebhook(endpoint, event) {
|
|
376
|
+
const timeout = endpoint.timeout ?? this.defaultTimeout;
|
|
377
|
+
const maxRetries = endpoint.retries ?? this.defaultRetries;
|
|
378
|
+
const payload = {
|
|
379
|
+
event: {
|
|
380
|
+
...event,
|
|
381
|
+
timestamp: event.timestamp.toISOString()
|
|
382
|
+
},
|
|
383
|
+
deliveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
384
|
+
webhookId: endpoint.id
|
|
385
|
+
};
|
|
386
|
+
const body = JSON.stringify(payload);
|
|
387
|
+
if (body.length > this.maxPayloadBytes) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`Webhook payload exceeds maximum size (${body.length} bytes > ${this.maxPayloadBytes} bytes)`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const headers = {
|
|
393
|
+
"Content-Type": "application/json",
|
|
394
|
+
...endpoint.headers
|
|
395
|
+
};
|
|
396
|
+
if (endpoint.secret) {
|
|
397
|
+
const signature = await this.signPayload(body, endpoint.secret);
|
|
398
|
+
headers["X-Webhook-Signature"] = signature;
|
|
399
|
+
}
|
|
400
|
+
await this.validateResolvedHost(endpoint.url);
|
|
401
|
+
let lastError;
|
|
402
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
403
|
+
try {
|
|
404
|
+
const controller = new AbortController();
|
|
405
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
406
|
+
const response = await this.fetchFn(endpoint.url, {
|
|
407
|
+
method: "POST",
|
|
408
|
+
headers,
|
|
409
|
+
body,
|
|
410
|
+
signal: controller.signal
|
|
411
|
+
});
|
|
412
|
+
clearTimeout(timeoutId);
|
|
413
|
+
if (response.ok) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
lastError = new Error(
|
|
417
|
+
`Webhook returned ${response.status}: ${response.statusText}`
|
|
418
|
+
);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
421
|
+
}
|
|
422
|
+
if (attempt < maxRetries) {
|
|
423
|
+
await this.sleep(this.retryDelay * Math.pow(2, attempt));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
throw lastError;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Sign a payload using HMAC-SHA256.
|
|
430
|
+
*/
|
|
431
|
+
async signPayload(payload, secret) {
|
|
432
|
+
const encoder = new TextEncoder();
|
|
433
|
+
const keyData = encoder.encode(secret);
|
|
434
|
+
const data = encoder.encode(payload);
|
|
435
|
+
const key = await crypto.subtle.importKey(
|
|
436
|
+
"raw",
|
|
437
|
+
keyData,
|
|
438
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
439
|
+
false,
|
|
440
|
+
["sign"]
|
|
441
|
+
);
|
|
442
|
+
const signature = await crypto.subtle.sign("HMAC", key, data);
|
|
443
|
+
const hashArray = Array.from(new Uint8Array(signature));
|
|
444
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
445
|
+
return `sha256=${hashHex}`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Sleep helper.
|
|
449
|
+
*/
|
|
450
|
+
sleep(ms) {
|
|
451
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Resolve the hostname from a URL and verify the resolved IP is not private/reserved.
|
|
455
|
+
* Prevents DNS rebinding attacks where a public hostname resolves to a private IP at send time.
|
|
456
|
+
*/
|
|
457
|
+
async validateResolvedHost(url) {
|
|
458
|
+
const parsed = new URL(url);
|
|
459
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
460
|
+
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const { address } = await dns.lookup(hostname);
|
|
465
|
+
if (isBlockedIp(address)) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Webhook URL hostname "${hostname}" resolves to blocked IP ${address}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (error instanceof Error && error.message.includes("resolves to blocked IP")) {
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
throw new Error(
|
|
475
|
+
`Failed to resolve webhook URL hostname "${hostname}": ${error}`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Validate a webhook URL to prevent SSRF attacks.
|
|
481
|
+
* Blocks non-HTTPS schemes (unless allowInsecureUrls is set),
|
|
482
|
+
* and rejects private/reserved IP ranges and cloud metadata endpoints.
|
|
483
|
+
*/
|
|
484
|
+
validateWebhookUrl(url) {
|
|
485
|
+
let parsed;
|
|
486
|
+
try {
|
|
487
|
+
parsed = new URL(url);
|
|
488
|
+
} catch {
|
|
489
|
+
throw new Error(`Invalid webhook URL: ${url}`);
|
|
490
|
+
}
|
|
491
|
+
if (!this.allowInsecureUrls && parsed.protocol !== "https:") {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`Webhook URL must use HTTPS (got ${parsed.protocol}). Set allowInsecureUrls for development.`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`Webhook URL must use HTTP(S) scheme (got ${parsed.protocol})`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
502
|
+
if (isBlockedHost(hostname)) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`Webhook URL hostname is blocked (private/reserved): ${hostname}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Enqueue a webhook request with concurrency limiting.
|
|
510
|
+
*/
|
|
511
|
+
enqueueRequest(fn) {
|
|
512
|
+
const execute = () => {
|
|
513
|
+
this.activeRequests++;
|
|
514
|
+
fn().finally(() => {
|
|
515
|
+
this.activeRequests--;
|
|
516
|
+
const next = this.requestQueue.shift();
|
|
517
|
+
if (next) next();
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
if (this.activeRequests < this.maxConcurrentRequests) {
|
|
521
|
+
execute();
|
|
522
|
+
} else {
|
|
523
|
+
this.requestQueue.push(execute);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
function isBlockedHost(hostname) {
|
|
528
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]") {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
if (hostname.startsWith("[")) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
if (hostname === "169.254.169.254") {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
const ipv4Match = hostname.match(
|
|
538
|
+
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
|
|
539
|
+
);
|
|
540
|
+
if (ipv4Match) {
|
|
541
|
+
const [, a, b] = ipv4Match.map(Number);
|
|
542
|
+
if (a === 10) return true;
|
|
543
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
544
|
+
if (a === 192 && b === 168) return true;
|
|
545
|
+
if (a === 127) return true;
|
|
546
|
+
if (a === 169 && b === 254) return true;
|
|
547
|
+
if (a === 0 && b === 0) return true;
|
|
548
|
+
}
|
|
549
|
+
if (hostname.startsWith("fe80:") || hostname.startsWith("[fe80:")) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
function isBlockedIp(ip) {
|
|
555
|
+
const ipv4Match = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
556
|
+
if (ipv4Match) {
|
|
557
|
+
const [, a, b] = ipv4Match.map(Number);
|
|
558
|
+
if (a === 127) return true;
|
|
559
|
+
if (a === 10) return true;
|
|
560
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
561
|
+
if (a === 192 && b === 168) return true;
|
|
562
|
+
if (a === 169 && b === 254) return true;
|
|
563
|
+
if (a === 0 && b === 0) return true;
|
|
564
|
+
}
|
|
565
|
+
if (ip === "::1") return true;
|
|
566
|
+
if (ip.startsWith("fe80:")) return true;
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export {
|
|
571
|
+
MemoryEventTransport,
|
|
572
|
+
SocketIOEventTransport,
|
|
573
|
+
WebhookEventTransport
|
|
574
|
+
};
|
|
575
|
+
//# sourceMappingURL=chunk-XCF4PLN7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/events/memory.ts","../src/events/socketio.ts","../src/events/webhook.ts"],"sourcesContent":["/**\n * In-memory event transport using Node.js EventEmitter.\n * Default transport for development and single-process deployments.\n *\n * NOTE: This adapter is intended for development and testing only. It stores\n * all state in-process with no persistence or multi-process safety guarantees.\n */\n\nimport { EventEmitter } from \"events\";\nimport type {\n EventTransport,\n WorkflowEvent,\n WorkflowEventType,\n EventCallback,\n Unsubscribe,\n} from \"./types\";\n\n/**\n * In-memory event transport implementation using EventEmitter.\n * Events are emitted to subscribers in the same process.\n */\nexport class MemoryEventTransport implements EventTransport {\n private emitter = new EventEmitter();\n\n constructor() {\n // Increase max listeners to avoid warnings in workflows with many steps\n this.emitter.setMaxListeners(100);\n }\n\n /**\n * Emit an event to all subscribers.\n */\n emit(event: WorkflowEvent): void {\n // Emit to global subscribers\n this.emitter.emit(\"event\", event);\n\n // Emit to run-specific subscribers\n this.emitter.emit(`run:${event.runId}`, event);\n\n // Emit to event-type subscribers\n this.emitter.emit(`type:${event.eventType}`, event);\n }\n\n /**\n * Subscribe to events for a specific run.\n */\n subscribe(runId: string, callback: EventCallback): Unsubscribe {\n const channel = `run:${runId}`;\n this.emitter.on(channel, callback);\n return () => {\n this.emitter.off(channel, callback);\n };\n }\n\n /**\n * Subscribe to all events.\n */\n subscribeAll(callback: EventCallback): Unsubscribe {\n this.emitter.on(\"event\", callback);\n return () => {\n this.emitter.off(\"event\", callback);\n };\n }\n\n /**\n * Subscribe to events of a specific type.\n */\n subscribeToType(\n eventType: WorkflowEventType,\n callback: EventCallback,\n ): Unsubscribe {\n const channel = `type:${eventType}`;\n this.emitter.on(channel, callback);\n return () => {\n this.emitter.off(channel, callback);\n };\n }\n\n /**\n * Close the transport and remove all listeners.\n */\n close(): void {\n this.emitter.removeAllListeners();\n }\n\n /**\n * Get the number of listeners for a channel (for testing).\n */\n getListenerCount(channel?: string): number {\n if (channel) {\n return this.emitter.listenerCount(channel);\n }\n return this.emitter.listenerCount(\"event\");\n }\n}\n","/**\n * Socket.IO Event Transport\n *\n * Broadcasts workflow events to connected clients via Socket.IO.\n * Clients can subscribe to specific run IDs or receive all events.\n */\n\nimport type {\n EventTransport,\n EventCallback,\n Unsubscribe,\n WorkflowEvent,\n} from \"./types\";\nimport type { Logger } from \"../core/types\";\n\n/**\n * Minimal Socket.IO Server interface.\n * This allows using the adapter without a direct dependency on socket.io.\n */\nexport interface SocketIOServer {\n to(room: string): {\n emit(event: string, ...args: unknown[]): void;\n };\n emit(event: string, ...args: unknown[]): void;\n}\n\n/**\n * Minimal Socket.IO Socket interface.\n */\nexport interface SocketIOSocket {\n join(room: string): void;\n leave(room: string): void;\n on(event: string, callback: (...args: unknown[]) => void): void;\n}\n\n/**\n * Authorization callback for Socket.IO run subscriptions.\n * Return true to allow the subscription, false to deny it.\n */\nexport type SocketIOAuthorizeFn = (\n runId: string,\n socket: SocketIOSocket,\n) => boolean | Promise<boolean>;\n\n/**\n * Configuration for SocketIOEventTransport.\n */\nexport interface SocketIOEventTransportConfig {\n /** Socket.IO server instance */\n io: SocketIOServer;\n\n /** Event name to use for workflow events (default: 'workflow:event') */\n eventName?: string;\n\n /** Room prefix for run-specific subscriptions (default: 'run:') */\n roomPrefix?: string;\n\n /** Whether to also broadcast to a global room (default: true) */\n broadcastGlobal?: boolean;\n\n /** Global room name (default: 'workflow:all') */\n globalRoom?: string;\n\n /** Logger for transport errors (default: console-based) */\n logger?: Logger;\n}\n\n/**\n * Socket.IO-based event transport for real-time workflow events.\n *\n * @example\n * ```typescript\n * import { Server } from 'socket.io';\n * import { SocketIOEventTransport } from 'stepflow';\n *\n * const io = new Server(httpServer);\n * const eventTransport = new SocketIOEventTransport({ io });\n *\n * const engine = new WorkflowEngine({\n * storage,\n * events: eventTransport,\n * });\n *\n * // Client-side:\n * // socket.emit('workflow:subscribe', runId);\n * // socket.on('workflow:event', (event) => console.log(event));\n * ```\n */\nexport class SocketIOEventTransport implements EventTransport {\n private io: SocketIOServer;\n private eventName: string;\n private roomPrefix: string;\n private broadcastGlobal: boolean;\n private globalRoom: string;\n private logger: Logger;\n\n // Local subscribers for server-side subscriptions\n private runSubscribers = new Map<string, Set<EventCallback>>();\n private globalSubscribers = new Set<EventCallback>();\n\n constructor(config: SocketIOEventTransportConfig) {\n this.io = config.io;\n this.eventName = config.eventName ?? \"workflow:event\";\n this.roomPrefix = config.roomPrefix ?? \"run:\";\n this.broadcastGlobal = config.broadcastGlobal ?? true;\n this.globalRoom = config.globalRoom ?? \"workflow:all\";\n this.logger = config.logger ?? {\n debug() {},\n info() {},\n warn() {},\n error(message: string, ...args: unknown[]) {\n console.error(message, ...args);\n },\n };\n }\n\n /**\n * Emit an event to Socket.IO clients and local subscribers.\n */\n emit(event: WorkflowEvent): void {\n // Serialize the event (convert Date to ISO string for transport)\n const serializedEvent = {\n ...event,\n timestamp: event.timestamp.toISOString(),\n };\n\n // Emit to run-specific room\n const room = `${this.roomPrefix}${event.runId}`;\n this.io.to(room).emit(this.eventName, serializedEvent);\n\n // Emit to global room\n if (this.broadcastGlobal) {\n this.io.to(this.globalRoom).emit(this.eventName, serializedEvent);\n }\n\n // Notify local subscribers\n const runSubs = this.runSubscribers.get(event.runId);\n if (runSubs) {\n for (const callback of runSubs) {\n try {\n callback(event);\n } catch (error) {\n this.logger.error(\"Event callback error:\", error);\n }\n }\n }\n\n for (const callback of this.globalSubscribers) {\n try {\n callback(event);\n } catch (error) {\n this.logger.error(\"Event callback error:\", error);\n }\n }\n }\n\n /**\n * Subscribe to events for a specific run (server-side).\n */\n subscribe(runId: string, callback: EventCallback): Unsubscribe {\n let subs = this.runSubscribers.get(runId);\n if (!subs) {\n subs = new Set();\n this.runSubscribers.set(runId, subs);\n }\n subs.add(callback);\n\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) {\n this.runSubscribers.delete(runId);\n }\n };\n }\n\n /**\n * Subscribe to all events (server-side).\n */\n subscribeAll(callback: EventCallback): Unsubscribe {\n this.globalSubscribers.add(callback);\n return () => {\n this.globalSubscribers.delete(callback);\n };\n }\n\n /**\n * Set up client subscription handlers on a socket.\n * Call this when a client connects to enable subscription commands.\n *\n * An `authorize` callback is required to prevent unauthorized access to run events.\n * The callback receives the run ID (or `'*'` for global subscriptions) and the socket,\n * and must return `true` to allow or `false` to deny the subscription.\n *\n * @param socket - The Socket.IO socket to set up handlers on\n * @param authorize - Callback to check if a socket can access a run.\n *\n * @example\n * ```typescript\n * io.on('connection', (socket) => {\n * eventTransport.setupClientHandlers(socket, async (runId, sock) => {\n * // Check if the authenticated user owns this run\n * const userId = sock.data?.userId;\n * return userId ? await canUserAccessRun(userId, runId) : false;\n * });\n * });\n * ```\n */\n setupClientHandlers(\n socket: SocketIOSocket,\n authorize: SocketIOAuthorizeFn,\n ): void {\n // Subscribe to a specific run\n socket.on(\"workflow:subscribe\", async (...args: unknown[]) => {\n const runId = args[0];\n if (typeof runId !== \"string\") return;\n\n try {\n const allowed = await authorize(runId, socket);\n if (!allowed) {\n this.logger.warn(`Subscription denied for run ${runId}`);\n return;\n }\n } catch (error) {\n this.logger.error(\"Authorization check failed:\", error);\n return;\n }\n\n socket.join(`${this.roomPrefix}${runId}`);\n });\n\n // Unsubscribe from a specific run\n socket.on(\"workflow:unsubscribe\", (...args: unknown[]) => {\n const runId = args[0];\n if (typeof runId === \"string\") {\n socket.leave(`${this.roomPrefix}${runId}`);\n }\n });\n\n // Subscribe to all events (requires authorization to pass with '*' as runId)\n socket.on(\"workflow:subscribe:all\", async () => {\n try {\n const allowed = await authorize(\"*\", socket);\n if (!allowed) {\n this.logger.warn(\"Global subscription denied\");\n return;\n }\n } catch (error) {\n this.logger.error(\"Authorization check failed:\", error);\n return;\n }\n\n socket.join(this.globalRoom);\n });\n\n // Unsubscribe from all events\n socket.on(\"workflow:unsubscribe:all\", () => {\n socket.leave(this.globalRoom);\n });\n }\n\n /**\n * Clear all in-process server-side subscribers.\n * The underlying Socket.IO server socket is not closed here — manage its lifecycle externally.\n */\n close(): void {\n this.runSubscribers.clear();\n this.globalSubscribers.clear();\n }\n}\n","/**\n * Webhook Event Transport\n *\n * Posts workflow events to HTTP endpoints via webhooks.\n * Supports multiple webhooks with optional filtering by event type.\n */\n\nimport { promises as dns } from \"node:dns\";\nimport type {\n EventTransport,\n EventCallback,\n Unsubscribe,\n WorkflowEvent,\n WorkflowEventType,\n} from \"./types\";\nimport type { Logger } from \"../core/types\";\n\n/**\n * Webhook endpoint configuration.\n */\nexport interface WebhookEndpoint {\n /** Unique identifier for this endpoint */\n id: string;\n\n /** URL to POST events to */\n url: string;\n\n /** Optional secret for signing payloads (HMAC-SHA256) */\n secret?: string;\n\n /** Optional filter for specific event types (empty = all events) */\n eventTypes?: WorkflowEventType[];\n\n /** Optional filter for specific workflow kinds (empty = all kinds) */\n workflowKinds?: string[];\n\n /** Optional custom headers to include */\n headers?: Record<string, string>;\n\n /** Whether this endpoint is enabled (default: true) */\n enabled?: boolean;\n\n /** Timeout in milliseconds (default: 5000) */\n timeout?: number;\n\n /** Number of retry attempts on failure (default: 3) */\n retries?: number;\n}\n\n/**\n * Configuration for WebhookEventTransport.\n */\nexport interface WebhookEventTransportConfig {\n /** Initial webhook endpoints */\n endpoints?: WebhookEndpoint[];\n\n /** Default timeout for all webhooks in ms (default: 5000) */\n defaultTimeout?: number;\n\n /** Default retry attempts (default: 3) */\n defaultRetries?: number;\n\n /** Retry delay in ms (default: 1000) */\n retryDelay?: number;\n\n /** Custom fetch function (for testing or custom HTTP clients) */\n fetchFn?: typeof fetch;\n\n /**\n * Whether to allow non-HTTPS URLs (default: false).\n * Set to true only in development environments.\n */\n allowInsecureUrls?: boolean;\n\n /**\n * Maximum payload size in bytes (default: 1048576 = 1 MB).\n * Payloads exceeding this limit will be rejected.\n */\n maxPayloadBytes?: number;\n\n /**\n * Maximum concurrent webhook requests across all endpoints (default: 50).\n * Requests beyond this limit are queued.\n */\n maxConcurrentRequests?: number;\n\n /** Logger for webhook transport errors (default: console-based) */\n logger?: import(\"../core/types\").Logger;\n}\n\n/**\n * Webhook payload structure sent to external endpoints.\n */\nexport interface WebhookPayload {\n /**\n * The workflow event with `timestamp` serialized as an ISO 8601 string.\n * The `Date` is converted to a string before delivery so that JSON consumers\n * receive a portable, timezone-aware representation without needing Date parsing.\n */\n event: Omit<WorkflowEvent, \"timestamp\"> & { timestamp: string };\n /** ISO 8601 timestamp of when this payload was delivered. */\n deliveredAt: string;\n /** ID of the webhook endpoint that received this delivery. */\n webhookId: string;\n}\n\n/**\n * Webhook-based event transport for integrating with external systems.\n *\n * @example\n * ```typescript\n * const webhookTransport = new WebhookEventTransport({\n * endpoints: [\n * {\n * id: 'slack-notifications',\n * url: 'https://hooks.slack.com/...',\n * eventTypes: ['run.completed', 'run.failed'],\n * },\n * {\n * id: 'analytics',\n * url: 'https://api.analytics.com/events',\n * secret: process.env.WEBHOOK_SECRET, // Use a cryptographically random secret (≥ 32 bytes)\n * },\n * ],\n * });\n *\n * const engine = new WorkflowEngine({\n * storage,\n * events: webhookTransport,\n * });\n * ```\n */\nexport class WebhookEventTransport implements EventTransport {\n private endpoints = new Map<string, WebhookEndpoint>();\n private defaultTimeout: number;\n private defaultRetries: number;\n private retryDelay: number;\n private fetchFn: typeof fetch;\n private allowInsecureUrls: boolean;\n private maxPayloadBytes: number;\n private maxConcurrentRequests: number;\n private activeRequests = 0;\n private requestQueue: Array<() => void> = [];\n private logger: Logger;\n\n // Local subscribers for server-side subscriptions\n private runSubscribers = new Map<string, Set<EventCallback>>();\n private globalSubscribers = new Set<EventCallback>();\n\n constructor(config: WebhookEventTransportConfig = {}) {\n this.defaultTimeout = config.defaultTimeout ?? 5000;\n this.defaultRetries = config.defaultRetries ?? 3;\n this.retryDelay = config.retryDelay ?? 1000;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n this.allowInsecureUrls = config.allowInsecureUrls ?? false;\n this.maxPayloadBytes = config.maxPayloadBytes ?? 1_048_576; // 1 MB\n this.maxConcurrentRequests = config.maxConcurrentRequests ?? 50;\n this.logger = config.logger ?? {\n debug() {},\n info() {},\n warn() {},\n error(message: string, ...args: unknown[]) {\n console.error(message, ...args);\n },\n };\n\n // Register initial endpoints (validates URLs)\n if (config.endpoints) {\n for (const endpoint of config.endpoints) {\n this.addEndpoint(endpoint);\n }\n }\n }\n\n /**\n * Add a webhook endpoint.\n * Validates the URL to prevent SSRF attacks.\n * @throws Error if the URL scheme is not allowed\n */\n addEndpoint(endpoint: WebhookEndpoint): void {\n this.validateWebhookUrl(endpoint.url);\n this.endpoints.set(endpoint.id, { enabled: true, ...endpoint });\n }\n\n /**\n * Remove a webhook endpoint.\n */\n removeEndpoint(id: string): boolean {\n return this.endpoints.delete(id);\n }\n\n /**\n * Get all registered endpoints.\n */\n getEndpoints(): WebhookEndpoint[] {\n return Array.from(this.endpoints.values());\n }\n\n /**\n * Enable or disable an endpoint.\n */\n setEndpointEnabled(id: string, enabled: boolean): void {\n const endpoint = this.endpoints.get(id);\n if (endpoint) {\n endpoint.enabled = enabled;\n }\n }\n\n /**\n * Emit an event to all matching webhook endpoints.\n */\n emit(event: WorkflowEvent): void {\n // Notify local subscribers first (synchronously)\n const runSubs = this.runSubscribers.get(event.runId);\n if (runSubs) {\n for (const callback of runSubs) {\n try {\n callback(event);\n } catch (error) {\n this.logger.error(\"Event callback error:\", error);\n }\n }\n }\n\n for (const callback of this.globalSubscribers) {\n try {\n callback(event);\n } catch (error) {\n this.logger.error(\"Event callback error:\", error);\n }\n }\n\n // Send to webhooks asynchronously with concurrency control\n for (const endpoint of this.endpoints.values()) {\n if (!endpoint.enabled) continue;\n if (!this.matchesFilter(event, endpoint)) continue;\n\n this.enqueueRequest(() =>\n this.sendWebhook(endpoint, event).catch((error) => {\n this.logger.error(`Webhook ${endpoint.id} failed:`, error);\n }),\n );\n }\n }\n\n /**\n * Subscribe to events for a specific run (server-side).\n */\n subscribe(runId: string, callback: EventCallback): Unsubscribe {\n let subs = this.runSubscribers.get(runId);\n if (!subs) {\n subs = new Set();\n this.runSubscribers.set(runId, subs);\n }\n subs.add(callback);\n\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) {\n this.runSubscribers.delete(runId);\n }\n };\n }\n\n /**\n * Subscribe to all events (server-side).\n */\n subscribeAll(callback: EventCallback): Unsubscribe {\n this.globalSubscribers.add(callback);\n return () => {\n this.globalSubscribers.delete(callback);\n };\n }\n\n /**\n * Close the transport.\n */\n close(): void {\n this.runSubscribers.clear();\n this.globalSubscribers.clear();\n this.endpoints.clear();\n }\n\n /**\n * Check if an event matches an endpoint's filters.\n */\n private matchesFilter(\n event: WorkflowEvent,\n endpoint: WebhookEndpoint,\n ): boolean {\n // Check event type filter\n if (endpoint.eventTypes && endpoint.eventTypes.length > 0) {\n if (!endpoint.eventTypes.includes(event.eventType)) {\n return false;\n }\n }\n\n // Check workflow kind filter\n if (endpoint.workflowKinds && endpoint.workflowKinds.length > 0) {\n if (!endpoint.workflowKinds.includes(event.kind)) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Send a webhook with retry support.\n */\n private async sendWebhook(\n endpoint: WebhookEndpoint,\n event: WorkflowEvent,\n ): Promise<void> {\n const timeout = endpoint.timeout ?? this.defaultTimeout;\n const maxRetries = endpoint.retries ?? this.defaultRetries;\n\n const payload: WebhookPayload = {\n event: {\n ...event,\n timestamp: event.timestamp.toISOString(),\n },\n deliveredAt: new Date().toISOString(),\n webhookId: endpoint.id,\n };\n\n const body = JSON.stringify(payload);\n\n // Enforce payload size limit to prevent excessive memory use and network abuse\n if (body.length > this.maxPayloadBytes) {\n throw new Error(\n `Webhook payload exceeds maximum size (${body.length} bytes > ${this.maxPayloadBytes} bytes)`,\n );\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...endpoint.headers,\n };\n\n // Sign payload if secret is provided\n if (endpoint.secret) {\n const signature = await this.signPayload(body, endpoint.secret);\n headers[\"X-Webhook-Signature\"] = signature;\n }\n\n // Resolve hostname and validate the resolved IP is not private/reserved (SSRF protection).\n // This prevents DNS rebinding attacks where a hostname initially resolves to a public IP\n // but later resolves to a private IP.\n await this.validateResolvedHost(endpoint.url);\n\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const response = await this.fetchFn(endpoint.url, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (response.ok) {\n return; // Success\n }\n\n lastError = new Error(\n `Webhook returned ${response.status}: ${response.statusText}`,\n );\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n }\n\n // Wait before retrying (except on last attempt)\n if (attempt < maxRetries) {\n await this.sleep(this.retryDelay * Math.pow(2, attempt));\n }\n }\n\n throw lastError;\n }\n\n /**\n * Sign a payload using HMAC-SHA256.\n */\n private async signPayload(payload: string, secret: string): Promise<string> {\n // Use Web Crypto API for HMAC-SHA256\n const encoder = new TextEncoder();\n const keyData = encoder.encode(secret);\n const data = encoder.encode(payload);\n\n const key = await crypto.subtle.importKey(\n \"raw\",\n keyData,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n\n const signature = await crypto.subtle.sign(\"HMAC\", key, data);\n const hashArray = Array.from(new Uint8Array(signature));\n const hashHex = hashArray\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return `sha256=${hashHex}`;\n }\n\n /**\n * Sleep helper.\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /**\n * Resolve the hostname from a URL and verify the resolved IP is not private/reserved.\n * Prevents DNS rebinding attacks where a public hostname resolves to a private IP at send time.\n */\n private async validateResolvedHost(url: string): Promise<void> {\n const parsed = new URL(url);\n const hostname = parsed.hostname.toLowerCase();\n\n // Skip DNS resolution for raw IP addresses — already checked by isBlockedHost in validateWebhookUrl\n if (/^(\\d{1,3}\\.){3}\\d{1,3}$/.test(hostname)) {\n return;\n }\n\n try {\n const { address } = await dns.lookup(hostname);\n if (isBlockedIp(address)) {\n throw new Error(\n `Webhook URL hostname \"${hostname}\" resolves to blocked IP ${address}`,\n );\n }\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"resolves to blocked IP\")\n ) {\n throw error;\n }\n throw new Error(\n `Failed to resolve webhook URL hostname \"${hostname}\": ${error}`,\n );\n }\n }\n\n /**\n * Validate a webhook URL to prevent SSRF attacks.\n * Blocks non-HTTPS schemes (unless allowInsecureUrls is set),\n * and rejects private/reserved IP ranges and cloud metadata endpoints.\n */\n private validateWebhookUrl(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`Invalid webhook URL: ${url}`);\n }\n\n // Scheme check\n if (!this.allowInsecureUrls && parsed.protocol !== \"https:\") {\n throw new Error(\n `Webhook URL must use HTTPS (got ${parsed.protocol}). Set allowInsecureUrls for development.`,\n );\n }\n\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `Webhook URL must use HTTP(S) scheme (got ${parsed.protocol})`,\n );\n }\n\n // Block dangerous hostnames\n const hostname = parsed.hostname.toLowerCase();\n if (isBlockedHost(hostname)) {\n throw new Error(\n `Webhook URL hostname is blocked (private/reserved): ${hostname}`,\n );\n }\n }\n\n /**\n * Enqueue a webhook request with concurrency limiting.\n */\n private enqueueRequest(fn: () => Promise<void>): void {\n const execute = () => {\n this.activeRequests++;\n fn().finally(() => {\n this.activeRequests--;\n const next = this.requestQueue.shift();\n if (next) next();\n });\n };\n\n if (this.activeRequests < this.maxConcurrentRequests) {\n execute();\n } else {\n this.requestQueue.push(execute);\n }\n }\n}\n\n/**\n * Check if a hostname resolves to a blocked (private/reserved) address.\n * Blocks loopback, RFC 1918 private ranges, link-local, and cloud metadata IPs.\n */\nfunction isBlockedHost(hostname: string): boolean {\n // Loopback (Node's URL parser wraps IPv6 in brackets, e.g. \"[::1]\")\n if (\n hostname === \"localhost\" ||\n hostname === \"127.0.0.1\" ||\n hostname === \"::1\" ||\n hostname === \"[::1]\"\n ) {\n return true;\n }\n\n // Block all IPv6 address literals (wrapped in brackets by URL parser)\n if (hostname.startsWith(\"[\")) {\n return true;\n }\n\n // Cloud metadata endpoint\n if (hostname === \"169.254.169.254\") {\n return true;\n }\n\n // Check for IP addresses in private/reserved ranges\n const ipv4Match = hostname.match(\n /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/,\n );\n if (ipv4Match) {\n const [, a, b] = ipv4Match.map(Number);\n // 10.0.0.0/8\n if (a === 10) return true;\n // 172.16.0.0/12\n if (a === 172 && b >= 16 && b <= 31) return true;\n // 192.168.0.0/16\n if (a === 192 && b === 168) return true;\n // 127.0.0.0/8\n if (a === 127) return true;\n // 169.254.0.0/16 (link-local)\n if (a === 169 && b === 254) return true;\n // 0.0.0.0\n if (a === 0 && b === 0) return true;\n }\n\n // IPv6 link-local (fe80::)\n if (hostname.startsWith(\"fe80:\") || hostname.startsWith(\"[fe80:\")) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if a resolved IP address falls within a private/reserved range.\n * Used for runtime DNS resolution validation to prevent SSRF via DNS rebinding.\n */\nfunction isBlockedIp(ip: string): boolean {\n const ipv4Match = ip.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (ipv4Match) {\n const [, a, b] = ipv4Match.map(Number);\n // 127.0.0.0/8 (loopback)\n if (a === 127) return true;\n // 10.0.0.0/8\n if (a === 10) return true;\n // 172.16.0.0/12\n if (a === 172 && b >= 16 && b <= 31) return true;\n // 192.168.0.0/16\n if (a === 192 && b === 168) return true;\n // 169.254.0.0/16 (link-local, including cloud metadata 169.254.169.254)\n if (a === 169 && b === 254) return true;\n // 0.0.0.0\n if (a === 0 && b === 0) return true;\n }\n\n // IPv6 loopback\n if (ip === \"::1\") return true;\n // IPv6 link-local\n if (ip.startsWith(\"fe80:\")) return true;\n\n return false;\n}\n"],"mappings":"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","names":[]}
|