@multiplier-labs/stepflow 0.3.3 → 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 (69) 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-11_comment-assessment.md +161 -0
  4. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
  5. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
  6. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
  7. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
  8. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
  9. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
  10. package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
  11. package/.github/workflows/publish.yml +6 -0
  12. package/dist/{chunk-4VOKMGP6.js → chunk-5XJBAPXF.js} +517 -522
  13. package/dist/chunk-5XJBAPXF.js.map +1 -0
  14. package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
  15. package/dist/chunk-FMPJBK2N.js.map +1 -0
  16. package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
  17. package/dist/chunk-JPXPB7U5.js.map +1 -0
  18. package/dist/{chunk-GYPNJBDP.js → chunk-U3PCVK6O.js} +266 -47
  19. package/dist/chunk-U3PCVK6O.js.map +1 -0
  20. package/dist/events/index.d.ts +56 -3
  21. package/dist/events/index.js +1 -1
  22. package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
  23. package/dist/index.d.ts +2 -12
  24. package/dist/index.js +764 -485
  25. package/dist/index.js.map +1 -1
  26. package/dist/scheduler/index.d.ts +1 -1
  27. package/dist/scheduler/index.js +2 -2
  28. package/dist/storage/index.d.ts +313 -47
  29. package/dist/storage/index.js +4 -2
  30. package/docs/stepflow-api-reference.md +3 -1
  31. package/package.json +7 -4
  32. package/src/core/engine/concurrency.ts +229 -0
  33. package/src/core/engine/lifecycle.ts +176 -0
  34. package/src/core/engine.test.ts +70 -0
  35. package/src/core/engine.ts +91 -185
  36. package/src/core/orchestrator/persistence.ts +165 -0
  37. package/src/core/orchestrator/plan-resolution.ts +140 -0
  38. package/src/core/orchestrator/state-transitions.ts +109 -0
  39. package/src/core/orchestrator/step-runner.ts +427 -0
  40. package/src/core/orchestrator.ts +40 -359
  41. package/src/events/webhook.test.ts +292 -0
  42. package/src/events/webhook.ts +320 -77
  43. package/src/planning/evaluate-condition.test.ts +153 -0
  44. package/src/planning/evaluate-condition.ts +105 -0
  45. package/src/planning/planner.ts +1 -83
  46. package/src/planning/registry.ts +3 -85
  47. package/src/scheduler/cron.test.ts +9 -1
  48. package/src/scheduler/cron.ts +26 -2
  49. package/src/scheduler/postgres-persistence.ts +27 -15
  50. package/src/scheduler/sqlite-persistence.ts +28 -3
  51. package/src/storage/postgres-core.ts +550 -0
  52. package/src/storage/postgres-migration.test.ts +69 -0
  53. package/src/storage/postgres-transaction-pagination.test.ts +107 -0
  54. package/src/storage/postgres.ts +171 -690
  55. package/src/storage/sqlite-core.ts +151 -0
  56. package/src/storage/sqlite.test.ts +177 -1
  57. package/src/storage/sqlite.ts +183 -93
  58. package/src/utils/index.ts +2 -0
  59. package/src/utils/pinned-fetch.test.ts +84 -0
  60. package/src/utils/pinned-fetch.ts +118 -0
  61. package/src/utils/postgres-deps.ts +16 -0
  62. package/src/utils/safe-json.test.ts +166 -0
  63. package/src/utils/safe-json.ts +107 -0
  64. package/src/utils/webhook-signature.test.ts +111 -0
  65. package/src/utils/webhook-signature.ts +74 -0
  66. package/dist/chunk-4VOKMGP6.js.map +0 -1
  67. package/dist/chunk-GYPNJBDP.js.map +0 -1
  68. package/dist/chunk-KHTUWLVZ.js.map +0 -1
  69. package/dist/chunk-ML35PIHX.js.map +0 -1
@@ -215,6 +215,23 @@ interface WebhookEventTransportConfig {
215
215
  * Requests beyond this limit are queued.
216
216
  */
217
217
  maxConcurrentRequests?: number;
218
+ /**
219
+ * Maximum number of queued webhook deliveries waiting for a concurrency slot
220
+ * (default: 1000, env override: `STEPFLOW_WEBHOOK_MAX_QUEUE_DEPTH`).
221
+ *
222
+ * When the queue is full, incoming `emit()` deliveries are dropped to
223
+ * protect the process from memory exhaustion under burst traffic. Drops
224
+ * are logged via `logger.warn` with the endpoint id, current queue depth,
225
+ * and a recommended `retryAfterMs`. A value of `0` disables the cap (not
226
+ * recommended in production).
227
+ */
228
+ maxQueueDepth?: number;
229
+ /**
230
+ * Suggested back-off in milliseconds reported on dropped deliveries
231
+ * (default: 1000). Surfaced in the warning log so external orchestrators
232
+ * (sidecars, sweeper jobs) can respect it like an HTTP `Retry-After`.
233
+ */
234
+ queueDropRetryAfterMs?: number;
218
235
  /** Logger for webhook transport errors (default: console-based) */
219
236
  logger?: Logger;
220
237
  }
