Initial backup of root workspace files
Glue files not covered by subproject repos: top-level docs, logo, keysat-design-system, and crosscheck tests. Subproject folders are gitignored (each has its own Gitea remote).
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# Keysat — Dashboard UI Kit
|
||||
|
||||
The creator's admin panel. Runs on the creator's own Start9. Manages products, policies, license keys, customers, discount codes, and the audit log.
|
||||
|
||||
## Files
|
||||
|
||||
- `index.html` — entry view (Overview / dashboard home).
|
||||
- `licenses.html` — license list, with row-level actions.
|
||||
- `license-detail.html` — single license: certificate-style header, customer info, audit timeline, revoke action.
|
||||
- `new-product.html` — create-product flow (product details + policy + price).
|
||||
- `signin.html` — admin sign-in (paste admin API key).
|
||||
|
||||
## Components (inline in each page)
|
||||
|
||||
- **Sidebar** — wordmark, primary nav (Overview, Products, Licenses, Customers, Discounts, Audit, Settings), BTCPay connection status footer.
|
||||
- **Topbar** — page title, breadcrumb, primary action button, search.
|
||||
- **Stat cards** — KPI tiles (active licenses, sales 30d, sats earned).
|
||||
- **Table** — license list, customer list. 52px rows, mono key column, status badge column.
|
||||
- **Drawer / detail header** — certificate motif borrowed from the marketing hero.
|
||||
- **Empty state** — gold-bordered cream card with a single Lucide icon.
|
||||
|
||||
## Iconography
|
||||
|
||||
Lucide via CDN. Stroke 1.75px, 18px in nav, 16px inline.
|
||||
|
||||
## Disclaimers
|
||||
|
||||
- The "Settings" tab from the user's brief was scoped down to **operator settings** (operator name, public key, BTCPay connection). Payouts removed because BTCPay handles money.
|
||||
- All data is fake.
|
||||
@@ -0,0 +1,206 @@
|
||||
/* Shared dashboard chrome — links to ../../colors_and_type.css for tokens */
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
color: var(--ink-900);
|
||||
background: var(--cream-100);
|
||||
background-image:
|
||||
radial-gradient(rgba(14,31,51,0.022) 1px, transparent 1px),
|
||||
radial-gradient(rgba(138,111,61,0.020) 1px, transparent 1px);
|
||||
background-size: 3px 3px, 7px 7px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
a { color: var(--navy-800); text-decoration: none; }
|
||||
|
||||
/* ---------- Layout ---------- */
|
||||
.app { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
|
||||
|
||||
/* ---------- Sidebar ---------- */
|
||||
.sidebar {
|
||||
background: var(--navy-950);
|
||||
color: var(--cream-200);
|
||||
padding: 24px 14px;
|
||||
display: flex; flex-direction: column;
|
||||
border-right: 1px solid var(--navy-900);
|
||||
}
|
||||
.sidebar .brand {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-family: var(--font-display); font-weight: 500; font-size: 14px; letter-spacing: 0.28em; text-transform: uppercase;
|
||||
color: var(--cream-50);
|
||||
padding: 0 8px 22px;
|
||||
border-bottom: 1px solid rgba(245,241,232,0.10);
|
||||
margin-bottom: 14px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.sidebar .brand img { width: 26px; height: 26px; }
|
||||
.sidebar .group-label {
|
||||
font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase;
|
||||
color: var(--gold-400); padding: 16px 10px 8px;
|
||||
}
|
||||
.sidebar a.nav {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 9px 10px; border-radius: 6px;
|
||||
font-size: 13.5px; color: rgba(245,241,232,0.72);
|
||||
transition: all 120ms;
|
||||
}
|
||||
.sidebar a.nav:hover { background: rgba(245,241,232,0.06); color: var(--cream-50); }
|
||||
.sidebar a.nav.active { background: var(--navy-800); color: var(--cream-50); }
|
||||
.sidebar a.nav [data-lucide] { width: 16px; height: 16px; }
|
||||
.sidebar a.nav .count {
|
||||
margin-left: auto; font-family: var(--font-mono); font-size: 11px;
|
||||
background: rgba(245,241,232,0.10); color: var(--cream-200);
|
||||
padding: 1px 7px; border-radius: 999px;
|
||||
}
|
||||
.sidebar a.nav.active .count { background: var(--gold-500); color: var(--navy-950); }
|
||||
.sidebar .footer {
|
||||
margin-top: auto; padding: 14px 10px; border-top: 1px solid rgba(245,241,232,0.10);
|
||||
font-size: 12px; color: rgba(245,241,232,0.55); display: flex; gap: 10px; align-items: center;
|
||||
}
|
||||
.sidebar .footer .dot { width: 7px; height: 7px; border-radius: 50%; background: #2D7A5F; box-shadow: 0 0 0 3px rgba(45,122,95,0.25); }
|
||||
|
||||
/* ---------- Main ---------- */
|
||||
.main { display: flex; flex-direction: column; min-width: 0; }
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
padding: 18px 32px; border-bottom: 1px solid var(--border-1);
|
||||
background: rgba(251,249,242,0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
position: sticky; top: 0; z-index: 5;
|
||||
}
|
||||
.topbar .crumb { font-size: 12.5px; color: var(--ink-500); }
|
||||
.topbar h1 {
|
||||
font-family: var(--font-display); font-weight: 700; font-size: 22px;
|
||||
letter-spacing: -0.015em; margin: 2px 0 0; color: var(--navy-950);
|
||||
}
|
||||
.topbar .search {
|
||||
margin-left: auto;
|
||||
position: relative; width: 280px;
|
||||
}
|
||||
.topbar .search input {
|
||||
width: 100%; padding: 8px 12px 8px 32px;
|
||||
font-family: var(--font-body); font-size: 13px;
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: 8px; background: var(--cream-50);
|
||||
color: var(--ink-900);
|
||||
}
|
||||
.topbar .search input:focus { outline: none; border-color: var(--navy-700); box-shadow: 0 0 0 3px rgba(30,58,95,0.15); }
|
||||
.topbar .search [data-lucide] { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: var(--ink-400); }
|
||||
.topbar .topbar-actions { display: flex; gap: 8px; align-items: center; }
|
||||
|
||||
.content { padding: 28px 32px 64px; max-width: 1280px; }
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
font-family: var(--font-body); font-weight: 600; font-size: 13px;
|
||||
padding: 8px 14px; border-radius: 7px; border: 1px solid transparent;
|
||||
cursor: pointer; transition: all 120ms; line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn [data-lucide] { width: 14px; height: 14px; }
|
||||
.btn.lg { font-size: 14px; padding: 11px 18px; }
|
||||
.btn.sm { font-size: 12px; padding: 6px 10px; }
|
||||
.btn.primary { background: var(--navy-800); color: var(--cream-50); border-color: var(--navy-800); }
|
||||
.btn.primary:hover { background: var(--navy-900); border-color: var(--navy-900); }
|
||||
.btn.secondary { background: var(--cream-50); color: var(--navy-900); border-color: var(--border-2); }
|
||||
.btn.secondary:hover { background: var(--cream-200); }
|
||||
.btn.ghost { background: transparent; color: var(--navy-900); }
|
||||
.btn.ghost:hover { background: rgba(14,31,51,0.06); }
|
||||
.btn.danger { color: var(--danger); border-color: rgba(178,58,58,0.3); background: transparent; }
|
||||
.btn.danger:hover { background: var(--danger-bg); }
|
||||
|
||||
/* ---------- Cards ---------- */
|
||||
.card {
|
||||
background: var(--cream-50);
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
.card .card-head {
|
||||
padding: 14px 18px; border-bottom: 1px solid var(--border-1);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.card .card-head h3 {
|
||||
font-family: var(--font-display); font-weight: 700; font-size: 15px;
|
||||
margin: 0; letter-spacing: -0.01em; color: var(--navy-950);
|
||||
}
|
||||
|
||||
/* ---------- Stats ---------- */
|
||||
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 24px; }
|
||||
.stat {
|
||||
background: var(--cream-50); border: 1px solid var(--border-1);
|
||||
border-radius: 10px; padding: 18px 18px 16px;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.stat::before {
|
||||
content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
|
||||
background: var(--gold-500); opacity: 0;
|
||||
}
|
||||
.stat.featured::before { opacity: 1; }
|
||||
.stat .label {
|
||||
font-size: 11px; font-weight: 700; letter-spacing: 0.14em;
|
||||
text-transform: uppercase; color: var(--ink-500); margin-bottom: 8px;
|
||||
}
|
||||
.stat .value {
|
||||
font-family: var(--font-display); font-weight: 500; font-size: 30px;
|
||||
color: var(--navy-950); letter-spacing: -0.022em; line-height: 1;
|
||||
}
|
||||
.stat .value .unit { font-family: var(--font-body); font-size: 13px; font-weight: 600; color: var(--ink-500); margin-left: 6px; }
|
||||
.stat .delta { font-size: 12px; color: var(--success); margin-top: 8px; font-weight: 600; }
|
||||
.stat .delta.down { color: var(--danger); }
|
||||
.stat .sub { font-size: 12px; color: var(--ink-500); margin-top: 6px; }
|
||||
|
||||
/* ---------- Table ---------- */
|
||||
table.t {
|
||||
width: 100%; border-collapse: separate; border-spacing: 0;
|
||||
background: var(--cream-50); border: 1px solid var(--border-1);
|
||||
border-radius: 10px; overflow: hidden;
|
||||
}
|
||||
table.t thead th {
|
||||
text-align: left; font-size: 11px; font-weight: 700;
|
||||
letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-500);
|
||||
padding: 12px 16px; background: var(--cream-100);
|
||||
border-bottom: 1px solid var(--border-1);
|
||||
}
|
||||
table.t tbody td {
|
||||
padding: 14px 16px; border-bottom: 1px solid var(--border-1);
|
||||
font-size: 13.5px; color: var(--ink-700); vertical-align: middle;
|
||||
}
|
||||
table.t tbody tr:last-child td { border-bottom: 0; }
|
||||
table.t tbody tr:hover { background: var(--cream-100); cursor: pointer; }
|
||||
table.t .key { font-family: var(--font-mono); font-size: 12.5px; color: var(--navy-900); font-weight: 600; }
|
||||
table.t .product { font-weight: 600; color: var(--navy-950); }
|
||||
table.t .meta { color: var(--ink-500); font-size: 12px; }
|
||||
|
||||
/* ---------- Badges ---------- */
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600;
|
||||
padding: 2px 9px; border-radius: 999px; line-height: 1.5; border: 1px solid transparent;
|
||||
}
|
||||
.b-success { background: var(--success-bg); color: #205c47; border-color: rgba(45,122,95,0.25); }
|
||||
.b-warning { background: var(--warning-bg); color: #7a5814; border-color: rgba(184,134,31,0.3); }
|
||||
.b-danger { background: var(--danger-bg); color: #8a2828; border-color: rgba(178,58,58,0.25); }
|
||||
.b-info { background: var(--navy-100); color: var(--navy-800); border-color: rgba(30,58,95,0.20); }
|
||||
.b-neutral { background: var(--cream-200); color: var(--ink-700); border-color: var(--border-1); }
|
||||
.b-gold { background: transparent; color: var(--gold-700); border-color: var(--gold-500); }
|
||||
.dot { width: 6px; height: 6px; border-radius: 50%; }
|
||||
|
||||
/* ---------- Forms ---------- */
|
||||
.field { margin-bottom: 14px; }
|
||||
.field label.lbl { display: block; font-size: 12.5px; font-weight: 600; color: var(--ink-700); margin-bottom: 6px; }
|
||||
.field .hint { font-size: 12px; color: var(--ink-500); margin-top: 5px; }
|
||||
.input, .select {
|
||||
width: 100%; padding: 9px 12px; font-family: var(--font-body); font-size: 13.5px;
|
||||
border: 1px solid var(--border-2); border-radius: 7px; background: #FFFFFF;
|
||||
color: var(--ink-900); transition: all 120ms;
|
||||
}
|
||||
.input:focus, .select:focus { outline: none; border-color: var(--navy-700); box-shadow: 0 0 0 3px rgba(30,58,95,0.18); }
|
||||
.input.mono { font-family: var(--font-mono); font-size: 13px; }
|
||||
|
||||
.eyebrow {
|
||||
font-size: 10.5px; font-weight: 700; letter-spacing: 0.18em;
|
||||
text-transform: uppercase; color: var(--gold-700);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Keysat — Overview</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="dash.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="brand"><img src="../../assets/keysat-mark.svg" alt=""><span>Keysat</span></div>
|
||||
<a class="nav active" href="index.html"><i data-lucide="layout-dashboard"></i>Overview</a>
|
||||
<a class="nav" href="#"><i data-lucide="package"></i>Products<span class="count">3</span></a>
|
||||
<a class="nav" href="licenses.html"><i data-lucide="key-round"></i>Licenses<span class="count">42</span></a>
|
||||
<a class="nav" href="#"><i data-lucide="users"></i>Customers<span class="count">38</span></a>
|
||||
<a class="nav" href="#"><i data-lucide="tag"></i>Discount codes</a>
|
||||
<div class="group-label">System</div>
|
||||
<a class="nav" href="#"><i data-lucide="scroll-text"></i>Audit log</a>
|
||||
<a class="nav" href="#"><i data-lucide="webhook"></i>Webhooks</a>
|
||||
<a class="nav" href="#"><i data-lucide="settings-2"></i>Settings</a>
|
||||
<div class="footer"><span class="dot"></span><div><div style="color: var(--cream-50); font-weight: 600">BTCPay connected</div><div>store: aurora-software</div></div></div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="crumb">Workspace · aurora-software</div>
|
||||
<h1>Overview</h1>
|
||||
</div>
|
||||
<div class="search"><i data-lucide="search"></i><input placeholder="Search licenses, customers, products"></div>
|
||||
<div class="topbar-actions">
|
||||
<button class="btn secondary"><i data-lucide="download"></i>Export</button>
|
||||
<button class="btn primary"><i data-lucide="plus"></i>New product</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="stats">
|
||||
<div class="stat featured">
|
||||
<div class="label">Active licenses</div>
|
||||
<div class="value">42</div>
|
||||
<div class="delta">+5 this month</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Sales · 30 days</div>
|
||||
<div class="value">12 <span class="unit">sales</span></div>
|
||||
<div class="delta">+33% vs prev</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Sats earned · 30d</div>
|
||||
<div class="value">412,500</div>
|
||||
<div class="sub">≈ $247.32</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Conversion</div>
|
||||
<div class="value">8.4 <span class="unit">%</span></div>
|
||||
<div class="delta down">−1.2% vs prev</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1.6fr 1fr; gap: 18px;">
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Recent licenses</h3><a href="licenses.html" class="btn ghost sm">View all <i data-lucide="arrow-right"></i></a></div>
|
||||
<table class="t" style="border:0; border-radius: 0;">
|
||||
<thead><tr><th>Key</th><th>Product</th><th>Customer</th><th>Status</th><th>Issued</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="key">KS-9F2A-7C41-XK22-6D8E</td><td class="product">Sundial 2.0</td><td>nina@dial.studio</td><td><span class="badge b-success"><span class="dot" style="background:#2D7A5F"></span>Active</span></td><td class="meta">2 hours ago</td></tr>
|
||||
<tr><td class="key">KS-A14C-PT09-LM31-R7Q4</td><td class="product">Sundial Pro</td><td>m@labry.dev</td><td><span class="badge b-success"><span class="dot" style="background:#2D7A5F"></span>Active</span></td><td class="meta">Yesterday</td></tr>
|
||||
<tr><td class="key">KS-T2X8-6K43-QQ91-WE0M</td><td class="product">Sundial 2.0</td><td>jo@kestrel.fm</td><td><span class="badge b-warning"><span class="dot" style="background:#B8861F"></span>Trial</span></td><td class="meta">2d ago</td></tr>
|
||||
<tr><td class="key">KS-BX9D-MM21-NU45-7F3R</td><td class="product">Sundial 2.0</td><td>ari@northpath.io</td><td><span class="badge b-success"><span class="dot" style="background:#2D7A5F"></span>Active</span></td><td class="meta">3d ago</td></tr>
|
||||
<tr><td class="key">KS-PW45-VR82-XA61-9K0L</td><td class="product">Sundial Pro</td><td>tom@workhorse.app</td><td><span class="badge b-danger"><span class="dot" style="background:#B23A3A"></span>Revoked</span></td><td class="meta">5d ago</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 14px;">
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Top products</h3></div>
|
||||
<div style="padding: 12px 16px;">
|
||||
<div style="display: flex; align-items: center; padding: 10px 0; gap: 12px; border-bottom: 1px solid var(--border-1);">
|
||||
<div style="flex: 1;"><div style="font-weight: 600; color: var(--navy-950)">Sundial 2.0</div><div style="font-size: 12px; color: var(--ink-500)">28 active · 50,000 sats</div></div>
|
||||
<div style="font-family: var(--font-mono); font-size: 12.5px; color: var(--navy-900); font-weight: 600">1.4M sats</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; padding: 10px 0; gap: 12px; border-bottom: 1px solid var(--border-1);">
|
||||
<div style="flex: 1;"><div style="font-weight: 600; color: var(--navy-950)">Sundial Pro</div><div style="font-size: 12px; color: var(--ink-500)">11 active · 200,000 sats</div></div>
|
||||
<div style="font-family: var(--font-mono); font-size: 12.5px; color: var(--navy-900); font-weight: 600">2.2M sats</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; padding: 10px 0; gap: 12px;">
|
||||
<div style="flex: 1;"><div style="font-weight: 600; color: var(--navy-950)">Aurora Plugin</div><div style="font-size: 12px; color: var(--ink-500)">3 active · 75,000 sats</div></div>
|
||||
<div style="font-family: var(--font-mono); font-size: 12.5px; color: var(--navy-900); font-weight: 600">225k sats</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="background: var(--cream-100); border-style: dashed;">
|
||||
<div style="padding: 18px;">
|
||||
<div class="eyebrow" style="margin-bottom: 6px;">Tip</div>
|
||||
<div style="font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--navy-950); margin-bottom: 4px; letter-spacing: -0.01em;">Embed your public key</div>
|
||||
<p style="font-size: 13px; color: var(--ink-700); margin: 0 0 12px; line-height: 1.5;">Paste this into your app's source. Verifies signatures offline.</p>
|
||||
<div style="background: var(--navy-950); color: var(--cream-50); padding: 10px 12px; border-radius: 7px; font-family: var(--font-mono); font-size: 12px; display: flex; gap: 10px; align-items: center; justify-content: space-between;">
|
||||
<span>mz7q8r4t1v…h3k2pXq9wL</span>
|
||||
<button class="btn sm" style="background: rgba(245,241,232,0.10); color: var(--cream-50); border: 0;">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Keysat — License KS-9F2A-7C41-XK22-6D8E</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="dash.css">
|
||||
<style>
|
||||
.detail-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 18px; align-items: start; }
|
||||
.cert-head { background: var(--cream-50); border: 1px solid var(--border-1); border-radius: 12px; box-shadow: 0 0 0 1px var(--gold-500) inset, var(--shadow-sm); padding: 28px; position: relative; }
|
||||
.cert-head::before, .cert-head::after { content: ''; position: absolute; left: 14px; right: 14px; height: 1px; background: var(--gold-500); opacity: 0.4; }
|
||||
.cert-head::before { top: 14px; } .cert-head::after { bottom: 14px; }
|
||||
.cert-stamp { font-size: 10px; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; color: var(--gold-700); margin-bottom: 14px; }
|
||||
.cert-key { font-family: var(--font-mono); font-size: 22px; font-weight: 600; color: var(--navy-950); letter-spacing: 0.02em; margin-bottom: 8px; }
|
||||
.cert-product { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--ink-700); margin-bottom: 18px; }
|
||||
.cert-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; padding-top: 16px; border-top: 1px dashed rgba(14,31,51,0.18); }
|
||||
.cert-grid .field { font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-500); margin-bottom: 4px; }
|
||||
.cert-grid .value { font-family: var(--font-mono); font-size: 13px; color: var(--navy-900); font-weight: 500; }
|
||||
.timeline { padding: 4px 16px; }
|
||||
.timeline .item { display: grid; grid-template-columns: 18px 1fr; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--border-1); }
|
||||
.timeline .item:last-child { border-bottom: 0; }
|
||||
.timeline .marker { width: 8px; height: 8px; border-radius: 50%; background: var(--navy-800); margin-top: 6px; }
|
||||
.timeline .marker.gold { background: var(--gold-500); }
|
||||
.timeline .marker.danger { background: var(--danger); }
|
||||
.timeline .head { font-size: 13px; font-weight: 600; color: var(--navy-950); }
|
||||
.timeline .meta { font-size: 12px; color: var(--ink-500); margin-top: 2px; font-family: var(--font-mono); }
|
||||
.timeline .body { font-size: 12.5px; color: var(--ink-700); margin-top: 4px; }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid var(--border-1); font-size: 13px; }
|
||||
.info-row:last-child { border-bottom: 0; }
|
||||
.info-row .k { color: var(--ink-500); }
|
||||
.info-row .v { color: var(--navy-900); font-weight: 500; }
|
||||
.actions-row { display: flex; gap: 8px; margin-top: 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="brand"><img src="../../assets/keysat-mark.svg" alt=""><span>Keysat</span></div>
|
||||
<a class="nav" href="index.html"><i data-lucide="layout-dashboard"></i>Overview</a>
|
||||
<a class="nav" href="#"><i data-lucide="package"></i>Products<span class="count">3</span></a>
|
||||
<a class="nav active" href="licenses.html"><i data-lucide="key-round"></i>Licenses<span class="count">42</span></a>
|
||||
<a class="nav" href="#"><i data-lucide="users"></i>Customers<span class="count">38</span></a>
|
||||
<a class="nav" href="#"><i data-lucide="tag"></i>Discount codes</a>
|
||||
<div class="group-label">System</div>
|
||||
<a class="nav" href="#"><i data-lucide="scroll-text"></i>Audit log</a>
|
||||
<a class="nav" href="#"><i data-lucide="webhook"></i>Webhooks</a>
|
||||
<a class="nav" href="#"><i data-lucide="settings-2"></i>Settings</a>
|
||||
<div class="footer"><span class="dot"></span><div><div style="color: var(--cream-50); font-weight: 600">BTCPay connected</div><div>store: aurora-software</div></div></div>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<header class="topbar">
|
||||
<div><div class="crumb"><a href="licenses.html">Licenses</a> · KS-9F2A-7C41-XK22-6D8E</div><h1>License detail</h1></div>
|
||||
<div class="topbar-actions">
|
||||
<button class="btn secondary"><i data-lucide="copy"></i>Copy key</button>
|
||||
<button class="btn secondary"><i data-lucide="mail"></i>Resend email</button>
|
||||
<button class="btn danger"><i data-lucide="x-circle"></i>Revoke</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content">
|
||||
<div class="cert-head">
|
||||
<div class="cert-stamp">— Certificate of License · Active —</div>
|
||||
<div class="cert-key">KS-9F2A-7C41-XK22-6D8E</div>
|
||||
<div class="cert-product">Sundial 2.0 · default policy</div>
|
||||
<div class="cert-grid">
|
||||
<div><div class="field">Issued</div><div class="value">2026-04-22</div></div>
|
||||
<div><div class="field">Expires</div><div class="value">2027-04-22</div></div>
|
||||
<div><div class="field">Seats</div><div class="value">1 of 1</div></div>
|
||||
<div><div class="field">Trial</div><div class="value">No</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid" style="margin-top: 18px;">
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Audit timeline</h3></div>
|
||||
<div class="timeline">
|
||||
<div class="item"><div class="marker gold"></div><div><div class="head">License issued</div><div class="meta">2026-04-22 · 14:32 · BTCPay invoice INV-9F2A</div><div class="body">Signed with issuer key mz7q8r4t1v…h3k2pXq9wL.</div></div></div>
|
||||
<div class="item"><div class="marker"></div><div><div class="head">Payment confirmed</div><div class="meta">2026-04-22 · 14:31 · 50,000 sats · Lightning</div><div class="body">Settled in 1 confirmation. Funds routed to wallet "aurora".</div></div></div>
|
||||
<div class="item"><div class="marker"></div><div><div class="head">Invoice created</div><div class="meta">2026-04-22 · 14:29</div><div class="body">Buyer landed on purchase URL from /pricing.</div></div></div>
|
||||
<div class="item"><div class="marker"></div><div><div class="head">First verification</div><div class="meta">2026-04-22 · 15:11 · 10.0.4.118</div><div class="body">Sundial v2.0.3 verified key offline at startup.</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 14px;">
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Customer</h3></div>
|
||||
<div class="info-row"><span class="k">Email</span><span class="v">nina@dial.studio</span></div>
|
||||
<div class="info-row"><span class="k">npub</span><span class="v" style="font-family: var(--font-mono); font-size: 12px">npub1aw…q4t8</span></div>
|
||||
<div class="info-row"><span class="k">First seen</span><span class="v">2 hours ago</span></div>
|
||||
<div class="info-row"><span class="k">Other licenses</span><span class="v">1 (Aurora Plugin)</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Policy</h3></div>
|
||||
<div class="info-row"><span class="k">Slug</span><span class="v" style="font-family: var(--font-mono)">default</span></div>
|
||||
<div class="info-row"><span class="k">Duration</span><span class="v">1 year</span></div>
|
||||
<div class="info-row"><span class="k">Seats</span><span class="v">1</span></div>
|
||||
<div class="info-row"><span class="k">Entitlements</span><span class="v">core, sync, export</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Keysat — Licenses</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="dash.css">
|
||||
<style>
|
||||
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
|
||||
.chip { font-size: 12.5px; padding: 6px 12px; border-radius: 999px; background: var(--cream-50); border: 1px solid var(--border-1); color: var(--ink-700); cursor: pointer; }
|
||||
.chip.active { background: var(--navy-800); color: var(--cream-50); border-color: var(--navy-800); }
|
||||
.chip .count { opacity: 0.6; margin-left: 4px; }
|
||||
.filter-spacer { flex: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="brand"><img src="../../assets/keysat-mark.svg" alt=""><span>Keysat</span></div>
|
||||
<a class="nav" href="index.html"><i data-lucide="layout-dashboard"></i>Overview</a>
|
||||
<a class="nav" href="#"><i data-lucide="package"></i>Products<span class="count">3</span></a>
|
||||
<a class="nav active" href="licenses.html"><i data-lucide="key-round"></i>Licenses<span class="count">42</span></a>
|
||||
<a class="nav" href="#"><i data-lucide="users"></i>Customers<span class="count">38</span></a>
|
||||
<a class="nav" href="#"><i data-lucide="tag"></i>Discount codes</a>
|
||||
<div class="group-label">System</div>
|
||||
<a class="nav" href="#"><i data-lucide="scroll-text"></i>Audit log</a>
|
||||
<a class="nav" href="#"><i data-lucide="webhook"></i>Webhooks</a>
|
||||
<a class="nav" href="#"><i data-lucide="settings-2"></i>Settings</a>
|
||||
<div class="footer"><span class="dot"></span><div><div style="color: var(--cream-50); font-weight: 600">BTCPay connected</div><div>store: aurora-software</div></div></div>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<header class="topbar">
|
||||
<div><div class="crumb">Workspace · aurora-software</div><h1>Licenses</h1></div>
|
||||
<div class="search"><i data-lucide="search"></i><input placeholder="Search by key, email, product…"></div>
|
||||
<div class="topbar-actions">
|
||||
<button class="btn secondary"><i data-lucide="download"></i>Export</button>
|
||||
<button class="btn primary"><i data-lucide="plus"></i>Issue license</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content">
|
||||
<div class="toolbar">
|
||||
<span class="chip active">All<span class="count">42</span></span>
|
||||
<span class="chip">Active<span class="count">35</span></span>
|
||||
<span class="chip">Trial<span class="count">4</span></span>
|
||||
<span class="chip">Expired<span class="count">2</span></span>
|
||||
<span class="chip">Revoked<span class="count">1</span></span>
|
||||
<div class="filter-spacer"></div>
|
||||
<button class="btn ghost sm"><i data-lucide="filter"></i>Filter</button>
|
||||
<button class="btn ghost sm"><i data-lucide="arrow-up-down"></i>Sort: newest</button>
|
||||
</div>
|
||||
<table class="t">
|
||||
<thead><tr><th>License key</th><th>Product</th><th>Customer</th><th>Status</th><th>Expires</th><th>Issued</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="key">KS-9F2A-7C41-XK22-6D8E</td><td class="product">Sundial 2.0</td><td>nina@dial.studio</td><td><span class="badge b-success"><span class="dot" style="background:#2D7A5F"></span>Active</span></td><td class="meta">Apr 2027</td><td class="meta">2 hours ago</td><td><i data-lucide="more-horizontal" style="color:var(--ink-400)"></i></td></tr>
|
||||
<tr><td class="key">KS-A14C-PT09-LM31-R7Q4</td><td class="product">Sundial Pro</td><td>m@labry.dev</td><td><span class="badge b-success"><span class="dot" style="background:#2D7A5F"></span>Active</span></td><td class="meta"><span class="badge b-gold">Lifetime</span></td><td class="meta">Yesterday</td><td><i data-lucide="more-horizontal" style="color:var(--ink-400)"></i></td></tr>
|
||||
<tr><td class="key">KS-T2X8-6K43-QQ91-WE0M</td><td class="product">Sundial 2.0</td><td>jo@kestrel.fm</td><td><span class="badge b-warning"><span class="dot" style="background:#B8861F"></span>Trial</span></td><td class="meta">in 12 days</td><td class="meta">2d ago</td><td><i data-lucide="more-horizontal" style="color:var(--ink-400)"></i></td></tr>
|
||||
<tr><td class="key">KS-BX9D-MM21-NU45-7F3R</td><td class="product">Sundial 2.0</td><td>ari@northpath.io</td><td><span class="badge b-success"><span class="dot" style="background:#2D7A5F"></span>Active</span></td><td class="meta">Apr 2027</td><td class="meta">3d ago</td><td><i data-lucide="more-horizontal" style="color:var(--ink-400)"></i></td></tr>
|
||||
<tr><td class="key">KS-PW45-VR82-XA61-9K0L</td><td class="product">Sundial Pro</td><td>tom@workhorse.app</td><td><span class="badge b-danger"><span class="dot" style="background:#B23A3A"></span>Revoked</span></td><td class="meta">—</td><td class="meta">5d ago</td><td><i data-lucide="more-horizontal" style="color:var(--ink-400)"></i></td></tr>
|
||||
<tr><td class="key">KS-MN23-LP08-RR54-VV01</td><td class="product">Aurora Plugin</td><td>kate@kate.codes</td><td><span class="badge b-success"><span class="dot" style="background:#2D7A5F"></span>Active</span></td><td class="meta">Mar 2027</td><td class="meta">1w ago</td><td><i data-lucide="more-horizontal" style="color:var(--ink-400)"></i></td></tr>
|
||||
<tr><td class="key">KS-DD12-XK77-AA98-PQ45</td><td class="product">Sundial 2.0</td><td>raj@spinwheel.app</td><td><span class="badge b-success"><span class="dot" style="background:#2D7A5F"></span>Active</span></td><td class="meta">Mar 2027</td><td class="meta">1w ago</td><td><i data-lucide="more-horizontal" style="color:var(--ink-400)"></i></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,123 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Keysat — New product</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="dash.css">
|
||||
<style>
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 380px; gap: 24px; align-items: start; }
|
||||
.panel-stack > * + * { margin-top: 14px; }
|
||||
.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.preview-cert { background: var(--cream-50); border: 1px solid var(--border-1); border-radius: 12px; box-shadow: 0 0 0 1px var(--gold-500) inset, var(--shadow-sm); padding: 22px; }
|
||||
.preview-cert .stamp { font-size: 10px; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; color: var(--gold-700); text-align: center; }
|
||||
.preview-cert .h { text-align: center; font-family: var(--font-display); font-weight: 500; font-size: 18px; color: var(--navy-950); margin: 8px 0 4px; }
|
||||
.preview-cert .sub { text-align: center; font-size: 12px; color: var(--ink-500); margin-bottom: 14px; }
|
||||
.preview-cert .field { font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-500); margin-bottom: 3px; }
|
||||
.preview-cert .value { font-family: var(--font-mono); font-size: 13px; color: var(--navy-900); margin-bottom: 10px; }
|
||||
.seg { display: flex; gap: 0; border: 1px solid var(--border-2); border-radius: 7px; overflow: hidden; }
|
||||
.seg button { flex: 1; background: transparent; border: 0; padding: 9px 12px; font-size: 13px; color: var(--ink-700); cursor: pointer; font-family: var(--font-body); border-right: 1px solid var(--border-1); }
|
||||
.seg button:last-child { border-right: 0; }
|
||||
.seg button.on { background: var(--navy-800); color: var(--cream-50); font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="brand"><img src="../../assets/keysat-mark.svg" alt=""><span>Keysat</span></div>
|
||||
<a class="nav" href="index.html"><i data-lucide="layout-dashboard"></i>Overview</a>
|
||||
<a class="nav active" href="#"><i data-lucide="package"></i>Products<span class="count">3</span></a>
|
||||
<a class="nav" href="licenses.html"><i data-lucide="key-round"></i>Licenses<span class="count">42</span></a>
|
||||
<a class="nav" href="#"><i data-lucide="users"></i>Customers<span class="count">38</span></a>
|
||||
<a class="nav" href="#"><i data-lucide="tag"></i>Discount codes</a>
|
||||
<div class="group-label">System</div>
|
||||
<a class="nav" href="#"><i data-lucide="scroll-text"></i>Audit log</a>
|
||||
<a class="nav" href="#"><i data-lucide="webhook"></i>Webhooks</a>
|
||||
<a class="nav" href="#"><i data-lucide="settings-2"></i>Settings</a>
|
||||
<div class="footer"><span class="dot"></span><div><div style="color: var(--cream-50); font-weight: 600">BTCPay connected</div></div></div>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<header class="topbar">
|
||||
<div><div class="crumb"><a href="#">Products</a> · New</div><h1>New product</h1></div>
|
||||
<div class="topbar-actions">
|
||||
<button class="btn ghost">Cancel</button>
|
||||
<button class="btn secondary">Save as draft</button>
|
||||
<button class="btn primary"><i data-lucide="check"></i>Create product</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content">
|
||||
<div class="form-grid">
|
||||
<div class="panel-stack">
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Product</h3></div>
|
||||
<div style="padding: 18px;">
|
||||
<div class="row-2">
|
||||
<div class="field"><label class="lbl">Product name</label><input class="input" value="Sundial 2.0"><div class="hint">Shown on receipts and the public purchase page.</div></div>
|
||||
<div class="field"><label class="lbl">Slug</label><input class="input mono" value="sundial-2"><div class="hint">Used in your purchase URL.</div></div>
|
||||
</div>
|
||||
<div class="field"><label class="lbl">Tagline</label><input class="input" value="A focused timer for deep work."></div>
|
||||
<div class="field"><label class="lbl">Description</label><textarea class="input" rows="3" style="resize: vertical; font-family: var(--font-body)">Sundial is a calm, single-window timer for deep work sessions. Sound design by Hauschka. macOS, Windows, Linux.</textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Default policy</h3><span class="eyebrow">Drives the public purchase URL</span></div>
|
||||
<div style="padding: 18px;">
|
||||
<div class="row-2">
|
||||
<div class="field"><label class="lbl">Duration</label>
|
||||
<div class="seg">
|
||||
<button>30 days</button><button class="on">1 year</button><button>3 years</button><button>Lifetime</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="lbl">Seats</label><input class="input" value="1"><div class="hint">Maximum machines per key.</div></div>
|
||||
</div>
|
||||
<div class="row-2">
|
||||
<div class="field"><label class="lbl">Trial available</label>
|
||||
<div class="seg"><button class="on">14 days</button><button>None</button></div>
|
||||
</div>
|
||||
<div class="field"><label class="lbl">Entitlements</label><input class="input mono" value="core, sync, export"><div class="hint">Comma-separated. Embedded in the signed key.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Price</h3></div>
|
||||
<div style="padding: 18px;">
|
||||
<div class="row-2">
|
||||
<div class="field"><label class="lbl">Amount</label><input class="input mono" value="50,000"></div>
|
||||
<div class="field"><label class="lbl">Unit</label>
|
||||
<div class="seg"><button class="on">sats</button><button>BTC</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint">≈ $30.00 USD at current rate. Updated every 30s from your BTCPay store.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-stack" style="position: sticky; top: 90px;">
|
||||
<div class="eyebrow">Preview</div>
|
||||
<div class="preview-cert">
|
||||
<div class="stamp">— Certificate of License —</div>
|
||||
<div class="h">Sundial 2.0</div>
|
||||
<div class="sub">default · 1 year · single seat</div>
|
||||
<div class="field">License key</div>
|
||||
<div class="value">KS-XXXX-XXXX-XXXX-XXXX</div>
|
||||
<div class="row-2">
|
||||
<div><div class="field">Price</div><div class="value">50,000 sats</div></div>
|
||||
<div><div class="field">Trial</div><div class="value">14 days</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="background: var(--cream-100); padding: 14px 16px; font-size: 12.5px; color: var(--ink-700);">
|
||||
<div style="display: flex; gap: 8px; align-items: start"><i data-lucide="info" style="width: 14px; height: 14px; color: var(--navy-700); flex-shrink: 0; margin-top: 2px;"></i>
|
||||
<span>Your public purchase URL will be<br><code style="font-family: var(--font-mono); color: var(--navy-900); font-size: 12px">aurora.keysat.local/buy/sundial-2</code></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Keysat — Sign in</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; font-family: var(--font-body); color: var(--ink-900); background: var(--cream-100); background-image: radial-gradient(rgba(14,31,51,0.025) 1px, transparent 1px), radial-gradient(rgba(138,111,61,0.022) 1px, transparent 1px); background-size: 3px 3px, 7px 7px; display: flex; align-items: center; justify-content: center; padding: 40px 20px; }
|
||||
.card { width: 420px; max-width: 100%; background: var(--cream-50); border: 1px solid var(--border-1); border-radius: 14px; box-shadow: 0 0 0 1px var(--gold-500) inset, var(--shadow-md); padding: 36px 36px 32px; position: relative; }
|
||||
.card::before { content: ''; position: absolute; left: 14px; right: 14px; top: 14px; height: 1px; background: var(--gold-500); opacity: 0.4; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; justify-content: center; margin-bottom: 6px; }
|
||||
.brand img { width: 56px; height: 56px; }
|
||||
h1 { font-family: var(--font-display); font-weight: 500; font-size: 26px; letter-spacing: -0.02em; color: var(--navy-950); margin: 14px 0 4px; text-align: center; }
|
||||
.sub { text-align: center; font-size: 13.5px; color: var(--ink-500); margin-bottom: 24px; }
|
||||
.lbl { display: block; font-size: 12.5px; font-weight: 600; color: var(--ink-700); margin-bottom: 6px; }
|
||||
.input { width: 100%; padding: 11px 13px; font-family: var(--font-mono); font-size: 13px; border: 1px solid var(--border-2); border-radius: 8px; background: white; box-sizing: border-box; }
|
||||
.input:focus { outline: none; border-color: var(--navy-700); box-shadow: 0 0 0 3px rgba(30,58,95,0.18); }
|
||||
.btn { width: 100%; padding: 12px; background: var(--navy-800); color: var(--cream-50); border: 0; border-radius: 8px; font-family: var(--font-body); font-weight: 600; font-size: 14px; cursor: pointer; margin-top: 14px; transition: background 120ms; }
|
||||
.btn:hover { background: var(--navy-900); }
|
||||
.hint { font-size: 12px; color: var(--ink-500); margin-top: 8px; line-height: 1.5; }
|
||||
.footnote { text-align: center; font-size: 12px; color: var(--ink-500); margin-top: 22px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="brand"><img src="../../assets/keysat-mark.svg" alt=""></div>
|
||||
<h1>Keysat admin</h1>
|
||||
<div class="sub">Paste the admin API key from your StartOS service page.</div>
|
||||
<label class="lbl">Admin API key</label>
|
||||
<input class="input" placeholder="ks_admin_…">
|
||||
<div class="hint">Find this in StartOS → Keysat → Properties → adminApiKey.</div>
|
||||
<button class="btn">Sign in</button>
|
||||
<div class="footnote">Connected to <code style="font-family: var(--font-mono); font-size: 11.5px">aurora.keysat.local</code></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
# Keysat — Docs UI Kit
|
||||
|
||||
Developer reference layout. Three-column shell: sidebar (sections), content (markdown), right rail (on-this-page).
|
||||
|
||||
## Files
|
||||
- `index.html` — Integration guide landing page with sidebar, prose, code samples, callouts.
|
||||
|
||||
## Components inline
|
||||
- Docs sidebar — grouped section nav, search.
|
||||
- Prose — h1/h2/h3, lists, callouts, inline code, code blocks.
|
||||
- Right rail — on-this-page jumplinks.
|
||||
@@ -0,0 +1,161 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Keysat Docs — Integration guide</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<style>
|
||||
*{box-sizing:border-box} html,body{margin:0;padding:0}
|
||||
body{font-family:var(--font-body);color:var(--ink-900);background:var(--cream-100);background-image:radial-gradient(rgba(14,31,51,0.022) 1px,transparent 1px),radial-gradient(rgba(138,111,61,0.020) 1px,transparent 1px);background-size:3px 3px,7px 7px}
|
||||
a{color:var(--navy-800);text-decoration:none}
|
||||
a:hover{text-decoration:underline;text-decoration-thickness:1.5px;text-underline-offset:3px}
|
||||
.topnav{position:sticky;top:0;z-index:10;background:rgba(245,241,232,0.9);backdrop-filter:blur(10px);border-bottom:1px solid var(--border-1);padding:14px 28px;display:flex;align-items:center;gap:18px}
|
||||
.topnav .brand{display:flex;align-items:center;gap:10px;font-family:var(--font-display);font-weight:500;color:var(--navy-900);font-size:14px;letter-spacing:0.28em;text-transform:uppercase}
|
||||
.topnav .brand img{width:26px;height:26px;}
|
||||
.topnav .docs-tag{font-size:11px;font-weight:700;letter-spacing:0.18em;text-transform:uppercase;color:var(--gold-700);padding-left:10px;border-left:1px solid var(--border-2)}
|
||||
.topnav nav{margin-left:auto;display:flex;gap:22px;font-size:13.5px;color:var(--ink-700)}
|
||||
.topnav nav a:hover{color:var(--navy-900)}
|
||||
.search{position:relative;width:240px}
|
||||
.search input{width:100%;padding:7px 10px 7px 30px;font-size:13px;border:1px solid var(--border-1);border-radius:7px;background:var(--cream-50)}
|
||||
.search [data-lucide]{position:absolute;left:9px;top:50%;transform:translateY(-50%);width:14px;height:14px;color:var(--ink-400)}
|
||||
.layout{display:grid;grid-template-columns:240px 1fr 220px;max-width:1280px;margin:0 auto;gap:32px;padding:28px 28px 64px}
|
||||
aside.side{position:sticky;top:74px;align-self:start;font-size:13.5px;max-height:calc(100vh - 90px);overflow:auto;padding-right:8px}
|
||||
aside.side .group{margin-bottom:18px}
|
||||
aside.side .group .glabel{font-size:10.5px;font-weight:700;letter-spacing:0.16em;text-transform:uppercase;color:var(--gold-700);margin:6px 8px 6px}
|
||||
aside.side a{display:block;padding:5px 10px;border-radius:5px;color:var(--ink-700);line-height:1.4}
|
||||
aside.side a:hover{background:var(--cream-200);text-decoration:none}
|
||||
aside.side a.active{background:var(--navy-800);color:var(--cream-50);font-weight:600}
|
||||
main.prose{min-width:0}
|
||||
.prose .crumb{font-size:12px;color:var(--ink-500);margin-bottom:8px;letter-spacing:0.04em}
|
||||
.prose h1{font-family:var(--font-display);font-weight: 500;font-size:38px;letter-spacing: -0.022em;color:var(--navy-950);margin:0 0 8px;line-height:1.1}
|
||||
.prose .lead{font-size:17px;line-height:1.55;color:var(--ink-700);margin:0 0 24px;max-width:640px}
|
||||
.prose h2{font-family:var(--font-display);font-weight:700;font-size:24px;letter-spacing:-0.015em;color:var(--navy-950);margin:36px 0 12px;padding-top:8px;border-top:1px solid var(--border-1);padding-top:24px}
|
||||
.prose h3{font-family:var(--font-display);font-weight:700;font-size:17px;color:var(--navy-950);margin:22px 0 8px;letter-spacing:-0.01em}
|
||||
.prose p{font-size:15px;line-height:1.65;color:var(--ink-700);margin:0 0 14px;max-width:680px}
|
||||
.prose ul{padding-left:22px;margin:0 0 14px;max-width:680px}
|
||||
.prose li{font-size:15px;line-height:1.65;color:var(--ink-700);margin-bottom:4px}
|
||||
.prose code{font-family:var(--font-mono);font-size:13px;background:var(--cream-200);padding:2px 6px;border-radius:4px;color:var(--navy-900)}
|
||||
pre.code{background:var(--navy-950);color:var(--cream-50);padding:18px 22px;border-radius:10px;overflow-x:auto;font-family:var(--font-mono);font-size:13px;line-height:1.7;margin:14px 0 20px;border:1px solid var(--navy-900)}
|
||||
pre.code .c{color:rgba(245,241,232,0.45)} pre.code .k{color:var(--gold-400)} pre.code .s{color:#d4b985} pre.code .f{color:var(--cream-50)}
|
||||
.callout{border:1px solid var(--border-1);border-left:3px solid var(--gold-500);background:var(--cream-50);border-radius:8px;padding:14px 16px;margin:14px 0 22px;display:flex;gap:12px;align-items:flex-start;max-width:680px}
|
||||
.callout [data-lucide]{color:var(--gold-700);width:18px;height:18px;flex-shrink:0;margin-top:2px}
|
||||
.callout p{margin:0;font-size:14px}
|
||||
.callout strong{color:var(--navy-950);font-weight:700}
|
||||
aside.toc{position:sticky;top:74px;align-self:start;font-size:12.5px;border-left:1px solid var(--border-1);padding:8px 0 8px 18px}
|
||||
aside.toc .label{font-size:10.5px;font-weight:700;letter-spacing:0.16em;text-transform:uppercase;color:var(--gold-700);margin-bottom:10px}
|
||||
aside.toc a{display:block;padding:4px 0;color:var(--ink-500);line-height:1.4}
|
||||
aside.toc a:hover{color:var(--navy-900);text-decoration:none}
|
||||
aside.toc a.active{color:var(--navy-900);font-weight:600;border-left:2px solid var(--gold-500);margin-left:-20px;padding-left:18px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topnav">
|
||||
<a href="#" class="brand"><img src="../../assets/keysat-mark.svg" alt=""><span>Keysat</span></a>
|
||||
<span class="docs-tag">Docs</span>
|
||||
<nav>
|
||||
<a href="#">Guide</a>
|
||||
<a href="#">Wire format</a>
|
||||
<a href="#">SDKs</a>
|
||||
<a href="#">Changelog</a>
|
||||
</nav>
|
||||
<div class="search"><i data-lucide="search"></i><input placeholder="Search docs"></div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="side">
|
||||
<div class="group">
|
||||
<div class="glabel">Get started</div>
|
||||
<a href="#">Introduction</a>
|
||||
<a href="#" class="active">Integration guide</a>
|
||||
<a href="#">Quickstart</a>
|
||||
<a href="#">Glossary</a>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="glabel">Concepts</div>
|
||||
<a href="#">Products & policies</a>
|
||||
<a href="#">Signing & verification</a>
|
||||
<a href="#">BTCPay webhooks</a>
|
||||
<a href="#">Discounts & comps</a>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="glabel">SDKs</div>
|
||||
<a href="#">Rust</a>
|
||||
<a href="#">TypeScript</a>
|
||||
<a href="#">Python</a>
|
||||
<a href="#">Wire format reference</a>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="glabel">Operate</div>
|
||||
<a href="#">Backups & recovery</a>
|
||||
<a href="#">Migrating Start9 hardware</a>
|
||||
<a href="#">Troubleshooting</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="prose">
|
||||
<div class="crumb">Get started · Integration guide</div>
|
||||
<h1>Integration guide</h1>
|
||||
<p class="lead">Wire Keysat licenses into your software in under an afternoon. The verifier is pure-function, offline, and ships in five lines.</p>
|
||||
|
||||
<h2 id="prereq">Prerequisites</h2>
|
||||
<p>Before you start, you should have:</p>
|
||||
<ul>
|
||||
<li>A Keysat installation running on your Start9 — see <a href="#">Installation</a>.</li>
|
||||
<li>BTCPay Server connected — see <a href="#">Connect BTCPay</a>.</li>
|
||||
<li>At least one product defined in the admin UI.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="install">Install the SDK</h2>
|
||||
<p>Pick the SDK for your language. All three are wire-compatible — a license issued by your Keysat verifies identically in any of them.</p>
|
||||
<pre class="code"><span class="c"># TypeScript</span>
|
||||
npm install @keysat/licensing-client
|
||||
|
||||
<span class="c"># Rust</span>
|
||||
cargo add licensing-client
|
||||
|
||||
<span class="c"># Python</span>
|
||||
pip install keysat-licensing-client</pre>
|
||||
|
||||
<h2 id="embed">Embed your public key</h2>
|
||||
<p>Copy your issuer public key from <strong>Settings → Issuer key</strong> in the admin UI. Paste it into your application's source code as a compile-time constant.</p>
|
||||
<pre class="code"><span class="k">const</span> <span class="f">ISSUER_PEM</span> = <span class="s">`-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
|
||||
-----END PUBLIC KEY-----`</span>;</pre>
|
||||
|
||||
<div class="callout">
|
||||
<i data-lucide="info"></i>
|
||||
<p><strong>Embed it. Don't fetch it.</strong> The whole point of offline verification is that your software can't be tricked by a network-level attacker. If you fetch the public key at runtime, you're back to trusting a server.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="verify">Verify a license</h2>
|
||||
<p>Read the user's license key from wherever you store it (a file, the keychain, an env var) and verify it at startup.</p>
|
||||
<pre class="code"><span class="k">import</span> { <span class="f">Verifier</span>, <span class="f">PublicKey</span> } <span class="k">from</span> <span class="s">'@keysat/licensing-client'</span>
|
||||
|
||||
<span class="k">const</span> verifier = <span class="k">new</span> <span class="f">Verifier</span>(<span class="f">PublicKey</span>.<span class="f">fromPem</span>(ISSUER_PEM))
|
||||
<span class="k">const</span> ok = verifier.<span class="f">verify</span>(licenseKeyFromUser)
|
||||
|
||||
<span class="k">if</span> (!ok.valid) <span class="f">exitUnlicensed</span>()
|
||||
<span class="k">if</span> (!ok.entitlements.<span class="f">has</span>(<span class="s">'export'</span>)) <span class="f">disableExport</span>()</pre>
|
||||
|
||||
<h2 id="renewals">Renewals & revocation</h2>
|
||||
<p>Keysat licenses are signed at issue time and do not phone home. If a license is revoked in the admin UI, the existing key continues to verify — that's the trade-off for offline. To support revocation, ship a thin <em>online</em> check that runs on a cadence (e.g. once a week) against your Keysat's public revocation feed.</p>
|
||||
|
||||
<div class="callout">
|
||||
<i data-lucide="key-round"></i>
|
||||
<p><strong>You decide the policy.</strong> Many indie developers don't ship revocation at all — once a key is sold, it stays valid. That's perfectly reasonable.</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="toc">
|
||||
<div class="label">On this page</div>
|
||||
<a href="#prereq">Prerequisites</a>
|
||||
<a href="#install">Install the SDK</a>
|
||||
<a href="#embed" class="active">Embed your public key</a>
|
||||
<a href="#verify">Verify a license</a>
|
||||
<a href="#renewals">Renewals & revocation</a>
|
||||
</aside>
|
||||
</div>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
# Keysat — Marketing UI Kit
|
||||
|
||||
A redesign of the single-page marketing site in the Keysat visual system: navy + cream, paper texture, classical type. Same content as the draft (`assets/keysat-draft-site.html`), new visual language.
|
||||
|
||||
## Files
|
||||
|
||||
- `index.html` — the full landing page (single-page marketing site).
|
||||
|
||||
## Sections
|
||||
|
||||
1. **Sticky header** — wordmark + nav + "Install" CTA.
|
||||
2. **Hero** — eyebrow → display headline with a gold-underlined "self-hosted" → lede → CTA row → trust strip ("Runs on Start9 · Pays via BTCPay · Verifies offline").
|
||||
3. **Value props** — 6-card grid; Lucide icons; gold underline on titles.
|
||||
4. **How it works** — numbered 5-step flow with gold serif numerals on cream paper cards.
|
||||
5. **Integration code** — language tabs (Rust / TypeScript / Python) over a navy code block.
|
||||
6. **Sovereign by default** — two-column "what you keep / what's outside the box".
|
||||
7. **Install** — marketplace + sideload, gold-bordered command card.
|
||||
8. **Footer** — navy surface, wordmark, links.
|
||||
|
||||
## Iconography
|
||||
|
||||
Lucide via `unpkg.com/lucide@latest`. Replaces the draft site's emoji 1:1:
|
||||
- ⚡ → `zap`
|
||||
- 🔐 → `key-round`
|
||||
- 📡 → `wifi-off`
|
||||
- 🎫 → `ticket`
|
||||
- 🏷️ → `tag`
|
||||
- 🛠️ → `wrench`
|
||||
@@ -0,0 +1,714 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Keysat — Bitcoin-paid software licensing, self-hosted on Start9</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
color: var(--ink-900);
|
||||
background: var(--cream-100);
|
||||
background-image:
|
||||
radial-gradient(rgba(14,31,51,0.025) 1px, transparent 1px),
|
||||
radial-gradient(rgba(138,111,61,0.022) 1px, transparent 1px);
|
||||
background-size: 3px 3px, 7px 7px;
|
||||
background-position: 0 0, 1px 1px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 32px; }
|
||||
a { color: var(--ink-900); text-decoration: none; }
|
||||
a:hover { color: var(--navy-900); }
|
||||
|
||||
/* ---------- Header ---------- */
|
||||
header.site {
|
||||
position: sticky; top: 0; z-index: 20;
|
||||
background: rgba(245, 241, 232, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border-1);
|
||||
}
|
||||
header.site .inner {
|
||||
display: flex; align-items: center; gap: 28px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
header.site .brand {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.30em;
|
||||
text-transform: uppercase;
|
||||
color: var(--navy-900);
|
||||
}
|
||||
header.site .brand img { width: 32px; height: 32px; }
|
||||
header.site nav { margin-left: auto; display: flex; gap: 28px; font-size: 14px; }
|
||||
header.site nav a { color: var(--ink-700); font-weight: 500; }
|
||||
header.site nav a:hover { color: var(--navy-900); }
|
||||
header.site .cta { margin-left: 8px; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
font-family: var(--font-body); font-weight: 600; font-size: 14px;
|
||||
padding: 11px 20px; border-radius: 8px; border: 1px solid transparent;
|
||||
cursor: pointer; transition: all 120ms var(--ease-standard); line-height: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn.lg { font-size: 15.5px; padding: 14px 24px; }
|
||||
.btn.primary { background: var(--navy-800); color: var(--cream-50); border-color: var(--navy-800); }
|
||||
.btn.primary:hover { background: var(--navy-900); border-color: var(--navy-900); color: var(--cream-50); }
|
||||
.btn.secondary { background: transparent; color: var(--navy-900); border-color: var(--border-2); }
|
||||
.btn.secondary:hover { background: var(--cream-200); color: var(--navy-900); }
|
||||
.btn.ghost { background: transparent; color: var(--navy-900); border: none; }
|
||||
.btn.ghost:hover { background: rgba(14,31,51,0.06); }
|
||||
.btn .arrow { transition: transform 200ms var(--ease-standard); }
|
||||
.btn:hover .arrow { transform: translateX(2px); }
|
||||
|
||||
/* ---------- Hero ---------- */
|
||||
section.hero { padding: 92px 0 72px; }
|
||||
.hero-grid { display: grid; grid-template-columns: 1.15fr 1fr; gap: 64px; align-items: center; }
|
||||
.hero .eyebrow {
|
||||
font-size: 11.5px; font-weight: 700; letter-spacing: 0.18em;
|
||||
text-transform: uppercase; color: var(--gold-700);
|
||||
display: inline-flex; align-items: center; gap: 10px; margin-bottom: 22px;
|
||||
}
|
||||
.hero .eyebrow::before {
|
||||
content: ''; display: inline-block; width: 28px; height: 1px; background: var(--gold-500);
|
||||
}
|
||||
.hero h1 {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(44px, 5.4vw, 72px);
|
||||
font-weight: 500;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.022em;
|
||||
color: var(--navy-950);
|
||||
margin: 0 0 22px;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.hero h1 .gold {
|
||||
background-image: linear-gradient(to top, var(--gold-400) 0, var(--gold-400) 6px, transparent 6px);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 95%;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.hero p.lede {
|
||||
font-size: 19px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-700);
|
||||
max-width: 540px;
|
||||
margin: 0 0 32px;
|
||||
}
|
||||
.hero .cta-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
.hero .trust {
|
||||
margin-top: 36px;
|
||||
display: flex; align-items: center; gap: 18px;
|
||||
font-size: 13px; color: var(--ink-500);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.hero .trust span { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.hero .trust .dot { width: 4px; height: 4px; border-radius: 50%; background: var(--gold-500); }
|
||||
.hero .trust [data-lucide] { color: var(--navy-700); }
|
||||
|
||||
/* Hero visual: mocked license certificate */
|
||||
.cert {
|
||||
background: var(--cream-50);
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 0 0 1px var(--gold-500) inset, 0 8px 16px rgba(14,31,51,0.10), 0 24px 64px rgba(14,31,51,0.10);
|
||||
padding: 36px 36px 30px;
|
||||
position: relative;
|
||||
transform: rotate(-1.2deg);
|
||||
max-width: 460px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.cert::before, .cert::after {
|
||||
content: ''; position: absolute; left: 16px; right: 16px; height: 1px; background: var(--gold-500); opacity: 0.5;
|
||||
}
|
||||
.cert::before { top: 16px; }
|
||||
.cert::after { bottom: 16px; }
|
||||
.cert .seal {
|
||||
position: absolute; right: -30px; top: -30px;
|
||||
width: 88px; height: 88px; border-radius: 50%;
|
||||
background: var(--cream-50);
|
||||
box-shadow: 0 0 0 1px var(--gold-500) inset, 0 0 0 5px var(--cream-50), 0 0 0 6px var(--gold-500), var(--shadow-md);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: var(--font-display); font-weight: 900; font-size: 32px; color: var(--navy-800);
|
||||
transform: rotate(8deg);
|
||||
}
|
||||
.cert .stamp {
|
||||
font-size: 9.5px; font-weight: 700; letter-spacing: 0.22em;
|
||||
text-transform: uppercase; color: var(--gold-700);
|
||||
text-align: center; margin-bottom: 14px;
|
||||
}
|
||||
.cert h4 {
|
||||
font-family: var(--font-display); font-weight: 500; font-size: 22px;
|
||||
text-align: center; color: var(--navy-900); margin: 0 0 4px; letter-spacing: -0.015em;
|
||||
}
|
||||
.cert .sub {
|
||||
text-align: center; font-size: 12px; color: var(--ink-500); margin-bottom: 22px;
|
||||
}
|
||||
.cert .field { font-size: 11px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-500); margin-bottom: 4px; }
|
||||
.cert .value { font-family: var(--font-mono); font-size: 14px; color: var(--navy-900); margin-bottom: 14px; }
|
||||
.cert .row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
|
||||
.cert .sig {
|
||||
border-top: 1px dashed rgba(14,31,51,0.2);
|
||||
padding-top: 12px; margin-top: 6px;
|
||||
font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-500); line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ---------- Section base ---------- */
|
||||
section.block { padding: 96px 0; }
|
||||
section.tinted { background: var(--cream-200); position: relative; }
|
||||
section.tinted::before, section.tinted::after {
|
||||
content: ''; position: absolute; left: 0; right: 0; height: 1px; background: var(--gold-500); opacity: 0.4;
|
||||
}
|
||||
section.tinted::before { top: 0; }
|
||||
section.tinted::after { bottom: 0; }
|
||||
|
||||
.section-head { max-width: 760px; margin-bottom: 56px; }
|
||||
.section-head .eyebrow {
|
||||
font-size: 11.5px; font-weight: 700; letter-spacing: 0.18em;
|
||||
text-transform: uppercase; color: var(--gold-700);
|
||||
margin-bottom: 14px; display: block;
|
||||
}
|
||||
.section-head h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(32px, 3.6vw, 46px);
|
||||
font-weight: 500; line-height: 1.05; letter-spacing: -0.022em;
|
||||
color: var(--navy-950); margin: 0 0 14px;
|
||||
}
|
||||
.section-head p {
|
||||
font-size: 18px; line-height: 1.55; color: var(--ink-700); margin: 0; max-width: 580px;
|
||||
}
|
||||
|
||||
/* ---------- Value grid ---------- */
|
||||
.value-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px;
|
||||
background: var(--border-1);
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: 14px; overflow: hidden;
|
||||
}
|
||||
.value-grid .item {
|
||||
background: var(--cream-50);
|
||||
padding: 32px 28px;
|
||||
transition: background 150ms;
|
||||
}
|
||||
.value-grid .item:hover { background: var(--cream-100); }
|
||||
.value-grid .icon-wrap {
|
||||
width: 40px; height: 40px; border-radius: 8px;
|
||||
background: var(--cream-200);
|
||||
border: 1px solid var(--border-1);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 18px; color: var(--navy-800);
|
||||
}
|
||||
.value-grid h3 {
|
||||
font-family: var(--font-display); font-weight: 700; font-size: 18px;
|
||||
color: var(--navy-950); margin: 0 0 6px; letter-spacing: -0.01em;
|
||||
}
|
||||
.value-grid h3 + .accent-bar {
|
||||
width: 22px; height: 2px; background: var(--gold-500); margin-bottom: 12px;
|
||||
}
|
||||
.value-grid p { margin: 0; font-size: 14.5px; color: var(--ink-700); line-height: 1.55; }
|
||||
|
||||
/* ---------- Flow ---------- */
|
||||
.flow { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; }
|
||||
.flow .step {
|
||||
background: var(--cream-50);
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: 12px;
|
||||
padding: 28px 22px 24px;
|
||||
position: relative;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.flow .num {
|
||||
font-family: var(--font-display); font-weight: 900; font-size: 56px;
|
||||
color: var(--gold-500); line-height: 1; margin-bottom: 14px;
|
||||
letter-spacing: -0.04em;
|
||||
font-variant-numeric: lining-nums;
|
||||
}
|
||||
.flow .step h3 {
|
||||
font-family: var(--font-display); font-weight: 700; font-size: 16px;
|
||||
color: var(--navy-950); margin: 0 0 6px; letter-spacing: -0.01em;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.flow .step p {
|
||||
font-size: 13.5px; color: var(--ink-700); line-height: 1.5; margin: 0;
|
||||
}
|
||||
|
||||
/* ---------- Code block ---------- */
|
||||
.code-card {
|
||||
background: var(--navy-950);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid var(--navy-900);
|
||||
}
|
||||
.code-tabs {
|
||||
display: flex;
|
||||
background: var(--navy-900);
|
||||
border-bottom: 1px solid rgba(245,241,232,0.08);
|
||||
padding: 0 8px;
|
||||
}
|
||||
.code-tabs button {
|
||||
background: transparent; border: 0;
|
||||
color: rgba(245,241,232,0.55);
|
||||
font-family: var(--font-body); font-weight: 500; font-size: 13px;
|
||||
padding: 14px 18px; cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 120ms;
|
||||
}
|
||||
.code-tabs button:hover { color: rgba(245,241,232,0.85); }
|
||||
.code-tabs button.active {
|
||||
color: var(--cream-50);
|
||||
border-bottom-color: var(--gold-500);
|
||||
}
|
||||
.code-tabs .install {
|
||||
margin-left: auto;
|
||||
padding: 14px 18px;
|
||||
font-family: var(--font-mono); font-size: 12px;
|
||||
color: var(--gold-400);
|
||||
}
|
||||
pre.code {
|
||||
margin: 0;
|
||||
padding: 24px 28px;
|
||||
font-family: var(--font-mono); font-size: 13.5px;
|
||||
line-height: 1.7; color: var(--cream-50);
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre.code .c { color: rgba(245,241,232,0.45); }
|
||||
pre.code .k { color: var(--gold-400); }
|
||||
pre.code .s { color: #d4b985; }
|
||||
pre.code .n { color: #a6b7cf; }
|
||||
pre.code .f { color: var(--cream-50); }
|
||||
pre.code .p { color: rgba(245,241,232,0.55); }
|
||||
|
||||
.code-section { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: start; }
|
||||
.code-section .pitch h3 { font-family: var(--font-display); font-weight: 700; font-size: 22px; color: var(--navy-950); margin: 0 0 12px; letter-spacing: -0.015em; }
|
||||
.code-section .pitch p { font-size: 16px; color: var(--ink-700); line-height: 1.55; margin: 0 0 16px; }
|
||||
.code-section .pitch ul { list-style: none; padding: 0; margin: 24px 0 0; }
|
||||
.code-section .pitch li { display: flex; align-items: start; gap: 12px; padding: 8px 0; font-size: 14.5px; color: var(--ink-700); }
|
||||
.code-section .pitch li::before { content: '✓'; color: var(--gold-600); font-weight: 700; flex-shrink: 0; margin-top: 1px; }
|
||||
|
||||
/* ---------- Sovereign panel ---------- */
|
||||
.sov { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
||||
.sov .panel {
|
||||
background: var(--cream-50);
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: 14px;
|
||||
padding: 32px 32px 28px;
|
||||
}
|
||||
.sov .panel.dark {
|
||||
background: var(--navy-950); color: var(--cream-50);
|
||||
border: 1px solid var(--navy-900);
|
||||
}
|
||||
.sov .panel h3 {
|
||||
font-family: var(--font-display); font-weight: 700; font-size: 19px;
|
||||
margin: 0 0 4px; letter-spacing: -0.015em; color: inherit;
|
||||
}
|
||||
.sov .panel .sub { font-size: 13px; color: var(--ink-500); margin-bottom: 22px; }
|
||||
.sov .panel.dark .sub { color: rgba(245,241,232,0.55); }
|
||||
.sov ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.sov li {
|
||||
font-size: 13px;
|
||||
padding: 6px 13px; border-radius: 999px;
|
||||
background: var(--cream-100);
|
||||
border: 1px solid var(--border-1);
|
||||
color: var(--ink-700);
|
||||
}
|
||||
.sov .panel.dark li {
|
||||
background: var(--navy-900);
|
||||
border-color: rgba(245,241,232,0.15);
|
||||
color: rgba(245,241,232,0.9);
|
||||
}
|
||||
.sov .panel.dark li.no::before {
|
||||
content: '✕ '; color: rgba(245,241,232,0.45); margin-right: 2px;
|
||||
}
|
||||
.sov .panel .footnote { font-size: 13px; color: var(--ink-500); margin: 18px 0 0; line-height: 1.55; }
|
||||
.sov .panel.dark .footnote { color: rgba(245,241,232,0.6); }
|
||||
|
||||
/* ---------- Install ---------- */
|
||||
.install-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
||||
.install-card {
|
||||
background: var(--cream-50); border: 1px solid var(--border-1);
|
||||
border-radius: 14px; padding: 28px;
|
||||
}
|
||||
.install-card.featured { box-shadow: 0 0 0 1px var(--gold-500) inset, var(--shadow-sm); }
|
||||
.install-card .cap {
|
||||
display: inline-block; font-size: 10.5px; font-weight: 700;
|
||||
letter-spacing: 0.18em; text-transform: uppercase; color: var(--gold-700);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.install-card h3 {
|
||||
font-family: var(--font-display); font-weight: 700; font-size: 22px;
|
||||
color: var(--navy-950); margin: 0 0 8px; letter-spacing: -0.015em;
|
||||
}
|
||||
.install-card p { font-size: 14.5px; color: var(--ink-700); margin: 0 0 16px; line-height: 1.55; }
|
||||
.cmd-card {
|
||||
background: var(--navy-950); color: var(--cream-50);
|
||||
border-radius: 10px; padding: 14px 16px;
|
||||
font-family: var(--font-mono); font-size: 13px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.cmd-card .copy {
|
||||
background: rgba(245,241,232,0.10); color: var(--cream-50);
|
||||
border: 0; padding: 6px 10px; border-radius: 6px;
|
||||
font-family: var(--font-body); font-size: 11.5px; cursor: pointer;
|
||||
transition: background 120ms;
|
||||
}
|
||||
.cmd-card .copy:hover { background: rgba(245,241,232,0.20); }
|
||||
.install-card ol { padding-left: 20px; margin: 8px 0 0; color: var(--ink-700); font-size: 14.5px; line-height: 1.7; }
|
||||
.install-card ol code { font-family: var(--font-mono); font-size: 0.9em; padding: 2px 5px; background: var(--cream-200); border-radius: 4px; }
|
||||
|
||||
/* ---------- Footer ---------- */
|
||||
footer.site {
|
||||
background: var(--navy-950); color: var(--cream-300);
|
||||
padding: 56px 0 36px;
|
||||
border-top: 1px solid var(--gold-500);
|
||||
}
|
||||
footer.site .top { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 36px; margin-bottom: 36px; }
|
||||
footer.site .col h5 {
|
||||
font-family: var(--font-body); font-size: 11.5px; font-weight: 700;
|
||||
letter-spacing: 0.18em; text-transform: uppercase; color: var(--gold-400);
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
footer.site .col a {
|
||||
display: block; color: rgba(245,241,232,0.7); padding: 4px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
footer.site .col a:hover { color: var(--cream-50); }
|
||||
footer.site .brand-block { max-width: 320px; }
|
||||
footer.site .brand {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-family: var(--font-display); font-weight: 500; font-size: 15px; letter-spacing: 0.30em; text-transform: uppercase;
|
||||
color: var(--cream-50);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
footer.site .brand img { width: 32px; height: 32px; }
|
||||
footer.site .tag { font-size: 13.5px; line-height: 1.55; color: rgba(245,241,232,0.65); margin: 0; }
|
||||
footer.site .bottom {
|
||||
border-top: 1px solid rgba(245,241,232,0.10);
|
||||
padding-top: 24px;
|
||||
display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px;
|
||||
font-size: 12.5px; color: rgba(245,241,232,0.45);
|
||||
}
|
||||
footer.site .bottom a { color: rgba(245,241,232,0.6); }
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (max-width: 980px) {
|
||||
.hero-grid { grid-template-columns: 1fr; gap: 48px; }
|
||||
.cert { transform: none; max-width: 100%; margin: 0 auto; }
|
||||
.value-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.flow { grid-template-columns: repeat(2, 1fr); }
|
||||
.code-section, .sov, .install-grid { grid-template-columns: 1fr; }
|
||||
header.site nav { display: none; }
|
||||
.section-head h2 { font-size: 36px; }
|
||||
section.block { padding: 64px 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="site">
|
||||
<div class="wrap inner">
|
||||
<a class="brand" href="#top">
|
||||
<img src="../../assets/keysat-mark.svg" alt="">
|
||||
<span>Keysat</span>
|
||||
</a>
|
||||
<nav>
|
||||
<a href="#why">Why</a>
|
||||
<a href="#how">How it works</a>
|
||||
<a href="#integrate">Integrate</a>
|
||||
<a href="#sovereign">Sovereign</a>
|
||||
<a href="#install">Install</a>
|
||||
</nav>
|
||||
<a href="#install" class="btn primary cta">Install Keysat</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="hero" id="top">
|
||||
<div class="wrap hero-grid">
|
||||
<div>
|
||||
<div class="eyebrow">Software licensing for Bitcoin creators</div>
|
||||
<h1>Bitcoin-paid software licensing, <span class="gold">self-hosted</span> on Start9.</h1>
|
||||
<p class="lede">
|
||||
Buyers pay in Bitcoin via your own BTCPay. Your software verifies signed keys offline. You own the signing key, the customer list, and the payment rails — no SaaS, no middleman, no platform risk.
|
||||
</p>
|
||||
<div class="cta-row">
|
||||
<a class="btn primary lg" href="#install">Install Keysat <span class="arrow">→</span></a>
|
||||
<a class="btn secondary lg" href="#how">See how it works</a>
|
||||
</div>
|
||||
<div class="trust">
|
||||
<span><i data-lucide="server" style="width:14px;height:14px"></i> Runs on Start9</span>
|
||||
<span class="dot"></span>
|
||||
<span><i data-lucide="bitcoin" style="width:14px;height:14px"></i> Pays via BTCPay</span>
|
||||
<span class="dot"></span>
|
||||
<span><i data-lucide="wifi-off" style="width:14px;height:14px"></i> Verifies offline</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cert" role="img" aria-label="Sample license certificate">
|
||||
<div class="seal">₿</div>
|
||||
<div class="stamp">— Certificate of License —</div>
|
||||
<h4>Sundial 2.0</h4>
|
||||
<div class="sub">Issued under default policy · single seat · 1 year</div>
|
||||
<div class="field">License key</div>
|
||||
<div class="value">KS-9F2A-7C41-XK22-6D8E</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<div class="field">Issued</div>
|
||||
<div class="value" style="font-size: 13px">2026-04-22</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="field">Expires</div>
|
||||
<div class="value" style="font-size: 13px">2027-04-22</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig">Ed25519 · mz7q8r4t1v…h3k2pXq9wL · ✓ verified offline</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block tinted" id="why">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">What this enables</span>
|
||||
<h2>A complete sell-your-software stack, sovereign end-to-end.</h2>
|
||||
<p>Keysat handles the licensing layer. BTCPay handles payments. Your hardware holds the keys. No third party can mint, revoke, or read your sales records.</p>
|
||||
</div>
|
||||
<div class="value-grid">
|
||||
<div class="item">
|
||||
<div class="icon-wrap"><i data-lucide="zap"></i></div>
|
||||
<h3>Bitcoin payments, your store</h3><div class="accent-bar"></div>
|
||||
<p>BTCPay Server on your own Start9 takes the payment. Lightning settles in seconds. Funds go straight to your wallet — no intermediary holds them.</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon-wrap"><i data-lucide="key-round"></i></div>
|
||||
<h3>You own the signing key</h3><div class="accent-bar"></div>
|
||||
<p>The Ed25519 keypair lives on your hardware. Every license is signed by it. There's no third party who could mint or revoke licenses.</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon-wrap"><i data-lucide="wifi-off"></i></div>
|
||||
<h3>Offline verification</h3><div class="accent-bar"></div>
|
||||
<p>Your software verifies licenses against an embedded public key. No network call. Customer apps work even if your Keysat goes offline.</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon-wrap"><i data-lucide="ticket"></i></div>
|
||||
<h3>Trials, expiries, seats, entitlements</h3><div class="accent-bar"></div>
|
||||
<p>Per-product policies for time-limited licenses, multi-seat caps, trial flags, feature entitlements baked into the key.</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon-wrap"><i data-lucide="tag"></i></div>
|
||||
<h3>Discount & referral codes</h3><div class="accent-bar"></div>
|
||||
<p>Percent-off, fixed-sats-off, or free-license codes (no payment). Run launch promos, comp keys for press, track partner campaigns.</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon-wrap"><i data-lucide="wrench"></i></div>
|
||||
<h3>SDKs in your language</h3><div class="accent-bar"></div>
|
||||
<p>Rust, TypeScript, Python — wire-compatible offline verifiers. Five lines of code in your app and you're verifying real signatures.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block" id="how">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">How it works</span>
|
||||
<h2>Five steps, end to end.</h2>
|
||||
<p>From sideload to first sale in an afternoon. No cloud account to create, no API keys to copy.</p>
|
||||
</div>
|
||||
<ol class="flow">
|
||||
<li class="step"><div class="num">01</div><h3>Install on your Start9</h3><p>Sideload the <code>.s9pk</code>, or install from <code>registry.keysat.xyz</code>. BTCPay comes bundled as a dependency.</p></li>
|
||||
<li class="step"><div class="num">02</div><h3>Connect BTCPay</h3><p>One click in the StartOS Actions tab. Authorize once on BTCPay's consent page; Keysat registers a webhook automatically.</p></li>
|
||||
<li class="step"><div class="num">03</div><h3>Define products + policies</h3><p>Declare a product, set its price in sats, define a policy (duration, seat cap, trial, entitlements).</p></li>
|
||||
<li class="step"><div class="num">04</div><h3>Embed your public key</h3><p>Copy your Keysat public key into your app. Add the SDK. Five lines of code verifies a signature at startup.</p></li>
|
||||
<li class="step"><div class="num">05</div><h3>Share your purchase URL</h3><p>Buyers hit your public URL, pay in Bitcoin, get a signed license. Their copy of your software boots up licensed.</p></li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block tinted" id="integrate">
|
||||
<div class="wrap">
|
||||
<div class="code-section">
|
||||
<div class="pitch">
|
||||
<span class="eyebrow" style="color: var(--gold-700); font-size: 11.5px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase;">For developers</span>
|
||||
<h3 style="margin-top: 14px; font-size: 36px; line-height: 1.1; letter-spacing: -0.022em">Five lines, in the language you already write.</h3>
|
||||
<p>Keysat licenses are Ed25519-signed and base32-encoded. Verification is pure-function — no network, no daemon, no shared state.</p>
|
||||
<ul>
|
||||
<li>Wire-compatible across SDKs</li>
|
||||
<li>Public key embedded at compile time</li>
|
||||
<li>Returns product, policy, expiry, entitlements</li>
|
||||
<li>Source-available, easy to port</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="code-card">
|
||||
<div class="code-tabs">
|
||||
<button class="active" data-lang="ts">TypeScript</button>
|
||||
<button data-lang="rs">Rust</button>
|
||||
<button data-lang="py">Python</button>
|
||||
<span class="install" id="install-cmd">npm install @keysat/licensing-client</span>
|
||||
</div>
|
||||
<pre class="code" id="code-ts"><span class="k">import</span> { <span class="f">Verifier</span>, <span class="f">PublicKey</span> } <span class="k">from</span> <span class="s">'@keysat/licensing-client'</span>
|
||||
|
||||
<span class="k">const</span> verifier = <span class="k">new</span> <span class="f">Verifier</span>(
|
||||
<span class="f">PublicKey</span>.<span class="f">fromPem</span>(ISSUER_PEM)
|
||||
)
|
||||
|
||||
<span class="k">const</span> ok = verifier.<span class="f">verify</span>(licenseKeyFromUser)
|
||||
console.<span class="f">log</span>(<span class="s">'licensed:'</span>, ok.productId, ok.expires)</pre>
|
||||
<pre class="code" id="code-rs" style="display:none"><span class="c">// Cargo.toml</span>
|
||||
<span class="c">// licensing-client = "0.1"</span>
|
||||
|
||||
<span class="k">use</span> licensing_client::{<span class="f">Verifier</span>, <span class="f">PublicKeyPem</span>};
|
||||
|
||||
<span class="k">let</span> pk = <span class="f">PublicKeyPem</span>::from_str(ISSUER_PEM)<span class="p">?</span>;
|
||||
<span class="k">let</span> verifier = <span class="f">Verifier</span>::new(pk);
|
||||
<span class="k">let</span> ok = verifier.verify(&license_key)<span class="p">?</span>;
|
||||
println!(<span class="s">"licensed: {}"</span>, ok.product_id);</pre>
|
||||
<pre class="code" id="code-py" style="display:none"><span class="k">from</span> keysat_licensing_client <span class="k">import</span> Verifier, PublicKey
|
||||
|
||||
verifier = <span class="f">Verifier</span>(<span class="f">PublicKey</span>.<span class="f">from_pem</span>(ISSUER_PEM))
|
||||
ok = verifier.<span class="f">verify</span>(license_key_from_user)
|
||||
|
||||
<span class="k">print</span>(<span class="s">"licensed for"</span>, ok.product_id)</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block" id="sovereign">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Sovereign by default</span>
|
||||
<h2>Everything stays on your hardware.</h2>
|
||||
<p>If you migrate Start9 boxes, all of Keysat goes with you. If Keysat the project disappears, your existing licenses keep verifying — the public key is embedded in your software, the private key is on your machine.</p>
|
||||
</div>
|
||||
<div class="sov">
|
||||
<div class="panel">
|
||||
<h3>What you keep</h3>
|
||||
<div class="sub">On your Start9, in your normal backups.</div>
|
||||
<ul>
|
||||
<li>Signing keypair</li>
|
||||
<li>Customer email · npub list</li>
|
||||
<li>Sale records</li>
|
||||
<li>Audit log</li>
|
||||
<li>BTCPay invoice history</li>
|
||||
<li>Webhook subscribers</li>
|
||||
<li>Bitcoin (your wallet)</li>
|
||||
</ul>
|
||||
<p class="footnote">Backed up automatically by StartOS as part of your normal backup routine.</p>
|
||||
</div>
|
||||
<div class="panel dark">
|
||||
<h3>What's outside the box</h3>
|
||||
<div class="sub">Things you don't have to deal with.</div>
|
||||
<ul>
|
||||
<li class="no">Stripe</li>
|
||||
<li class="no">Gumroad</li>
|
||||
<li class="no">Paddle</li>
|
||||
<li class="no">Cryptlex</li>
|
||||
<li class="no">Keygen</li>
|
||||
<li class="no">LicenseSpring</li>
|
||||
<li class="no">SaaS subscription fees</li>
|
||||
<li class="no">Platform decisions about who you sell to</li>
|
||||
</ul>
|
||||
<p class="footnote">Source-available license · one-time payment in sats · ships with you when you migrate hardware.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block tinted" id="install">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Install</span>
|
||||
<h2>From the marketplace, or sideload directly.</h2>
|
||||
</div>
|
||||
<div class="install-grid">
|
||||
<div class="install-card featured">
|
||||
<span class="cap">Recommended</span>
|
||||
<h3>From the marketplace</h3>
|
||||
<p>Add the Keysat marketplace to your Start9, then click Install.</p>
|
||||
<div class="cmd-card">
|
||||
<span>https://registry.keysat.xyz</span>
|
||||
<button class="copy">Copy</button>
|
||||
</div>
|
||||
<p style="margin-top: 16px; font-size: 13.5px; color: var(--ink-500)">StartOS dashboard → Marketplace → Add → paste the URL.</p>
|
||||
</div>
|
||||
<div class="install-card">
|
||||
<span class="cap">Alternative</span>
|
||||
<h3>Sideload</h3>
|
||||
<p>If you'd rather not add the marketplace:</p>
|
||||
<ol>
|
||||
<li>Download <code>keysat_x86_64.s9pk</code> from <a href="#">GitHub releases</a>.</li>
|
||||
<li>StartOS dashboard → Sideload → drag the file in.</li>
|
||||
<li>Click Install.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="site">
|
||||
<div class="wrap">
|
||||
<div class="top">
|
||||
<div class="brand-block">
|
||||
<div class="brand"><img src="../../assets/keysat-mark.svg" alt=""><span>Keysat</span></div>
|
||||
<p class="tag">Software licensing for Bitcoin creators. Self-hosted, sovereign, source-available.</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5>Product</h5>
|
||||
<a href="#why">Why Keysat</a>
|
||||
<a href="#how">How it works</a>
|
||||
<a href="#install">Install</a>
|
||||
<a href="#">Marketplace</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5>Developers</h5>
|
||||
<a href="#integrate">Integration</a>
|
||||
<a href="#">SDKs</a>
|
||||
<a href="#">Wire format</a>
|
||||
<a href="#">GitHub</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h5>Contact</h5>
|
||||
<a href="mailto:licensing@keysat.xyz">licensing@keysat.xyz</a>
|
||||
<a href="#">Status</a>
|
||||
<a href="#">Changelog</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<span>© Keysat. Source-available; not open-source.</span>
|
||||
<span>Runs on Start9 · Pays via BTCPay · Verifies offline</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
// Tab switching for code samples
|
||||
const installCmds = {
|
||||
ts: 'npm install @keysat/licensing-client',
|
||||
rs: 'cargo add licensing-client',
|
||||
py: 'pip install keysat-licensing-client',
|
||||
};
|
||||
document.querySelectorAll('.code-tabs button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const lang = btn.dataset.lang;
|
||||
document.querySelectorAll('.code-tabs button').forEach(b => b.classList.toggle('active', b === btn));
|
||||
['ts','rs','py'].forEach(l => {
|
||||
document.getElementById('code-' + l).style.display = l === lang ? 'block' : 'none';
|
||||
});
|
||||
document.getElementById('install-cmd').textContent = installCmds[lang];
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user