@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.
- package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +17 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +60 -7
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
- package/dist/index.d.ts +15 -23
- package/dist/index.js +784 -547
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +322 -56
- package/dist/storage/index.js +4 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +5 -4
- package/package.json +12 -8
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +536 -522
- package/src/core/engine.ts +116 -235
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +57 -395
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +489 -235
- package/src/events/webhook.ts +355 -139
- package/src/index.ts +19 -19
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +45 -152
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +20 -106
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +149 -150
- package/src/scheduler/cron.ts +58 -69
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +89 -105
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +40 -22
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- package/src/storage/postgres-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +222 -937
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +330 -281
- package/src/storage/sqlite.ts +225 -170
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +6 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +20 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- package/dist/chunk-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- 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
|
|
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
|
|
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
|