// Sirius Studio — a site builder for BCNR names. // // GrapesJS (BSD-3, vendored under ./vendor/grapesjs/) edits the page. On // Publish the exported HTML+CSS becomes index.html in the name's own folder // on Sia — bns// — uploaded through the gateway's /api/site route, // every request signed with the wallet key that holds the name's NFT. The // editor's project data is saved next to it (_studio.json) so the site can // be reopened and edited later. If the name's on-chain s3 record does not // point at that folder yet, Publish also sends one UPD that sets it. // // The gateway never holds a key: it verifies each upload's signature // against the current NFT owner and refuses writes outside bns//. import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd"; import { initAssistant } from "./studio-ai.js?v=20260920ai"; const API = "https://silentmode.st"; const $ = (id) => document.getElementById(id); const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); const name = (new URLSearchParams(location.search).get("name") || "").toLowerCase().trim(); const folder = name ? `bns/${name}/` : null; const readUrl = (path) => `${API}/api/site/${encodeURIComponent(name)}/${path}`; const siteUrl = () => `${API}/bns/${encodeURIComponent(name)}/`; let wallet = null; let entry = null; // { records, category, ... } from /api/name let editor = null; let dirty = false; function status(text, cls = "") { const el = $("status"); el.textContent = text; el.className = "st " + cls; } // ---------- session ---------- async function restoreWallet() { if (window.siriusWallet) return window.siriusWallet; const S = window.siriusSession; if (!S?.restore) return null; try { const s = await S.restore(); if (!s?.mnemonic) return null; return await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.CHIPNET_PREFIX, s.accountPath || undefined); } catch { return null; } } // ---------- signed uploads ---------- const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join(""); async function signedHeaders(path, bodyBytes) { const ts = String(Date.now()); const bodyHash = hex(await crypto.subtle.digest("SHA-256", bodyBytes)); const msg = `BNS-SITE1\n${name}\n${path}\n${bodyHash}\n${ts}`; const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg))); const sig = wallet.signMessage(digest); return { "x-bns-ts": ts, "x-bns-sig": btoa(String.fromCharCode(...sig)) }; } async function putFile(path, body, contentType) { const bytes = body instanceof Uint8Array ? body : new Uint8Array(await new Blob([body]).arrayBuffer()); const headers = { "content-type": contentType, ...(await signedHeaders(path, bytes)) }; const r = await fetch(readUrl(path), { method: "PUT", headers, body: bytes }); const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(`${path}: ${j.error || r.status}`); return j; } async function listFiles(live = false) { const r = await fetch(`${API}/api/site/${encodeURIComponent(name)}${live ? "?src=live" : ""}`, { cache: "no-store" }); if (!r.ok) return { files: [], prefix: null, source: null }; const j = await r.json().catch(() => ({})); return { files: j.files || [], prefix: j.prefix ?? null, source: j.source ?? null }; } // ---------- import the page the name serves right now ---------- // Sites are hosted three ways: the Studio folder bns//, a CLI-published // bucket the `s3` record points at (bns/