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`.
This commit is contained in:
parent
d6f8504bfe
commit
916440fb31
2 changed files with 358 additions and 54 deletions
|
|
@ -28,7 +28,7 @@ import { signMessageBip137, addressFromPrivateKey } from '../service/lib/sign-me
|
|||
const BASE = process.env.BASE || 'http://127.0.0.1:3700';
|
||||
const PUBLISHER = process.env.PUBLISHER || 'testpub.x';
|
||||
const SLUG = process.env.SLUG || `hello-world-${Date.now()}`;
|
||||
const CATEGORY = process.env.CATEGORY || 'utilities';
|
||||
const CATEGORY = process.env.CATEGORY || 'productivity';
|
||||
|
||||
// A hard-coded deterministic test key — this is NEVER a real wallet key, just
|
||||
// a predictable 32-byte scalar so the roundtrip script can print the matching
|
||||
|
|
|
|||
410
service/pages.js
410
service/pages.js
|
|
@ -307,97 +307,401 @@ export function submit() {
|
|||
return {
|
||||
title: 'Submit an app',
|
||||
description:
|
||||
'Upload an app to Prometheus. We scan it, generate the manifest and the APS1 record, you sign with your BNS key and the listing goes on chain.',
|
||||
'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: `
|
||||
<header class="hero compact">
|
||||
<h1>Submit an app</h1>
|
||||
<p class="tag">Upload, scan, sign, publish. Four screens; your key stays with you.</p>
|
||||
<p class="tag">Upload → fill manifest → sign with your wallet → publish.
|
||||
Your key stays with you.</p>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<!-- Progress indicator -->
|
||||
<section class="plain">
|
||||
<div class="note warn"><b>Milestone 1 of 4: upload + scan.</b> This step is
|
||||
live and testable. The next milestone wires the manifest and APS1 record
|
||||
(see <a href="/publish">the publishers' walkthrough</a>); payment and the
|
||||
on-chain publish come after that.</div>
|
||||
|
||||
<ol class="steps" style="margin-top:1.4rem">
|
||||
<li><b>1. Upload the binary.</b>
|
||||
<span>Pick a file (any file — we don't care what the format is, only
|
||||
what ClamAV says about the bytes). The server scans it, hashes it,
|
||||
and hands back a signed scan report.</span></li>
|
||||
<li><b>2. Fill the manifest.</b>
|
||||
<span>Name, description, category, version, icon URL. Scan verdict is
|
||||
attached automatically. <em>Not wired yet.</em></span></li>
|
||||
<li><b>3. Sign with your BNS key.</b>
|
||||
<span>The service shows the APS1 payload. Sign in Theseus, paste a
|
||||
BIP-137 signature from any BCH wallet, or import your name's mnemonic
|
||||
once in the page (wiped on close). <em>Not wired yet.</em></span></li>
|
||||
<li><b>4. Pay the listing fee (eventually).</b>
|
||||
<span>Free during the opening wave; a BCH invoice for the per-listing
|
||||
fee once storage costs cross a threshold. <em>Not wired yet.</em></span></li>
|
||||
<ol class="steps" id="step-indicator" style="margin:0 0 2rem">
|
||||
<li data-step="1"><b>Upload</b> <span id="s1-sub">the binary (any size up to 2 GiB)</span></li>
|
||||
<li data-step="2"><b>Describe</b> <span id="s2-sub">name, publisher name, category, description</span></li>
|
||||
<li data-step="3"><b>Sign</b> <span id="s3-sub">with the BCH key for your publisher BNS name</span></li>
|
||||
<li data-step="4"><b>Publish</b> <span id="s4-sub">POST to the directory</span></li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Upload a file to try it</h2>
|
||||
<p class="lede">Pick any file to send through the scanner. The upload is
|
||||
accepted, SHA-256'd, and either kept (clean) or discarded (infected or
|
||||
error). Nothing is published; this is just the scan step.</p>
|
||||
<!-- =============== STEP 1: upload =============== -->
|
||||
<section id="step1">
|
||||
<h2>1 · Upload the binary</h2>
|
||||
<p class="lede">The server scans with ClamAV + VirusTotal (where configured),
|
||||
computes the SHA-256, and attaches a signed scan report to your listing.
|
||||
Only clean uploads can proceed.</p>
|
||||
|
||||
<form id="up" style="display:grid;gap:12px;max-width:520px;margin-top:1rem">
|
||||
<form id="upload-form" style="display:grid;gap:12px;max-width:560px;margin-top:1rem">
|
||||
<input type="file" name="file" required
|
||||
style="padding:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;color:var(--ink)">
|
||||
<button class="btn acid" type="submit">Upload & scan</button>
|
||||
<div id="status" style="color:var(--mut);font-size:14px;min-height:1.4em"></div>
|
||||
<div id="upload-status" style="color:var(--mut);font-size:14px;min-height:1.4em"></div>
|
||||
</form>
|
||||
|
||||
<pre id="result" style="display:none;max-height:480px;overflow:auto;margin-top:1.2rem"></pre>
|
||||
<div id="upload-result" class="card" style="display:none;margin-top:1.2rem">
|
||||
<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>
|
||||
<button class="btn acid" type="button" id="to-step2" style="margin-top:1rem">Continue → describe your listing</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- =============== STEP 2: describe =============== -->
|
||||
<section id="step2" style="display:none">
|
||||
<h2>2 · Describe your listing</h2>
|
||||
<p class="lede">Every field is signed in step 3, so take a minute — editing
|
||||
the signed payload is a new signature, not a quick fix.</p>
|
||||
|
||||
<form id="manifest-form" style="display:grid;gap:14px;max-width:620px;margin-top:1rem">
|
||||
<label>Publisher BNS name
|
||||
<input name="publisher" required placeholder="example.x"
|
||||
pattern="^[a-z0-9][a-z0-9-]{0,62}\\.[a-z0-9]{1,20}$"
|
||||
style="width:100%;padding:10px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-family:ui-monospace,monospace;margin-top:.3rem">
|
||||
<small style="color:var(--dim)">The BCH key for this name's on-chain owner is who signs. Register one at <a href="//sirius.x/">sirius.x</a>.</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/<publisher>/<slug></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 & web tools</option>
|
||||
<option value="communication">Communication</option>
|
||||
<option value="privacy">Privacy & security</option>
|
||||
<option value="devtools">Developer tools</option>
|
||||
<option value="media">Media & 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>
|
||||
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<!-- =============== STEP 3: sign =============== -->
|
||||
<section id="step3" style="display:none">
|
||||
<h2>3 · Sign with your BNS key</h2>
|
||||
<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(--panel);border:1px solid var(--line);border-left:3px solid var(--acid);border-radius:10px">
|
||||
<b style="color:var(--ink)">Theseus wallet detected.</b>
|
||||
<p style="margin:.3rem 0;color:var(--mut);font-size:14px">Aegis is injected on this page. Sign without pasting.</p>
|
||||
<button class="btn acid" type="button" id="theseus-sign-btn">Sign with Theseus wallet</button>
|
||||
</div>
|
||||
|
||||
<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(--panel);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>
|
||||
|
||||
<div id="sign-status" style="color:var(--mut);font-size:14px;min-height:1.4em;margin-top:.8rem"></div>
|
||||
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<!-- =============== STEP 4: publish =============== -->
|
||||
<section id="step4" style="display:none">
|
||||
<h2>4 · Publish</h2>
|
||||
<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>
|
||||
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:1rem">
|
||||
<button class="btn ghost" type="button" id="back-to-3">← Back to sign</button>
|
||||
<button class="btn acid" type="button" id="publish-btn">Publish now</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const form = document.getElementById('up');
|
||||
const status = document.getElementById('status');
|
||||
const result = document.getElementById('result');
|
||||
// ===== 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
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (ev) => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
function show(step) {
|
||||
for (const n of [1,2,3,4]) $('step'+n).style.display = n === step ? '' : 'none';
|
||||
// Progress indicator highlights the current step.
|
||||
document.querySelectorAll('#step-indicator li').forEach((li) => {
|
||||
const s = Number(li.dataset.step);
|
||||
li.style.opacity = s === step ? '1' : s < step ? '.5' : '.3';
|
||||
});
|
||||
window.scrollTo({top:0, behavior:'smooth'});
|
||||
}
|
||||
show(1);
|
||||
|
||||
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(',') + '}';
|
||||
}
|
||||
|
||||
// ===== STEP 1: upload =====
|
||||
$('upload-form').addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const file = form.file.files[0];
|
||||
const file = ev.target.file.files[0];
|
||||
if (!file) return;
|
||||
|
||||
status.textContent = \`Uploading \${file.name} (\${fmtBytes(file.size)})…\`;
|
||||
result.style.display = 'none';
|
||||
result.textContent = '';
|
||||
|
||||
W.file = file;
|
||||
const status = $('upload-status');
|
||||
status.textContent = \`Uploading \${file.name} (\${fmtBytes(file.size)}) and scanning…\`;
|
||||
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();
|
||||
const elapsed = Math.round(performance.now() - started);
|
||||
const v = json?.scan?.verdict;
|
||||
const emoji = v === 'clean' ? '✅' : v === 'infected' ? '🚨' : '⚠️';
|
||||
status.innerHTML = \`\${emoji} <b>\${v || 'done'}</b> — \${elapsed} ms round trip.\`;
|
||||
} catch (err) {
|
||||
status.innerHTML = '⚠️ <b>network error</b> — ' + String(err?.message || err);
|
||||
status.innerHTML = '⚠️ <b>Network error</b> — ' + String(err?.message || err);
|
||||
return;
|
||||
}
|
||||
result.style.display = 'block';
|
||||
result.textContent = JSON.stringify(json, null, 2);
|
||||
const elapsed = Math.round(performance.now() - started);
|
||||
if (!json.ok) {
|
||||
status.innerHTML = '⚠️ <b>Rejected</b> — ' + (json.reason || json.error || 'unknown error');
|
||||
return;
|
||||
}
|
||||
if (json.scan.verdict !== 'clean') {
|
||||
status.innerHTML = \`🚨 <b>\${json.scan.verdict}</b> — the scanner refused this file. \` +
|
||||
(json.scan.signatures?.length ? \`Signatures: \${json.scan.signatures.join(', ')}.\` : '') +
|
||||
' Try a different file.';
|
||||
return;
|
||||
}
|
||||
W.upload = json;
|
||||
$('u-name').textContent = json.upload.name;
|
||||
$('u-size').textContent = fmtBytes(json.upload.size);
|
||||
$('u-hash').textContent = json.upload.hash.slice(0,8) + '…' + json.upload.hash.slice(-6);
|
||||
$('u-scan').innerHTML = '<span class="pill p-live">scan ✓ clean (' + json.scan.engine + ', ' + elapsed + ' ms)</span>';
|
||||
$('upload-result').style.display = 'block';
|
||||
status.textContent = '';
|
||||
});
|
||||
|
||||
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';
|
||||
// ===== STEP 1 → 2 =====
|
||||
$('to-step2').addEventListener('click', () => {
|
||||
// Prefill name + download_url + hash preview.
|
||||
const f = $('manifest-form');
|
||||
if (W.upload) {
|
||||
// leave the fields the user needs to fill blank; show the hash we'll use
|
||||
}
|
||||
show(2);
|
||||
});
|
||||
|
||||
// ===== STEP 2: describe =====
|
||||
$('back-to-1').addEventListener('click', () => show(1));
|
||||
$('manifest-form').addEventListener('submit', (ev) => {
|
||||
ev.preventDefault();
|
||||
if (!W.upload) { alert('Upload a file first.'); show(1); return; }
|
||||
const f = ev.target;
|
||||
W.manifest = {
|
||||
publisher: f.publisher.value.trim().toLowerCase(),
|
||||
slug: f.slug.value.trim().toLowerCase(),
|
||||
name: f.name.value.trim(),
|
||||
description: f.description.value.trim(),
|
||||
category: f.category.value,
|
||||
version: f.version.value.trim(),
|
||||
homepage: f.homepage.value.trim() || null,
|
||||
icon: f.icon.value.trim() || null,
|
||||
download: {
|
||||
url: f.download_url.value.trim(),
|
||||
sha256: W.upload.upload.hash,
|
||||
size: W.upload.upload.size,
|
||||
},
|
||||
scan_report: W.upload.report,
|
||||
};
|
||||
// Canonical signing message, same shape service/listings.js builds.
|
||||
const canonical = canonicalJson({
|
||||
kind: 'prometheus.listing',
|
||||
v: 1,
|
||||
publisher: W.manifest.publisher,
|
||||
slug: W.manifest.slug,
|
||||
name: W.manifest.name,
|
||||
description: W.manifest.description,
|
||||
category: W.manifest.category,
|
||||
version: W.manifest.version,
|
||||
homepage: W.manifest.homepage,
|
||||
icon: W.manifest.icon,
|
||||
download: W.manifest.download,
|
||||
scan_report_pubkey: W.upload.report.pubkey,
|
||||
scan_report_signature: W.upload.report.signature,
|
||||
});
|
||||
W.message = 'PROMETHEUS-LISTING\\n' + canonical;
|
||||
$('sign-publisher').textContent = W.manifest.publisher;
|
||||
$('sign-message').textContent = W.message;
|
||||
// Reset step 3 state so a revise can proceed cleanly.
|
||||
W.signature = null;
|
||||
$('sig-paste').value = '';
|
||||
$('sign-status').textContent = '';
|
||||
$('to-step4').disabled = true;
|
||||
show(3);
|
||||
});
|
||||
|
||||
// ===== STEP 3: sign =====
|
||||
$('back-to-2').addEventListener('click', () => show(2));
|
||||
|
||||
// Theseus-wallet injected? Show the one-click sign path.
|
||||
if (window.bitcoincash && typeof window.bitcoincash.signMessage === 'function') {
|
||||
$('theseus-sign').style.display = 'block';
|
||||
}
|
||||
|
||||
$('theseus-sign-btn').addEventListener('click', async () => {
|
||||
const status = $('sign-status');
|
||||
status.textContent = 'Asking Theseus wallet for a signature…';
|
||||
try {
|
||||
const sig = await window.bitcoincash.signMessage(W.message);
|
||||
// Different wallets return different shapes; normalise to base64 string.
|
||||
const base64 = typeof sig === 'string' ? sig : sig?.signature || sig?.sig || '';
|
||||
if (!base64) throw new Error('wallet returned no signature');
|
||||
W.signature = base64;
|
||||
$('sig-paste').value = base64;
|
||||
status.innerHTML = '✓ Signed. <b>' + base64.length + ' chars.</b> Continue to publish.';
|
||||
$('to-step4').disabled = false;
|
||||
} catch (err) {
|
||||
status.innerHTML = '⚠️ Theseus-wallet sign failed — ' + String(err?.message || err) +
|
||||
'. Paste a signature from another wallet below.';
|
||||
}
|
||||
});
|
||||
|
||||
$('sig-paste').addEventListener('input', (ev) => {
|
||||
const v = ev.target.value.trim();
|
||||
if (v.length === 88 || v.length === 86 || v.length === 87) {
|
||||
W.signature = v;
|
||||
$('sign-status').innerHTML = '✓ ' + v.length + '-char signature captured. Continue to publish.';
|
||||
$('to-step4').disabled = false;
|
||||
} else {
|
||||
W.signature = null;
|
||||
$('to-step4').disabled = true;
|
||||
$('sign-status').textContent = v ? \`Need 86–88 chars; got \${v.length}.\` : '';
|
||||
}
|
||||
});
|
||||
|
||||
$('to-step4').addEventListener('click', () => show(4));
|
||||
|
||||
// ===== STEP 4: publish =====
|
||||
$('back-to-3').addEventListener('click', () => show(3));
|
||||
$('publish-btn').addEventListener('click', async () => {
|
||||
const status = $('publish-status');
|
||||
const result = $('publish-result');
|
||||
status.textContent = 'Publishing…';
|
||||
result.innerHTML = '';
|
||||
$('publish-btn').disabled = true;
|
||||
|
||||
const body = { ...W.manifest, signature: W.signature };
|
||||
let json;
|
||||
try {
|
||||
const r = await fetch('/api/listing', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
json = await r.json();
|
||||
if (!r.ok || !json.ok) {
|
||||
status.innerHTML = '⚠️ <b>' + (r.status) + '</b> — ' + (json.reason || json.error || 'unknown');
|
||||
$('publish-btn').disabled = false;
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
status.innerHTML = '⚠️ Network error — ' + String(err?.message || err);
|
||||
$('publish-btn').disabled = false;
|
||||
return;
|
||||
}
|
||||
status.innerHTML = '✅ <b>Published.</b>';
|
||||
const href = '/app/' + encodeURIComponent(json.listing.publisher) + '/' + encodeURIComponent(json.listing.slug);
|
||||
result.innerHTML =
|
||||
'<div class="card" style="padding:22px 26px">' +
|
||||
'<h3 style="margin:0 0 .3rem">' + json.listing.name + '</h3>' +
|
||||
'<p style="margin:0;color:var(--mut);font-size:13.5px">Signed by ' + json.listing.publisher +
|
||||
' · ' + json.listing.category + ' · v' + json.listing.version + '</p>' +
|
||||
'<p style="margin:1rem 0 0"><a class="btn acid" href="' + href + '">View your listing →</a> ' +
|
||||
'<a class="btn ghost" href="/browse">Back to browse</a></p>' +
|
||||
'</div>';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue