@multiplier-labs/stepflow 0.3.1 → 0.3.4

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 (112) hide show
  1. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
  2. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
  3. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  4. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
  5. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
  6. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  7. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
  8. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
  9. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  10. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
  11. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  12. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
  13. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
  14. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  15. package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
  16. package/.github/CODEOWNERS +15 -0
  17. package/.github/dependabot.yml +23 -0
  18. package/.github/workflows/ci.yml +38 -0
  19. package/.github/workflows/publish.yml +17 -5
  20. package/.github/workflows/security.yml +29 -0
  21. package/CHANGELOG.md +34 -0
  22. package/README.md +24 -0
  23. package/SECURITY.md +40 -0
  24. package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
  25. package/dist/chunk-5XJBAPXF.js.map +1 -0
  26. package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
  27. package/dist/chunk-FMPJBK2N.js.map +1 -0
  28. package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
  29. package/dist/chunk-JPXPB7U5.js.map +1 -0
  30. package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
  31. package/dist/chunk-U3PCVK6O.js.map +1 -0
  32. package/dist/events/index.d.ts +60 -7
  33. package/dist/events/index.js +1 -1
  34. package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
  35. package/dist/index.d.ts +15 -23
  36. package/dist/index.js +784 -547
  37. package/dist/index.js.map +1 -1
  38. package/dist/scheduler/index.d.ts +4 -4
  39. package/dist/scheduler/index.js +2 -2
  40. package/dist/storage/index.d.ts +322 -56
  41. package/dist/storage/index.js +4 -2
  42. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  43. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  44. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  45. package/docs/stepflow-api-reference.md +5 -4
  46. package/package.json +12 -8
  47. package/src/core/engine/concurrency.ts +229 -0
  48. package/src/core/engine/lifecycle.ts +176 -0
  49. package/src/core/engine.test.ts +536 -522
  50. package/src/core/engine.ts +116 -235
  51. package/src/core/index.ts +3 -3
  52. package/src/core/orchestrator/persistence.ts +165 -0
  53. package/src/core/orchestrator/plan-resolution.ts +140 -0
  54. package/src/core/orchestrator/state-transitions.ts +109 -0
  55. package/src/core/orchestrator/step-runner.ts +427 -0
  56. package/src/core/orchestrator.ts +57 -395
  57. package/src/core/types.ts +7 -29
  58. package/src/events/index.ts +4 -4
  59. package/src/events/memory.test.ts +39 -41
  60. package/src/events/memory.ts +7 -10
  61. package/src/events/socketio.test.ts +112 -162
  62. package/src/events/socketio.ts +20 -27
  63. package/src/events/types.ts +16 -19
  64. package/src/events/webhook.test.ts +489 -235
  65. package/src/events/webhook.ts +355 -139
  66. package/src/index.ts +19 -19
  67. package/src/planning/evaluate-condition.test.ts +153 -0
  68. package/src/planning/evaluate-condition.ts +105 -0
  69. package/src/planning/index.ts +6 -3
  70. package/src/planning/planner.ts +45 -152
  71. package/src/planning/planning.test.ts +303 -366
  72. package/src/planning/registry.test.ts +204 -251
  73. package/src/planning/registry.ts +20 -106
  74. package/src/planning/types.ts +23 -27
  75. package/src/scheduler/cron.test.ts +149 -150
  76. package/src/scheduler/cron.ts +58 -69
  77. package/src/scheduler/index.ts +4 -14
  78. package/src/scheduler/postgres-persistence.ts +89 -105
  79. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  80. package/src/scheduler/sqlite-persistence.ts +40 -22
  81. package/src/scheduler/types.ts +4 -9
  82. package/src/storage/index.ts +5 -5
  83. package/src/storage/memory.test.ts +133 -183
  84. package/src/storage/memory.ts +21 -42
  85. package/src/storage/postgres-core.ts +550 -0
  86. package/src/storage/postgres-migration.test.ts +69 -0
  87. package/src/storage/postgres-transaction-pagination.test.ts +107 -0
  88. package/src/storage/postgres.ts +222 -937
  89. package/src/storage/sqlite-core.ts +151 -0
  90. package/src/storage/sqlite.test.ts +330 -281
  91. package/src/storage/sqlite.ts +225 -170
  92. package/src/storage/types.ts +33 -58
  93. package/src/utils/errors.test.ts +78 -80
  94. package/src/utils/errors.ts +27 -57
  95. package/src/utils/id.ts +3 -3
  96. package/src/utils/index.ts +6 -4
  97. package/src/utils/logger.test.ts +52 -58
  98. package/src/utils/logger.ts +4 -4
  99. package/src/utils/pinned-fetch.test.ts +84 -0
  100. package/src/utils/pinned-fetch.ts +118 -0
  101. package/src/utils/postgres-deps.ts +20 -4
  102. package/src/utils/retry.test.ts +47 -67
  103. package/src/utils/retry.ts +9 -9
  104. package/src/utils/safe-json.test.ts +166 -0
  105. package/src/utils/safe-json.ts +107 -0
  106. package/src/utils/webhook-signature.test.ts +111 -0
  107. package/src/utils/webhook-signature.ts +74 -0
  108. package/dist/chunk-FQCT3I2A.js.map +0 -1
  109. package/dist/chunk-GXY66V7Q.js.map +0 -1
  110. package/dist/chunk-U5QGN3RT.js.map +0 -1
  111. package/dist/chunk-XCF4PLN7.js.map +0 -1
  112. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -1,10 +1,6 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import {
3
- SocketIOEventTransport,
4
- SocketIOServer,
5
- SocketIOSocket,
6
- } from "./socketio";
7
- import type { WorkflowEvent } from "./types";
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { SocketIOEventTransport, SocketIOServer, SocketIOSocket } from './socketio';
3
+ import type { WorkflowEvent } from './types';
8
4
 
