prometheus: allow BCH-address-only publishers (no BNS name needed)
A BNS name costs cents but is still a gate — someone discovering Prometheus for the first time shouldn't have to go register sirius.x before they can publish. Accept a raw cashaddress in the `publisher` field: the sign path skips the BNS owner-of lookup and treats the field itself as the signing address, which is still an ECDSA pubkey the chain verifies against. - listings.js: PUBLISHER_RE = BNS name OR cashaddress; isCashaddr() helper; verify branch skips lookupName() and uses the address directly as expectedAddress (recovery-is-not-verification still holds — we compare the recovered address to the claimed one, both sides) - pages.js submit form: pattern + placeholder + hint explain both options, with the trade-off (address publishers display as truncated hex instead of a readable name) - pages-browse.js: shortenPublisher() collapses raw addresses to bchtest:qpumq…sh478 on cards + detail page + <title>; full address kept in title attribute for hover, and in the URL path. BNS names are untouched.
This commit is contained in:
parent
f5a8ed40e7
commit
1abaefe948
3 changed files with 68 additions and 25 deletions
|
|
@ -49,7 +49,14 @@ const CATEGORIES = new Set([
|
|||
|
||||
const MAX_BODY_BYTES = 1024 * 1024; // 1 MiB — listings are small JSON
|
||||
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$/;
|
||||
const BNS_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}\.[a-z0-9]{1,20}$/;
|
||||
// A publisher is EITHER a BNS name (foo.x) OR a raw cashaddress (bitcoincash:q…).
|
||||
// Address-only publishers skip the on-chain name lookup — the signer_address
|
||||
// IS the identity, with no name claim to verify. Readers' trust lists can
|
||||
// still subscribe to the address; they just lose the mnemonic benefit of a name.
|
||||
const BNS_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}\.[a-z0-9]{1,20}$/;
|
||||
const CASHADDR_RE = /^(bitcoincash|bchtest):q[a-z0-9]{41}$/;
|
||||
const PUBLISHER_RE = new RegExp(`(?:${BNS_NAME_RE.source.slice(1,-1)})|(?:${CASHADDR_RE.source.slice(1,-1)})`);
|
||||
function isCashaddr(s) { return CASHADDR_RE.test(String(s)); }
|
||||
|
||||
/* ======================================================================
|
||||
POST /api/listing
|
||||
|
|
@ -82,19 +89,34 @@ export async function handleListingCreate(req, res) {
|
|||
// field except itself, so changing any value invalidates the signature.
|
||||
const message = buildSigningMessage(body);
|
||||
|
||||
// Resolve the publisher's claimed BNS name to its current on-chain owner.
|
||||
// This is the trust claim — recovery alone is never enough; we must know
|
||||
// *who* was supposed to sign before we accept a signature as proof.
|
||||
// Two paths for the "who signed this" check:
|
||||
//
|
||||
// - BNS name: look up the current on-chain owner, verify the signature
|
||||
// recovers to that address. The address can change (name transfer); the
|
||||
// signature's validity is anchored to who owns the name RIGHT NOW.
|
||||
//
|
||||
// - Cashaddress: skip the lookup entirely. The address IS the identity,
|
||||
// and the signature just has to recover to it. The publisher trades the
|
||||
// mnemonic benefit of a name for not needing one.
|
||||
//
|
||||
// Either way, recovery alone is never enough — we always compare the
|
||||
// recovered address against a claimed one, per [[recovery-is-not-verification]].
|
||||
const name = String(body.publisher).toLowerCase();
|
||||
const nameInfo = await lookupName(name);
|
||||
if (!nameInfo.ok) {
|
||||
return json(res, 400, { ok: false, reason: `publisher '${name}': ${nameInfo.reason}` });
|
||||
let expectedAddress;
|
||||
if (isCashaddr(name)) {
|
||||
expectedAddress = name;
|
||||
} else {
|
||||
const nameInfo = await lookupName(name);
|
||||
if (!nameInfo.ok) {
|
||||
return json(res, 400, { ok: false, reason: `publisher '${name}': ${nameInfo.reason}` });
|
||||
}
|
||||
expectedAddress = nameInfo.owner;
|
||||
}
|
||||
|
||||
const verify = verifyControl({
|
||||
message,
|
||||
signature: body.signature,
|
||||
expectedAddress: nameInfo.owner,
|
||||
expectedAddress,
|
||||
prefix: DEFAULT_PREFIX,
|
||||
});
|
||||
if (!verify.ok) {
|
||||
|
|
@ -102,7 +124,7 @@ export async function handleListingCreate(req, res) {
|
|||
ok: false,
|
||||
reason: `signature verification failed: ${verify.reason}`,
|
||||
...(verify.signedBy ? { signed_by: verify.signedBy } : {}),
|
||||
claimed_owner: nameInfo.owner,
|
||||
claimed_owner: expectedAddress,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -198,8 +220,11 @@ function validate(body) {
|
|||
if (!body || typeof body !== 'object') {
|
||||
return { ok: false, reason: 'body must be a JSON object' };
|
||||
}
|
||||
if (!BNS_NAME_RE.test(String(body.publisher || '').toLowerCase())) {
|
||||
return { ok: false, reason: '`publisher` must be a BNS name like `label.tld`' };
|
||||
if (!PUBLISHER_RE.test(String(body.publisher || '').toLowerCase())) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: '`publisher` must be a BNS name like `foo.x` or a BCH cashaddress like `bitcoincash:q…`',
|
||||
};
|
||||
}
|
||||
if (!SLUG_RE.test(String(body.slug || '').toLowerCase())) {
|
||||
return { ok: false, reason: '`slug` must be [a-z0-9-], 1..64 chars, no leading/trailing dash' };
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ export function appPage(req, publisher, slug) {
|
|||
: `<div style="width:80px;height:80px;border-radius:16px;border:1px solid var(--line);background:var(--panel);display:grid;place-items:center;font-size:36px">📦</div>`;
|
||||
|
||||
return {
|
||||
title: `${row.name} by ${row.publisher}`,
|
||||
title: `${row.name} by ${shortenPublisher(row.publisher)}`,
|
||||
description: row.description.slice(0, 200),
|
||||
here: `/app/${row.publisher}/${row.slug}`,
|
||||
ogType: 'article',
|
||||
|
|
@ -334,7 +334,7 @@ export function appPage(req, publisher, slug) {
|
|||
<div style="text-align:left">
|
||||
<h1 style="margin:0;font-size:1.8rem">${escapeHtml(row.name)}</h1>
|
||||
<div style="color:var(--mut);font-size:14px;margin-top:.3rem">
|
||||
by <b style="color:var(--ink)">${escapeHtml(row.publisher)}</b> ·
|
||||
by <b style="color:var(--ink)" title="${escapeAttr(row.publisher)}">${escapeHtml(shortenPublisher(row.publisher))}</b> ·
|
||||
v${escapeHtml(row.version)} ·
|
||||
${catChip}
|
||||
</div>
|
||||
|
|
@ -375,10 +375,12 @@ export function appPage(req, publisher, slug) {
|
|||
|
||||
<section>
|
||||
<h2>Who published this</h2>
|
||||
<p>Signed by <b>${escapeHtml(row.publisher)}</b> — a BNS name owned by
|
||||
<code class="inline">${escapeHtml(row.signer_address)}</code>. Any installer
|
||||
the chain says belongs to that key is trusted by your client; any client
|
||||
that follows a different trust list will not see it as trusted.</p>
|
||||
<p>Signed by <b title="${escapeAttr(row.publisher)}">${escapeHtml(shortenPublisher(row.publisher))}</b> —
|
||||
${/^(bitcoincash|bchtest):q[a-z0-9]{41}$/.test(String(row.publisher))
|
||||
? `a raw BCH address publisher. The signing key is this address itself; no BNS name is attached.`
|
||||
: `a BNS name owned by <code class="inline">${escapeHtml(row.signer_address)}</code>.`}
|
||||
Any installer the chain says belongs to that key is trusted by your client;
|
||||
any client that follows a different trust list will not see it as trusted.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
|
@ -414,7 +416,7 @@ function renderListingCard(row) {
|
|||
${escapeHtml(truncate(row.description, 160))}
|
||||
</p>
|
||||
<div style="color:var(--dim);font-size:12px;margin-top:auto;padding-top:.5rem">
|
||||
by ${escapeHtml(row.publisher)} · v${escapeHtml(row.version)} · ${formatBytes(row.download_size)}
|
||||
by ${escapeHtml(shortenPublisher(row.publisher))} · v${escapeHtml(row.version)} · ${formatBytes(row.download_size)}
|
||||
</div>
|
||||
</a>`;
|
||||
}
|
||||
|
|
@ -455,6 +457,20 @@ function shortSha(sha) {
|
|||
return `${s.slice(0, 8)}…${s.slice(-6)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten a publisher for display. BNS names (foo.x) are returned as-is;
|
||||
* raw cashaddresses are collapsed to `bchtest:qpumq…sh478` so they fit a
|
||||
* card without breaking the layout. The URL/path side still uses the full
|
||||
* publisher — this is a display-only transform.
|
||||
*/
|
||||
function shortenPublisher(pub) {
|
||||
const s = String(pub || '');
|
||||
const m = s.match(/^(bitcoincash|bchtest):(q[a-z0-9]{41})$/);
|
||||
if (!m) return s;
|
||||
const addr = m[2];
|
||||
return `${m[1]}:${addr.slice(0, 5)}…${addr.slice(-5)}`;
|
||||
}
|
||||
|
||||
function truncate(s, max) {
|
||||
const str = String(s || '');
|
||||
if (str.length <= max) return str;
|
||||
|
|
|
|||
|
|
@ -446,11 +446,11 @@ export function submit() {
|
|||
the signed payload is a new signature, not a quick fix.</p>
|
||||
|
||||
<form id="manifest-form" style="display:grid;gap:14px;max-width:620px;margin-top:1rem">
|
||||
<label>Publisher BNS name
|
||||
<input name="publisher" required placeholder="example.x"
|
||||
pattern="^[a-z0-9][a-z0-9-]{0,62}\\.[a-z0-9]{1,20}$"
|
||||
<label>Publisher
|
||||
<input name="publisher" required placeholder="foo.x — or bchtest:q..."
|
||||
pattern="(?:^[a-z0-9][a-z0-9-]{0,62}\\.[a-z0-9]{1,20}$)|(?:^(?:bitcoincash|bchtest):q[a-z0-9]{41}$)"
|
||||
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>
|
||||
<small style="color:var(--dim)">A BNS name like <code class="inline">foo.x</code> (register at <a href="//sirius.x/">sirius.x</a>, costs cents) — <b>or</b> a BCH cashaddress if you don't have a name yet. Addresses work; they just show on your listing as <code class="inline">bchtest:qpumq…sh478</code> instead of a readable name, which is harder for readers to follow.</small>
|
||||
</label>
|
||||
|
||||
<label>Short slug (URL)
|
||||
|
|
@ -530,9 +530,11 @@ export function submit() {
|
|||
</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>
|
||||
form. Sign it with the BCH key for
|
||||
<b style="color:var(--ink)" id="sign-publisher">your publisher</b>
|
||||
— the on-chain owner of your BNS name, or the key for your cashaddress
|
||||
if you went that route. 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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue