The forge's container has no curl, so every scenario that talks to the private, events or Caddy servers failed there. test/http.ts is a small client with the curl options the scenarios use (-o -D -i -I -w -H -X -d -u --max-time, -s and -N), curl's output (the status line and headers as received, CRLF and all) and its exit codes (7 when nothing answers, 28 on --max-time, which private-events relies on). lib.sh calls it http, and wait_http and response use it, so the requests are the same on every host and the suite needs no curl at all. Curl stays out of the image, which needs it for nothing else.
- commit
- 18f6a757c8e0e2c96166361830122eb3ea9973a7
- parent
- 396feb70568544c0208d1c83dd0e78e28ba535c9
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 81s, signed by cat
- kind
- test risk score 1.8 of 3, Jev's estimate
- review
- #5 Pass the scenarios in the container, as root
ci attestation
forge-attestation 1 type ci commit 18f6a757c8e0e2c96166361830122eb3ea9973a7 tree b68155c82fb3f103ce302e54d894cdc25b534071 script .forge/ci e6f038ae810b87f90a4be115a762a9c91dd76a01 result pass exit 0 started 2026-10-18T22:58:45Z duration 81s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:c2ea633add157b1cdaa5cbbe48b709218e1c1fa384b2ab53ec3fac0397e54057 240 lines |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |8c3fdab running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |8c3fdab pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |8d4bdfd running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |8d4bdfd fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |3febabf running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |3febabf pass in 0s, signed |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |warning: You appear to have cloned an empty repository. |site home: deployed 171c730 -> /sites/ana/dotfiles/home/ |created ana/notes |remote: review #1: /ana/notes/reviews/1/ |pushed ana/todo |landing #1 A todo list: ana/todo -> main |c401d37 running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |c401d37 pass in 0s, signed |pushed main |3ae2aba running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |3ae2aba pass in 0s, signed |remote: review #7: /dami/greet/reviews/7/ |pushed dami/regions |created dami/forge |warning: You appear to have cloned an empty repository. |site guide: deployed 982f0cb -> /sites/dami/forge/guide/ |(node:58775) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/dotfiles in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:58899) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/notes in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:59038) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/forge in 0s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:59168) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/greet in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:59833) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built - in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |226 of 226 pages render the same without 11ty |guide: 4 files, links ok |ok
Signature: good, cat
@@ -0,0 +1,109 @@
1+// An HTTP client for scenarios, so they don't need curl (the forge's2+// container has none): the few curl options they use, with curl's output3+// and exit codes, over Node's own http client. test/lib.sh calls it `http`.4+//5+// node test/http.ts [options] <url>6+// -o <file> the body to file instead of stdout (- is stdout)7+// -D <file> the status line and headers to file (- is stdout)8+// -i the status line and headers before the body9+// -I a HEAD request, its status line and headers10+// -w <format> then format, with %{http_code} and %{redirect_url}11+// -H 'Name: value' a request header (more than one: repeat it)12+// -X <method> the request's method13+// -d <data> a POST body, form-encoded unless -H says otherwise14+// -u user:password basic auth15+// --max-time <s> give up after that long (exit 28, as curl)16+// -s, -N accepted, and what it does anyway: no progress, no buffering17+// Flags without a value may share a dash (-sN).18+// Exit 0 for any response, 7 when nothing answers, 28 on --max-time.19+import fs from 'node:fs';20+import http from 'node:http';21+22+const args = process.argv.slice(2).flatMap((a) => (/^-[sNiI]{2,}$/.test(a) ? [...a.slice(1)].map((c) => `-${c}`) : [a]));23+let url = '';24+let out = '-';25+let dump = '';26+let include = false;27+let head = false;28+let format = '';29+let method = '';30+let data: string | undefined;31+let maxTime = 0;32+const headers: [string, string][] = [];33+for (let i = 0; i < args.length; i++) {34+ const a = args[i];35+ const next = (): string => {36+ if (i + 1 >= args.length) usage(`${a} wants a value`);37+ return args[++i];38+ };39+ if (a === '-s' || a === '-N') continue;40+ else if (a === '-o') out = next();41+ else if (a === '-D') dump = next();42+ else if (a === '-i') include = true;43+ else if (a === '-I') head = true;44+ else if (a === '-w') format = next();45+ else if (a === '-X') method = next();46+ else if (a === '-d') data = next();47+ else if (a === '--max-time') maxTime = Number(next());48+ else if (a === '-u') headers.push(['Authorization', `Basic ${Buffer.from(next()).toString('base64')}`]);49+ else if (a === '-H') {50+ const h = next();51+ const c = h.indexOf(':');52+ if (c <= 0) usage(`not a header: ${h}`);53+ headers.push([h.slice(0, c).trim(), h.slice(c + 1).trim()]);54+ } else if (a.startsWith('-')) usage(`unknown option ${a}`);55+ else if (url) usage('one URL at a time');56+ else url = a;57+}58+if (!url) usage('no URL');59+60+function usage(why: string): never {61+ process.stderr.write(`test/http.ts: ${why}\n`);62+ process.exit(2);63+}64+65+const has = (name: string): boolean => headers.some(([k]) => k.toLowerCase() === name);66+if (data !== undefined) {67+ if (!has('content-type')) headers.push(['Content-Type', 'application/x-www-form-urlencoded']);68+ headers.push(['Content-Length', String(Buffer.byteLength(data))]);69+}70+if (!has('host')) headers.unshift(['Host', new URL(url).host]);71+if (!has('accept')) headers.push(['Accept', '*/*']);72+method ||= head ? 'HEAD' : data !== undefined ? 'POST' : 'GET';73+74+const fd = (f: string): number => (f === '-' ? 1 : fs.openSync(f, 'w'));75+const body = fd(out);76+const headFd = dump ? fd(dump) : -1;77+let code = 0;78+let location = '';79+80+function finish(exit: number): never {81+ if (format) {82+ const w = format83+ .replaceAll('%{http_code}', String(code).padStart(3, '0'))84+ .replaceAll('%{redirect_url}', location ? new URL(location, url).href : '')85+ .replaceAll('\\n', '\n');86+ fs.writeSync(1, w);87+ }88+ process.exit(exit);89+}90+91+if (maxTime > 0) setTimeout(() => finish(28), maxTime * 1000).unref();92+93+const req = http.request(url, { method, headers: headers.flat() }, (res) => {94+ code = res.statusCode ?? 0;95+ location = typeof res.headers.location === 'string' ? res.headers.location : '';96+ let lines = `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}\r\n`;97+ for (let i = 0; i < res.rawHeaders.length; i += 2) lines += `${res.rawHeaders[i]}: ${res.rawHeaders[i + 1]}\r\n`;98+ lines += '\r\n';99+ if (headFd >= 0) fs.writeSync(headFd, lines);100+ if (include || head) fs.writeSync(body, lines);101+ res.on('data', (d: Buffer) => {102+ if (!head) fs.writeSync(body, d);103+ });104+ res.on('end', () => finish(0));105+ res.on('error', () => finish(56));106+});107+req.on('error', () => finish(code ? 56 : 7));108+if (data !== undefined) req.write(data);109+req.end();@@ -20,7 +20,8 @@
2020 # sync_builds hooks and ssh commands build pages before returning2121 # lq "<sql>" forge lake query: the rows as CSV, no header2222 # private_server lib/private-server.ts for this forge, at $PRIV23-# response url [curl args] a whole HTTP response less its Date, to compare23+# http [curl options] url an HTTP request: curl's options (test/http.ts)24+# response url [http options] a whole HTTP response less its Date, to compare2425 #2526 # Assertions print what failed and stop the scenario:2627 # ok cmd... cmd succeeds fails cmd... cmd fails@@ -237,10 +238,14 @@ lq() {
237238 # free_port: a TCP port nothing listens on just now.238239 free_port() { node -e 'const s = require("net").createServer().listen(0, "127.0.0.1", () => { console.log(s.address().port); s.close(); })'; }239240 241+# http [options] url: the curl options scenarios use, from test/http.ts, so242+# they run where curl isn't installed (the forge's container).243+http() { node "$T_HOME/test/http.ts" "$@"; }244+240245 # wait_http url: until something answers there (5s at most).241246 wait_http() {242247 n=0243- until curl -s -o /dev/null "$1" || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done248+ until http -s -o /dev/null "$1" || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done244249 }245250 246251 # private_server: lib/private-server.ts on a free port, in the background,@@ -275,10 +280,10 @@ stop_servers() {
275280 T_PIDS=276281 }277282 278-# response url [curl args...]: the whole HTTP response (status line,283+# response url [http options...]: the whole HTTP response (status line,279284 # headers, body) less its Date, to compare byte for byte.280285 response() {281286 u=$1282287 shift283- curl -s -i "$@" "$u" | grep -av '^Date: '288+ http -s -i "$@" "$u" | grep -av '^Date: '284289 }@@ -38,6 +38,6 @@ exists "$r/.forge/identity.db"
3838 exists "$r/.forge/private/www/dami/secret/index.html"3939 export FORGE_ROOT=$r FORGE_WWW=$r/.forge/www4040 private_server41-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/")" 20041+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/")" 2004242 ok ssh_as dami tokens4343 has "$T_LOG" ' ci$'@@ -37,12 +37,12 @@ export FORGE_NOW=$T_NOW
3737 code=$(link ana)3838 ok forge user rm ana3939 fails_with 'HTTP 400: .*expired or has been used' auth enroll "$PRIV" "$code" "$T/ana.key"40-eq "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H 'Origin: https://forge.test' -d '{"code":"nonsense"}' "$PRIV/login/enroll/options")" 40040+eq "$(http -s -o /dev/null -w '%{http_code}' -X POST -H 'Origin: https://forge.test' -d '{"code":"nonsense"}' "$PRIV/login/enroll/options")" 4004141 4242 # No FORGE_BASE_URL, no sign-in: the page says why, and posts are refused.4343 stop_servers4444 unset FORGE_BASE_URL4545 private_server46-curl -s "$PRIV/login/" >"$T/page"46+http -s "$PRIV/login/" >"$T/page"4747 has "$T/page" "Sign-in isn't set up here"48-eq "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H 'Origin: http://localhost' -d '{}' "$PRIV/login/options")" 40348+eq "$(http -s -o /dev/null -w '%{http_code}' -X POST -H 'Origin: http://localhost' -d '{}' "$PRIV/login/options")" 403@@ -12,12 +12,12 @@ build
1212 export FORGE_BASE_URL=https://forge.test1313 private_server1414 code=$(ssh_as dami passkey quantum | sed -n 's/^ .*#enroll=//p')15-curl -s -X POST -H 'Origin: https://forge.test' -d "{\"code\":\"$code\"}" "$PRIV/login/enroll/options" >"$T/options"15+http -s -X POST -H 'Origin: https://forge.test' -d "{\"code\":\"$code\"}" "$PRIV/login/enroll/options" >"$T/options"1616 has "$T/options" '"pubKeyCredParams":\[\{"alg":-48,'1717 c=$(node "$T_HOME/test/authenticator.ts" enroll "$PRIV" "$code" "$T/dami.key" --alg ml-dsa-44) || fail "enroll"18-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$c" "$PRIV/dami/secret/")" 20018+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$c" "$PRIV/dami/secret/")" 2001919 c=$(node "$T_HOME/test/authenticator.ts" login "$PRIV" "$T/dami.key") || fail "login"20-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$c" "$PRIV/dami/secret/")" 20020+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$c" "$PRIV/dami/secret/")" 2002121 n=$(node -e '2222 const { DatabaseSync } = require("node:sqlite");2323 console.log(new DatabaseSync(process.argv[1], { readOnly: true }).prepare("SELECT length(public_key) AS n FROM passkeys").get().n)' "$FORGE_ROOT/.forge/identity.db" 2>/dev/null)@@ -15,7 +15,7 @@ build
1515 export FORGE_BASE_URL=https://forge.test1616 private_server1717 auth() { node "$T_HOME/test/authenticator.ts" "$@"; }18-page() { curl -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$1" "$PRIV/dami/secret/"; }18+page() { http -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$1" "$PRIV/dami/secret/"; }1919 2020 code=$(ssh_as dami passkey laptop | sed -n 's/^ .*#enroll=//p')2121 c1=$(auth enroll "$PRIV" "$code" "$T/dami.key") || fail "enroll"@@ -15,14 +15,14 @@ build
1515 export FORGE_BASE_URL=https://forge.test1616 private_server1717 auth() { node "$T_HOME/test/authenticator.ts" "$@"; }18-page() { curl -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$1" "$PRIV/dami/secret/"; }18+page() { http -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$1" "$PRIV/dami/secret/"; }1919 for k in one two; do2020 code=$(ssh_as dami passkey "$k" | sed -n 's/^ .*#enroll=//p')2121 ok auth enroll "$PRIV" "$code" "$T/$k.key"2222 done2323 2424 c=$(auth login "$PRIV" "$T/one.key")25-curl -s -D "$T/h" -o "$T/login" -H "Cookie: __Host-forge=$c" "$PRIV/login/"25+http -s -D "$T/h" -o "$T/login" -H "Cookie: __Host-forge=$c" "$PRIV/login/"2626 lacks "$T/h" '^Set-Cookie'2727 has "$T/h" "^Content-Security-Policy: default-src 'self'"2828 has "$T/h" '^Cache-Control: private, no-store'@@ -38,7 +38,7 @@ grep -q 'name="csrf" value="[A-Za-z0-9_-]\{32\}"' "$T/login" || fail "no csrf to
3838 stop_servers3939 export FORGE_NOW=$((T_NOW + 3660))4040 private_server41-curl -s -D "$T/h" -o /dev/null -H "Cookie: __Host-forge=$c" "$PRIV/dami/secret/"41+http -s -D "$T/h" -o /dev/null -H "Cookie: __Host-forge=$c" "$PRIV/dami/secret/"4242 has "$T/h" '^HTTP/1.1 200'4343 has "$T/h" "^Set-Cookie: __Host-forge=$c; Path=/; Max-Age=2592000; Secure; HttpOnly; SameSite=Lax"4444 ok ssh_as dami sessions@@ -58,13 +58,13 @@ private_server
5858 5959 # Sign-out: only from the forge's page, with its token.6060 c=$(auth login "$PRIV" "$T/one.key")61-out() { curl -s -o /dev/null -w '%{http_code}' -X POST -H "Cookie: __Host-forge=$c" "$@" "$PRIV/login/logout"; }62-csrf=$(curl -s -H "Cookie: __Host-forge=$c" "$PRIV/login/" | sed -n 's/.*name="csrf" value="\([^"]*\)".*/\1/p')61+out() { http -s -o /dev/null -w '%{http_code}' -X POST -H "Cookie: __Host-forge=$c" "$@" "$PRIV/login/logout"; }62+csrf=$(http -s -H "Cookie: __Host-forge=$c" "$PRIV/login/" | sed -n 's/.*name="csrf" value="\([^"]*\)".*/\1/p')6363 eq "$(out -H 'Origin: https://evil.test' -d "csrf=$csrf")" 4036464 eq "$(out -d "csrf=$csrf")" 4036565 eq "$(out -H 'Origin: https://forge.test' -d 'csrf=nope')" 4036666 eq "$(page "$c")" 20067-curl -s -o /dev/null -D "$T/h" -X POST -H "Cookie: __Host-forge=$c" -H 'Origin: https://forge.test' -d "csrf=$csrf" "$PRIV/login/logout"67+http -s -o /dev/null -D "$T/h" -X POST -H "Cookie: __Host-forge=$c" -H 'Origin: https://forge.test' -d "csrf=$csrf" "$PRIV/login/logout"6868 has "$T/h" '^HTTP/1.1 303'6969 has "$T/h" '^Location: /login/'7070 has "$T/h" '^Set-Cookie: __Host-forge=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax'@@ -32,10 +32,10 @@ if grep -rlq 'dami/secret' "$FORGE_WWW"; then fail "www names the private repo";
3232 private_server3333 tok=$(ssh_as dami token --repo dami/secret 2>/dev/null)3434 cat_tok=$(ssh_as cat token 2>/dev/null)35-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/insights/")" 20036-curl -s -H "Authorization: Bearer $tok" "$PRIV/dami/secret/insights/insights.json" >"$T/got"35+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/insights/")" 20036+http -s -H "Authorization: Bearer $tok" "$PRIV/dami/secret/insights/insights.json" >"$T/got"3737 ok cmp "$p/insights.json" "$T/got"38-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/insights/ci.parquet")" 20038+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/insights/ci.parquet")" 2003939 response "$PRIV/dami/nope/insights/insights.json" >"$T/missing"4040 for a in "" "$cat_tok"; do4141 response "$PRIV/dami/secret/insights/insights.json" ${a:+-H "Authorization: Bearer $a"} >"$T/got"@@ -17,17 +17,17 @@ FORGE_STATE=$FORGE_ROOT/.forge FORGE_EVENTS_PORT=$port node "$T_HOME/lib/events-
1717 T_PIDS="$T_PIDS $!"1818 es=http://127.0.0.1:$port/events1919 n=020-until curl -s -o /dev/null "$es/x/y/" || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done20+until http -s -o /dev/null "$es/x/y/" || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done2121 22-code() { curl -s -o /dev/null -w '%{http_code}' --max-time 1 "$@" || true; }22+code() { http -s -o /dev/null -w '%{http_code}' --max-time 1 "$@" || true; }2323 eq "$(code "$es/dami/secret/")" 4042424 eq "$(code "$es/dami/nope/")" 4042525 eq "$(code "$es/dami/open/")" 2002626 eq "$(code "$es/")" 2002727 28-curl -sN --max-time 4 "$es/" >"$T/all" &28+http -sN --max-time 4 "$es/" >"$T/all" &2929 all=$!30-curl -sN --max-time 4 "$es/dami/open/" >"$T/open" &30+http -sN --max-time 4 "$es/dami/open/" >"$T/open" &3131 one=$!3232 T_PIDS="$T_PIDS $all $one"3333 sleep 1@@ -39,7 +39,7 @@ sleep 1
3939 rm "$FORGE_ROOT/dami/open.git/git-daemon-export-ok"4040 push dami/open after4141 # The watcher of dami/open is let go before its 4 seconds are up.42-if wait "$one"; then :; else fail "the stream of dami/open wasn't closed (curl exited $?)"; fi42+if wait "$one"; then :; else fail "the stream of dami/open wasn't closed (http exited $?)"; fi4343 wait "$all" || true4444 has "$T/all" '"refs":"refs/heads/before"'4545 lacks "$T/all" 'dami/secret'@@ -62,9 +62,9 @@ eq "$(code -H "Cookie: __Host-forge=$ana" "$es/dami/secret/")" 200
6262 eq "$(code -H "Cookie: __Host-forge=$cat" "$es/dami/secret/")" 4046363 eq "$(code -H 'Cookie: __Host-forge=nonsense' "$es/dami/secret/")" 4046464 eq "$(code -H "Cookie: __Host-forge=$ana" "$es/dami/nope/")" 40465-curl -sN --max-time 4 -H "Cookie: __Host-forge=$ana" "$es/dami/secret/" >"$T/ana" &65+http -sN --max-time 4 -H "Cookie: __Host-forge=$ana" "$es/dami/secret/" >"$T/ana" &6666 one=$!67-curl -sN --max-time 4 -H "Cookie: __Host-forge=$cat" "$es/" >"$T/cat" &67+http -sN --max-time 4 -H "Cookie: __Host-forge=$cat" "$es/" >"$T/cat" &6868 all=$!6969 T_PIDS="$T_PIDS $one $all"7070 sleep 1@@ -72,7 +72,7 @@ push dami/secret for-readers ',"private":true'
7272 sleep 17373 ok ssh_as dami readers dami/secret rm ana7474 push dami/secret after-rm ',"private":true'75-if wait "$one"; then :; else fail "ana's stream of dami/secret wasn't closed (curl exited $?)"; fi75+if wait "$one"; then :; else fail "ana's stream of dami/secret wasn't closed (http exited $?)"; fi7676 wait "$all" || true7777 has "$T/ana" '"refs":"refs/heads/for-readers"'7878 lacks "$T/ana" 'after-rm'@@ -90,5 +90,5 @@ same -u "ana:$ana_open"
9090 same -u "ana:$revoked"9191 has "$T/got" '^HTTP/1.1 401'9292 # Nothing else in a private repo is served, token or not.93-eq "$(curl -s -o /dev/null -w '%{http_code}' -u "ana:$ana" "$c/dami/secret.git/config")" 40494-eq "$(curl -s -o /dev/null -w '%{http_code}' -u "ana:$ana" "$c/dami/secret.git/git-receive-pack")" 40493+eq "$(http -s -o /dev/null -w '%{http_code}' -u "ana:$ana" "$c/dami/secret.git/config")" 40494+eq "$(http -s -o /dev/null -w '%{http_code}' -u "ana:$ana" "$c/dami/secret.git/git-receive-pack")" 404@@ -29,7 +29,7 @@ signin() {
2929 }3030 ana=$(signin ana) || fail "ana"3131 cat=$(signin cat) || fail "cat"32-as() { u=$1; shift; curl -s -H "Cookie: __Host-forge=$u" "$@"; }32+as() { u=$1; shift; http -s -H "Cookie: __Host-forge=$u" "$@"; }3333 3434 eq "$(as "$ana" -o /dev/null -w '%{http_code}' "$PRIV/dami/secret/")" 2003535 as "$ana" "$PRIV/dami/secret/" | grep -q 'hidden thing' || fail "ana doesn't see dami/secret"@@ -53,15 +53,15 @@ lacks "$T/list" '<b>(own|open)</b>'
5353 as "$cat" "$PRIV/private/" >"$T/list"5454 has "$T/list" '<b>own</b>'5555 lacks "$T/list" '<b>(secret|unbuilt|open)</b>'56-eq "$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$PRIV/private/")" "303 $PRIV/login/?next=/private/"57-curl -s "$PRIV/login/?next=/dami/secret/" >"$T/page"56+eq "$(http -s -o /dev/null -w '%{http_code} %{redirect_url}' "$PRIV/private/")" "303 $PRIV/login/?next=/private/"57+http -s "$PRIV/login/?next=/dami/secret/" >"$T/page"5858 has "$T/page" 'data-next="/dami/secret/"'59-curl -s "$PRIV/login/?next=//evil.test/" >"$T/page"59+http -s "$PRIV/login/?next=//evil.test/" >"$T/page"6060 has "$T/page" 'data-next=""'6161 6262 # Its scripts and styles, from the private server itself when www lacks them.6363 for a in $(grep -oE '(href|src)="/assets/[^"]+"' "$T/page" | sed -E 's/^[a-z]+="//; s/"$//' | sort -u); do64- eq "$(curl -s -o /dev/null -w '%{http_code}' "$PRIV$a")" 20064+ eq "$(http -s -o /dev/null -w '%{http_code}' "$PRIV$a")" 2006565 done6666 grep -q '/assets/webauthn\.[A-Za-z0-9_-]*\.js' "$T/page" || fail "no webauthn script on the sign-in page"6767 grep -q '/assets/login\.[A-Za-z0-9_-]*\.js' "$T/page" || fail "no login script on the sign-in page"@@ -32,10 +32,10 @@ u=$PRIV/dami/secret/
3232 3333 # Readers.3434 for t in "$dami" "$ana"; do35- eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $t" "$u")" 20035+ eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $t" "$u")" 2003636 done37-eq "$(curl -s -o /dev/null -w '%{http_code}' -u "ana:$ana" "$u")" 20038-eq "$(curl -s -o /dev/null -w '%{http_code}' -u "$ana:" "$u")" 20037+eq "$(http -s -o /dev/null -w '%{http_code}' -u "ana:$ana" "$u")" 20038+eq "$(http -s -o /dev/null -w '%{http_code}' -u "$ana:" "$u")" 2003939 response "$u" -H "Authorization: Bearer $dami" >"$T/page"4040 has "$T/page" '^HTTP/1.1 200 OK'4141 has "$T/page" '^Cache-Control: private, no-cache'@@ -44,9 +44,9 @@ has "$T/page" '^ETag: "'
4444 has "$T/page" '^Last-Modified: '4545 has "$T/page" 'hidden thing'4646 etag=$(sed -n 's/^ETag: //p' "$T/page" | tr -d '\r')47-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" -H "If-None-Match: $etag" "$u")" 30448-eq "$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret")" "308 $PRIV/dami/secret/"49-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret/search.json")" 20047+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" -H "If-None-Match: $etag" "$u")" 30448+eq "$(http -s -o /dev/null -w '%{http_code} %{redirect_url}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret")" "308 $PRIV/dami/secret/"49+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret/search.json")" 2005050 5151 # Everyone and everything else: the same 404.5252 response "$PRIV/dami/nope/" >"$T/missing"@@ -84,7 +84,7 @@ stop_servers
8484 export FORGE_NOW=$((T_NOW + 3700))8585 private_server8686 same "$PRIV/dami/secret/" -H "Authorization: Bearer $short" -I87-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret/")" 20087+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret/")" 2008888 ok ssh_as dami tokens8989 eq "$(grep -c ' token$' "$T_LOG")" 19090 has "$T_LOG" ' 2026-09-01 01:01 +token$'@@ -30,26 +30,26 @@ export FORGE_BASE_URL=https://forge.test
3030 private_server3131 u=$PRIV/dami/secret/releases/download/v1/tool.html3232 tok=$(ssh_as ana token --repo dami/secret 2>/dev/null)33-curl -s -D "$T/h" -o "$T/got" -u "ana:$tok" "$u"33+http -s -D "$T/h" -o "$T/got" -u "ana:$tok" "$u"3434 has "$T/h" '^HTTP/1.1 200'3535 has "$T/h" '^Content-Disposition: attachment'3636 has "$T/h" '^X-Content-Type-Options: nosniff'3737 has "$T/h" '^Cache-Control: private, no-cache'3838 has "$T/h" '^Accept-Ranges: bytes'3939 ok cmp "$T/f/tool.html" "$T/got"40-curl -s -D "$T/h" -o "$T/part" -H "Authorization: Bearer $tok" -H 'Range: bytes=7-10' "$u"40+http -s -D "$T/h" -o "$T/part" -H "Authorization: Bearer $tok" -H 'Range: bytes=7-10' "$u"4141 has "$T/h" '^HTTP/1.1 206'4242 has "$T/h" "^Content-Range: bytes 7-10/$(wc -c <"$T/f/tool.html" | tr -d ' ')"4343 has "$T/h" '^Content-Disposition: attachment'4444 eq "$(cat "$T/part")" "tool"4545 etag=$(sed -n 's/^ETag: //p' "$T/h" | tr -d '\r')46-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" -H "If-None-Match: $etag" "$u")" 30447-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/releases/download/v1/SHA256SUMS.sig")" 20046+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" -H "If-None-Match: $etag" "$u")" 30447+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/releases/download/v1/SHA256SUMS.sig")" 2004848 4949 # Signed in with a passkey.5050 code=$(ssh_as ana passkey | sed -n 's/^ .*#enroll=//p')5151 ana=$(node "$T_HOME/test/authenticator.ts" enroll "$PRIV" "$code" "$T/ana.key") || fail "ana can't sign in"52-curl -s -D "$T/h" -o "$T/got" -H "Cookie: __Host-forge=$ana" -H 'Range: bytes=0-5' "$u"52+http -s -D "$T/h" -o "$T/got" -H "Cookie: __Host-forge=$ana" -H 'Range: bytes=0-5' "$u"5353 has "$T/h" '^HTTP/1.1 206'5454 has "$T/h" '^Content-Disposition: attachment'5555 has "$T/h" '^X-Content-Type-Options: nosniff'