prometheus/service/pages.js

1060 lines
46 KiB
JavaScript
Raw Normal View History

/**
* 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.
*/
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</h2>
<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</code>) is in
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.</div>
` : ''}
</section>`;
}
/* ------------------------------- home ------------------------------- */
export function home() {
prometheus: size-tiered pricing, 2 GiB cap, new hero copy, SEO LD Three threads, one commit: 1. Size-tiered listing fee instead of a flat $1 priceUsdForSize(bytes) in service/payments.js: 0 - 100 MiB → free 100.001 - 200 MiB → $1 200.001 - 300 MiB → $2 ... 1900.001 - 2000 MiB → $19 2000.001 - 2048 MiB → $20 (ceiling at 2 GiB) listingNeedsPayment(bytes) replaces paymentsRequired() — a free-tier upload always publishes immediately even when PAYMENT_REQUIRED=1; the operator switch only governs the paid tiers. createInvoice derives the fee from the listing's own size at invoice time. 2. Upload cap back to 2 GiB The 200-MiB cap from the previous commit (which fixed the Caddy-413 HTML bug) is reverted. The paywall is now the real size gate; Caddy is just the ceiling. p2pexchange Caddyfile already updated + reloaded. 3. Hero + SEO Hero leads with the plain-English product category: The decentralized app store. A home for apps, programs, dApps, and libraries — listed and settled on the Bitcoin Cash chain, hosted on the decentralized web, endorsed by signature. Anyone can publish. Nobody can gatekeep. Named for the one who gave fire to humans and did not ask the gods first. "hosted on Sia" → "hosted on the decentralized web". Sia is the mechanism, not the pitch — general readers aren't tripped on the acronym on the hero. Meta description rewritten to lead with "decentralized app store" and name the long-tail terms the audience searches for ("web3 browsers", "BCH wallets", "mobile and desktop apps"). Two JSON-LD blocks (WebSite + Organization) inlined on home via layout's headExtra channel — knowledge panel + WebSite result card. 4. Submit wizard Step 1 result shows the computed listing fee under the scan verdict, so publishers see the price BEFORE filling out the manifest. Step 4 publish branches on the server's payment_required flag. On a paid listing it POSTs /api/invoice right after the listing POST and renders a payment card with the exact sat amount, the operator receive address, a `bitcoincash:` URI the wallet opens, and the invoice status URL for polling.
2026-10-22 23:31:43 +03:00
// Schema.org on home — gives Google a WebSite + SearchAction result card for
// "prometheus.x" queries, plus Organization for the knowledge panel.
const websiteLd = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Prometheus.X',
alternateName: ['Prometheus', 'Decentralized App Store'],
url: 'https://prometheus.x/',
description: 'The decentralized app store. Apps, programs, dApps and libraries signed on the Bitcoin Cash chain and hosted on the decentralized web.',
});
const orgLd = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Prometheus',
url: 'https://prometheus.x/',
sameAs: ['https://silentmode.st/prometheus', 'https://code.silentmode.st/silentmode/prometheus'],
description: 'A permissionless app store: listings on the Bitcoin Cash chain, binaries on the decentralized web, endorsements by signature. Anyone can publish; nobody can gatekeep.',
});
return {
prometheus: size-tiered pricing, 2 GiB cap, new hero copy, SEO LD Three threads, one commit: 1. Size-tiered listing fee instead of a flat $1 priceUsdForSize(bytes) in service/payments.js: 0 - 100 MiB → free 100.001 - 200 MiB → $1 200.001 - 300 MiB → $2 ... 1900.001 - 2000 MiB → $19 2000.001 - 2048 MiB → $20 (ceiling at 2 GiB) listingNeedsPayment(bytes) replaces paymentsRequired() — a free-tier upload always publishes immediately even when PAYMENT_REQUIRED=1; the operator switch only governs the paid tiers. createInvoice derives the fee from the listing's own size at invoice time. 2. Upload cap back to 2 GiB The 200-MiB cap from the previous commit (which fixed the Caddy-413 HTML bug) is reverted. The paywall is now the real size gate; Caddy is just the ceiling. p2pexchange Caddyfile already updated + reloaded. 3. Hero + SEO Hero leads with the plain-English product category: The decentralized app store. A home for apps, programs, dApps, and libraries — listed and settled on the Bitcoin Cash chain, hosted on the decentralized web, endorsed by signature. Anyone can publish. Nobody can gatekeep. Named for the one who gave fire to humans and did not ask the gods first. "hosted on Sia" → "hosted on the decentralized web". Sia is the mechanism, not the pitch — general readers aren't tripped on the acronym on the hero. Meta description rewritten to lead with "decentralized app store" and name the long-tail terms the audience searches for ("web3 browsers", "BCH wallets", "mobile and desktop apps"). Two JSON-LD blocks (WebSite + Organization) inlined on home via layout's headExtra channel — knowledge panel + WebSite result card. 4. Submit wizard Step 1 result shows the computed listing fee under the scan verdict, so publishers see the price BEFORE filling out the manifest. Step 4 publish branches on the server's payment_required flag. On a paid listing it POSTs /api/invoice right after the listing POST and renders a payment card with the exact sat amount, the operator receive address, a `bitcoincash:` URI the wallet opens, and the invoice status URL for polling.
2026-10-22 23:31:43 +03:00
title: 'The decentralized app store',
description:
prometheus: size-tiered pricing, 2 GiB cap, new hero copy, SEO LD Three threads, one commit: 1. Size-tiered listing fee instead of a flat $1 priceUsdForSize(bytes) in service/payments.js: 0 - 100 MiB → free 100.001 - 200 MiB → $1 200.001 - 300 MiB → $2 ... 1900.001 - 2000 MiB → $19 2000.001 - 2048 MiB → $20 (ceiling at 2 GiB) listingNeedsPayment(bytes) replaces paymentsRequired() — a free-tier upload always publishes immediately even when PAYMENT_REQUIRED=1; the operator switch only governs the paid tiers. createInvoice derives the fee from the listing's own size at invoice time. 2. Upload cap back to 2 GiB The 200-MiB cap from the previous commit (which fixed the Caddy-413 HTML bug) is reverted. The paywall is now the real size gate; Caddy is just the ceiling. p2pexchange Caddyfile already updated + reloaded. 3. Hero + SEO Hero leads with the plain-English product category: The decentralized app store. A home for apps, programs, dApps, and libraries — listed and settled on the Bitcoin Cash chain, hosted on the decentralized web, endorsed by signature. Anyone can publish. Nobody can gatekeep. Named for the one who gave fire to humans and did not ask the gods first. "hosted on Sia" → "hosted on the decentralized web". Sia is the mechanism, not the pitch — general readers aren't tripped on the acronym on the hero. Meta description rewritten to lead with "decentralized app store" and name the long-tail terms the audience searches for ("web3 browsers", "BCH wallets", "mobile and desktop apps"). Two JSON-LD blocks (WebSite + Organization) inlined on home via layout's headExtra channel — knowledge panel + WebSite result card. 4. Submit wizard Step 1 result shows the computed listing fee under the scan verdict, so publishers see the price BEFORE filling out the manifest. Step 4 publish branches on the server's payment_required flag. On a paid listing it POSTs /api/invoice right after the listing POST and renders a payment card with the exact sat amount, the operator receive address, a `bitcoincash:` URI the wallet opens, and the invoice status URL for polling.
2026-10-22 23:31:43 +03:00
'Prometheus.X — the decentralized app store. Web3 browsers, BCH wallets, mobile and desktop apps, dApps and libraries. Listings on the Bitcoin Cash chain, binaries on the decentralized web, endorsements by signature. Anyone can publish; nobody can gatekeep.',
here: '/',
prometheus: size-tiered pricing, 2 GiB cap, new hero copy, SEO LD Three threads, one commit: 1. Size-tiered listing fee instead of a flat $1 priceUsdForSize(bytes) in service/payments.js: 0 - 100 MiB → free 100.001 - 200 MiB → $1 200.001 - 300 MiB → $2 ... 1900.001 - 2000 MiB → $19 2000.001 - 2048 MiB → $20 (ceiling at 2 GiB) listingNeedsPayment(bytes) replaces paymentsRequired() — a free-tier upload always publishes immediately even when PAYMENT_REQUIRED=1; the operator switch only governs the paid tiers. createInvoice derives the fee from the listing's own size at invoice time. 2. Upload cap back to 2 GiB The 200-MiB cap from the previous commit (which fixed the Caddy-413 HTML bug) is reverted. The paywall is now the real size gate; Caddy is just the ceiling. p2pexchange Caddyfile already updated + reloaded. 3. Hero + SEO Hero leads with the plain-English product category: The decentralized app store. A home for apps, programs, dApps, and libraries — listed and settled on the Bitcoin Cash chain, hosted on the decentralized web, endorsed by signature. Anyone can publish. Nobody can gatekeep. Named for the one who gave fire to humans and did not ask the gods first. "hosted on Sia" → "hosted on the decentralized web". Sia is the mechanism, not the pitch — general readers aren't tripped on the acronym on the hero. Meta description rewritten to lead with "decentralized app store" and name the long-tail terms the audience searches for ("web3 browsers", "BCH wallets", "mobile and desktop apps"). Two JSON-LD blocks (WebSite + Organization) inlined on home via layout's headExtra channel — knowledge panel + WebSite result card. 4. Submit wizard Step 1 result shows the computed listing fee under the scan verdict, so publishers see the price BEFORE filling out the manifest. Step 4 publish branches on the server's payment_required flag. On a paid listing it POSTs /api/invoice right after the listing POST and renders a payment card with the exact sat amount, the operator receive address, a `bitcoincash:` URI the wallet opens, and the invoice status URL for polling.
2026-10-22 23:31:43 +03:00
headExtra:
`<script type="application/ld+json">${websiteLd}</script>\n` +
`<script type="application/ld+json">${orgLd}</script>`,
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"/>
</svg>
</div>
<h1>Prometheus<span class="g">.X</span></h1>
prometheus: size-tiered pricing, 2 GiB cap, new hero copy, SEO LD Three threads, one commit: 1. Size-tiered listing fee instead of a flat $1 priceUsdForSize(bytes) in service/payments.js: 0 - 100 MiB → free 100.001 - 200 MiB → $1 200.001 - 300 MiB → $2 ... 1900.001 - 2000 MiB → $19 2000.001 - 2048 MiB → $20 (ceiling at 2 GiB) listingNeedsPayment(bytes) replaces paymentsRequired() — a free-tier upload always publishes immediately even when PAYMENT_REQUIRED=1; the operator switch only governs the paid tiers. createInvoice derives the fee from the listing's own size at invoice time. 2. Upload cap back to 2 GiB The 200-MiB cap from the previous commit (which fixed the Caddy-413 HTML bug) is reverted. The paywall is now the real size gate; Caddy is just the ceiling. p2pexchange Caddyfile already updated + reloaded. 3. Hero + SEO Hero leads with the plain-English product category: The decentralized app store. A home for apps, programs, dApps, and libraries — listed and settled on the Bitcoin Cash chain, hosted on the decentralized web, endorsed by signature. Anyone can publish. Nobody can gatekeep. Named for the one who gave fire to humans and did not ask the gods first. "hosted on Sia" → "hosted on the decentralized web". Sia is the mechanism, not the pitch — general readers aren't tripped on the acronym on the hero. Meta description rewritten to lead with "decentralized app store" and name the long-tail terms the audience searches for ("web3 browsers", "BCH wallets", "mobile and desktop apps"). Two JSON-LD blocks (WebSite + Organization) inlined on home via layout's headExtra channel — knowledge panel + WebSite result card. 4. Submit wizard Step 1 result shows the computed listing fee under the scan verdict, so publishers see the price BEFORE filling out the manifest. Step 4 publish branches on the server's payment_required flag. On a paid listing it POSTs /api/invoice right after the listing POST and renders a payment card with the exact sat amount, the operator receive address, a `bitcoincash:` URI the wallet opens, and the invoice status URL for polling.
2026-10-22 23:31:43 +03:00
<p class="tag"><b>The decentralized app store.</b> A home for apps, programs,
dApps, and libraries — listed and settled on the Bitcoin Cash chain,
hosted on the decentralized web, endorsed by signature. Anyone can
publish. Nobody can gatekeep.</p>
<p class="tag" style="margin-top:1rem;color:var(--dim);font-size:14px">
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>
</div>
</header>
<div class="wrap">
${renderHomeBrowseSection()}
<section>
<h2>How Prometheus works, in a nutshell</h2>
<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</code>
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.</span></li>
<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.</span></li>
<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.</span></li>
<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.</span></li>
</ol>
<p style="margin-top:1.2rem"><a href="/publish">The full publishers' walkthrough,
step by step →</a></p>
</section>
</div>`,
};
}
/* ------------------------------- 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</h1>
<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>
</header>
<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</span> or
<span class="pill p-designed">designed</span> so it is clear what you can do
now and what is waiting on the APS1 record format landing.</p>
</section>
<section>
<h2>What you need before you start</h2>
<div class="grid">
<div class="card">
<span class="pill p-live">available today</span>
<h3>A BNS name</h3>
<p>Any name registered through <a href="//sirius.x/">Sirius</a> works. The
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>
</div>
<div class="card">
<span class="pill p-live">available today</span>
<h3>Somewhere to put the binary</h3>
<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>
</div>
<div class="card">
<span class="pill p-live">available today</span>
<h3>A signing key</h3>
<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>
</div>
<div class="card">
<span class="pill p-live">available today</span>
<h3>A pinch of BCH</h3>
<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>
</div>
</div>
</section>
<section>
<h2>The walkthrough</h2>
<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.</span></li>
<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.</span></li>
<li><b>3. Fill in the manifest.</b>
<span>Name, description, category, version, icon, download URL,
<code class="inline">sha256</code>, and the signing-cert fingerprint when
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.</span></li>
<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.</span></li>
<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.</span></li>
<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.</span></li>
<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.</span></li>
</ol>
</section>
<section>
<h2>A note on endorsements</h2>
<p>A listing is one signed record. An <em>endorsement</em> is another — a short
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> Any name can publish an endorsement
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.</div>
</section>
<section>
<h2>What this costs</h2>
<div class="grid">
<div class="card">
<h3>Per listing</h3>
<p>One Bitcoin Cash transaction. Fractions of a cent at current rates. Paid
once, lives on chain.</p>
</div>
<div class="card">
<h3>Per update</h3>
<p>Another transaction. Same shape, same cost. There is no "update bundle"
discount because there is no central party to collect one.</p>
</div>
<div class="card">
<h3>Per month of hosting</h3>
<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>
</div>
<div class="card">
<h3>Per download</h3>
<p>Zero to the directory. The reader pulls the binary from wherever the
manifest points; Prometheus never meters the traffic.</p>
</div>
</div>
</section>
<section>
<div class="note warn"><b>Where this stands today.</b> Names, Sia hosting and
BCH signing exist and are in use. The <code class="inline">APS1</code> record
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.</div>
</section>
</div>`,
};
}
/* ------------------------------- browse ----------------------------- */
export function browse() {
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'}.`;
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</h1>
<p class="tag">Signed listings on chain, scan reports attached, your trust list
decides what to render.</p>
</header>
<div class="wrap">
<section class="plain">
<p class="lede">${totalLabel}</p>
${renderCategoriesGrid({ verbose: false })}
</section>
</div>`,
};
}
/* ------------------------------- 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.',
here: '/submit',
body: `
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<style>
/* Collapsible step cards for the submit wizard. The step indicator and
the step content are the same DOM — a step's "head" (number + title +
state chip) is always visible; the "body" (form, result, etc.) only
shows when the step is current. */
.wiz-step{
border:1px solid var(--line);border-radius:14px;background:var(--panel);
margin:0 0 14px;overflow:hidden;transition:border-color .15s;
}
.wiz-step[data-state="current"]{border-color:rgba(214,255,61,.35)}
.wiz-step[data-state="pending"]{opacity:.55}
.wiz-head{
display:flex;align-items:center;gap:14px;padding:16px 20px;cursor:pointer;
user-select:none;background:transparent;border:0;color:inherit;
font:inherit;text-align:left;width:100%;
}
.wiz-step[data-state="pending"] .wiz-head{cursor:not-allowed}
.wiz-num{
width:28px;height:28px;border-radius:50%;
background:rgba(214,255,61,.14);color:var(--acid);
display:grid;place-items:center;font-weight:700;font-size:14px;flex-shrink:0;
}
.wiz-step[data-state="complete"] .wiz-num{
background:var(--acid);color:#0b0e14;
}
.wiz-title{font-size:15px;font-weight:600;color:var(--ink)}
.wiz-sub{color:var(--mut);font-size:13.5px;margin-left:4px}
.wiz-chip{
margin-left:auto;font-size:11.5px;padding:3px 10px;border-radius:999px;
text-transform:uppercase;letter-spacing:.04em;
}
.wiz-state-complete{background:rgba(79,209,165,.14);color:var(--srv)}
.wiz-state-current{background:rgba(214,255,61,.14);color:var(--acid)}
.wiz-state-pending{background:rgba(139,152,169,.14);color:var(--mut)}
.wiz-body{padding:0 20px 22px;display:none}
.wiz-step[data-state="current"] .wiz-body{display:block}
/* Drag-and-drop upload zone */
.drop-zone{
display:block;padding:46px 24px;border:2px dashed var(--line);
border-radius:14px;text-align:center;cursor:pointer;background:var(--panel2);
transition:border-color .18s ease, background .18s ease, transform .18s ease;
}
.drop-zone:hover,.drop-zone.dragover{
border-color:var(--acid);background:rgba(214,255,61,.05);
}
.drop-zone.dragover{transform:scale(1.01)}
.drop-zone .drop-icon{font-size:34px;line-height:1;margin-bottom:.6rem}
.drop-zone .drop-main{color:var(--ink);font-size:15px}
.drop-zone .drop-link{color:var(--acid);text-decoration:underline}
.drop-zone .drop-small{color:var(--dim);font-size:12.5px;margin-top:.5rem;display:block}
.drop-zone input[type=file]{display:none}
</style>
<header class="hero compact">
<h1>Submit an app</h1>
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>
</header>
<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
<!-- =============== STEP 1: upload =============== -->
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<article class="wiz-step" data-step="1" data-state="current" id="step1">
<button class="wiz-head" type="button" data-jump="1">
<span class="wiz-num">1</span>
<span class="wiz-title">Upload</span>
<span class="wiz-sub" id="s1-sub">the binary (up to 2 GiB — free below 100 MiB)</span>
<span class="wiz-chip wiz-state-current" id="s1-chip">current</span>
</button>
<div class="wiz-body">
<p class="lede">Drop a binary below, or pick a file. 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>
<label class="drop-zone" id="drop-zone">
<input type="file" name="file" id="file-input">
<div class="drop-icon">📁</div>
<div class="drop-main"><b>Drop a file here</b>, or <span class="drop-link">pick one</span></div>
<small class="drop-small">Any format. 2 GiB cap. Free below 100 MiB, then $1 per 100 MiB.</small>
</label>
<div id="upload-status" style="color:var(--mut);font-size:14px;min-height:1.4em;margin-top:1rem"></div>
<div id="upload-result" class="card" style="display:none;margin-top:1rem">
<h3 style="margin:0 0 .4rem">Upload accepted</h3>
<p style="margin:0;color:var(--mut);font-size:13.5px">
<b style="color:var(--ink)" id="u-name"></b> ·
<span id="u-size"></span> ·
<code class="inline" id="u-hash"></code>
</p>
<p style="margin:.5rem 0 0" id="u-scan"></p>
<p style="margin:.5rem 0 0" id="u-price"></p>
<button class="btn acid" type="button" id="to-step2" style="margin-top:1rem">Continue → describe your listing</button>
</div>
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>
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
</article>
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 2: describe =============== -->
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<article class="wiz-step" data-step="2" data-state="pending" id="step2">
<button class="wiz-head" type="button" data-jump="2">
<span class="wiz-num">2</span>
<span class="wiz-title">Describe</span>
<span class="wiz-sub" id="s2-sub">name, publisher name, category, description</span>
<span class="wiz-chip wiz-state-pending" id="s2-chip">pending</span>
</button>
<div class="wiz-body">
<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">
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
<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>.</small>
</label>
<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;/&lt;slug&gt;</code>. Lowercase, hyphens; 1–64 chars.</small>
</label>
<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">
</label>
<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</option>
<option value="communication">Communication</option>
<option value="privacy">Privacy &amp; security</option>
<option value="devtools">Developer tools</option>
<option value="media">Media &amp; publishing</option>
<option value="productivity">Productivity</option>
<option value="other">Other</option>
</optgroup>
<option value="games">Games</option>
<option value="fintech">FinTech</option>
</select>
</label>
<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">
</label>
<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"></textarea>
<small style="color:var(--dim)">Up to 2000 characters.</small>
</label>
<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.</small>
</label>
<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">
</label>
<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.</small>
</label>
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<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</button>
<button class="btn acid" type="submit">Continue → sign</button>
</div>
</form>
</div>
</article>
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 =============== -->
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<article class="wiz-step" data-step="3" data-state="pending" id="step3">
<button class="wiz-head" type="button" data-jump="3">
<span class="wiz-num">3</span>
<span class="wiz-title">Sign</span>
<span class="wiz-sub" id="s3-sub">with the BCH key for your publisher BNS name</span>
<span class="wiz-chip wiz-state-pending" id="s3-chip">pending</span>
</button>
<div class="wiz-body">
<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> on chipnet.
Any BIP-137 wallet works (Electron Cash, Deviant, Cashonize, Theseus's Aegis).</p>
<h3 style="margin-top:1.4rem">Message to sign</h3>
<pre id="sign-message" style="max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-all;font-size:12px"></pre>
<div id="theseus-sign" style="display:none;margin-top:1.2rem;padding:14px 18px;background:var(--panel2);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</button>
</div>
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
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<div style="margin-top:1.2rem">
<h3 style="margin:0 0 .3rem">Or paste a BIP-137 signature</h3>
<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(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;font-size:12px;resize:vertical"
placeholder="H1234abcd…"></textarea>
</div>
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
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<div id="sign-status" style="color:var(--mut);font-size:14px;min-height:1.4em;margin-top:.8rem"></div>
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
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<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</button>
<button class="btn acid" type="button" id="to-step4" disabled>Continue → publish</button>
</div>
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>
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
</article>
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 4: publish =============== -->
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<article class="wiz-step" data-step="4" data-state="pending" id="step4">
<button class="wiz-head" type="button" data-jump="4">
<span class="wiz-num">4</span>
<span class="wiz-title">Publish</span>
<span class="wiz-sub" id="s4-sub">POST to the directory</span>
<span class="wiz-chip wiz-state-pending" id="s4-chip">pending</span>
</button>
<div class="wiz-body">
<p class="lede">POSTs the signed listing to <code class="inline">/api/listing</code>.
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>
<button class="btn ghost" type="button" id="back-to-3">← Back to sign</button>
<button class="btn acid" type="button" id="publish-btn">Publish now</button>
</div>
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
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
<div id="publish-status" style="color:var(--mut);font-size:14px;min-height:1.4em;margin-top:1rem"></div>
<div id="publish-result" style="margin-top:1rem"></div>
</div>
</article>
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
<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: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
priceUsd: null, // size-tiered fee computed in step 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
};
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);
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
const SUBS = {
1: 'the binary (up to 2 GiB — free below 100 MiB)',
2: 'name, publisher name, category, description',
3: 'with the BCH key for your publisher BNS name',
4: 'POST to the directory',
};
// A step is reachable only if every earlier step is complete OR it's the
// current one — stops a publisher from clicking "Publish" before they have
// signed anything, and leaves "come back and edit step 2" available.
function canReach(n) {
for (let i = 1; i < n; i++) {
const el = $('step' + i);
if (el.dataset.state !== 'complete' && el.dataset.state !== 'current') return false;
}
return true;
}
function setState(n, state) {
const el = $('step' + n);
el.dataset.state = state;
const chip = $('s' + n + '-chip');
chip.textContent = state;
chip.className = 'wiz-chip wiz-state-' + state;
}
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 show(step) {
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
for (const n of [1,2,3,4]) {
const el = $('step' + n);
const s = el.dataset.state;
if (n === step) setState(n, 'current');
else if (s === 'current') setState(n, canReach(n) && hasStepData(n) ? 'complete' : 'pending');
}
// Scroll the active step card into view, not the top of the page.
$('step' + step).scrollIntoView({behavior:'smooth', block:'start'});
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
}
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
// "Does the state have what step N would have produced?" — used when
// leaving a step to decide if it should be marked complete or back to
// pending (e.g., clicking back to step 2 before editing is still complete).
function hasStepData(n) {
if (n === 1) return !!W.upload;
if (n === 2) return !!W.manifest;
if (n === 3) return !!W.signature;
if (n === 4) return false; // publish never "completes" in-session
return false;
}
// Clickable headers — jump to any reachable step.
document.querySelectorAll('[data-jump]').forEach((btn) => {
btn.addEventListener('click', () => {
const n = Number(btn.dataset.jump);
if (!canReach(n)) return;
show(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
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 page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
// ===== STEP 1: upload (drag-drop zone + file picker) =====
const dz = $('drop-zone');
const fi = $('file-input');
dz.addEventListener('click', (ev) => {
// Clicking the hidden <input> directly works; stop click bubbling from
// the inner link span so we don't open the picker twice.
if (ev.target === fi) return;
fi.click();
});
['dragover','dragenter'].forEach((e) => dz.addEventListener(e, (ev) => {
ev.preventDefault(); dz.classList.add('dragover');
}));
['dragleave','drop'].forEach((e) => dz.addEventListener(e, () => dz.classList.remove('dragover')));
dz.addEventListener('drop', (ev) => {
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
ev.preventDefault();
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
const file = ev.dataTransfer.files[0];
if (file) startUpload(file);
});
fi.addEventListener('change', () => fi.files[0] && startUpload(fi.files[0]));
async function startUpload(file) {
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
W.file = file;
const status = $('upload-status');
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
$('upload-result').style.display = 'none';
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.textContent = \`Uploading \${file.name} (\${fmtBytes(file.size)}) and scanning…\`;
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
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>';
prometheus: size-tiered pricing, 2 GiB cap, new hero copy, SEO LD Three threads, one commit: 1. Size-tiered listing fee instead of a flat $1 priceUsdForSize(bytes) in service/payments.js: 0 - 100 MiB → free 100.001 - 200 MiB → $1 200.001 - 300 MiB → $2 ... 1900.001 - 2000 MiB → $19 2000.001 - 2048 MiB → $20 (ceiling at 2 GiB) listingNeedsPayment(bytes) replaces paymentsRequired() — a free-tier upload always publishes immediately even when PAYMENT_REQUIRED=1; the operator switch only governs the paid tiers. createInvoice derives the fee from the listing's own size at invoice time. 2. Upload cap back to 2 GiB The 200-MiB cap from the previous commit (which fixed the Caddy-413 HTML bug) is reverted. The paywall is now the real size gate; Caddy is just the ceiling. p2pexchange Caddyfile already updated + reloaded. 3. Hero + SEO Hero leads with the plain-English product category: The decentralized app store. A home for apps, programs, dApps, and libraries — listed and settled on the Bitcoin Cash chain, hosted on the decentralized web, endorsed by signature. Anyone can publish. Nobody can gatekeep. Named for the one who gave fire to humans and did not ask the gods first. "hosted on Sia" → "hosted on the decentralized web". Sia is the mechanism, not the pitch — general readers aren't tripped on the acronym on the hero. Meta description rewritten to lead with "decentralized app store" and name the long-tail terms the audience searches for ("web3 browsers", "BCH wallets", "mobile and desktop apps"). Two JSON-LD blocks (WebSite + Organization) inlined on home via layout's headExtra channel — knowledge panel + WebSite result card. 4. Submit wizard Step 1 result shows the computed listing fee under the scan verdict, so publishers see the price BEFORE filling out the manifest. Step 4 publish branches on the server's payment_required flag. On a paid listing it POSTs /api/invoice right after the listing POST and renders a payment card with the exact sat amount, the operator receive address, a `bitcoincash:` URI the wallet opens, and the invoice status URL for polling.
2026-10-22 23:31:43 +03:00
// Size-tier price: free below 100 MiB, then $1 USD per 100 MiB above.
const freeTier = 100 * 1024 * 1024;
const perTier = 100 * 1024 * 1024;
const priceUsd = json.upload.size <= freeTier
? 0
: Math.ceil((json.upload.size - freeTier) / perTier);
W.priceUsd = priceUsd;
$('u-price').innerHTML = priceUsd === 0
? '<span class="pill p-live">listing fee: free (≤100 MiB)</span>'
: '<span class="pill p-upcoming">listing fee: $' + priceUsd + ' USD (settled in BCH at the step-4 oracle rate)</span>';
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-result').style.display = 'block';
status.textContent = '';
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
// Mark step 1 complete + update the sub-text in its collapsed form so a
// publisher who scrolls back sees the actual filename not the hint.
$('s1-sub').textContent = json.upload.name + ' · ' + fmtBytes(json.upload.size) +
(priceUsd === 0 ? ' · free tier' : ' · $' + priceUsd + ' listing fee');
}
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 =====
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
$('to-step2').addEventListener('click', () => show(2));
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 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;
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
// Collapsed-step summary for step 2.
$('s2-sub').textContent = W.manifest.name + ' by ' + W.manifest.publisher +
' · ' + W.manifest.category + ' · v' + W.manifest.version;
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
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');
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
acceptSignature(base64, 'Theseus wallet');
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
} 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) {
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
acceptSignature(v, 'paste');
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
} else {
W.signature = null;
$('to-step4').disabled = true;
$('sign-status').textContent = v ? \`Need 86–88 chars; got \${v.length}.\` : '';
}
});
prometheus: submit page is a collapsible 4-card wizard Three polish fixes from a publisher's view in Theseus: 1. The redundant "1 · Upload the binary" h2 under a progress indicator that already said "Upload" is gone. The step indicator IS the step content now — each step is one <article> card whose head is always visible (number + title + state chip) and whose body opens only when that step is current. 2. Upload input became a proper drag-and-drop zone. 46 px tall, dashed border that lights up acid on dragover (and the whole card wobbles 1% so there's no doubt the drop will land). Click opens the file picker; drop handles files directly. 3. Step headers are clickable. A publisher can jump back to any completed step to re-check or edit (canReach() guards: step 4 can't be clicked if step 3 hasn't produced a signature yet). The head's sub-text updates as each step completes: 1 · "Upload" → "theseus-setup.exe · 137 MiB · $1 listing fee" 2 · "Describe" → "My App by foo.x · browsers · v1.0.0" 3 · "Sign" → "signed · IJIEBM6U…" Internals: - One <article data-step="N" data-state="current|complete|pending"> per step. CSS keys off data-state for the open/closed body and the chip's colour. One setState() helper, one show(n) that walks the four and decides each's state based on canReach() + hasStepData(n). - scroll-to-top replaced by scrollIntoView on the active card, so a publisher who jumps back to step 2 after reaching step 4 lands on the step-2 header, not the hero. - acceptSignature(base64, how) is now one path used by both the Theseus wallet handler and the paste listener — stops the two from drifting on what counts as "signature captured".
2026-10-23 01:17:54 +03:00
function acceptSignature(base64, how) {
W.signature = base64;
$('sig-paste').value = base64;
$('sign-status').innerHTML = '✓ Signed via ' + how + ' (' + base64.length + ' chars). Continue to publish.';
$('to-step4').disabled = false;
$('s3-sub').textContent = 'signed · ' + base64.slice(0, 10) + '…';
}
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
$('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;
}
const href = '/app/' + encodeURIComponent(json.listing.publisher) + '/' + encodeURIComponent(json.listing.slug);
prometheus: size-tiered pricing, 2 GiB cap, new hero copy, SEO LD Three threads, one commit: 1. Size-tiered listing fee instead of a flat $1 priceUsdForSize(bytes) in service/payments.js: 0 - 100 MiB → free 100.001 - 200 MiB → $1 200.001 - 300 MiB → $2 ... 1900.001 - 2000 MiB → $19 2000.001 - 2048 MiB → $20 (ceiling at 2 GiB) listingNeedsPayment(bytes) replaces paymentsRequired() — a free-tier upload always publishes immediately even when PAYMENT_REQUIRED=1; the operator switch only governs the paid tiers. createInvoice derives the fee from the listing's own size at invoice time. 2. Upload cap back to 2 GiB The 200-MiB cap from the previous commit (which fixed the Caddy-413 HTML bug) is reverted. The paywall is now the real size gate; Caddy is just the ceiling. p2pexchange Caddyfile already updated + reloaded. 3. Hero + SEO Hero leads with the plain-English product category: The decentralized app store. A home for apps, programs, dApps, and libraries — listed and settled on the Bitcoin Cash chain, hosted on the decentralized web, endorsed by signature. Anyone can publish. Nobody can gatekeep. Named for the one who gave fire to humans and did not ask the gods first. "hosted on Sia" → "hosted on the decentralized web". Sia is the mechanism, not the pitch — general readers aren't tripped on the acronym on the hero. Meta description rewritten to lead with "decentralized app store" and name the long-tail terms the audience searches for ("web3 browsers", "BCH wallets", "mobile and desktop apps"). Two JSON-LD blocks (WebSite + Organization) inlined on home via layout's headExtra channel — knowledge panel + WebSite result card. 4. Submit wizard Step 1 result shows the computed listing fee under the scan verdict, so publishers see the price BEFORE filling out the manifest. Step 4 publish branches on the server's payment_required flag. On a paid listing it POSTs /api/invoice right after the listing POST and renders a payment card with the exact sat amount, the operator receive address, a `bitcoincash:` URI the wallet opens, and the invoice status URL for polling.
2026-10-22 23:31:43 +03:00
if (json.payment_required) {
// Fetch the invoice right away so the publisher gets a payable URI
// without a second click. Admins reconcile from /admin/payments
// until the mempool watcher lands.
status.innerHTML = '💰 <b>Listing recorded; payment required to go live.</b>';
let invJson;
try {
const invR = await fetch('/api/invoice', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ listing_id: json.id }),
});
invJson = await invR.json();
if (!invR.ok || !invJson.ok) throw new Error(invJson.reason || invJson.error || 'invoice failed');
} catch (err) {
result.innerHTML =
'<div class="note danger">Could not issue invoice — ' + String(err?.message || err) +
'. Open <a href="/admin/payments">/admin/payments</a> and have an operator issue one by hand.</div>';
return;
}
const inv = invJson.invoice;
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>' +
'<hr style="border:none;border-top:1px solid var(--line);margin:1rem 0">' +
'<h4 style="margin:0 0 .3rem">Pay to publish — $' + inv.amount_usd + ' USD</h4>' +
'<p style="margin:0;color:var(--mut);font-size:13px">Exact amount <b style="color:var(--ink);font-family:ui-monospace,monospace">' + inv.amount_sat.toLocaleString() + ' sats</b> ' +
'(= ' + inv.amount_bch + ' BCH). To: <code class="inline" style="font-size:11px">' + inv.address + '</code></p>' +
'<p style="margin:1rem 0 0"><a class="btn acid" href="' + inv.bitcoincash_uri + '">Open in BCH wallet</a> ' +
'<a class="btn ghost" href="' + href + '">View your listing (hidden until paid)</a></p>' +
'<p style="margin:1rem 0 0;color:var(--dim);font-size:12.5px">Invoice expires at ' + inv.expires_at + '. Status: /api/invoice/' + inv.id + '</p>' +
'</div>';
return;
}
status.innerHTML = '✅ <b>Published.</b>';
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
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
})();
</script>
</div>`,
};
}
/* ------------------------------- 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</h1>
<p class="tag">What this directory is not, and why.</p>
</header>
<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>
</section>
<section>
<h2>What Prometheus is not</h2>
<div class="grid">
<div class="card">
<h3>Not a curator</h3>
<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>
</div>
<div class="card">
<h3>Not a moderator</h3>
<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>
</div>
<div class="card">
<h3>Not a payment processor</h3>
<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>
</div>
<div class="card">
<h3>Not an identity provider</h3>
<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>
</div>
</div>
</section>
<section>
<h2>What Silent Mode does control</h2>
<div class="grid">
<div class="card">
<h3>The scan report</h3>
<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>
</div>
<div class="card">
<h3>Which binaries we host</h3>
<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>
</div>
<div class="card">
<h3>The default sort</h3>
<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>
</div>
<div class="card">
<h3>The publish mode</h3>
<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>
</div>
</div>
</section>
<section>
<h2>Storage costs money</h2>
<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>
</section>
</div>`,
};
}
/* ------------------------------- 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</h1>
<p class="tag">Nothing lives at <code class="inline">${escapeHtml(req?.url || '')}</code>.</p>
</header>
<div class="wrap">
<section class="plain">
<p>Try the <a href="/">home page</a>, the <a href="/browse">browse</a> or the
<a href="/publish">publishers' walkthrough</a>.</p>
</section>
</div>`,
};
}
/* ------------------------------- helpers ---------------------------- */
function escapeHtml(s) {
return String(s)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}