#!/usr/bin/env bash # The operations console: its fence, and its break-glass door. # # ops.threadcount.tech is served by the same Next process as the product, so the only thing # separating them is the host branch at the top of proxy.ts. The first half of this file is that # branch's test. The second half signs an operator in through the password door and proves the # three session kinds are not interchangeable. # # Two of the fence checks are regression guards rather than features, and they are the reason the # file exists: /my/signin and /m/login must keep answering on the product host. proxy.ts tests # `pathname.startsWith("/m")` and "/my" starts with "/m", so the staff app is safe only while no # matcher pattern matches /my. Widen the matcher to a catch-all and every wearer — and the # Play-shipped staff app — is redirected to the coordinator sign-in with no session they could # ever obtain. That failure is silent, total, and aimed at the surface with the most users. set -u B=${BASE:-http://127.0.0.1:3111} OPS=ops.threadcount.tech T=${TMP:-/tmp} OJ="$T/tc-ops-op.txt" # the operator's cookie jar CJ="$T/tc-ops-coord.txt" # a coordinator's, for the doctrine test FJ="$T/tc-ops-fake.txt" # the coordinator's cookie wearing the operator's name rm -f "$OJ" "$CJ" "$FJ" PASS=0; FAIL=0 ok() { PASS=$((PASS+1)); echo " ✓ $1"; } fail() { FAIL=$((FAIL+1)); echo " ✗ $1 :: $2"; } code() { curl -s -o /dev/null -w '%{http_code}' "$@"; } where() { curl -s -o /dev/null -w '%{redirect_url}' "$@"; } is() { local name=$1 want=$2 got=$3; if [ "$got" = "$want" ]; then ok "$name"; else fail "$name" "wanted $want, got $got"; fi; } check() { local name=$1 out=$2 pat=$3; if echo "$out" | grep -q "$pat"; then ok "$name"; else fail "$name" "$(echo "$out" | head -c 200)"; fi; } no() { local name=$1 out=$2 pat=$3; if echo "$out" | grep -q "$pat"; then fail "$name" "$(echo "$out" | head -c 200)"; else ok "$name"; fi; } py() { python3 -c "import sys,json; d=json.load(sys.stdin); $1"; } # Requests to the console carry the Host the proxy branches on, and an Origin that agrees with it # so lib/csrf.ts is exercised rather than sidestepped. # A fresh address per run: the sign-in route counts failures per address for fifteen minutes, in # the server's memory, and this file deliberately fails several sign-ins. Without this, a second # run inside the window trips the limiter — which is the limiter working, not the door failing. # The other suites do the same for signup; clientIp() takes the last forwarded entry. XFF="10.99.$((RANDOM%250)).$((RANDOM%250))" opost() { curl -s -b "$OJ" -c "$OJ" -X POST "$B$1" -H "Host: $OPS" -H "origin: http://$OPS" -H "x-forwarded-for: $XFF" -H 'content-type: application/json' -d "$2"; } echo "== the ops hostname serves nothing but its own door" is "the root goes to the console" 307 "$(code -H "Host: $OPS" "$B/")" is " and every product api is refused" 404 "$(code -H "Host: $OPS" "$B/api/health")" # Unconditional today, and on this hostname it would declare both Android apps authorised for # credential sharing — so a tapped ops link could open in the staff app. is " and .well-known/assetlinks.json" 404 "$(code -H "Host: $OPS" "$B/.well-known/assetlinks.json")" # This one mints a coordinator session and redirects to /app. is " and the demo door" 404 "$(code -H "Host: $OPS" "$B/api/auth/demo?as=admin")" is " and robots.txt" 404 "$(code -H "Host: $OPS" "$B/robots.txt")" echo "== the console's paths do not exist on the product hostname" is "/ops is refused" 404 "$(code "$B/ops")" is " and anything under it" 404 "$(code "$B/ops/facilities")" is " and the sign-in page" 404 "$(code "$B/ops/login")" is " and the sign-in route" 404 "$(code -X POST "$B/api/ops/auth/login" -H 'content-type: application/json' -d '{}')" echo "== the product is untouched" # Everything below passed before the host branch existed and must still pass. A 200 on the home # page also proves adding "/" to the matcher did not turn the marketing site into a redirect. is "the home page still answers" 200 "$(code "$B/")" is "health still answers" 200 "$(code "$B/api/health")" is "assetlinks still answers" 200 "$(code "$B/.well-known/assetlinks.json")" is "robots still answers" 200 "$(code "$B/robots.txt")" is "the coordinator sign-in answers" 200 "$(code "$B/m/login")" echo "== the staff app is untouched — the regression this file exists for" is "the wearer sign-in still answers" 200 "$(code "$B/my/signin")" MY=$(where "$B/my/signin") if [ -z "$MY" ]; then ok " and does not redirect anywhere"; else fail " and does not redirect anywhere" "redirects to $MY"; fi echo "== an unknown host is treated as the product, not as ops" # The safe default: the console is opt-in by exact match. A stray Host header must not open it, # and must not close the product either. is "an unknown host still gets the product" 200 "$(code -H "Host: example.invalid" "$B/")" is " and a near-miss host does too" 200 "$(code -H "Host: opsXthreadcountYtech" "$B/")" # The near-miss above is the exact string Next's `has: [{type:'host'}]` matcher would have # admitted, because it compiles its value as an unescaped regular expression. echo "== the break-glass door" TS=$(date +%s) OE="ops-$TS@example.com" # The seed script takes the password from the environment, never argv. check "an operator is seeded" "$(OPERATOR_PASSWORD='e2e-operator-password-1' node scripts/create-operator.cjs "$OE" "Ops Test" OWNER 2>&1)" "created operator $OE" is "the console redirects a stranger to sign in" 307 "$(code -H "Host: $OPS" "$B/ops")" check " to the sign-in page" "$(where -H "Host: $OPS" "$B/ops")" '/ops/login' is "the sign-in page answers" 200 "$(code -H "Host: $OPS" "$B/ops/login")" check "a wrong password is refused" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"wrong\"}")" 'match' is " with no cookie set" 307 "$(code -b "$OJ" -H "Host: $OPS" "$B/ops")" check "the right password signs in" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\"}")" '"ok":true' check " and the jar holds tc_ops" "$(cat "$OJ")" 'tc_ops' R=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops") check " and the console opens" "$R" 'Signed in' check " naming the operator" "$R" 'Ops Test' check " who has no second factor yet" "$R" 'not yet enrolled' is "signing out answers with a redirect" 303 "$(code -b "$OJ" -c "$OJ" -X POST -H "Host: $OPS" "$B/api/ops/auth/logout")" is " after which the console is closed" 307 "$(code -b "$OJ" -H "Host: $OPS" "$B/ops")" echo "== a coordinator's cookie is not an operator's" # The doctrine in lib/ops/session.ts: three cookies, three keys, three claim names. A coordinator # session presented under the operator cookie's name must fail the signature — and if it somehow # didn't, it has no `oid`. This test takes a real tc_session value and renames it. check "a coordinator signs up" "$(curl -s -c "$CJ" -X POST "$B/api/auth/signup" -H 'content-type: application/json' -H "x-forwarded-for: 10.12.$((RANDOM%250)).$((RANDOM%250))" -d "{\"first\":\"Ops\",\"last\":\"Doctrine\",\"facility\":\"Doctrine Hospital $TS\",\"email\":\"doctrine$TS@example.com\",\"password\":\"password123\"}")" '"ok":true' check " and holds tc_session" "$(cat "$CJ")" 'tc_session' sed 's/\ttc_session\t/\ttc_ops\t/' "$CJ" > "$FJ" check " which is renamed to tc_ops" "$(cat "$FJ")" 'tc_ops' is "the console refuses it" 307 "$(code -b "$FJ" -H "Host: $OPS" "$B/ops")" check " and sends it to sign in" "$(where -b "$FJ" -H "Host: $OPS" "$B/ops")" '/ops/login' # And the reverse: the operator's cookie opens nothing on the product. check "the operator signs in again" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\"}")" '"ok":true' is " and their cookie does not open the counter" 307 "$(code -b "$OJ" "$B/app")" echo "== the console shows counts, never a person" # The coordinator signed up above has a name and an email on their facility. The facility must # appear on the console by name; the person must not appear anywhere on it. This tests the # projection at the rendering layer — it holds even where the database cannot enforce roles. CN="Doctrine Hospital $TS" # Signup sets the coordinator's NAME on the facility but not an email, so a "no email on the # console" check would pass with nothing to catch. Give the facility a real one first — the # console must then leave it out on purpose, not by accident of the data. check "the coordinator records their email on the facility" "$(curl -s -b "$CJ" -c "$CJ" -X POST "$B/api/mutate" -H 'content-type: application/json' -d "{\"op\":\"settings.update\",\"payload\":{\"coordinatorEmail\":\"doctrine$TS@example.com\"}}")" '"ok":true' FL=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities") check "the facilities page lists the new facility" "$FL" "$CN" no " without the coordinator's email" "$FL" "doctrine$TS@example.com" no " or the coordinator's name" "$FL" 'Ops Doctrine' check " and calls it never set up" "$FL" 'Never set up' FID=$(echo "$FL" | grep -o "/ops/facilities/[a-z0-9]*\"[^>]*>$CN" | head -1 | sed 's#/ops/facilities/\([a-z0-9]*\).*#\1#') check " with a link to its page" "$FID" '^[a-z0-9]\{20,\}$' FD=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FID") check "the facility page opens" "$FD" "$CN" check " with the contacts masked" "$FD" 'cannot read these' no " and no email on it" "$FD" "doctrine$TS@example.com" no " and no name on it" "$FD" 'Ops Doctrine' check " and the coordinator counted" "$FD" '1 admin' is "a made-up facility id is not found" 404 "$(code -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/nope")" OV=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops") check "the overview lists it as needing attention" "$OV" "$CN" check " because it has no staff groups" "$OV" 'no staff groups named' check " and shows the migration count" "$OV" 'applied /' no " and no drift" "$OV" 'DRIFT' echo "== the second factor" # The operator is signed in from the section above. Enrolment goes through the real routes with a # live code computed by the product's own TOTP implementation (scripts/totp-code.ts). SETUP=$(opost /api/ops/auth/totp '{"action":"setup"}') check "setup returns a QR" "$SETUP" 'c.query(process.argv[1],process.argv.slice(2))).then(r=>{console.log(r.rows.map(x=>Object.values(x).join(" ")).join("\n"));return c.end()}).catch(e=>{console.error(e.message);process.exit(1)})' "$@"; } check "no reason is refused" "$(opost /api/ops/reveal "{\"facilityId\":\"$FID\"}")" 'Give a reason' check " and a short one too" "$(opost /api/ops/reveal "{\"facilityId\":\"$FID\",\"reason\":\"why\"}")" 'Give a reason' check " and a made-up facility" "$(opost /api/ops/reveal '{"facilityId":"abcdefghijklmnopqrstuvwxy","reason":"testing a facility that is not there"}')" 'No such facility' no " and nothing was granted" "$(sql 'SELECT count(*) FROM "RevealGrant" WHERE "facilityId"=$1' "$FID")" '[1-9]' FD=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FID") check "the page still masks the contacts" "$FD" 'cannot read these' check " and offers the reveal" "$FD" 'Reveal contacts' RV=$(opost /api/ops/reveal "{\"facilityId\":\"$FID\",\"reason\":\"e2e: checking the reveal path end to end\"}") check "a typed reason opens the window" "$RV" '"ok":true' check " for thirty minutes" "$RV" '"minutes":30' is "one grant row was written" 1 "$(sql 'SELECT count(*)::int FROM "RevealGrant" WHERE "facilityId"=$1' "$FID")" check " carrying the reason" "$(sql 'SELECT reason FROM "RevealGrant" WHERE "facilityId"=$1' "$FID")" 'checking the reveal path' is "one trail row was written" 1 "$(sql 'SELECT count(*)::int FROM "OperatorEvent" WHERE "facilityId"=$1 AND action=$2' "$FID" 'ops:reveal')" check " naming the facility, not the person" "$(sql 'SELECT subject FROM "OperatorEvent" WHERE "facilityId"=$1 AND action=$2' "$FID" 'ops:reveal')" "$CN" FD=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FID") check "the page now shows the email" "$FD" "doctrine$TS@example.com" check " and the coordinator's name" "$FD" 'Ops Doctrine' check " and says until when" "$FD" 'revealed until' check " and why" "$FD" 'checking the reveal path' no " and no longer offers the button" "$FD" 'Reveal contacts' no "the facilities list still masks it" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities")" "doctrine$TS@example.com" # Force the expiry: the window is a server-side row, so ending it is one update. A literal past # timestamp rather than now() - interval: the local PGlite server's now() is not on UTC. check "the window is ended in the database" "$(sql 'UPDATE "RevealGrant" SET "expiresAt" = $2 WHERE "facilityId"=$1 RETURNING 1' "$FID" '2000-01-01T00:00:00Z')" '1' FD=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FID") check "after expiry the contacts are masked again" "$FD" 'cannot read these' no " with no email on the page" "$FD" "doctrine$TS@example.com" no " and no name" "$FD" 'Ops Doctrine' check " and the reveal is offered again" "$FD" 'Reveal contacts' is "the trail row is still there" 1 "$(sql 'SELECT count(*)::int FROM "OperatorEvent" WHERE "facilityId"=$1 AND action=$2' "$FID" 'ops:reveal')" is "signing out after the reveal" 303 "$(code -b "$OJ" -c "$OJ" -X POST -H "Host: $OPS" "$B/api/ops/auth/logout")" echo "== the switches" # The sign-up and demo doors used to be environment variables; now they are one row the console # flips, and the product's own routes read that row. Close, prove the product refuses, reopen. CODE=$(TOTP_SECRET="$SECRET" npx tsx scripts/totp-code.ts) check "the operator signs in for the controls" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\",\"code\":\"$CODE\"}")" '"ok":true' CP=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/controls") check "the controls page opens" "$CP" 'Switches' check " with sign-ups open" "$CP" 'Close sign-ups' check "closing sign-ups" "$(opost /api/ops/controls '{"action":"switch","key":"signupsDisabled","value":true}')" '"ok":true' check " and the sign-up route refuses" "$(curl -s -X POST "$B/api/auth/signup" -H 'content-type: application/json' -H "x-forwarded-for: 10.13.$((RANDOM%250)).$((RANDOM%250))" -d "{\"first\":\"No\",\"last\":\"Body\",\"facility\":\"Shut $TS\",\"email\":\"shut$TS@example.com\",\"password\":\"password123\"}")" 'sign-ups are closed' no " and the sign-in page hides the form" "$(curl -s "$B/auth")" 'Create your facility' check " and the trail says who" "$(sql 'SELECT detail FROM "OperatorEvent" WHERE action=$1 AND subject=$2 ORDER BY at DESC LIMIT 1' 'ops:switch' 'signupsDisabled')" 'closed' check "reopening sign-ups" "$(opost /api/ops/controls '{"action":"switch","key":"signupsDisabled","value":false}')" '"ok":true' DJ="$T/tc-ops-del.txt"; rm -f "$DJ" DN="Deletable Hospital $TS" check " and a facility can sign up again" "$(curl -s -c "$DJ" -X POST "$B/api/auth/signup" -H 'content-type: application/json' -H "x-forwarded-for: 10.14.$((RANDOM%250)).$((RANDOM%250))" -d "{\"first\":\"Del\",\"last\":\"Etable\",\"facility\":\"$DN\",\"email\":\"deletable$TS@example.com\",\"password\":\"password123\"}")" '"ok":true' check "taking the demo out of service" "$(opost /api/ops/controls '{"action":"switch","key":"demoDisabled","value":true}')" '"ok":true' is " and the demo entry answers 404" 404 "$(code "$B/api/auth/demo?as=admin")" check " and the demo page says so" "$(curl -s "$B/demo")" 'closed for the moment' check "putting the demo back" "$(opost /api/ops/controls '{"action":"switch","key":"demoDisabled","value":false}')" '"ok":true' is " and the demo entry opens again" 303 "$(code "$B/api/auth/demo?as=admin")" check "resetting the demo on demand" "$(opost /api/ops/controls '{"action":"demo.reset"}')" '"ok":true' check " is in the trail" "$(sql 'SELECT count(*)::int FROM "OperatorEvent" WHERE action=$1' 'ops:demo.reset')" '[1-9]' check "an unknown switch is refused" "$(opost /api/ops/controls '{"action":"switch","key":"everything","value":true}')" 'Unknown switch' echo "== the billing scaffold" DFID=$(sql 'SELECT id FROM "Facility" WHERE name=$1' "$DN") check "the new facility has an id" "$DFID" '^[a-z0-9]\{20,\}$' check "a plan is recorded" "$(opost /api/ops/controls "{\"action\":\"plan\",\"facilityId\":\"$DFID\",\"act\":\"set\",\"plan\":\"health_service\",\"planNote\":\"e2e pilot until the end of the quarter\"}")" '"ok":true' DP=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$DFID") check " and shown on the facility page" "$DP" 'Health Service' check " with its note" "$DP" 'end of the quarter' check " and on the list" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities")" 'grandfathered' # The coordinator's two views of their own facility: the app shell (the snapshot is embedded in # its HTML) and the backup export. The console's note about them is the console's, not theirs — # the snapshot carries the plan's name and state (scripts/e2e-plan.sh), never the note. is "the coordinator's app opens" 200 "$(code -b "$DJ" "$B/app")" no " without the console's note in it" "$(curl -s -b "$DJ" "$B/app")" 'end of the quarter' no " nor in the backup export" "$(curl -s -b "$DJ" "$B/api/backup")" 'planNote' echo "== the danger zone" # Owner, the exact name, a live code. Each guard refuses on its own before anything is deleted. CODE=$(TOTP_SECRET="$SECRET" npx tsx scripts/totp-code.ts) check "the wrong name is refused" "$(opost /api/ops/controls "{\"action\":\"facility.delete\",\"facilityId\":\"$DFID\",\"confirm\":\"Deletable Hospital\",\"code\":\"$CODE\"}")" 'Type the facility name exactly' check "a wrong code is refused" "$(opost /api/ops/controls "{\"action\":\"facility.delete\",\"facilityId\":\"$DFID\",\"confirm\":\"$DN\",\"code\":\"000000\"}")" 'That code isn' is " and the facility is still there" 200 "$(code -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$DFID")" check "the exact name and a live code delete it" "$(opost /api/ops/controls "{\"action\":\"facility.delete\",\"facilityId\":\"$DFID\",\"confirm\":\"$DN\",\"code\":\"$CODE\"}")" '"deleted":"'"$DN"'"' is " and its page is gone" 404 "$(code -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$DFID")" no " and the list no longer names it" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities")" "$DN" is " and its coordinator's session is dead" 307 "$(code -b "$DJ" "$B/app")" is " and the trail keeps the record" 1 "$(sql 'SELECT count(*)::int FROM "OperatorEvent" WHERE "facilityId"=$1 AND action=$2' "$DFID" 'ops:facility.delete')" check " naming what it had" "$(sql 'SELECT detail FROM "OperatorEvent" WHERE "facilityId"=$1 AND action=$2' "$DFID" 'ops:facility.delete')" '1 coordinators' is "signing out after the controls" 303 "$(code -b "$OJ" -c "$OJ" -X POST -H "Host: $OPS" "$B/api/ops/auth/logout")" echo "== the version panel" # scripts/deploy.sh writes .release.json before the build; the overview reads it per request. Here # there is none, then one is written for a moment, then removed — the file is the box's, not the # repo's (gitignored). CODE=$(TOTP_SECRET="$SECRET" npx tsx scripts/totp-code.ts) check "the operator signs in for the version panel" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\",\"code\":\"$CODE\"}")" '"ok":true' check "with no release file the overview says so" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops")" 'not recorded' printf '{"sha":"e2e0abc","from":"e2e0aaa","at":"2026-09-12T01:02:03Z"}\n' > .release.json OVV=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops") rm -f .release.json check "with one it shows the deployed sha" "$OVV" 'e2e0abc' check " and what it replaced" "$OVV" 'was e2e0aaa' check " and what is running" "$OVV" 'release unknown\|STALE' is "the release file is gone again" 0 "$(ls .release.json 2>/dev/null | wc -l)" is "signing out after the version panel" 303 "$(code -b "$OJ" -c "$OJ" -X POST -H "Host: $OPS" "$B/api/ops/auth/logout")" echo "== single sign-on fails closed" # Locally the two Access variables are unset, so there is no SSO: the route must refuse every # assertion — including a well-formed forged one — and the sign-in page must neither hand off nor # loop. With the variables set (production) the same route verifies a real assertion against # Cloudflare's keys; that half is exercised on the box, not here. FORGED="eyJhbGciOiJSUzI1NiIsImtpZCI6Im5vcGUifQ.eyJlbWFpbCI6Im9wc0BleGFtcGxlLmNvbSIsImF1ZCI6WyJ4Il0sImlzcyI6Imh0dHBzOi8veCIsImV4cCI6NDEwMjQ0NDgwMH0.c2ln" is "the sso route with no assertion sends to the fire escape" 303 "$(code -H "Host: $OPS" "$B/api/ops/auth/sso")" check " at /ops/login?sso=failed" "$(where -H "Host: $OPS" "$B/api/ops/auth/sso")" 'sso=failed' check "a forged assertion is refused" "$(where -H "Host: $OPS" -H "cf-access-jwt-assertion: $FORGED" "$B/api/ops/auth/sso")" 'sso=failed' is " and sets no cookie" 307 "$(code -H "Host: $OPS" -H "cf-access-jwt-assertion: $FORGED" "$B/ops")" is "the sign-in page does not hand off without SSO configured" 200 "$(code -H "Host: $OPS" -H "cf-access-jwt-assertion: $FORGED" "$B/ops/login")" LP=$(curl -s -H "Host: $OPS" "$B/ops/login?sso=failed") check "after a failure the page shows the form" "$LP" 'ops-pw' check " and says why" "$LP" 'could not complete' check " and does not loop" "$(where -H "Host: $OPS" -H "cf-access-jwt-assertion: $FORGED" "$B/ops/login?sso=failed")" '^$' # The landing-path rule is tested directly: locally SSO never succeeds, so a request with a bad # next= would be refused for the assertion before the path was ever looked at. NX=$(npx tsx -e 'import { safeOpsNext as s } from "./lib/ops/cfAccess"; console.log([s("https://example.com"), s("//example.com/ops"), s("/ops/login?x=1"), s("/opsx"), s(""), s(null), s("/ops/facilities/abc")].join(" "))') is "after sign-on, only the console's own paths are landing places" "/ops /ops /ops /ops /ops /ops /ops/facilities/abc" "$NX" echo; echo "PASS=$PASS FAIL=$FAIL"; [ "$FAIL" -eq 0 ]