forgedock 1.0.21 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +134 -80
  2. package/bin/demo.mjs +386 -0
  3. package/bin/forgedock.mjs +357 -30
  4. package/bin/init-detect.mjs +95 -0
  5. package/bin/runner.mjs +780 -0
  6. package/commands/cleanup.md +85 -39
  7. package/commands/deploy-info.md +6 -2
  8. package/commands/forge-stats.md +1 -1
  9. package/commands/optimize.md +1 -1
  10. package/commands/orchestrate.md +320 -26
  11. package/commands/quality-gate.md +91 -49
  12. package/commands/review-pr-agents.md +62 -38
  13. package/commands/review-pr.md +33 -2
  14. package/commands/scope.md +11 -3
  15. package/commands/security-audit.md +24 -16
  16. package/commands/sync-ecosystem.md +2 -2
  17. package/commands/work-on/build/architect.md +47 -3
  18. package/commands/work-on/build/context.md +36 -5
  19. package/commands/work-on/build/validate.md +16 -7
  20. package/commands/work-on/close.md +129 -1
  21. package/commands/work-on/investigate.md +24 -1
  22. package/commands/work-on.md +226 -8
  23. package/docs/site/annotations-explained.md +55 -0
  24. package/docs/site/command-learning-path.md +104 -0
  25. package/docs/site/command-reference.md +3 -0
  26. package/docs/site/getting-started.md +36 -14
  27. package/docs/site/troubleshooting.md +208 -0
  28. package/docs/spec/forge-protocol-v1.md +768 -0
  29. package/examples/forgedock-demo/README.md +113 -0
  30. package/examples/forgedock-demo/bootstrap.sh +125 -0
  31. package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
  32. package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
  33. package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
  34. package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
  35. package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
  36. package/examples/forgedock-demo/labels.json +21 -0
  37. package/examples/forgedock-demo/package.json +16 -0
  38. package/examples/forgedock-demo/scripts/smoke.js +61 -0
  39. package/examples/forgedock-demo/src/auth.js +28 -0
  40. package/examples/forgedock-demo/src/db.js +66 -0
  41. package/examples/forgedock-demo/src/routes/notes.js +64 -0
  42. package/examples/forgedock-demo/src/server.js +79 -0
  43. package/package.json +5 -1
  44. package/templates/forge.yaml.minimal +44 -0
