@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
@@ -5,15 +5,9 @@
5
5
  * Supports multiple webhooks with optional filtering by event type.
6
6
  */
7
7
 
8
- import { promises as dns } from "node:dns";
9
- import type {
10
- EventTransport,
11
- EventCallback,
12
- Unsubscribe,
13
- WorkflowEvent,
14
- WorkflowEventType,
15
- } from "./types";
16
- import type { Logger } from "../core/types";
8
+ import { promises as dns } from 'node:dns';
9
+ import type { EventTransport, EventCallback, Unsubscribe, WorkflowEvent, WorkflowEventType } from './types';
10
+ import type { Logger } from '../core/types';
17
11
 
18
12
  /**
19
13
  * Webhook endpoint configuration.
@@ -85,7 +79,7 @@ export interface WebhookEventTransportConfig {
85
79
  maxConcurrentRequests?: number;
86
80
 
87
81
  /** Logger for webhook transport errors (default: console-based) */
88
- logger?: import("../core/types").Logger;
82
+ logger?: import('../core/types').Logger;
89
83
  }
90
84
 
91
85
  /**
@@ -97,7 +91,7 @@ export interface WebhookPayload {
97
91
  * The `Date` is converted to a string before delivery so that JSON consumers
98
92
  * receive a portable, timezone-aware representation without needing Date parsing.
99
93
  */
100
- event: Omit<WorkflowEvent, "timestamp"> & { timestamp: string };
94
+ event: Omit<WorkflowEvent, 'timestamp'> & { timestamp: string };
101
95
  /** ISO 8601 timestamp of when this payload was delivered. */
102
96
  deliveredAt: string;
103
97
  /** ID of the webhook endpoint that received this delivery. */
@@ -155,15 +149,20 @@ export class WebhookEventTransport implements EventTransport {
155
149
  this.allowInsecureUrls = config.allowInsecureUrls ?? false;
156
150
  this.maxPayloadBytes = config.maxPayloadBytes ?? 1_048_576; // 1 MB
157
151
  this.maxConcurrentRequests = config.maxConcurrentRequests ?? 50;
152
+ if (this.maxConcurrentRequests <= 0) {
153
+ throw new Error('maxConcurrentRequests must be greater than 0');
154
+ }
158
155
  this.logger = config.logger ?? {
159
156
  debug() {},
160
157
  info() {},
161
158
  warn() {},
162
- error(message: string, ...args: unknown[]) {
163
- console.error(message, ...args);
164
- },
159
+ error(message: string, ...args: unknown[]) { console.error(message, ...args); },
165
160
  };
166
161
 
162
+ if (this.allowInsecureUrls) {
163
+ this.logger.warn('WebhookEventTransport: allowInsecureUrls is enabled. Webhook payloads will be sent over HTTP. Do not use this setting in production.');
164
+ }
165
+
167
166
  // Register initial endpoints (validates URLs)
168
167
  if (config.endpoints) {
169
168
  for (const endpoint of config.endpoints) {
@@ -217,7 +216,7 @@ export class WebhookEventTransport implements EventTransport {
217
216
  try {
218
217
  callback(event);
219
218
  } catch (error) {
220
- this.logger.error("Event callback error:", error);
219
+ this.logger.error('Event callback error:', error);
221
220
  }
222
221
  }
223
222
  }
@@ -226,7 +225,7 @@ export class WebhookEventTransport implements EventTransport {
226
225
  try {
227
226
  callback(event);
228
227
  } catch (error) {
229
- this.logger.error("Event callback error:", error);
228
+ this.logger.error('Event callback error:', error);
230
229
  }
231
230
  }
232
231
 
@@ -238,7 +237,7 @@ export class WebhookEventTransport implements EventTransport {
238
237
  this.enqueueRequest(() =>
239
238
  this.sendWebhook(endpoint, event).catch((error) => {
240
239
  this.logger.error(`Webhook ${endpoint.id} failed:`, error);
241
- }),
240
+ })
242
241
  );
243
242
  }
244
243
  }
