Files
keysat-root/keysat-design-system/ui_kits/docs/index.html
T
Keysat 843ff0e5d7 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).
2026-06-12 17:51:40 -05:00

162 lines
9.7 KiB
HTML

<!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 &amp; policies</a>
<a href="#">Signing &amp; verification</a>
<a href="#">BTCPay webhooks</a>
<a href="#">Discounts &amp; 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 &amp; 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 &amp; 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 &amp; revocation</a>
</aside>
</div>
<script src="https://unpkg.com/lucide@latest"></script>
<script>lucide.createIcons();</script>
</body>
</html>