WIP: Claude Code progress on #55

Automated salvage commit — agent session ended (exit 124).
Work in progress, may need continuation.
This commit is contained in:
Alexander Whitestone
2026-03-23 16:19:49 -04:00
parent 113095d2f0
commit 3a9dbd80e8
2 changed files with 286 additions and 17 deletions

View File

@@ -2,7 +2,7 @@ import { Router, type Request, type Response } from "express";
import { randomBytes, randomUUID } from "crypto";
import { verifyEvent, validateEvent } from "nostr-tools";
import { db, nostrTrustVouches, nostrIdentities, timmyNostrEvents } from "@workspace/db";
import { eq, count } from "drizzle-orm";
import { eq, count, desc } from "drizzle-orm";
import { trustService } from "../lib/trust.js";
import { timmyIdentityService } from "../lib/timmy-identity.js";
import { makeLogger } from "../lib/logger.js";
@@ -406,4 +406,85 @@ router.get("/identity/me", async (req: Request, res: Response) => {
}
});
// ── GET /identity/leaderboard ─────────────────────────────────────────────────
// Returns the top 20 verified Nostr identities sorted by trust score descending.
// Public endpoint — no authentication required.
router.get("/identity/leaderboard", async (_req: Request, res: Response) => {
try {
const rows = await db
.select({
pubkey: nostrIdentities.pubkey,
trustScore: nostrIdentities.trustScore,
tier: nostrIdentities.tier,
interactionCount: nostrIdentities.interactionCount,
memberSince: nostrIdentities.createdAt,
})
.from(nostrIdentities)
.orderBy(desc(nostrIdentities.trustScore))
.limit(20);
res.json({ leaderboard: rows });
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to fetch leaderboard" });
}
});
// ── POST /identity/me/decay ───────────────────────────────────────────────────
// TEST-ONLY endpoint: triggers one synthetic decay cycle for the authenticated
// identity, decrementing trust_score by the configured DECAY_PER_DAY constant.
// Disabled in production (returns 404). Used by T37 in the testkit.
const DECAY_PER_DAY_TEST = parseInt(process.env["TRUST_DECAY_PER_DAY"] ?? "1", 10) || 1;
router.post("/identity/me/decay", async (req: Request, res: Response) => {
if (process.env["NODE_ENV"] === "production") {
res.status(404).json({ error: "Not found" });
return;
}
const raw = req.headers["x-nostr-token"];
const token = typeof raw === "string" ? raw.trim() : null;
if (!token) {
res.status(401).json({ error: "Missing X-Nostr-Token header" });
return;
}
const parsed = trustService.verifyToken(token);
if (!parsed) {
res.status(401).json({ error: "Invalid or expired nostr_token" });
return;
}
try {
const identity = await trustService.getIdentity(parsed.pubkey);
if (!identity) {
res.status(404).json({ error: "Identity not found" });
return;
}
const newScore = Math.max(0, identity.trustScore - DECAY_PER_DAY_TEST);
await db
.update(nostrIdentities)
.set({ trustScore: newScore, updatedAt: new Date() })
.where(eq(nostrIdentities.pubkey, parsed.pubkey));
logger.info("test decay applied", {
pubkey: parsed.pubkey.slice(0, 8),
before: identity.trustScore,
after: newScore,
});
res.json({
pubkey: parsed.pubkey,
trust_score_before: identity.trustScore,
trust_score_after: newScore,
decayed_by: identity.trustScore - newScore,
});
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to apply decay" });
}
});
export default router;

View File

