ThreadCount Community edition

Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
ThreadCount
2026-09-13 08:45:19 +10:00
commit 1bc2de655a
505 changed files with 56223 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
#!/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" '<svg'
SECRET=$(echo "$SETUP" | py "print(d['secret'])")
check " and a secret" "$SECRET" '^[A-Z2-7]\{16,\}$'
check " but the password is still enough for now" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/api/ops/auth/totp")" '"enabled":false'
CODE=$(TOTP_SECRET="$SECRET" npx tsx scripts/totp-code.ts)
check "a wrong code does not enable it" "$(opost /api/ops/auth/totp '{"action":"enable","code":"000000"}')" 'That code isn'
ENABLE=$(opost /api/ops/auth/totp "{\"action\":\"enable\",\"code\":\"$CODE\"}")
check "a live code enables it" "$ENABLE" '"codes"'
RC=$(echo "$ENABLE" | py "print(d['codes'][0])")
check " handing back recovery codes" "$RC" '^[A-F0-9]\{5\}-[A-F0-9]\{5\}$'
check " and it now reports on" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/api/ops/auth/totp")" '"enabled":true'
no " and the landing page stops nagging" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops")" 'not yet enrolled'
is "signing out" 303 "$(code -b "$OJ" -c "$OJ" -X POST -H "Host: $OPS" "$B/api/ops/auth/logout")"
check "the password alone is no longer enough" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\"}")" 'needCode'
is " and sets no cookie" 307 "$(code -b "$OJ" -H "Host: $OPS" "$B/ops")"
check "a wrong code is refused" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\",\"code\":\"000000\"}")" 'That code isn'
CODE=$(TOTP_SECRET="$SECRET" npx tsx scripts/totp-code.ts)
check "password and a live code sign in" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\",\"code\":\"$CODE\"}")" '"ok":true'
is " opening the console" 200 "$(code -b "$OJ" -H "Host: $OPS" "$B/ops")"
is "signing out again" 303 "$(code -b "$OJ" -c "$OJ" -X POST -H "Host: $OPS" "$B/api/ops/auth/logout")"
check "password and a recovery code sign in" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\",\"code\":\"$RC\"}")" '"ok":true'
is "signing out once more" 303 "$(code -b "$OJ" -c "$OJ" -X POST -H "Host: $OPS" "$B/api/ops/auth/logout")"
# Spent in the same conditional update that found it: the second use must fail.
check "the same recovery code is refused the second time" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\",\"code\":\"$RC\"}")" 'already been used'
is " and the console stays closed" 307 "$(code -b "$OJ" -H "Host: $OPS" "$B/ops")"
echo "== revealing a coordinator's contacts"
# Sign back in with a live code, then walk the reveal: refused without a reason, granted with one,
# the contacts on the page, one grant row and one trail row behind it, and masked again the moment
# the grant expires — expiry is forced in the database, because the window is the row, not a
# cookie. The database queries below use the app role on purpose: they are the test's view of the
# console's own tables, not the console's.
CODE=$(TOTP_SECRET="$SECRET" npx tsx scripts/totp-code.ts)
check "the operator signs in for the reveal" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\",\"code\":\"$CODE\"}")" '"ok":true'
sql() { node -e 'require("dotenv/config");const{Client}=require("pg");const c=new Client({connectionString:process.env.DATABASE_URL});c.connect().then(()=>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 ]