@@ -267,12 +284,25 @@ declare class WebhookEventTransport implements EventTransport {
267
284
  private defaultRetries;
268
285
  private retryDelay;
269
286
  private fetchFn;
287
+ /**
288
+ * Whether `fetchFn` was caller-supplied. When `false` the default fetch is
289
+ * replaced with `pinnedFetch` so the TCP connection can target the IP that
290
+ * was just validated (closes the DNS-rebinding TOCTOU window). When `true`
291
+ * the custom fetch is used as-is so test injection still works.
292
+ */
293
+ private hasCustomFetch;
270
294
  private allowInsecureUrls;
271
295
  private maxPayloadBytes;
272
296
  private maxConcurrentRequests;
297
+ private maxQueueDepth;
298
+ private queueDropRetryAfterMs;
273
299
  private activeRequests;
274
300
  private requestQueue;
301
+ private droppedRequests;
275
302
  private logger;
303
+ private dnsCache;
304
+ private static readonly DNS_CACHE_TTL_MS;
305
+ private static readonly DNS_TIMEOUT_MS;
276
306
  private runSubscribers;
277
307
  private globalSubscribers;
278
308
  constructor(config?: WebhookEventTransportConfig);
@@ -298,6 +328,15 @@ declare class WebhookEventTransport implements EventTransport {
298
328
  * Emit an event to all matching webhook endpoints.
299
329
  */
300
330
  emit(event: WorkflowEvent): void;
331
+ /**
332
+ * Number of webhook deliveries dropped due to queue overflow since startup.
333
+ * Useful for surfacing back-pressure to operators (metrics/health checks).
334
+ */
335
+ getDroppedRequestCount(): number;
336
+ /**
337
+ * Current depth of the pending request queue.
338
+ */
339
+ getQueueDepth(): number;
301
340
  /**
302
341
  * Subscribe to events for a specific run (server-side).
303
342
  */
@@ -327,8 +366,18 @@ declare class WebhookEventTransport implements EventTransport {
327
366
  */
328
367
  private sleep;
329
368
  /**
330
- * Resolve the hostname from a URL and verify the resolved IP is not private/reserved.
331
- * Prevents DNS rebinding attacks where a public hostname resolves to a private IP at send time.
369
+ * Resolve a webhook URL's hostname exactly once, verify the resolved IP is
370
+ * not private/reserved, and return the address + family so the caller can
371
+ * pin the connection to it.
372
+ *
373
+ * Returns `null` for raw-IP URLs (already validated by `validateWebhookUrl`).
374
+ *
375
+ * Pinning the resolved IP closes the DNS rebinding TOCTOU window (audit M1):
376
+ * with the original implementation `fetch()` re-resolved the hostname,
377
+ * giving a hostile DNS server a chance to flip the answer to an internal
378
+ * address between SSRF validation and connect.
379
+ *
380
+ * Uses a short-TTL cache and per-call timeout to avoid blocking the event loop.
332
381
  */
333
382
  private validateResolvedHost;
334
383
  /**
@@ -338,7 +387,11 @@ declare class WebhookEventTransport implements EventTransport {
338
387
  */
339
388
  private validateWebhookUrl;
340
389
  /**
341
- * Enqueue a webhook request with concurrency limiting.
390
+ * Enqueue a webhook request with concurrency limiting and a queue-depth cap.
391
+ * Returns `true` if the request was accepted (executed or queued), or
392
+ * `false` if the queue is full and the request was rejected. The caller is
393
+ * responsible for logging the drop and incrementing any drop metrics, since
394
+ * it has the endpoint context.
342
395
  */
343
396
  private enqueueRequest;
344
397
  }
@@ -2,7 +2,7 @@ import {
2
2
  MemoryEventTransport,
3
3
  SocketIOEventTransport,
4
4
  WebhookEventTransport
5
- } from "../chunk-GYPNJBDP.js";
5
+ } from "../chunk-U3PCVK6O.js";
6
6
  export {
7
7
  MemoryEventTransport,
8
8
  SocketIOEventTransport,
@@ -10,6 +10,10 @@ import { Pool, PoolConfig } from 'pg';
10
10
  * This is the primary entry point for the workflow engine.
11
11
  * It provides methods for registering workflows, starting runs,
12
12
  * and managing workflow execution.
13
+ *
14
+ * Internal concerns are decomposed into co-located submodules under
15
+ * `./engine/` (see `engine/concurrency.ts`, `engine/lifecycle.ts`).
16
+ * WorkflowEngine itself stays the thin public face.
13
17
  */
14
18
 
15
19
  /**
@@ -47,14 +51,40 @@ interface StartRunOptions<TInput = Record<string, unknown>> {
47
51
  /** Priority for queue ordering (higher = runs first, default: 0) */
48
52
  priority?: number;
49
53
  }
54
+ /**
55
+ * Main workflow engine class.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const engine = new WorkflowEngine({
60
+ * storage: new MemoryStorageAdapter(),
61
+ * });
62
+ *
63
+ * engine.registerWorkflow({
64
+ * kind: 'my.workflow',
65
+ * name: 'My Workflow',
66
+ * steps: [
67
+ * {
68
+ * key: 'step1',
69
+ * name: 'First Step',
70
+ * handler: async (ctx) => {
71
+ * return { message: 'Hello!' };
72
+ * },
73
+ * },
74
+ * ],
75
+ * });
76
+ *
77
+ * const runId = await engine.startRun({ kind: 'my.workflow' });
78
+ * ```
79
+ */
50
80
  declare class WorkflowEngine {
51
81
  private registry;
52
82
  private storage;
53
83
  private events;
54
84
  private logger;
55
- private activeRuns;
56
85
  private settings;
57
- private runQueue;
86
+ private concurrency;
87
+ private lifecycle;
58
88
  constructor(config?: WorkflowEngineConfig);
59
89
  /**
60
90
  * Initialize the engine and its storage/event adapters.
@@ -70,10 +100,6 @@ declare class WorkflowEngine {
70
100
  * Get the number of queued runs waiting for capacity.
71
101
  */
72
102
  getQueuedRunCount(): number;
73
- /**
74
- * Check if capacity is available for a new run.
75
- */
76
- private hasCapacity;
77
103
  /**
78
104
  * Register a workflow definition.
79
105
  * Must be called before runs of this type can be started.
@@ -110,10 +136,6 @@ declare class WorkflowEngine {
110
136
  * @throws WorkflowNotFoundError if the workflow kind is not registered
111
137
  */
112
138
  startRun<TInput = Record<string, unknown>>(options: StartRunOptions<TInput>): Promise<string>;
113
- /**
114
- * Queue a run in priority order.
115
- */
116
- private queueRun;
117
139
  /**
118
140
  * Execute a run (internal method).
119
141
  */
@@ -124,7 +146,8 @@ declare class WorkflowEngine {
124
146
  */
125
147
  private launchRun;
126
148
  /**
127
- * Process the queue and start runs if capacity is available.
149
+ * Drain the queue, dispatching each ready run via `executeRun`. Resilience
150
+ * against per-item dispatch failures lives in `ConcurrencyManager.processQueue`.
128
151
  */
129
152
  private processQueue;
130
153
  /**
@@ -415,6 +438,12 @@ interface SQLiteSchedulePersistenceConfig {
415
438
  db: Database;
416
439
  /** Table name for schedules (default: workflow_schedules) */
417
440
  tableName?: string;
441
+ /**
442
+ * Optional structured logger used to surface JSON corruption events.
443
+ * Without one, parse failures still increment the corruption counter
444
+ * exposed via `getJsonParseCorruptionCount()` but produce no log output.
445
+ */
446
+ logger?: Logger;
418
447
  }
419
448
  /**
420
449
  * SQLite-based persistence for workflow schedules.
@@ -435,6 +464,7 @@ interface SQLiteSchedulePersistenceConfig {
435
464
  declare class SQLiteSchedulePersistence implements SchedulePersistence {
436
465
  private db;
437
466
  private tableName;
467
+ private logger;
438
468
  private stmts;
439
469
  constructor(config: SQLiteSchedulePersistenceConfig);
440
470
  private initializeDatabase;
@@ -442,6 +472,7 @@ declare class SQLiteSchedulePersistence implements SchedulePersistence {
442
472
  saveSchedule(schedule: WorkflowSchedule): Promise<void>;
443
473
  updateSchedule(scheduleId: string, updates: Partial<WorkflowSchedule>): Promise<void>;
444
474
  deleteSchedule(scheduleId: string): Promise<void>;
475
+ private safeJsonParse;
445
476
  private rowToSchedule;
446
477
  }
447
478
 
@@ -484,6 +515,12 @@ interface PostgresSchedulePersistenceConfig {
484
515
  * @default true
485
516
  */
486
517
  autoMigrate?: boolean;
518
+ /**
519
+ * Optional structured logger used to surface JSON corruption events.
520
+ * Without one, parse failures still increment the corruption counter
521
+ * exposed via `getJsonParseCorruptionCount()` but produce no log output.
522
+ */
523
+ logger?: Logger;
487
524
  }
488
525
  /**
489
526
  * PostgreSQL-based persistence for workflow schedules.
@@ -523,6 +560,7 @@ declare class PostgresSchedulePersistence implements SchedulePersistence {
523
560
  private autoMigrate;
524
561
  private initialized;
525
562
  private config;
563
+ private logger;
526
564
  constructor(config: PostgresSchedulePersistenceConfig);
527
565
  /**
528
566
  * Get a schema-scoped query builder.
@@ -561,6 +599,7 @@ declare class PostgresSchedulePersistence implements SchedulePersistence {
561
599
  * Get workflow completion triggers for a specific workflow kind.
562
600
  */
563
601
  getCompletionTriggers(triggerOnWorkflowKind: string): Promise<WorkflowSchedule[]>;
602
+ private safeParseOptionalField;
564
603
  private rowToSchedule;
565
604
  }
