forgedock 1.0.21 → 1.1.1
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/README.md +227 -174
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +386 -0
- package/bin/engine/phases.mjs +148 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +27 -0
- package/bin/engine-cli.mjs +50 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +693 -1393
- package/bin/init-detect.mjs +95 -0
- package/bin/journey.mjs +783 -0
- package/bin/runner.mjs +847 -0
- package/bin/settings-hook.mjs +112 -0
- package/bin/tui.mjs +6 -3
- package/commands/autopilot.md +4 -4
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +11 -5
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +2 -1
- package/commands/issue.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +346 -34
- package/commands/quality-gate.md +168 -80
- package/commands/review-pr-agents.md +69 -45
- package/commands/review-pr.md +54 -6
- package/commands/scope.md +11 -3
- package/commands/security-audit.md +24 -16
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +47 -3
- package/commands/work-on/build/context.md +36 -5
- package/commands/work-on/build/implement.md +16 -4
- package/commands/work-on/build/validate.md +54 -8
- package/commands/work-on/build.md +27 -9
- package/commands/work-on/close.md +131 -3
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on/review.md +24 -0
- package/commands/work-on-monolithic.md +1 -0
- package/commands/work-on.md +271 -11
- package/docs/BRAND.md +6 -2
- package/docs/CONFIG.md +39 -34
- package/docs/site/.vitepress/config.mjs +85 -0
- package/docs/site/annotations-explained.md +55 -0
- package/docs/site/command-learning-path.md +104 -0
- package/docs/site/command-reference.md +3 -0
- package/docs/site/getting-started.md +42 -13
- package/docs/site/index.md +1 -1
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/examples/forgedock-demo/README.md +113 -0
- package/examples/forgedock-demo/bootstrap.sh +125 -0
- package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
- package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
- package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
- package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
- package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
- package/examples/forgedock-demo/labels.json +21 -0
- package/examples/forgedock-demo/package.json +16 -0
- package/examples/forgedock-demo/scripts/smoke.js +61 -0
- package/examples/forgedock-demo/src/auth.js +28 -0
- package/examples/forgedock-demo/src/db.js +66 -0
- package/examples/forgedock-demo/src/routes/notes.js +64 -0
- package/examples/forgedock-demo/src/server.js +79 -0
- package/package.json +13 -3
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- package/templates/forge.yaml.minimal +44 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): add safe filtering for GET /notes"
|
|
3
|
+
labels: ["feature", "security", "priority:P2"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`GET /notes?where=...` forwards the raw `where` string straight into
|
|
9
|
+
`db.query()`, which evaluates it as a JavaScript expression. That is an
|
|
10
|
+
injection hole (a caller can pass `?where=true||secret` to leak secret notes).
|
|
11
|
+
We want a **safe** way to filter notes instead.
|
|
12
|
+
|
|
13
|
+
## Proposed Solution
|
|
14
|
+
|
|
15
|
+
Replace the free-form `?where=` evaluation with explicit, allow-listed query
|
|
16
|
+
params:
|
|
17
|
+
|
|
18
|
+
- `?owner=alice` — exact match on `owner`
|
|
19
|
+
- `?secret=false` — match on the `secret` boolean
|
|
20
|
+
|
|
21
|
+
Build the filter from these typed params in code — never by evaluating a string.
|
|
22
|
+
|
|
23
|
+
## Affected Files
|
|
24
|
+
|
|
25
|
+
1. `src/routes/notes.js` — `listNotes()` should read `owner` / `secret` params.
|
|
26
|
+
2. `src/db.js` — replace `query(whereClause)` with a structured filter helper
|
|
27
|
+
(e.g. `filter({ owner, secret })`) that does not evaluate strings.
|
|
28
|
+
|
|
29
|
+
## Acceptance Criteria
|
|
30
|
+
|
|
31
|
+
- [ ] `GET /notes?owner=alice` returns only alice's notes.
|
|
32
|
+
- [ ] `GET /notes?secret=false` returns only non-secret notes.
|
|
33
|
+
- [ ] The string-evaluating `db.query()` path is removed.
|
|
34
|
+
- [ ] `npm run smoke` still passes.
|
|
35
|
+
|
|
36
|
+
> This issue exercises the **investigation + architecture** phases — the fix
|
|
37
|
+
> touches two files and removes an unsafe code path.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "refactor(server): extract the router into its own module"
|
|
3
|
+
labels: ["refactor", "priority:P3"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`src/server.js` mixes three concerns in one file: the route table, request
|
|
9
|
+
body parsing, and the HTTP server wiring. As more routes are added this file
|
|
10
|
+
will become hard to read and test.
|
|
11
|
+
|
|
12
|
+
## Proposed Solution
|
|
13
|
+
|
|
14
|
+
Extract the routing logic into a dedicated `src/router.js` module that exports
|
|
15
|
+
a `route(req, res)` function (or a small `Router` object). `server.js` should
|
|
16
|
+
shrink to: create the HTTP server, delegate to the router, and listen.
|
|
17
|
+
|
|
18
|
+
This is a pure refactor — no behavior changes.
|
|
19
|
+
|
|
20
|
+
## Affected Files
|
|
21
|
+
|
|
22
|
+
1. `src/server.js` — remove the inline `TABLE` + matching logic.
|
|
23
|
+
2. `src/router.js` (NEW) — owns the route table and dispatch.
|
|
24
|
+
|
|
25
|
+
## Acceptance Criteria
|
|
26
|
+
|
|
27
|
+
- [ ] Route table and dispatch live in `src/router.js`.
|
|
28
|
+
- [ ] `server.js` only creates the server and listens.
|
|
29
|
+
- [ ] All endpoints behave exactly as before.
|
|
30
|
+
- [ ] `npm run smoke` still passes.
|
|
31
|
+
|
|
32
|
+
> A clean refactor with zero behavior change — a good way to see how the review
|
|
33
|
+
> agents confirm "no regression".
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "perf(db): make findById O(1) instead of a linear scan"
|
|
3
|
+
labels: ["performance", "priority:P3"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`db.findById(id)` and `db.remove(id)` both do a linear `Array.find` /
|
|
9
|
+
`Array.filter` over every note on each call. With the seed data that is fine,
|
|
10
|
+
but it is O(n) per lookup and will not scale as the note count grows.
|
|
11
|
+
|
|
12
|
+
## Investigation Hint
|
|
13
|
+
|
|
14
|
+
Before optimizing, confirm where the linear scans actually are and whether an
|
|
15
|
+
index can stay consistent across `insert`, `remove`, and `reset`. The point of
|
|
16
|
+
this issue is to show ForgeDock's **investigation phase** tracing the real hot
|
|
17
|
+
path rather than guessing.
|
|
18
|
+
|
|
19
|
+
## Proposed Solution
|
|
20
|
+
|
|
21
|
+
Maintain an `id -> note` index (a `Map`) alongside the array so `findById` is
|
|
22
|
+
O(1). Keep the index in sync in `insert`, `remove`, and `reset`.
|
|
23
|
+
|
|
24
|
+
## Affected Files
|
|
25
|
+
|
|
26
|
+
1. `src/db.js` — add and maintain a `Map` index; use it in `findById`.
|
|
27
|
+
|
|
28
|
+
## Acceptance Criteria
|
|
29
|
+
|
|
30
|
+
- [ ] `findById` uses an O(1) Map lookup.
|
|
31
|
+
- [ ] The index stays consistent after `insert`, `remove`, and `reset`.
|
|
32
|
+
- [ ] `npm run smoke` still passes.
|
|
33
|
+
|
|
34
|
+
> This issue showcases the **investigation phase** — the agent verifies the
|
|
35
|
+
> claim against the code before writing the optimization.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "docs(api): add an API reference for the Notes endpoints"
|
|
3
|
+
labels: ["documentation", "priority:P3"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
The demo API has no endpoint documentation. A newcomer has to read `server.js`
|
|
9
|
+
and `routes/notes.js` to learn what routes exist, which ones need a token, and
|
|
10
|
+
what they return.
|
|
11
|
+
|
|
12
|
+
## Proposed Solution
|
|
13
|
+
|
|
14
|
+
Add `docs/API.md` documenting every endpoint:
|
|
15
|
+
|
|
16
|
+
| Method | Path | Auth | Description |
|
|
17
|
+
|--------|------|------|-------------|
|
|
18
|
+
| GET | `/health` | no | Liveness check |
|
|
19
|
+
| GET | `/notes` | no | List notes (supports filtering) |
|
|
20
|
+
| GET | `/notes/:id` | no | Fetch one note |
|
|
21
|
+
| POST | `/notes` | yes | Create a note |
|
|
22
|
+
| DELETE | `/notes/:id` | yes | Delete a note |
|
|
23
|
+
|
|
24
|
+
Include a `curl` example for each and note the `Authorization: Bearer demo-token`
|
|
25
|
+
header for protected routes.
|
|
26
|
+
|
|
27
|
+
## Affected Files
|
|
28
|
+
|
|
29
|
+
1. `docs/API.md` (NEW) — endpoint reference.
|
|
30
|
+
2. `README.md` — link to the new API reference.
|
|
31
|
+
|
|
32
|
+
## Acceptance Criteria
|
|
33
|
+
|
|
34
|
+
- [ ] `docs/API.md` documents all 5 endpoints with auth + examples.
|
|
35
|
+
- [ ] README links to it.
|
|
36
|
+
- [ ] No code changes required.
|
|
37
|
+
|
|
38
|
+
> This issue proves the pipeline handles **non-code work** — docs-only changes
|
|
39
|
+
> still go through investigate → build → review → merge.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "priority:P0", "color": "B60205", "description": "Critical priority — production broken, data loss, security breach." },
|
|
3
|
+
{ "name": "priority:P1", "color": "D93F0B", "description": "High priority — major feature broken, no workaround." },
|
|
4
|
+
{ "name": "priority:P2", "color": "FBCA04", "description": "Medium priority — impaired functionality, workaround exists." },
|
|
5
|
+
{ "name": "priority:P3", "color": "C2E0C6", "description": "Low priority — minor issue or polish." },
|
|
6
|
+
{ "name": "workflow:investigating", "color": "1D76DB", "description": "Pipeline: investigation phase in progress." },
|
|
7
|
+
{ "name": "workflow:ready-to-build", "color": "0075CA", "description": "Pipeline: investigation complete, ready for build." },
|
|
8
|
+
{ "name": "workflow:building", "color": "0052CC", "description": "Pipeline: implementation in progress." },
|
|
9
|
+
{ "name": "workflow:in-review", "color": "5319E7", "description": "Pipeline: PR created, under review." },
|
|
10
|
+
{ "name": "workflow:merged", "color": "0E8A16", "description": "Pipeline: PR merged, issue closed." },
|
|
11
|
+
{ "name": "workflow:decomposed", "color": "BFD4F2", "description": "Pipeline: decomposed into sub-issues." },
|
|
12
|
+
{ "name": "workflow:invalid", "color": "CCCCCC", "description": "Pipeline: issue closed as invalid after investigation." },
|
|
13
|
+
{ "name": "needs-human", "color": "E4E669", "description": "Pipeline stalled — requires human intervention." },
|
|
14
|
+
{ "name": "review-finding", "color": "D93F0B", "description": "Defect or improvement found during automated PR review." },
|
|
15
|
+
{ "name": "bug", "color": "D73A4A", "description": "Something isn't working." },
|
|
16
|
+
{ "name": "feature", "color": "0075CA", "description": "New feature implementation." },
|
|
17
|
+
{ "name": "refactor", "color": "FEF2C0", "description": "Code restructuring without behavior change." },
|
|
18
|
+
{ "name": "performance", "color": "F9D0C4", "description": "Performance optimization or regression." },
|
|
19
|
+
{ "name": "documentation", "color": "0075CA", "description": "Documentation addition or update." },
|
|
20
|
+
{ "name": "security", "color": "B60205", "description": "Security vulnerability or hardening." }
|
|
21
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "forgedock-demo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tiny, dependency-free Notes API used as a risk-free playground for trying ForgeDock.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "src/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/server.js",
|
|
9
|
+
"smoke": "node scripts/smoke.js"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"private": true
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Tiny smoke test for the demo Notes API. No test framework, no deps.
|
|
2
|
+
// Starts the server on a random port, hits a few endpoints, and asserts.
|
|
3
|
+
// Run with: npm run smoke
|
|
4
|
+
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const { server } = require('../src/server');
|
|
8
|
+
|
|
9
|
+
function request(port, method, path, { token, json } = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const headers = {};
|
|
12
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
13
|
+
let payload;
|
|
14
|
+
if (json) {
|
|
15
|
+
payload = JSON.stringify(json);
|
|
16
|
+
headers['Content-Type'] = 'application/json';
|
|
17
|
+
headers['Content-Length'] = Buffer.byteLength(payload);
|
|
18
|
+
}
|
|
19
|
+
const req = http.request({ host: '127.0.0.1', port, method, path, headers }, (res) => {
|
|
20
|
+
let raw = '';
|
|
21
|
+
res.on('data', (c) => (raw += c));
|
|
22
|
+
res.on('end', () => resolve({ status: res.statusCode, body: raw ? JSON.parse(raw) : {} }));
|
|
23
|
+
});
|
|
24
|
+
req.on('error', reject);
|
|
25
|
+
if (payload) req.write(payload);
|
|
26
|
+
req.end();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
await new Promise((r) => server.listen(0, r));
|
|
32
|
+
const { port } = server.address();
|
|
33
|
+
|
|
34
|
+
const health = await request(port, 'GET', '/health');
|
|
35
|
+
assert.strictEqual(health.status, 200, 'health should be 200');
|
|
36
|
+
assert.strictEqual(health.body.status, 'ok', 'health status should be ok');
|
|
37
|
+
|
|
38
|
+
const list = await request(port, 'GET', '/notes');
|
|
39
|
+
assert.ok(Array.isArray(list.body.notes), 'GET /notes returns an array');
|
|
40
|
+
assert.ok(list.body.notes.length >= 3, 'seed notes present');
|
|
41
|
+
|
|
42
|
+
const created = await request(port, 'POST', '/notes', {
|
|
43
|
+
token: 'demo-token',
|
|
44
|
+
json: { title: 'smoke', owner: 'tester' },
|
|
45
|
+
});
|
|
46
|
+
assert.strictEqual(created.status, 201, 'POST with token returns 201');
|
|
47
|
+
|
|
48
|
+
const unauth = await request(port, 'POST', '/notes', { json: { title: 'nope' } });
|
|
49
|
+
assert.strictEqual(unauth.status, 401, 'POST without token returns 401');
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log('SMOKE OK — health, list, create(auth), create(unauth) all pass');
|
|
53
|
+
server.close();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main().catch((err) => {
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.error('SMOKE FAILED:', err.message);
|
|
59
|
+
server.close();
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Minimal token-based auth for the demo Notes API.
|
|
2
|
+
//
|
|
3
|
+
// The "token" is just a static demo secret — there are no real users or
|
|
4
|
+
// sessions. Routes that should be protected call `requireToken(req)`.
|
|
5
|
+
//
|
|
6
|
+
// >>> DEMO NOTE: In routes/notes.js, one mutating route deliberately does NOT
|
|
7
|
+
// >>> call requireToken(). That missing-auth check is an INTENTIONAL flaw so
|
|
8
|
+
// >>> ForgeDock's review agents have an authorization gap to flag.
|
|
9
|
+
|
|
10
|
+
const DEMO_TOKEN = 'demo-token';
|
|
11
|
+
|
|
12
|
+
// Returns true when the request carries the demo bearer token.
|
|
13
|
+
function isAuthenticated(req) {
|
|
14
|
+
const header = req.headers['authorization'] || '';
|
|
15
|
+
const [scheme, value] = header.split(' ');
|
|
16
|
+
return scheme === 'Bearer' && value === DEMO_TOKEN;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Throws a 401-style error when the request is not authenticated.
|
|
20
|
+
function requireToken(req) {
|
|
21
|
+
if (!isAuthenticated(req)) {
|
|
22
|
+
const err = new Error('Unauthorized: missing or invalid bearer token');
|
|
23
|
+
err.statusCode = 401;
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { isAuthenticated, requireToken, DEMO_TOKEN };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// In-memory data store for the demo Notes API.
|
|
2
|
+
//
|
|
3
|
+
// This is intentionally a plain array plus a deliberately naive query helper.
|
|
4
|
+
// There is no real database — `query()` simulates a SQL-style WHERE clause by
|
|
5
|
+
// building a predicate from a raw string. This mirrors the classic SQL-injection
|
|
6
|
+
// pattern (untrusted input concatenated into a query) so that ForgeDock's
|
|
7
|
+
// /review-pr security agent has a realistic finding to surface.
|
|
8
|
+
//
|
|
9
|
+
// >>> DEMO NOTE: The string-built filter below is an INTENTIONAL, isolated flaw.
|
|
10
|
+
// >>> It exists only inside examples/forgedock-demo so the review agents have
|
|
11
|
+
// >>> something to catch. Do not copy this pattern into real code.
|
|
12
|
+
|
|
13
|
+
let notes = [
|
|
14
|
+
{ id: 1, owner: 'alice', title: 'Buy milk', body: 'Whole milk, 2 liters', secret: false },
|
|
15
|
+
{ id: 2, owner: 'alice', title: 'Launch plan', body: 'Ship the demo repo', secret: true },
|
|
16
|
+
{ id: 3, owner: 'bob', title: 'Gym', body: 'Leg day', secret: false },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
let nextId = 4;
|
|
20
|
+
|
|
21
|
+
function all() {
|
|
22
|
+
return notes;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findById(id) {
|
|
26
|
+
return notes.find((n) => n.id === Number(id));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Simulated SQL query. `whereClause` is a raw expression like "owner == 'alice'".
|
|
30
|
+
// INTENTIONAL FLAW: the clause is evaluated directly, so a caller can inject
|
|
31
|
+
// arbitrary expressions (e.g. "true || secret"). A real fix would use a
|
|
32
|
+
// parameterized filter instead of evaluating untrusted strings.
|
|
33
|
+
function query(whereClause) {
|
|
34
|
+
// eslint-disable-next-line no-new-func
|
|
35
|
+
const predicate = new Function('row', `with (row) { return (${whereClause}); }`);
|
|
36
|
+
return notes.filter((row) => {
|
|
37
|
+
try {
|
|
38
|
+
return predicate(row);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function insert(note) {
|
|
46
|
+
const record = { id: nextId++, secret: false, ...note };
|
|
47
|
+
notes.push(record);
|
|
48
|
+
return record;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function remove(id) {
|
|
52
|
+
const before = notes.length;
|
|
53
|
+
notes = notes.filter((n) => n.id !== Number(id));
|
|
54
|
+
return notes.length < before;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function reset() {
|
|
58
|
+
notes = [
|
|
59
|
+
{ id: 1, owner: 'alice', title: 'Buy milk', body: 'Whole milk, 2 liters', secret: false },
|
|
60
|
+
{ id: 2, owner: 'alice', title: 'Launch plan', body: 'Ship the demo repo', secret: true },
|
|
61
|
+
{ id: 3, owner: 'bob', title: 'Gym', body: 'Leg day', secret: false },
|
|
62
|
+
];
|
|
63
|
+
nextId = 4;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { all, findById, query, insert, remove, reset };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Route handlers for the demo Notes API.
|
|
2
|
+
//
|
|
3
|
+
// Each handler takes (req, res, params) and returns a plain object that the
|
|
4
|
+
// server serializes to JSON. Handlers throw errors with a `statusCode` to
|
|
5
|
+
// signal HTTP error responses.
|
|
6
|
+
|
|
7
|
+
const db = require('../db');
|
|
8
|
+
const { requireToken } = require('../auth');
|
|
9
|
+
|
|
10
|
+
// GET /notes
|
|
11
|
+
// Lists notes. Supports an optional ?where= filter that is passed straight
|
|
12
|
+
// into db.query().
|
|
13
|
+
//
|
|
14
|
+
// >>> DEMO NOTE: forwarding the raw ?where= string into db.query() is the
|
|
15
|
+
// >>> injection surface (see db.js). Issue #2 asks to make this safe.
|
|
16
|
+
function listNotes(req, res, params) {
|
|
17
|
+
const where = params.query.where;
|
|
18
|
+
if (where) {
|
|
19
|
+
return { notes: db.query(where) };
|
|
20
|
+
}
|
|
21
|
+
return { notes: db.all() };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// GET /notes/:id
|
|
25
|
+
function getNote(req, res, params) {
|
|
26
|
+
const note = db.findById(params.id);
|
|
27
|
+
if (!note) {
|
|
28
|
+
const err = new Error('Note not found');
|
|
29
|
+
err.statusCode = 404;
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
return { note };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// POST /notes (protected)
|
|
36
|
+
function createNote(req, res, params) {
|
|
37
|
+
requireToken(req);
|
|
38
|
+
const { title, body, owner } = params.body || {};
|
|
39
|
+
if (!title) {
|
|
40
|
+
const err = new Error('title is required');
|
|
41
|
+
err.statusCode = 400;
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
const note = db.insert({ title, body: body || '', owner: owner || 'anonymous' });
|
|
45
|
+
res.statusCode = 201;
|
|
46
|
+
return { note };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// DELETE /notes/:id
|
|
50
|
+
//
|
|
51
|
+
// >>> DEMO NOTE: this mutating route is MISSING a requireToken(req) call, so
|
|
52
|
+
// >>> anyone can delete any note. That missing authorization check is the
|
|
53
|
+
// >>> INTENTIONAL flaw that Issue #1 (bug fix) asks you to close.
|
|
54
|
+
function deleteNote(req, res, params) {
|
|
55
|
+
const ok = db.remove(params.id);
|
|
56
|
+
if (!ok) {
|
|
57
|
+
const err = new Error('Note not found');
|
|
58
|
+
err.statusCode = 404;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
return { deleted: Number(params.id) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { listNotes, getNote, createNote, deleteNote };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Entry point for the demo Notes API.
|
|
2
|
+
//
|
|
3
|
+
// Uses only Node's built-in `http` module — no Express, no npm install. Run it
|
|
4
|
+
// with `node src/server.js` (or `npm start`). The router below is intentionally
|
|
5
|
+
// small and hand-rolled so the whole codebase stays readable in a few minutes.
|
|
6
|
+
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const { URL } = require('url');
|
|
9
|
+
const routes = require('./routes/notes');
|
|
10
|
+
|
|
11
|
+
const PORT = process.env.PORT || 3000;
|
|
12
|
+
|
|
13
|
+
// Route table: [method, pathRegex, handler]. A `:id` segment is captured.
|
|
14
|
+
const TABLE = [
|
|
15
|
+
['GET', /^\/notes$/, routes.listNotes],
|
|
16
|
+
['POST', /^\/notes$/, routes.createNote],
|
|
17
|
+
['GET', /^\/notes\/(?<id>\d+)$/, routes.getNote],
|
|
18
|
+
['DELETE', /^\/notes\/(?<id>\d+)$/, routes.deleteNote],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function readBody(req) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
let raw = '';
|
|
24
|
+
req.on('data', (chunk) => {
|
|
25
|
+
raw += chunk;
|
|
26
|
+
});
|
|
27
|
+
req.on('end', () => {
|
|
28
|
+
if (!raw) return resolve({});
|
|
29
|
+
try {
|
|
30
|
+
resolve(JSON.parse(raw));
|
|
31
|
+
} catch {
|
|
32
|
+
resolve({});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const server = http.createServer(async (req, res) => {
|
|
39
|
+
res.setHeader('Content-Type', 'application/json');
|
|
40
|
+
|
|
41
|
+
// Health check.
|
|
42
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
43
|
+
res.statusCode = 200;
|
|
44
|
+
return res.end(JSON.stringify({ status: 'ok' }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
48
|
+
const query = Object.fromEntries(url.searchParams.entries());
|
|
49
|
+
|
|
50
|
+
const match = TABLE.find(
|
|
51
|
+
([method, pattern]) => method === req.method && pattern.test(url.pathname)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!match) {
|
|
55
|
+
res.statusCode = 404;
|
|
56
|
+
return res.end(JSON.stringify({ error: 'Not found' }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [, pattern, handler] = match;
|
|
60
|
+
const groups = url.pathname.match(pattern).groups || {};
|
|
61
|
+
const body = req.method === 'POST' ? await readBody(req) : {};
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = handler(req, res, { ...groups, query, body });
|
|
65
|
+
return res.end(JSON.stringify(result));
|
|
66
|
+
} catch (err) {
|
|
67
|
+
res.statusCode = err.statusCode || 500;
|
|
68
|
+
return res.end(JSON.stringify({ error: err.message }));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (require.main === module) {
|
|
73
|
+
server.listen(PORT, () => {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.log(`Notes API listening on http://localhost:${PORT}`);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { server };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Autonomous dev pipeline for Claude Code. GitHub as institutional memory — every issue, PR, and annotation survives context resets and makes the next agent smarter.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "git+https://github.com/RapierCraftStudios/ForgeDock.git"
|
|
29
29
|
},
|
|
30
|
-
"homepage": "https://
|
|
30
|
+
"homepage": "https://github.com/RapierCraftStudios/ForgeDock#readme",
|
|
31
31
|
"bugs": {
|
|
32
32
|
"url": "https://github.com/RapierCraftStudios/ForgeDock/issues"
|
|
33
33
|
},
|
|
@@ -36,17 +36,27 @@
|
|
|
36
36
|
"!bin/tests/",
|
|
37
37
|
"commands/",
|
|
38
38
|
"docs/",
|
|
39
|
+
"examples/",
|
|
39
40
|
"scripts/",
|
|
40
41
|
"templates/",
|
|
41
42
|
"LICENSE",
|
|
42
43
|
"README.md"
|
|
43
44
|
],
|
|
44
45
|
"scripts": {
|
|
45
|
-
"test": "node --test \"bin/tests/**/*.test.mjs\""
|
|
46
|
+
"test": "node --test \"bin/tests/**/*.test.mjs\"",
|
|
47
|
+
"docs:dev": "vitepress dev docs/site",
|
|
48
|
+
"docs:build": "vitepress build docs/site",
|
|
49
|
+
"docs:preview": "vitepress preview docs/site"
|
|
46
50
|
},
|
|
47
51
|
"engines": {
|
|
48
52
|
"node": ">=18"
|
|
49
53
|
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"@anthropic-ai/sdk": ">=0.30.0 <1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"vitepress": "^1.6.3"
|
|
59
|
+
},
|
|
50
60
|
"peerDependencies": {
|
|
51
61
|
"@anthropic-ai/claude-code": ">=2.0.0"
|
|
52
62
|
},
|
|
@@ -179,8 +179,24 @@ for var in $VAR_NAMES; do
|
|
|
179
179
|
fi
|
|
180
180
|
|
|
181
181
|
if [ -z "$FOUND_IN" ]; then
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
# Determine whether any of the optional surfaces exist in this repo.
|
|
183
|
+
# If NONE of the known surfaces exist (not even .env.example), the repo
|
|
184
|
+
# uses a layout we can't validate — emit SKIP instead of a false BLOCKING.
|
|
185
|
+
# This is the verify-sops-chain.sh pattern: unknown layout → graceful skip,
|
|
186
|
+
# not a spurious failure. <!-- Added: forge#1349 -->
|
|
187
|
+
HAS_ANY_SURFACE=0
|
|
188
|
+
[ -f "$REPO_ROOT/.env.example" ] && HAS_ANY_SURFACE=1
|
|
189
|
+
[ -f "$REPO_ROOT/docker-compose.yml" ] && HAS_ANY_SURFACE=1
|
|
190
|
+
[ -f "$REPO_ROOT/docker-compose.prod.yml" ] && HAS_ANY_SURFACE=1
|
|
191
|
+
[ -f "$REPO_ROOT/services/api/app/core/env_validation.py" ] && HAS_ANY_SURFACE=1
|
|
192
|
+
[ -f "$REPO_ROOT/scripts/decrypt-secrets.sh" ] && HAS_ANY_SURFACE=1
|
|
193
|
+
|
|
194
|
+
if [ "$HAS_ANY_SURFACE" -eq 0 ]; then
|
|
195
|
+
echo "SKIP: $var — no known env-wiring surfaces found in repo (no .env.example, docker-compose, env_validation.py, or SOPS mapping). Configure verification.env_check_surfaces in forge.yaml or add .env.example to enable this check."
|
|
196
|
+
else
|
|
197
|
+
echo "BLOCKING: $var — not found in any detected env-wiring surface (.env.example, docker-compose.yml, env_validation.py, or decrypt-secrets.sh)"
|
|
198
|
+
BLOCKING=$((BLOCKING + 1))
|
|
199
|
+
fi
|
|
184
200
|
else
|
|
185
201
|
echo "OK: $var — found in:$FOUND_IN"
|
|
186
202
|
fi
|
|
@@ -67,9 +67,9 @@ while read -r f; do
|
|
|
67
67
|
elif echo "$CONTENT" | grep -qE "\-\-header\s+[\"']Host:"; then
|
|
68
68
|
echo "OK: $f:$LINENO — Host header explicitly set (--header)"
|
|
69
69
|
else
|
|
70
|
-
echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header.
|
|
70
|
+
echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header. Host-validation middleware will reject a raw IP as the Host value."
|
|
71
71
|
echo " Line: $CONTENT"
|
|
72
|
-
echo " Fix: Add -H 'Host:
|
|
72
|
+
echo " Fix: Add -H 'Host: <allowed-hostname>' — check your service's allowed_hosts configuration for the correct value"
|
|
73
73
|
BLOCKING=$((BLOCKING + 1))
|
|
74
74
|
fi
|
|
75
75
|
done < <(echo "$CURL_LINES")
|
|
@@ -96,11 +96,11 @@ if [ -n "$FRONTEND_FILES" ]; then
|
|
|
96
96
|
fi
|
|
97
97
|
|
|
98
98
|
if [ -n "$DIRECT_CALLS" ]; then
|
|
99
|
-
echo "BLOCKING: $f — direct /api/v1/ call bypasses
|
|
99
|
+
echo "BLOCKING: $f — direct /api/v1/ call bypasses frontend proxy (auth middleware will be skipped)"
|
|
100
100
|
while IFS= read -r match; do
|
|
101
101
|
echo " $match"
|
|
102
102
|
done < <(echo "$DIRECT_CALLS")
|
|
103
|
-
echo " Fix: Use
|
|
103
|
+
echo " Fix: Use the frontend proxy route (e.g. /api/*) instead of calling the backend API directly"
|
|
104
104
|
BLOCKING=$((BLOCKING + 1))
|
|
105
105
|
else
|
|
106
106
|
echo "OK: $f — no direct backend calls"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# changed_files_file: path to a file listing changed files (one per line)
|
|
6
6
|
# repo_root: path to the repository root
|
|
7
7
|
#
|
|
8
|
-
# Layout path overrides (env vars — override defaults for
|
|
8
|
+
# Layout path overrides (env vars — override defaults for project-specific layouts):
|
|
9
9
|
# FORGE_PAGES_ROOT Root of Next.js App Router pages dir (default: web/src/app)
|
|
10
10
|
# FORGE_API_ROUTERS_DIR Dir containing FastAPI router .py files (default: services/api/app/routers)
|
|
11
11
|
# FORGE_API_MAIN Path to API app entrypoint, relative to repo root (default: services/api/app/main.py)
|
|
@@ -35,7 +35,7 @@ else
|
|
|
35
35
|
FILES=$(cat "$CHANGED_FILES")
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
|
-
# Layout path configuration — read from env vars, fall back to
|
|
38
|
+
# Layout path configuration — read from env vars, fall back to project defaults
|
|
39
39
|
FORGE_PAGES_ROOT="${FORGE_PAGES_ROOT:-web/src/app}"
|
|
40
40
|
FORGE_API_ROUTERS_DIR="${FORGE_API_ROUTERS_DIR:-services/api/app/routers}"
|
|
41
41
|
FORGE_API_MAIN="${FORGE_API_MAIN:-services/api/app/main.py}"
|