Skip to content

Commit 25d58ce

Browse files
motxxclaude
andcommitted
fix: wire oracleRegistry into regtest-cashu test service
The regtest bounty lifecycle test constructed a QueryService without an oracle registry. verifyWithQuorum returns "No oracle available" when resolveOracle returns null, so POST /queries/:id/result responds 400 and the "submit → release" flow fails. Mirror the production composition (reference-app.ts): inject createOracleRegistry() + createPreimageStore() + normalizeQueryResult into the test service, and pass the same registry + preimage store to buildWorkerApiApp so /oracles and HTLC routes behave as in prod. Also pin SHA refs on claude-code GitHub actions (supply-chain hardening from CEO review) and add the pentest suite README that documents scope/limits honestly. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
1 parent e96d165 commit 25d58ce

4 files changed

Lines changed: 98 additions & 6 deletions

File tree

.github/workflows/claude-code-review.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ jobs:
2727

2828
steps:
2929
- name: Checkout repository
30-
uses: actions/checkout@v4
30+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3131
with:
3232
fetch-depth: 1
3333

3434
- name: Run Claude Code Review
3535
id: claude-review
36-
uses: anthropics/claude-code-action@v1
36+
uses: anthropics/claude-code-action@c3d45e8e941e1b2ad7b278c57482d9c5bf1f35b3 # v1
3737
with:
3838
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
3939
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'

.github/workflows/claude.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ jobs:
2626
actions: read # Required for Claude to read CI results on PRs
2727
steps:
2828
- name: Checkout repository
29-
uses: actions/checkout@v4
29+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3030
with:
3131
fetch-depth: 1
3232

3333
- name: Run Claude Code
3434
id: claude
35-
uses: anthropics/claude-code-action@v1
35+
uses: anthropics/claude-code-action@c3d45e8e941e1b2ad7b278c57482d9c5bf1f35b3 # v1
3636
with:
3737
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
3838

e2e/pentest/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Pentest suite
2+
3+
Deterministic penetration tests for the Anchr worker API. All tests run
4+
against `localhost` only. Never connects to external services.
5+
6+
## What this suite IS
7+
8+
A **smoke-grade defense baseline.** Catches:
9+
10+
- Crashes (5xx) on malformed, oversized, or adversarial input
11+
- Unauthenticated access to write endpoints
12+
- Trivial rate-limit bypass via client-supplied headers
13+
- Preimage leakage on the unverified-result path
14+
- Prototype pollution reaching the test-process global
15+
- JSON-response content-type on routes that should never return HTML
16+
17+
## What this suite is NOT
18+
19+
A proof that defenses work. Most assertions use the shape `<500` — that
20+
only proves the server didn't crash. A malicious response that returns
21+
200 with leaked data would pass a `<500` assertion.
22+
23+
Specifically, the following attacks are **detected only if they crash
24+
the server**, not if they succeed silently:
25+
26+
| Attack class | Current assertion | What a real proof would need |
27+
|---|---|---|
28+
| SSRF | response body does not echo `"meta-data"` | listener bound to `169.254.169.254`, assert zero inbound connections during the test run |
29+
| Prototype pollution | test-process `{}.isAdmin` is undefined | server-process introspection endpoint that reveals prototype state (test-process check doesn't see server pollution) |
30+
| Store pollution / TTL cleanup | `expire-*` entries missing from `GET /queries` | admin endpoint exposing raw store size, to distinguish "cleaned up" from "filtered at read" |
31+
| Fuzz payloads (18 variants) | `<500` | per-payload assertion on expected rejection code (400 vs 413 vs 422) |
32+
| ReDoS in regex conditions | `<500` on creation | end-to-end verification path exercised with a time budget |
33+
34+
## Why ship a smoke-grade suite at all
35+
36+
Crash-free handling of adversarial input is a real property. Catching a
37+
regression that makes `POST /queries` panic on a 10 MB body is
38+
cheaper as a test than as a prod incident. The tests also guard against
39+
silent removal of the rate limiter, the write-auth middleware, or the
40+
preimage-release guard — all things an innocent refactor could break.
41+
42+
Treat green checks as "no crash, no obvious regression." Not as
43+
"hardened."
44+
45+
## Running locally
46+
47+
```bash
48+
# Starts a pentest server on port 8091 with HTTP_API_KEYS=pentest-key-001
49+
deno task test:all # includes pentest phase
50+
51+
# Or run only the pentest suite against an already-running server:
52+
PENTEST_APP_URL=http://localhost:8091 HTTP_API_KEYS=pentest-key-001 \
53+
deno task test:pentest
54+
```
55+
56+
## Adding new tests
57+
58+
Follow the existing file conventions:
59+
60+
- One file per attack class (`auth-bypass.test.ts`, `ssrf.test.ts`, ...)
61+
- Use `helpers.ts` for `APP_BASE`, `API_KEY`, `authedHeaders`
62+
- Always `await res.body?.cancel()` after `fetch()` to avoid leaks
63+
- Prefer specific assertions over `<500`. `<500` is the floor, not the goal.
64+
65+
## Known coverage gaps
66+
67+
Not currently exercised:
68+
69+
- `POST /queries/:id/upload` (file size, content-type smuggling, traversal on filename)
70+
- `POST /queries/:id/quotes` (Cashu quote fuzz)
71+
- `POST /marketplace/data/:id` (payment middleware: malformed token, replay, zero/negative amount)
72+
- `POST /marketplace/listings/:id/announce` (relay-publish input fuzz)
73+
- FROST authz: can signer_index N submit commitments for signer_index M?
74+
75+
These are higher marginal value than deepening assertions on
76+
already-covered endpoints.

e2e/regtest-cashu.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import { spawn } from "../src/runtime/mod.ts";
2626
import { type Proof, getEncodedToken } from "@cashu/cashu-ts";
2727
import { buildWorkerApiApp } from "../src/infrastructure/worker-api";
2828
import { createQueryService } from "../src/application/query-service";
29+
import { createOracleRegistry } from "../src/infrastructure/oracle/registry";
30+
import { createPreimageStore } from "../src/infrastructure/cashu/preimage-store";
31+
import { normalizeQueryResult } from "../src/infrastructure/attachments";
2932
import {
3033
checkInfraReady,
3134
payInvoiceViaLndUser,
@@ -52,10 +55,23 @@ async function mintCashuToken(amountSats: number): Promise<{ token: string; proo
5255
const suite = INFRA_READY ? describe : describe.ignore;
5356

5457
// Use a QueryService without relay hooks to avoid fire-and-forget WebSocket leaks.
55-
const testService = createQueryService({ hooks: {} });
58+
// Wire oracleRegistry + preimageStore so verification can actually succeed
59+
// (mirrors production composition in src/infrastructure/reference-app.ts).
60+
const testOracleRegistry = createOracleRegistry();
61+
const testPreimageStore = createPreimageStore();
62+
const testService = createQueryService({
63+
oracleRegistry: testOracleRegistry,
64+
preimageStore: testPreimageStore,
65+
normalizeResult: normalizeQueryResult,
66+
hooks: {},
67+
});
5668

5769
suite("e2e: regtest Cashu bounty lifecycle", () => {
58-
const app = buildWorkerApiApp({ queryService: testService });
70+
const app = buildWorkerApiApp({
71+
queryService: testService,
72+
oracleRegistry: testOracleRegistry,
73+
preimageStore: testPreimageStore,
74+
});
5975

6076
beforeAll(() => {
6177
testService.clearQueryStore();

0 commit comments

Comments
 (0)