566
605
 
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { a as WorkflowError, L as Logger, W as WorkflowKind, b as StepErrorStrategy, c as WorkflowStep } from './types-K5Gjk3H_.js';
2
2
  export { d as RunResult, R as RunStatus, e as SpawnChildOptions, S as StepStatus, f as WorkflowContext, g as WorkflowDefinition, h as WorkflowHooks } from './types-K5Gjk3H_.js';
3
- export { C as CronScheduler, a as CronSchedulerConfig, P as PostgresSchedulePersistence, b as PostgresSchedulePersistenceConfig, S as SQLiteSchedulePersistence, c as SQLiteSchedulePersistenceConfig, d as SchedulePersistence, e as Scheduler, f as StartRunOptions, T as TriggerType, W as WorkflowEngine, g as WorkflowEngineConfig, h as WorkflowSchedule } from './index-Dzr437iW.js';
3
+ export { C as CronScheduler, a as CronSchedulerConfig, P as PostgresSchedulePersistence, b as PostgresSchedulePersistenceConfig, S as SQLiteSchedulePersistence, c as SQLiteSchedulePersistenceConfig, d as SchedulePersistence, e as Scheduler, f as StartRunOptions, T as TriggerType, W as WorkflowEngine, g as WorkflowEngineConfig, h as WorkflowSchedule } from './index-Cd0IZ40L.js';
4
4
  export { C as CreateRunInput, E as ExtendedListRunsOptions, a as ExtendedRunStatus, b as ExtendedStepStatus, c as ExtendedWorkflowRunRecord, L as ListEventsOptions, d as ListRunsOptions, P as PaginatedResult, S as StepRecord, e as StepResult, f as StepflowDatabase, g as StepflowRunsTable, h as StepflowStepResultsTable, i as StorageAdapter, U as UpdateRunInput, W as WorkflowEventRecord, j as WorkflowRunRecord, k as WorkflowRunStepRecord, l as WorkflowStorage } from './types-DSEjiLMj.js';
