prometheus/scripts/seed.mjs
Local Dev a9ca956c2e prometheus: standalone identity + first-wave Silent Mode listings
Four changes, one coherent pass:

1. Prometheus gets its own flame mark (SVG, not an emoji). Used in the
   topnav brand, in the hero at 88 px, and as the favicon. Replaces the
   🔥 glyph in the service and in site/prometheus/index.html.

2. silentmode.st/prometheus is now a brief introduction with two big
   buttons (Open prometheus.x · Open apps.silentmode.st mirror) and a
   short "what / who / status" block, instead of the full category
   grid that duplicated what prometheus.x already renders. The /prometheus
   page's job on silentmode.st is "point you at the real site," not host
   the content.

3. Footer on the Prometheus service drops the "Silent Mode — a Deviant
   project" byline and reframes the sibling-x links as "Also live in the
   decentralized web:" so Prometheus reads as its own site rather than a
   Silent Mode subpage. Keeps the attribution via the MPL-2.0 + source
   link on the final line. aegis.x and pithos.x added to the sibling row.

4. Four Silent Mode apps seeded as first-wave listings
   (scripts/seed-listings.json + scripts/seed.mjs). Theseus Navigator,
   Ariadne's Thread (Windows + Android), Pithos. Each carries the
   project's own SVG icon from public/icons/, pulled from the project's
   own site. The seed is a direct-DB insert (not through /api/listing)
   with signer_address = chipnet operator and signature = sentinel
   `SM_OPERATOR_SEED`; scan_report is an operator-attested envelope
   signed by the service key so the display pipeline is unchanged. Run
   with `node scripts/seed.mjs`; idempotent (skips existing (publisher,
   slug)).

   Deliberately NOT seeded: Theseus extensions that have no standalone
   form — pdf-editor, docx-editor, shield-blocker, bergamot provider.
   They ship via Theseus's own OTA, not as Prometheus listings. Aegis
   is in the same boat today (a Theseus bundled add-on without a
   standalone binary); its icon is included in public/icons/ for when
   it has its own distribution.

Real file sizes + SHAs come from the current dl.silentmode.st manifest
(theseus 0.3.89, ariadne resolver 0.1.13, ariadne android 0.22,
pithos-desktop 0.3.26). When APS1 lands cross-session, these listings
migrate to real on-chain records signed by each project's own publisher
key and the sentinel values go away.
2026-10-09 01:23:21 +02:00

123 lines
4.2 KiB
JavaScript

#!/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`);