#!/usr/bin/env bash
# Regression test for cf-token.sh. Stubs curl via a PATH shim backed by a state
# file so mint -> verify sequencing and the post-mint 10000 window are simulated
# offline. No live Cloudflare account is touched.
#
# Covers:
#   1. cfat_ master mints pages-d1, routes verify/perm/mint to /accounts/...
#   2. cfut_ master routes to /user/... and never to /accounts/...
#   3. an already-persisted canonical key is reused with no mint call
#   4. a legacy key (CF_PAGES_TOKEN) is reused and reported as the used key
#   5. the post-mint 10000 window is retried, then verify passes
#   6. access poison duplicate-id: first id 10000 on functional read -> alt id
#   7. an unrecognised master prefix fails closed
#   8. a real failure (non-transient verify code) exits non-zero, code surfaced
#   9/10. no token value ever reaches stdout/stderr; stdout is exactly the key
set -u

HELPER="$(cd "$(dirname "$0")/.." && pwd)/cf-token.sh"
[ -x "$HELPER" ] || { echo "FAIL: $HELPER not executable" >&2; exit 1; }

PASS=0; FAIL=0
SECRET_VALUE="cfat_minted_SECRET_do_not_leak_0123456789"   # fake minted token value

ok()   { echo "PASS: $1"; PASS=$((PASS+1)); }
bad()  { echo "FAIL: $1" >&2; FAIL=$((FAIL+1)); }

# Build a scratch sandbox: a fake-curl dir on PATH + an account-style secrets file.
# $1 = master token value (its prefix selects cfat_/cfut_ routing).
setup() {
  SANDBOX=$(mktemp -d)
  BIN="$SANDBOX/bin"; mkdir -p "$BIN"
  SDIR="$SANDBOX/acme-code/data/accounts/acc123/secrets"; mkdir -p "$SDIR"
  SECRETS="$SDIR/cloudflare.env"
  ( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "$1" "acc123" > "$SECRETS" )
  : > "$SANDBOX/curl.log"
  install_default_curl
  PATH="$BIN:$PATH"; export PATH
}
teardown() { rm -rf "$SANDBOX"; }

# Default fake curl: a healthy account with all permission groups; mint -> value;
# verify -> active; functional access read -> ok.
install_default_curl() {
  cat > "$BIN/curl" <<CURL
#!/usr/bin/env bash
SB="\$(cd "\$(dirname "\$0")/.." && pwd)"
echo "\$@" >> "\$SB/curl.log"
url=""; for a in "\$@"; do case "\$a" in https://*) url="\$a";; esac; done
case "\$url" in
  *"/tokens/permission_groups"*)
    echo '{"success":true,"result":[{"id":"pg_pages","name":"Pages Write"},{"id":"pg_d1","name":"D1 Write"},{"id":"pg_r2","name":"Workers R2 Storage Write"},{"id":"pg_dns","name":"DNS Write"},{"id":"pg_acc1","name":"Access: Apps and Policies Write"},{"id":"pg_acc2","name":"Access: Apps and Policies Write"}]}'
    ;;
  *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
  *"/tokens"*)        echo '{"success":true,"result":{"id":"tok1","value":"'"$SECRET_VALUE"'"}}';;
  *"/access/apps"*)   echo '{"success":true,"result":[]}';;
  *) echo '{"success":false,"errors":[{"code":9999,"message":"unmapped"}]}';;
esac
CURL
  chmod +x "$BIN/curl"
}

# Run the helper. $1 name $2 scope $3 expected_exit. Sets OUT/ERR/RC.
run() {
  local name="$1" scope="$2" exp="$3" of ef
  of=$(mktemp); ef=$(mktemp)
  CF_BACKOFF_SECONDS=0 bash "$HELPER" "$scope" "$SECRETS" >"$of" 2>"$ef"
  RC=$?
  OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
  [ "$RC" -eq "$exp" ] && ok "$name (exit=$RC)" || { bad "$name (exit=$RC want $exp)"; echo "  stderr: $ERR" >&2; }
}

no_leak() {  # assert no fake token value appears in OUT or ERR
  if printf '%s%s' "$OUT" "$ERR" | grep -qE "SECRET|BADtoken|GOODtoken"; then bad "$1 leaked a token value"
  else ok "$1 no token leak"; fi
}

