@@ -29,6 +29,12 @@ 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.
* - T41 ADDED: POST /api/jobs with valid Nostr token → nostrPubkey in response matches identity.
* - T42 ADDED: POST /api/sessions with valid Nostr token → nostrPubkey in response matches identity.
* - T43 ADDED: GET /identity/me returns full trust fields (tier, score, interactionCount).
* - T44 ADDED: POST /identity/me/decay (test-only endpoint, 404 in prod) → score decremented.
* - T45 ADDED: GET /identity/leaderboard → HTTP 200, array sorted by trustScore desc.
* New endpoints identity/me/decay and identity/leaderboard added to identity.ts.
*/
router . get ( "/testkit" , ( req : Request , res : Response ) = > {
const proto =
@@ -1092,6 +1098,208 @@ NODESCRIPT
fi
fi
# ===========================================================================
# T41– T45 — Nostr identity lifecycle: token decorates jobs/sessions + trust ops
# Requires node + nostr-tools (same guard as T36). All five tests share one
# inline node script that performs the full lifecycle and emits a JSON blob.
# ===========================================================================
# ---------------------------------------------------------------------------
# T41– T45 Preamble — ephemeral keypair → challenge → sign → verify → token
# Then: create job, create session, GET /identity/me, decay, leaderboard.
# ---------------------------------------------------------------------------
NOSTR_LC_SKIP=false
NOSTR_LC_OUT=""
if ! command -v node >/dev/null 2>&1; then
NOSTR_LC_SKIP=true
fi
if [[ " \ $ NOSTR_LC_SKIP" == "false" ]]; then
NOSTR_LC_TMPFILE= \ $ (mktemp /tmp/nostr_lc_XXXXXX.cjs)
cat > " \ $ NOSTR_LC_TMPFILE" << 'NODESCRIPT'
'use strict';
const https = require('https');
const http = require('http');
const BASE = process.argv[2];
let nt;
const NOSTR_CJS = '/home/runner/workspace/artifacts/api-server/node_modules/nostr-tools/lib/cjs/index.js';
try { nt = require('nostr-tools'); } catch (_) { try { nt = require(NOSTR_CJS); } 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() {
const sk = generateSecretKey();
const pubkey = getPublicKey(sk);
// challenge → sign → verify
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);
const event = finalizeEvent({ kind: 27235, content: nonce, tags: [], created_at: Math.floor(Date.now() / 1000) }, sk);
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 { nostr_token: token } = JSON.parse(verRes.body);
// POST /jobs with Nostr token
const jobRes = await request(BASE + '/api/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Nostr-Token': token } }, JSON.stringify({ request: 'T41 Nostr job test' }));
const jobBody = JSON.parse(jobRes.body);
const jobCode = jobRes.status;
const jobId = jobBody.jobId || null;
const jobNpub = jobBody.nostrPubkey || null;
// POST /sessions with Nostr token
const sessRes = await request(BASE + '/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Nostr-Token': token } }, JSON.stringify({ amount_sats: 200 }));
const sessBody = JSON.parse(sessRes.body);
const sessCode = sessRes.status;
const sessId = sessBody.sessionId || null;
const sessNpub = sessBody.nostrPubkey || null;
// GET /identity/me
const meRes = await request(BASE + '/api/identity/me', { method: 'GET', headers: { 'X-Nostr-Token': token } });
const meBody = JSON.parse(meRes.body);
const meScore = meBody.trust ? meBody.trust.score : null;
const meTier = meBody.trust ? meBody.trust.tier : null;
const meIcount = meBody.trust ? meBody.trust.interactionCount : null;
// POST /identity/me/decay (test-only; non-200 → skip T44 gracefully)
const decayRes = await request(BASE + '/api/identity/me/decay', { method: 'POST', headers: { 'X-Nostr-Token': token } });
const decayBody = JSON.parse(decayRes.body);
const decayCode = decayRes.status;
const decayPrev = decayBody.previousScore !== undefined ? decayBody.previousScore : null;
const decayNew = decayBody.newScore !== undefined ? decayBody.newScore : null;
// GET /identity/leaderboard
const lbRes = await request(BASE + '/api/identity/leaderboard', { method: 'GET', headers: {} });
const lbCode = lbRes.status;
let lbBody = [];
try { lbBody = JSON.parse(lbRes.body); } catch (_) {}
const lbIsArray = Array.isArray(lbBody);
const lbSorted = lbIsArray && lbBody.length < 2 ? true :
lbIsArray && lbBody.every((v, i) => i === 0 || lbBody[i - 1].trustScore >= v.trustScore);
process.stdout.write(JSON.stringify({
pubkey, token,
jobCode, jobId, jobNpub,
sessCode, sessId, sessNpub,
meScore, meTier, meIcount,
decayCode, decayPrev, decayNew,
lbCode, lbIsArray, lbSorted,
}) + ' \ n');
}
main().catch(err => { process.stderr.write(String(err) + ' \ n'); process.exit(1); });
NODESCRIPT
NOSTR_LC_EXIT=0
NOSTR_LC_OUT= \ $ (node " \ $ NOSTR_LC_TMPFILE" " \ $ BASE" 2>/dev/null) || NOSTR_LC_EXIT= \ $ ?
rm -f " \ $ NOSTR_LC_TMPFILE"
if [[ \ $ NOSTR_LC_EXIT -ne 0 || -z " \ $ NOSTR_LC_OUT" ]]; then
NOSTR_LC_SKIP=true
fi
fi
# Helper: extract a field from NOSTR_LC_OUT
_lc() { echo " \ $ NOSTR_LC_OUT" | jq -r ". \ $ 1" 2>/dev/null || echo ""; }
# ---------------------------------------------------------------------------
# T41 — POST /jobs with valid Nostr token → nostrPubkey in response
# ---------------------------------------------------------------------------
sep "Test 41 — POST /jobs with Nostr token → nostrPubkey set"
if [[ " \ $ NOSTR_LC_SKIP" == "true" ]]; then
note SKIP "node unavailable or lifecycle preamble failed — skipping T41"
SKIP= \ $ ((SKIP+1))
else
T41_CODE= \ $ (_lc jobCode); T41_NPUB= \ $ (_lc jobNpub); T41_PK= \ $ (_lc pubkey)
if [[ " \ $ T41_CODE" == "201" && -n " \ $ T41_NPUB" && " \ $ T41_NPUB" != "null" && " \ $ T41_NPUB" == " \ $ T41_PK" ]]; then
note PASS "HTTP 201, nostrPubkey= \ ${ T41_NPUB :0 : 8 } ... matches token identity"
PASS= \ $ ((PASS+1))
else
note FAIL "code= \ $ T41_CODE nostrPubkey=' \ $ T41_NPUB' expected=' \ $ T41_PK'"
FAIL= \ $ ((FAIL+1))
fi
fi
# ---------------------------------------------------------------------------
# T42 — POST /sessions with valid Nostr token → nostrPubkey in response
# ---------------------------------------------------------------------------
sep "Test 42 — POST /sessions with Nostr token → nostrPubkey set"
if [[ " \ $ NOSTR_LC_SKIP" == "true" ]]; then
note SKIP "node unavailable or lifecycle preamble failed — skipping T42"
SKIP= \ $ ((SKIP+1))
else
T42_CODE= \ $ (_lc sessCode); T42_NPUB= \ $ (_lc sessNpub); T42_PK= \ $ (_lc pubkey)
if [[ " \ $ T42_CODE" == "201" && -n " \ $ T42_NPUB" && " \ $ T42_NPUB" != "null" && " \ $ T42_NPUB" == " \ $ T42_PK" ]]; then
note PASS "HTTP 201, nostrPubkey= \ ${ T42_NPUB :0 : 8 } ... matches token identity"
PASS= \ $ ((PASS+1))
else
note FAIL "code= \ $ T42_CODE nostrPubkey=' \ $ T42_NPUB' expected=' \ $ T42_PK'"
FAIL= \ $ ((FAIL+1))
fi
fi
# ---------------------------------------------------------------------------
# T43 — GET /identity/me returns full trust fields (tier, score, interactionCount)
# ---------------------------------------------------------------------------
sep "Test 43 — GET /identity/me returns tier + score + interactionCount"
if [[ " \ $ NOSTR_LC_SKIP" == "true" ]]; then
note SKIP "node unavailable or lifecycle preamble failed — skipping T43"
SKIP= \ $ ((SKIP+1))
else
T43_TIER= \ $ (_lc meTier); T43_SCORE= \ $ (_lc meScore); T43_ICOUNT= \ $ (_lc meIcount)
if [[ -n " \ $ T43_TIER" && " \ $ T43_TIER" != "null" \
&& " \ $ T43_SCORE" != "" && " \ $ T43_SCORE" != "null" \
&& " \ $ T43_ICOUNT" != "" && " \ $ T43_ICOUNT" != "null" ]]; then
note PASS "tier= \ $ T43_TIER score= \ $ T43_SCORE interactionCount= \ $ T43_ICOUNT"
PASS= \ $ ((PASS+1))
else
note FAIL "tier=' \ $ T43_TIER' score=' \ $ T43_SCORE' icount=' \ $ T43_ICOUNT'"
FAIL= \ $ ((FAIL+1))
fi
fi
# ---------------------------------------------------------------------------
# T44 — POST /identity/me/decay (test-only endpoint) → score decremented
# Skipped gracefully if endpoint returns non-200 (e.g., production mode).
# ---------------------------------------------------------------------------
sep "Test 44 — POST /identity/me/decay (test mode) → trust_score decremented"
if [[ " \ $ NOSTR_LC_SKIP" == "true" ]]; then
note SKIP "node unavailable or lifecycle preamble failed — skipping T44"
SKIP= \ $ ((SKIP+1))
else
T44_CODE= \ $ (_lc decayCode); T44_PREV= \ $ (_lc decayPrev); T44_NEW= \ $ (_lc decayNew)
if [[ " \ $ T44_CODE" != "200" ]]; then
note SKIP "decay endpoint returned code= \ $ T44_CODE (not in test mode) — skipping T44"
SKIP= \ $ ((SKIP+1))
elif [[ -n " \ $ T44_PREV" && -n " \ $ T44_NEW" && " \ $ T44_NEW" =~ ^[0-9]+ \ $ && " \ $ T44_PREV" =~ ^[0-9]+ \ $ && \ $ T44_NEW -le \ $ T44_PREV ]]; then
note PASS "previousScore= \ $ T44_PREV newScore= \ $ T44_NEW (decremented or floored at 0)"
PASS= \ $ ((PASS+1))
else
note FAIL "code= \ $ T44_CODE previousScore=' \ $ T44_PREV' newScore=' \ $ T44_NEW' (expected new ≤ prev)"
FAIL= \ $ ((FAIL+1))
fi
fi
# ---------------------------------------------------------------------------
# T45 — GET /identity/leaderboard → HTTP 200, array sorted by trust score
# ---------------------------------------------------------------------------
sep "Test 45 — GET /identity/leaderboard → sorted array"
if [[ " \ $ NOSTR_LC_SKIP" == "true" ]]; then
note SKIP "node unavailable or lifecycle preamble failed — skipping T45"
SKIP= \ $ ((SKIP+1))
else
T45_CODE= \ $ (_lc lbCode); T45_ARRAY= \ $ (_lc lbIsArray); T45_SORTED= \ $ (_lc lbSorted)
if [[ " \ $ T45_CODE" == "200" && " \ $ T45_ARRAY" == "true" && " \ $ T45_SORTED" == "true" ]]; then
note PASS "HTTP 200, array returned and sorted by trustScore desc"
PASS= \ $ ((PASS+1))
else
note FAIL "code= \ $ T45_CODE isArray= \ $ T45_ARRAY sorted= \ $ T45_SORTED"
FAIL= \ $ ((FAIL+1))
fi
fi
# ===========================================================================
# FUTURE STUBS — placeholders for upcoming tasks (do not affect PASS/FAIL)
# ===========================================================================