@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
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';
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ evaluateCondition,
4
+ getNestedValue,
5
+ MAX_REGEX_PATTERN_LENGTH,
6
+ } from './evaluate-condition';
7
+
8
+ describe('getNestedValue', () => {
9
+ it('returns top-level values', () => {
10
+ expect(getNestedValue({ a: 1 }, 'a')).toBe(1);
11
+ });
12
+
13
+ it('returns nested values via dot notation', () => {
14
+ expect(getNestedValue({ a: { b: { c: 'deep' } } }, 'a.b.c')).toBe('deep');
15
+ });
16
+
17
+ it('returns undefined for missing paths', () => {
18
+ expect(getNestedValue({ a: 1 }, 'b')).toBeUndefined();
19
+ expect(getNestedValue({ a: { b: 1 } }, 'a.c')).toBeUndefined();
20
+ expect(getNestedValue({ a: { b: 1 } }, 'a.b.c')).toBeUndefined();
21
+ });
22
+
23
+ it('returns undefined when traversing through a non-object', () => {
24
+ expect(getNestedValue({ a: 5 }, 'a.b')).toBeUndefined();
25
+ expect(getNestedValue({ a: null }, 'a.b')).toBeUndefined();
26
+ });
27
+
28
+ it('preserves falsy values (0, false, empty string)', () => {
29
+ expect(getNestedValue({ a: 0 }, 'a')).toBe(0);
30
+ expect(getNestedValue({ a: false }, 'a')).toBe(false);
31
+ expect(getNestedValue({ a: '' }, 'a')).toBe('');
32
+ });
33
+ });
34
+
35
+ describe('evaluateCondition', () => {
36
+ describe('eq / neq', () => {
37
+ it('uses strict equality', () => {
38
+ expect(evaluateCondition('eq', 1, 1)).toBe(true);
39
+ expect(evaluateCondition('eq', 1, '1')).toBe(false);
40
+ expect(evaluateCondition('eq', 'a', 'a')).toBe(true);
41
+ expect(evaluateCondition('neq', 1, 2)).toBe(true);
42
+ expect(evaluateCondition('neq', 'a', 'a')).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe('numeric comparisons', () => {
47
+ it('gt / gte / lt / lte work for numbers', () => {
48
+ expect(evaluateCondition('gt', 5, 3)).toBe(true);
49
+ expect(evaluateCondition('gt', 3, 5)).toBe(false);
50
+ expect(evaluateCondition('gt', 5, 5)).toBe(false);
51
+ expect(evaluateCondition('gte', 5, 5)).toBe(true);
52
+ expect(evaluateCondition('lt', 3, 5)).toBe(true);
53
+ expect(evaluateCondition('lt', 5, 5)).toBe(false);
54
+ expect(evaluateCondition('lte', 5, 5)).toBe(true);
55
+ });
56
+
57
+ it('returns false when either side is not a number', () => {
58
+ expect(evaluateCondition('gt', '5', 3)).toBe(false);
59
+ expect(evaluateCondition('gt', 5, '3')).toBe(false);
60
+ expect(evaluateCondition('gte', null, 0)).toBe(false);
61
+ expect(evaluateCondition('lt', undefined, 1)).toBe(false);
62
+ expect(evaluateCondition('lte', true, 0)).toBe(false);
63
+ });
64
+ });
65
+
66
+ describe('contains', () => {
67
+ it('matches substrings in strings', () => {
68
+ expect(evaluateCondition('contains', 'hello world', 'world')).toBe(true);
69
+ expect(evaluateCondition('contains', 'hello world', 'xyz')).toBe(false);
70
+ });
71
+
72
+ it('matches elements in arrays', () => {
73
+ expect(evaluateCondition('contains', [1, 2, 3], 2)).toBe(true);
74
+ expect(evaluateCondition('contains', ['a', 'b'], 'c')).toBe(false);
75
+ });
76
+
77
+ it('returns false for unsupported types', () => {
78
+ expect(evaluateCondition('contains', 42, 4)).toBe(false);
79
+ expect(evaluateCondition('contains', { a: 1 }, 'a')).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe('matches', () => {
84
+ it('returns true for matching regex', () => {
85
+ expect(evaluateCondition('matches', 'hello world', '^hello')).toBe(true);
86
+ expect(evaluateCondition('matches', 'foo123', '\\d+')).toBe(true);
87
+ });
88
+
89
+ it('returns false for non-matching regex', () => {
90
+ expect(evaluateCondition('matches', 'hello', '^world')).toBe(false);
91
+ });
92
+
93
+ it('returns false on invalid regex (does not throw)', () => {
94
+ expect(evaluateCondition('matches', 'hello', '[invalid')).toBe(false);
95
+ });
96
+
97
+ it('returns false when either side is not a string', () => {
98
+ expect(evaluateCondition('matches', 123, '\\d+')).toBe(false);
99
+ expect(evaluateCondition('matches', 'abc', /abc/)).toBe(false);
100
+ });
101
+
102
+ it('rejects unsafe RE2-incompatible patterns rather than throwing', () => {
103
+ // Backreferences are unsupported in RE2 — verify we degrade gracefully.
104
+ expect(evaluateCondition('matches', 'aa', '(a)\\1')).toBe(false);
105
+ });
106
+
107
+ it('rejects patterns longer than MAX_REGEX_PATTERN_LENGTH without compiling', () => {
108
+ // A pattern equal to the limit should still compile and match.
109
+ const atLimit = 'a'.repeat(MAX_REGEX_PATTERN_LENGTH);
110
+ expect(evaluateCondition('matches', atLimit, atLimit)).toBe(true);
111
+
112
+ // One character over the limit should be rejected (returns false, no throw).
113
+ const overLimit = 'a'.repeat(MAX_REGEX_PATTERN_LENGTH + 1);
114
+ expect(evaluateCondition('matches', overLimit, overLimit)).toBe(false);
115
+ });
116
+ });
117
+
118
+ describe('exists / notExists', () => {
119
+ it('exists is true for any non-empty value', () => {
120
+ expect(evaluateCondition('exists', 'value', undefined)).toBe(true);
121
+ expect(evaluateCondition('exists', 0, undefined)).toBe(true);
122
+ expect(evaluateCondition('exists', false, undefined)).toBe(true);
123
+ expect(evaluateCondition('exists', [], undefined)).toBe(true);
124
+ });
125
+
126
+ it('exists is false for undefined, null, or empty string', () => {
127
+ expect(evaluateCondition('exists', undefined, undefined)).toBe(false);
128
+ expect(evaluateCondition('exists', null, undefined)).toBe(false);
129
+ expect(evaluateCondition('exists', '', undefined)).toBe(false);
130
+ });
131
+
132
+ it('notExists mirrors exists', () => {
133
+ expect(evaluateCondition('notExists', undefined, undefined)).toBe(true);
134
+ expect(evaluateCondition('notExists', null, undefined)).toBe(true);
135
+ expect(evaluateCondition('notExists', '', undefined)).toBe(true);
136
+ expect(evaluateCondition('notExists', 'value', undefined)).toBe(false);
137
+ expect(evaluateCondition('notExists', 0, undefined)).toBe(false);
138
+ });
139
+ });
140
+
141
+ describe('unknown operator', () => {
142
+ it('returns false for an unrecognized operator', () => {
143
+ // Cast through unknown to test the default branch.
144
+ expect(
145
+ evaluateCondition(
146
+ 'bogus' as unknown as Parameters<typeof evaluateCondition>[0],
147
+ 1,
148
+ 1
149
+ )
150
+ ).toBe(false);
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared condition evaluation helpers used by the planner and registry.
3
+ *
4
+ * Extracted to a single module so the planner's recipe scoring and the
5
+ * registry's `query({ matchConditions })` filter cannot drift apart.
6
+ */
7
+
8
+ import RE2 from 're2';
9
+ import type { ConditionOperator } from './types';
10
+
11
+ /**
12
+ * Maximum length (in characters) accepted for a `matches` regex pattern.
13
+ *
14
+ * Recipe conditions can come from user-supplied configuration; even with RE2's
15
+ * linear-time guarantee, very large patterns waste CPU/memory at compile time
16
+ * and inflate the per-evaluation cost. Cap the source length so a pathological
17
+ * recipe cannot hand the planner a megabyte of regex.
18
+ */
19
+ export const MAX_REGEX_PATTERN_LENGTH = 512;
20
+
21
+ /**
22
+ * Get a nested value from an object using dot notation.
23
+ */
24
+ export function getNestedValue(
25
+ obj: Record<string, unknown>,
26
+ path: string
27
+ ): unknown {
28
+ return path.split('.').reduce<unknown>((current, key) => {
29
+ if (current && typeof current === 'object' && key in current) {
30
+ return (current as Record<string, unknown>)[key];
31
+ }
32
+ return undefined;
33
+ }, obj);
34
+ }
35
+
36
+ /**
37
+ * Evaluate a single condition against a value.
38
+ *
39
+ * The `matches` operator uses RE2 (not the JS RegExp engine) to avoid
40
+ * catastrophic-backtracking DoS from untrusted recipe inputs.
41
+ */
42
+ export function evaluateCondition(
43
+ operator: ConditionOperator,
44
+ fieldValue: unknown,
45
+ conditionValue: unknown
46
+ ): boolean {
47
+ switch (operator) {
48
+ case 'eq':
49
+ return fieldValue === conditionValue;
50
+
51
+ case 'neq':
52
+ return fieldValue !== conditionValue;
53
+
54
+ case 'gt':
55
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
56
+ ? fieldValue > conditionValue
57
+ : false;
58
+
59
+ case 'gte':
60
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
61
+ ? fieldValue >= conditionValue
62
+ : false;
63
+
64
+ case 'lt':
65
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
66
+ ? fieldValue < conditionValue
67
+ : false;
68
+
69
+ case 'lte':
70
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
71
+ ? fieldValue <= conditionValue
72
+ : false;
73
+
74
+ case 'contains':
75
+ if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
76
+ return fieldValue.includes(conditionValue);
77
+ }
78
+ if (Array.isArray(fieldValue)) {
79
+ return fieldValue.includes(conditionValue);
80
+ }
81
+ return false;
82
+
83
+ case 'matches':
84
+ if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
85
+ if (conditionValue.length > MAX_REGEX_PATTERN_LENGTH) {
86
+ return false;
87
+ }
88
+ try {
89
+ return new RE2(conditionValue).test(fieldValue);
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ return false;
95
+
96
+ case 'exists':
97
+ return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
98
+
99
+ case 'notExists':
100
+ return fieldValue === undefined || fieldValue === null || fieldValue === '';
101
+
102
+ default:
103
+ return false;
104
+ }
105
+ }
@@ -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';