# ---------------------------------------------------------------------------
# 1. cfat_ master mints pages-d1, routes to /accounts/...
setup "cfat_master_abc"
run "cfat pages mint" pages 0
grep -q "/accounts/acc123/tokens" "$SANDBOX/curl.log" && ok "cfat routes accounts" || bad "cfat routing"
[ "$OUT" = "CF_PAGES_D1_TOKEN" ] && ok "stdout is canonical key" || bad "stdout='$OUT'"
grep -q "^CF_PAGES_D1_TOKEN=" "$SECRETS" && ok "persisted canonical key" || bad "not persisted"
echo "$ERR" | grep -q "op=resolve scope=pages master=cfat action=mint result=active code=0" && ok "mint lifeline verbatim" || bad "lifeline: $ERR"
no_leak "cfat mint"
teardown

# 2. cfut_ master routes to /user/... only
setup "cfut_master_xyz"
run "cfut pages mint" pages 0
grep -q "/user/tokens" "$SANDBOX/curl.log" && ok "cfut routes user" || bad "cfut routing"
grep -q "/accounts/acc123/tokens" "$SANDBOX/curl.log" && bad "cfut hit account endpoint" || ok "no account route for cfut"
echo "$ERR" | grep -q "master=cfut" && ok "cfut lifeline tag" || bad "lifeline tag: $ERR"
teardown

# 3. reuse: pre-persist canonical key -> no mint POST
setup "cfat_master_abc"
( umask 077; printf 'CF_PAGES_D1_TOKEN=%s\n' "cfat_already_here" >> "$SECRETS" )
run "reuse no mint" pages 0
grep -q -- "-X POST" "$SANDBOX/curl.log" && bad "reuse path minted" || ok "reuse no mint call"
echo "$ERR" | grep -q "action=reuse result=active" && ok "reuse lifeline" || bad "reuse lifeline: $ERR"
[ "$OUT" = "CF_PAGES_D1_TOKEN" ] && ok "reuse reports canonical key" || bad "reuse out='$OUT'"
teardown

# 4. legacy key reuse: CF_PAGES_TOKEN present -> reused, reported as legacy key
setup "cfat_master_abc"
( umask 077; printf 'CF_PAGES_TOKEN=%s\n' "cfat_legacy_here" >> "$SECRETS" )
run "legacy reuse" pages 0
grep -q -- "-X POST" "$SANDBOX/curl.log" && bad "legacy reuse minted" || ok "legacy reuse no mint"
[ "$OUT" = "CF_PAGES_TOKEN" ] && ok "reports legacy key" || bad "legacy out='$OUT'"
teardown

# 5. post-mint 10000 window then active
setup "cfat_master_abc"
cat > "$BIN/curl" <<'CURL'
#!/usr/bin/env bash
SB="$(cd "$(dirname "$0")/.." && pwd)"
echo "$@" >> "$SB/curl.log"
url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
case "$url" in
  *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_pages","name":"Pages Write"},{"id":"pg_d1","name":"D1 Write"}]}';;
  *"/tokens/verify"*)
     n=$(cat "$SB/vcount" 2>/dev/null || echo 0); n=$((n+1)); echo "$n" > "$SB/vcount"
     if [ "$n" -ge 3 ]; then echo '{"success":true,"result":{"status":"active"}}'
     else echo '{"success":false,"errors":[{"code":10000,"message":"Authentication error"}]}'; fi;;
  *"/tokens"*) echo '{"success":true,"result":{"value":"cfat_minted_SECRET_do_not_leak_0123456789"}}';;
  *) echo '{"success":false,"errors":[{"code":9999}]}';;
esac
CURL
chmod +x "$BIN/curl"; : > "$SANDBOX/vcount"
run "10000 retry then active" pages 0
no_leak "10000 retry"
teardown

# 6. access poison id: first candidate's functional read returns 10000, second is fine
setup "cfat_master_abc"
cat > "$BIN/curl" <<'CURL'
#!/usr/bin/env bash
SB="$(cd "$(dirname "$0")/.." && pwd)"
echo "$@" >> "$SB/curl.log"
url=""; data=""; hdr=""; prev=""
for a in "$@"; do
  case "$a" in https://*) url="$a";; esac
  [ "$prev" = "--data" ] && data="$a"
  [ "$prev" = "-H" ] && hdr="$hdr $a"
  prev="$a"
