#!/usr/bin/env node /** * Seed the first-wave Silent Mode listings into the local DB. * * This is a one-time (idempotent) direct-DB insert, NOT a POST through * /api/listing. The listings in scripts/seed-listings.json are Silent Mode's * own apps — they get a special `signer_address` (the chipnet operator) and * sentinel `signature` + `signed_message` fields so a reader of the DB can * tell at a glance that these were operator-seeded rather than publisher-POSTed. * * When APS1 lands cross-session, these listings migrate to real on-chain * records signed by each project's actual publisher key, and the sentinel * values go away. * * Why bypass /api/listing: the publisher-signature gate on that endpoint * requires an actual BIP-137 signature from the on-chain owner of each * publisher name. Theseus's, Ariadne's and Pithos's publisher keys live on * machines that are not where this seed runs, and asking each project owner * to produce a signature for a seed import is friction that buys nothing — * the operator vouches for these as a bootstrap, and the admin audit log * records who ran the seed and when. * * Usage (locally, against a dev DB): * node scripts/seed.mjs * * On the VPS (against the live service's volume): * docker exec prometheus-prometheus-1 node scripts/seed.mjs */ import { readFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { db, getListing } from '../service/db.js'; import { signReport } from '../service/signing.js'; const HERE = dirname(fileURLToPath(import.meta.url)); const SEED_FILE = resolve(HERE, 'seed-listings.json'); const seed = JSON.parse(readFileSync(SEED_FILE, 'utf8')); const { operator, listings } = seed; if (!operator?.address || !/^(bitcoincash|bchtest):q[a-z0-9]{41}$/.test(operator.address)) { console.error('seed-listings.json: operator.address missing or malformed'); process.exit(1); } const insert = db.prepare(` INSERT INTO listings ( id, publisher, slug, name, description, category, version, homepage, icon, download_url, download_sha256, download_size, scan_report, signer_address, signature, signed_message, created_at, payment_status ) VALUES ( @id, @publisher, @slug, @name, @description, @category, @version, @homepage, @icon, @download_url, @download_sha256, @download_size, @scan_report, @signer_address, @signature, @signed_message, @created_at, 'free' ) `); let inserted = 0, skipped = 0; for (const l of listings) { const id = `${l.publisher.toLowerCase()}/${l.slug.toLowerCase()}`; if (getListing(id)) { console.log(`skip ${id} (already present)`); skipped += 1; continue; } // The scan report is an "operator-attested" envelope — same shape as a // real one from /api/upload so the display code is unchanged, but signed // by this service's key (not a real ClamAV scan). When APS1 lands, these // listings re-publish with real scans; for now the operator attests. const scanPayload = { v: 1, kind: 'prometheus.scan-report', service: 'prometheus', issued_at: new Date().toISOString(), upload: { hash: l.download_sha256, size: l.download_size, name: l.download_url.split('/').pop(), mime: 'application/octet-stream', }, scan: { verdict: 'clean', engine: 'operator-attested', signatures: [], duration_ms: 0, }, stored: false, attested_by: operator.publisher, }; const scanReport = signReport(scanPayload); const now = new Date().toISOString(); const row = { id, publisher: l.publisher.toLowerCase(), slug: l.slug.toLowerCase(), name: l.name, description: l.description, category: l.category, version: l.version, homepage: l.homepage || null, icon: l.icon || null, download_url: l.download_url, download_sha256: l.download_sha256, download_size: l.download_size, scan_report: JSON.stringify(scanReport), signer_address: operator.address, signature: 'SM_OPERATOR_SEED', signed_message: 'SM_OPERATOR_SEED', created_at: now, }; insert.run(row); console.log(`add ${id}`); inserted += 1; } console.log(`\nseed: ${inserted} inserted, ${skipped} already present`);