@@ -29,6 +29,10 @@ 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.
* - T36 EXTENDED: verify response shape (nostr_token + trust.tier), POST /jobs + GET /jobs/:id
* with valid token → nostrPubkey set, POST /sessions + GET /sessions/:id trust_tier.
* - T37 ADDED: POST /api/identity/me/decay (test-only endpoint) → trust_score decremented.
* - T38 ADDED: GET /api/identity/leaderboard → array sorted by trustScore desc.
*/
router.get("/testkit", (req: Request, res: Response) => {
const proto =
@@ -1042,26 +1046,92 @@ async function main() {
// 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
// 4. POST /identity/verify — assert response shape: nostr_token + trust.tier
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;
const token = verBody.nostr_token;
const tier = verBody.trust && verBody.trust.tier;
const icount = verBody.trust && verBody.trust.interactionCount;
const verifyHasToken = typeof token === 'string' && token.length > 0;
const verifyHasTier = typeof tier === 'string' && tier.length > 0;
// 5. GET /identity/me
// 5. GET /identity/me — assert trust fields + pubkey round-trip
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 meBody = JSON.parse(meRes.body);
const meTier = meBody.trust && meBody.trust.tier;
const mePubkey = meBody.pubkey;
const meTrustFields = meBody.trust &&
typeof meBody.trust.tier === 'string' &&
typeof meBody.trust.score === 'number' &&
typeof meBody.trust.interactionCount === 'number';
// 6. POST /jobs with valid Nostr token — assert nostrPubkey in response
let jobPubkey = null;
let jobId = null;
try {
const jobRes = await request(BASE + '/api/jobs',
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Nostr-Token': token } },
JSON.stringify({ request: 'T36 nostrPubkey probe' }));
if (jobRes.status === 201) {
const jobBody = JSON.parse(jobRes.body);
jobPubkey = jobBody.nostrPubkey || null;
jobId = jobBody.jobId || null;
}
} catch (_) { /* network error — leave null */ }
// 7. GET /jobs/:id — assert nostrPubkey present if job was created
let getJobPubkey = null;
if (jobId) {
try {
const gjRes = await request(BASE + '/api/jobs/' + jobId, { method: 'GET', headers: {} });
if (gjRes.status === 200) {
const gjBody = JSON.parse(gjRes.body);
getJobPubkey = gjBody.nostrPubkey || null;
}
} catch (_) { /* ignore */ }
}
// 8. POST /sessions with valid Nostr token — assert nostrPubkey in response
let sessionPubkey = null;
let sessionId = null;
let sessionTier = null;
try {
const sessRes = await request(BASE + '/api/sessions',
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Nostr-Token': token } },
JSON.stringify({ amount_sats: 200 }));
if (sessRes.status === 201) {
const sessBody = JSON.parse(sessRes.body);
sessionPubkey = sessBody.nostrPubkey || null;
sessionId = sessBody.sessionId || null;
sessionTier = sessBody.trust_tier || null;
}
} catch (_) { /* network error — leave null */ }
// 9. GET /sessions/:id — assert trust_tier present
let getSessionTier = null;
if (sessionId) {
try {
const gsRes = await request(BASE + '/api/sessions/' + sessionId, { method: 'GET', headers: {} });
if (gsRes.status === 200) {
const gsBody = JSON.parse(gsRes.body);
getSessionTier = gsBody.trust_tier || null;
}
} catch (_) { /* ignore */ }
}
// Print result as single JSON line
process.stdout.write(JSON.stringify({ pubkey, tier, icount, meTier, mePubkey }) + '\n');
process.stdout.write(JSON.stringify({
pubkey, tier, icount, meTier, mePubkey,
verifyHasToken, verifyHasTier, meTrustFields,
jobPubkey, getJobPubkey,
sessionPubkey, getSessionTier,
token,
}) + '\n');
}
main().catch(err => { process.stderr.write(String(err) + '\n'); process.exit(1); });
NODESCRIPT
@@ -1072,26 +1142,144 @@ NODESCRIPT
T36_OUT=\$(node "\$T36_TMPFILE" "\$BASE" 2>/dev/null) || T36_EXIT=\$?
rm -f "\$T36_TMPFILE"
# Stash the token for T37 (decay) if the node script succeeded
T36_TOKEN=\$(echo "\$T36_OUT" | jq -r '.token' 2>/dev/null || echo "")
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 "")
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 "")
T36_VER_TOKEN=\$(echo "\$T36_OUT" | jq -r '.verifyHasToken' 2>/dev/null || echo "false")
T36_VER_TIER=\$(echo "\$T36_OUT" | jq -r '.verifyHasTier' 2>/dev/null || echo "false")
T36_ME_FIELDS=\$(echo "\$T36_OUT" | jq -r '.meTrustFields' 2>/dev/null || echo "false")
T36_JOB_PK=\$(echo "\$T36_OUT" | jq -r '.jobPubkey' 2>/dev/null || echo "")
T36_GJOB_PK=\$(echo "\$T36_OUT" | jq -r '.getJobPubkey' 2>/dev/null || echo "")
T36_SESS_PK=\$(echo "\$T36_OUT" | jq -r '.sessionPubkey' 2>/dev/null || echo "")
T36_GSESS_T=\$(echo "\$T36_OUT" | jq -r '.getSessionTier' 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"
&& "\$T36_METIER" == "new" && "\$T36_PK" == "\$T36_MEPK" && -n "\$T36_PK" \\
&& "\$T36_VER_TOKEN" == "true" && "\$T36_VER_TIER" == "true" \\
&& "\$T36_ME_FIELDS" == "true" ]]; then
note PASS "challenge→sign→verify OK: tier=\$T36_TIER icount=\$T36_ICOUNT identity/me pubkey matches, response fields verified"
PASS=\$((PASS+1))
else
note FAIL "tier=\$T36_TIER icount=\$T36_ICOUNT meTier=\$T36_METIER pkMatch=\$([[ \$T36_PK == \$T36_MEPK ]] && echo yes || echo no)"
note FAIL "tier=\$T36_TIER icount=\$T36_ICOUNT meTier=\$T36_METIER pkMatch=\$([[ \$T36_PK == \$T36_MEPK ]] && echo yes || echo no) verToken=\$T36_VER_TOKEN verTier=\$T36_VER_TIER meFields=\$T36_ME_FIELDS"
FAIL=\$((FAIL+1))
fi
# Sub-assertion: POST /jobs with valid Nostr token → nostrPubkey set
if [[ -n "\$T36_JOB_PK" && "\$T36_JOB_PK" != "null" && "\$T36_JOB_PK" == "\$T36_PK" ]]; then
note PASS "POST /jobs with token → nostrPubkey=\${T36_JOB_PK:0:8}... matches identity pubkey"
PASS=\$((PASS+1))
elif [[ -z "\$T36_JOB_PK" || "\$T36_JOB_PK" == "null" ]]; then
note SKIP "POST /jobs did not return nostrPubkey (job creation may have failed in eval/payment gate)"
SKIP=\$((SKIP+1))
else
note FAIL "POST /jobs nostrPubkey='\$T36_JOB_PK' does not match identity pubkey '\$T36_PK'"
FAIL=\$((FAIL+1))
fi
# Sub-assertion: GET /jobs/:id → nostrPubkey present
if [[ -n "\$T36_GJOB_PK" && "\$T36_GJOB_PK" != "null" && "\$T36_GJOB_PK" == "\$T36_PK" ]]; then
note PASS "GET /jobs/:id → nostrPubkey=\${T36_GJOB_PK:0:8}... matches"
PASS=\$((PASS+1))
elif [[ -z "\$T36_GJOB_PK" || "\$T36_GJOB_PK" == "null" ]]; then
note SKIP "GET /jobs/:id nostrPubkey not present (skipped if job creation was skipped)"
SKIP=\$((SKIP+1))
else
note FAIL "GET /jobs/:id nostrPubkey='\$T36_GJOB_PK' mismatch"
FAIL=\$((FAIL+1))
fi
# Sub-assertion: POST /sessions with valid Nostr token → nostrPubkey set
if [[ -n "\$T36_SESS_PK" && "\$T36_SESS_PK" != "null" && "\$T36_SESS_PK" == "\$T36_PK" ]]; then
note PASS "POST /sessions with token → nostrPubkey=\${T36_SESS_PK:0:8}... matches"
PASS=\$((PASS+1))
elif [[ -z "\$T36_SESS_PK" || "\$T36_SESS_PK" == "null" ]]; then
note SKIP "POST /sessions did not return nostrPubkey"
SKIP=\$((SKIP+1))
else
note FAIL "POST /sessions nostrPubkey='\$T36_SESS_PK' mismatch"
FAIL=\$((FAIL+1))
fi
# Sub-assertion: GET /sessions/:id → trust_tier present
if [[ -n "\$T36_GSESS_T" && "\$T36_GSESS_T" != "null" ]]; then
note PASS "GET /sessions/:id → trust_tier=\$T36_GSESS_T present"
PASS=\$((PASS+1))
else
note SKIP "GET /sessions/:id trust_tier absent (session creation may have been skipped)"
SKIP=\$((SKIP+1))
fi
fi
fi
# ---------------------------------------------------------------------------
# T37 — POST /identity/me/decay (test mode) → trust_score decremented
# Uses T36_TOKEN if available; SKIPs if node script failed or endpoint absent.
# ---------------------------------------------------------------------------
sep "Test 37 — POST /identity/me/decay → trust_score decremented (test-only endpoint)"
if [[ -z "\$T36_TOKEN" || "\$T36_TOKEN" == "null" ]]; then
note SKIP "No T36_TOKEN available (T36 node script did not run)"
SKIP=\$((SKIP+1))
else
T37_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/identity/me/decay" \\
-H "X-Nostr-Token: \$T36_TOKEN")
T37_BODY=\$(body_of "\$T37_RES"); T37_CODE=\$(code_of "\$T37_RES")
if [[ "\$T37_CODE" == "404" ]]; then
note SKIP "Endpoint returned 404 — disabled in production or not yet deployed"
SKIP=\$((SKIP+1))
else
T37_BEFORE=\$(echo "\$T37_BODY" | jq -r '.trust_score_before' 2>/dev/null || echo "")
T37_AFTER=\$(echo "\$T37_BODY" | jq -r '.trust_score_after' 2>/dev/null || echo "")
T37_DECAYED=\$(echo "\$T37_BODY" | jq -r '.decayed_by' 2>/dev/null || echo "")
# trust_score_before=0, after=0 (already at floor), decayed_by=0 is still valid
if [[ "\$T37_CODE" == "200" \\
&& "\$T37_BEFORE" =~ ^[0-9]+\$ \\
&& "\$T37_AFTER" =~ ^[0-9]+\$ \\
&& "\$T37_AFTER" -le "\$T37_BEFORE" ]]; then
note PASS "HTTP 200, trust_score_before=\$T37_BEFORE trust_score_after=\$T37_AFTER decayed_by=\$T37_DECAYED"
PASS=\$((PASS+1))
else
note FAIL "code=\$T37_CODE before='\$T37_BEFORE' after='\$T37_AFTER' body=\$T37_BODY"
FAIL=\$((FAIL+1))
fi
fi
fi
# ---------------------------------------------------------------------------
# T38 — GET /identity/leaderboard → array sorted by trust score
# ---------------------------------------------------------------------------
sep "Test 38 — GET /identity/leaderboard → sorted array"
T38_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/identity/leaderboard")
T38_BODY=\$(body_of "\$T38_RES"); T38_CODE=\$(code_of "\$T38_RES")
T38_IS_ARRAY=\$(echo "\$T38_BODY" | jq '.leaderboard | type == "array"' 2>/dev/null || echo "false")
T38_SORTED=false
if [[ "\$T38_IS_ARRAY" == "true" ]]; then
# Verify sorted: jq returns true if each element's trustScore >= next element's
T38_SORTED=\$(echo "\$T38_BODY" | jq '
.leaderboard as \$a |
if (\$a | length) <= 1 then true
else
[range(\$a | length - 1)] |
all(. as \$i | \$a[\$i].trustScore >= \$a[\$i+1].trustScore)
end
' 2>/dev/null || echo "false")
fi
if [[ "\$T38_CODE" == "200" && "\$T38_IS_ARRAY" == "true" && "\$T38_SORTED" == "true" ]]; then
T38_LEN=\$(echo "\$T38_BODY" | jq '.leaderboard | length' 2>/dev/null || echo "?")
note PASS "HTTP 200, leaderboard array length=\$T38_LEN, sorted by trustScore desc"
PASS=\$((PASS+1))
else
note FAIL "code=\$T38_CODE isArray=\$T38_IS_ARRAY sorted=\$T38_SORTED body=\$T38_BODY"
FAIL=\$((FAIL+1))
fi
# ===========================================================================
# FUTURE STUBS — placeholders for upcoming tasks (do not affect PASS/FAIL)
# ===========================================================================