@multiplier-labs/stepflow 0.3.1 → 0.3.3
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/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +11 -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-4VOKMGP6.js} +23 -42
- package/dist/chunk-4VOKMGP6.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
- package/dist/chunk-GYPNJBDP.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
- package/dist/chunk-KHTUWLVZ.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
- package/dist/chunk-ML35PIHX.js.map +1 -0
- package/dist/events/index.d.ts +4 -4
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
- package/dist/index.d.ts +14 -12
- package/dist/index.js +40 -82
- 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 +13 -13
- package/dist/storage/index.js +2 -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 +2 -3
- package/package.json +7 -6
- package/src/core/engine.test.ts +466 -522
- package/src/core/engine.ts +42 -67
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator.ts +64 -83
- 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 +197 -235
- package/src/events/webhook.ts +44 -71
- package/src/index.ts +19 -19
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +66 -91
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +39 -43
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +141 -150
- package/src/scheduler/cron.ts +32 -67
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +71 -99
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +12 -19
- 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.ts +197 -393
- package/src/storage/sqlite.test.ts +154 -281
- package/src/storage/sqlite.ts +50 -85
- 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 +4 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/postgres-deps.ts +4 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- 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,208 @@
|
|
|
1
|
+
# Security Audit: stepflow
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-04-03T20:03:06.542Z
|
|
4
|
+
**Repository**: /srv/repos/stepflow
|
|
5
|
+
**Branch**: main
|
|
6
|
+
**Workflow Run**: 54523493-771d-44b8-b39f-03f2d6caa49e
|
|
7
|
+
**Session**: 53e402d9-8d47-49b4-9a27-03f2ae5ec912
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
I now have all the information needed to produce the report. Here is the security audit:
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Summary
|
|
16
|
+
|
|
17
|
+
**Overall Risk Rating: Low**
|
|
18
|
+
|
|
19
|
+
The Stepflow project is a TypeScript workflow orchestration library with a mature security posture. No critical or high vulnerabilities were identified. The codebase demonstrates intentional, defence-in-depth practices: parameterized queries, SSRF protection with DNS-rebinding prevention, HMAC-SHA256 webhook signing, and delegated authorization. Recent commits (`48e5f0a`, `d61858c`) show that prior SSRF and SQL-injection issues were already discovered and remediated.
|
|
20
|
+
|
|
21
|
+
| Severity | Count |
|
|
22
|
+
|----------|-------|
|
|
23
|
+
| Critical | 0 |
|
|
24
|
+
| High | 0 |
|
|
25
|
+
| Medium | 2 |
|
|
26
|
+
| Low | 3 |
|
|
27
|
+
| Informational | 4 |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Critical Findings
|
|
32
|
+
|
|
33
|
+
None.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## High Findings
|
|
38
|
+
|
|
39
|
+
None.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Medium Findings
|
|
44
|
+
|
|
45
|
+
### M1 — Missing Input Validation on `tableName` in `PostgresSchedulePersistence`
|
|
46
|
+
|
|
47
|
+
**File:** `src/scheduler/postgres-persistence.ts:153`
|
|
48
|
+
|
|
49
|
+
**Description:**
|
|
50
|
+
`PostgresSchedulePersistence` validates `schema` with a strict regex (`^[a-zA-Z_][a-zA-Z0-9_]{0,62}$`) but applies **no equivalent validation to `tableName`**. The raw `tableName` string is concatenated into `fullTableName` (`${this.schema}.${this.tableName}`) at line 235 and then passed to Kysely's `sql.table()` and `sql.ref()` at lines 257–296. While Kysely correctly double-quotes identifiers — preventing SQL injection — there is no upfront guard to reject nonsensical or adversarially crafted values.
|
|
51
|
+
|
|
52
|
+
Contrast: `SQLiteSchedulePersistence` (`src/scheduler/sqlite-persistence.ts:61`) validates its `tableName` with `^[a-zA-Z_][a-zA-Z0-9_]*$` and the Postgres *storage* adapter (`src/storage/postgres.ts:228`) validates its schema similarly. The omission in `postgres-persistence.ts` is an inconsistency in the defensive pattern.
|
|
53
|
+
|
|
54
|
+
**Impact:**
|
|
55
|
+
In the current implementation, `sql.table()` quoting prevents SQL injection. However, a consumer passing a malformed `tableName` (e.g. `"foo\x00bar"`, very long strings, or strings containing periods) will receive a confusing runtime database error rather than a clear upfront `Error`. If the Kysely dependency ever changes its quoting behaviour, this becomes a direct injection point.
|
|
56
|
+
|
|
57
|
+
**Remediation:**
|
|
58
|
+
Add the same regex guard used for `schema` immediately after line 153:
|
|
59
|
+
```typescript
|
|
60
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.tableName)) {
|
|
61
|
+
throw new Error(`Invalid table name "${this.tableName}".`);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### M2 — Non-Cryptographic ID Generation Used for Workflow Run Identifiers
|
|
68
|
+
|
|
69
|
+
**File:** `src/utils/id.ts:17`
|
|
70
|
+
|
|
71
|
+
**Description:**
|
|
72
|
+
`generateId()` constructs identifiers using `Date.now()` and `Math.random()`:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const randomPart = Math.random().toString(36).substring(2, 10);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`Math.random()` is not a CSPRNG. V8's implementation is seeded with a 64-bit value derived from the system clock at startup; an attacker with knowledge of the process start time can reconstruct the PRNG state and predict future IDs. These IDs are used as primary keys for workflow runs and steps (`src/core/orchestrator.ts`).
|
|
79
|
+
|
|
80
|
+
**Impact:**
|
|
81
|
+
If run IDs are used for access-control purposes by a consumer (e.g. "share a link containing the run ID" patterns), predictable IDs enable enumeration and unauthorized access. The library itself does not enforce any access control on run IDs, so impact depends entirely on the consuming application's threat model.
|
|
82
|
+
|
|
83
|
+
**Remediation:**
|
|
84
|
+
Replace `Math.random()` with `crypto.randomBytes` (Node) or `crypto.getRandomValues` (Web Crypto), both available in Node.js 18+:
|
|
85
|
+
```typescript
|
|
86
|
+
import { randomBytes } from 'node:crypto';
|
|
87
|
+
const randomPart = randomBytes(6).toString('base64url');
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Low Findings
|
|
93
|
+
|
|
94
|
+
### L1 — Blanket Block of All IPv6 Literals in SSRF Protection
|
|
95
|
+
|
|
96
|
+
**File:** `src/events/webhook.ts:493–496`
|
|
97
|
+
|
|
98
|
+
**Description:**
|
|
99
|
+
The `isBlockedHost` function blocks **all** IPv6 address literals, not just private/reserved ones:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Block all IPv6 address literals (wrapped in brackets by URL parser)
|
|
103
|
+
if (hostname.startsWith('[')) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This was added to prevent SSRF via `[::1]` and similar loopback bypasses (commit `48e5f0a`). While the intent is correct, the implementation also blocks legitimate public IPv6 targets such as `[2001:db8::1]`.
|
|
109
|
+
|
|
110
|
+
**Impact:**
|
|
111
|
+
Consumers cannot register webhook endpoints using public IPv6 addresses. This is a functional limitation, not a security vulnerability. As global IPv6 adoption grows, this will become increasingly restrictive.
|
|
112
|
+
|
|
113
|
+
**Remediation:**
|
|
114
|
+
Instead of blocking all bracket-wrapped addresses, parse the IPv6 address, expand it to full form, and check only against reserved ranges (loopback `::1`, ULA `fc00::/7`, link-local `fe80::/10`, documentation prefixes, etc.) using a library or the same range-check logic applied to IPv4.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### L2 — `allowInsecureUrls` Flag Lacks Production-Safety Warning
|
|
119
|
+
|
|
120
|
+
**File:** `src/events/webhook.ts:64–67`
|
|
121
|
+
|
|
122
|
+
**Description:**
|
|
123
|
+
The `allowInsecureUrls` option on `WebhookEventTransportConfig` relaxes the HTTPS requirement and bypasses URL scheme validation:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
/**
|
|
127
|
+
* Whether to allow non-HTTPS URLs (default: false).
|
|
128
|
+
* Set to true only in development environments.
|
|
129
|
+
*/
|
|
130
|
+
allowInsecureUrls?: boolean;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The comment mentions development use, but there is no runtime enforcement or warning when this flag is enabled in a context that looks like production (e.g. `NODE_ENV=production`).
|
|
134
|
+
|
|
135
|
+
**Impact:**
|
|
136
|
+
A consumer who copies a development configuration to production silently downgrades all webhooks to plain HTTP, exposing payloads (which may contain workflow results and metadata) to network interception, and disabling part of the SSRF protection.
|
|
137
|
+
|
|
138
|
+
**Remediation:**
|
|
139
|
+
Emit a logger warning at construction time when `allowInsecureUrls: true` is set, e.g.:
|
|
140
|
+
```typescript
|
|
141
|
+
if (this.allowInsecureUrls) {
|
|
142
|
+
this.logger.warn('allowInsecureUrls is enabled. Do not use this in production.');
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### L3 — No Rate Limiting on Outbound Webhook Dispatch
|
|
149
|
+
|
|
150
|
+
**File:** `src/events/webhook.ts:465–480`
|
|
151
|
+
|
|
152
|
+
**Description:**
|
|
153
|
+
The webhook transport implements concurrency limiting (default 50 concurrent requests) and a queue, but no per-endpoint rate limiting or per-second emission cap. A burst of workflow events can cause hundreds of webhook deliveries in a short window.
|
|
154
|
+
|
|
155
|
+
**Impact:**
|
|
156
|
+
Could trigger rate limiting or bans from external webhook receivers. Does not directly introduce an inbound attack surface (the library sends, not receives), but could cause unintended denial-of-service toward third-party services.
|
|
157
|
+
|
|
158
|
+
**Remediation:**
|
|
159
|
+
Add an optional `rateLimit` configuration (e.g. `maxRequestsPerSecond`) per endpoint or globally, with token-bucket or leaky-bucket logic.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Secrets & Credentials Exposure
|
|
164
|
+
|
|
165
|
+
**Result: None found.**
|
|
166
|
+
|
|
167
|
+
- `git grep` across all `*.ts`, `*.js`, `*.json`, `*.yaml`, `*.yml` files for `password`, `secret`, `api_key`, `token`, and `private_key` returned only:
|
|
168
|
+
- **`src/events/webhook.ts:22–23`** — `secret?: string` type definition for HMAC signing (no value)
|
|
169
|
+
- **`src/events/webhook.ts:116`** — JSDoc comment example: `secret: process.env.WEBHOOK_SECRET` (correctly references an environment variable, not a hardcoded value)
|
|
170
|
+
- **`src/planning/planner.ts`, `src/planning/types.ts`** — `tokensPerStep`, `tokens` — LLM token budget fields, not credentials
|
|
171
|
+
- **`dist/` build artefacts** — mirror of source, no embedded values
|
|
172
|
+
|
|
173
|
+
- No `.env` files exist in the repository at any path.
|
|
174
|
+
- `git log --all -p -- "*.env" "*.pem" "*.key"` returned no output — no credential files were ever committed to git history.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Recommendations
|
|
179
|
+
|
|
180
|
+
The following are ordered by risk impact:
|
|
181
|
+
|
|
182
|
+
1. **[Medium — M1] Validate `tableName` in `PostgresSchedulePersistence`.**
|
|
183
|
+
Add a regex guard (`^[a-zA-Z_][a-zA-Z0-9_]{0,62}$`) for `tableName` at `src/scheduler/postgres-persistence.ts:153`, matching the existing pattern in `sqlite-persistence.ts:61` and `storage/postgres.ts:228`. This closes the inconsistency and provides a clean failure mode.
|
|
184
|
+
|
|
185
|
+
2. **[Medium — M2] Replace `Math.random()` with a CSPRNG in ID generation.**
|
|
186
|
+
Replace `Math.random()` at `src/utils/id.ts:17` with `crypto.getRandomValues` or Node's `crypto.randomBytes`. The timestamp prefix can remain. This is low-effort and eliminates predictability of workflow run IDs if consumers use them for access decisions.
|
|
187
|
+
|
|
188
|
+
3. **[Low — L2] Add a runtime warning for `allowInsecureUrls: true`.**
|
|
189
|
+
Log a warning through the configured logger in `WebhookEventTransport`'s constructor when `allowInsecureUrls` is set. This is a single-line addition that prevents silent production misconfiguration.
|
|
190
|
+
|
|
191
|
+
4. **[Low — L1] Narrow IPv6 blocking to reserved ranges only.**
|
|
192
|
+
Replace the blanket `hostname.startsWith('[')` block in `isBlockedHost` (`webhook.ts:493`) with targeted checks for `[::1]`, `[fc00:...]`, `[fe80:...]`, and other RFC-reserved IPv6 prefixes, allowing legitimate public IPv6 webhook endpoints.
|
|
193
|
+
|
|
194
|
+
5. **[Low — L3] Document or implement outbound webhook rate limiting.**
|
|
195
|
+
Either add an optional `rateLimit` configuration to `WebhookEventTransport`, or explicitly document in JSDoc that consumers are responsible for choosing receivers that can handle burst traffic.
|
|
196
|
+
|
|
197
|
+
6. **[Informational] Add explicit authorization enforcement documentation.**
|
|
198
|
+
The Socket.IO transport (`src/events/socketio.ts`) requires consumers to provide an `authorize` callback. Add JSDoc `@throws` documentation and an example showing what happens when no `authorize` callback is supplied, to prevent consumers from accidentally running with open subscriptions.
|
|
199
|
+
|
|
200
|
+
7. **[Informational] Consider documenting CORS requirements for Socket.IO consumers.**
|
|
201
|
+
The library accepts an already-constructed `SocketIOServer` (`src/events/socketio.ts:45`) and does not configure CORS. Add a README section or JSDoc example showing the minimum recommended CORS configuration to avoid consumers defaulting to wildcard origins.
|
|
202
|
+
|
|
203
|
+
8. **[Informational] Run `npm audit` in CI.**
|
|
204
|
+
The production dependency surface is minimal (only `cron-parser` and `re2`), but automated vulnerability scanning should be enforced in the CI pipeline to catch transitive dependency CVEs early.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
*Audit performed: 2026-04-03. Branch: `main` (HEAD `4d9838a`). No source files were modified during this audit.*
|
|
@@ -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,14 +9,14 @@ 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
|
|
@@ -24,7 +24,13 @@ jobs:
|
|
|
24
24
|
- name: Build
|
|
25
25
|
run: npm run build
|
|
26
26
|
|
|
27
|
+
- name: Typecheck
|
|
28
|
+
run: npm run typecheck
|
|
29
|
+
|
|
30
|
+
- name: Test
|
|
31
|
+
run: npm test
|
|
32
|
+
|
|
27
33
|
- name: Publish
|
|
28
|
-
run: npm publish
|
|
34
|
+
run: npm publish
|
|
29
35
|
env:
|
|
30
36
|
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
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
generateId,
|
|
3
3
|
loadPostgresDeps,
|
|
4
4
|
sanitizeErrorForStorage
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ML35PIHX.js";
|
|
6
6
|
|
|
7
7
|
// src/storage/memory.ts
|
|
8
8
|
var MemoryStorageAdapter = class {
|
|
@@ -84,9 +84,7 @@ var MemoryStorageAdapter = class {
|
|
|
84
84
|
}
|
|
85
85
|
/** Retrieve all step records for a workflow run, ordered by start time ascending. */
|
|
86
86
|
async getStepsForRun(runId) {
|
|
87
|
-
return Array.from(this.steps.values()).filter((s) => s.runId === runId).sort(
|
|
88
|
-
(a, b) => (a.startedAt?.getTime() ?? 0) - (b.startedAt?.getTime() ?? 0)
|
|
89
|
-
);
|
|
87
|
+
return Array.from(this.steps.values()).filter((s) => s.runId === runId).sort((a, b) => (a.startedAt?.getTime() ?? 0) - (b.startedAt?.getTime() ?? 0));
|
|
90
88
|
}
|
|
91
89
|
// ============================================================================
|
|
92
90
|
// Event Operations
|
|
@@ -101,9 +99,7 @@ var MemoryStorageAdapter = class {
|
|
|
101
99
|
}
|
|
102
100
|
/** Retrieve events for a workflow run with optional filtering and pagination. */
|
|
103
101
|
async getEventsForRun(runId, options = {}) {
|
|
104
|
-
let items = Array.from(this.events.values()).filter(
|
|
105
|
-
(e) => e.runId === runId
|
|
106
|
-
);
|
|
102
|
+
let items = Array.from(this.events.values()).filter((e) => e.runId === runId);
|
|
107
103
|
if (options.stepKey) {
|
|
108
104
|
items = items.filter((e) => e.stepKey === options.stepKey);
|
|
109
105
|
}
|
|
@@ -173,6 +169,7 @@ CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
|
173
169
|
input_json TEXT NOT NULL,
|
|
174
170
|
metadata_json TEXT NOT NULL,
|
|
175
171
|
context_json TEXT NOT NULL,
|
|
172
|
+
completed_steps_json TEXT,
|
|
176
173
|
error_json TEXT,
|
|
177
174
|
created_at TEXT NOT NULL,
|
|
178
175
|
started_at TEXT,
|
|
@@ -243,8 +240,8 @@ var SQLiteStorageAdapter = class {
|
|
|
243
240
|
prepareStatements() {
|
|
244
241
|
this.stmts = {
|
|
245
242
|
insertRun: this.db.prepare(`
|
|
246
|
-
INSERT INTO workflow_runs (id, kind, status, parent_run_id, input_json, metadata_json, context_json, error_json, created_at, started_at, finished_at)
|
|
247
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
243
|
+
INSERT INTO workflow_runs (id, kind, status, parent_run_id, input_json, metadata_json, context_json, completed_steps_json, error_json, created_at, started_at, finished_at)
|
|
244
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
248
245
|
`),
|
|
249
246
|
getRun: this.db.prepare(`
|
|
250
247
|
SELECT * FROM workflow_runs WHERE id = ?
|
|
@@ -253,6 +250,7 @@ var SQLiteStorageAdapter = class {
|
|
|
253
250
|
UPDATE workflow_runs
|
|
254
251
|
SET status = COALESCE(?, status),
|
|
255
252
|
context_json = COALESCE(?, context_json),
|
|
253
|
+
completed_steps_json = COALESCE(?, completed_steps_json),
|
|
256
254
|
error_json = COALESCE(?, error_json),
|
|
257
255
|
started_at = COALESCE(?, started_at),
|
|
258
256
|
finished_at = COALESCE(?, finished_at)
|
|
@@ -344,6 +342,7 @@ var SQLiteStorageAdapter = class {
|
|
|
344
342
|
JSON.stringify(run.input),
|
|
345
343
|
JSON.stringify(run.metadata),
|
|
346
344
|
JSON.stringify(run.context),
|
|
345
|
+
run.completedSteps ? JSON.stringify(run.completedSteps) : null,
|
|
347
346
|
run.error ? JSON.stringify(sanitizeErrorForStorage(run.error)) : null,
|
|
348
347
|
createdAt.toISOString(),
|
|
349
348
|
run.startedAt?.toISOString() ?? null,
|
|
@@ -363,6 +362,7 @@ var SQLiteStorageAdapter = class {
|
|
|
363
362
|
this.stmts.updateRun.run(
|
|
364
363
|
updates.status ?? null,
|
|
365
364
|
updates.context ? JSON.stringify(updates.context) : null,
|
|
365
|
+
updates.completedSteps ? JSON.stringify(updates.completedSteps) : null,
|
|
366
366
|
updates.error ? JSON.stringify(sanitizeErrorForStorage(updates.error)) : null,
|
|
367
367
|
updates.startedAt?.toISOString() ?? null,
|
|
368
368
|
updates.finishedAt?.toISOString() ?? null,
|
|
@@ -550,6 +550,7 @@ var SQLiteStorageAdapter = class {
|
|
|
550
550
|
input: JSON.parse(row.input_json),
|
|
551
551
|
metadata: JSON.parse(row.metadata_json),
|
|
552
552
|
context: JSON.parse(row.context_json),
|
|
553
|
+
completedSteps: row.completed_steps_json ? JSON.parse(row.completed_steps_json) : void 0,
|
|
553
554
|
error: row.error_json ? JSON.parse(row.error_json) : void 0,
|
|
554
555
|
createdAt: new Date(row.created_at),
|
|
555
556
|
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
@@ -663,9 +664,7 @@ var PostgresStorageAdapter = class {
|
|
|
663
664
|
this.pool = this.config.pool;
|
|
664
665
|
this.ownsPool = false;
|
|
665
666
|
} else if (this.config.connectionString) {
|
|
666
|
-
this.pool = new pgModule.Pool({
|
|
667
|
-
connectionString: this.config.connectionString
|
|
668
|
-
});
|
|
667
|
+
this.pool = new pgModule.Pool({ connectionString: this.config.connectionString });
|
|
669
668
|
this.ownsPool = true;
|
|
670
669
|
} else if (this.config.poolConfig) {
|
|
671
670
|
this.pool = new pgModule.Pool(this.config.poolConfig);
|
|
@@ -700,9 +699,7 @@ var PostgresStorageAdapter = class {
|
|
|
700
699
|
*/
|
|
701
700
|
async createTables() {
|
|
702
701
|
if (this.schema !== "public") {
|
|
703
|
-
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.ref(this.schema)}`.execute(
|
|
704
|
-
this.db
|
|
705
|
-
);
|
|
702
|
+
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.ref(this.schema)}`.execute(this.db);
|
|
706
703
|
}
|
|
707
704
|
await sql`
|
|
708
705
|
CREATE TABLE IF NOT EXISTS ${sql.table(`${this.schema}.runs`)} (
|
|
@@ -890,9 +887,7 @@ var PostgresStorageAdapter = class {
|
|
|
890
887
|
updateData.output_json = JSON.stringify(updates.output);
|
|
891
888
|
}
|
|
892
889
|
if (updates.error !== void 0) {
|
|
893
|
-
updateData.error_json = JSON.stringify(
|
|
894
|
-
sanitizeErrorForStorage(updates.error)
|
|
895
|
-
);
|
|
890
|
+
updateData.error_json = JSON.stringify(sanitizeErrorForStorage(updates.error));
|
|
896
891
|
}
|
|
897
892
|
if (updates.startedAt !== void 0) {
|
|
898
893
|
updateData.started_at = updates.startedAt;
|
|
@@ -987,9 +982,7 @@ var PostgresStorageAdapter = class {
|
|
|
987
982
|
updateData.result_json = JSON.stringify(updates.result);
|
|
988
983
|
}
|
|
989
984
|
if (updates.error !== void 0) {
|
|
990
|
-
updateData.error_json = JSON.stringify(
|
|
991
|
-
sanitizeErrorForStorage(updates.error)
|
|
992
|
-
);
|
|
985
|
+
updateData.error_json = JSON.stringify(sanitizeErrorForStorage(updates.error));
|
|
993
986
|
}
|
|
994
987
|
if (updates.finishedAt !== void 0) {
|
|
995
988
|
updateData.finished_at = updates.finishedAt;
|
|
@@ -1362,18 +1355,11 @@ var PostgresTransactionAdapter = class {
|
|
|
1362
1355
|
async updateRun(runId, updates) {
|
|
1363
1356
|
const updateData = {};
|
|
1364
1357
|
if (updates.status !== void 0) updateData.status = updates.status;
|
|
1365
|
-
if (updates.context !== void 0)
|
|
1366
|
-
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
if (updates.
|
|
1370
|
-
updateData.error_json = JSON.stringify(
|
|
1371
|
-
sanitizeErrorForStorage(updates.error)
|
|
1372
|
-
);
|
|
1373
|
-
if (updates.startedAt !== void 0)
|
|
1374
|
-
updateData.started_at = updates.startedAt;
|
|
1375
|
-
if (updates.finishedAt !== void 0)
|
|
1376
|
-
updateData.finished_at = updates.finishedAt;
|
|
1358
|
+
if (updates.context !== void 0) updateData.context_json = JSON.stringify(updates.context);
|
|
1359
|
+
if ("output" in updates && updates.output !== void 0) updateData.output_json = JSON.stringify(updates.output);
|
|
1360
|
+
if (updates.error !== void 0) updateData.error_json = JSON.stringify(sanitizeErrorForStorage(updates.error));
|
|
1361
|
+
if (updates.startedAt !== void 0) updateData.started_at = updates.startedAt;
|
|
1362
|
+
if (updates.finishedAt !== void 0) updateData.finished_at = updates.finishedAt;
|
|
1377
1363
|
if (Object.keys(updateData).length > 0) {
|
|
1378
1364
|
await this.qb.updateTable("runs").set(updateData).where("id", "=", runId).execute();
|
|
1379
1365
|
}
|
|
@@ -1424,14 +1410,9 @@ var PostgresTransactionAdapter = class {
|
|
|
1424
1410
|
const updateData = {};
|
|
1425
1411
|
if (updates.status !== void 0) updateData.status = updates.status;
|
|
1426
1412
|
if (updates.attempt !== void 0) updateData.attempt = updates.attempt;
|
|
1427
|
-
if (updates.result !== void 0)
|
|
1428
|
-
|
|
1429
|
-
if (updates.
|
|
1430
|
-
updateData.error_json = JSON.stringify(
|
|
1431
|
-
sanitizeErrorForStorage(updates.error)
|
|
1432
|
-
);
|
|
1433
|
-
if (updates.finishedAt !== void 0)
|
|
1434
|
-
updateData.finished_at = updates.finishedAt;
|
|
1413
|
+
if (updates.result !== void 0) updateData.result_json = JSON.stringify(updates.result);
|
|
1414
|
+
if (updates.error !== void 0) updateData.error_json = JSON.stringify(sanitizeErrorForStorage(updates.error));
|
|
1415
|
+
if (updates.finishedAt !== void 0) updateData.finished_at = updates.finishedAt;
|
|
1435
1416
|
if (Object.keys(updateData).length > 0) {
|
|
1436
1417
|
await this.qb.updateTable("workflow_run_steps").set(updateData).where("id", "=", stepId).execute();
|
|
1437
1418
|
}
|
|
@@ -1509,4 +1490,4 @@ export {
|
|
|
1509
1490
|
SQLiteStorageAdapter,
|
|
1510
1491
|
PostgresStorageAdapter
|
|
1511
1492
|
};
|
|
1512
|
-
//# sourceMappingURL=chunk-
|
|
1493
|
+
//# sourceMappingURL=chunk-4VOKMGP6.js.map
|