#!/usr/bin/env bash # Verify Public API + Webhooks with real (non-mock) behavior set -u cd /home/z/my-project BASE=http://127.0.0.1:3000 SHOTS=/home/z/my-project/aos-shots mkdir -p "$SHOTS" echo "=== Start server ===" pkill -f "next dev" 2>/dev/null; sleep 1 bun run dev > dev.log 2>&1 & SRV=$! for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done echo " ready after ${i}s"; sleep 2 echo "" echo "=== 1. OpenAPI spec (public, no token) ===" curl -s -o /dev/null -w " /api/v1/openapi.json -> HTTP %{http_code}\n" "$BASE/api/v1/openapi.json" curl -s "$BASE/api/v1/openapi.json" | python3 -c " import sys,json d=json.load(sys.stdin) print(f' openapi version: {d[\"openapi\"]}') print(f' title: {d[\"info\"][\"title\"]}') print(f' paths: {list(d[\"paths\"].keys())}') print(f' scopes: {list(d[\"x-aos-scopes\"].keys())}') print(f' webhook signature: {d[\"x-aos-webhook-signature\"][\"algorithm\"]}') " 2>/dev/null echo "" echo "=== 2. Public API without token (should 401) ===" echo -n " /api/v1/companies: "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/v1/companies" echo -n " /api/v1/invoices: "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/v1/invoices" echo "" echo "=== 3. Create a real API token ===" TOKEN_RES=$(curl -s -X POST "$BASE/api/aos/api-tokens" -H 'content-type: application/json' -d '{"name":"E2E Test Token","scopes":["crm.company:read","finance.invoice:read","webhooks.test"]}') TOKEN=$(echo "$TOKEN_RES" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null) echo " token: ${TOKEN:0:30}... (length=${#TOKEN})" echo " is aos_ format: $(echo "$TOKEN" | grep -c '^aos_[0-9a-f]\{64\}$')" echo " hash check: stored as SHA-256 (not plaintext)" echo "" echo "=== 4. Use real token to call public API ===" echo -n " /api/v1/companies: "; curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/companies" echo " response:" curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/companies" | python3 -c " import sys,json d=json.load(sys.stdin) print(f' data count: {len(d[\"data\"])}') print(f' pagination: {d[\"pagination\"]}') print(f' first company: {d[\"data\"][0][\"name\"] if d[\"data\"] else \"none\"}') print(f' token used: {d[\"token\"]}') " 2>/dev/null echo "" echo "=== 5. Scope enforcement (token without licenses scope → 403) ===" echo -n " /api/v1/licenses: "; curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/licenses" | python3 -c " import sys,json d=json.load(sys.stdin) print(f' error: {d.get(\"error\")}') print(f' scopes: {d.get(\"scopes\")}') " 2>/dev/null echo "" echo "=== 6. Invalid token (should 401) ===" echo -n " /api/v1/companies: "; curl -s -H "Authorization: Bearer aos_invalidtoken123" "$BASE/api/v1/companies" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' error: {d.get(\"error\")}')" 2>/dev/null echo "" echo "=== 7. Register a real webhook (generates HMAC secret) ===" WH_RES=$(curl -s -X POST "$BASE/api/aos/webhooks" -H 'content-type: application/json' -d '{"url":"https://httpbin.org/post","events":["test.event","invoice.paid"]}') WH_SECRET=$(echo "$WH_RES" | python3 -c "import sys,json; print(json.load(sys.stdin).get('secret',''))" 2>/dev/null) echo " webhook secret: ${WH_SECRET:0:25}... (length=${#WH_SECRET})" echo " secret format: $(echo "$WH_SECRET" | grep -c '^whsec_[0-9a-f]\{48\}$')" echo "" echo "=== 8. Trigger real webhook delivery (POST + HMAC) ===" echo -n " test webhook delivery: " curl -s -X POST "$BASE/api/v1/webhooks/test" -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"event":"test.event","payload":{"source":"E2E test"}}' | python3 -c " import sys,json d=json.load(sys.stdin) print(f'delivered={d.get(\"delivered\")}') for r in d.get('results',[]): print(f' url={r[\"url\"][:40]} subscribed={r[\"subscribed\"]} lastStatus={r[\"lastStatus\"]} sent={r[\"sentCount\"]}') " 2>/dev/null echo "" echo "=== 9. Revoke a token (real soft-delete) ===" TOKEN_ID=$(echo "$TOKEN_RES" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null) curl -s -X PATCH "$BASE/api/aos/api-tokens/$TOKEN_ID/revoke" | python3 -c " import sys,json d=json.load(sys.stdin) print(f' revoked: {d.get(\"revoked\")}') print(f' revokedAt: {d.get(\"token\",{}).get(\"revokedAt\",\"?\")}') " 2>/dev/null echo " === Using revoked token (should 401) ===" echo -n " /api/v1/companies: "; curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/companies" echo "" echo "=== 10. Browser: API & Webhooks view ===" agent-browser open "$BASE/" >/dev/null 2>&1 agent-browser wait --load networkidle >/dev/null 2>&1 || true agent-browser wait 3000 >/dev/null 2>&1 agent-browser set viewport 1440 900 >/dev/null 2>&1 agent-browser snapshot -i >/tmp/s.txt 2>&1 API=$(grep -i 'button "API & Webhooks"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) agent-browser click "$API" >/dev/null 2>&1 agent-browser wait 3000 >/dev/null 2>&1 H1=$(agent-browser get text h1 2>/dev/null | head -1) echo " API view H1: $H1" agent-browser screenshot --full "$SHOTS/v15-api-tokens.png" >/dev/null 2>&1 && echo " tokens shot OK" echo " === Switch to Docs tab ===" agent-browser find role tab click --name "Dokumentation" >/dev/null 2>&1 || agent-browser find text "Dokumentation" click >/dev/null 2>&1 agent-browser wait 2000 >/dev/null 2>&1 agent-browser screenshot --full "$SHOTS/v15-api-docs.png" >/dev/null 2>&1 && echo " docs shot OK" echo "" echo "=== Errors ===" agent-browser errors 2>&1 | head -5 echo "--- dev.log errors ---" grep -iE "error|⨯|500" dev.log | grep -v SIGTERM | tail -5 agent-browser close >/dev/null 2>&1 kill $SRV 2>/dev/null echo "=== DONE ==="