task/35: Testkit T25–T36 — Nostr identity + trust engine coverage
## 12 new tests added to artifacts/api-server/src/routes/testkit.ts
Inserted before the Summary block, after T24 (cost ledger).
T25 — POST /identity/challenge: HTTP 200, nonce=64-char hex, expiresAt=ISO
T26 — POST /identity/verify {}: HTTP 400, non-empty error
T27 — POST /identity/verify fake nonce: HTTP 401, error contains "Nonce not found"
(uses a plausible-looking event structure to hit the nonce check, not the
signature check — tests the right layer)
T28 — GET /identity/me no header: HTTP 401, error contains "Missing"
T29 — GET /identity/me invalid token: HTTP 401 (Invalid/expired wording)
T30 — POST /sessions bad X-Nostr-Token: HTTP 401, "Invalid or expired", no sessionId
T31 — POST /jobs bad X-Nostr-Token: HTTP 401, "Invalid or expired"
T32 — POST /sessions anonymous: HTTP 201, trust_tier="anonymous"; captures T32_SESSION_ID
T33 — POST /jobs anonymous: HTTP 201, trust_tier="anonymous"; captures T33_JOB_ID
T34 — GET /jobs/:id (using T33_JOB_ID): HTTP 200, trust_tier non-null and "anonymous"
T35 — GET /sessions/:id (using T32_SESSION_ID): HTTP 200, trust_tier="anonymous"
T36 — Full challenge→sign→verify E2E: inline node CJS script generates ephemeral secp256k1
keypair via nostr-tools CJS bundle, POSTs challenge, signs kind=27235 event with
finalizeEvent(), verifies → nostr_token, GETs /identity/me, asserts tier=new,
interactionCount=0, pubkey matches. Guard: SKIP if node not in PATH or script fails.
## nostr-tools import strategy
nostr-tools v2 is ESM-only. CJS workaround: the package ships a CJS bundle at
lib/cjs/index.js. T36 uses require() with the absolute path to that bundle.
Falls back to bare require('nostr-tools') for portability, exits with code 1 if
neither works — bash guard catches this and marks T36 SKIP (not FAIL).
## Stubs T37–T40 added as bash block comments after T36
Format: `# FUTURE T3N: <description>` so they are grepped easily.
Covers: GET /api/estimate (cost preview), anonymous Lightning gate, trusted free tier,
Timmy-initiates-zap. Does not affect PASS/FAIL totals.
## TIMMY_TEST_PLAN.md updated
New "Nostr identity + trust engine (tests 25–36)" section added to the test table.
## TypeScript: 0 errors. All 12 tests smoke-tested individually against localhost:8080.
T25-T35: all correct HTTP status codes and JSON fields verified via curl.
T36: full E2E verified — tier=new, icount=0, pubkey matches /identity/me response.
This commit is contained in:
@@ -57,6 +57,23 @@ Requirements: `curl`, `bash`, `jq` — nothing else.
|
||||
| 15 | Request without macaroon | Same endpoint, no `Authorization` header → HTTP 401 |
|
||||
| 16 | Topup invoice creation | `POST /api/sessions/:id/topup {"amount_sats":500}` with macaroon → HTTP 200, `topup.paymentRequest` present, `topup.amountSats=500` |
|
||||
|
||||
### Nostr identity + trust engine (tests 25–36)
|
||||
|
||||
| # | Name | What it checks |
|
||||
|---|------|----------------|
|
||||
| 25 | Challenge nonce | `POST /api/identity/challenge` → HTTP 200, `nonce` is 64-char hex, `expiresAt` is ISO in future |
|
||||
| 26 | Verify: missing event | `POST /api/identity/verify {}` → HTTP 400, non-empty `error` |
|
||||
| 27 | Verify: unknown nonce | `POST /api/identity/verify` with fake nonce in content → HTTP 401, `error` contains "Nonce not found" |
|
||||
| 28 | Me: no token | `GET /api/identity/me` without header → HTTP 401, `error` contains "Missing" |
|
||||
| 29 | Me: invalid token | `GET /api/identity/me` with `X-Nostr-Token: totally.invalid.token` → HTTP 401 |
|
||||
| 30 | Sessions: bogus token | `POST /api/sessions` with `X-Nostr-Token: badtoken` → HTTP 401, no `sessionId` in response |
|
||||
| 31 | Jobs: bogus token | `POST /api/jobs` with `X-Nostr-Token: badtoken` → HTTP 401 |
|
||||
| 32 | Sessions anonymous tier | `POST /api/sessions` (no token) → HTTP 201, `trust_tier == "anonymous"` |
|
||||
| 33 | Jobs anonymous tier | `POST /api/jobs` (no token) → HTTP 201, `trust_tier == "anonymous"` |
|
||||
| 34 | GET jobs/:id includes tier | `GET /api/jobs/:id` → HTTP 200, `trust_tier` non-null (anonymous job → `"anonymous"`) |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Architecture notes for reviewers
|
||||
|
||||
@@ -745,6 +745,338 @@ else
|
||||
SKIP=\$((SKIP+1))
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# T25–T36 Nostr identity + trust engine
|
||||
# ===========================================================================
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T25 — POST /identity/challenge returns valid nonce
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 25 — POST /identity/challenge returns valid nonce"
|
||||
T25_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/identity/challenge")
|
||||
T25_BODY=\$(body_of "\$T25_RES"); T25_CODE=\$(code_of "\$T25_RES")
|
||||
T25_NONCE=\$(echo "\$T25_BODY" | jq -r '.nonce' 2>/dev/null || echo "")
|
||||
T25_EXP=\$(echo "\$T25_BODY" | jq -r '.expiresAt' 2>/dev/null || echo "")
|
||||
T25_NONCE_OK=false; T25_EXP_OK=false
|
||||
[[ "\$T25_NONCE" =~ ^[0-9a-f]{64}\$ ]] && T25_NONCE_OK=true || true
|
||||
[[ "\$T25_EXP" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]] && T25_EXP_OK=true || true
|
||||
if [[ "\$T25_CODE" == "200" && "\$T25_NONCE_OK" == "true" && "\$T25_EXP_OK" == "true" ]]; then
|
||||
note PASS "HTTP 200, nonce=64-char-hex, expiresAt=ISO"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T25_CODE nonce='\$T25_NONCE' expiresAt='\$T25_EXP'"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T26 — POST /identity/verify with missing event body → 400
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 26 — POST /identity/verify missing event → 400"
|
||||
T26_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/identity/verify" \\
|
||||
-H "Content-Type: application/json" -d '{}')
|
||||
T26_BODY=\$(body_of "\$T26_RES"); T26_CODE=\$(code_of "\$T26_RES")
|
||||
T26_ERR=\$(echo "\$T26_BODY" | jq -r '.error' 2>/dev/null || echo "")
|
||||
if [[ "\$T26_CODE" == "400" && -n "\$T26_ERR" ]]; then
|
||||
note PASS "HTTP 400, error: \${T26_ERR:0:80}"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T26_CODE body=\$T26_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T27 — POST /identity/verify with unknown nonce → 401 "Nonce not found"
|
||||
# Uses a real-looking (structurally plausible) event with a fake nonce.
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 27 — POST /identity/verify unknown nonce → 401"
|
||||
T27_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/identity/verify" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"event":{"pubkey":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","content":"0000000000000000000000000000000000000000000000000000000000000000","kind":27235,"tags":[],"created_at":1700000000,"id":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sig":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"}}')
|
||||
T27_BODY=\$(body_of "\$T27_RES"); T27_CODE=\$(code_of "\$T27_RES")
|
||||
T27_ERR=\$(echo "\$T27_BODY" | jq -r '.error' 2>/dev/null || echo "")
|
||||
if [[ "\$T27_CODE" == "401" && "\$T27_ERR" == *"Nonce not found"* ]]; then
|
||||
note PASS "HTTP 401, error contains 'Nonce not found'"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T27_CODE err='\$T27_ERR' body=\$T27_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T28 — GET /identity/me with no X-Nostr-Token header → 401 "Missing"
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 28 — GET /identity/me without token → 401"
|
||||
T28_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/identity/me")
|
||||
T28_BODY=\$(body_of "\$T28_RES"); T28_CODE=\$(code_of "\$T28_RES")
|
||||
T28_ERR=\$(echo "\$T28_BODY" | jq -r '.error' 2>/dev/null || echo "")
|
||||
if [[ "\$T28_CODE" == "401" && "\$T28_ERR" == *"Missing"* ]]; then
|
||||
note PASS "HTTP 401, error contains 'Missing'"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T28_CODE err='\$T28_ERR'"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T29 — GET /identity/me with invalid X-Nostr-Token → 401
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 29 — GET /identity/me with invalid token → 401"
|
||||
T29_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/identity/me" \\
|
||||
-H "X-Nostr-Token: totally.invalid.token")
|
||||
T29_BODY=\$(body_of "\$T29_RES"); T29_CODE=\$(code_of "\$T29_RES")
|
||||
T29_ERR=\$(echo "\$T29_BODY" | jq -r '.error' 2>/dev/null || echo "")
|
||||
if [[ "\$T29_CODE" == "401" && ( "\$T29_ERR" == *"Invalid"* || "\$T29_ERR" == *"expired"* ) ]]; then
|
||||
note PASS "HTTP 401, error: \${T29_ERR:0:80}"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T29_CODE err='\$T29_ERR'"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T30 — POST /sessions with bogus X-Nostr-Token → 401
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 30 — POST /sessions with bad token → 401"
|
||||
T30_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/sessions" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-Nostr-Token: badtoken" \\
|
||||
-d '{"amount_sats":200}')
|
||||
T30_BODY=\$(body_of "\$T30_RES"); T30_CODE=\$(code_of "\$T30_RES")
|
||||
T30_ERR=\$(echo "\$T30_BODY" | jq -r '.error' 2>/dev/null || echo "")
|
||||
T30_SESSION=\$(echo "\$T30_BODY" | jq -r '.sessionId' 2>/dev/null || echo "")
|
||||
if [[ "\$T30_CODE" == "401" && "\$T30_ERR" == *"Invalid or expired"* && -z "\$T30_SESSION" ]]; then
|
||||
note PASS "HTTP 401, error contains 'Invalid or expired', no sessionId created"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T30_CODE err='\$T30_ERR' sessionId='\$T30_SESSION'"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T31 — POST /jobs with bogus X-Nostr-Token → 401
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 31 — POST /jobs with bad token → 401"
|
||||
T31_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-Nostr-Token: badtoken" \\
|
||||
-d '{"request":"What is Bitcoin?"}')
|
||||
T31_BODY=\$(body_of "\$T31_RES"); T31_CODE=\$(code_of "\$T31_RES")
|
||||
T31_ERR=\$(echo "\$T31_BODY" | jq -r '.error' 2>/dev/null || echo "")
|
||||
if [[ "\$T31_CODE" == "401" && "\$T31_ERR" == *"Invalid or expired"* ]]; then
|
||||
note PASS "HTTP 401, error contains 'Invalid or expired'"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T31_CODE err='\$T31_ERR' body=\$T31_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T32 — POST /sessions (anonymous) includes trust_tier = "anonymous"
|
||||
# Capture T32_SESSION_ID for reuse in T35.
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 32 — POST /sessions anonymous → trust_tier=anonymous"
|
||||
T32_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/sessions" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"amount_sats":200}')
|
||||
T32_BODY=\$(body_of "\$T32_RES"); T32_CODE=\$(code_of "\$T32_RES")
|
||||
T32_SESSION_ID=\$(echo "\$T32_BODY" | jq -r '.sessionId' 2>/dev/null || echo "")
|
||||
T32_TIER=\$(echo "\$T32_BODY" | jq -r '.trust_tier' 2>/dev/null || echo "")
|
||||
if [[ "\$T32_CODE" == "201" && "\$T32_TIER" == "anonymous" && -n "\$T32_SESSION_ID" ]]; then
|
||||
note PASS "HTTP 201, trust_tier=anonymous, sessionId=\${T32_SESSION_ID:0:8}..."
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T32_CODE trust_tier='\$T32_TIER' body=\$T32_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T33 — POST /jobs (anonymous) includes trust_tier = "anonymous"
|
||||
# Capture T33_JOB_ID for reuse in T34.
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 33 — POST /jobs anonymous → trust_tier=anonymous"
|
||||
T33_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"request":"T33 trust tier probe"}')
|
||||
T33_BODY=\$(body_of "\$T33_RES"); T33_CODE=\$(code_of "\$T33_RES")
|
||||
T33_JOB_ID=\$(echo "\$T33_BODY" | jq -r '.jobId' 2>/dev/null || echo "")
|
||||
T33_TIER=\$(echo "\$T33_BODY" | jq -r '.trust_tier' 2>/dev/null || echo "")
|
||||
if [[ "\$T33_CODE" == "201" && "\$T33_TIER" == "anonymous" && -n "\$T33_JOB_ID" ]]; then
|
||||
note PASS "HTTP 201, trust_tier=anonymous, jobId=\${T33_JOB_ID:0:8}..."
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T33_CODE trust_tier='\$T33_TIER' body=\$T33_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T34 — GET /jobs/:id always includes trust_tier (uses T33_JOB_ID)
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 34 — GET /jobs/:id always includes trust_tier"
|
||||
if [[ -n "\$T33_JOB_ID" && "\$T33_JOB_ID" != "null" ]]; then
|
||||
T34_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/\$T33_JOB_ID")
|
||||
T34_BODY=\$(body_of "\$T34_RES"); T34_CODE=\$(code_of "\$T34_RES")
|
||||
T34_TIER=\$(echo "\$T34_BODY" | jq -r '.trust_tier' 2>/dev/null || echo "")
|
||||
if [[ "\$T34_CODE" == "200" && -n "\$T34_TIER" && "\$T34_TIER" != "null" && "\$T34_TIER" == "anonymous" ]]; then
|
||||
note PASS "HTTP 200, trust_tier=\$T34_TIER (anonymous job)"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T34_CODE trust_tier='\$T34_TIER' body=\$T34_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
else
|
||||
note SKIP "No T33_JOB_ID available — skipping"
|
||||
SKIP=\$((SKIP+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T35 — GET /sessions/:id always includes trust_tier (uses T32_SESSION_ID)
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 35 — GET /sessions/:id always includes trust_tier"
|
||||
if [[ -n "\$T32_SESSION_ID" && "\$T32_SESSION_ID" != "null" ]]; then
|
||||
T35_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/sessions/\$T32_SESSION_ID")
|
||||
T35_BODY=\$(body_of "\$T35_RES"); T35_CODE=\$(code_of "\$T35_RES")
|
||||
T35_TIER=\$(echo "\$T35_BODY" | jq -r '.trust_tier' 2>/dev/null || echo "")
|
||||
if [[ "\$T35_CODE" == "200" && "\$T35_TIER" == "anonymous" ]]; then
|
||||
note PASS "HTTP 200, trust_tier=\$T35_TIER"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T35_CODE trust_tier='\$T35_TIER' body=\$T35_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
else
|
||||
note SKIP "No T32_SESSION_ID available — skipping"
|
||||
SKIP=\$((SKIP+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T36 — Full challenge → sign → verify end-to-end flow (inline node script)
|
||||
# Uses nostr-tools CJS bundle (node_modules absolute path for portability).
|
||||
# Guards on node availability and script success; SKIP if either fails.
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 36 — Full challenge → sign → verify (Nostr identity flow)"
|
||||
T36_SKIP=false
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
note SKIP "node not found in PATH — skipping T36"
|
||||
SKIP=\$((SKIP+1))
|
||||
T36_SKIP=true
|
||||
fi
|
||||
|
||||
if [[ "\$T36_SKIP" == "false" ]]; then
|
||||
# Write a temp CommonJS script so we can import nostr-tools via require()
|
||||
# with an absolute path to the CJS bundle. Falls back to SKIP on any failure.
|
||||
T36_TMPFILE=\$(mktemp /tmp/nostr_t36_XXXXXX.cjs)
|
||||
cat > "\$T36_TMPFILE" << 'NODESCRIPT'
|
||||
'use strict';
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const BASE = process.argv[2];
|
||||
|
||||
// Try the absolute CJS bundle path first (dev/Replit), then bare module name.
|
||||
let nt;
|
||||
const NOSTR_CJS = '/home/runner/workspace/artifacts/api-server/node_modules/nostr-tools/lib/cjs/index.js';
|
||||
try { nt = require(NOSTR_CJS); } catch { try { nt = require('nostr-tools'); } catch { process.stderr.write('nostr-tools not importable\n'); process.exit(1); } }
|
||||
const { generateSecretKey, getPublicKey, finalizeEvent } = nt;
|
||||
|
||||
function request(url, opts, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const mod = u.protocol === 'https:' ? https : http;
|
||||
const req = mod.request(u, opts, (res) => {
|
||||
let data = '';
|
||||
res.on('data', c => data += c);
|
||||
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. Generate ephemeral keypair
|
||||
const sk = generateSecretKey();
|
||||
const pubkey = getPublicKey(sk);
|
||||
|
||||
// 2. Get challenge nonce
|
||||
const chalRes = await request(BASE + '/api/identity/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' } }, '{}');
|
||||
if (chalRes.status !== 200) { process.stderr.write('challenge failed: ' + chalRes.status + '\n'); process.exit(1); }
|
||||
const { nonce } = JSON.parse(chalRes.body);
|
||||
|
||||
// 3. Build and sign Nostr event (kind=27235, content=nonce)
|
||||
const event = finalizeEvent({ kind: 27235, content: nonce, tags: [], created_at: Math.floor(Date.now() / 1000) }, sk);
|
||||
|
||||
// 4. POST /identity/verify
|
||||
const verRes = await request(BASE + '/api/identity/verify',
|
||||
{ method: 'POST', headers: { 'Content-Type': 'application/json' } },
|
||||
JSON.stringify({ event }));
|
||||
if (verRes.status !== 200) { process.stderr.write('verify failed: ' + verRes.status + ' ' + verRes.body + '\n'); process.exit(1); }
|
||||
const verBody = JSON.parse(verRes.body);
|
||||
const token = verBody.nostr_token;
|
||||
const tier = verBody.trust && verBody.trust.tier;
|
||||
const icount = verBody.trust && verBody.trust.interactionCount;
|
||||
|
||||
// 5. GET /identity/me
|
||||
const meRes = await request(BASE + '/api/identity/me',
|
||||
{ method: 'GET', headers: { 'X-Nostr-Token': token } });
|
||||
if (meRes.status !== 200) { process.stderr.write('identity/me failed: ' + meRes.status + '\n'); process.exit(1); }
|
||||
const meBody = JSON.parse(meRes.body);
|
||||
const meTier = meBody.trust && meBody.trust.tier;
|
||||
const mePubkey = meBody.pubkey;
|
||||
|
||||
// Print result as single JSON line
|
||||
process.stdout.write(JSON.stringify({ pubkey, tier, icount, meTier, mePubkey }) + '\n');
|
||||
}
|
||||
main().catch(err => { process.stderr.write(String(err) + '\n'); process.exit(1); });
|
||||
NODESCRIPT
|
||||
|
||||
T36_OUT=\$(node "\$T36_TMPFILE" "\$BASE" 2>/dev/null)
|
||||
T36_EXIT=\$?
|
||||
rm -f "\$T36_TMPFILE"
|
||||
|
||||
if [[ \$T36_EXIT -ne 0 || -z "\$T36_OUT" ]]; then
|
||||
note SKIP "node script failed (nostr-tools unavailable or network error)"
|
||||
SKIP=\$((SKIP+1))
|
||||
else
|
||||
T36_TIER=\$(echo "\$T36_OUT" | jq -r '.tier' 2>/dev/null || echo "")
|
||||
T36_ICOUNT=\$(echo "\$T36_OUT" | jq -r '.icount' 2>/dev/null || echo "")
|
||||
T36_METIER=\$(echo "\$T36_OUT" | jq -r '.meTier' 2>/dev/null || echo "")
|
||||
T36_PK=\$(echo "\$T36_OUT" | jq -r '.pubkey' 2>/dev/null || echo "")
|
||||
T36_MEPK=\$(echo "\$T36_OUT" | jq -r '.mePubkey' 2>/dev/null || echo "")
|
||||
if [[ "\$T36_TIER" == "new" && "\$T36_ICOUNT" == "0" \\
|
||||
&& "\$T36_METIER" == "new" && "\$T36_PK" == "\$T36_MEPK" && -n "\$T36_PK" ]]; then
|
||||
note PASS "challenge→sign→verify OK: tier=\$T36_TIER icount=\$T36_ICOUNT identity/me pubkey matches"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "tier=\$T36_TIER icount=\$T36_ICOUNT meTier=\$T36_METIER pkMatch=\$([[ \$T36_PK == \$T36_MEPK ]] && echo yes || echo no)"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
fi
|
||||
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 (Task #27 — cost routing): GET /api/estimate returns cost preview
|
||||
# GET \$BASE/api/estimate?request=<text>
|
||||
# Assert HTTP 200, estimatedSats is a positive integer
|
||||
# Assert model, inputTokens, outputTokens are present
|
||||
#
|
||||
# FUTURE T38 (Task #27 — cost routing): 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 (Task #27 — free tier): 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 (Task #29 — Timmy as peer): Timmy initiates a zap
|
||||
# POST to /api/identity/me/tip (or similar)
|
||||
# Assert Timmy initiates a Lightning outbound payment to caller's LNURL
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user