@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
@@ -3,17 +3,16 @@
3
3
  * Provides storage and retrieval of recipes and handlers.
4
4
  */
5
5
 
6
- import RE2 from "re2";
7
- import type { WorkflowStep } from "../core/types";
6
+ import type { WorkflowStep } from '../core/types';
7
+ import { evaluateCondition, getNestedValue } from './evaluate-condition';
8
8
  import type {
9
9
  Recipe,
10
- RecipeCondition,
11
10
  RecipeRegistry,
12
11
  RecipeQueryOptions,
13
12
  RegisteredStepHandler,
14
13
  StepHandlerRegistry,
15
14
  WorkflowKind,
16
- } from "./types";
15
+ } from './types';
17
16
 
18
17
  // ============================================================================
19
18
  // Step Handler Registry Implementation
@@ -28,7 +27,7 @@ export class MemoryStepHandlerRegistry implements StepHandlerRegistry {
28
27
 
29
28
  /** Register a step handler. Throws if a handler with the same ID is already registered. */
30
29
  register<TInput = Record<string, unknown>>(
31
- handler: RegisteredStepHandler<TInput>,
30
+ handler: RegisteredStepHandler<TInput>
32
31
  ): void {
33
32
  if (this.handlers.has(handler.id)) {
34
33
  throw new Error(`Step handler '${handler.id}' is already registered`);
@@ -68,7 +67,7 @@ export class MemoryStepHandlerRegistry implements StepHandlerRegistry {
68
67
  if (!ids) return [];
69
68
 
70
69
  return Array.from(ids)
71
- .map((id) => this.handlers.get(id))
70
+ .map(id => this.handlers.get(id))
72
71
  .filter((h): h is RegisteredStepHandler => h !== undefined);
73
72
  }
74
73
 
@@ -76,7 +75,7 @@ export class MemoryStepHandlerRegistry implements StepHandlerRegistry {
76
75
  * Resolve a handler reference to a WorkflowStep handler function.
77
76
  * Returns undefined if the handler is not found.
78
77
  */
79
- resolve(handlerRef: string): WorkflowStep["handler"] | undefined {
78
+ resolve(handlerRef: string): WorkflowStep['handler'] | undefined {
80
79
  const registered = this.handlers.get(handlerRef);
81
80
  return registered?.handler;
82
81
  }
@@ -121,7 +120,7 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
121
120
  const variantKey = `${recipe.workflowKind}:${recipe.variant}`;
122
121
  if (this.variantIndex.has(variantKey)) {
123
122
  throw new Error(
124
- `Recipe variant '${recipe.variant}' for '${recipe.workflowKind}' is already registered`,
123
+ `Recipe variant '${recipe.variant}' for '${recipe.workflowKind}' is already registered`
125
124
  );
126
125
  }
127
126
  this.variantIndex.set(variantKey, recipe.id);
@@ -160,7 +159,7 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
160
159
  if (!ids) return [];
161
160
 
162
161
  return Array.from(ids)
163
- .map((id) => this.recipes.get(id))
162
+ .map(id => this.recipes.get(id))
164
163
  .filter((r): r is Recipe => r !== undefined);
165
164
  }
166
165
 
@@ -179,7 +178,7 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
179
178
  */
180
179
  getDefault(workflowKind: WorkflowKind): Recipe | undefined {
181
180
  // First try to find a 'default' variant
182
- const defaultRecipe = this.getVariant(workflowKind, "default");
181
+ const defaultRecipe = this.getVariant(workflowKind, 'default');
183
182
  if (defaultRecipe) return defaultRecipe;
184
183
 
185
184
  // Fall back to the recipe with the lowest numeric priority (highest precedence).
@@ -198,7 +197,7 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
198
197
  /** List all variant names registered for a workflow kind. */
199
198
  listVariants(workflowKind: WorkflowKind): string[] {
200
199
  const recipes = this.getByKind(workflowKind);
201
- return recipes.map((r) => r.variant);
200
+ return recipes.map(r => r.variant);
202
201
  }
203
202
 
204
203
  /** Query recipes with optional filters for kind, variant, tags, and input conditions. */
@@ -206,21 +205,21 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
206
205
  let results = this.list();
207
206
 
208
207
  if (options.workflowKind) {
209
- results = results.filter((r) => r.workflowKind === options.workflowKind);
208
+ results = results.filter(r => r.workflowKind === options.workflowKind);
210
209
  }
211
210
 
212
211
  if (options.variant) {
213
- results = results.filter((r) => r.variant === options.variant);
212
+ results = results.filter(r => r.variant === options.variant);
214
213
  }
215
214
 
216
215
  if (options.tags && options.tags.length > 0) {
217
- results = results.filter((r) =>
218
- options.tags!.some((tag) => r.tags?.includes(tag)),
216
+ results = results.filter(r =>
217
+ options.tags!.some(tag => r.tags?.includes(tag))
219
218
  );
220
219
  }
221
220
 
222
221
  if (options.matchConditions) {
223
- results = results.filter((r) => {
222
+ results = results.filter(r => {
224
223
  if (!r.conditions || r.conditions.length === 0) return true;
225
224
  return this.evaluateConditions(r.conditions, options.matchConditions!);
226
225
  });
@@ -247,103 +246,18 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
247
246
  /**
248
247
  * Evaluate recipe conditions against an input.
249
248
  * Returns true if all conditions match.
250
- *
251
- * Note: This intentionally mirrors the condition evaluation in planner.ts
252
- * to keep registry queries independent of the planner implementation.
253
249
  */
254
250
  private evaluateConditions(
255
- conditions: Recipe["conditions"],
256
- input: Record<string, unknown>,
251
+ conditions: Recipe['conditions'],
252
+ input: Record<string, unknown>
257
253
  ): boolean {
258
254
  if (!conditions || conditions.length === 0) return true;
259
255
 
260
- return conditions.every((condition) => {
261
- const fieldValue = this.getNestedValue(input, condition.field);
262
- return this.evaluateCondition(condition, fieldValue);
256
+ return conditions.every(condition => {
257
+ const fieldValue = getNestedValue(input, condition.field);
258
+ return evaluateCondition(condition.operator, fieldValue, condition.value);
263
259
  });
264
260
  }
265
-
266
- /**
267
- * Get a nested value from an object using dot notation.
268
- */
269
- private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
270
- return path.split(".").reduce<unknown>((current, key) => {
271
- if (current && typeof current === "object" && key in current) {
272
- return (current as Record<string, unknown>)[key];
273
- }
274
- return undefined;
275
- }, obj);
276
- }
277
-
278
- /**
279
- * Evaluate a single condition.
280
- */
281
- private evaluateCondition(
282
- condition: RecipeCondition,
283
- fieldValue: unknown,
284
- ): boolean {
285
- const { operator, value } = condition;
286
-
287
- switch (operator) {
288
- case "eq":
289
- return fieldValue === value;
290
-
291
- case "neq":
292
- return fieldValue !== value;
293
-
294
- case "gt":
295
- return typeof fieldValue === "number" && typeof value === "number"
296
- ? fieldValue > value
297
- : false;
298
-
299
- case "gte":
300
- return typeof fieldValue === "number" && typeof value === "number"
301
- ? fieldValue >= value
302
- : false;
303
-
304
- case "lt":
305
- return typeof fieldValue === "number" && typeof value === "number"
306
- ? fieldValue < value
307
- : false;
308
-
309
- case "lte":
310
- return typeof fieldValue === "number" && typeof value === "number"
311
- ? fieldValue <= value
312
- : false;
313
-
314
- case "contains":
315
- if (typeof fieldValue === "string" && typeof value === "string") {
316
- return fieldValue.includes(value);
317
- }
318
- if (Array.isArray(fieldValue)) {
319
- return fieldValue.includes(value);
320
- }
321
- return false;
322
-
323
- case "matches":
324
- if (typeof fieldValue === "string" && typeof value === "string") {
325
- try {
326
- return new RE2(value).test(fieldValue);
327
- } catch {
328
- return false;
329
- }
330
- }
331
- return false;
332
-
333
- case "exists":
334
- return (
335
- fieldValue !== undefined && fieldValue !== null && fieldValue !== ""
336
- );
337
-
338
- case "notExists":
339
- return (
340
- fieldValue === undefined || fieldValue === null || fieldValue === ""
341
- );
342
-
343
- default:
344
- return false;
345
- }
346
- }
347
261
  }
348
262
 
349
263
  // ============================================================================
@@ -3,14 +3,10 @@
3
3
  * Enables dynamic workflow orchestration through recipes and plans.
4
4
  */
5
5
 
6
- import type {
7
- WorkflowKind,
8
- StepErrorStrategy,
9
- WorkflowStep,
10
- } from "../core/types";
6
+ import type { WorkflowKind, StepErrorStrategy, WorkflowStep } from '../core/types';
11
7
 
12
8
  // Re-export core types used in planning
13
- export type { WorkflowKind, StepErrorStrategy } from "../core/types";
9
+ export type { WorkflowKind, StepErrorStrategy } from '../core/types';
14
10
 
15
11
  // ============================================================================
16
12
  // Recipe Types
@@ -20,16 +16,16 @@ export type { WorkflowKind, StepErrorStrategy } from "../core/types";
20
16
  * Comparison operators for recipe conditions.
21
17
  */
22
18
  export type ConditionOperator =
23
- | "eq" // equals
24
- | "neq" // not equals
25
- | "gt" // greater than
26
- | "gte" // greater than or equal
27
- | "lt" // less than
28
- | "lte" // less than or equal
29
- | "contains" // string/array contains
30
- | "matches" // regex match
31
- | "exists" // field exists and is truthy
32
- | "notExists"; // field does not exist or is falsy
19
+ | 'eq' // equals
20
+ | 'neq' // not equals
21
+ | 'gt' // greater than
22
+ | 'gte' // greater than or equal
23
+ | 'lt' // less than
24
+ | 'lte' // less than or equal
25
+ | 'contains' // string/array contains
26
+ | 'matches' // regex match
27
+ | 'exists' // field exists and is truthy
28
+ | 'notExists'; // field does not exist or is falsy
33
29
 
34
30
  /**
35
31
  * A condition that determines when a recipe should be selected.
@@ -169,11 +165,11 @@ export interface Recipe {
169
165
  * Types of modifications that can be made to a plan.
170
166
  */
171
167
  export type PlanModificationType =
172
- | "add_step" // Add a new step
173
- | "remove_step" // Remove an existing step
174
- | "modify_step" // Modify step configuration
175
- | "reorder_steps" // Change step order
176
- | "set_default"; // Override a default parameter
168
+ | 'add_step' // Add a new step
169
+ | 'remove_step' // Remove an existing step
170
+ | 'modify_step' // Modify step configuration
171
+ | 'reorder_steps' // Change step order
172
+ | 'set_default'; // Override a default parameter
177
173
 
178
174
  /**
179
175
  * A modification to apply to a recipe when generating a plan.
@@ -306,7 +302,7 @@ export interface Plan {
306
302
  /**
307
303
  * Priority modes for planning.
308
304
  */
309
- export type PlanningPriority = "speed" | "quality" | "cost" | "balanced";
305
+ export type PlanningPriority = 'speed' | 'quality' | 'cost' | 'balanced';
310
306
 
311
307
  /**
312
308
  * Constraints that limit planning and execution.
@@ -407,7 +403,7 @@ export interface Planner {
407
403
  selectRecipe(
408
404
  workflowKind: WorkflowKind,
409
405
  input: Record<string, unknown>,
410
- context?: PlanningContext,
406
+ context?: PlanningContext
411
407
  ): Promise<RecipeSelectionResult>;
412
408
 
413
409
  /**
@@ -416,7 +412,7 @@ export interface Planner {
416
412
  generatePlan(
417
413
  recipe: Recipe,
418
414
  input: Record<string, unknown>,
419
- context?: PlanningContext,
415
+ context?: PlanningContext
420
416
  ): Promise<Plan>;
421
417
 
422
418
  /**
@@ -425,7 +421,7 @@ export interface Planner {
425
421
  plan(
426
422
  workflowKind: WorkflowKind,
427
423
  input: Record<string, unknown>,
428
- context?: PlanningContext,
424
+ context?: PlanningContext
429
425
  ): Promise<Plan>;
430
426
 
431
427
  /**
@@ -454,7 +450,7 @@ export interface RegisteredStepHandler<TInput = Record<string, unknown>> {
454
450
  description?: string;
455
451
 
456
452
  /** The handler function */
457
- handler: WorkflowStep<TInput>["handler"];
453
+ handler: WorkflowStep<TInput>['handler'];
458
454
 
459
455
  /** Tags for categorization */
460
456
  tags?: string[];
@@ -469,7 +465,7 @@ export interface StepHandlerRegistry {
469
465
  * Register a step handler.
470
466
  */
471
467
  register<TInput = Record<string, unknown>>(
472
- handler: RegisteredStepHandler<TInput>,
468
+ handler: RegisteredStepHandler<TInput>
473
469
  ): void;
474
470
 
475
471
  /**