9
5
  // Mock Socket.IO server
10
6
  function createMockServer(): SocketIOServer & {
@@ -52,19 +48,17 @@ function createMockSocket(): SocketIOSocket & {
52
48
  };
53
49
  }
54
50
 
55
- function createTestEvent(
56
- overrides: Partial<WorkflowEvent> = {},
57
- ): WorkflowEvent {
51
+ function createTestEvent(overrides: Partial<WorkflowEvent> = {}): WorkflowEvent {
58
52
  return {
59
- eventType: "run.started",
60
- runId: "run-123",
61
- kind: "test.workflow",
62
- timestamp: new Date("2024-01-01T12:00:00Z"),
53
+ eventType: 'run.started',
54
+ runId: 'run-123',
55
+ kind: 'test.workflow',
56
+ timestamp: new Date('2024-01-01T12:00:00Z'),
63
57
  ...overrides,
64
58
  };
65
59
  }
66
60
 
67
- describe("SocketIOEventTransport", () => {
61
+ describe('SocketIOEventTransport', () => {
68
62
  let io: ReturnType<typeof createMockServer>;
69
63
  let transport: SocketIOEventTransport;
70
64
 
@@ -73,130 +67,124 @@ describe("SocketIOEventTransport", () => {
73
67
  transport = new SocketIOEventTransport({ io });
74
68
  });
75
69
 
76
- describe("emit", () => {
77
- it("should emit event to run-specific room", () => {
78
- const event = createTestEvent({ runId: "run-456" });
70
+ describe('emit', () => {
71
+ it('should emit event to run-specific room', () => {
72
+ const event = createTestEvent({ runId: 'run-456' });
79
73
  transport.emit(event);
80
74
 
81
- const roomEmit = io._rooms.get("run:run-456");
75
+ const roomEmit = io._rooms.get('run:run-456');
82
76
  expect(roomEmit).toBeDefined();
83
- expect(roomEmit!.emit).toHaveBeenCalledWith("workflow:event", {
77
+ expect(roomEmit!.emit).toHaveBeenCalledWith('workflow:event', {
84
78
  ...event,
85
- timestamp: "2024-01-01T12:00:00.000Z",
79
+ timestamp: '2024-01-01T12:00:00.000Z',
86
80
  });
87
81
  });
88
82
 
89
- it("should emit event to global room by default", () => {
83
+ it('should emit event to global room by default', () => {
90
84
  const event = createTestEvent();
91
85
  transport.emit(event);
92
86
 
93
- const globalRoomEmit = io._rooms.get("workflow:all");
87
+ const globalRoomEmit = io._rooms.get('workflow:all');
94
88
  expect(globalRoomEmit).toBeDefined();
95
89
  expect(globalRoomEmit!.emit).toHaveBeenCalled();
96
90
  });
97
91
 
98
- it("should not emit to global room when broadcastGlobal is false", () => {
92
+ it('should not emit to global room when broadcastGlobal is false', () => {
99
93
  transport = new SocketIOEventTransport({ io, broadcastGlobal: false });
100
94
  const event = createTestEvent();
101
95
  transport.emit(event);
102
96
 
103
97
  // Should only have run-specific room, not global
104
98
  expect(io._rooms.size).toBe(1);
105
- expect(io._rooms.has("run:run-123")).toBe(true);
106
- expect(io._rooms.has("workflow:all")).toBe(false);
99
+ expect(io._rooms.has('run:run-123')).toBe(true);
100
+ expect(io._rooms.has('workflow:all')).toBe(false);
107
101
  });
108
102
 
109
- it("should use custom event name", () => {
110
- transport = new SocketIOEventTransport({ io, eventName: "custom:event" });
103
+ it('should use custom event name', () => {
104
+ transport = new SocketIOEventTransport({ io, eventName: 'custom:event' });
111
105
  const event = createTestEvent();
112
106
  transport.emit(event);
113
107
 
114
- const roomEmit = io._rooms.get("run:run-123");
115
- expect(roomEmit!.emit).toHaveBeenCalledWith(
116
- "custom:event",
117
- expect.any(Object),
118
- );
108
+ const roomEmit = io._rooms.get('run:run-123');
109
+ expect(roomEmit!.emit).toHaveBeenCalledWith('custom:event', expect.any(Object));
119
110
  });
120
111
 
121
- it("should use custom room prefix", () => {
122
- transport = new SocketIOEventTransport({ io, roomPrefix: "workflow:" });
123
- const event = createTestEvent({ runId: "xyz" });
112
+ it('should use custom room prefix', () => {
113
+ transport = new SocketIOEventTransport({ io, roomPrefix: 'workflow:' });
114
+ const event = createTestEvent({ runId: 'xyz' });
124
115
  transport.emit(event);
125
116
 
126
- expect(io._rooms.has("workflow:xyz")).toBe(true);
117
+ expect(io._rooms.has('workflow:xyz')).toBe(true);
127
118
  });
128
119
 
129
- it("should serialize timestamp to ISO string", () => {
120
+ it('should serialize timestamp to ISO string', () => {
130
121
  const event = createTestEvent({
131
- timestamp: new Date("2024-06-15T10:30:00Z"),
122
+ timestamp: new Date('2024-06-15T10:30:00Z'),
132
123
  });
133
124
  transport.emit(event);
134
125
 
135
- const roomEmit = io._rooms.get("run:run-123");
136
- const emittedEvent = roomEmit!.emit.mock.calls[0][1] as Record<
137
- string,
138
- unknown
139
- >;
140
- expect(emittedEvent.timestamp).toBe("2024-06-15T10:30:00.000Z");
126
+ const roomEmit = io._rooms.get('run:run-123');
127
+ const emittedEvent = roomEmit!.emit.mock.calls[0][1] as Record<string, unknown>;
128
+ expect(emittedEvent.timestamp).toBe('2024-06-15T10:30:00.000Z');
141
129
  });
142
130
  });
143
131
 
144
- describe("subscribe", () => {
145
- it("should call callback when event matches runId", () => {
132
+ describe('subscribe', () => {
133
+ it('should call callback when event matches runId', () => {
146
134
  const callback = vi.fn();
147
- transport.subscribe("run-123", callback);
135
+ transport.subscribe('run-123', callback);
148
136
 
149
- const event = createTestEvent({ runId: "run-123" });
137
+ const event = createTestEvent({ runId: 'run-123' });
150
138
  transport.emit(event);
151
139
 
152
140
  expect(callback).toHaveBeenCalledWith(event);
153
141
  });
154
142
 
155
- it("should not call callback for different runId", () => {
143
+ it('should not call callback for different runId', () => {
156
144
  const callback = vi.fn();
157
- transport.subscribe("run-123", callback);
145
+ transport.subscribe('run-123', callback);
158
146
 
159
- const event = createTestEvent({ runId: "run-456" });
147
+ const event = createTestEvent({ runId: 'run-456' });
160
148
  transport.emit(event);
161
149
 
162
150
  expect(callback).not.toHaveBeenCalled();
163
151
  });
164
152
 
165
- it("should allow multiple subscribers for same runId", () => {
153
+ it('should allow multiple subscribers for same runId', () => {
166
154
  const callback1 = vi.fn();
167
155
  const callback2 = vi.fn();
168
- transport.subscribe("run-123", callback1);
169
- transport.subscribe("run-123", callback2);
156
+ transport.subscribe('run-123', callback1);
157
+ transport.subscribe('run-123', callback2);
170
158
 
171
- const event = createTestEvent({ runId: "run-123" });
159
+ const event = createTestEvent({ runId: 'run-123' });
172
160
  transport.emit(event);
173
161
 
174
162
  expect(callback1).toHaveBeenCalledWith(event);
175
163
  expect(callback2).toHaveBeenCalledWith(event);
176
164
  });
177
165
 
178
- it("should unsubscribe when calling returned function", () => {
166
+ it('should unsubscribe when calling returned function', () => {
179
167
  const callback = vi.fn();
180
- const unsubscribe = transport.subscribe("run-123", callback);
168
+ const unsubscribe = transport.subscribe('run-123', callback);
181
169
 
182
170
  unsubscribe();
183
171
 
184
- const event = createTestEvent({ runId: "run-123" });
172
+ const event = createTestEvent({ runId: 'run-123' });
185
173
  transport.emit(event);
186
174
 
187
175
  expect(callback).not.toHaveBeenCalled();
188
176
  });
189
177
 
190
- it("should handle callback errors gracefully", () => {
178
+ it('should handle callback errors gracefully', () => {
191
179
  const errorCallback = vi.fn().mockImplementation(() => {
192
- throw new Error("Callback error");
180
+ throw new Error('Callback error');
193
181
  });
194
182
  const normalCallback = vi.fn();
195
183
 
196
- transport.subscribe("run-123", errorCallback);
197
- transport.subscribe("run-123", normalCallback);
184
+ transport.subscribe('run-123', errorCallback);
185
+ transport.subscribe('run-123', normalCallback);
198
186
 
199
- const event = createTestEvent({ runId: "run-123" });
187
+ const event = createTestEvent({ runId: 'run-123' });
200
188
  transport.emit(event);
201
189
 
202
190
  // Both callbacks should have been called despite error
@@ -205,13 +193,13 @@ describe("SocketIOEventTransport", () => {
205
193
  });
206
194
  });
207
195
 
208
- describe("subscribeAll", () => {
209
- it("should call callback for all events", () => {
196
+ describe('subscribeAll', () => {
197
+ it('should call callback for all events', () => {
210
198
  const callback = vi.fn();
211
199
  transport.subscribeAll(callback);
212
200
 
213
- const event1 = createTestEvent({ runId: "run-1" });
214
- const event2 = createTestEvent({ runId: "run-2" });
201
+ const event1 = createTestEvent({ runId: 'run-1' });
202
+ const event2 = createTestEvent({ runId: 'run-2' });
215
203
  transport.emit(event1);
216
204
  transport.emit(event2);
217
205
 
@@ -220,7 +208,7 @@ describe("SocketIOEventTransport", () => {
220
208
  expect(callback).toHaveBeenCalledWith(event2);
221
209
  });
222
210
 
223
- it("should unsubscribe when calling returned function", () => {
211
+ it('should unsubscribe when calling returned function', () => {
224
212
  const callback = vi.fn();
225
213
  const unsubscribe = transport.subscribeAll(callback);
226
214
 
@@ -233,80 +221,74 @@ describe("SocketIOEventTransport", () => {
233
221
  });
234
222
  });
235
223
 
236
- describe("setupClientHandlers", () => {
224
+ describe('setupClientHandlers', () => {
237
225
  const allowAll = vi.fn().mockResolvedValue(true);
238
226
 
239
- it("should register subscription handlers on socket", () => {
227
+ it('should register subscription handlers on socket', () => {
240
228
  const socket = createMockSocket();
241
229
  transport.setupClientHandlers(socket, allowAll);
242
230
 
243
- expect(socket._handlers.has("workflow:subscribe")).toBe(true);
244
- expect(socket._handlers.has("workflow:unsubscribe")).toBe(true);
245
- expect(socket._handlers.has("workflow:subscribe:all")).toBe(true);
246
- expect(socket._handlers.has("workflow:unsubscribe:all")).toBe(true);
231
+ expect(socket._handlers.has('workflow:subscribe')).toBe(true);
232
+ expect(socket._handlers.has('workflow:unsubscribe')).toBe(true);
233
+ expect(socket._handlers.has('workflow:subscribe:all')).toBe(true);
234
+ expect(socket._handlers.has('workflow:unsubscribe:all')).toBe(true);
247
235
  });
248
236
 
249
- it("should join run-specific room on subscribe when authorized", async () => {
237
+ it('should join run-specific room on subscribe when authorized', async () => {
250
238
  const socket = createMockSocket();
251
239
  transport.setupClientHandlers(socket, allowAll);
252
240
 
253
- const subscribeHandler = socket._handlers.get("workflow:subscribe")!;
254
- await subscribeHandler("run-123");
241
+ const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
242
+ await subscribeHandler('run-123');
255
243
 
256
- expect(socket._joinedRooms.has("run:run-123")).toBe(true);
244
+ expect(socket._joinedRooms.has('run:run-123')).toBe(true);
257
245
  });
258
246
 
259
- it("should leave run-specific room on unsubscribe", async () => {
247
+ it('should leave run-specific room on unsubscribe', async () => {
260
248
  const socket = createMockSocket();
261
249
  transport.setupClientHandlers(socket, allowAll);
262
250
 
263
251
  // First subscribe
264
- const subscribeHandler = socket._handlers.get("workflow:subscribe")!;
265
- await subscribeHandler("run-123");
252
+ const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
253
+ await subscribeHandler('run-123');
266
254
 
267
255
  // Then unsubscribe
268
- const unsubscribeHandler = socket._handlers.get("workflow:unsubscribe")!;
269
- unsubscribeHandler("run-123");
256
+ const unsubscribeHandler = socket._handlers.get('workflow:unsubscribe')!;
257
+ unsubscribeHandler('run-123');
270
258
 
271
- expect(socket._joinedRooms.has("run:run-123")).toBe(false);
259
+ expect(socket._joinedRooms.has('run:run-123')).toBe(false);
272
260
  });
273
261
 
274
- it("should join global room on subscribe:all when authorized", async () => {
262
+ it('should join global room on subscribe:all when authorized', async () => {
275
263
  const socket = createMockSocket();
276
264
  transport.setupClientHandlers(socket, allowAll);
277
265
 
278
- const subscribeAllHandler = socket._handlers.get(
279
- "workflow:subscribe:all",
280
- )!;
266
+ const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
281
267
  await subscribeAllHandler();
282
268
 
283
- expect(socket._joinedRooms.has("workflow:all")).toBe(true);
269
+ expect(socket._joinedRooms.has('workflow:all')).toBe(true);
284
270
  });
285
271
 
286
- it("should leave global room on unsubscribe:all", async () => {
272
+ it('should leave global room on unsubscribe:all', async () => {
287
273
  const socket = createMockSocket();
288
274
  transport.setupClientHandlers(socket, allowAll);
289
275
 
290
276
  // First subscribe
291
- const subscribeAllHandler = socket._handlers.get(
292
- "workflow:subscribe:all",
293
- )!;
277
+ const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
294
278
  await subscribeAllHandler();
295
279
 
296
280
  // Then unsubscribe
297
- const unsubscribeAllHandler = socket._handlers.get(
298
- "workflow:unsubscribe:all",
299
- )!;
281
+ const unsubscribeAllHandler = socket._handlers.get('workflow:unsubscribe:all')!;
300
282
  unsubscribeAllHandler();
301
283
 
302
- expect(socket._joinedRooms.has("workflow:all")).toBe(false);
284
+ expect(socket._joinedRooms.has('workflow:all')).toBe(false);
303
285
  });
304
286
 
305
- it("should ignore non-string runId on subscribe", async () => {
287
+ it('should ignore non-string runId on subscribe', async () => {
306
288
  const socket = createMockSocket();
307
289
  transport.setupClientHandlers(socket, allowAll);
308
290
 
309
- const subscribeHandler = socket._handlers.get("workflow:subscribe")!;
291
+ const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
310
292
  await subscribeHandler(123); // Not a string
311
293
  await subscribeHandler(null);
312
294
  await subscribeHandler(undefined);
@@ -314,111 +296,79 @@ describe("SocketIOEventTransport", () => {
314
296
  expect(socket._joinedRooms.size).toBe(0);
315
297
  });
316
298
 
317
- it("should deny run subscription when authorize returns false", async () => {
318
- const logger = {
319
- debug: vi.fn(),
320
- info: vi.fn(),
321
- warn: vi.fn(),
322
- error: vi.fn(),
323
- };
299
+ it('should deny run subscription when authorize returns false', async () => {
300
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
324
301
  transport = new SocketIOEventTransport({ io, logger });
325
302
 
326
303
  const socket = createMockSocket();
327
304
  const authorize = vi.fn().mockResolvedValue(false);
328
305
  transport.setupClientHandlers(socket, authorize);
329
306
 
330
- const subscribeHandler = socket._handlers.get("workflow:subscribe")!;
331
- await subscribeHandler("run-secret");
307
+ const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
308
+ await subscribeHandler('run-secret');
332
309
 
333
- expect(authorize).toHaveBeenCalledWith("run-secret", socket);
334
- expect(socket._joinedRooms.has("run:run-secret")).toBe(false);
335
- expect(logger.warn).toHaveBeenCalledWith(
336
- expect.stringContaining("run-secret"),
337
- );
310
+ expect(authorize).toHaveBeenCalledWith('run-secret', socket);
311
+ expect(socket._joinedRooms.has('run:run-secret')).toBe(false);
312
+ expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('run-secret'));
338
313
  });
339
314
 
340
- it("should deny run subscription when authorize throws", async () => {
341
- const logger = {
342
- debug: vi.fn(),
343
- info: vi.fn(),
344
- warn: vi.fn(),
345
- error: vi.fn(),
346
- };
315
+ it('should deny run subscription when authorize throws', async () => {
316
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
347
317
  transport = new SocketIOEventTransport({ io, logger });
348
318
 
349
319
  const socket = createMockSocket();
350
- const authorize = vi.fn().mockRejectedValue(new Error("auth error"));
320
+ const authorize = vi.fn().mockRejectedValue(new Error('auth error'));
351
321
  transport.setupClientHandlers(socket, authorize);
352
322
 
353
- const subscribeHandler = socket._handlers.get("workflow:subscribe")!;
354
- await subscribeHandler("run-123");
323
+ const subscribeHandler = socket._handlers.get('workflow:subscribe')!;
324
+ await subscribeHandler('run-123');
355
325
 
356
- expect(socket._joinedRooms.has("run:run-123")).toBe(false);
357
- expect(logger.error).toHaveBeenCalledWith(
358
- "Authorization check failed:",
359
- expect.any(Error),
360
- );
326
+ expect(socket._joinedRooms.has('run:run-123')).toBe(false);
327
+ expect(logger.error).toHaveBeenCalledWith('Authorization check failed:', expect.any(Error));
361
328
  });
362
329
 
363
- it("should deny global subscription when authorize returns false", async () => {
364
- const logger = {
365
- debug: vi.fn(),
366
- info: vi.fn(),
367
- warn: vi.fn(),
368
- error: vi.fn(),
369
- };
330
+ it('should deny global subscription when authorize returns false', async () => {
331
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
370
332
  transport = new SocketIOEventTransport({ io, logger });
371
333
 
372
334
  const socket = createMockSocket();
373
335
  const authorize = vi.fn().mockResolvedValue(false);
374
336
  transport.setupClientHandlers(socket, authorize);
375
337
 
376
- const subscribeAllHandler = socket._handlers.get(
377
- "workflow:subscribe:all",
378
- )!;
338
+ const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
379
339
  await subscribeAllHandler();
380
340
 
381
- expect(authorize).toHaveBeenCalledWith("*", socket);
382
- expect(socket._joinedRooms.has("workflow:all")).toBe(false);
383
- expect(logger.warn).toHaveBeenCalledWith("Global subscription denied");
341
+ expect(authorize).toHaveBeenCalledWith('*', socket);
342
+ expect(socket._joinedRooms.has('workflow:all')).toBe(false);
343
+ expect(logger.warn).toHaveBeenCalledWith('Global subscription denied');
384
344
  });
385
345
 
386
- it("should deny global subscription when authorize throws", async () => {
387
- const logger = {
388
- debug: vi.fn(),
389
- info: vi.fn(),
390
- warn: vi.fn(),
391
- error: vi.fn(),
392
- };
346
+ it('should deny global subscription when authorize throws', async () => {
347
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
393
348
  transport = new SocketIOEventTransport({ io, logger });
394
349
 
395
350
  const socket = createMockSocket();
396
- const authorize = vi.fn().mockRejectedValue(new Error("auth down"));
351
+ const authorize = vi.fn().mockRejectedValue(new Error('auth down'));
397
352
  transport.setupClientHandlers(socket, authorize);
398
353
 
399
- const subscribeAllHandler = socket._handlers.get(
400
- "workflow:subscribe:all",
401
- )!;
354
+ const subscribeAllHandler = socket._handlers.get('workflow:subscribe:all')!;
402
355
  await subscribeAllHandler();
403
356
 
404
- expect(socket._joinedRooms.has("workflow:all")).toBe(false);
405
- expect(logger.error).toHaveBeenCalledWith(
406
- "Authorization check failed:",
407
- expect.any(Error),
408
- );
357
+ expect(socket._joinedRooms.has('workflow:all')).toBe(false);
358
+ expect(logger.error).toHaveBeenCalledWith('Authorization check failed:', expect.any(Error));
409
359
  });
410
360
  });
411
361
 
412
- describe("close", () => {
413
- it("should clear all subscribers", () => {
362
+ describe('close', () => {
363
+ it('should clear all subscribers', () => {
414
364
  const runCallback = vi.fn();
415
365
  const globalCallback = vi.fn();
416
- transport.subscribe("run-123", runCallback);
366
+ transport.subscribe('run-123', runCallback);
417
367
  transport.subscribeAll(globalCallback);
418
368
 
419
369
  transport.close();
420
370
 
421
- const event = createTestEvent({ runId: "run-123" });
371
+ const event = createTestEvent({ runId: 'run-123' });
422
372
  transport.emit(event);
423
373
 
424
374
  expect(runCallback).not.toHaveBeenCalled();
@@ -5,13 +5,8 @@
5
5
  * Clients can subscribe to specific run IDs or receive all events.
6
6
  */
7
7
 
8
- import type {
9
- EventTransport,
10
- EventCallback,
11
- Unsubscribe,
12
- WorkflowEvent,
13
- } from "./types";
14
- import type { Logger } from "../core/types";
8
+ import type { EventTransport, EventCallback, Unsubscribe, WorkflowEvent } from './types';
9
+ import type { Logger } from '../core/types';
15
10
 
16
11
  /**
17
12
  * Minimal Socket.IO Server interface.
@@ -39,7 +34,7 @@ export interface SocketIOSocket {
39
34
  */
40
35
  export type SocketIOAuthorizeFn = (
41
36
  runId: string,
42
- socket: SocketIOSocket,
37
+ socket: SocketIOSocket
43
38
  ) => boolean | Promise<boolean>;
44
39
 
45
40
  /**
@@ -100,17 +95,15 @@ export class SocketIOEventTransport implements EventTransport {
100
95
 
101
96
  constructor(config: SocketIOEventTransportConfig) {
102
97
  this.io = config.io;
103
- this.eventName = config.eventName ?? "workflow:event";
104
- this.roomPrefix = config.roomPrefix ?? "run:";
98
+ this.eventName = config.eventName ?? 'workflow:event';
99
+ this.roomPrefix = config.roomPrefix ?? 'run:';
105
100
  this.broadcastGlobal = config.broadcastGlobal ?? true;
106
- this.globalRoom = config.globalRoom ?? "workflow:all";
101
+ this.globalRoom = config.globalRoom ?? 'workflow:all';
107
102
  this.logger = config.logger ?? {
108
103
  debug() {},
109
104
  info() {},
110
105
  warn() {},
111
- error(message: string, ...args: unknown[]) {
112
- console.error(message, ...args);
113
- },
106
+ error(message: string, ...args: unknown[]) { console.error(message, ...args); },
114
107
  };
115
108
  }
116
109
 
@@ -140,7 +133,7 @@ export class SocketIOEventTransport implements EventTransport {
140
133
  try {
141
134
  callback(event);
142
135
  } catch (error) {
143
- this.logger.error("Event callback error:", error);
136
+ this.logger.error('Event callback error:', error);
144
137
  }
145
138
  }
146
139
  }
@@ -149,7 +142,7 @@ export class SocketIOEventTransport implements EventTransport {
149
142
  try {
150
143
  callback(event);
151
144
  } catch (error) {
152
- this.logger.error("Event callback error:", error);
145
+ this.logger.error('Event callback error:', error);
153
146
  }
154
147
  }
155
148
  }
@@ -207,12 +200,12 @@ export class SocketIOEventTransport implements EventTransport {
207
200
  */
208
201
  setupClientHandlers(
209
202
  socket: SocketIOSocket,
210
- authorize: SocketIOAuthorizeFn,
203
+ authorize: SocketIOAuthorizeFn
211
204
  ): void {
212
205
  // Subscribe to a specific run
213
- socket.on("workflow:subscribe", async (...args: unknown[]) => {
206
+ socket.on('workflow:subscribe', async (...args: unknown[]) => {
214
207
  const runId = args[0];
215
- if (typeof runId !== "string") return;
208
+ if (typeof runId !== 'string') return;
216
209
 
217
210
  try {
218
211
  const allowed = await authorize(runId, socket);
@@ -221,7 +214,7 @@ export class SocketIOEventTransport implements EventTransport {
221
214
  return;
222
215
  }
223
216
  } catch (error) {
224
- this.logger.error("Authorization check failed:", error);
217
+ this.logger.error('Authorization check failed:', error);
225
218
  return;
226
219
  }
227
220
 
@@ -229,23 +222,23 @@ export class SocketIOEventTransport implements EventTransport {
229
222
  });
230
223
 
231
224
  // Unsubscribe from a specific run
232
- socket.on("workflow:unsubscribe", (...args: unknown[]) => {
225
+ socket.on('workflow:unsubscribe', (...args: unknown[]) => {
233
226
  const runId = args[0];
234
- if (typeof runId === "string") {
227
+ if (typeof runId === 'string') {
235
228
  socket.leave(`${this.roomPrefix}${runId}`);
236
229
  }
237
230
  });
238
231
 
239
232
  // Subscribe to all events (requires authorization to pass with '*' as runId)
240
- socket.on("workflow:subscribe:all", async () => {
233
+ socket.on('workflow:subscribe:all', async () => {
241
234
  try {
242
- const allowed = await authorize("*", socket);
235
+ const allowed = await authorize('*', socket);
243
236
  if (!allowed) {
244
- this.logger.warn("Global subscription denied");
237
+ this.logger.warn('Global subscription denied');
245
238
  return;
246
239
  }
247
240
  } catch (error) {
248
- this.logger.error("Authorization check failed:", error);
241
+ this.logger.error('Authorization check failed:', error);
249
242
  return;
250
243
  }
251
244
 
@@ -253,7 +246,7 @@ export class SocketIOEventTransport implements EventTransport {
253
246
  });
254
247
 
255
248
  // Unsubscribe from all events
256
- socket.on("workflow:unsubscribe:all", () => {
249
+ socket.on('workflow:unsubscribe:all', () => {
257
250
  socket.leave(this.globalRoom);
258
251
  });
259
252
  }