191 lines
5.7 KiB
JavaScript
191 lines
5.7 KiB
JavaScript
|
|
// 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<name>\n<path>\n<sha256hex(body)>\n<unix_ms>")
|
||
|
|
// 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();
|
||
|
|
}
|
||
|
|
})();
|