@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,297 @@
1
+ # Security Audit: stepflow
2
+
3
+ **Date**: 2026-04-11T02:46:55.102Z
4
+ **Repository**: /srv/repos/stepflow
5
+ **Branch**: main
6
+ **Workflow Run**: 1376d4fe-4d4b-45f4-8663-781da416479e
7
+ **Session**: 20d039cc-df62-48da-a043-3b916b144149
8
+
9
+ ---
10
+
11
+ Now I have all the information needed to produce the full security audit report.
12
+
13
+ ## Security Audit: `@multiplier-labs/stepflow`
14
+
15
+ **Date:** 2026-04-11
16
+ **Repository:** `/srv/repos/stepflow`
17
+ **Branch:** `main` (HEAD `bd16961`)
18
+ **Tech Stack:** TypeScript / Node.js · better-sqlite3 · Kysely + PostgreSQL · Socket.IO (optional peer) · cron-parser · RE2
19
+
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ **Overall Risk Rating: Low**
25
+
26
+ The codebase is a well-structured TypeScript workflow-orchestration library. Parameterized queries, SSRF protection with DNS-rebinding prevention, HMAC-SHA256 webhook signing, and a mandatory authorization callback on the Socket.IO transport demonstrate deliberate defence-in-depth. No hardcoded secrets or committed credential files were found. No critical or high vulnerabilities were discovered.
27
+
28
+ However, three medium findings from the prior audit (2026-04-03) remain **unaddressed** in `HEAD`:
29
+
30
+ | Severity | Count |
31
+ |----------|-------|
32
+ | Critical | 0 |
33
+ | High | 0 |
34
+ | Medium | 3 |
35
+ | Low | 3 |
36
+ | Informational | 3 |
37
+
38
+ ---
39
+
40
+ ## Critical Findings
41
+
42
+ None.
43
+
44
+ ---
45
+
46
+ ## High Findings
47
+
48
+ None.
49
+
50
+ ---
51
+
52
+ ## Medium Findings
53
+
54
+ ### M1 — Non-Cryptographic ID Generation for Workflow Run and Step Identifiers
55
+
56
+ **File:** `src/utils/id.ts:17`
57
+
58
+ **Description:**
59
+ `generateId()` constructs record primary keys using `Math.random()`:
60
+
61
+ ```typescript
62
+ const randomPart = Math.random().toString(36).substring(2, 10);
63
+ ```
64
+
65
+ `Math.random()` is not a CSPRNG. V8's PRNG (`xorshift128+`) is seeded from system entropy at startup, but the full internal state can be recovered from a sequence of observed outputs. An attacker who can observe any generated IDs (e.g. via API responses, event payloads, or log lines) can reconstruct the PRNG state and predict past and future run IDs with high confidence.
66
+
67
+ These IDs are used as primary keys for all workflow runs (`src/core/orchestrator.ts`), steps (`src/storage/sqlite.ts`, `src/storage/postgres.ts`), schedules (`src/scheduler/cron.ts`), and events.
68
+
69
+ **Note:** This finding was identified in the 2026-04-03 audit and is still **unaddressed** in HEAD.
70
+
71
+ **Impact:** If a consumer treats run IDs as a capability token (e.g. embedding them in shareable URLs or using them to gate access), an attacker who can observe any prior run IDs can enumerate or predict run IDs they were never given, bypassing application-level access controls. Impact is entirely dependent on the consuming application's threat model, but the library cannot guarantee ID unpredictability for security-conscious consumers.
72
+
73
+ **Remediation:**
74
+ Replace `Math.random()` with a CSPRNG. Both options are available in Node.js 18+:
75
+
76
+ ```typescript
77
+ // Option A – Node.js crypto (if running in Node)
78
+ import { randomBytes } from 'node:crypto';
79
+ export function generateId(): string {
80
+ const timestamp = Date.now().toString(36);
81
+ const randomPart = randomBytes(6).toString('base64url');
82
+ return `${timestamp}${randomPart}`;
83
+ }
84
+
85
+ // Option B – Web Crypto (works in Node 18+ and browsers)
86
+ export function generateId(): string {
87
+ const timestamp = Date.now().toString(36);
88
+ const buf = new Uint8Array(6);
89
+ crypto.getRandomValues(buf);
90
+ const randomPart = Buffer.from(buf).toString('base64url');
91
+ return `${timestamp}${randomPart}`;
92
+ }
93
+ ```
94
+
95
+ ---
96
+
97
+ ### M2 — Missing `tableName` Validation in `PostgresSchedulePersistence`
98
+
99
+ **File:** `src/scheduler/postgres-persistence.ts:153`
100
+
101
+ **Description:**
102
+ `PostgresSchedulePersistence` validates the `schema` configuration option with a strict regex at construction time:
103
+
104
+ ```typescript
105
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.schema)) {
106
+ throw new Error(`Invalid schema name "${this.schema}"...`);
107
+ }
108
+ ```
109
+
110
+ However, `tableName` (line 153) receives **no equivalent validation**. The raw value is concatenated into `fullTableName` at line 234–235 and injected into DDL statements via `sql.table()` and `sql.ref()` at lines 257–299.
111
+
112
+ By contrast: `SQLiteSchedulePersistence` validates `tableName` with `^[a-zA-Z_][a-zA-Z0-9_]*$` at `src/scheduler/sqlite-persistence.ts:61`, and `PostgresStorageAdapter` validates `schema` at `src/storage/postgres.ts:228`. The omission in `postgres-persistence.ts` is an inconsistency in an otherwise consistent defensive pattern.
113
+
114
+ **Note:** This finding was identified in the 2026-04-03 audit and is still **unaddressed** in HEAD.
115
+
116
+ **Impact:** Kysely's `sql.table()` correctly double-quotes identifiers, so SQL injection is currently blocked by the ORM layer. However: (1) a consumer passing a malformed name receives an opaque database error instead of a clear early failure; (2) if Kysely's quoting is bypassed by any code path or future refactor, this becomes a direct DDL injection point; (3) the existing defensive pattern in the same file (for `schema`) creates a false sense of symmetry.
117
+
118
+ **Remediation:**
119
+ Add a regex guard immediately after line 153, matching the pattern already used for `schema`:
120
+
121
+ ```typescript
122
+ this.tableName = config.tableName ?? 'workflow_schedules';
123
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.tableName)) {
124
+ throw new Error(
125
+ `Invalid table name "${this.tableName}". Table name must start with a letter or ` +
126
+ `underscore, contain only alphanumeric characters and underscores, and be at most 63 characters.`
127
+ );
128
+ }
129
+ ```
130
+
131
+ ---
132
+
133
+ ### M3 — Stack Traces Included in `WorkflowError` Records Returned to Library Callers
134
+
135
+ **File:** `src/utils/errors.ts:28`, `src/utils/errors.ts:45`
136
+
137
+ **Description:**
138
+ `WorkflowEngineError.toRecord()` explicitly includes `stack: this.stack` in the serialized `WorkflowError`:
139
+
140
+ ```typescript
141
+ toRecord(): WorkflowError {
142
+ return {
143
+ code: this.code,
144
+ message: this.message,
145
+ stack: this.stack, // ← internal file paths exposed
146
+ details: this.details,
147
+ };
148
+ }
149
+ ```
150
+
151
+ Similarly, `WorkflowEngineError.fromError()` (line 45) includes `stack: error.stack` when wrapping a generic `Error`. These `WorkflowError` records appear in `RunResult.error` (returned by `engine.startRun()`, `engine.waitForRun()`) and in the `WorkflowContext` passed to step handlers.
152
+
153
+ The library does apply `sanitizeErrorForStorage()` (which strips `stack`) before writing errors to the database. However, the in-memory `RunResult` returned synchronously to callers retains the full stack. If a consuming application forwards `RunResult` to an HTTP response body (a common pattern), the stack trace is exposed to external clients.
154
+
155
+ **Impact:** Stack traces expose absolute file-system paths, internal module names, and line numbers. This information aids an attacker in mapping the server's deployment layout and understanding application logic, reducing the effort required to exploit other vulnerabilities.
156
+
157
+ **Remediation:**
158
+ Strip the `stack` field from `WorkflowError` before it is surfaced via the public `RunResult` return value. This can be done in the `WorkflowError` type (mark `stack` as internal), in `toRecord()`, or in `executeWorkflow`'s error handling:
159
+
160
+ ```typescript
161
+ toRecord(): WorkflowError {
162
+ return {
163
+ code: this.code,
164
+ message: this.message,
165
+ // stack intentionally omitted from public records
166
+ details: this.details,
167
+ };
168
+ }
169
+ ```
170
+
171
+ If consumers need stack traces for server-side diagnostics, they can be logged via the `Logger` interface rather than included in the result object.
172
+
173
+ ---
174
+
175
+ ## Low Findings
176
+
177
+ ### L1 — Blanket Block of All IPv6 Address Literals in SSRF Protection
178
+
179
+ **File:** `src/events/webhook.ts:493–496`
180
+
181
+ **Description:**
182
+ `isBlockedHost()` blocks **all** bracket-enclosed hostnames:
183
+
184
+ ```typescript
185
+ // Block all IPv6 address literals (wrapped in brackets by URL parser)
186
+ if (hostname.startsWith('[')) {
187
+ return true;
188
+ }
189
+ ```
190
+
191
+ This blocks the loopback address `[::1]` as intended, but also blocks every legitimate public IPv6 address (e.g. `[2001:4860:4860::8888]`). The downstream `isBlockedIp()` function used for DNS-resolved addresses does contain scoped IPv6 checks, creating an inconsistency.
192
+
193
+ **Note:** This finding was identified in the 2026-04-03 audit and is still **unaddressed** in HEAD.
194
+
195
+ **Impact:** Functional limitation, not a security vulnerability. Consumers cannot register webhook endpoints at public IPv6 addresses. As IPv6 adoption grows, this will increasingly block legitimate integrations.
196
+
197
+ **Remediation:** Replace the blanket bracket-check with targeted checks for RFC-reserved IPv6 ranges: `::1` (loopback), `fc00::/7` (ULA), `fe80::/10` (link-local), and `::ffff:0:0/96` (IPv4-mapped). A minimal implementation:
198
+
199
+ ```typescript
200
+ if (hostname.startsWith('[')) {
201
+ const bare = hostname.slice(1, -1).toLowerCase();
202
+ if (bare === '::1' || bare.startsWith('fe80:') || bare.startsWith('fc') || bare.startsWith('fd')) {
203
+ return true;
204
+ }
205
+ // Allow other public IPv6 addresses
206
+ return false;
207
+ }
208
+ ```
209
+
210
+ ---
211
+
212
+ ### L2 — `allowInsecureUrls: true` Provides No Runtime Warning
213
+
214
+ **File:** `src/events/webhook.ts:64–67`, `src/events/webhook.ts:145`
215
+
216
+ **Description:**
217
+ The `allowInsecureUrls` flag disables the HTTPS requirement and part of the SSRF scheme check. The JSDoc comment says "Set to true only in development environments," but there is no runtime enforcement or warning emitted via the configured logger when this flag is enabled.
218
+
219
+ **Impact:** A consumer who copies a development configuration to production silently downgrades all webhook deliveries to plain HTTP. Payload contents (which include workflow results, metadata, and run IDs) are then transmitted in cleartext and are susceptible to interception and replay attacks. The absence of a warning means this misconfiguration could persist undetected.
220
+
221
+ **Remediation:** Emit a warning at construction time when the flag is set:
222
+
223
+ ```typescript
224
+ if (this.allowInsecureUrls) {
225
+ this.logger.warn(
226
+ 'WebhookEventTransport: allowInsecureUrls is enabled. ' +
227
+ 'Webhook payloads will be sent over HTTP. Do not use this setting in production.'
228
+ );
229
+ }
230
+ ```
231
+
232
+ ---
233
+
234
+ ### L3 — No Per-Endpoint Rate Limiting on Outbound Webhook Dispatch
235
+
236
+ **File:** `src/events/webhook.ts:465–480`
237
+
238
+ **Description:**
239
+ The webhook transport implements a global concurrency cap (`maxConcurrentRequests`, default 50) and a FIFO queue, but no per-endpoint or per-second emission rate limit. A burst of workflow events (e.g. completing 200 steps in rapid succession) can cause hundreds of webhook deliveries to a single endpoint within seconds.
240
+
241
+ **Impact:** Could trigger rate-limiting responses (HTTP 429) or IP bans from external webhook receivers, disrupting event delivery for all consumers sharing that endpoint. Does not introduce an inbound attack surface but could constitute inadvertent abuse of third-party services.
242
+
243
+ **Remediation:** Add an optional per-endpoint `rateLimit` configuration (requests per second) with token-bucket logic, or document explicitly in the API that callers are responsible for selecting receivers capable of handling burst traffic.
244
+
245
+ ---
246
+
247
+ ## Secrets & Credentials Exposure
248
+
249
+ **Result: None found.**
250
+
251
+ Exhaustive `git grep` across all TypeScript, JavaScript, JSON, YAML, and environment file patterns for `password`, `secret`, `api_key`, `token`, and `private_key` found only:
252
+
253
+ | Location | Type | Assessment |
254
+ |---|---|---|
255
+ | `src/events/webhook.ts:22–23` | `secret?: string` type definition | Safe — no value |
256
+ | `src/events/webhook.ts:116` | JSDoc example: `process.env.WEBHOOK_SECRET` | Safe — env-var reference, not a literal |
257
+ | `src/planning/planner.ts`, `src/planning/types.ts` | `tokensPerStep`, `tokens` | LLM budget fields, not credentials |
258
+ | `dist/` build artifacts | Mirror of source | No embedded values |
259
+
260
+ No `.env` files exist in the repository. The full `git log` (`bd16961`…`0c78d90`) contains no commits that added credential files. No Base64-encoded or obfuscated secrets were detected.
261
+
262
+ ---
263
+
264
+ ## Recommendations
265
+
266
+ Ordered by risk impact:
267
+
268
+ 1. **[Medium — M1] Replace `Math.random()` with a CSPRNG in `src/utils/id.ts:17`.**
269
+ Use `crypto.getRandomValues()` or `crypto.randomBytes()`. This eliminates ID predictability for security-sensitive consumers and is a one-line change. Existing stored IDs are unaffected.
270
+
271
+ 2. **[Medium — M2] Add `tableName` regex validation in `PostgresSchedulePersistence` constructor.**
272
+ Add `if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.tableName))` at `src/scheduler/postgres-persistence.ts:153`, matching the pattern already applied to `schema` in the same constructor and to `tableName` in `SQLiteSchedulePersistence`. This closes a defensive inconsistency and provides a clean failure mode.
273
+
274
+ 3. **[Medium — M3] Strip `stack` from public `WorkflowError` records in `src/utils/errors.ts:28,45`.**
275
+ Remove `stack` from `toRecord()` and `fromError()` return values, or strip it at the `RunResult` construction site in the orchestrator. Log stack traces server-side via the `Logger` interface. This prevents internal paths leaking to callers who forward `RunResult` to HTTP responses.
276
+
277
+ 4. **[Low — L2] Add a `logger.warn()` in `WebhookEventTransport` constructor when `allowInsecureUrls: true`.**
278
+ Single-line addition that prevents silent HTTP downgrade in production. Can be gated on `process.env.NODE_ENV === 'production'` if strictness is undesirable during integration tests.
279
+
280
+ 5. **[Low — L1] Narrow IPv6 blocking to RFC-reserved ranges only in `isBlockedHost()`.**
281
+ Replace `hostname.startsWith('[')` at `webhook.ts:494` with targeted checks for `[::1]`, `[fe80:…]`, `[fc…]`, and `[fd…]` prefixes. Allows legitimate public IPv6 webhook endpoints without weakening SSRF protection.
282
+
283
+ 6. **[Low — L3] Document or implement outbound webhook rate limiting.**
284
+ Either add an optional `rateLimit` option to `WebhookEventTransportConfig` with token-bucket semantics, or add explicit API documentation warning consumers about burst dispatch behaviour.
285
+
286
+ 7. **[Informational] Add `@throws` JSDoc to `setupClientHandlers` documenting behavior when `authorize` returns `false`.**
287
+ Currently the silent `return` on denied subscriptions (socketio.ts:211, 235) is not documented in the public API. Clarify that a denied `workflow:subscribe` results in the socket not joining the room with no client-visible error, to prevent consumers from implementing broken "subscription failed" logic.
288
+
289
+ 8. **[Informational] Document CORS configuration requirements for the Socket.IO transport.**
290
+ `SocketIOEventTransport` accepts an already-configured `SocketIOServer` and does not set CORS headers. Add a README example showing a minimal secure CORS configuration (`origin: ['https://app.example.com']` rather than `origin: '*'`) to prevent consumers from defaulting to wildcard origins.
291
+
292
+ 9. **[Informational] Add `npm audit` to the CI pipeline.**
293
+ The production dependency surface is minimal (`cron-parser`, `re2`) but automated CVE scanning should run on every pull request. A GitHub Actions step of `npm audit --audit-level=high` would catch transitive vulnerabilities before they reach a release.
294
+
295
+ ---
296
+
297
+ *Audit performed: 2026-04-11. Branch: `main` (HEAD `bd16961`). No source files were modified during this audit. Three findings (M1, M2, L1) were also present in the prior audit dated 2026-04-03 and remain unaddressed.*
@@ -0,0 +1,15 @@
1
+ # Default owners for all files
2
+ * @Multiplier-Labs/engineering
3
+
4
+ # CI/CD and GitHub configuration — require admin review
5
+ .github/ @Multiplier-Labs/admin
6
+ package.json @Multiplier-Labs/admin
7
+ package-lock.json @Multiplier-Labs/admin
8
+
9
+ # Security-sensitive code
10
+ src/events/webhook.ts @Multiplier-Labs/admin
11
+ src/utils/id.ts @Multiplier-Labs/admin
12
+
13
+ # Persistence layer
14
+ src/storage/ @Multiplier-Labs/engineering
15
+ src/scheduler/ @Multiplier-Labs/engineering
@@ -0,0 +1,23 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: npm
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ day: monday
8
+ open-pull-requests-limit: 10
9
+ reviewers:
10
+ - Multiplier-Labs/engineering
11
+ labels:
12
+ - dependencies
13
+
14
+ - package-ecosystem: github-actions
15
+ directory: /
16
+ schedule:
17
+ interval: weekly
18
+ day: monday
19
+ open-pull-requests-limit: 5
20
+ reviewers:
21
+ - Multiplier-Labs/admin
22
+ labels:
23
+ - ci
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build-and-test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ matrix:
17
+ node-version: [20, 22]
18
+ steps:
19
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
20
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ cache: npm
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Audit dependencies
29
+ run: npm audit --audit-level=high
30
+
31
+ - name: Typecheck
32
+ run: npm run typecheck
33
+
34
+ - name: Build
35
+ run: npm run build
36
+
37
+ - name: Test
38
+ run: npm test
@@ -1,4 +1,4 @@
1
- name: Publish to npm
1
+ name: Publish to GitHub Packages
2
2
 