done
case "$url" in
  *"/permission_groups"*) echo '{"success":true,"result":[{"id":"acc_bad","name":"Access: Apps and Policies Write"},{"id":"acc_good","name":"Access: Apps and Policies Write"}]}';;
  *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
  *"/tokens"*)
     case "$data" in
       *acc_bad*)  echo '{"success":true,"result":{"value":"cfat_BADtoken_0"}}';;
       *acc_good*) echo '{"success":true,"result":{"value":"cfat_GOODtoken_1"}}';;
       *) echo '{"success":true,"result":{"value":"cfat_x"}}';; esac;;
  *"/access/apps"*)
     case "$hdr" in *BADtoken*) echo '{"success":false,"errors":[{"code":10000}]}';;
                    *) echo '{"success":true,"result":[]}';; esac;;
  *) echo '{"success":false,"errors":[{"code":9999}]}';;
esac
CURL
chmod +x "$BIN/curl"
run "access poison retried to good id" access 0
grep -q "acc_good" "$SANDBOX/curl.log" && ok "minted with good id" || bad "never tried good id"
no_leak "access poison"
teardown

# 7. unknown prefix -> fail closed
setup "v1.0-legacyapikeystyle"
run "unknown prefix fail closed" pages 1
grep -q -- "-X POST" "$SANDBOX/curl.log" && bad "unknown prefix still minted" || ok "unknown prefix no mint"
teardown

# 8. real failure: verify returns a non-transient code -> non-zero, surfaced, masked
setup "cfat_master_abc"
cat > "$BIN/curl" <<'CURL'
#!/usr/bin/env bash
SB="$(cd "$(dirname "$0")/.." && pwd)"
echo "$@" >> "$SB/curl.log"
url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
case "$url" in
  *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_pages","name":"Pages Write"},{"id":"pg_d1","name":"D1 Write"}]}';;
  *"/tokens/verify"*) echo '{"success":false,"errors":[{"code":1001,"message":"bad"}]}';;
  *"/tokens"*) echo '{"success":true,"result":{"value":"cfat_minted_SECRET_do_not_leak_0123456789"}}';;
  *) echo '{"success":false,"errors":[{"code":9999}]}';;
esac
CURL
chmod +x "$BIN/curl"
run "real failure non-zero" pages 1
echo "$ERR" | grep -q "code=1001" && ok "failure code surfaced" || bad "code not surfaced: $ERR"
no_leak "real failure"
teardown

# 9. pages exact-name: three custom-pages decoys ordered BEFORE the real group.
#    The old fuzzy /pages.*(write|edit)/ + head -1 picks "Access: Custom Pages Write";
#    exact-name must mint the real Pages Write + D1 Write ids and none of the decoys.
setup "cfat_master_abc"
cat > "$BIN/curl" <<'CURL'
#!/usr/bin/env bash
SB="$(cd "$(dirname "$0")/.." && pwd)"
echo "$@" >> "$SB/curl.log"
url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
case "$url" in
  *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_x1","name":"Access: Custom Pages Write"},{"id":"pg_x2","name":"Account Custom Pages Write"},{"id":"pg_x3","name":"Custom Pages Write"},{"id":"pg_pages_real","name":"Pages Write"},{"id":"pg_d1_real","name":"D1 Write"}]}';;
  *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
  *"/tokens"*) echo '{"success":true,"result":{"value":"cfat_minted_SECRET_do_not_leak_0123456789"}}';;
  *) echo '{"success":false,"errors":[{"code":9999}]}';;
esac
CURL
chmod +x "$BIN/curl"
run "pages exact-name skips custom-pages decoys" pages 0
POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
echo "$POSTLINE" | grep -q "pg_pages_real" && ok "minted real Pages Write id" || bad "did not mint real pages id: $POSTLINE"
echo "$POSTLINE" | grep -q "pg_d1_real" && ok "minted real D1 Write id" || bad "did not mint real d1 id"
echo "$POSTLINE" | grep -qE "pg_x1|pg_x2|pg_x3" && bad "minted a custom-pages decoy id: $POSTLINE" || ok "no custom-pages decoy id minted"
no_leak "pages exact-name"
teardown