@@ -0,0 +1,113 @@
1
+ # ForgeDock Demo — A Risk-Free First Pipeline Run
2
+
3
+ A tiny **Notes API** (~300 lines, zero dependencies) with five pre-written
4
+ issues. It exists so you can watch ForgeDock investigate, build, review, and
5
+ merge a change **without touching your real codebase**.
6
+
7
+ > Clone → `npx forgedock` → open Claude Code → `/work-on 1` → watch it go.
8
+
9
+ ---
10
+
11
+ ## Why this exists
12
+
13
+ Trying ForgeDock on your own project means trusting an AI pipeline with real
14
+ code on your very first run. This demo removes that risk: it's a self-contained
15
+ sandbox where every issue is small, fast, and safe to break.
16
+
17
+ The API has a couple of **intentional, clearly-labelled flaws** (a missing auth
18
+ check, an unsafe query path) so that ForgeDock's `/review-pr` agents have real
19
+ findings to surface — that's the part that makes the review feel impressive.
20
+
21
+ ---
22
+
23
+ ## What's inside
24
+
25
+ ```
26
+ forgedock-demo/
27
+ ├── src/
28
+ │ ├── server.js # built-in http server + router (no Express)
29
+ │ ├── router.js # (added by Issue #3)
30
+ │ ├── routes/notes.js # CRUD handlers — has the intentional flaws
31
+ │ ├── db.js # in-memory store
32
+ │ └── auth.js # bearer-token check
33
+ ├── scripts/smoke.js # dependency-free smoke test (npm run smoke)
34
+ ├── issues/ # the 5 pre-written issue specs
35
+ ├── forge.yaml # minimal working ForgeDock config
36
+ ├── labels.json # workflow + priority labels
37
+ └── bootstrap.sh # one command to stand up the live repo
38
+ ```
39
+
40
+ No database. No Docker. No API keys. Runs on Node 18+.
41
+
42
+ ---
43
+
44
+ ## Run it locally (optional)
45
+
46
+ ```bash
47
+ node src/server.js # starts on http://localhost:3000
48
+ # in another terminal:
49
+ curl http://localhost:3000/notes
50
+ npm run smoke # runs the smoke test
51
+ ```
52
+
53
+ ---
54
+
55
+ ## The five demo issues
56
+
57
+ Each issue targets a different pipeline strength and is scoped to finish fast.
58
+
59
+ | # | Type | Title | What it shows |
60
+ |---|------|-------|---------------|
61
+ | 1 | Bug / security | DELETE is missing an auth check | Simplest fix — fastest run |
62
+ | 2 | Feature / security | Safe filtering for `GET /notes` | Investigation + architecture |
63
+ | 3 | Refactor | Extract the router module | "No behavior change" review |
64
+ | 4 | Performance | O(1) `findById` | Investigation phase tracing the hot path |
65
+ | 5 | Docs | Add an API reference | Pipeline handles non-code work |
66
+
67
+ Full specs live in [`issues/`](./issues). When you run `bootstrap.sh`, these
68
+ become real GitHub issues #1–#5 in your demo repo, in this order.
69
+
70
+ ---
71
+
72
+ ## Stand up your own live demo repo
73
+
74
+ The scaffold is complete and ready — the only step that needs your GitHub
75
+ credentials is creating the actual repo. One command does it:
76
+
77
+ ```bash
78
+ # requires an authenticated gh CLI (gh auth login)
79
+ ./bootstrap.sh # creates <your-user>/forgedock-demo (public)
80
+ # or:
81
+ ./bootstrap.sh my-org/forgedock-demo # custom owner/repo
82
+ ```
83
+
84
+ `bootstrap.sh` will:
85
+
86
+ 1. Create the GitHub repo (skips if it already exists).
87
+ 2. Push this codebase to `main`.
88
+ 3. Create the workflow + priority labels (via `npx forgedock labels setup`,
89
+ falling back to `labels.json`).
90
+ 4. File the five issues from [`issues/`](./issues).
91
+
92
+ Then:
93
+
94
+ ```bash
95
+ git clone https://github.com/<you>/forgedock-demo.git
96
+ cd forgedock-demo
97
+ npx forgedock # install the ForgeDock commands
98
+ # open Claude Code in this directory, then run:
99
+ /work-on 1
100
+ ```
101
+
102
+ Watch ForgeDock investigate the missing-auth bug, write the fix, run the quality
103
+ gate, open a PR, review it, and merge — all on a repo you can safely throw away.
104
+
105
+ ---
106
+
107
+ ## After your first run
108
+
109
+ Try `/work-on 2` through `/work-on 5` to see the other pipeline phases, or run
110
+ `/orchestrate` to let ForgeDock work several issues in parallel.
111
+
112
+ When you're comfortable, point ForgeDock at your real project — you've already
113
+ seen exactly what it does.
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bootstrap.sh — stand up a live ForgeDock demo repo from this scaffold.
4
+ #
5
+ # This is the ONE step that needs a human: it creates a real GitHub repo (which
6
+ # requires your credentials), pushes this code, sets up the workflow labels, and
7
+ # files the five demo issues. Everything else in the scaffold is ready to go.
8
+ #
9
+ # Usage:
10
+ # ./bootstrap.sh # creates <your-user>/forgedock-demo (public)
11
+ # ./bootstrap.sh my-org/forgedock-demo # custom owner/repo
12
+ # ./bootstrap.sh my-org/forgedock-demo --private
13
+ #
14
+ # Requirements: an authenticated `gh` CLI (`gh auth status`) and `git`.
15
+
16
+ set -euo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ cd "$SCRIPT_DIR"
20
+
21
+ # ---- args -------------------------------------------------------------------
22
+ REPO_SLUG="${1:-}"
23
+ VISIBILITY="--public"
24
+ for arg in "$@"; do
25
+ case "$arg" in
26
+ --private) VISIBILITY="--private" ;;
27
+ --public) VISIBILITY="--public" ;;
28
+ esac
29
+ done
30
+
31
+ if [ -z "$REPO_SLUG" ] || [[ "$REPO_SLUG" == --* ]]; then
32
+ GH_USER="$(gh api user --jq '.login')"
33
+ REPO_SLUG="${GH_USER}/forgedock-demo"
34
+ fi
35
+
36
+ echo "==> Target repo: $REPO_SLUG ($VISIBILITY)"
37
+
38
+ command -v gh >/dev/null || { echo "ERROR: gh CLI not found"; exit 1; }
39
+ gh auth status >/dev/null || { echo "ERROR: run 'gh auth login' first"; exit 1; }
40
+
41
+ # ---- 1. create the repo -----------------------------------------------------
42
+ REPO_CREATED=false
43
+ if gh repo view "$REPO_SLUG" >/dev/null 2>&1; then
44
+ echo "==> Repo already exists, skipping creation."
45
+ else
46
+ echo "==> Creating repo..."
47
+ gh repo create "$REPO_SLUG" $VISIBILITY \
48
+ --description "Try ForgeDock risk-free — a tiny Notes API with pre-written issues." \
49
+ --disable-wiki
50
+ REPO_CREATED=true
51
+ fi
52
+
53
+ # ---- 2. push the scaffold ---------------------------------------------------
54
+ TMP_DIR="$(mktemp -d)"
55
+ echo "==> Staging scaffold in $TMP_DIR"
56
+ # Copy everything except bootstrap.sh and the issues/ specs (issues become GH issues).
57
+ cp -r src scripts package.json forge.yaml labels.json README.md .gitignore "$TMP_DIR/" 2>/dev/null || true
58
+ # Ship the issue specs too, for reference inside the repo.
59
+ cp -r issues "$TMP_DIR/issues"
60
+
61
+ (
62
+ cd "$TMP_DIR"
63
+ git init -q
64
+ git checkout -q -b main
65
+ git add .
66
+ git -c user.name="forgedock-demo" -c user.email="demo@forgedock.dev" \
67
+ commit -q -m "chore: initial demo scaffold"
68
+ git remote add origin "https://github.com/${REPO_SLUG}.git"
69
+ if [ "$REPO_CREATED" = "true" ]; then
70
+ # Fresh repo we just created — safe to push.
71
+ git push -q -u origin main
72
+ else
73
+ # Repo pre-existed. Never force-push over someone's existing main; do a
74
+ # non-destructive push and let it fail loudly if the repo already has commits.
75
+ if ! git push -q -u origin main 2>/dev/null; then
76
+ echo "ERROR: '$REPO_SLUG' already has content on main. Refusing to overwrite it." >&2
77
+ echo " Use a fresh/empty repo, or push manually if you intend to replace it." >&2
78
+ exit 1
79
+ fi
80
+ fi
81
+ )
82
+ echo "==> Pushed scaffold to $REPO_SLUG"
83
+
84
+ # ---- 3. labels --------------------------------------------------------------
85
+ echo "==> Creating labels..."
86
+ # Prefer the ForgeDock CLI if available; otherwise create from labels.json.
87
+ if command -v npx >/dev/null && npx --no-install forgedock --help >/dev/null 2>&1; then
88
+ npx forgedock labels setup --repo "$REPO_SLUG" || true
89
+ else
90
+ node -e '
91
+ const fs = require("fs");
92
+ const labels = JSON.parse(fs.readFileSync("labels.json", "utf8"));
93
+ for (const l of labels) console.log([l.name, l.color, l.description].join("\t"));
94
+ ' labels.json | while IFS=$'\t' read -r name color desc; do
95
+ gh label create "$name" --color "$color" --description "$desc" --force -R "$REPO_SLUG" >/dev/null 2>&1 || true
96
+ done
97
+ fi
98
+
99
+ # ---- 4. issues --------------------------------------------------------------
100
+ echo "==> Creating issues..."
101
+ for f in issues/*.md; do
102
+ title="$(grep -m1 '^title:' "$f" | sed -E 's/^title:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/')"
103
+ labels="$(grep -m1 '^labels:' "$f" | sed -E 's/^labels:[[:space:]]*\[(.*)\][[:space:]]*$/\1/' | tr -d '"' | tr -d ' ')"
104
+ # Body = everything after the closing front-matter '---'.
105
+ body="$(awk 'f{print} /^---[[:space:]]*$/{c++; if(c==2) f=1}' "$f")"
106
+ label_args=()
107
+ if [ -n "$labels" ]; then
108
+ IFS=',' read -ra parts <<< "$labels"
109
+ for l in "${parts[@]}"; do label_args+=(--label "$l"); done
110
+ fi
111
+ echo " - $title"
112
+ gh issue create -R "$REPO_SLUG" --title "$title" --body "$body" "${label_args[@]}" >/dev/null \
113
+ || gh issue create -R "$REPO_SLUG" --title "$title" --body "$body" >/dev/null
114
+ done
115
+
116
+ rm -rf "$TMP_DIR"
117
+
118
+ echo ""
119
+ echo "Done. Your demo repo is ready:"
120
+ echo " https://github.com/${REPO_SLUG}"
121
+ echo ""
122
+ echo "Next:"
123
+ echo " git clone https://github.com/${REPO_SLUG}.git && cd forgedock-demo"
124
+ echo " npx forgedock # install the commands"
125
+ echo " # open Claude Code here, then run: /work-on 1"
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: "fix(notes): DELETE /notes/:id is missing an auth check"
3
+ labels: ["bug", "security", "priority:P1"]
4
+ ---
5
+
6
+ ## Problem
7
+
8
+ `DELETE /notes/:id` lets **anyone** delete any note. The other mutating route
9
+ (`POST /notes`) requires a bearer token via `requireToken(req)`, but the delete
10
+ handler skips that check entirely. This is an authorization gap.
11
+
12
+ ## Steps to Reproduce
13
+
14
+ ```bash
15
+ # No token, yet it succeeds:
16
+ curl -X DELETE http://localhost:3000/notes/1
17
+ # => {"deleted":1}
18
+ ```
19
+
20
+ ## Affected Files
21
+
22
+ 1. `src/routes/notes.js` — `deleteNote()` is missing a `requireToken(req)` call.
23
+
24
+ ## Expected Behavior
25
+
26
+ `DELETE /notes/:id` returns `401 Unauthorized` when no valid bearer token is
27
+ supplied, matching the protection on `POST /notes`.
28
+
29
+ ## Acceptance Criteria
30
+
31
+ - [ ] `deleteNote()` calls `requireToken(req)` before deleting.
32
+ - [ ] Unauthenticated delete returns 401.
33
+ - [ ] Authenticated delete (`Authorization: Bearer demo-token`) still works.
34
+ - [ ] No regression in `npm run smoke`.
35
+
36
+ > Good first run: this is the simplest issue — expect the pipeline to finish in
37
+ > a couple of minutes.
@@ -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 };