sirius/tld.html
Local Dev 109e9e7351 feat(sirius-x): admin TLD hide/unhide + tabbed sign-in modal (4 direct actions)
Two features:

1. Operator can hide TLDs from the public /api/tlds listing so hidden
   TLDs stop appearing in name-search UIs. On-chain registrations
   under a hidden TLD keep resolving — this is a UX filter, not
   enforcement.

   Gateway (public-gateway.mjs):
   - Persistent HIDDEN_TLDS set backed by hidden-tlds.json next to
     the service script
   - GET /api/tld-visibility  -> {hidden:[...]}                (public)
   - POST /api/tld-visibility -> updates the list (operator-gated by
     Bearer BNS_OPERATOR_TOKEN env var; if unset, all writes refused
     so we default-deny)
   - /api/tlds filters out HIDDEN_TLDS; add ?include_hidden=1 to see
     everything (used by the admin panel to show all rows)
   - Operator token installed via systemd override on the VPS

   Admin panel:
   - New 'Operator token' card at the top of the TLD-registry section;
     token stored in sessionStorage (not localStorage) so a full
     browser close forgets it
   - Each TLD row got a 'Hidden from public' checkbox that POSTs on
     toggle and refreshes the table; failures roll back the checkbox
     and surface the error next to the token field

2. Wallet dropdown restored to 4 direct actions
   (Unlock / Create a wallet / Import a wallet / WizardConnect) and
   the shared mint/sign-in modal grew a tab strip so users can switch
   between the four wallet actions from any step without going back
   to a choice screen.

   register-flow.js:
   - renderTabs(active) prepended to stepCreate/stepImport/stepUnlock/
     stepExternal when signInOnly is set. Unlock tab only appears
     when a saved wallet exists.
   - Delegated click handler on the sheet routes tab clicks to the
     matching step; switching away from a live WC session tears it
     down first so we don't leak WebSockets.

   profile-menu.js:
   - Restored 4-item onboarding menu (Create/Import/WC plus Unlock
     when saved). Each item is a direct entry point; the tabbed modal
     lets the user pivot to any other option without closing.

Cache-buster bumped on all 8 sirius-x pages to ?v=20260908tabs.
2026-09-08 02:19:41 +02:00