# 10. dns exact-name on cfat_: skip the "Account DNS Settings Write" / "DNS Firewall
#     Write" decoys, mint the real "DNS Write" id, AND nest the zone resource under
#     the account resource (account-owned masters reject the flat zone.* with 1001).
setup "cfat_master_abc"
cat > "$BIN/curl" <<'CURL'
#!/usr/bin/env bash
SB="$(cd "$(dirname "$0")/.." && pwd)"
echo "$@" >> "$SB/curl.log"
url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
case "$url" in
  *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_dnsx1","name":"Account DNS Settings Write"},{"id":"pg_dnsx2","name":"DNS Firewall Write"},{"id":"pg_dns_real","name":"DNS Write"}]}';;
  *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
  *"/tokens"*) echo '{"success":true,"result":{"value":"cfat_minted_SECRET_do_not_leak_0123456789"}}';;
  *) echo '{"success":false,"errors":[{"code":9999}]}';;
esac
CURL
chmod +x "$BIN/curl"
run "dns exact-name cfat nests zone under account" dns 0
POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
echo "$POSTLINE" | grep -q "pg_dns_real" && ok "minted real DNS Write id" || bad "did not mint real dns id: $POSTLINE"
echo "$POSTLINE" | grep -qE "pg_dnsx1|pg_dnsx2" && bad "minted a dns decoy id: $POSTLINE" || ok "no dns decoy id minted"
echo "$POSTLINE" | grep -qF '"resources":{"com.cloudflare.api.account.acc123":{"com.cloudflare.api.account.zone.*":"*"}}' && ok "cfat nests zone under account" || bad "zone resource not nested for cfat: $POSTLINE"
no_leak "dns cfat nested"
teardown

# 11. dns exact-name on cfut_: the flat top-level zone resource is preserved, with
#     no account nesting (user-owned masters accept the flat zone.* shape).
setup "cfut_master_xyz"
cat > "$BIN/curl" <<'CURL'
#!/usr/bin/env bash
SB="$(cd "$(dirname "$0")/.." && pwd)"
echo "$@" >> "$SB/curl.log"
url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
case "$url" in
  *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_dns_real","name":"DNS Write"}]}';;
  *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
  *"/tokens"*) echo '{"success":true,"result":{"value":"cfut_minted_SECRET_do_not_leak_0123456789"}}';;
  *) echo '{"success":false,"errors":[{"code":9999}]}';;
esac
CURL
chmod +x "$BIN/curl"
run "dns cfut keeps flat zone resource" dns 0
POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
echo "$POSTLINE" | grep -qF '"resources":{"com.cloudflare.api.account.zone.*":"*"}' && ok "cfut flat zone resource" || bad "cfut resource not flat: $POSTLINE"
echo "$POSTLINE" | grep -qF 'account.acc123":{' && bad "cfut wrongly nested under account: $POSTLINE" || ok "cfut not nested under account"
no_leak "dns cfut flat"
teardown

# 12. a Cloudflare group rename surfaces as a NAMED miss, not a silent wrong-group
#     mint: with only decoys present and no exact "DNS Write", the helper dies 1 and
#     names the exact group it looked for.
setup "cfat_master_abc"
cat > "$BIN/curl" <<'CURL'
#!/usr/bin/env bash
SB="$(cd "$(dirname "$0")/.." && pwd)"
echo "$@" >> "$SB/curl.log"
url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
case "$url" in
  *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_dnsx1","name":"Account DNS Settings Write"},{"id":"pg_dnsx2","name":"DNS Firewall Write"}]}';;
  *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
  *"/tokens"*) echo '{"success":true,"result":{"value":"cfat_minted_SECRET_do_not_leak_0123456789"}}';;
  *) echo '{"success":false,"errors":[{"code":9999}]}';;
esac
CURL
chmod +x "$BIN/curl"
run "missing exact group fails closed" dns 1
grep -q -- "-X POST" "$SANDBOX/curl.log" && bad "minted despite no exact group match" || ok "no mint on missing exact group"
echo "$ERR" | grep -qF "no permission group named 'DNS Write'" && ok "die names the missing exact group" || bad "die message not named: $ERR"
teardown

