// Manual-mode publishing: sign in the browser, upload straight to the gateway. // // The point of this file is that the server never holds the key. WordPress // builds the bytes — it is the only thing that can, since it knows how the // theme renders — and then hands them here. This page hashes them, signs the // gateway's upload envelope with a phrase typed a moment ago, PUTs the file, // and tells WordPress what happened. The phrase is wiped when the run ends. // // The envelope is BNS-SITE1: // sha256("BNS-SITE1\n\n\n\n") // signed as a 65-byte recoverable signature, base64, in x-bns-sig, with the // same timestamp in x-bns-ts. The gateway recovers the signer and checks it // against whoever owns the name on-chain right now. (() => { "use strict"; const CFG = window.SIRIUS_EXPORT || {}; const els = {}; let stopping = false; let running = false; const hex = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join(""); const enc = new TextEncoder(); function log(text, kind = "") { const li = document.createElement("li"); li.textContent = text; if (kind === "error") li.style.color = "#b32d2e"; if (kind === "ok") li.style.color = "#007017"; els.log.prepend(li); } function status(text) { els.status.textContent = text; } const base64ToBytes = (b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); async function api(path, options = {}) { const response = await fetch(CFG.restUrl + path, { credentials: "same-origin", ...options, headers: { "content-type": "application/json", "x-wp-nonce": CFG.nonce, ...(options.headers || {}), }, }); const json = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(json.message || `WordPress returned HTTP ${response.status}`); } return json; } /** Sign and PUT one file. Returns nothing; throws on failure. */ async function upload(wallet, item) { const body = base64ToBytes(item.body_b64); const ts = Date.now(); const envelope = `BNS-SITE1\n${CFG.name}\n${item.path}\n${item.sha256}\n${ts}`; const digest = await wallet.sha256(enc.encode(envelope)); const signature = await wallet.signRaw(digest); const url = CFG.gateway.replace(/\/$/, "") + "/api/site/" + encodeURIComponent(CFG.name) + "/" + item.path.split("/").map(encodeURIComponent).join("/"); const response = await fetch(url, { method: "PUT", headers: { "content-type": item.mime, "x-bns-sig": signature, "x-bns-ts": String(ts), }, body, }); const json = await response.json().catch(() => ({})); if (!response.ok || !json.ok) { throw new Error(json.error || `gateway returned HTTP ${response.status}`); } } async function run() { if (running) return; const phrase = els.phrase.value; const check = window.SiriusWallet.validatePhrase(phrase); if (!check.ok) { status(check.error); return; } let wallet; try { wallet = await window.SiriusWallet.fromPhrase(phrase, { prefix: CFG.prefix, path: CFG.path, }); } catch (err) { status(err.message || String(err)); return; } // Out of the DOM as soon as it has been used. The wallet object keeps the // derived key for the run and is wiped in the finally below. els.phrase.value = ""; running = true; stopping = false; els.stop.hidden = false; els.run.disabled = true; log(`signing as ${wallet.address}`); let published = 0; let failed = 0; try { for (;;) { if (stopping) { status("Stopped."); break; } status("Building the next batch…"); const batch = await api("/next?limit=3"); if (!batch.items.length) { status( published || failed ? `Finished — ${published} published, ${failed} failed.` : "Nothing queued.", ); break; } for (const item of batch.items) { if (stopping) break; status(`Publishing ${item.path} — ${batch.left} left`); try { await upload(wallet, item); await api("/ack", { method: "POST", body: JSON.stringify({ id: item.id, sha256: item.sha256 }), }); published++; log(`✓ ${item.path}`, "ok"); } catch (err) { failed++; const message = err.message || String(err); log(`✗ ${item.path} — ${message}`, "error"); await api("/ack", { method: "POST", body: JSON.stringify({ id: item.id, error: message }), }).catch(() => {}); } } } } catch (err) { status(err.message || String(err)); } finally { if (wallet.forget) wallet.forget(); running = false; els.stop.hidden = true; els.run.disabled = false; } } function start() { els.phrase = document.getElementById("sirius_export_phrase"); els.run = document.getElementById("sirius_export_run"); els.stop = document.getElementById("sirius_export_stop"); els.status = document.getElementById("sirius_export_status"); els.log = document.getElementById("sirius_export_log"); if (!els.run || !window.SiriusWallet) return; els.run.addEventListener("click", run); els.stop.addEventListener("click", () => { stopping = true; status("Stopping after this file…"); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", start); } else { start(); } })();