5
5
  export { MemoryStorageAdapter, PostgresStorage, PostgresStorage as PostgresStorageAdapter, PostgresStorageConfig, SQLiteStorageAdapter, SQLiteStorageConfig } from './storage/index.js';
6
6
  export { B as BuiltInEventType, E as EventCallback, a as EventTransport, U as Unsubscribe, W as WorkflowEvent, b as WorkflowEventType } from './types-DZUdWmCc.js';
7
7
  export { MemoryEventTransport, SocketIOAuthorizeFn, SocketIOEventTransport, SocketIOEventTransportConfig, SocketIOServer, SocketIOSocket, WebhookEndpoint, WebhookEventTransport, WebhookEventTransportConfig, WebhookPayload } from './events/index.js';
8
8
  import 'better-sqlite3';
9
9
  import 'pg';
10
+ import 'kysely';
10
11
 
11
12
  /**
12
13
  * ID generation utilities for the workflow engine.
@@ -647,19 +648,8 @@ declare class MemoryRecipeRegistry implements RecipeRegistry {
647
648
  /**
648
649
  * Evaluate recipe conditions against an input.
649
650
  * Returns true if all conditions match.
650
- *
651
- * Note: This intentionally mirrors the condition evaluation in planner.ts
652
- * to keep registry queries independent of the planner implementation.
653
651
  */
654
652
  private evaluateConditions;
655
- /**
656
- * Get a nested value from an object using dot notation.
657
- */
658
- private getNestedValue;
659
- /**
660
- * Evaluate a single condition.
661
- */
662
- private evaluateCondition;
663
653
  }
664
654
  /**
665
655
  * Create a combined registry that holds both recipes and step handlers.