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.
123 lines
4.2 KiB
JavaScript
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`);
|