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:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
# ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user