@@ -284,10 +283,7 @@ export class WebhookEventTransport implements EventTransport {
284
283
  /**
285
284
  * Check if an event matches an endpoint's filters.
286
285
  */
287
- private matchesFilter(
288
- event: WorkflowEvent,
289
- endpoint: WebhookEndpoint,
290
- ): boolean {
286
+ private matchesFilter(event: WorkflowEvent, endpoint: WebhookEndpoint): boolean {
291
287
  // Check event type filter
292
288
  if (endpoint.eventTypes && endpoint.eventTypes.length > 0) {
293
289
  if (!endpoint.eventTypes.includes(event.eventType)) {
@@ -308,10 +304,7 @@ export class WebhookEventTransport implements EventTransport {
308
304
  /**
309
305
  * Send a webhook with retry support.
310
306
  */
311
- private async sendWebhook(
312
- endpoint: WebhookEndpoint,
313
- event: WorkflowEvent,
314
- ): Promise<void> {
307
+ private async sendWebhook(endpoint: WebhookEndpoint, event: WorkflowEvent): Promise<void> {
315
308
  const timeout = endpoint.timeout ?? this.defaultTimeout;
316
309
  const maxRetries = endpoint.retries ?? this.defaultRetries;
317
310
 
@@ -329,19 +322,19 @@ export class WebhookEventTransport implements EventTransport {
329
322
  // Enforce payload size limit to prevent excessive memory use and network abuse
330
323
  if (body.length > this.maxPayloadBytes) {
331
324
  throw new Error(
332
- `Webhook payload exceeds maximum size (${body.length} bytes > ${this.maxPayloadBytes} bytes)`,
325
+ `Webhook payload exceeds maximum size (${body.length} bytes > ${this.maxPayloadBytes} bytes)`
333
326
  );
334
327
  }
335
328
 
336
329
  const headers: Record<string, string> = {
337
- "Content-Type": "application/json",
330
+ 'Content-Type': 'application/json',
338
331
  ...endpoint.headers,
339
332
  };
340
333
 
341
334
  // Sign payload if secret is provided
342
335
  if (endpoint.secret) {
343
336
  const signature = await this.signPayload(body, endpoint.secret);
344
- headers["X-Webhook-Signature"] = signature;
337
+ headers['X-Webhook-Signature'] = signature;
345
338
  }
346
339
 
347
340
  // Resolve hostname and validate the resolved IP is not private/reserved (SSRF protection).
@@ -357,7 +350,7 @@ export class WebhookEventTransport implements EventTransport {
357
350
  const timeoutId = setTimeout(() => controller.abort(), timeout);
358
351
 
359
352
  const response = await this.fetchFn(endpoint.url, {
360
- method: "POST",
353
+ method: 'POST',
361
354
  headers,
362
355
  body,
363
356
  signal: controller.signal,
@@ -369,9 +362,7 @@ export class WebhookEventTransport implements EventTransport {
369
362
  return; // Success
370
363
  }
371
364
 
372
- lastError = new Error(
373
- `Webhook returned ${response.status}: ${response.statusText}`,
374
- );
365
+ lastError = new Error(`Webhook returned ${response.status}: ${response.statusText}`);
375
366
  } catch (error) {
376
367
  lastError = error instanceof Error ? error : new Error(String(error));
377
368
  }
@@ -395,18 +386,16 @@ export class WebhookEventTransport implements EventTransport {
395
386
  const data = encoder.encode(payload);
396
387
 
397
388
  const key = await crypto.subtle.importKey(
398
- "raw",
389
+ 'raw',
399
390
  keyData,
400
- { name: "HMAC", hash: "SHA-256" },
391
+ { name: 'HMAC', hash: 'SHA-256' },
401
392
  false,
402
- ["sign"],
393
+ ['sign']
403
394
  );
404
395
 
405
- const signature = await crypto.subtle.sign("HMAC", key, data);
396
+ const signature = await crypto.subtle.sign('HMAC', key, data);
406
397
  const hashArray = Array.from(new Uint8Array(signature));
407
- const hashHex = hashArray
408
- .map((b) => b.toString(16).padStart(2, "0"))
409
- .join("");
398
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
410
399
 
411
400
  return `sha256=${hashHex}`;
412
401
  }
@@ -415,7 +404,7 @@ export class WebhookEventTransport implements EventTransport {
415
404
  * Sleep helper.
416
405
  */
417
406
  private sleep(ms: number): Promise<void> {
418
- return new Promise((resolve) => setTimeout(resolve, ms));
407
+ return new Promise(resolve => setTimeout(resolve, ms));
419
408
  }
420
409
 
421
410
  /**
@@ -435,19 +424,14 @@ export class WebhookEventTransport implements EventTransport {
435
424
  const { address } = await dns.lookup(hostname);
436
425
  if (isBlockedIp(address)) {
437
426
  throw new Error(
438
- `Webhook URL hostname "${hostname}" resolves to blocked IP ${address}`,
427
+ `Webhook URL hostname "${hostname}" resolves to blocked IP ${address}`
439
428
  );
440
429
  }
441
430
  } catch (error) {
442
- if (
443
- error instanceof Error &&
444
- error.message.includes("resolves to blocked IP")
445
- ) {
431
+ if (error instanceof Error && error.message.includes('resolves to blocked IP')) {
446
432
  throw error;
447
433
  }
448
- throw new Error(
449
- `Failed to resolve webhook URL hostname "${hostname}": ${error}`,
450
- );
434
+ throw new Error(`Failed to resolve webhook URL hostname "${hostname}": ${error}`);
451
435
  }
452
436
  }
453
437
 
@@ -465,24 +449,20 @@ export class WebhookEventTransport implements EventTransport {
465
449
  }
466
450
 
467
451
  // Scheme check
468
- if (!this.allowInsecureUrls && parsed.protocol !== "https:") {
452
+ if (!this.allowInsecureUrls && parsed.protocol !== 'https:') {
469
453
  throw new Error(
470
- `Webhook URL must use HTTPS (got ${parsed.protocol}). Set allowInsecureUrls for development.`,
454
+ `Webhook URL must use HTTPS (got ${parsed.protocol}). Set allowInsecureUrls for development.`
471
455
  );
472
456
  }
473
457
 
474
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
475
- throw new Error(
476
- `Webhook URL must use HTTP(S) scheme (got ${parsed.protocol})`,
477
- );
458
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
459
+ throw new Error(`Webhook URL must use HTTP(S) scheme (got ${parsed.protocol})`);
478
460
  }
479
461
 
480
462
  // Block dangerous hostnames
481
463
  const hostname = parsed.hostname.toLowerCase();
482
464
  if (isBlockedHost(hostname)) {
483
- throw new Error(
484
- `Webhook URL hostname is blocked (private/reserved): ${hostname}`,
485
- );
465
+ throw new Error(`Webhook URL hostname is blocked (private/reserved): ${hostname}`);
486
466
  }
487
467
  }
488
468
 
@@ -513,29 +493,22 @@ export class WebhookEventTransport implements EventTransport {
513
493
  */
514
494
  function isBlockedHost(hostname: string): boolean {
515
495
  // Loopback (Node's URL parser wraps IPv6 in brackets, e.g. "[::1]")
516
- if (
517
- hostname === "localhost" ||
518
- hostname === "127.0.0.1" ||
519
- hostname === "::1" ||
520
- hostname === "[::1]"
521
- ) {
496
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]') {
522
497
  return true;
523
498
  }
524
499
 
525
500
  // Block all IPv6 address literals (wrapped in brackets by URL parser)
526
- if (hostname.startsWith("[")) {
501
+ if (hostname.startsWith('[')) {
527
502
  return true;
528
503
  }
529
504
 
530
505
  // Cloud metadata endpoint
531
- if (hostname === "169.254.169.254") {
506
+ if (hostname === '169.254.169.254') {
532
507
  return true;
533
508
  }
534
509
 
535
510
  // Check for IP addresses in private/reserved ranges
536
- const ipv4Match = hostname.match(
537
- /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
538
- );
511
+ const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
539
512
  if (ipv4Match) {
540
513
  const [, a, b] = ipv4Match.map(Number);
541
514
  // 10.0.0.0/8
@@ -553,7 +526,7 @@ function isBlockedHost(hostname: string): boolean {
553
526
  }
554
527
 
555
528
  // IPv6 link-local (fe80::)
556
- if (hostname.startsWith("fe80:") || hostname.startsWith("[fe80:")) {
529
+ if (hostname.startsWith('fe80:') || hostname.startsWith('[fe80:')) {
557
530
  return true;
558
531
  }
559
532
 
@@ -583,9 +556,9 @@ function isBlockedIp(ip: string): boolean {
583
556
  }
584
557
 
585
558
  // IPv6 loopback
586
- if (ip === "::1") return true;
559
+ if (ip === '::1') return true;
587
560
  // IPv6 link-local
588
- if (ip.startsWith("fe80:")) return true;
561
+ if (ip.startsWith('fe80:')) return true;
589
562
 
590
563
  return false;
591
564
  }
package/src/index.ts CHANGED
@@ -73,14 +73,14 @@ export {
73
73
  type WorkflowStep,
74
74
  type WorkflowHooks,
75
75
  type WorkflowDefinition,
76
- } from "./core/types";
76
+ } from './core/types';
77
77
 
78
78
  export {
79
79
  // Main Engine
80
80
  WorkflowEngine,
81
81
  type WorkflowEngineConfig,
82
82
  type StartRunOptions,
83
- } from "./core/engine";
83
+ } from './core/engine';
84
84
 
85
85
  // ============================================================================
86
86
  // Storage
@@ -108,25 +108,25 @@ export {
108
108
  type StepflowRunsTable,
109
109
  type StepflowStepResultsTable,
110
110
  type StepflowDatabase,
111
- } from "./storage/types";
111
+ } from './storage/types';
112
112
 
113
113
  export {
114
114
  // In-Memory Storage
115
115
  MemoryStorageAdapter,
116
- } from "./storage/memory";
116
+ } from './storage/memory';
117
117
 
118
118
  export {
119
119
  // SQLite Storage
120
120
  SQLiteStorageAdapter,
121
121
  type SQLiteStorageConfig,
122
- } from "./storage/sqlite";
122
+ } from './storage/sqlite';
123
123
 
124
124
  export {
125
125
  // PostgreSQL Storage
126
126
  PostgresStorageAdapter,
127
127
  PostgresStorageAdapter as PostgresStorage,
128
128
  type PostgresStorageConfig,
129
- } from "./storage/postgres";
129
+ } from './storage/postgres';
130
130
 
131
131
  // ============================================================================
132
132
  // Events
@@ -140,12 +140,12 @@ export {
140
140
  type BuiltInEventType,
141
141
  type EventCallback,
142
142
  type Unsubscribe,
143
- } from "./events/types";
143
+ } from './events/types';
144
144
 
145
145
  export {
146
146
  // In-Memory Event Transport
147
147
  MemoryEventTransport,
148
- } from "./events/memory";
148
+ } from './events/memory';
149
149
 
150
150
  export {
151
151
  // Socket.IO Event Transport
@@ -154,7 +154,7 @@ export {
154
154
  type SocketIOServer,
155
155
  type SocketIOSocket,
156
156
  type SocketIOAuthorizeFn,
157
- } from "./events/socketio";
157
+ } from './events/socketio';
158
158
 
159
159
  export {
160
160
  // Webhook Event Transport
@@ -162,7 +162,7 @@ export {
162
162
  type WebhookEventTransportConfig,
163
163
  type WebhookEndpoint,
164
164
  type WebhookPayload,
165
- } from "./events/webhook";
165
+ } from './events/webhook';
166
166
 
167
167
  // ============================================================================
168
168
  // Scheduler
@@ -172,23 +172,23 @@ export {
172
172
  type TriggerType,
173
173
  type WorkflowSchedule,
174
174
  type Scheduler,
175
- } from "./scheduler/types";
175
+ } from './scheduler/types';
176
176
 
177
177
  export {
178
178
  CronScheduler,
179
179
  type CronSchedulerConfig,
180
180
  type SchedulePersistence,
181
- } from "./scheduler/cron";
181
+ } from './scheduler/cron';
182
182
 
183
183
  export {
184
184
  SQLiteSchedulePersistence,
185
185
  type SQLiteSchedulePersistenceConfig,
186
- } from "./scheduler/sqlite-persistence";
186
+ } from './scheduler/sqlite-persistence';
187
187
 
188
188
  export {
189
189
  PostgresSchedulePersistence,
190
190
  type PostgresSchedulePersistenceConfig,
191
- } from "./scheduler/postgres-persistence";
191
+ } from './scheduler/postgres-persistence';
192
192
 
193
193
  // ============================================================================
194
194
  // Utilities
@@ -197,7 +197,7 @@ export {
197
197
  export {
198
198
  // ID Generation
199
199
  generateId,
200
- } from "./utils/id";
200
+ } from './utils/id';
201
201
 
202
202
  export {
203
203
  // Errors
@@ -210,7 +210,7 @@ export {
210
210
  WaitForRunTimeoutError,
211
211
  WorkflowCanceledError,
212
212
  WorkflowTimeoutError,
213
- } from "./utils/errors";
213
+ } from './utils/errors';
214
214
 
215
215
  export {
216
216
  // Logger
@@ -219,7 +219,7 @@ export {
219
219
  createScopedLogger,
220
220
  sanitizeErrorForStorage,
221
221
  type LogLevel,
222
- } from "./utils/logger";
222
+ } from './utils/logger';
223
223
 
224
224
  export {
225
225
  // Retry
@@ -228,7 +228,7 @@ export {
228
228
  sleep,
229
229
  withRetry,
230
230
  calculateRetryDelay,
231
- } from "./utils/retry";
231
+ } from './utils/retry';
232
232
 
233
233
  // ============================================================================
234
234
  // Planning
@@ -267,4 +267,4 @@ export {
267
267
  type CombinedRegistry,
268
268
  RuleBasedPlanner,
269
269
  type RuleBasedPlannerConfig,
270
- } from "./planning";
270
+ } from './planning';
@@ -39,7 +39,7 @@ export type {
39
39
  StepHandlerRegistry,
40
40
  RecipeQueryOptions,
41
41
  RecipeRegistry,
42
- } from "./types";
42
+ } from './types';
43
43
 
44
44
  // Implementations
45
45
  export {
@@ -47,6 +47,9 @@ export {
47
47
  MemoryRecipeRegistry,
48
48
  createRegistry,
49
49
  type CombinedRegistry,
50
- } from "./registry";
50
+ } from './registry';
51
51
 
52
- export { RuleBasedPlanner, type RuleBasedPlannerConfig } from "./planner";
52
+ export {
53
+ RuleBasedPlanner,
54
+ type RuleBasedPlannerConfig,
55
+ } from './planner';