Commit graph

17 commits

Author SHA1 Message Date
Local Dev
d6f8504bfe prometheus: SEO terms in the Browsers & web tools blurb
Readers searching for "web3 browsers" or "decentralized domain names"
weren't finding the Browsers & web tools category because the blurb
only talked about BCNR, which almost nobody types into a search box.

Blurb reworded to:
  Web3 browsers and decentralized domain name resolvers. BCNR tooling
  that teaches Chrome, Firefox and other browsers to reach .x and the
  other decentralized TLDs.

Same category, same scope — new terms up front so a crawler reading
the sub-chip on /t/apps, the category hero on /c/browsers, or the
<meta description> on either page sees the words a web3-curious reader
would type to find it. The listing cards for Ariadne's Thread, Ariadne
Navigator and Theseus Navigator inside the category pick up the SEO
value by association.

Single file: the blurb lives once in CATEGORIES and propagates to
every surface that renders it.
2026-10-22 22:50:24 +03:00
Local Dev
1192f38ea5 prometheus: taxonomy — Apps / Games / FinTech, with sub-chips inside Apps
Three top-level groups replace the flat eight categories on the home
page and /browse:

  Apps        7 sub-categories inside: browsers / communication /
              privacy / devtools / media / productivity / other
  Games       single bucket for now; sub-chips later when there's volume
  FinTech     single bucket for now (BCH wallets, bridges, POS, DeFi)

Routes:
  /t/<slug>   top-level page. For Apps shows the 7 sub-chips + an
              aggregated listings view across them. For Games and
              FinTech shows the listings directly (no sub-chips).
  /c/<slug>   sub-category page under Apps (browsers, communication, …).
              The sole-sub categories /c/games and /c/fintech 301 to
              /t/games and /t/fintech respectively, so each view has
              exactly one canonical URL.

Slug renames + removals in the CATEGORIES allow-list on listings.js:
  utilities → productivity   (the display label was already pointing at
                              that in people's heads — "productivity"
                              fits storage + sync + file tools better)
  wallets   → fintech         (broader scope: bridges + POS + DeFi,
                              not just wallets)
  games                       stays as a top-level sole-bucket
  browsers                    NEW sub-category under Apps for BCNR
                              resolvers, mobile browsers, extensions

Seed listings re-categorised + two slug fixes:
  theseus.x/theseus-navigator-win       category: other → browsers
  ariadne.x/ariadne-resolver-win        → RENAMED to ariadne-thread-win,
                                           name "Ariadne's Thread
                                           (Windows resolver)" shortened
                                           to just "Ariadne's Thread";
                                           category: privacy → browsers
  ariadne.x/ariadne-android             → RENAMED to ariadne-navigator,
                                           name "Ariadne's Thread
                                           (Android)" → "Ariadne
                                           Navigator" (it is a
                                           different program, not a
                                           platform build of Thread);
                                           category: privacy → browsers
  pithos.x/pithos-desktop                category: utilities → productivity

scripts/migrate-taxonomy.mjs is a one-shot that deletes every row with
signature='SM_OPERATOR_SEED'; a subsequent `seed.mjs` run then inserts
the fresh set with the new slugs. Publisher-POSTed listings (none in
production today) are left alone. Idempotent — a second run finds
nothing to remove.

New icons:
  top-apps.svg       2×2 rounded tile grid (OS launcher metaphor)
  cat-browsers.svg   browser window + 3 chrome dots + content block

Three icons reused: cat-games.svg doubles as the Games top icon,
cat-wallets.svg as the FinTech top icon (same visual, different label);
cat-utilities.svg stays as the Productivity sub icon.
2026-10-22 22:39:08 +03:00
Local Dev
8e0830f046 prometheus: SVG icons for the 8 categories
Replaces the emoji glyphs (💬🛡️💸🎮🧰🎧⚙️✨) in the category grid with
dedicated SVG icons under /static/icons/cat-<slug>.svg, following the
same visual family as the sibling .x marks (theseus, sirius, ariadne,
pithos, aegis): 12-radius dark square + acid-green mark, legible at
16 px tab-icon size and 40 px grid size.