256 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Register a TLD — Sirius.X</title>
<meta name="description" content="Mint a top-level domain on Bitcoin Cash. Any wallet can register a TLD; second-level names can then be sold under it.">
<link rel="icon" href="./assets/favicon.svg" type="image/svg+xml">
<link rel="mask-icon" href="./assets/favicon.svg" color="#d6ff3d">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Sirius.X">
<meta property="og:title" content="Register a TLD on Bitcoin Cash">
<meta property="og:description" content="Mint a TLD certificate. One BCH transaction, no renewal, no registrar.">
<meta property="og:url" content="https://silentmode.st/sirius-x/tld.html">
<meta property="og:image" content="https://silentmode.st/sirius-x/brand/banner.svg">
<style>
:root{
--bg:#0b0e14; --panel:#141a24; --panel2:#18202c; --line:rgba(255,255,255,.09);
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
--ok:#4fd1a5; --taken:#f6768a; --warn:#ffc75f;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:radial-gradient(1100px 560px at 50% -12%,#16202e,var(--bg));
color:var(--ink);font:16px/1.65 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh}
a{color:var(--acid)}
.wrap{max-width:820px;margin:0 auto;padding:0 1.2rem}
.topnav{position:sticky;top:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap;
padding:10px 1.2rem;background:rgba(11,14,20,.82);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.topnav .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:14px;font-size:15px;letter-spacing:.2px}
.topnav .brand .g{color:var(--acid)}
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px}
.topnav a:hover{color:var(--ink);background:var(--panel)}
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)}
.topnav a.portal{margin-left:auto;background:rgba(214,255,61,.10);color:var(--acid);font-weight:500}
.topnav a.portal:hover{background:rgba(214,255,61,.18)}
header.hero{text-align:center;padding:3.4rem 1.2rem 1rem}
.mark{font-size:44px;line-height:1}
h1{font-size:clamp(1.7rem,4.5vw,2.3rem);margin:.3rem 0 .3rem}
h1 .g{color:var(--acid)}
.tag{color:var(--mut);font-size:1rem;max-width:600px;margin:0 auto}
h2{font-size:1.25rem;margin:0 0 .4rem}
section{padding:2rem 0;border-top:1px solid var(--line);margin-top:2rem}
section:first-of-type{border-top:none;margin-top:1rem}
.btn{display:inline-block;text-decoration:none;padding:10px 18px;border-radius:10px;background:#4b7bec;color:#fff;font-size:14px;border:none;cursor:pointer;font-family:inherit}
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
.btn.acid{background:var(--acid);color:#0b0e14;font-weight:600}
.btn:hover{filter:brightness(1.12)}
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
.muted{color:var(--mut);font-size:13px}
.dim{color:var(--dim);font-size:12.5px}
code,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px;word-break:break-all}
.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:999px;font-weight:600}
.b-ok{background:rgba(79,209,165,.15);color:var(--ok)}
.b-taken{background:rgba(246,118,138,.15);color:var(--taken)}
.b-check{background:rgba(139,152,169,.14);color:var(--mut)}
.status{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 14px;font-size:13px;color:var(--mut);margin-top:12px}
.status.ok{border-left:3px solid var(--ok)}
.status.err{border-left:3px solid var(--taken);color:var(--taken)}
.status.warn{border-left:3px solid var(--warn);color:var(--warn)}
/* TLD search cell */
.search{max-width:600px;margin:1.5rem auto 0}
.searchrow{display:flex;gap:8px}
.searchrow input{flex:1;padding:14px 18px;font-size:17px;border-radius:12px;border:1px solid #ffffff22;background:#141a24;color:var(--ink);font-family:inherit;outline:none}
.searchrow input:focus{border-color:#4b7bec}
.searchhint{font-size:12.5px;color:var(--mut);margin-top:8px;text-align:center}
.result-card{display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;
background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 18px;margin-top:12px}
.result-card .lbl{font-family:ui-monospace,monospace;font-size:18px;color:var(--ink)}
.result-card .lbl .dot{color:var(--acid)}
/* Registered-TLDs table */
table.tlds{width:100%;border-collapse:collapse;margin-top:1rem;font-size:13px}
table.tlds th,table.tlds td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
table.tlds th{color:var(--mut);font-weight:500;font-size:11.5px;text-transform:uppercase;letter-spacing:.4px}
table.tlds .tld{font-family:ui-monospace,monospace;color:var(--acid);font-size:13.5px;font-weight:600}
table.tlds .cat{font-family:ui-monospace,monospace;color:var(--mut);font-size:11.5px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
</style>
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script>
</head>
<body>
<nav class="topnav">
<a class="brand" href="./">⭐ <span class="g">Sirius.X</span></a>
<a href="./#try">Try it</a>
<a href="./#search-input">✏️ Register</a>
<a href="./tld.html" class="here">🌐 TLD</a>
<a href="./theseus/"><svg width="14" height="14" viewBox="0 0 64 64" style="vertical-align:-2px;margin-right:4px" aria-hidden="true"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/><path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/><circle cx="32" cy="32" r="3" fill="#0b0e14"/></svg>Theseus</a>
<a href="./portal.html" class="portal">🔑 Sign in</a>
</nav>
<header class="hero">
<div class="mark">🌐</div>
<h1>Register a <span class="g">TLD</span></h1>
<p class="tag">A TLD is a certificate on the Bitcoin Cash chain. You mint it, you own it — any
second-level name registered under it lives in your TLD's registry. One BCH transaction,
no renewal, no ICANN.</p>
</header>
<div class="wrap">
<section id="search-section">
<h2>Search a TLD</h2>
<p class="muted">Type a label (a–z, 0–9, hyphen; up to 16 chars). Available TLDs get a Register
button that opens the shared wallet-choice modal — the same flow name registration uses.</p>
<div class="search">
<div class="searchrow">
<input id="q" placeholder="your-tld" spellcheck="false" autocomplete="off" autofocus maxlength="16">
</div>
<div class="searchhint">Register minting to your token address; you become the registry operator.</div>
</div>
<div id="result"></div>
</section>
<section id="listing">
<h2>Registered TLDs</h2>
<p class="muted">Fetched from the on-chain TLD registry via the public gateway. Refresh the page for the latest.</p>
<div id="tlds-status" class="status">Loading…</div>
<table class="tlds" id="tlds-table" style="display:none">
<thead><tr><th>TLD</th><th>Category</th></tr></thead>
<tbody></tbody>
</table>
</section>
</div>
<footer id="site-footer"></footer>
<script>
// Simple oracle stub: static chipnet rate for testing. Same number the
// portal uses. Mainnet swaps for a real price feed.
const CHIPNET_SATS_PER_USD = 250_000;
const DEFAULT_USD = 5;
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
const validLabel = (s) => /^[a-z0-9-]{1,16}$/.test(s);
const normLabel = (s) => String(s).toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 16);
// ---------- registered-TLD list ----------
let registeredTlds = null;
async function loadTldList() {
const status = $("tlds-status"), tbody = $("tlds-table").querySelector("tbody");
try {
const r = await fetch("https://navigate.st/api/tlds");
if (!r.ok) throw new Error("api " + r.status);
const j = await r.json();
const tlds = (j.tlds || []).map((t) => (typeof t === "string" ? { tld: t } : t));
tlds.sort((a, b) => a.tld.localeCompare(b.tld));
tbody.innerHTML = tlds.map((t) =>
`<tr><td class="tld">.${esc(t.tld)}</td><td class="cat">${esc(t.certificate || t.category || "")}</td></tr>`
).join("");
$("tlds-table").style.display = "";
status.className = "status ok";
status.textContent = `${tlds.length} TLDs registered on chipnet.`;
registeredTlds = tlds.map((t) => t.tld);
refreshSearch();
} catch (e) {
status.className = "status err";
status.textContent = "Could not load TLD list: " + (e.message || e);
}
}
loadTldList();
// ---------- search result renderer ----------
function renderResult(label, state) {
const res = $("result");
if (!label) { res.innerHTML = ""; return; }
if (state === "checking") {
res.innerHTML = `<div class="result-card">
<div class="lbl"><span class="dot">.</span>${esc(label)}</div>
<span class="badge b-check">checking…</span><span></span></div>`;
return;
}
if (state === "taken") {
res.innerHTML = `<div class="result-card">
<div class="lbl"><span class="dot">.</span>${esc(label)}</div>
<span class="badge b-taken">taken</span><span></span></div>
<p class="dim" style="margin-top:8px">Someone already minted <code>.${esc(label)}</code>. Try a
different label — TLDs are first-come, first-served.</p>`;
return;
}
res.innerHTML = `<div class="result-card">
<div class="lbl"><span class="dot">.</span>${esc(label)}</div>
<span class="badge b-ok">available</span>
<button class="btn acid" id="go-register">Register .${esc(label)} →</button>
</div>`;
$("go-register").onclick = () => beginRegister(label);
}
// ---------- search wiring ----------
let currentLabel = "";
function refreshSearch() {
const raw = normLabel($("q").value);
currentLabel = raw;
if (!raw) { renderResult("", null); return; }
if (!validLabel(raw)) { renderResult(raw, "invalid"); return; }
if (registeredTlds !== null) {
renderResult(raw, registeredTlds.includes(raw) ? "taken" : "ok");
return;
}
renderResult(raw, "checking");
}
$("q").addEventListener("input", () => {
$("q").value = normLabel($("q").value);
refreshSearch();
});
$("q").addEventListener("keydown", (e) => {
if (e.key === "Enter" && currentLabel && registeredTlds && !registeredTlds.includes(currentLabel)) {
beginRegister(currentLabel);
}
});
// ---------- mint delegation ----------
// Register button hands the label off to the shared modal (register-flow.js
// — window.siriusRegisterTld). That modal owns the wallet-choice, fund,
// confirm, and mint steps — same UX as name registration.
function beginRegister(label) {
const call = () => window.siriusRegisterTld(label, {
serviceFeeSats: BigInt(DEFAULT_USD * CHIPNET_SATS_PER_USD),
});
if (typeof window.siriusRegisterTld === "function") { call(); return; }
// Module still loading — retry until it lands.
const btn = $("go-register");
const orig = btn?.textContent;
if (btn) { btn.disabled = true; btn.textContent = "loading…"; }
const t = setInterval(() => {
if (typeof window.siriusRegisterTld === "function") {
clearInterval(t);
if (btn) { btn.disabled = false; btn.textContent = orig; }
call();
}
}, 100);
}
// ---------- ?q= deep-link (from landing links etc.) ----------
(() => {
const q = normLabel(new URLSearchParams(location.search).get("q") || "");
if (!q) return;
$("q").value = q;
const wait = setInterval(() => {
if (registeredTlds === null) return;
clearInterval(wait);
refreshSearch();
setTimeout(() => $("q").scrollIntoView({ behavior: "smooth", block: "start" }), 100);
}, 150);
})();
</script>
<script type="module" src="./js/register-flow.js?v=20260908tabs"></script>
<script defer src="./js/site-footer.js?v=20260907rel"></script>
<script defer src="./js/profile-menu.js?v=20260908tabs"></script>
</body>
</html>