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
/ * *
* The pages Prometheus . X renders .
*
* Each export is ` (req?) => {title, description, body} ` . The server is the one
* that wraps them in the layout — these just return the inner content .
*
* The copy was lifted from site / prometheus / index . html ( the ICANN teaser ) and
* split by concern so each page is one topic a reader opened on purpose . The
* single - scroll version lives at silentmode . st / prometheus and will stay there
* as the SEO - friendly mirror .
* /
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
import { renderCategoriesGrid , countsByCategory } from './pages-browse.js' ;
import { countActive } from './db.js' ;
/ * *
* The "Browse by category" section used on the home page . Lives here so the
* home page ' s inline template reads cleanly , but delegates the actual grid to
* pages - browse . js so a single row of counts drives both surfaces .
* /
function renderHomeBrowseSection ( ) {
const total = countActive ( ) ;
return ` <section class="plain">
< h2 > Browse by category < / h 2 >
< p class = "lede" > On a conventional store , one company decides which shelf an app
belongs on . Here the publisher labels their own listing , and your trust list
decides whether to show it . The categories below are the initial shape ; a new
one gets proposed by publishing a listing against it . < / p >
$ { renderCategoriesGrid ( { verbose : true } ) }
$ { total === 0 ? `
< div class = "note warn" style = "margin-top:1.6rem" > < b > No live listings yet . < / b >
The on - chain publication protocol ( < code class = "inline" > APS1 < / c o d e > ) i s i n
progress ; in the meantime the site , the scanner , the publisher - signed
listing endpoint and the publishers ' walkthrough are up . First real
listings appear in this grid as they publish . < / d i v >
` : ''}
< / s e c t i o n > ` ;
}
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
/* ------------------------------- home ------------------------------- */
export function home ( ) {
return {
title : 'Apps and programs on the decentralized web' ,
description :
'Prometheus is a home for apps, programs, and dApps published on the Bitcoin Cash chain. Listings live on chain, binaries live on Sia, endorsements are signed. Anyone can publish; nobody can gatekeep.' ,
here : '/' ,
body : `
< header class = "hero" >
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
< div class = "mark" style = "width:88px;height:88px;margin:0 auto 1rem" >
< svg viewBox = "0 0 64 64" width = "88" height = "88" aria - hidden = "true" style = "display:block" >
< path d = "M32 6 C 38 18 46 24 46 36 C 46 48 40 56 32 56 C 24 56 18 48 18 36 C 18 28 24 22 20 14 C 26 18 30 20 32 6 Z" fill = "#d6ff3d" / >
< path d = "M32 20 C 36 28 40 32 40 40 C 40 48 36 52 32 52 C 28 52 24 48 24 40 C 24 34 28 30 32 20 Z" fill = "#0b0e14" / >
< circle cx = "32" cy = "44" r = "3" fill = "#d6ff3d" / >
< / s v g >
< / d i v >
< h1 > Prometheus < span class = "g" > . X < / s p a n > < / h 1 >
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
< p class = "tag" > A home for apps , programs , dApps , and libraries — < b > listed on the
Bitcoin Cash chain , hosted on Sia , endorsed by signature < / b > . A n y o n e c a n p u b l i s h .
Nobody can gatekeep . Named for the one who gave fire to humans and did not ask
the gods first . < / p >
< div class = "cta" >
< a class = "btn acid" href = "/browse" > Browse apps < / a >
< a class = "btn ghost" href = "/submit" > Submit an app < / a >
< / d i v >
< / h e a d e r >
< div class = "wrap" >
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
$ { renderHomeBrowseSection ( ) }
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
< section >
< h2 > How Prometheus works , in a nutshell < / h 2 >
< ol class = "steps" >
< li > < b > A listing is a signed on - chain record . < / b >
< span > Publishing an app writes a small < code class = "inline" > APS1 < / c o d e >
record to Bitcoin Cash — same protocol shape as a BNS name registration ,
different prefix . The payload is a hash pointing at a full manifest stored
on Sia . Nobody approves the record ; the transaction is the publish . < / s p a n > < / l i >
< li > < b > The binary lives on Sia . < / b >
< span > App files are uploaded to the Sia storage network — encrypted on
the publisher ' s machine , sharded across roughly 30 independent hosts ,
held by nobody in particular . The listing carries the SHA - 256 the file
must match . < / s p a n > < / l i >
< li > < b > Scan reports are signed and attached . < / b >
< span > Prometheus runs an automated malware scan on every binary it hosts
and publishes the signed report alongside the listing . The chain still
goes up ; the report tells your client whether to warn . < / s p a n > < / l i >
< li > < b > Install verifies against the chain . < / b >
< span > Clients pull the file , check the SHA - 256 against the on - chain
record , check the signing certificate against the fingerprint , and only
then install . The chain is the truth , not the mirror . < / s p a n > < / l i >
< / o l >
< p style = "margin-top:1.2rem" > < a href = "/publish" > The full publishers ' walkthrough ,
step by step → < / a > < / p >
< / s e c t i o n >
< / d i v > ` ,
} ;
}
/* ------------------------------- publish ---------------------------- */
export function publish ( ) {
return {
title : "Publishers' walkthrough" ,
description :
'The end-to-end flow of publishing an app on Prometheus: name, hosting, manifest, listing, verification, updates, withdrawal, and endorsements.' ,
here : '/publish' ,
body : `
< header class = "hero compact" >
< h1 > Publishers ' walkthrough < / h 1 >
< p class = "tag" > Pick a name , host the binary , write the manifest , sign the listing ,
verify it resolves , ship an update when it 's time. Here' s the whole shape . < / p >
< / h e a d e r >
< div class = "wrap" >
< section class = "plain" >
< p class = "lede" > Publishing an app on Prometheus is a sequence of signed records
pointing at files stored where you keep them . The whole thing — identity ,
hosting , the listing itself — is yours ; nothing here grants anyone else
permission to pull your app down , raise your price , or take a cut of what
people pay you for it . < / p >
< p > Steps are tagged < span class = "pill p-live" > available today < / s p a n > o r
< span class = "pill p-designed" > designed < / s p a n > s o i t i s c l e a r w h a t y o u c a n d o
now and what is waiting on the APS1 record format landing . < / p >
< / s e c t i o n >
< section >
< h2 > What you need before you start < / h 2 >
< div class = "grid" >
< div class = "card" >
< span class = "pill p-live" > available today < / s p a n >
< h3 > A BNS name < / h 3 >
< p > Any name registered through < a href = "//sirius.x/" > Sirius < / a > w o r k s . T h e
name is what signs the listing , so it is also what readers trust the
listing by . Fractions of a cent to register , no monthly bill . < / p >
< / d i v >
< div class = "card" >
< span class = "pill p-live" > available today < / s p a n >
< h3 > Somewhere to put the binary < / h 3 >
< p > Prometheus can host for you — upload here and we put it on Sia , signed
and scanned . If you ' d rather self - host on your own URL , the listing just
records the URL and the hash and will fetch from wherever you say . < / p >
< / d i v >
< div class = "card" >
< span class = "pill p-live" > available today < / s p a n >
< h3 > A signing key < / h 3 >
< p > The private key for the BNS name . If the name is in a wallet you already
use for Bitcoin Cash , that key already exists ; publishing re - uses it . < / p >
< / d i v >
< div class = "card" >
< span class = "pill p-live" > available today < / s p a n >
< h3 > A pinch of BCH < / h 3 >
< p > One listing is one transaction . The transaction fee is cents , denominated
in sats . During the free wave , that is the only cost . Later , a flat
listing fee covers the Sia storage . < / p >
< / d i v >
< / d i v >
< / s e c t i o n >
< section >
< h2 > The walkthrough < / h 2 >
< ol class = "steps" >
< li > < b > 1. Register a name — or use one you already own . < / b >
< span > The listing is signed by the name ' s key . A reader who follows your
name will see your listing with the publisher chip set to that name ; a
reader who does not will see it marked unsigned by anyone they trust .
One name can carry any number of listings , so a studio with three games
publishes under one identity . < / s p a n > < / l i >
< li > < b > 2. Upload the binary ( or point us at your URL ) . < / b >
< span > Prometheus runs a malware scan on upload . If it passes , the file
goes into a Sia bucket Silent Mode controls , the signed scan report is
prepared , and the manifest fields are prefilled . If you ' d rather host
the file yourself , give us the URL and the expected SHA - 256 ; the manifest
records both , the scan still runs against whatever the URL serves , and
the client refuses to install a byte - mismatched download . < / s p a n > < / l i >
< li > < b > 3. Fill in the manifest . < / b >
< span > Name , description , category , version , icon , download URL ,
< code class = "inline" > sha256 < / c o d e > , a n d t h e s i g n i n g - c e r t f i n g e r p r i n t w h e n
the binary itself is signed ( it should be , if the platform supports it ) .
Icons are referenced by URL , not embedded . A 2 KB manifest with a 500 KB
icon beside it is the right shape ; a 500 KB manifest is not . < / s p a n > < / l i >
< li > < b > 4. Sign the APS1 record with your BNS key . < / b >
< span > One transaction . The on - chain record carries the manifest hash ,
the category label , and the signature from your name ' s key . From the
moment the transaction confirms , every client that follows the chain can
see your listing — no approval queue , no editorial review , no publisher
dashboard waiting for a human on the other end . < / s p a n > < / l i >
< li > < b > 5. Verify it resolves . < / b >
< span > Open one of the Ariadne clients and look for your listing in the
category you labelled it under . The client should pull the manifest from
Sia , show the fields you wrote , surface the scan report , and offer the
download . Clicking install should fetch the binary , check the SHA - 256 ,
check the signing certificate , and only then run . < / s p a n > < / l i >
< li > < b > 6. Ship updates with another signed record . < / b >
< span > A new version is another APS1 transaction referencing the same app
id , signed by the same key , pointing at a new manifest hash . Clients
already following the app see the update offered the next time they
check . Nobody can push a hostile update unless they have your key . < / s p a n > < / l i >
< li > < b > 7. Withdraw a listing when it is time . < / b >
< span > A withdrawal is a signed record that marks the app id as retired .
Clients hide the listing from browse and refuse to install it , though
anyone with the manifest hash and the binary can still side - load it —
the chain is a signal , not a lock . < / s p a n > < / l i >
< / o l >
< / s e c t i o n >
< section >
< h2 > A note on endorsements < / h 2 >
< p > A listing is one signed record . An < em > endorsement < / e m > i s a n o t h e r — a s h o r t
on - chain statement from any name saying "I vouch for this app." Readers whose
trust list includes the endorser ' s name will see your listing marked as
endorsed by them ; readers who do not follow that name will not . Endorsements
are not reviews , and the client does not aggregate them into a star rating .
They are statements : who said what , when , signed . < / p >
< div class = "note" > < b > You can endorse too . < / b > A n y n a m e c a n p u b l i s h a n e n d o r s e m e n t
of any listing , and most publishers end up endorsing a handful of apps they
use themselves . The trust graph that emerges is the directory ' s real curation
layer ; the categories are just filing . < / d i v >
< / s e c t i o n >
< section >
< h2 > What this costs < / h 2 >
< div class = "grid" >
< div class = "card" >
< h3 > Per listing < / h 3 >
< p > One Bitcoin Cash transaction . Fractions of a cent at current rates . Paid
once , lives on chain . < / p >
< / d i v >
< div class = "card" >
< h3 > Per update < / h 3 >
< p > Another transaction . Same shape , same cost . There is no "update bundle"
discount because there is no central party to collect one . < / p >
< / d i v >
< div class = "card" >
< h3 > Per month of hosting < / h 3 >
< p > Free during the opening wave while Prometheus underwrites the storage .
Afterwards , a flat per - listing fee ( planned around $1 at current rates )
covers Sia storage for the typical binary . Your own HTTPS host costs
what it costs you . < / p >
< / d i v >
< div class = "card" >
< h3 > Per download < / h 3 >
< p > Zero to the directory . The reader pulls the binary from wherever the
manifest points ; Prometheus never meters the traffic . < / p >
< / d i v >
< / d i v >
< / s e c t i o n >
< section >
< div class = "note warn" > < b > Where this stands today . < / b > N a m e s , S i a h o s t i n g a n d
BCH signing exist and are in use . The < code class = "inline" > APS1 < / c o d e > r e c o r d
format and the client - side browse / install flow are being wired . This page
describes the end state ; the first live listings will go up as soon as the
submit path lands on this site and the Ariadne clients learn to read it . < / d i v >
< / s e c t i o n >
< / d i v > ` ,
} ;
}
/* ------------------------------- browse ----------------------------- */
export function browse ( ) {
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
const total = countActive ( ) ;
const totalLabel = total === 0
? ` No listings yet — the eight categories are set and ready to receive publications. `
: ` ${ total } ${ total === 1 ? 'listing' : 'listings' } across ${ Object . values ( countsByCategory ( ) ) . filter ( ( n ) => n > 0 ) . length } ${ Object . values ( countsByCategory ( ) ) . filter ( ( n ) => n > 0 ) . length === 1 ? 'category' : 'categories' } . ` ;
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
return {
title : 'Browse' ,
description :
'Browse apps and programs published on Prometheus by category. Signed listings on the Bitcoin Cash chain; scan reports attached; your trust list filters.' ,
here : '/browse' ,
body : `
< header class = "hero compact" >
< h1 > Browse < / h 1 >
< p class = "tag" > Signed listings on chain , scan reports attached , your trust list
decides what to render . < / p >
< / h e a d e r >
< div class = "wrap" >
< section class = "plain" >
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
< p class = "lede" > $ { totalLabel } < / p >
$ { renderCategoriesGrid ( { verbose : false } ) }
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
< / s e c t i o n >
< / d i v > ` ,
} ;
}
/* ------------------------------- submit ----------------------------- */
export function submit ( ) {
return {
title : 'Submit an app' ,
description :
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
'Upload an app to Prometheus. We scan it, generate the manifest, you sign with your BNS key and the listing goes live.' ,
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
here : '/submit' ,
body : `
< header class = "hero compact" >
< h1 > Submit an app < / h 1 >
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
< p class = "tag" > Upload → fill manifest → sign with your wallet → publish .
Your key stays with you . < / p >
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
< / h e a d e r >
< div class = "wrap" >
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
<!-- Progress indicator -- >
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
< section class = "plain" >
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
< ol class = "steps" id = "step-indicator" style = "margin:0 0 2rem" >
2026-10-22 23:20:19 +03:00
< li data - step = "1" > < b > Upload < / b > < s p a n i d = " s 1 - s u b " > t h e b i n a r y ( u p t o 2 0 0 M i B ) < / s p a n > < / l i >
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
< li data - step = "2" > < b > Describe < / b > < s p a n i d = " s 2 - s u b " > n a m e , p u b l i s h e r n a m e , c a t e g o r y , d e s c r i p t i o n < / s p a n > < / l i >
< li data - step = "3" > < b > Sign < / b > < s p a n i d = " s 3 - s u b " > w i t h t h e B C H k e y f o r y o u r p u b l i s h e r B N S n a m e < / s p a n > < / l i >
< li data - step = "4" > < b > Publish < / b > < s p a n i d = " s 4 - s u b " > P O S T t o t h e d i r e c t o r y < / s p a n > < / l i >
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
< / o l >
< / s e c t i o n >
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
<!-- === === === === === STEP 1 : upload === === === === === -- >
< section id = "step1" >
< h2 > 1 · Upload the binary < / h 2 >
< p class = "lede" > The server scans with ClamAV + VirusTotal ( where configured ) ,
computes the SHA - 256 , and attaches a signed scan report to your listing .
Only clean uploads can proceed . < / p >
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
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
< form id = "upload-form" style = "display:grid;gap:12px;max-width:560px;margin-top:1rem" >
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
< input type = "file" name = "file" required
style = "padding:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;color:var(--ink)" >
< button class = "btn acid" type = "submit" > Upload & amp ; scan < / b u t t o n >
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
< div id = "upload-status" style = "color:var(--mut);font-size:14px;min-height:1.4em" > < / d i v >
< / f o r m >
< div id = "upload-result" class = "card" style = "display:none;margin-top:1.2rem" >
< h3 style = "margin:0 0 .4rem" > Upload accepted < / h 3 >
< p style = "margin:0;color:var(--mut);font-size:13.5px" >
< b style = "color:var(--ink)" id = "u-name" > < / b > ·
< span id = "u-size" > < / s p a n > ·
< code class = "inline" id = "u-hash" > < / c o d e >
< / p >
< p style = "margin:.5rem 0 0" id = "u-scan" > < / p >
< button class = "btn acid" type = "button" id = "to-step2" style = "margin-top:1rem" > Continue → describe your listing < / b u t t o n >
< / d i v >
< / s e c t i o n >
<!-- === === === === === STEP 2 : describe === === === === === -- >
< section id = "step2" style = "display:none" >
< h2 > 2 · Describe your listing < / h 2 >
< p class = "lede" > Every field is signed in step 3 , so take a minute — editing
the signed payload is a new signature , not a quick fix . < / p >
< form id = "manifest-form" style = "display:grid;gap:14px;max-width:620px;margin-top:1rem" >
< label > Publisher BNS name
< input name = "publisher" required placeholder = "example.x"
pattern = "^[a-z0-9][a-z0-9-]{0,62}\\.[a-z0-9]{1,20}$"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;margin-top:.3rem" >
< small style = "color:var(--dim)" > The BCH key for this name ' s on - chain owner is who signs . Register one at < a href = "//sirius.x/" > sirius . x < / a > . < / s m a l l >
< / l a b e l >
< label > Short slug ( URL )
< input name = "slug" required placeholder = "my-app"
pattern = "^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;margin-top:.3rem" >
< small style = "color:var(--dim)" > Lives at < code class = "inline" > /app/ & lt ; publisher & gt ; / & l t ; s l u g & g t ; < / c o d e > . L o w e r c a s e , h y p h e n s ; 1 – 6 4 c h a r s . < / s m a l l >
< / l a b e l >
< label > Display name
< input name = "name" required maxlength = "128" placeholder = "My App"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);margin-top:.3rem" >
< / l a b e l >
< label > Category
< select name = "category" required
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);margin-top:.3rem" >
< optgroup label = "Apps" >
< option value = "browsers" > Browsers & amp ; web tools < / o p t i o n >
< option value = "communication" > Communication < / o p t i o n >
< option value = "privacy" > Privacy & amp ; security < / o p t i o n >
< option value = "devtools" > Developer tools < / o p t i o n >
< option value = "media" > Media & amp ; publishing < / o p t i o n >
< option value = "productivity" > Productivity < / o p t i o n >
< option value = "other" > Other < / o p t i o n >
< / o p t g r o u p >
< option value = "games" > Games < / o p t i o n >
< option value = "fintech" > FinTech < / o p t i o n >
< / s e l e c t >
< / l a b e l >
< label > Version
< input name = "version" required maxlength = "32" placeholder = "1.0.0"
pattern = "^[0-9A-Za-z.+_-]{1,32}$"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;margin-top:.3rem" >
< / l a b e l >
< label > Description
< textarea name = "description" required maxlength = "2000" rows = "4"
placeholder = "What does this app do? Who's it for? What makes it worth the install?"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);margin-top:.3rem;resize:vertical" > < / t e x t a r e a >
< small style = "color:var(--dim)" > Up to 2000 characters . < / s m a l l >
< / l a b e l >
< label > Download URL
< input name = "download_url" required type = "url" placeholder = "https://example.com/myapp-1.0.0.exe"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;margin-top:.3rem" >
< small style = "color:var(--dim)" > Where readers ' clients fetch the binary . The SHA - 256 ( from step 1 ) must match what the URL serves . < / s m a l l >
< / l a b e l >
< label > Homepage ( optional )
< input name = "homepage" type = "url" placeholder = "https://example.com"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;margin-top:.3rem" >
< / l a b e l >
< label > Icon URL ( optional )
< input name = "icon" type = "url" placeholder = "https://example.com/icon.svg"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;margin-top:.3rem" >
< small style = "color:var(--dim)" > Shown at 80 px on the listing detail . Fallback : your category ' s icon . < / s m a l l >
< / l a b e l >
< div style = "display:flex;gap:10px;flex-wrap:wrap;margin-top:.6rem" >
< button class = "btn ghost" type = "button" id = "back-to-1" > ← Back to upload < / b u t t o n >
< button class = "btn acid" type = "submit" > Continue → sign < / b u t t o n >
< / d i v >
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
< / f o r m >
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
< / s e c t i o n >
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
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
<!-- === === === === === STEP 3 : sign === === === === === -- >
< section id = "step3" style = "display:none" >
< h2 > 3 · Sign with your BNS key < / h 2 >
< p class = "lede" > The message below carries every field from step 2 in canonical
form . Sign it with the BCH key for the address that owns
< b style = "color:var(--ink)" id = "sign-publisher" > your name < / b > o n c h i p n e t .
Any BIP - 137 wallet works ( Electron Cash , Deviant , Cashonize , Theseus ' s Aegis ) . < / p >
< h3 style = "margin-top:1.4rem" > Message to sign < / h 3 >
< pre id = "sign-message" style = "max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-all;font-size:12px" > < / p r e >
< div id = "theseus-sign" style = "display:none;margin-top:1.2rem;padding:14px 18px;background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid);border-radius:10px" >
< b style = "color:var(--ink)" > Theseus wallet detected . < / b >
< p style = "margin:.3rem 0;color:var(--mut);font-size:14px" > Aegis is injected on this page . Sign without pasting . < / p >
< button class = "btn acid" type = "button" id = "theseus-sign-btn" > Sign with Theseus wallet < / b u t t o n >
< / d i v >
< div style = "margin-top:1.2rem" >
< h3 style = "margin:0 0 .3rem" > Or paste a BIP - 137 signature < / h 3 >
< p style = "color:var(--mut);font-size:13.5px;margin:0 0 .5rem" > Base64 , 88 characters . From any wallet ' s "sign message" dialog . < / p >
< textarea id = "sig-paste" rows = "3"
style = "width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;font-size:12px;resize:vertical"
placeholder = "H1234abcd…" > < / t e x t a r e a >
< / d i v >
< div id = "sign-status" style = "color:var(--mut);font-size:14px;min-height:1.4em;margin-top:.8rem" > < / d i v >
< div style = "display:flex;gap:10px;flex-wrap:wrap;margin-top:1rem" >
< button class = "btn ghost" type = "button" id = "back-to-2" > ← Back to details < / b u t t o n >
< button class = "btn acid" type = "button" id = "to-step4" disabled > Continue → publish < / b u t t o n >
< / d i v >
< / s e c t i o n >
<!-- === === === === === STEP 4 : publish === === === === === -- >
< section id = "step4" style = "display:none" >
< h2 > 4 · Publish < / h 2 >
< p class = "lede" > POSTs the signed listing to < code class = "inline" > / a p i / l i s t i n g < / c o d e > .
The server re - verifies the signature against the on - chain owner of your BNS
name and stores the row . The listing is live on browse the moment the POST
returns 201. < / p >
< div style = "display:flex;gap:10px;flex-wrap:wrap;margin-top:1rem" >
< button class = "btn ghost" type = "button" id = "back-to-3" > ← Back to sign < / b u t t o n >
< button class = "btn acid" type = "button" id = "publish-btn" > Publish now < / b u t t o n >
< / d i v >
< div id = "publish-status" style = "color:var(--mut);font-size:14px;min-height:1.4em;margin-top:1rem" > < / d i v >
< div id = "publish-result" style = "margin-top:1rem" > < / d i v >
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
< / s e c t i o n >
< script >
( ( ) => {
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
// ===== state =====
const W = {
file : null ,
upload : null , // from /api/upload: {upload:{hash,size,name,...}, scan:{...}, report:{...}}
manifest : null , // from the form in step 2
message : null , // canonical message string in step 3
signature : null , // base64 BIP-137
} ;
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
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
const $ = ( id ) => document . getElementById ( id ) ;
function show ( step ) {
for ( const n of [ 1 , 2 , 3 , 4 ] ) $ ( 'step' + n ) . style . display = n === step ? '' : 'none' ;
// Progress indicator highlights the current step.
document . querySelectorAll ( '#step-indicator li' ) . forEach ( ( li ) => {
const s = Number ( li . dataset . step ) ;
li . style . opacity = s === step ? '1' : s < step ? '.5' : '.3' ;
} ) ;
window . scrollTo ( { top : 0 , behavior : 'smooth' } ) ;
}
show ( 1 ) ;
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
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
function fmtBytes ( n ) {
if ( n < 1024 ) return n + ' B' ;
if ( n < 1024 * 1024 ) return ( n / 1024 ) . toFixed ( 1 ) + ' KiB' ;
if ( n < 1024 * 1024 * 1024 ) return ( n / 1024 / 1024 ) . toFixed ( 1 ) + ' MiB' ;
return ( n / 1024 / 1024 / 1024 ) . toFixed ( 2 ) + ' GiB' ;
}
// Canonical JSON: keys sorted at every level, no whitespace. MUST match the
// service's canonicalJson() in service/listings.js — the signature covers
// exactly these bytes, and a mismatch here is an unverifiable signature.
function canonicalJson ( v ) {
if ( v === null || typeof v !== 'object' ) return JSON . stringify ( v ) ;
if ( Array . isArray ( v ) ) return '[' + v . map ( canonicalJson ) . join ( ',' ) + ']' ;
const k = Object . keys ( v ) . sort ( ) ;
return '{' + k . map ( ( key ) => JSON . stringify ( key ) + ':' + canonicalJson ( v [ key ] ) ) . join ( ',' ) + '}' ;
}
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
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
// ===== STEP 1: upload =====
$ ( 'upload-form' ) . addEventListener ( 'submit' , async ( ev ) => {
ev . preventDefault ( ) ;
const file = ev . target . file . files [ 0 ] ;
if ( ! file ) return ;
W . file = file ;
const status = $ ( 'upload-status' ) ;
status . textContent = \ ` Uploading \$ {file.name} ( \$ {fmtBytes(file.size)}) and scanning… \` ;
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
const body = new FormData ( ) ;
body . append ( 'file' , file ) ;
const started = performance . now ( ) ;
let json ;
try {
const r = await fetch ( '/api/upload' , { method : 'POST' , body } ) ;
json = await r . json ( ) ;
} catch ( err ) {
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
status . innerHTML = '⚠️ <b>Network error</b> — ' + String ( err ? . message || err ) ;
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
return ;
}
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
const elapsed = Math . round ( performance . now ( ) - started ) ;
if ( ! json . ok ) {
status . innerHTML = '⚠️ <b>Rejected</b> — ' + ( json . reason || json . error || 'unknown error' ) ;
return ;
}
if ( json . scan . verdict !== 'clean' ) {
status . innerHTML = \ ` 🚨 <b> \$ {json.scan.verdict}</b> — the scanner refused this file. \` +
( json . scan . signatures ? . length ? \ ` Signatures: \$ {json.scan.signatures.join(', ')}. \` : '') +
' Try a different file.' ;
return ;
}
W . upload = json ;
$ ( 'u-name' ) . textContent = json . upload . name ;
$ ( 'u-size' ) . textContent = fmtBytes ( json . upload . size ) ;
$ ( 'u-hash' ) . textContent = json . upload . hash . slice ( 0 , 8 ) + '…' + json . upload . hash . slice ( - 6 ) ;
$ ( 'u-scan' ) . innerHTML = '<span class="pill p-live">scan ✓ clean (' + json . scan . engine + ', ' + elapsed + ' ms)</span>' ;
$ ( 'upload-result' ) . style . display = 'block' ;
status . textContent = '' ;
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
} ) ;
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
// ===== STEP 1 → 2 =====
$ ( 'to-step2' ) . addEventListener ( 'click' , ( ) => {
// Prefill name + download_url + hash preview.
const f = $ ( 'manifest-form' ) ;
if ( W . upload ) {
// leave the fields the user needs to fill blank; show the hash we'll use
}
show ( 2 ) ;
} ) ;
// ===== STEP 2: describe =====
$ ( 'back-to-1' ) . addEventListener ( 'click' , ( ) => show ( 1 ) ) ;
$ ( 'manifest-form' ) . addEventListener ( 'submit' , ( ev ) => {
ev . preventDefault ( ) ;
if ( ! W . upload ) { alert ( 'Upload a file first.' ) ; show ( 1 ) ; return ; }
const f = ev . target ;
W . manifest = {
publisher : f . publisher . value . trim ( ) . toLowerCase ( ) ,
slug : f . slug . value . trim ( ) . toLowerCase ( ) ,
name : f . name . value . trim ( ) ,
description : f . description . value . trim ( ) ,
category : f . category . value ,
version : f . version . value . trim ( ) ,
homepage : f . homepage . value . trim ( ) || null ,
icon : f . icon . value . trim ( ) || null ,
download : {
url : f . download _url . value . trim ( ) ,
sha256 : W . upload . upload . hash ,
size : W . upload . upload . size ,
} ,
scan _report : W . upload . report ,
} ;
// Canonical signing message, same shape service/listings.js builds.
const canonical = canonicalJson ( {
kind : 'prometheus.listing' ,
v : 1 ,
publisher : W . manifest . publisher ,
slug : W . manifest . slug ,
name : W . manifest . name ,
description : W . manifest . description ,
category : W . manifest . category ,
version : W . manifest . version ,
homepage : W . manifest . homepage ,
icon : W . manifest . icon ,
download : W . manifest . download ,
scan _report _pubkey : W . upload . report . pubkey ,
scan _report _signature : W . upload . report . signature ,
} ) ;
W . message = 'PROMETHEUS-LISTING\\n' + canonical ;
$ ( 'sign-publisher' ) . textContent = W . manifest . publisher ;
$ ( 'sign-message' ) . textContent = W . message ;
// Reset step 3 state so a revise can proceed cleanly.
W . signature = null ;
$ ( 'sig-paste' ) . value = '' ;
$ ( 'sign-status' ) . textContent = '' ;
$ ( 'to-step4' ) . disabled = true ;
show ( 3 ) ;
} ) ;
// ===== STEP 3: sign =====
$ ( 'back-to-2' ) . addEventListener ( 'click' , ( ) => show ( 2 ) ) ;
// Theseus-wallet injected? Show the one-click sign path.
if ( window . bitcoincash && typeof window . bitcoincash . signMessage === 'function' ) {
$ ( 'theseus-sign' ) . style . display = 'block' ;
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
}
prometheus: /submit becomes a 4-step publisher wizard
The submit page was a one-form upload tester that said "not wired yet"
on the three steps that would turn an upload into a published listing.
It now walks a publisher through the whole thing in-page.
Four client-side steps, state in one object (W = {file, upload, manifest,
message, signature}), scroll-to-top on each transition, back buttons
preserved so revisions are a correction not a restart:
Step 1 — Upload
File picker → POST /api/upload. Response rendered as a card showing
name + size + short hash + the scan verdict chip ("scan ✓ clean
(clamav, 2ms)"). Advance button only enabled when scan is clean;
infected or error paths show the reason inline and stay on step 1.
Step 2 — Describe
Form fields for publisher BNS name, slug, display name, category
(optgroup'd by top-level: Apps subs, then Games, then FinTech),
version, description, download URL, homepage + icon (optional).
Pattern + maxlength validation matches the server's rules in
service/listings.js. Submit builds W.manifest and canonicalises
the signing message.
Step 3 — Sign
Shows the canonical message to sign. If window.bitcoincash is
injected (Theseus with Aegis), a one-click "Sign with Theseus
wallet" button calls bitcoincash.signMessage(). Fallback paste
textarea accepts a 86–88 char BIP-137 signature from any wallet
(Electron Cash, Deviant, Cashonize). Both paths populate
W.signature and enable the continue button.
Step 4 — Publish
POST /api/listing with the full body. On 201 shows the listing
card + a "View your listing →" button pointing at
/app/<pub>/<slug>. On error shows the server's reason inline and
re-enables the Publish button for retry.
Canonical JSON in the client is a direct copy of service/listings.js's
canonicalJson — same bytes, same signature. A test that drifts them
apart would be nice; for now a comment on each copy flags the pairing.
scripts/test-listing-roundtrip.mjs's default category changed from the
old `utilities` slug (removed in the taxonomy change) to `productivity`.
2026-10-22 22:58:35 +03:00
$ ( 'theseus-sign-btn' ) . addEventListener ( 'click' , async ( ) => {
const status = $ ( 'sign-status' ) ;
status . textContent = 'Asking Theseus wallet for a signature…' ;
try {
const sig = await window . bitcoincash . signMessage ( W . message ) ;
// Different wallets return different shapes; normalise to base64 string.
const base64 = typeof sig === 'string' ? sig : sig ? . signature || sig ? . sig || '' ;
if ( ! base64 ) throw new Error ( 'wallet returned no signature' ) ;
W . signature = base64 ;
$ ( 'sig-paste' ) . value = base64 ;
status . innerHTML = '✓ Signed. <b>' + base64 . length + ' chars.</b> Continue to publish.' ;
$ ( 'to-step4' ) . disabled = false ;
} catch ( err ) {
status . innerHTML = '⚠️ Theseus-wallet sign failed — ' + String ( err ? . message || err ) +
'. Paste a signature from another wallet below.' ;
}
} ) ;
$ ( 'sig-paste' ) . addEventListener ( 'input' , ( ev ) => {
const v = ev . target . value . trim ( ) ;
if ( v . length === 88 || v . length === 86 || v . length === 87 ) {
W . signature = v ;
$ ( 'sign-status' ) . innerHTML = '✓ ' + v . length + '-char signature captured. Continue to publish.' ;
$ ( 'to-step4' ) . disabled = false ;
} else {
W . signature = null ;
$ ( 'to-step4' ) . disabled = true ;
$ ( 'sign-status' ) . textContent = v ? \ ` Need 86– 88 chars; got \$ {v.length}. \` : '';
}
} ) ;
$ ( 'to-step4' ) . addEventListener ( 'click' , ( ) => show ( 4 ) ) ;
// ===== STEP 4: publish =====
$ ( 'back-to-3' ) . addEventListener ( 'click' , ( ) => show ( 3 ) ) ;
$ ( 'publish-btn' ) . addEventListener ( 'click' , async ( ) => {
const status = $ ( 'publish-status' ) ;
const result = $ ( 'publish-result' ) ;
status . textContent = 'Publishing…' ;
result . innerHTML = '' ;
$ ( 'publish-btn' ) . disabled = true ;
const body = { ... W . manifest , signature : W . signature } ;
let json ;
try {
const r = await fetch ( '/api/listing' , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON . stringify ( body ) ,
} ) ;
json = await r . json ( ) ;
if ( ! r . ok || ! json . ok ) {
status . innerHTML = '⚠️ <b>' + ( r . status ) + '</b> — ' + ( json . reason || json . error || 'unknown' ) ;
$ ( 'publish-btn' ) . disabled = false ;
return ;
}
} catch ( err ) {
status . innerHTML = '⚠️ Network error — ' + String ( err ? . message || err ) ;
$ ( 'publish-btn' ) . disabled = false ;
return ;
}
status . innerHTML = '✅ <b>Published.</b>' ;
const href = '/app/' + encodeURIComponent ( json . listing . publisher ) + '/' + encodeURIComponent ( json . listing . slug ) ;
result . innerHTML =
'<div class="card" style="padding:22px 26px">' +
'<h3 style="margin:0 0 .3rem">' + json . listing . name + '</h3>' +
'<p style="margin:0;color:var(--mut);font-size:13.5px">Signed by ' + json . listing . publisher +
' · ' + json . listing . category + ' · v' + json . listing . version + '</p>' +
'<p style="margin:1rem 0 0"><a class="btn acid" href="' + href + '">View your listing →</a> ' +
'<a class="btn ghost" href="/browse">Back to browse</a></p>' +
'</div>' ;
} ) ;
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
} ) ( ) ;
< / s c r i p t >
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
< / d i v > ` ,
} ;
}
/* ------------------------------- boundaries ------------------------- */
export function boundaries ( ) {
return {
title : 'Boundaries' ,
description :
'What Prometheus does and does not do. Not a curator. Not a moderator. Storage costs money. Readers decide; the chain carries signed records.' ,
here : '/boundaries' ,
body : `
< header class = "hero compact" >
< h1 > Boundaries < / h 1 >
< p class = "tag" > What this directory is not , and why . < / p >
< / h e a d e r >
< div class = "wrap" >
< section class = "plain" >
< p class = "lede" > A permissionless app directory earns the right to call itself
permissionless by being honest about what that costs . Here is the shape of
the trade . < / p >
< / s e c t i o n >
< section >
< h2 > What Prometheus is not < / h 2 >
< div class = "grid" >
< div class = "card" >
< h3 > Not a curator < / h 3 >
< p > Prometheus does not decide what is on it . Malware can be published — as
it can on any store . The scan report tells your client to warn ; the
trust lists you subscribe to filter the view . If nobody you trust
endorses it , your client says so . < / p >
< / d i v >
< div class = "card" >
< h3 > Not a moderator < / h 3 >
< p > Content that is illegal in your jurisdiction is your responsibility to
avoid , same as any other permissionless network . Silent Mode operates
infrastructure ; it does not decide what runs on it . < / p >
< / d i v >
< div class = "card" >
< h3 > Not a payment processor < / h 3 >
< p > Payments in an app go publisher - to - reader . The listing may carry a BCH
receive address or a wallet - connect URL ; Prometheus never routes a
transaction . There is no refund desk because there is no middleman to
issue a refund . < / p >
< / d i v >
< div class = "card" >
< h3 > Not an identity provider < / h 3 >
< p > The only identity on Prometheus is the BNS name a listing is signed by .
Readers decide which names count via their own trust lists . There is no
Prometheus account , no "verified publisher" badge , no sign - in flow for
readers . < / p >
< / d i v >
< / d i v >
< / s e c t i o n >
< section >
< h2 > What Silent Mode does control < / h 2 >
< div class = "grid" >
< div class = "card" >
< h3 > The scan report < / h 3 >
< p > Our ClamAV + VirusTotal pipeline runs on every binary we host . The
signed report is attached to the listing . We cannot stop a listing from
being on chain ; we can tell readers what we found . < / p >
< / d i v >
< div class = "card" >
< h3 > Which binaries we host < / h 3 >
< p > Silent Mode ' s Sia bucket . Operators can refuse to host something and
require the publisher to self - host at their own URL . The listing still
exists ; the scan report says we did not host it . < / p >
< / d i v >
< div class = "card" >
< h3 > The default sort < / h 3 >
< p > What the Browse page shows first is a choice . We publish the sort order
and let clients override it . The chain holds the data ; the directory is
one lens . < / p >
< / d i v >
< div class = "card" >
< h3 > The publish mode < / h 3 >
< p > Three modes exist ( append - only , scan - first - blocking , human - review ) .
Operators pick which one is active per category . The current mode is
always visible on this page ; a change is announced before it takes
effect . < / p >
< / d i v >
< / d i v >
< / s e c t i o n >
< section >
< h2 > Storage costs money < / h 2 >
< p > Sia hosting is inexpensive but not free — publishers pay for the bytes
their apps take . There is no advertising model , no data harvesting , no
subscription . Storage is the only cost , and it is small . During the
opening wave Silent Mode absorbs it ; afterwards it is a flat per - listing
fee paid in BCH . < / p >
< / s e c t i o n >
< / d i v > ` ,
} ;
}
/* ------------------------------- 404 -------------------------------- */
export function notFound ( req ) {
return {
title : 'Not here' ,
description : 'The requested page does not exist on Prometheus.' ,
here : '' ,
body : `
< header class = "hero compact" >
< h1 > Not here < / h 1 >
< p class = "tag" > Nothing lives at < code class = "inline" > $ { escapeHtml ( req ? . url || '' ) } < / c o d e > . < / p >
< / h e a d e r >
< div class = "wrap" >
< section class = "plain" >
< p > Try the < a href = "/" > home page < / a > , t h e < a h r e f = " / b r o w s e " > b r o w s e < / a > o r t h e
< a href = "/publish" > publishers ' walkthrough < / a > . < / p >
< / s e c t i o n >
< / d i v > ` ,
} ;
}
/* ------------------------------- helpers ---------------------------- */
function escapeHtml ( s ) {
return String ( s )
. replaceAll ( '&' , '&' )
. replaceAll ( '<' , '<' )
. replaceAll ( '>' , '>' )
. replaceAll ( '"' , '"' )
. replaceAll ( "'" , ''' ) ;
}