# 13. house-env fallback (multi-tenant): the per-account file has NO master
#     (stripped by the 1631 remediation) but the house env carries it. cf-token
#     must resolve/mint/persist against the HOUSE env, report src=house, and
#     leave the stripped per-account file untouched. PLATFORM_ROOT locates the
#     house env at $ROOT/config/cloudflare-house.env.
setup "cfat_master_abc"
# Strip the master from the per-account file (post-remediation state): keep only
# the non-credential account-id line.
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
HOUSE_ENV="$HOUSE_DIR/cloudflare-house.env"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_ENV" )
# The caller gate (cases 18-22) admits only the house account, so this case
# spawns as the house account.
mkdir -p "$SANDBOX/acme-code/data/accounts/acc123"
printf '{"role":"house"}\n' > "$SANDBOX/acme-code/data/accounts/acc123/account.json"
of=$(mktemp); ef=$(mktemp)
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -eq 0 ] && ok "house fallback resolves (exit=0)" || { bad "house fallback (exit=$RC)"; echo "  stderr: $ERR" >&2; }
[ "$OUT" = "CF_PAGES_D1_TOKEN" ] && ok "house fallback stdout is canonical key" || bad "house out='$OUT'"
grep -q "^CF_PAGES_D1_TOKEN=" "$HOUSE_ENV" && ok "minted token persisted to HOUSE env" || bad "not persisted to house env"
grep -q "^CF_PAGES_D1_TOKEN=" "$SECRETS" && bad "token wrongly written to stripped per-account file" || ok "stripped per-account file untouched"
grep -q "^CLOUDFLARE_API_TOKEN=" "$SECRETS" && bad "master reappeared in per-account file" || ok "no master in per-account file"
echo "$ERR" | grep -q "src=house" && ok "lifeline reports src=house" || bad "no src=house in lifeline: $ERR"
no_leak "house fallback"
teardown

# 14. single-tenant unchanged: the per-account file still carries the master, so
#     NO fallback happens even with a house env present. src=account, and the
#     token persists to the per-account file exactly as before.
setup "cfat_master_abc"
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_HOUSE_should_not_be_used" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
of=$(mktemp); ef=$(mktemp)
PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -eq 0 ] && ok "single-tenant resolves (exit=0)" || bad "single-tenant (exit=$RC): $ERR"
grep -q "^CF_PAGES_D1_TOKEN=" "$SECRETS" && ok "single-tenant persists to per-account file" || bad "not persisted to per-account file"
grep -q "^CF_PAGES_D1_TOKEN=" "$HOUSE_DIR/cloudflare-house.env" && bad "single-tenant wrongly wrote to house env" || ok "house env untouched in single-tenant"
echo "$ERR" | grep -q "src=account" && ok "lifeline reports src=account" || bad "no src=account in lifeline: $ERR"
teardown

# 15. fail-closed: stripped per-account file and NO house env reachable (no
#     PLATFORM_ROOT, no CF_HOUSE_ENV) -> non-zero, no mint, and a message that
#     names the missing credential source. Reaching this branch means no house
#     credential was found at all; the caller gate (cases 18-22) is what decides
#     whether a reachable house credential may actually be used.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
of=$(mktemp); ef=$(mktemp)
env -u PLATFORM_ROOT -u MAXY_PLATFORM_ROOT -u CF_HOUSE_ENV \
  CF_BACKOFF_SECONDS=0 bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -ne 0 ] && ok "no-master no-house fails closed (exit=$RC)" || bad "did not fail closed (exit=$RC)"
grep -q -- "-X POST" "$SANDBOX/curl.log" && bad "minted despite no credential source" || ok "no mint without a credential source"
echo "$ERR" | grep -qi "no cloudflare master" && ok "die names missing master source" || bad "die message not named: $ERR"
teardown

# 16. explicit CF_HOUSE_ENV override wins over PLATFORM_ROOT derivation.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
OVERRIDE="$SANDBOX/acme-code/override-house.env"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$OVERRIDE" )
# The caller gate admits only the house account, and locates the accounts dir
# from PLATFORM_ROOT. The PLATFORM_ROOT-derived house path does not exist here,
# so a pass still proves CF_HOUSE_ENV won.
mkdir -p "$SANDBOX/acme-code/data/accounts/acc123"
printf '{"role":"house"}\n' > "$SANDBOX/acme-code/data/accounts/acc123/account.json"
of=$(mktemp); ef=$(mktemp)
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_HOUSE_ENV="$OVERRIDE" CF_BACKOFF_SECONDS=0 bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -eq 0 ] && ok "CF_HOUSE_ENV override resolves (exit=0)" || bad "override (exit=$RC): $ERR"
grep -q "^CF_PAGES_D1_TOKEN=" "$OVERRIDE" && ok "override env received the minted token" || bad "override not persisted"
echo "$ERR" | grep -q "src=house" && ok "override reports src=house" || bad "override src: $ERR"
teardown

