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.
- package/README.md +134 -80
- package/bin/demo.mjs +386 -0
- package/bin/forgedock.mjs +357 -30
- package/bin/init-detect.mjs +95 -0
- package/bin/runner.mjs +780 -0
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +6 -2
- package/commands/forge-stats.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +320 -26
- package/commands/quality-gate.md +91 -49
- package/commands/review-pr-agents.md +62 -38
- package/commands/review-pr.md +33 -2
- 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/validate.md +16 -7
- package/commands/work-on/close.md +129 -1
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on.md +226 -8
- 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 +36 -14
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -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 +5 -1
- 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 };
|