@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
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Regression tests for PostgresStorageAdapter migration error handling.
3
+ * See audit 2026-04-27 finding C1: schema migrations must surface failures
4
+ * instead of silently swallowing them.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { PostgresStorageAdapter } from './postgres';
9
+
10
+ interface QueryHandler {
11
+ (text: string, params: unknown[]): Promise<{ command: string; rowCount: number; rows: unknown[] }>;
12
+ }
13
+
14
+ /**
15
+ * Build the smallest pg.Pool-like object Kysely's PostgresDialect needs:
16
+ * `connect()` returning a client with `query()` and `release()`, plus `end()`.
17
+ */
18
+ function buildFakePool(handler: QueryHandler) {
19
+ const client = {
20
+ query: (text: string, params: unknown[] = []) => handler(text, params),
21
+ release: () => {},
22
+ };
23
+ const pool = {
24
+ connect: async () => client,
25
+ end: async () => {},
26
+ on: () => pool,
27
+ off: () => pool,
28
+ };
29
+ return pool;
30
+ }
31
+
32
+ describe('PostgresStorageAdapter — migration error propagation', () => {
33
+ it('rethrows a non-benign migration error with a clear message', async () => {
34
+ const handler: QueryHandler = async (text) => {
35
+ if (/ALTER\s+TABLE/i.test(text)) {
36
+ // Simulate a non-benign migration failure (e.g., permission denied).
37
+ const err: Error & { code?: string } = new Error('permission denied for table runs');
38
+ err.code = '42501'; // insufficient_privilege — not in benign list
39
+ throw err;
40
+ }
41
+ return { command: 'CREATE', rowCount: 0, rows: [] };
42
+ };
43
+
44
+ const adapter = new PostgresStorageAdapter({
45
+ pool: buildFakePool(handler) as unknown as import('pg').Pool,
46
+ });
47
+
48
+ await expect(adapter.initialize()).rejects.toThrow(/Schema migration failed/);
49
+ await expect(adapter.initialize()).rejects.toThrow(/permission denied/);
50
+ });
51
+
52
+ it('tolerates benign duplicate-column errors during idempotent re-runs', async () => {
53
+ const handler: QueryHandler = async (text) => {
54
+ if (/ALTER\s+TABLE/i.test(text)) {
55
+ const err: Error & { code?: string } = new Error('column "priority" of relation "runs" already exists');
56
+ err.code = '42701'; // duplicate_column — benign
57
+ throw err;
58
+ }
59
+ return { command: 'CREATE', rowCount: 0, rows: [] };
60
+ };
61
+
62
+ const adapter = new PostgresStorageAdapter({
63
+ pool: buildFakePool(handler) as unknown as import('pg').Pool,
64
+ });
65
+
66
+ await expect(adapter.initialize()).resolves.toBeUndefined();
67
+ await adapter.close();
68
+ });
69
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Regression test for PostgresTransactionAdapter.listRuns pagination total.
3
+ * See audit 2026-04-27 finding C2: when listRuns is called inside a
4
+ * transaction, the returned `total` must reflect the rows visible to that
5
+ * transaction (via COUNT(*) on the same connection), not the size of the
6
+ * returned page.
7
+ */
8
+
9
+ import { describe, it, expect, beforeAll } from 'vitest';
10
+ import { PostgresStorageAdapter, PostgresTransactionAdapter } from './postgres';
11
+
12
+ beforeAll(async () => {
13
+ // PostgresTransactionAdapter.listRuns uses the module-level `sql` template
14
+ // tag from postgres.ts, which is only populated when a PostgresStorageAdapter
15
+ // is initialized. Initialize one with a no-op fake pool so the module state
16
+ // is ready for the direct unit test below.
17
+ const noopPool = {
18
+ connect: async () => ({
19
+ query: async () => ({ command: 'SELECT', rowCount: 0, rows: [] }),
20
+ release: () => {},
21
+ }),
22
+ end: async () => {},
23
+ on: () => noopPool,
24
+ off: () => noopPool,
25
+ };
26
+ const bootstrap = new PostgresStorageAdapter({
27
+ pool: noopPool as unknown as import('pg').Pool,
28
+ });
29
+ await bootstrap.initialize();
30
+ await bootstrap.close();
31
+ });
32
+
33
+ /**
34
+ * Build a Kysely-transaction-shaped stub backed by an in-memory row list.
35
+ * It distinguishes the COUNT(*) query (started with `selectFrom().select()`)
36
+ * from the data query (started with `selectFrom().selectAll()`) and replies
37
+ * with the right shape for each.
38
+ */
39
+ function buildFakeTrx<T extends Record<string, unknown>>(rows: T[]) {
40
+ function buildCountChain() {
41
+ const chain: any = {};
42
+ chain.where = () => chain;
43
+ chain.executeTakeFirst = async () => ({ count: rows.length });
44
+ return chain;
45
+ }
46
+ function buildDataChain() {
47
+ const chain: any = {};
48
+ let limit: number | undefined;
49
+ let offset = 0;
50
+ chain.where = () => chain;
51
+ chain.orderBy = () => chain;
52
+ chain.limit = (n: number) => { limit = n; return chain; };
53
+ chain.offset = (n: number) => { offset = n; return chain; };
54
+ chain.execute = async () => {
55
+ const sliced = rows.slice(offset, limit !== undefined ? offset + limit : undefined);
56
+ return sliced;
57
+ };
58
+ return chain;
59
+ }
60
+ const qb = {
61
+ selectFrom: () => ({
62
+ select: () => buildCountChain(),
63
+ selectAll: () => buildDataChain(),
64
+ }),
65
+ };
66
+ return { withSchema: () => qb } as any;
67
+ }
68
+
69
+ describe('PostgresTransactionAdapter — listRuns pagination total', () => {
70
+ it('reports total from COUNT(*) on the same transaction, not the page size', async () => {
71
+ // Simulate 7 rows visible in the transaction; ask for a page of 3.
72
+ const fakeRow = {
73
+ id: 'r', kind: 'k', status: 'queued', parent_run_id: null,
74
+ input_json: '{}', metadata_json: '{}', context_json: '{}',
75
+ output_json: null, error_json: null, priority: 0, timeout_ms: null,
76
+ created_at: new Date(), started_at: null, finished_at: null,
77
+ };
78
+ const rows = Array.from({ length: 7 }, (_, i) => ({ ...fakeRow, id: `r${i}` }));
79
+ const trx = buildFakeTrx(rows);
80
+
81
+ const adapter = new PostgresTransactionAdapter(trx, 'public');
82
+ const result = await adapter.listRuns({ limit: 3 });
83
+
84
+ // Page contains only 3 rows...
85
+ expect(result.items).toHaveLength(3);
86
+ // ...but total reflects all 7 rows visible in the transaction.
87
+ // Pre-fix this returned 3 (rows.length).
88
+ expect(result.total).toBe(7);
89
+ });
90
+
91
+ it('total reflects all created rows even when limit exceeds page size', async () => {
92
+ const fakeRow = {
93
+ id: 'r', kind: 'k', status: 'queued', parent_run_id: null,
94
+ input_json: '{}', metadata_json: '{}', context_json: '{}',
95
+ output_json: null, error_json: null, priority: 0, timeout_ms: null,
96
+ created_at: new Date(), started_at: null, finished_at: null,
97
+ };
98
+ const rows = Array.from({ length: 5 }, (_, i) => ({ ...fakeRow, id: `r${i}` }));
99
+ const trx = buildFakeTrx(rows);
100
+
101
+ const adapter = new PostgresTransactionAdapter(trx, 'public');
102
+ const result = await adapter.listRuns({ limit: 100 });
103
+
104
+ expect(result.items).toHaveLength(5);
105
+ expect(result.total).toBe(5);
106
+ });
107
+ });