Designs chosen to not blur with the Silent Mode product icons also
rendered on the site:
  cat-communication  speech bubble + three dots (not an envelope — too
                     close to the Hephaestus letter imagery elsewhere)
  cat-privacy        shield + checkmark (DISTINCT from aegis.svg's
                     shield+chevron, so category + listing don't collide)
  cat-wallets        stacked coins + B serif (hints at the BCH coin)
  cat-games          stylised controller (d-pad + two action buttons)
  cat-devtools       code brackets < / > (reads as "code" at 16 px
                     where a wrench/hammer blurs with the Utilities gear)
  cat-media          play triangle in a circle (equally "player" and
                     "publish/broadcast")
  cat-utilities      gear with hollow centre (less visual weight than
                     the Media circle; the two side-by-side don't
                     crowd each other)
  cat-other          sparkle burst + two smaller sparkles (sibling of
                     sirius.svg's 4-point mark, extras so the two don't
                     look identical)

pages-browse.js: CATEGORIES[].icon is now the path to the SVG, not an
emoji string. The three places the icon rendered — grid card, category
hero, and the inline chip on listing detail — all now use <img> at the
size they need (40, 56, 14 respectively). The CSS entry
`.cat .ico` loses the font-size + grayscale (text styling) and gains
width/height for the image plus a tiny hover rotation that matches the
existing lift transform.

Also: when a listing has no `icon` of its own, the detail page's icon
slot now falls back to the category's SVG (centered + padded) instead
of a plain 📦 emoji, so the detail hero always has a vector icon.
2026-10-22 21:30:13 +03:00
Local Dev
a6606d0c49 prometheus: rework snapshot timer unit for the docker deploy
The .service unit assumed a native systemd install (ExecStart=/usr/bin/node
/opt/prometheus/scripts/snapshot.mjs --upload) that does not match how the
service actually runs on p2pexchange — it is a docker-compose sibling of
Hephaestus's Caddy stack, not a user=prometheus host process.

Two phases now:
  1. docker exec into the running prometheus container, render the static
     mirror into /data/build (which is in the container's data volume on
     the host at /var/lib/docker/volumes/prometheus_data/_data/build).
  2. On the host, aws --profile sia-storage s3 sync that directory to
     bns/prometheus/, using the operator's existing sia-storage credentials
     per [[sia-storage-credentials-location]].

Keeping aws CLI out of the service image is a deliberate trade — adding
it would bloat the image by ~100 MB for a once-a-week job the host is
already set up to run.

Not yet installed on the VPS. To enable:
  install -m644 ~/prometheus/deploy/prometheus-snapshot.{service,timer} \
    /etc/systemd/system/
  systemctl daemon-reload
  systemctl enable --now prometheus-snapshot.timer
2026-10-11 00:58:53 +02:00
Local Dev
998d0b0fca prometheus: add ClamAV sidecar — real uploads can scan now
Before this: service's /healthz always 503'd and the upload endpoint
rejected every submission with scan verdict=error, because clamd was not
installed anywhere reachable. Service worked for the seeded browse view
but no publisher could POST a listing.

Now: a `clamav` sibling container runs the official clamav/clamav:stable
image on an internal docker network only Prometheus can reach. The
Prometheus service connects via CLAMD_HOST=clamav. Signatures live in a
dedicated volume so restarts reuse the ~250 MB freshclam downloaded on
first start.

Two networks on the Prometheus container now:
  prometheus-internal   bridge, service + clamav only; clamd never exposed
  hephaestus            external hephaestus_default, so Hephaestus's Caddy
                        continues to route prometheus.x → prometheus:3700

depends_on with condition=service_healthy means Prometheus waits for
clamd's healthcheck to pass before starting. clamav's start_period is
5 min to accommodate the signature download; the healthcheck itself
is clamdcheck.sh / clamdscan --ping with a 10s timeout per probe.

Deploy on the VPS:
  cd ~/prometheus && git pull && cd deploy
  docker compose pull clamav   # first time, 500+ MB
  docker compose up -d         # Prometheus will wait for clamav healthy
2026-10-09 21:58:56 +02:00
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
Local Dev
7950430aa0 prometheus: Dockerfile + docker-compose for the sidecar deploy
The VPS runs Caddy inside Docker as part of the Hephaestus stack (not
system-wide angie as the earlier deploy/README assumed), so the shape
of the deploy is: Prometheus runs as its own small docker-compose that
attaches to Hephaestus's existing network, and Hephaestus's Caddy
reverse-proxies prometheus.x → prometheus:3700.

Dockerfile: node:22-alpine base (keeps the image ~55 MB and gives us
node:sqlite stable), tini for signal handling, non-root `node` user,
one /data volume holding the DB + ed25519 service key + uploads +
snapshot build output.

docker-compose: one service, attaches hephaestus_default as external
network, env sourced from an .env file alongside the compose. The .env
is NOT committed (contains ADMIN_PUBLISHER, optional VirusTotal key,
optional payment config). Healthcheck polls /healthz.

The deploy/prometheus-service.service systemd unit from the earlier
commit is now unused on this host (kept for anyone deploying natively
on a non-dockered box).
2026-10-09 00:47:40 +02:00
Local Dev
40bf5fc641 prometheus: deploy — systemd unit, angie vhost, TLS pin, deploy recipe
Everything the VPS needs to run Prometheus is in Prometheus/deploy/:

  prometheus-service.service   systemd unit, Node on 127.0.0.1:3700,
                               ReadWritePaths narrowed to the dirs the
                               service actually writes (db, uploads, keys,
                               build). Hardened with NoNewPrivileges,
                               PrivateTmp, ProtectSystem=strict,
                               ProtectHome. Reads /etc/default/prometheus
                               for instance env (admin name, VT key,
                               payment config) so the repo carries no
                               per-install secrets.

  prometheus-x.conf            angie vhost for the BNS name's apex.
                               TLS-terminated with a self-signed cert that
                               the on-chain `tls` record pins (no CA
                               involved). Reverse-proxies / to the Node
                               service, 2 GiB client_max_body_size for
                               POST /api/upload, proxy_request_buffering
                               off so large uploads do not spool to disk.

  make-tls-cert.sh             ECDSA P-256, 10-year expiry (the on-chain
                               pin is what actually expires, not the cert
                               metadata). Prints the DER sha256 that goes
                               in the `tls` record. Idempotent — rerun to
                               rotate.

  README.md                    First-time install recipe (useradd →
                               rsync → npm install --omit=dev → make-tls
                               → angie reload → systemctl enable --now),
                               the matching Argus register.js command for
                               the record change, update + rollback.

Execution — the deploy itself, the record change, enabling payments —
is the operator's call, not something a script here runs on its own.
2026-10-08 21:53:40 +02:00
Local Dev
34efb319cc prometheus: BCH payments — invoices, price oracle, admin reconcile
Payment flow for the paid wave (the free wave is still the default):

  1. Publisher POSTs /api/listing. If PAYMENT_REQUIRED=1 is set, the
     listing lands with payment_status='pending' and is hidden from
     browse. If not set, the listing goes straight to 'free' and is
     visible immediately (today's behaviour, unchanged).
  2. Anyone (publisher, their script, their wallet) POSTs
     /api/invoice {listing_id} and gets back a one-shot invoice:
     operator receive address, exact sat amount (USD-pegged + random
     1..256 sat jitter so no two in-flight invoices collide),
     bitcoincash: URI for a wallet, 20-min expiry.
  3. Reader pays. The amount matches exactly one pending invoice, and
     when the mempool watcher reports (address, amount, txid) to
     tryMatchPayment, both the invoice and the listing flip to paid.
     Browse starts showing the listing.

The mempool watcher that would do (3) automatically is deliberately NOT
in this commit — it needs an electrum-cash subscription or a block
explorer webhook, and that's a next milestone. In the meantime, admins
reconcile by hand from /admin/payments, which shows every invoice with
a Mark paid form (optional txid field). Same effect on the DB, same
audit log entry, different `paid_by` string.

Design decisions worth recording:
- One receive address + amount matching, NOT an HD wallet per invoice.
  BitPay used this for years; it is simple, auditable, and doesn't
  require a seed on the server to spend from later.
- Random few-sat jitter per invoice guarantees uniqueness of the
  (address, amount) pair across all in-flight invoices.
- Price oracle is CoinGecko (public, no key, 60s cache, 15-min stale
  fallback). If both the fresh fetch and the stale fallback fail,
  /api/invoice refuses — issuing invoices on a stale price is worse
  than refusing to quote until the oracle recovers.
- Quote is locked at issue time; the 20-min deadline is the lock.
- Lazy expiry: GET /api/invoice/<id> flips an expired pending invoice
  to 'expired' as a side effect, so there's no sweeper to run.

Admin UI /admin/payments shows every invoice (pending/paid/expired),
plus a dashboard-ish header with the configured mode + receive address.
The gating config appears in the UI even when payments are disabled, so
an operator spinning up a new instance can see at a glance what's set.

Config:
  PAYMENT_REQUIRED=1              — turn paid mode on
  PROMETHEUS_PAYMENT_ADDRESS=…    — the operator's receive address
  PROMETHEUS_LISTING_FEE_USD=1    — the fee (default 1 USD)
  PROMETHEUS_PRICE_SOURCE=…       — override the CoinGecko URL

End-to-end verified:
  PAYMENT_REQUIRED=1 → new listing hidden from browse → /api/invoice
  returns correct sats at live BCH price → admin login + mark-paid →
  listing becomes visible → /api/invoice/<id> returns status 'paid'
  with paid_by='manual:admin.x'.
2026-10-08 21:52:01 +02:00
Local Dev
6eeec54266 prometheus: SEO polish and the apps.silentmode.st mirror vhost
layout gained three optional fields: ogType, ogImage, headExtra. The
listing detail page uses all three — ogType=article, ogImage=the icon
URL (fallback to the Prometheus favicon), and headExtra for schema.org
SoftwareApplication JSON-LD. Google renders this as a rich card for
app results; the SEO uplift is real and the data is straightforward.

JSON-LD payload composed in service/seo.js:
  @type SoftwareApplication, name, description, softwareVersion,
  applicationCategory (mapped from Prometheus's slug to schema's name),
  url, downloadUrl, fileSize, offers (free), author (BNS name as org),
  image (icon when present), contentRating ("ClamAV clean" on a clean
  scan).

OG defaults for every page: og:type (website unless overridden),
og:image (favicon unless overridden). The canonical URL stays
prometheus.x on both the live service and the apps.silentmode.st
mirror — crawlers that land on the mirror attribute the content to
the real address, matching the pattern [[silentmode-site-url-scheme]]
describes.

deploy/apps-silentmode-st.conf is the angie vhost for the mirror:
reverse-proxies to 127.0.0.1:3700, declines /admin (operator always
goes direct), 2 GiB client_max_body_size for POST /api/upload,
proxy_request_buffering off so a slow upload does not spool to disk.
Reuses the main silentmode.st certificate (SAN *.silentmode.st; if
that's not set up yet, request one and reload before enabling).
2026-10-08 21:45:59 +02:00
Local Dev
c9008ef3ba prometheus: VirusTotal enrichment, gated on VIRUSTOTAL_API_KEY
After clamd returns a verdict, scanStream queries VirusTotal by the file's
SHA-256 and attaches the finding under scan.virustotal. The call never
blocks, never changes the overall verdict, and never ships without a key
— enrichment is informational and strictly additive.

Shape in the signed scan report:
  scan.virustotal: {status: 'known', positives, total, scan_date, signatures}
                 | {status: 'unknown'}
                 | {status: 'rate_limited'}
                 | {status: 'error', reason}
                 | (absent)              — enrichment disabled

Rate-limited to VIRUSTOTAL_RATE_PER_MIN (default 4, matching the free
tier) via a sliding window; the service never fails the request when the
quota is spent, it just skips the enrichment that time. Hashes are
cached in-process so a resubmitted file does not re-spend quota.

The upload POST response passes the virustotal field through on both the
success and error branches so the submit UI can display it alongside the
clamd verdict.

uploads.js passes the SHA-256 it already computed into scanStream rather
than making scanStream hash a second time — the current call site is the
only one that needs the hash downstream.
2026-10-08 21:44:34 +02:00
Local Dev
9f16ce3128 prometheus: weekly s3 snapshot of the browse surface to bns/prometheus
scripts/snapshot.mjs renders every public page (home, browse, publish,
boundaries, 8 categories, every active listing) to <out>/<path>/index.html,
plus /sitemap.xml, /robots.txt, and the two static assets. Writing to a
directory next to the service AND uploading via `aws s3 sync` are two
flags on the same command: --out to pick the directory, --upload to push.

Live routes added for /sitemap.xml and /robots.txt so a crawler hitting
the live service sees the same enumeration the snapshot bakes into the
bucket. renderSitemap / renderRobots in service/seo.js are the single
rendering path both call sites use.

Scope deliberately limited to the browse surface — /submit and the API
routes depend on the live backend and were never going to work on a
static mirror. The browse surface IS what the fallback is for: readers
survive an outage, publishers can't.

systemd unit + timer under deploy/ run the snapshot weekly on Sunday
04:00 UTC with 30 min jitter and Persistent=yes so a missed run after a
reboot catches up. Unit uses the sia-storage AWS profile per
[[sia-storage-credentials-location]] and the per-process endpoint URL
baked in via Environment=.

Local dry-run against an empty DB produced 14 HTML files + sitemap +
robots + the two assets.
2026-10-08 21:42:54 +02:00
Local Dev
cb1589cf36 prometheus: admin panel — login, listings, 3-mode toggle, audit log
/admin is wallet-gated. The configured ADMIN_PUBLISHER is a BNS name;
only the on-chain owner of that name can sign in. The sign-in flow is
BIP-137 (same primitive publishers use): the login page shows a fresh
one-shot challenge, the admin signs it in their wallet, pastes the
signature, and the service verifies that it recovers to the current
on-chain owner. Session is a 24-hour ed25519-signed cookie minted by
the service key.

Pages:
  /admin             dashboard — totals, mode state, recent listings + audit
  /admin/login       BIP-137 challenge sign-in
  /admin/listings    all listings, with a Withdraw button per row
  /admin/modes       global + per-category publish-mode toggle
  /admin/audit       the hash-chained audit log

The three publish modes exist as toggles:
  `append`     — any valid listing goes in (launch default)
  `scan-first` — infected uploads are rejected before they land
  `review`     — clean listings wait for human approval
Global mode is the fallback; a per-category override takes precedence
when set. The toggle persists; the review-queue enforcement lands in a
later milestone (the enforcement sketch is in modes.js's header).

Audit log:
  Every admin action (login, set_mode, clear_mode, withdraw_listing) is
  an append-only row with a sha256 chain over the previous row's hash.
  The chain does not prevent edits; it makes retroactive ones loud —
  row N's `prev_hash` disagreeing with row N-1's `this_hash` is the
  tamper signal.

Withdrawal:
  Soft — `withdrawn_at` on the listing row. Browse hides withdrawn
  listings (already the case) and the detail page returns 404 for them.
  Nothing in the chain is undone; this is Prometheus's own view only.

Session design:
  Cookie is `prom_admin=<payload b64url>.<signature b64url>`, signed with
  the ed25519 service key, HttpOnly, SameSite=Strict, Path=/admin,
  24-hour expiry. Reconfiguring ADMIN_PUBLISHER invalidates in-flight
  sessions by name mismatch. Challenges live in a 10-min TTL map and
  are tied to a specific name so one admin's challenge cannot authenticate
  another.

Added:
  service/admin.js         route handlers + dispatch
  service/admin-pages.js   HTML templates for the four pages
  service/admin-session.js challenge + cookie mint/verify + readSession
  service/audit.js         hash-chained append helper
  service/modes.js         publish-mode read/write with override semantics
  schema: settings + audit tables

scripts/test-admin-roundtrip.mjs drives the whole flow (unauth redirect
→ challenge scrape → sign → login → dashboard → set mode → audit
inspection) with the same deterministic test key the listing-roundtrip
script uses. Withdraw-flow spot-check: create a listing, log in as
admin, withdraw it, confirm browse hides it and the detail page returns
404. All seven checks pass.
2026-10-08 21:35:48 +02:00
Local Dev
bb07c73aaf prometheus: browse pages — categories and per-listing detail
/c/<slug>  renders one of the eight categories with every active listing
           in it, newest first, as a grid of cards. Empty category shows
           a "be the first" prompt rather than a dead page.

/app/<pub>/<slug>  renders a listing: icon + name + publisher + version
                   chip, a prominent Download card with size, hash chip,
                   signed scan badge (green/clean | red/infected |
                   neutral/unknown), description, homepage link, who
                   signed, and an endorsements placeholder for when APS1
                   lands.

The home-page and /browse grids now read real counts from the DB — the
hardcoded "0 listings" placeholders are gone. A new helper
`renderCategoriesGrid()` is the one place category cards are rendered,
so the two surfaces always agree.

404s for unknown categories (`/c/nope`) and unknown listings
(`/app/foo/bar`) go through the same notFound page and now actually
return HTTP 404 instead of 200 — pages opt in by returning the
notFound object, server detects it by title and sets the status.

CATEGORIES is the single source of truth for slug/icon/title/blurb;
adding a new category now means one row here plus one entry in the
listings allow-list.

Verified end-to-end with three listings across three categories: counts
update, cards render, detail page shows the signed scan badge, 404s
return 404.
2026-10-08 01:56:52 +02:00
Local Dev
72f7d41902 prometheus: signed listings — BIP-137 publisher sign, SQLite index
POST /api/listing accepts a JSON body with the manifest fields, a
reference to a signed scan report from /api/upload, and a BIP-137
signature. Before the row lands in SQLite the service verifies three
things:

  1. the attached scan_report was signed by this service, is for the
     exact (hash, size) the listing claims, and the verdict is clean —
     a publisher cannot swap in a different file's clean report;

  2. the publisher's signature recovers to a valid cashaddress, and
     that address matches the on-chain owner of the publisher's BNS
     name (gateway /api/name/<name>) — recovery alone is never enough
     per [[recovery-is-not-verification]];

  3. the usual shape checks: category in the allow-list, sha256 is 64
     hex, download URL is http(s), slug matches [a-z0-9-]{1..64}, body
     under 1 MiB.

GET /api/listings and GET /api/listings/<publisher>/<slug> are read-only
endpoints the browse milestone will render from.

MOCK_BNS_OWNER=<cashaddr> short-circuits the gateway call for local
testing without a chipnet gateway reachable — same pattern as MOCK_SCAN.

Dependencies:
- @bitauth/libauth, for the secp256k1 primitives behind BIP-137 recovery
  and the cashaddress encoder. Same primitive Argus's registrar uses, so
  signatures from Argus-compatible wallets verify unchanged.
- node:sqlite (ships with Node 22+) for the listings DB — one file next
  to the service, WAL journaling, no native build step. engines bumped
  to ">=22".

Lifted service/lib/sign-message.js from Argus so Prometheus does not
have to pull the resolver's whole dependency tree in for one verifier.
Removed its NETWORK import; the default address prefix comes from
BNS_NETWORK=chipnet|mainnet instead.

INTERIM: no APS1 record on chain yet. The signed row IS the listing;
when APS1 is defined cross-session the service will add an on-chain
publish step that points at the same signed manifest. The browse and
admin milestones can proceed against this DB in the meantime.

scripts/test-listing-roundtrip.mjs drives the whole flow end-to-end
with a deterministic test key, so a sanity check is a two-command
sequence: start the server with MOCK_SCAN=clean + MOCK_BNS_OWNER=<the
test address>, then run the script. Negative paths (fake scan report,
bad category, duplicate slug) also verified by hand.
2026-10-08 01:48:12 +02:00
Local Dev
c4566b4ea3 prometheus: upload + ClamAV scan + signed scan reports
POST /api/upload accepts a multipart file, spools it to a temp path while
hashing to SHA-256, hands the on-disk copy to clamd via INSTREAM, keeps
the file at uploads/<sha256>.bin on a clean verdict and discards it on
anything else. The response carries the (hash, size, verdict) tuple
signed by the service's ed25519 key — the artifact the manifest step
will attach to the APS1 record later.

For local dev on a box without ClamAV, MOCK_SCAN=clean|infected|error
short-circuits the TCP call while still draining the bytes end-to-end.
CLAMD_HOST / CLAMD_PORT / PROMETHEUS_MAX_UPLOAD_BYTES override the
defaults (127.0.0.1:3310, 2 GiB).

Service key lives at .keys/service.key (gitignored, 0600), generated on
first start if absent. The matching public key is served at
/.well-known/prometheus-service-key.json so clients can cache it per
origin. ed25519 chosen over BIP-137 because the scan report is a service
attestation, not a Bitcoin thing — the key never signs value, never
signs for a user, never touches the chain.

Canonicalisation walks the payload recursively (sorted keys at every
level, no whitespace). The first draft used JSON.stringify with a keys
array, which only serialises top-level keys from the array — nested
objects came out as {}, so the signature verified nothing useful.

/healthz pings clamd (or the mock) so monitors can tell when the scanner
is down.

The /submit page gets a working upload form that drives the endpoint
and renders the full signed response, so the whole chain is testable in
the browser as well as via curl.

One dependency added: busboy. Rolling our own multipart parser was the
obvious alternative; busboy is one small, maintained library with no
runtime deps of its own, and multipart RFC 7578 edge cases are not
places this project earns anything by rediscovering.
2026-10-05 22:41:55 +02:00
Local Dev
2c5df40d77 prometheus: scaffold the standalone service — pages, routing, static assets
New top-level Prometheus/ directory holds the Node service that will own
prometheus.x once the on-chain record flips from the silentmode.st
redirect to ip+tls. Nothing dynamic yet — this milestone delivers the
site skeleton and proves the family's visual conventions port cleanly to
a dynamic server.

Shape:
  Prometheus/service/server.js    node:http listener + map-based router
  Prometheus/service/layout.js    document shell, topnav, footer
  Prometheus/service/pages.js     home / browse / submit / publish / boundaries
  Prometheus/public/style.css     ported silentmode.st tokens + category grid
  Prometheus/public/favicon.svg   flame mark

Routes: /, /browse, /submit, /publish, /boundaries, /static/*.
Dynamic routes (/api/upload, /api/manifest, /api/publish, /api/pay,
/admin/*) land here in later milestones per the architecture doc.

Launch config .claude/launch.json gets a prometheus-service entry on
port 3700 so the preview harness can run it.

The pre-pivot WordPress-based scaffold at site-prometheus-x/ is left
in place for the next session to delete deliberately — the architecture
doc marks it as abandoned for prometheus.x but keeps the Sirius Press
infra fixes it carried in.
2026-10-05 22:22:47 +02:00