c673b10a94
Disposable rig that runs the onboarding-tester agent docs-only against the buyer-pays journey: a sandbox daemon wired to a Dockerized BTCPay regtest stack, a scoped key with payment_providers:write, and a regtest buyer-pay helper. Includes the de-risk probe + findings and an end-to-end gate check (validate-gate.sh, 10/10). The doc-onboarding loop converged completed-clean; see stage2/STAGE2-RESULT.md. Scratch (.live-env, probe-out/) is gitignored.
90 lines
5.1 KiB
Bash
Executable File
90 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# End-to-end validation of the agent-payment-connect gate against the LIVE
|
|
# regtest BTCPay (the spec's hard requirement). Boots a throwaway Keysat daemon
|
|
# in sandbox mode pointed at the regtest BTCPay stack, mints a scoped
|
|
# `payment_providers:write` key, and drives the full OAuth round-trip for two
|
|
# stores:
|
|
# - no-wallet store → network undetermined → FAIL CLOSED → connect DENIED (400)
|
|
# - regtest store → bcrt1 address → non-mainnet → connect ALLOWED (persisted)
|
|
#
|
|
# Requires the regtest stack up (docker compose -p keysat-btcpay up -d) and
|
|
# .live-env populated (GATE_TOK_REGTEST / GATE_TOK_NOWALLET — single-store BTCPay
|
|
# tokens). Reads the daemon release binary built by `cargo build --release`.
|
|
set -uo pipefail
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$HERE/btcpay-regtest/.live-env"
|
|
BIN="$HERE/../../licensing-service/target/release/keysat"
|
|
[[ -x "$BIN" ]] || { echo "FAIL: release binary missing ($BIN) — run cargo build --release"; exit 1; }
|
|
|
|
PORT=$(node -e 'const s=require("net").createServer();s.listen(0,"127.0.0.1",()=>{console.log(s.address().port);s.close();})')
|
|
MASTER=$(openssl rand -hex 32)
|
|
TMP=$(mktemp -d)
|
|
BASE="http://127.0.0.1:$PORT"
|
|
pass=0; fail=0
|
|
ok(){ echo " ✅ $*"; pass=$((pass+1)); }
|
|
no(){ echo " ❌ $*"; fail=$((fail+1)); }
|
|
|
|
echo "== booting sandbox daemon on $BASE (btcpay → $KEYSAT_LIVE_BTCPAY_URL) =="
|
|
KEYSAT_BIND="127.0.0.1:$PORT" \
|
|
KEYSAT_DB_PATH="$TMP/keysat.db" \
|
|
KEYSAT_ADMIN_API_KEY="$MASTER" \
|
|
KEYSAT_SANDBOX_MODE=1 \
|
|
BTCPAY_URL="$KEYSAT_LIVE_BTCPAY_URL" \
|
|
KEYSAT_PUBLIC_URL="$BASE" \
|
|
KEYSAT_OPERATOR_NAME="Stage2 Gate Validation" \
|
|
nohup "$BIN" >"$TMP/daemon.log" 2>&1 &
|
|
DAEMON_PID=$!
|
|
trap 'kill $DAEMON_PID 2>/dev/null; rm -rf "$TMP"' EXIT
|
|
for i in $(seq 1 75); do curl -fsS "$BASE/healthz" >/dev/null 2>&1 && break; sleep 0.2; [[ $i == 75 ]] && { echo "FAIL: daemon never healthy"; tail -20 "$TMP/daemon.log"; exit 1; }; done
|
|
|
|
M=(-H "Authorization: Bearer $MASTER")
|
|
|
|
echo "== 1. sandbox flag surfaced read-only in /v1/admin/tier =="
|
|
[[ "$(curl -sS "${M[@]}" "$BASE/v1/admin/tier" | jq -r '.sandbox')" == "true" ]] && ok "tier.sandbox == true" || no "sandbox flag not surfaced"
|
|
|
|
echo "== 2. mint scoped merchant-onboard + payment_providers:write key =="
|
|
SK="$(curl -sS "${M[@]}" -X POST "$BASE/v1/admin/api-keys" -H 'Content-Type: application/json' \
|
|
-d '{"label":"agent","role":"merchant-onboard","scopes":["payment_providers:write"]}' | jq -r '.token')"
|
|
[[ "$SK" == ks_* ]] && ok "scoped key minted" || { no "mint failed"; }
|
|
S=(-H "Authorization: Bearer $SK")
|
|
|
|
# drive a connect: returns HTTP status of the callback. $1=btcpay token
|
|
drive_connect(){
|
|
local tok="$1"
|
|
local st; st="$(curl -sS "${S[@]}" -X POST "$BASE/v1/admin/btcpay/connect" | jq -r '.state')"
|
|
[[ -n "$st" && "$st" != null ]] || { echo "000"; return; }
|
|
curl -sS -o /tmp/gate-cb.out -w '%{http_code}' -X POST "$BASE/v1/btcpay/authorize/callback?state=$st" \
|
|
--data-urlencode "apiKey=$tok"
|
|
}
|
|
|
|
echo "== 3. DENY: scoped connect to a no-wallet store (undetermined → fail-closed) =="
|
|
code="$(drive_connect "$GATE_TOK_NOWALLET")"
|
|
if [[ "$code" == 400 ]]; then
|
|
ok "callback rejected with HTTP 400"
|
|
grep -qi "non-mainnet" /tmp/gate-cb.out && ok "rejection cites the non-mainnet restriction" || no "rejection message unexpected: $(cat /tmp/gate-cb.out | head -c200)"
|
|
else
|
|
no "expected 400, got $code ($(cat /tmp/gate-cb.out | head -c200))"
|
|
fi
|
|
[[ "$(curl -sS "${M[@]}" "$BASE/v1/admin/btcpay/status" | jq -r '.connected')" == "false" ]] && ok "no provider persisted on deny" || no "a provider was persisted despite deny!"
|
|
# The GET callback form (what the agent docs show) must ALSO deny with a 4xx,
|
|
# not a 200 error page (regression guard for the GET-handler status fix).
|
|
gst="$(curl -sS "${S[@]}" -X POST "$BASE/v1/admin/btcpay/connect" | jq -r '.state')"
|
|
gcode="$(curl -sS -o /dev/null -w '%{http_code}' "$BASE/v1/btcpay/authorize/callback?state=$gst&apiKey=$GATE_TOK_NOWALLET")"
|
|
[[ "$gcode" == 4* ]] && ok "GET callback form denies with HTTP $gcode (not a 200 error page)" || no "GET callback returned $gcode (expected 4xx)"
|
|
|
|
echo "== 4. ALLOW: scoped connect to the regtest store (bcrt1 → non-mainnet) =="
|
|
code="$(drive_connect "$GATE_TOK_REGTEST")"
|
|
if [[ "$code" == 200 ]]; then ok "callback succeeded with HTTP 200"; else no "expected 200, got $code ($(cat /tmp/gate-cb.out | head -c300))"; fi
|
|
ST_JSON="$(curl -sS "${M[@]}" "$BASE/v1/admin/btcpay/status")"
|
|
[[ "$(echo "$ST_JSON" | jq -r '.connected')" == "true" ]] && ok "provider persisted" || no "provider not persisted on allow"
|
|
[[ "$(echo "$ST_JSON" | jq -r '.store_id')" == "$KEYSAT_LIVE_BTCPAY_STORE_REGTEST" ]] && ok "persisted store is the regtest store" || no "wrong store persisted: $(echo "$ST_JSON" | jq -c '.store_id')"
|
|
|
|
echo "== 5. scoped connect is audited with the resolved network =="
|
|
AUD="$(curl -sS "${M[@]}" "$BASE/v1/admin/audit?action=payment_provider.connect_scoped" | jq -c '.entries[0] // empty')"
|
|
echo " audit: $AUD"
|
|
echo "$AUD" | grep -qi "regtest" && ok "audit row records network=regtest" || no "audit row missing/!regtest"
|
|
|
|
echo
|
|
echo "==== RESULT: $pass passed, $fail failed ===="
|
|
[[ $fail == 0 ]] || exit 1
|