3
3
  on:
4
4
  release:
@@ -9,22 +9,34 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  permissions:
11
11
  contents: read
12
+ packages: write
12
13
  steps:
13
- - uses: actions/checkout@v4
14
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
14
15
 
15
- - uses: actions/setup-node@v4
16
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
16
17
  with:
17
18
  node-version: '20'
18
19
  registry-url: 'https://registry.npmjs.org'
19
- scope: '@multiplier-labs'
20
20
 
21
21
  - name: Install dependencies
22
22
  run: npm ci
23
23
 
24
+ - name: Audit dependencies
25
+ # Audit M3: a vulnerable dependency introduced between the last CI
26
+ # run and the release tag would otherwise ship to npm. Match the
27
+ # threshold used by ci.yml so the gate is consistent.
28
+ run: npm audit --audit-level=high
29
+
24
30
  - name: Build
25
31
  run: npm run build
26
32
 
33
+ - name: Typecheck
34
+ run: npm run typecheck
35
+
36
+ - name: Test
37
+ run: npm test
38
+
27
39
  - name: Publish
28
- run: npm publish --access public
40
+ run: npm publish
29
41
  env:
30
42
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,29 @@
1
+ name: Security
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ - cron: '0 8 * * 1' # Weekly on Monday at 08:00 UTC
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ audit:
16
+ name: Dependency Audit
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
20
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
21
+ with:
22
+ node-version: '20'
23
+ cache: npm
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Run npm audit
29
+ run: npm audit --audit-level=moderate
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - CI workflow for automated build, typecheck, and test on pull requests
12
+ - Security scanning workflow with weekly `npm audit` checks
13
+ - Dependabot configuration for automated dependency and GitHub Actions updates
14
+ - `CODEOWNERS` file requiring designated reviewers for security-sensitive paths
15
+ - `SECURITY.md` with vulnerability reporting instructions and disclosure policy
16
+ - Branch protection documentation in README
17
+
18
+ ### Changed
19
+ - Pinned GitHub Actions in `publish.yml` to full commit SHAs to prevent supply-chain attacks via tag hijacking
20
+
21
+ ## [0.2.6] - 2026-04-03
22
+
23
+ ### Added
24
+ - Initial public release
25
+ - Durable workflow execution engine with step orchestration
26
+ - SQLite and PostgreSQL storage backends
27
+ - In-memory storage for testing
28
+ - Cron-based scheduling with SQLite and PostgreSQL persistence
29
+ - Workflow completion triggers
30
+ - Socket.IO and webhook event transports with HMAC-SHA256 signing
31
+ - SSRF protection for webhook URLs with DNS-rebinding prevention
32
+ - Concurrency control with priority queues
33
+ - Rule-based planning system for dynamic workflow generation
34
+ - Full TypeScript support with type inference
package/README.md CHANGED
@@ -403,6 +403,30 @@ See the [API Reference](docs/stepflow-api-reference.md#planning-system) for full
403
403
 
404
404
  For the complete API reference including all classes, interfaces, and configuration options, see [docs/stepflow-api-reference.md](docs/stepflow-api-reference.md).
405
405
 
406
+ ## Security
407
+
408
+ This project takes security seriously. The repository enforces:
409
+
410
+ - **Branch protection** on `main` with required PR reviews and status checks
411
+ - **CODEOWNERS** requiring designated reviewer approval for security-sensitive paths
412
+ - **Pinned GitHub Actions** to full commit SHAs to prevent supply-chain attacks
413
+ - **Automated dependency scanning** via Dependabot with weekly update PRs
414
+ - **npm audit** in CI on every pull request and weekly scheduled runs
415
+ - **Secret scanning** and **push protection** to prevent credential leaks
416
+
417
+ For vulnerability reporting, see [SECURITY.md](SECURITY.md).
418
+
419
+ ## Contributing
420
+
421
+ 1. Fork the repository and create a feature branch from `main`
422
+ 2. Ensure your changes pass all checks: `npm run typecheck && npm run build && npm test`
423
+ 3. Open a pull request — CI will run automatically
424
+ 4. A CODEOWNERS reviewer will be assigned based on the files changed
425
+
426
+ ## Changelog
427
+
428
+ See [CHANGELOG.md](CHANGELOG.md) for a history of notable changes.
429
+
406
430
  ## License
407
431
 
408
432
  MIT
package/SECURITY.md ADDED
@@ -0,0 +1,40 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | 0.2.x | Yes |
8
+ | < 0.2 | No |
9
+
10
+ ## Reporting a Vulnerability
11
+
12
+ If you discover a security vulnerability in Stepflow, please report it responsibly.
13
+
14
+ **Do not open a public GitHub issue for security vulnerabilities.**
15
+
16
+ Instead, please email **security@multiplier-labs.com** with:
17
+
18
+ - A description of the vulnerability
19
+ - Steps to reproduce the issue
20
+ - The potential impact
21
+ - Any suggested remediation (optional)
22
+
23
+ We will acknowledge receipt within **48 hours** and aim to provide an initial assessment within **5 business days**.
24
+
25
+ ## Disclosure Policy
26
+
27
+ - We follow coordinated disclosure. Please allow us reasonable time to address the issue before public disclosure.
28
+ - Once a fix is released, we will publish a security advisory via GitHub Security Advisories.
29
+ - Contributors who report valid vulnerabilities will be credited in the advisory (unless they prefer anonymity).
30
+
31
+ ## Security Practices
32
+
33
+ This project follows these security practices:
34
+
35
+ - **Signed commits** are encouraged for all contributors
36
+ - **Branch protection** is enforced on `main` with required reviews and status checks
37
+ - **Dependency scanning** via Dependabot with automated PRs for security updates
38
+ - **npm audit** runs in CI on every pull request
39
+ - **Secret scanning** and **push protection** are enabled on the repository
40
+ - **CODEOWNERS** requires review from designated maintainers for security-sensitive code paths