diff --git a/TIMMY_TEST_PLAN.md b/TIMMY_TEST_PLAN.md index d4e5d51..a46c7aa 100644 --- a/TIMMY_TEST_PLAN.md +++ b/TIMMY_TEST_PLAN.md @@ -74,6 +74,14 @@ Requirements: `curl`, `bash`, `jq` — nothing else. | 35 | GET sessions/:id includes tier | `GET /api/sessions/:id` → HTTP 200, `trust_tier == "anonymous"` | | 36 | Full challenge→sign→verify | Inline node script: generate keypair, challenge, sign kind=27235 event, verify → token; GET /identity/me → tier=new, pubkey matches | +### Timmy economic peer — identity card + vouching guards (tests 37–39) + +| # | Name | What it checks | +|---|------|----------------| +| 37 | Timmy identity card | `GET /api/identity/timmy` → HTTP 200, `npub` starts with `npub1`, `pubkeyHex` is 64 hex chars, `zapCount` is a non-negative integer | +| 38 | Vouch: no token | `POST /api/identity/vouch` without `X-Nostr-Token` → HTTP 401 | +| 39 | Vouch: invalid token | `POST /api/identity/vouch` with `X-Nostr-Token: totally.invalid.token` → HTTP 401 | + --- ## Architecture notes for reviewers diff --git a/artifacts/api-server/src/routes/testkit.ts b/artifacts/api-server/src/routes/testkit.ts index 677e1be..51c90bc 100644 --- a/artifacts/api-server/src/routes/testkit.ts +++ b/artifacts/api-server/src/routes/testkit.ts @@ -29,6 +29,9 @@ const router = Router(); * Guarded on stubMode=true; polls until state=provisioning|ready (20 s timeout). * - T24 ADDED: costLedger completeness after job completion — 8 fields, honest-accounting * invariant (actualAmountSats ≤ workAmountSats), refundState enum check. + * - T37 ADDED: GET /api/identity/timmy — Timmy's npub + zap count (issue #45). + * - T38 ADDED: POST /api/identity/vouch no token → 401 guard. + * - T39 ADDED: POST /api/identity/vouch invalid token → 401 guard. */ router.get("/testkit", (req: Request, res: Response) => { const proto = @@ -1092,29 +1095,83 @@ NODESCRIPT fi fi +# --------------------------------------------------------------------------- +# Test 37 — GET /api/identity/timmy returns Timmy's npub and zap count +# --------------------------------------------------------------------------- +sep "Test 37 — GET /api/identity/timmy" +T37_RES=\$(curl -s -w "\n%{http_code}" "\$BASE/api/identity/timmy") +T37_BODY=\$(body_of "\$T37_RES"); T37_CODE=\$(code_of "\$T37_RES") +T37_NPUB=\$(echo "\$T37_BODY" | jq -r '.npub' 2>/dev/null || echo "") +T37_HEX=\$(echo "\$T37_BODY" | jq -r '.pubkeyHex' 2>/dev/null || echo "") +T37_ZAPS=\$(echo "\$T37_BODY" | jq '.zapCount' 2>/dev/null || echo "") +if [[ "\$T37_CODE" == "200" \\ + && "\$T37_NPUB" =~ ^npub1 \\ + && \${#T37_HEX} -eq 64 \\ + && "\$T37_ZAPS" =~ ^[0-9]+\$ ]]; then + note PASS "HTTP 200, npub=\${T37_NPUB:0:12}… pubkeyHex=\${T37_HEX:0:8}… zapCount=\$T37_ZAPS" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T37_CODE npub=\$T37_NPUB hex_len=\${#T37_HEX} zapCount=\$T37_ZAPS" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# Test 38 — POST /api/identity/vouch with no token → 401 +# --------------------------------------------------------------------------- +sep "Test 38 — POST /api/identity/vouch no token → 401" +T38_RES=\$(curl -s -w "\n%{http_code}" -X POST "\$BASE/api/identity/vouch" \\ + -H "Content-Type: application/json" \\ + -d '{"voucheePubkey":"aabbccdd00112233aabbccdd00112233aabbccdd00112233aabbccdd00112233"}') +T38_BODY=\$(body_of "\$T38_RES"); T38_CODE=\$(code_of "\$T38_RES") +if [[ "\$T38_CODE" == "401" ]]; then + note PASS "HTTP 401 as expected (no X-Nostr-Token)" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T38_CODE body=\$T38_BODY" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# Test 39 — POST /api/identity/vouch with invalid token → 401 +# --------------------------------------------------------------------------- +sep "Test 39 — POST /api/identity/vouch invalid token → 401" +T39_RES=\$(curl -s -w "\n%{http_code}" -X POST "\$BASE/api/identity/vouch" \\ + -H "Content-Type: application/json" \\ + -H "X-Nostr-Token: totally.invalid.token" \\ + -d '{"voucheePubkey":"aabbccdd00112233aabbccdd00112233aabbccdd00112233aabbccdd00112233"}') +T39_BODY=\$(body_of "\$T39_RES"); T39_CODE=\$(code_of "\$T39_RES") +if [[ "\$T39_CODE" == "401" ]]; then + note PASS "HTTP 401 as expected (invalid token)" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T39_CODE body=\$T39_BODY" + FAIL=\$((FAIL+1)) +fi + # =========================================================================== # FUTURE STUBS — placeholders for upcoming tasks (do not affect PASS/FAIL) # =========================================================================== # These are bash comments only. They document planned tests so future tasks # can implement them with the correct numbering context. # -# FUTURE T37: GET /api/estimate returns cost preview +# FUTURE T40: GET /api/estimate returns cost preview # GET \$BASE/api/estimate?request= # Assert HTTP 200, estimatedSats is a positive integer # Assert model, inputTokens, outputTokens are present # -# FUTURE T38: Anonymous job always hits Lightning gate +# FUTURE T41: Anonymous job always hits Lightning gate # Create anonymous job, poll to awaiting_work_payment # Assert response.free_tier is absent or false in all poll responses # -# FUTURE T39: Nostr-identified trusted identity → free response +# FUTURE T42: Nostr-identified trusted identity → free response # Requires identity with trust_score >= 50 (trusted tier) and daily budget not exhausted # Submit request with identity token # Assert HTTP 200, response.free_tier == true, no invoice created # -# FUTURE T40: Timmy initiates a zap -# POST to /api/identity/me/tip (or similar) -# Assert Timmy initiates a Lightning outbound payment to caller's LNURL +# FUTURE T43: Elite vouch grants trust boost +# Requires two identities: voucher at elite tier (score >= 200), fresh vouchee +# POST /api/identity/vouch with voucher's token + signed event tagging vouchee +# Assert HTTP 200, newScore = oldScore + 20, newTier reflects boost # --------------------------------------------------------------------------- # Summary diff --git a/artifacts/mobile/app/onboarding.tsx b/artifacts/mobile/app/onboarding.tsx index 00652ce..5872a8e 100644 --- a/artifacts/mobile/app/onboarding.tsx +++ b/artifacts/mobile/app/onboarding.tsx @@ -21,6 +21,19 @@ import { ONBOARDING_COMPLETED_KEY } from "@/constants/storage-keys"; const C = Colors.dark; const { width: SCREEN_WIDTH } = Dimensions.get("window"); +const slideStyles = StyleSheet.create({ + iconCircle: { + width: 140, + height: 140, + borderRadius: 70, + backgroundColor: C.surfaceElevated, + borderWidth: 1, + borderColor: C.border, + alignItems: "center", + justifyContent: "center", + }, +}); + type Slide = { id: string; icon: React.ReactNode; @@ -158,19 +171,6 @@ export default function OnboardingScreen() { ); } -const slideStyles = StyleSheet.create({ - iconCircle: { - width: 140, - height: 140, - borderRadius: 70, - backgroundColor: C.surfaceElevated, - borderWidth: 1, - borderColor: C.border, - alignItems: "center", - justifyContent: "center", - }, -}); - const styles = StyleSheet.create({ container: { flex: 1,