@multiplier-labs/stepflow 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  2. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  3. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  4. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  5. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  6. package/.github/CODEOWNERS +15 -0
  7. package/.github/dependabot.yml +23 -0
  8. package/.github/workflows/ci.yml +38 -0
  9. package/.github/workflows/publish.yml +11 -5
  10. package/.github/workflows/security.yml +29 -0
  11. package/CHANGELOG.md +34 -0
  12. package/README.md +24 -0
  13. package/SECURITY.md +40 -0
  14. package/dist/{chunk-FQCT3I2A.js → chunk-4VOKMGP6.js} +23 -42
  15. package/dist/chunk-4VOKMGP6.js.map +1 -0
  16. package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
  17. package/dist/chunk-GYPNJBDP.js.map +1 -0
  18. package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
  19. package/dist/chunk-KHTUWLVZ.js.map +1 -0
  20. package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
  21. package/dist/chunk-ML35PIHX.js.map +1 -0
  22. package/dist/events/index.d.ts +4 -4
  23. package/dist/events/index.js +1 -1
  24. package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
  25. package/dist/index.d.ts +14 -12
  26. package/dist/index.js +40 -82
  27. package/dist/index.js.map +1 -1
  28. package/dist/scheduler/index.d.ts +4 -4
  29. package/dist/scheduler/index.js +2 -2
  30. package/dist/storage/index.d.ts +13 -13
  31. package/dist/storage/index.js +2 -2
  32. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  33. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  34. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  35. package/docs/stepflow-api-reference.md +2 -3
  36. package/package.json +7 -6
  37. package/src/core/engine.test.ts +466 -522
  38. package/src/core/engine.ts +42 -67
  39. package/src/core/index.ts +3 -3
  40. package/src/core/orchestrator.ts +64 -83
  41. package/src/core/types.ts +7 -29
  42. package/src/events/index.ts +4 -4
  43. package/src/events/memory.test.ts +39 -41
  44. package/src/events/memory.ts +7 -10
  45. package/src/events/socketio.test.ts +112 -162
  46. package/src/events/socketio.ts +20 -27
  47. package/src/events/types.ts +16 -19
  48. package/src/events/webhook.test.ts +197 -235
  49. package/src/events/webhook.ts +44 -71
  50. package/src/index.ts +19 -19
  51. package/src/planning/index.ts +6 -3
  52. package/src/planning/planner.ts +66 -91
  53. package/src/planning/planning.test.ts +303 -366
  54. package/src/planning/registry.test.ts +204 -251
  55. package/src/planning/registry.ts +39 -43
  56. package/src/planning/types.ts +23 -27
  57. package/src/scheduler/cron.test.ts +141 -150
  58. package/src/scheduler/cron.ts +32 -67
  59. package/src/scheduler/index.ts +4 -14
  60. package/src/scheduler/postgres-persistence.ts +71 -99
  61. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  62. package/src/scheduler/sqlite-persistence.ts +12 -19
  63. package/src/scheduler/types.ts +4 -9
  64. package/src/storage/index.ts +5 -5
  65. package/src/storage/memory.test.ts +133 -183
  66. package/src/storage/memory.ts +21 -42
  67. package/src/storage/postgres.ts +197 -393
  68. package/src/storage/sqlite.test.ts +154 -281
  69. package/src/storage/sqlite.ts +50 -85
  70. package/src/storage/types.ts +33 -58
  71. package/src/utils/errors.test.ts +78 -80
  72. package/src/utils/errors.ts +27 -57
  73. package/src/utils/id.ts +3 -3
  74. package/src/utils/index.ts +4 -4
  75. package/src/utils/logger.test.ts +52 -58
  76. package/src/utils/logger.ts +4 -4
  77. package/src/utils/postgres-deps.ts +4 -4
  78. package/src/utils/retry.test.ts +47 -67
  79. package/src/utils/retry.ts +9 -9
  80. package/dist/chunk-FQCT3I2A.js.map +0 -1
  81. package/dist/chunk-GXY66V7Q.js.map +0 -1
  82. package/dist/chunk-U5QGN3RT.js.map +0 -1
  83. package/dist/chunk-XCF4PLN7.js.map +0 -1
  84. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -1,27 +1,25 @@
1
- import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
- import * as dns from "node:dns";
3
- import { WebhookEventTransport, WebhookEndpoint } from "./webhook";
4
- import type { WorkflowEvent } from "./types";
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import * as dns from 'node:dns';
3
+ import { WebhookEventTransport, WebhookEndpoint } from './webhook';
4
+ import type { WorkflowEvent } from './types';
5
5
 