# 17. storage scope (Task 1670): the broker's data token needs BOTH D1 Write and
#     Workers R2 Storage Write, persisted under CF_STORAGE_TOKEN.
setup "cfat_master_abc"
run "storage mint D1+R2" storage 0
[ "$OUT" = "CF_STORAGE_TOKEN" ] && ok "storage stdout is CF_STORAGE_TOKEN" || bad "storage out='$OUT'"
POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
echo "$POSTLINE" | grep -q "pg_d1" && ok "storage minted D1 id" || bad "storage no D1 id: $POSTLINE"
echo "$POSTLINE" | grep -q "pg_r2" && ok "storage minted R2 id" || bad "storage no R2 id: $POSTLINE"
grep -q "^CF_STORAGE_TOKEN=" "$SECRETS" && ok "storage token persisted" || bad "storage not persisted"
no_leak "storage mint"
teardown

# 18. calendar-d1 scope (Task 1670): the calendar's data token needs D1 Write
#     ONLY (no R2), persisted under CF_CALENDAR_D1_TOKEN.
setup "cfat_master_abc"
run "calendar-d1 mint D1 only" calendar-d1 0
[ "$OUT" = "CF_CALENDAR_D1_TOKEN" ] && ok "calendar-d1 stdout is CF_CALENDAR_D1_TOKEN" || bad "calendar-d1 out='$OUT'"
POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
echo "$POSTLINE" | grep -q "pg_d1" && ok "calendar-d1 minted D1 id" || bad "calendar-d1 no D1 id: $POSTLINE"
echo "$POSTLINE" | grep -q "pg_r2" && bad "calendar-d1 wrongly minted R2 id: $POSTLINE" || ok "calendar-d1 no R2 id"
grep -q "^CF_CALENDAR_D1_TOKEN=" "$SECRETS" && ok "calendar-d1 token persisted" || bad "calendar-d1 not persisted"
no_leak "calendar-d1 mint"
teardown

# ---------------------------------------------------------------------------
# Caller gate (Task 1646). The house env carries an account-wide master, so the
# fallback must fire only for the house account. "House" mirrors
# resolveHouseOrSoleAccountId: account.json role:"house", or the sole account.
# The gate reads the caller's own account.json under
# $(dirname $PLATFORM_ROOT)/data/accounts/$ACCOUNT_ID/, which the sandbox
# layout already matches ($SANDBOX/acme-code/data/accounts/acc123).

# 18. a sub-account caller must NOT resolve the house master: deny, non-zero,
#     no mint, and nothing persisted to either file.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
ADIR="$SANDBOX/acme-code/data/accounts"
mkdir -p "$ADIR/acc123" "$ADIR/house1"
printf '{"role":"client"}\n' > "$ADIR/acc123/account.json"
printf '{"role":"house"}\n'  > "$ADIR/house1/account.json"
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
HOUSE_ENV="$HOUSE_DIR/cloudflare-house.env"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_ENV" )
of=$(mktemp); ef=$(mktemp)
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
  bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -ne 0 ] && ok "sub-account denied the house master (exit=$RC)" || bad "sub-account NOT denied (exit=$RC)"
grep -q -- "-X POST" "$SANDBOX/curl.log" && bad "minted for a sub-account" || ok "no mint for a sub-account"
grep -q "^CF_PAGES_D1_TOKEN=" "$HOUSE_ENV" && bad "sub-account wrote a token to house env" || ok "house env untouched on deny"
grep -q "^CF_PAGES_D1_TOKEN=" "$SECRETS" && bad "sub-account wrote a token to its own file" || ok "per-account file untouched on deny"
echo "$ERR" | grep -q "action=deny" && ok "deny lifeline emitted" || bad "no deny lifeline: $ERR"
echo "$ERR" | grep -q "reason=not-house" && ok "deny lifeline names the reason" || bad "no reason: $ERR"
no_leak "sub-account deny"
teardown

# 19. the house account (role:"house") is allowed, preserving house-run hosting.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/other1"
printf '{"role":"house"}\n'  > "$ADIR/acc123/account.json"
printf '{"role":"client"}\n' > "$ADIR/other1/account.json"
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
HOUSE_ENV="$HOUSE_DIR/cloudflare-house.env"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_ENV" )
of=$(mktemp); ef=$(mktemp)
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
  bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -eq 0 ] && ok "house caller allowed (exit=0)" || { bad "house caller denied (exit=$RC)"; echo "  stderr: $ERR" >&2; }
