#!/usr/bin/env node /** * End-to-end roundtrip for the listing endpoint. * * Not a CI test — a manual sanity check you can run against a locally running * service to confirm every link in the chain is wired. It: * * 1. generates a test key, derives the cashaddress, * 2. uploads a small file to /api/upload (requires the server with * MOCK_SCAN=clean), * 3. builds a listing body around the scan report, signs the canonical * message with the test key (BIP-137), * 4. POSTs it to /api/listing (requires MOCK_BNS_OWNER set to the test * address so the on-chain-owner check passes without a real chipnet * lookup), and * 5. reads /api/listings and prints the row. * * Run the service like this: * MOCK_SCAN=clean MOCK_BNS_OWNER= \ * node service/server.js * * Then from another terminal: * node scripts/test-listing-roundtrip.mjs */ import { signMessageBip137, addressFromPrivateKey } from '../service/lib/sign-message.js'; const BASE = process.env.BASE || 'http://127.0.0.1:3700'; const PUBLISHER = process.env.PUBLISHER || 'testpub.x'; const SLUG = process.env.SLUG || `hello-world-${Date.now()}`; const CATEGORY = process.env.CATEGORY || 'utilities'; // A hard-coded deterministic test key — this is NEVER a real wallet key, just // a predictable 32-byte scalar so the roundtrip script can print the matching // cashaddress BEFORE anyone sets MOCK_BNS_OWNER, instead of asking you to // chase your own tail. Override with TEST_PRIVKEY=<64 hex> if a different key // is useful (e.g. testing two publishers at once). const DEFAULT_TEST_PRIVKEY_HEX = '0101010101010101010101010101010101010101010101010101010101010101'; const PRIVKEY_HEX = process.env.TEST_PRIVKEY || DEFAULT_TEST_PRIVKEY_HEX; if (!/^[0-9a-f]{64}$/i.test(PRIVKEY_HEX)) { console.error('TEST_PRIVKEY must be 64 hex chars'); process.exit(2); } const privateKey = Buffer.from(PRIVKEY_HEX, 'hex'); const log = (...a) => console.log('[test]', ...a); const fail = (msg, extra) => { console.error('[test] FAIL:', msg); if (extra !== undefined) console.error(extra); process.exit(1); }; async function main() { // 1. derive the test address from the deterministic key const address = addressFromPrivateKey(privateKey); log(`test address: ${address}`); log(`→ server must have: MOCK_SCAN=clean MOCK_BNS_OWNER=${address}`); // 2. upload log('uploading a 20-byte sample file …'); const sample = Buffer.from(`prometheus listing test ${Date.now()}`); const form = new FormData(); form.append('file', new Blob([sample], { type: 'text/plain' }), 'sample.txt'); const upResp = await fetch(`${BASE}/api/upload`, { method: 'POST', body: form }); const upJson = await upResp.json(); if (!upResp.ok || !upJson.ok) fail('upload failed', upJson); log(`upload hash: ${upJson.upload.hash} size: ${upJson.upload.size} verdict: ${upJson.scan.verdict}`); // 3. build the listing const listing = { publisher: PUBLISHER, slug: SLUG, name: 'Hello, world', description: 'An unremarkable sample listing to prove the publisher-sign path works end-to-end.', category: CATEGORY, version: '0.1.0', homepage: 'https://example.com', icon: 'https://example.com/icon.png', download: { url: 'https://example.com/sample.txt', sha256: upJson.upload.hash, size: upJson.upload.size, }, scan_report: upJson.report, }; // 4. canonical signing message, same shape the service builds const message = buildSigningMessage(listing); const signature = signMessageBip137(message, privateKey); log(`signed ${message.length} bytes of canonical message`); const body = { ...listing, signature }; // 5. POST /api/listing log(`POST ${BASE}/api/listing …`); const resp = await fetch(`${BASE}/api/listing`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const json = await resp.json(); if (!resp.ok || !json.ok) fail(`POST returned ${resp.status}`, json); log(`✓ listing created: ${json.id}`); // 6. read it back via /api/listings/ and /api/listings?category=… const detail = await (await fetch(`${BASE}/api/listings/${json.id}`)).json(); if (!detail.ok) fail('GET detail failed', detail); log(`✓ GET /api/listings/${json.id} returned ${detail.listing.name} v${detail.listing.version}`); const index = await (await fetch(`${BASE}/api/listings?category=${CATEGORY}`)).json(); if (!index.ok) fail('GET index failed', index); const found = index.listings.find((l) => l.id === json.id); if (!found) fail(`listing ${json.id} missing from category index`, index); log(`✓ listing appears in /api/listings?category=${CATEGORY} (${index.count} total in category)`); log('all four checks passed.'); } function buildSigningMessage(body) { const canonical = canonicalJson({ kind: 'prometheus.listing', v: 1, publisher: String(body.publisher).toLowerCase(), slug: String(body.slug).toLowerCase(), name: String(body.name), description: String(body.description), category: String(body.category), version: String(body.version), homepage: body.homepage || null, icon: body.icon || null, download: { url: String(body.download.url), sha256: String(body.download.sha256), size: Number(body.download.size), }, scan_report_pubkey: body.scan_report.pubkey, scan_report_signature: body.scan_report.signature, }); return `PROMETHEUS-LISTING\n${canonical}`; } function canonicalJson(value) { if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return '[' + value.map(canonicalJson).join(',') + ']'; const keys = Object.keys(value).sort(); return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(value[k])).join(',') + '}'; } main().catch((err) => fail(err?.message || String(err), err?.stack));