6
- vi.mock("node:dns", () => ({
6
+ vi.mock('node:dns', () => ({
7
7
  promises: {
8
- lookup: vi.fn().mockResolvedValue({ address: "93.184.216.34", family: 4 }),
8
+ lookup: vi.fn().mockResolvedValue({ address: '93.184.216.34', family: 4 }),
9
9
  },
10
10
  }));
11
11
 
12
- function createTestEvent(
13
- overrides: Partial<WorkflowEvent> = {},
14
- ): WorkflowEvent {
12
+ function createTestEvent(overrides: Partial<WorkflowEvent> = {}): WorkflowEvent {
15
13
  return {
16
- eventType: "run.started",
17
- runId: "run-123",
18
- kind: "test.workflow",
19
- timestamp: new Date("2024-01-01T12:00:00Z"),
14
+ eventType: 'run.started',
15
+ runId: 'run-123',
16
+ kind: 'test.workflow',
17
+ timestamp: new Date('2024-01-01T12:00:00Z'),
20
18
  ...overrides,
21
19
  };
22
20
  }
23
21
 
24
- describe("WebhookEventTransport", () => {
22
+ describe('WebhookEventTransport', () => {
25
23
  let transport: WebhookEventTransport;
26
24
  let mockFetch: ReturnType<typeof vi.fn>;
27
25
 
@@ -38,72 +36,72 @@ describe("WebhookEventTransport", () => {
38
36
  transport.close();
39
37
  });
40
38
 
41
- describe("addEndpoint / removeEndpoint", () => {
42
- it("should add an endpoint", () => {
39
+ describe('addEndpoint / removeEndpoint', () => {
40
+ it('should add an endpoint', () => {
43
41
  const endpoint: WebhookEndpoint = {
44
- id: "test-1",
45
- url: "https://example.com/webhook",
42
+ id: 'test-1',
43
+ url: 'https://example.com/webhook',
46
44
  };
47
45
 
48
46
  transport.addEndpoint(endpoint);
49
47
 
50
48
  expect(transport.getEndpoints()).toHaveLength(1);
51
- expect(transport.getEndpoints()[0].id).toBe("test-1");
49
+ expect(transport.getEndpoints()[0].id).toBe('test-1');
52
50
  });
53
51
 
54
- it("should add endpoint with enabled=true by default", () => {
52
+ it('should add endpoint with enabled=true by default', () => {
55
53
  transport.addEndpoint({
56
- id: "test-1",
57
- url: "https://example.com/webhook",
54
+ id: 'test-1',
55
+ url: 'https://example.com/webhook',
58
56
  });
59
57
 
60
58
  expect(transport.getEndpoints()[0].enabled).toBe(true);
61
59
  });
62
60
 
63
- it("should remove an endpoint", () => {
61
+ it('should remove an endpoint', () => {
64
62
  transport.addEndpoint({
65
- id: "test-1",
66
- url: "https://example.com/webhook",
63
+ id: 'test-1',
64
+ url: 'https://example.com/webhook',
67
65
  });
68
66
 
69
- const removed = transport.removeEndpoint("test-1");
67
+ const removed = transport.removeEndpoint('test-1');
70
68
 
71
69
  expect(removed).toBe(true);
72
70
  expect(transport.getEndpoints()).toHaveLength(0);
73
71
  });
74
72
 
75
- it("should return false when removing non-existent endpoint", () => {
76
- const removed = transport.removeEndpoint("nonexistent");
73
+ it('should return false when removing non-existent endpoint', () => {
74
+ const removed = transport.removeEndpoint('nonexistent');
77
75
  expect(removed).toBe(false);
78
76
  });
79
77
  });
80
78
 
81
- describe("setEndpointEnabled", () => {
82
- it("should enable/disable an endpoint", () => {
79
+ describe('setEndpointEnabled', () => {
80
+ it('should enable/disable an endpoint', () => {
83
81
  transport.addEndpoint({
84
- id: "test-1",
85
- url: "https://example.com/webhook",
82
+ id: 'test-1',
83
+ url: 'https://example.com/webhook',
86
84
  enabled: true,
87
85
  });
88
86
 
89
- transport.setEndpointEnabled("test-1", false);
87
+ transport.setEndpointEnabled('test-1', false);
90
88
  expect(transport.getEndpoints()[0].enabled).toBe(false);
91
89
 
92
- transport.setEndpointEnabled("test-1", true);
90
+ transport.setEndpointEnabled('test-1', true);
93
91
  expect(transport.getEndpoints()[0].enabled).toBe(true);
94
92
  });
95
93
 
96
- it("should do nothing for non-existent endpoint", () => {
97
- transport.setEndpointEnabled("nonexistent", false);
94
+ it('should do nothing for non-existent endpoint', () => {
95
+ transport.setEndpointEnabled('nonexistent', false);
98
96
  // Should not throw
99
97
  });
100
98
  });
101
99
 
102
- describe("emit", () => {
103
- it("should send event to enabled endpoints", async () => {
100
+ describe('emit', () => {
101
+ it('should send event to enabled endpoints', async () => {
104
102
  transport.addEndpoint({
105
- id: "test-1",
106
- url: "https://example.com/webhook",
103
+ id: 'test-1',
104
+ url: 'https://example.com/webhook',
107
105
  });
108
106
 
109
107
  const event = createTestEvent();
@@ -114,20 +112,20 @@ describe("WebhookEventTransport", () => {
114
112
 
115
113
  expect(mockFetch).toHaveBeenCalledTimes(1);
116
114
  expect(mockFetch).toHaveBeenCalledWith(
117
- "https://example.com/webhook",
115
+ 'https://example.com/webhook',
118
116
  expect.objectContaining({
119
- method: "POST",
117
+ method: 'POST',
120
118
  headers: expect.objectContaining({
121
- "Content-Type": "application/json",
119
+ 'Content-Type': 'application/json',
122
120
  }),
123
- }),
121
+ })
124
122
  );
125
123
  });
126
124
 
127
- it("should not send to disabled endpoints", async () => {
125
+ it('should not send to disabled endpoints', async () => {
128
126
  transport.addEndpoint({
129
- id: "test-1",
130
- url: "https://example.com/webhook",
127
+ id: 'test-1',
128
+ url: 'https://example.com/webhook',
131
129
  enabled: false,
132
130
  });
133
131
 
@@ -139,11 +137,11 @@ describe("WebhookEventTransport", () => {
139
137
  expect(mockFetch).not.toHaveBeenCalled();
140
138
  });
141
139
 
142
- it("should include custom headers", async () => {
140
+ it('should include custom headers', async () => {
143
141
  transport.addEndpoint({
144
- id: "test-1",
145
- url: "https://example.com/webhook",
146
- headers: { "X-Custom-Header": "custom-value" },
142
+ id: 'test-1',
143
+ url: 'https://example.com/webhook',
144
+ headers: { 'X-Custom-Header': 'custom-value' },
147
145
  });
148
146
 
149
147
  transport.emit(createTestEvent());
@@ -151,18 +149,18 @@ describe("WebhookEventTransport", () => {
151
149
  await new Promise((r) => setTimeout(r, 50));
152
150
 
153
151
  const callArgs = mockFetch.mock.calls[0];
154
- expect(callArgs[1].headers["X-Custom-Header"]).toBe("custom-value");
152
+ expect(callArgs[1].headers['X-Custom-Header']).toBe('custom-value');
155
153
  });
156
154
 
157
- it("should serialize payload correctly", async () => {
155
+ it('should serialize payload correctly', async () => {
158
156
  transport.addEndpoint({
159
- id: "test-1",
160
- url: "https://example.com/webhook",
157
+ id: 'test-1',
158
+ url: 'https://example.com/webhook',
161
159
  });
162
160
 
163
161
  const event = createTestEvent({
164
- runId: "run-456",
165
- kind: "my.workflow",
162
+ runId: 'run-456',
163
+ kind: 'my.workflow',
166
164
  });
167
165
  transport.emit(event);
168
166
 
@@ -171,81 +169,81 @@ describe("WebhookEventTransport", () => {
171
169
  const callArgs = mockFetch.mock.calls[0];
172
170
  const body = JSON.parse(callArgs[1].body);
173
171
 
174
- expect(body.event.runId).toBe("run-456");
175
- expect(body.event.kind).toBe("my.workflow");
176
- expect(body.event.timestamp).toBe("2024-01-01T12:00:00.000Z");
177
- expect(body.webhookId).toBe("test-1");
172
+ expect(body.event.runId).toBe('run-456');
173
+ expect(body.event.kind).toBe('my.workflow');
174
+ expect(body.event.timestamp).toBe('2024-01-01T12:00:00.000Z');
175
+ expect(body.webhookId).toBe('test-1');
178
176
  expect(body.deliveredAt).toBeDefined();
179
177
  });
180
178
  });
181
179
 
182
- describe("event filtering", () => {
183
- it("should filter by event type", async () => {
180
+ describe('event filtering', () => {
181
+ it('should filter by event type', async () => {
184
182
  transport.addEndpoint({
185
- id: "test-1",
186
- url: "https://example.com/webhook",
187
- eventTypes: ["run.completed", "run.failed"],
183
+ id: 'test-1',
184
+ url: 'https://example.com/webhook',
185
+ eventTypes: ['run.completed', 'run.failed'],
188
186
  });
189
187
 
190
188
  // Should be filtered out
191
- transport.emit(createTestEvent({ eventType: "run.started" }));
189
+ transport.emit(createTestEvent({ eventType: 'run.started' }));
192
190
 
193
191
  // Should be sent
194
- transport.emit(createTestEvent({ eventType: "run.completed" }));
192
+ transport.emit(createTestEvent({ eventType: 'run.completed' }));
195
193
 
196
194
  await new Promise((r) => setTimeout(r, 50));
197
195
 
198
196
  expect(mockFetch).toHaveBeenCalledTimes(1);
199
197
  });
200
198
 
201
- it("should filter by workflow kind", async () => {
199
+ it('should filter by workflow kind', async () => {
202
200
  transport.addEndpoint({
203
- id: "test-1",
204
- url: "https://example.com/webhook",
205
- workflowKinds: ["order.process", "order.ship"],
201
+ id: 'test-1',
202
+ url: 'https://example.com/webhook',
203
+ workflowKinds: ['order.process', 'order.ship'],
206
204
  });
207
205
 
208
206
  // Should be filtered out
209
- transport.emit(createTestEvent({ kind: "user.notify" }));
207
+ transport.emit(createTestEvent({ kind: 'user.notify' }));
210
208
 
211
209
  // Should be sent
212
- transport.emit(createTestEvent({ kind: "order.process" }));
210
+ transport.emit(createTestEvent({ kind: 'order.process' }));
213
211
 
214
212
  await new Promise((r) => setTimeout(r, 50));
215
213
 
216
214
  expect(mockFetch).toHaveBeenCalledTimes(1);
217
215
  });
218
216
 
219
- it("should apply both filters (AND logic)", async () => {
217
+ it('should apply both filters (AND logic)', async () => {
220
218
  transport.addEndpoint({
221
- id: "test-1",
222
- url: "https://example.com/webhook",
223
- eventTypes: ["run.completed"],
224
- workflowKinds: ["order.process"],
219
+ id: 'test-1',
220
+ url: 'https://example.com/webhook',
221
+ eventTypes: ['run.completed'],
222
+ workflowKinds: ['order.process'],
225
223
  });
226
224
 
227
225
  // Wrong event type
228
226
  transport.emit(
229
227
  createTestEvent({
230
- eventType: "run.started",
231
- kind: "order.process",
232
- }),
228
+ eventType: 'run.started',
229
+ kind: 'order.process',
230
+ })
233
231
  );
234
232
 
235
233
  // Wrong workflow kind
236
234
  transport.emit(
237
235
  createTestEvent({
238
- eventType: "run.completed",
239
- kind: "user.notify",
240
- }),
236
+ eventType: 'run.completed',
237
+ kind: 'user.notify',
238
+ })
241
239
  );
242
240
 
243
241
  // Both match
244
242
  transport.emit(
245
243
  createTestEvent({
246
- eventType: "run.completed",
247
- kind: "order.process",
248
- }),
244
+ eventType: 'run.completed',
245
+ kind: 'order.process',
246
+ })
249
247
  );
250
248
 
251
249
  await new Promise((r) => setTimeout(r, 50));
@@ -253,14 +251,14 @@ describe("WebhookEventTransport", () => {
253
251
  expect(mockFetch).toHaveBeenCalledTimes(1);
254
252
  });
255
253
 
256
- it("should send to all endpoints when no filters", async () => {
254
+ it('should send to all endpoints when no filters', async () => {
257
255
  transport.addEndpoint({
258
- id: "test-1",
259
- url: "https://example.com/webhook1",
256
+ id: 'test-1',
257
+ url: 'https://example.com/webhook1',
260
258
  });
261
259
  transport.addEndpoint({
262
- id: "test-2",
263
- url: "https://example.com/webhook2",
260
+ id: 'test-2',
261
+ url: 'https://example.com/webhook2',
264
262
  });
265
263
 
266
264
  transport.emit(createTestEvent());
@@ -271,12 +269,12 @@ describe("WebhookEventTransport", () => {
271
269
  });
272
270
  });
273
271
 
274
- describe("HMAC signing", () => {
275
- it("should sign payload when secret is provided", async () => {
272
+ describe('HMAC signing', () => {
273
+ it('should sign payload when secret is provided', async () => {
276
274
  transport.addEndpoint({
277
- id: "test-1",
278
- url: "https://example.com/webhook",
279
- secret: "my-secret-key",
275
+ id: 'test-1',
276
+ url: 'https://example.com/webhook',
277
+ secret: 'my-secret-key',
280
278
  });
281
279
 
282
280
  transport.emit(createTestEvent());
@@ -284,16 +282,16 @@ describe("WebhookEventTransport", () => {
284
282
  await new Promise((r) => setTimeout(r, 50));
285
283
 
286
284
  const callArgs = mockFetch.mock.calls[0];
287
- const signature = callArgs[1].headers["X-Webhook-Signature"];
285
+ const signature = callArgs[1].headers['X-Webhook-Signature'];
288
286
 
289
287
  expect(signature).toBeDefined();
290
288
  expect(signature).toMatch(/^sha256=[a-f0-9]{64}$/);
291
289
  });
292
290
 
293
- it("should not include signature header when no secret", async () => {
291
+ it('should not include signature header when no secret', async () => {
294
292
  transport.addEndpoint({
295
- id: "test-1",
296
- url: "https://example.com/webhook",
293
+ id: 'test-1',
294
+ url: 'https://example.com/webhook',
297
295
  });
298
296
 
299
297
  transport.emit(createTestEvent());
@@ -301,15 +299,15 @@ describe("WebhookEventTransport", () => {
301
299
  await new Promise((r) => setTimeout(r, 50));
302
300
 
303
301
  const callArgs = mockFetch.mock.calls[0];
304
- expect(callArgs[1].headers["X-Webhook-Signature"]).toBeUndefined();
302
+ expect(callArgs[1].headers['X-Webhook-Signature']).toBeUndefined();
305
303
  });
306
304
  });
307
305
 
308
- describe("retries", () => {
309
- it("should retry on failure", async () => {
306
+ describe('retries', () => {
307
+ it('should retry on failure', async () => {
310
308
  mockFetch
311
- .mockRejectedValueOnce(new Error("Network error"))
312
- .mockRejectedValueOnce(new Error("Network error"))
309
+ .mockRejectedValueOnce(new Error('Network error'))
310
+ .mockRejectedValueOnce(new Error('Network error'))
313
311
  .mockResolvedValueOnce({ ok: true });
314
312
 
315
313
  transport = new WebhookEventTransport({
@@ -320,8 +318,8 @@ describe("WebhookEventTransport", () => {
320
318
  });
321
319
 
322
320
  transport.addEndpoint({
323
- id: "test-1",
324
- url: "https://example.com/webhook",
321
+ id: 'test-1',
322
+ url: 'https://example.com/webhook',
325
323
  });
326
324
 
327
325
  transport.emit(createTestEvent());
@@ -332,13 +330,9 @@ describe("WebhookEventTransport", () => {
332
330
  expect(mockFetch).toHaveBeenCalledTimes(3);
333
331
  });
334
332
 
335
- it("should retry on non-ok response", async () => {
333
+ it('should retry on non-ok response', async () => {
336
334
  mockFetch
337
- .mockResolvedValueOnce({
338
- ok: false,
339
- status: 500,
340
- statusText: "Internal Server Error",
341
- })
335
+ .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Internal Server Error' })
342
336
  .mockResolvedValueOnce({ ok: true });
343
337
 
344
338
  transport = new WebhookEventTransport({
@@ -349,8 +343,8 @@ describe("WebhookEventTransport", () => {
349
343
  });
350
344
 
351
345
  transport.addEndpoint({
352
- id: "test-1",
353
- url: "https://example.com/webhook",
346
+ id: 'test-1',
347
+ url: 'https://example.com/webhook',
354
348
  });
355
349
 
356
350
  transport.emit(createTestEvent());
@@ -361,72 +355,72 @@ describe("WebhookEventTransport", () => {
361
355
  });
362
356
  });
363
357
 
364
- describe("subscribe / subscribeAll", () => {
365
- it("should call local subscribers on emit", () => {
358
+ describe('subscribe / subscribeAll', () => {
359
+ it('should call local subscribers on emit', () => {
366
360
  const callback = vi.fn();
367
- transport.subscribe("run-123", callback);
361
+ transport.subscribe('run-123', callback);
368
362
 
369
- const event = createTestEvent({ runId: "run-123" });
363
+ const event = createTestEvent({ runId: 'run-123' });
370
364
  transport.emit(event);
371
365
 
372
366
  expect(callback).toHaveBeenCalledWith(event);
373
367
  });
374
368
 
375
- it("should not call subscriber for different runId", () => {
369
+ it('should not call subscriber for different runId', () => {
376
370
  const callback = vi.fn();
377
- transport.subscribe("run-123", callback);
371
+ transport.subscribe('run-123', callback);
378
372
 
379
- const event = createTestEvent({ runId: "run-456" });
373
+ const event = createTestEvent({ runId: 'run-456' });
380
374
  transport.emit(event);
381
375
 
382
376
  expect(callback).not.toHaveBeenCalled();
383
377
  });
384
378
 
385
- it("should call global subscribers for all events", () => {
379
+ it('should call global subscribers for all events', () => {
386
380
  const callback = vi.fn();
387
381
  transport.subscribeAll(callback);
388
382
 
389
- transport.emit(createTestEvent({ runId: "run-1" }));
390
- transport.emit(createTestEvent({ runId: "run-2" }));
383
+ transport.emit(createTestEvent({ runId: 'run-1' }));
384
+ transport.emit(createTestEvent({ runId: 'run-2' }));
391
385
 
392
386
  expect(callback).toHaveBeenCalledTimes(2);
393
387
  });
394
388
 
395
- it("should unsubscribe correctly", () => {
389
+ it('should unsubscribe correctly', () => {
396
390
  const callback = vi.fn();
397
- const unsubscribe = transport.subscribe("run-123", callback);
391
+ const unsubscribe = transport.subscribe('run-123', callback);
398
392
 
399
393
  unsubscribe();
400
394
 
401
- transport.emit(createTestEvent({ runId: "run-123" }));
395
+ transport.emit(createTestEvent({ runId: 'run-123' }));
402
396
  expect(callback).not.toHaveBeenCalled();
403
397
  });
404
398
 
405
- it("should handle callback errors gracefully", () => {
399
+ it('should handle callback errors gracefully', () => {
406
400
  const errorCallback = vi.fn().mockImplementation(() => {
407
- throw new Error("Callback error");
401
+ throw new Error('Callback error');
408
402
  });
409
403
  const normalCallback = vi.fn();
410
404
 
411
- transport.subscribe("run-123", errorCallback);
412
- transport.subscribe("run-123", normalCallback);
405
+ transport.subscribe('run-123', errorCallback);
406
+ transport.subscribe('run-123', normalCallback);
413
407
 
414
- transport.emit(createTestEvent({ runId: "run-123" }));
408
+ transport.emit(createTestEvent({ runId: 'run-123' }));
415
409
 
416
410
  expect(errorCallback).toHaveBeenCalled();
417
411
  expect(normalCallback).toHaveBeenCalled();
418
412
  });
419
413
  });
420
414
 
421
- describe("close", () => {
422
- it("should clear all endpoints and subscribers", () => {
415
+ describe('close', () => {
416
+ it('should clear all endpoints and subscribers', () => {
423
417
  transport.addEndpoint({
424
- id: "test-1",
425
- url: "https://example.com/webhook",
418
+ id: 'test-1',
419
+ url: 'https://example.com/webhook',
426
420
  });
427
421
 
428
422
  const callback = vi.fn();
429
- transport.subscribe("run-123", callback);
423
+ transport.subscribe('run-123', callback);
430
424
  transport.subscribeAll(vi.fn());
431
425
 
432
426
  transport.close();
@@ -434,91 +428,77 @@ describe("WebhookEventTransport", () => {
434
428
  expect(transport.getEndpoints()).toHaveLength(0);
435
429
 
436
430
  // Callback should not be called after close
437
- transport.emit(createTestEvent({ runId: "run-123" }));
431
+ transport.emit(createTestEvent({ runId: 'run-123' }));
438
432
  expect(callback).not.toHaveBeenCalled();
439
433
  });
440
434
  });
441
435
 
442
- describe("SSRF protection - isBlockedHost", () => {
443
- it("should block 10.x.x.x private range", () => {
436
+ describe('SSRF protection - isBlockedHost', () => {
437
+ it('should block 10.x.x.x private range', () => {
444
438
  expect(() =>
445
- transport.addEndpoint({ id: "ssrf-1", url: "https://10.0.0.1/hook" }),
439
+ transport.addEndpoint({ id: 'ssrf-1', url: 'https://10.0.0.1/hook' })
446
440
  ).toThrow(/blocked/);
447
441
  });
448
442
 
449
- it("should block 172.16-31.x.x private range", () => {
443
+ it('should block 172.16-31.x.x private range', () => {
450
444
  expect(() =>
451
- transport.addEndpoint({ id: "ssrf-2", url: "https://172.16.0.1/hook" }),
445
+ transport.addEndpoint({ id: 'ssrf-2', url: 'https://172.16.0.1/hook' })
452
446
  ).toThrow(/blocked/);
453
447
  expect(() =>
454
- transport.addEndpoint({
455
- id: "ssrf-3",
456
- url: "https://172.31.255.255/hook",
457
- }),
448
+ transport.addEndpoint({ id: 'ssrf-3', url: 'https://172.31.255.255/hook' })
458
449
  ).toThrow(/blocked/);
459
450
  // 172.32 should NOT be blocked
460
- transport.addEndpoint({ id: "ssrf-ok", url: "https://172.32.0.1/hook" });
461
- expect(
462
- transport.getEndpoints().find((e) => e.id === "ssrf-ok"),
463
- ).toBeDefined();
451
+ transport.addEndpoint({ id: 'ssrf-ok', url: 'https://172.32.0.1/hook' });
452
+ expect(transport.getEndpoints().find(e => e.id === 'ssrf-ok')).toBeDefined();
464
453
  });
465
454
 
466
- it("should block 192.168.x.x private range", () => {
455
+ it('should block 192.168.x.x private range', () => {
467
456
  expect(() =>
468
- transport.addEndpoint({
469
- id: "ssrf-4",
470
- url: "https://192.168.1.1/hook",
471
- }),
457
+ transport.addEndpoint({ id: 'ssrf-4', url: 'https://192.168.1.1/hook' })
472
458
  ).toThrow(/blocked/);
473
459
  });
474
460
 
475
- it("should block 169.254.x.x link-local range", () => {
461
+ it('should block 169.254.x.x link-local range', () => {
476
462
  expect(() =>
477
- transport.addEndpoint({
478
- id: "ssrf-5",
479
- url: "https://169.254.1.1/hook",
480
- }),
463
+ transport.addEndpoint({ id: 'ssrf-5', url: 'https://169.254.1.1/hook' })
481
464
  ).toThrow(/blocked/);
482
465
  });
483
466
 
484
- it("should block 0.0.0.0", () => {
467
+ it('should block 0.0.0.0', () => {
485
468
  expect(() =>
486
- transport.addEndpoint({ id: "ssrf-6", url: "https://0.0.0.0/hook" }),
469
+ transport.addEndpoint({ id: 'ssrf-6', url: 'https://0.0.0.0/hook' })
487
470
  ).toThrow(/blocked/);
488
471
  });
489
472
 
490
- it("should block IPv6 link-local (fe80::)", () => {
473
+ it('should block IPv6 link-local (fe80::)', () => {
491
474
  expect(() =>
492
- transport.addEndpoint({ id: "ssrf-7", url: "https://[fe80::1]/hook" }),
475
+ transport.addEndpoint({ id: 'ssrf-7', url: 'https://[fe80::1]/hook' })
493
476
  ).toThrow(/blocked/);
494
477
  });
495
478
 
496
- it("should block localhost and 127.x.x.x", () => {
479
+ it('should block localhost and 127.x.x.x', () => {
497
480
  expect(() =>
498
- transport.addEndpoint({ id: "ssrf-8", url: "https://localhost/hook" }),
481
+ transport.addEndpoint({ id: 'ssrf-8', url: 'https://localhost/hook' })
499
482
  ).toThrow(/blocked/);
500
483
  expect(() =>
501
- transport.addEndpoint({ id: "ssrf-9", url: "https://127.0.0.1/hook" }),
484
+ transport.addEndpoint({ id: 'ssrf-9', url: 'https://127.0.0.1/hook' })
502
485
  ).toThrow(/blocked/);
503
486
  expect(() =>
504
- transport.addEndpoint({ id: "ssrf-10", url: "https://127.0.0.2/hook" }),
487
+ transport.addEndpoint({ id: 'ssrf-10', url: 'https://127.0.0.2/hook' })
505
488
  ).toThrow(/blocked/);
506
489
  });
507
490
 
508
- it("should block cloud metadata endpoint", () => {
491
+ it('should block cloud metadata endpoint', () => {
509
492
  expect(() =>
510
- transport.addEndpoint({
511
- id: "ssrf-11",
512
- url: "https://169.254.169.254/latest/meta-data",
513
- }),
493
+ transport.addEndpoint({ id: 'ssrf-11', url: 'https://169.254.169.254/latest/meta-data' })
514
494
  ).toThrow(/blocked/);
515
495
  });
516
496
  });
517
497
 
518
- describe("emit subscriber error isolation", () => {
519
- it("should catch subscriber error and still call other subscribers", () => {
498
+ describe('emit subscriber error isolation', () => {
499
+ it('should catch subscriber error and still call other subscribers', () => {
520
500
  const errorCallback = vi.fn().mockImplementation(() => {
521
- throw new Error("Subscriber exploded");
501
+ throw new Error('Subscriber exploded');
522
502
  });
523
503
  const normalCallback = vi.fn();
524
504
 
@@ -532,16 +512,16 @@ describe("WebhookEventTransport", () => {
532
512
  expect(normalCallback).toHaveBeenCalledWith(event);
533
513
  });
534
514
 
535
- it("should catch run-subscriber error and still call other run-subscribers", () => {
515
+ it('should catch run-subscriber error and still call other run-subscribers', () => {
536
516
  const errorCallback = vi.fn().mockImplementation(() => {
537
- throw new Error("Run subscriber failed");
517
+ throw new Error('Run subscriber failed');
538
518
  });
539
519
  const normalCallback = vi.fn();
540
520
 
541
- transport.subscribe("run-123", errorCallback);
542
- transport.subscribe("run-123", normalCallback);
521
+ transport.subscribe('run-123', errorCallback);
522
+ transport.subscribe('run-123', normalCallback);
543
523
 
544
- const event = createTestEvent({ runId: "run-123" });
524
+ const event = createTestEvent({ runId: 'run-123' });
545
525
  transport.emit(event);
546
526
 
547
527
  expect(errorCallback).toHaveBeenCalled();
@@ -549,12 +529,12 @@ describe("WebhookEventTransport", () => {
549
529
  });
550
530
  });
551
531
 
552
- describe("close clears all state", () => {
553
- it("should clear endpoints, run subscribers, and global subscribers after close", () => {
554
- transport.addEndpoint({ id: "ep-1", url: "https://example.com/hook" });
532
+ describe('close clears all state', () => {
533
+ it('should clear endpoints, run subscribers, and global subscribers after close', () => {
534
+ transport.addEndpoint({ id: 'ep-1', url: 'https://example.com/hook' });
555
535
  const runCb = vi.fn();
556
536
  const globalCb = vi.fn();
557
- transport.subscribe("run-123", runCb);
537
+ transport.subscribe('run-123', runCb);
558
538
  transport.subscribeAll(globalCb);
559
539
 
560
540
  transport.close();
@@ -562,19 +542,19 @@ describe("WebhookEventTransport", () => {
562
542
  expect(transport.getEndpoints()).toHaveLength(0);
563
543
 
564
544
  // Subscribers should no longer receive events
565
- transport.emit(createTestEvent({ runId: "run-123" }));
545
+ transport.emit(createTestEvent({ runId: 'run-123' }));
566
546
  expect(runCb).not.toHaveBeenCalled();
567
547
  expect(globalCb).not.toHaveBeenCalled();
568
548
  });
569
549
  });
570
550
 
571
- describe("constructor with initial endpoints", () => {
572
- it("should register initial endpoints", () => {
551
+ describe('constructor with initial endpoints', () => {
552
+ it('should register initial endpoints', () => {
573
553
  transport = new WebhookEventTransport({
574
554
  fetchFn: mockFetch,
575
555
  endpoints: [
576
- { id: "ep-1", url: "https://example.com/1" },
577
- { id: "ep-2", url: "https://example.com/2" },
556
+ { id: 'ep-1', url: 'https://example.com/1' },
557
+ { id: 'ep-2', url: 'https://example.com/2' },
578
558
  ],
579
559
  });
580
560
 
@@ -582,17 +562,12 @@ describe("WebhookEventTransport", () => {
582
562
  });
583
563
  });
584
564
 
585
- describe("SSRF protection - DNS resolution", () => {
586
- it("should block webhooks when hostname resolves to private IP", async () => {
565
+ describe('SSRF protection - DNS resolution', () => {
566
+ it('should block webhooks when hostname resolves to private IP', async () => {
587
567
  const dnsLookup = dns.promises.lookup as ReturnType<typeof vi.fn>;
588
- dnsLookup.mockResolvedValueOnce({ address: "10.0.0.5", family: 4 });
568
+ dnsLookup.mockResolvedValueOnce({ address: '10.0.0.5', family: 4 });
589
569
 
590
- const logger = {
591
- debug: vi.fn(),
592
- info: vi.fn(),
593
- warn: vi.fn(),
594
- error: vi.fn(),
595
- };
570
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
596
571
  transport = new WebhookEventTransport({
597
572
  fetchFn: mockFetch,
598
573
  defaultRetries: 0,
@@ -600,8 +575,8 @@ describe("WebhookEventTransport", () => {
600
575
  });
601
576
 
602
577
  transport.addEndpoint({
603
- id: "ssrf-dns",
604
- url: "https://evil.example.com/hook",
578
+ id: 'ssrf-dns',
579
+ url: 'https://evil.example.com/hook',
605
580
  });
606
581
 
607
582
  transport.emit(createTestEvent());
@@ -610,23 +585,18 @@ describe("WebhookEventTransport", () => {
610
585
 
611
586
  expect(mockFetch).not.toHaveBeenCalled();
612
587
  expect(logger.error).toHaveBeenCalledWith(
613
- expect.stringContaining("ssrf-dns"),
588
+ expect.stringContaining('ssrf-dns'),
614
589
  expect.objectContaining({
615
- message: expect.stringContaining("resolves to blocked IP"),
616
- }),
590
+ message: expect.stringContaining('resolves to blocked IP'),
591
+ })
617
592
  );
618
593
  });
619
594
 
620
- it("should block webhooks when hostname resolves to loopback", async () => {
595
+ it('should block webhooks when hostname resolves to loopback', async () => {
621
596
  const dnsLookup = dns.promises.lookup as ReturnType<typeof vi.fn>;
622
- dnsLookup.mockResolvedValueOnce({ address: "127.0.0.1", family: 4 });
597
+ dnsLookup.mockResolvedValueOnce({ address: '127.0.0.1', family: 4 });
623
598
 
624
- const logger = {
625
- debug: vi.fn(),
626
- info: vi.fn(),
627
- warn: vi.fn(),
628
- error: vi.fn(),
629
- };
599
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
630
600
  transport = new WebhookEventTransport({
631
601
  fetchFn: mockFetch,
632
602
  defaultRetries: 0,
@@ -634,8 +604,8 @@ describe("WebhookEventTransport", () => {
634
604
  });
635
605
 
636
606
  transport.addEndpoint({
637
- id: "ssrf-loopback",
638
- url: "https://loopback.example.com/hook",
607
+ id: 'ssrf-loopback',
608
+ url: 'https://loopback.example.com/hook',
639
609
  });
640
610
 
641
611
  transport.emit(createTestEvent());
@@ -645,19 +615,11 @@ describe("WebhookEventTransport", () => {
645
615
  expect(mockFetch).not.toHaveBeenCalled();
646
616
  });
647
617
 
648
- it("should block webhooks when hostname resolves to cloud metadata IP", async () => {
618
+ it('should block webhooks when hostname resolves to cloud metadata IP', async () => {
649
619
  const dnsLookup = dns.promises.lookup as ReturnType<typeof vi.fn>;
650
- dnsLookup.mockResolvedValueOnce({
651
- address: "169.254.169.254",
652
- family: 4,
653
- });
620
+ dnsLookup.mockResolvedValueOnce({ address: '169.254.169.254', family: 4 });
654
621
 
655
- const logger = {
656
- debug: vi.fn(),
657
- info: vi.fn(),
658
- warn: vi.fn(),
659
- error: vi.fn(),
660
- };
622
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
661
623
  transport = new WebhookEventTransport({
662
624
  fetchFn: mockFetch,
663
625
  defaultRetries: 0,
@@ -665,8 +627,8 @@ describe("WebhookEventTransport", () => {
665
627
  });
666
628
 
667
629
  transport.addEndpoint({
668
- id: "ssrf-metadata",
669
- url: "https://metadata.example.com/hook",
630
+ id: 'ssrf-metadata',
631
+ url: 'https://metadata.example.com/hook',
670
632
  });
671
633
 
672
634
  transport.emit(createTestEvent());
@@ -676,13 +638,13 @@ describe("WebhookEventTransport", () => {
676
638
  expect(mockFetch).not.toHaveBeenCalled();
677
639
  });
678
640
 
679
- it("should allow webhooks when hostname resolves to public IP", async () => {
641
+ it('should allow webhooks when hostname resolves to public IP', async () => {
680
642
  const dnsLookup = dns.promises.lookup as ReturnType<typeof vi.fn>;
681
- dnsLookup.mockResolvedValueOnce({ address: "93.184.216.34", family: 4 });
643
+ dnsLookup.mockResolvedValueOnce({ address: '93.184.216.34', family: 4 });
682
644
 
683
645
  transport.addEndpoint({
684
- id: "public-ok",
685
- url: "https://example.com/hook",
646
+ id: 'public-ok',
647
+ url: 'https://example.com/hook',
686
648
  });
687
649
 
688
650
  transport.emit(createTestEvent());