[ "$OUT" = "CF_PAGES_D1_TOKEN" ] && ok "house caller stdout is canonical key" || bad "house caller out='$OUT'"
grep -q "^CF_PAGES_D1_TOKEN=" "$HOUSE_ENV" && ok "house caller persisted to house env" || bad "house caller not persisted"
teardown

# 20. the sole unlabelled account IS the house account (resolveHouseOrSoleAccountId's
#     second arm) -> allowed.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123"
printf '{"name":"only"}\n' > "$ADIR/acc123/account.json"
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
HOUSE_ENV="$HOUSE_DIR/cloudflare-house.env"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_ENV" )
of=$(mktemp); ef=$(mktemp)
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
  bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -eq 0 ] && ok "sole unlabelled account allowed (exit=0)" || bad "sole account denied (exit=$RC): $ERR"
teardown

# 21. unset ACCOUNT_ID fails closed.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/house1"
printf '{"role":"client"}\n' > "$ADIR/acc123/account.json"
printf '{"role":"house"}\n'  > "$ADIR/house1/account.json"
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
of=$(mktemp); ef=$(mktemp)
env -u ACCOUNT_ID PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
  bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -ne 0 ] && ok "unset ACCOUNT_ID fails closed (exit=$RC)" || bad "unset ACCOUNT_ID allowed (exit=$RC)"
echo "$ERR" | grep -q "reason=no-account-id" && ok "no-account-id reason surfaced" || bad "reason: $ERR"
teardown

# 22. a caller whose account.json is absent fails closed.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/house1"
printf '{"role":"house"}\n' > "$ADIR/house1/account.json"
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
of=$(mktemp); ef=$(mktemp)
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
  bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -ne 0 ] && ok "missing account.json fails closed (exit=$RC)" || bad "missing account.json allowed"
echo "$ERR" | grep -q "reason=no-account-json" && ok "no-account-json reason surfaced" || bad "reason: $ERR"
teardown

# 23. two role:"house" accounts is corruption: resolveAccountDir returns null
#     rather than picking one, so the gate denies rather than trusting a
#     self-declared house.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/house2"
printf '{"role":"house"}\n' > "$ADIR/acc123/account.json"
printf '{"role":"house"}\n' > "$ADIR/house2/account.json"
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
of=$(mktemp); ef=$(mktemp)
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
  bash "$HELPER" pages "$SECRETS" >"$of" 2>"$ef"
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
[ "$RC" -ne 0 ] && ok "two houses fails closed (exit=$RC)" || bad "two houses allowed (exit=$RC)"
echo "$ERR" | grep -q "reason=multiple-house-accounts" && ok "multiple-house reason surfaced" || bad "reason: $ERR"
teardown

# 24. the deny message names the scope actually requested, not a fixed list.
setup "cfat_master_abc"
( umask 077; printf 'CLOUDFLARE_ACCOUNT_ID=%s\n' "acc123" > "$SECRETS" )
ADIR="$SANDBOX/acme-code/data/accounts"; mkdir -p "$ADIR/acc123" "$ADIR/house1"
printf '{"role":"client"}\n' > "$ADIR/acc123/account.json"
printf '{"role":"house"}\n'  > "$ADIR/house1/account.json"
HOUSE_DIR="$SANDBOX/acme-code/platform/config"; mkdir -p "$HOUSE_DIR"
( umask 077; printf 'CLOUDFLARE_API_TOKEN=%s\nCLOUDFLARE_ACCOUNT_ID=%s\n' "cfat_master_abc" "acc123" > "$HOUSE_DIR/cloudflare-house.env" )
of=$(mktemp); ef=$(mktemp)
ACCOUNT_ID=acc123 PLATFORM_ROOT="$SANDBOX/acme-code/platform" CF_BACKOFF_SECONDS=0 \
  bash "$HELPER" dns "$SECRETS" >"$of" 2>"$ef"
RC=$?; ERR=$(cat "$ef"); rm -f "$of" "$ef"
echo "$ERR" | grep -q "scope=dns" && ok "deny lifeline names scope=dns" || bad "lifeline scope: $ERR"
echo "$ERR" | grep -qF "'dns' scope" && ok "deny message names the dns scope" || bad "deny message scope: $ERR"
teardown

echo "----"; echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]
