Docs polish: active-pill sync, license-sidebar bug fix, pricing standardized, ~70 em-dashes removed

- docs.js (new): sync sidebar .active pill with location.hash on load, click, and hashchange so in-page anchor links (Architecture, Discount codes, Backups, etc.) update the pill instead of leaving it stuck on whatever was statically marked
- Wire docs.js into every page just before </body>
- license.html: sidebar Project/Operate order matches every other page (Project first)
- pricing.html: rewritten to use the standard docs layout (full sidebar groups, prose main, breadcrumb) instead of a one-off shell that felt detached from the rest of the docs
- Reference section: remove Admin API + SDKs anchor links (they masqueraded as separate pages but just scrolled within integrate.html); Wire format stands alone
- Pricing copy: Zaprite reframed as "expanded payment options including card payment capabilities", "shipping in v0.3" removed (it shipped), Patron rephrased as perpetual (never expires or renews)
- "Toggling inactive" cap-evasion language replaced — admin UI exposes delete only, no soft-disable affordance for products
- ~70 em-dashes removed across 8 pages using a small pattern set (elaboration→period, list-intro→colon, tight clarification→comma, parentheticals→parens). Decorative stamp ornaments and references to actual third-party UI labels are kept verbatim.
This commit is contained in:
Keysat
2026-05-12 09:25:57 -05:00
parent 348a0b9f13
commit 87fd4f32e3
9 changed files with 285 additions and 220 deletions
+15 -16
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs Agent integration</title> <title>Keysat Docs: Agent integration</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css"> <link rel="stylesheet" href="docs.css">
</head> </head>
@@ -33,8 +33,6 @@
<div class="group"> <div class="group">
<div class="glabel">Reference</div> <div class="glabel">Reference</div>
<a href="wire-format.html">Wire format</a> <a href="wire-format.html">Wire format</a>
<a href="integrate.html#api">Admin API</a>
<a href="integrate.html#sdks">SDKs</a>
</div> </div>
<div class="group"> <div class="group">
<div class="glabel">Project</div> <div class="glabel">Project</div>
@@ -53,7 +51,7 @@
<div class="crumb">Get started · Agent integration</div> <div class="crumb">Get started · Agent integration</div>
<h1>Agent integration guide.</h1> <h1>Agent integration guide.</h1>
<p class="lead">How to build agents, bots, and automation that operate a Keysat instance. Keysat was designed from the start to be agent-friendly: the admin API uses plain HTTP + JSON with Bearer-token auth, an OpenAPI 3.1 spec drives discovery, scoped API keys grant least-privilege access without exposing the master credential, errors carry stable machine-readable codes, and webhooks let an agent react to events instead of polling.</p> <p class="lead">How to build agents, bots, and automation that operate a Keysat instance. Keysat was designed from the start to be agent-friendly: the admin API uses plain HTTP + JSON with Bearer-token auth, an OpenAPI 3.1 spec drives discovery, scoped API keys grant least-privilege access without exposing the master credential, errors carry stable machine-readable codes, and webhooks let an agent react to events instead of polling.</p>
<p>This guide covers the <em>operator side</em> of Keysat &mdash; running, configuring, and performing day-to-day operations. For the <em>buyer side</em> (validating licenses inside your app), see <a href="integrate.html">Integrate the SDK</a>.</p> <p>This guide covers the <em>operator side</em> of Keysat: running, configuring, and performing day-to-day operations. For the <em>buyer side</em> (validating licenses inside your app), see <a href="integrate.html">Integrate the SDK</a>.</p>
<h2 id="quick-start">Quick start</h2> <h2 id="quick-start">Quick start</h2>
<pre><code># 1. Discover the API surface <pre><code># 1. Discover the API surface
@@ -64,7 +62,7 @@ curl -X POST https://your-keysat-host/v1/admin/api-keys \
-H "Authorization: Bearer $MASTER_ADMIN_KEY" \ -H "Authorization: Bearer $MASTER_ADMIN_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"label":"Support bot","role":"support"}' -d '{"label":"Support bot","role":"support"}'
# Response includes `token: ks_...`. Save it — it's only shown once. # Response includes `token: ks_...`. Save it. It's only shown once.
# 3. Use the scoped key # 3. Use the scoped key
curl https://your-keysat-host/v1/admin/licenses?status=active \ curl https://your-keysat-host/v1/admin/licenses?status=active \
@@ -75,9 +73,9 @@ curl https://your-keysat-host/v1/admin/licenses?status=active \
<pre><code>Authorization: Bearer &lt;token&gt;</code></pre> <pre><code>Authorization: Bearer &lt;token&gt;</code></pre>
<p>Two kinds of tokens are accepted.</p> <p>Two kinds of tokens are accepted.</p>
<p><strong>Master admin API key</strong> &mdash; the env-configured <code>KEYSAT_ADMIN_API_KEY</code> (visible in StartOS Actions Show credentials on first install). Full access to every endpoint. This is the operator's credential. Don't hand it to agents.</p> <p><strong>Master admin API key</strong>: the env-configured <code>KEYSAT_ADMIN_API_KEY</code> (visible in StartOS Actions &rarr; Show credentials on first install). Full access to every endpoint. This is the operator's credential. Don't hand it to agents.</p>
<p><strong>Scoped API keys</strong> &mdash; additional tokens generated in admin UI Settings API keys. Each carries a role that bounds what it can do. Format: <code>ks_&lt;43 chars&gt;</code>. Operators can revoke any scoped key from the same UI; revoked tokens stop working immediately.</p> <p><strong>Scoped API keys</strong>: additional tokens generated in admin UI &rarr; Settings &rarr; API keys. Each carries a role that bounds what it can do. Format: <code>ks_&lt;43 chars&gt;</code>. Operators can revoke any scoped key from the same UI; revoked tokens stop working immediately.</p>
<h3>Role to scope mapping</h3> <h3>Role to scope mapping</h3>
<table> <table>
@@ -89,13 +87,13 @@ curl https://your-keysat-host/v1/admin/licenses?status=active \
<tr><td><code>full-admin</code></td><td>Every scope. Equivalent to the master key for most endpoints.</td></tr> <tr><td><code>full-admin</code></td><td>Every scope. Equivalent to the master key for most endpoints.</td></tr>
</tbody> </tbody>
</table> </table>
<p>Endpoints that touch settings (operator name, payment provider connections, self-license activation, scoped API key management) always require the master admin key. A <code>full-admin</code> scoped key cannot, for example, generate another scoped key &mdash; that's a self-defeating elevation path.</p> <p>Endpoints that touch settings (operator name, payment provider connections, self-license activation, scoped API key management) always require the master admin key. A <code>full-admin</code> scoped key cannot, for example, generate another scoped key. That's a self-defeating elevation path.</p>
<h2 id="discovery">Discovering the API</h2> <h2 id="discovery">Discovering the API</h2>
<p>Two complementary discovery mechanisms.</p> <p>Two complementary discovery mechanisms.</p>
<h3>OpenAPI 3.1 spec</h3> <h3>OpenAPI 3.1 spec</h3>
<p><code>GET /v1/openapi.json</code> &mdash; unauthenticated. Returns a curated spec covering the agent-relevant subset of endpoints. Use this with:</p> <p><code>GET /v1/openapi.json</code>. Unauthenticated. Returns a curated spec covering the agent-relevant subset of endpoints. Use this with:</p>
<ul> <ul>
<li><strong>OpenAI Custom GPTs</strong>: paste the URL as an Action.</li> <li><strong>OpenAI Custom GPTs</strong>: paste the URL as an Action.</li>
<li><strong>OpenAI Assistants / Functions</strong>: feed the spec to tool definition generators.</li> <li><strong>OpenAI Assistants / Functions</strong>: feed the spec to tool definition generators.</li>
@@ -177,7 +175,7 @@ curl https://your-keysat-host/v1/admin/licenses?status=active \
<h3>Find a license by email</h3> <h3>Find a license by email</h3>
<pre><code>curl "$KS/v1/admin/licenses?buyer_email=alice@example.com" \ <pre><code>curl "$KS/v1/admin/licenses?buyer_email=alice@example.com" \
-H "Authorization: Bearer ks_..."</code></pre> -H "Authorization: Bearer ks_..."</code></pre>
<p>Returns matching licenses (without the <code>license_key</code> field &mdash; that's only returned on issue / recover). Use the <code>id</code> for follow-up operations.</p> <p>Returns matching licenses (without the <code>license_key</code> field, which is only returned on issue / recover). Use the <code>id</code> for follow-up operations.</p>
<p><em>Scope required: <code>licenses:read</code>.</em></p> <p><em>Scope required: <code>licenses:read</code>.</em></p>
<h3>Cancel a buyer's subscription</h3> <h3>Cancel a buyer's subscription</h3>
@@ -209,7 +207,7 @@ curl -X POST $KS/v1/admin/subscriptions/$SUB_ID/cancel \
<p>Always applies as comp (no invoice) from the admin path. Buyer-initiated paid upgrades go through <code>/v1/upgrade</code> (different endpoint, signed-license auth).</p> <p>Always applies as comp (no invoice) from the admin path. Buyer-initiated paid upgrades go through <code>/v1/upgrade</code> (different endpoint, signed-license auth).</p>
<p><em>Scope required: <code>licenses:write</code>.</em></p> <p><em>Scope required: <code>licenses:write</code>.</em></p>
<h2 id="webhooks">Webhooks &mdash; react to events instead of polling</h2> <h2 id="webhooks">Webhooks: react to events instead of polling</h2>
<p>Configure webhook endpoints in admin UI → Webhooks. The daemon POSTs JSON payloads, HMAC-SHA256 signed with the endpoint's secret, on these events:</p> <p>Configure webhook endpoints in admin UI → Webhooks. The daemon POSTs JSON payloads, HMAC-SHA256 signed with the endpoint's secret, on these events:</p>
<table> <table>
<thead><tr><th>Event</th><th>Fires on</th></tr></thead> <thead><tr><th>Event</th><th>Fires on</th></tr></thead>
@@ -238,21 +236,21 @@ def verify(body_bytes: bytes, signature_header: str, secret: str) -> bool:
<p>A few patterns that work well in practice.</p> <p>A few patterns that work well in practice.</p>
<h3>Idempotency</h3> <h3>Idempotency</h3>
<p>The daemon's mutation endpoints are idempotent where they can be. Revoke, suspend, unsuspend, archive, unarchive, subscription cancel &mdash; all return success on the second call without changing state. Your agent can safely retry on network errors.</p> <p>The daemon's mutation endpoints are idempotent where they can be. Revoke, suspend, unsuspend, archive, unarchive, subscription cancel. All return success on the second call without changing state. Your agent can safely retry on network errors.</p>
<h3>Pagination</h3> <h3>Pagination</h3>
<p>List endpoints return up to ~100 rows by default. Use <code>?limit=N</code> and <code>?offset=N</code> for larger result sets. The OpenAPI spec documents the limits per endpoint.</p> <p>List endpoints return up to ~100 rows by default. Use <code>?limit=N</code> and <code>?offset=N</code> for larger result sets. The OpenAPI spec documents the limits per endpoint.</p>
<h3>Rate limits</h3> <h3>Rate limits</h3>
<p>The admin endpoints have no per-IP rate limit today &mdash; operators are trusted. The public endpoints (<code>/v1/validate</code>, <code>/v1/recover</code>) are rate-limited per client IP (10/min for <code>/recover</code>; <code>/validate</code> is unlimited but a reasonable agent calls it once per app boot + once per hour).</p> <p>The admin endpoints have no per-IP rate limit today. Operators are trusted. The public endpoints (<code>/v1/validate</code>, <code>/v1/recover</code>) are rate-limited per client IP (10/min for <code>/recover</code>; <code>/validate</code> is unlimited but a reasonable agent calls it once per app boot + once per hour).</p>
<h3>Master key handling</h3> <h3>Master key handling</h3>
<p>If your automation needs <code>full-admin</code> because it touches operator-only operations (creating other API keys, changing payment providers), use the master key from a secret manager. If it can stay within license / product / policy operations, <strong>always use a scoped key</strong>. Operators can revoke a compromised scoped key without rotating the master credential.</p> <p>If your automation needs <code>full-admin</code> because it touches operator-only operations (creating other API keys, changing payment providers), use the master key from a secret manager. If it can stay within license / product / policy operations, <strong>always use a scoped key</strong>. Operators can revoke a compromised scoped key without rotating the master credential.</p>
<h3>Backoff on 5xx</h3> <h3>Backoff on 5xx</h3>
<p><code>internal_error</code> (500) is a bug or a transient DB lock. Retry with exponential backoff (1s, 2s, 4s, 8s, give up). Don't retry on 4xx &mdash; those are deterministic client errors.</p> <p><code>internal_error</code> (500) is a bug or a transient DB lock. Retry with exponential backoff (1s, 2s, 4s, 8s, give up). Don't retry on 4xx. Those are deterministic client errors.</p>
<h2 id="recipe">Concrete recipe &mdash; "Comp a license to anyone who emails support@"</h2> <h2 id="recipe">Concrete recipe: "Comp a license to anyone who emails support@"</h2>
<pre><code>import os, requests, imaplib, email <pre><code>import os, requests, imaplib, email
KS = os.environ["KEYSAT_URL"] KS = os.environ["KEYSAT_URL"]
@@ -274,7 +272,7 @@ def issue_comp_license(buyer_email: str, product_slug: str, reason: str) -> str:
return r.json()["license_key"] return r.json()["license_key"]
# Poll IMAP, parse incoming requests, call issue_comp_license, reply with the key</code></pre> # Poll IMAP, parse incoming requests, call issue_comp_license, reply with the key</code></pre>
<p>That's the entire pattern. The agent doesn't need full admin &mdash; just the license-issuer role. If it ever gets compromised, you revoke the scoped key in the admin UI and generate a new one in 30 seconds.</p> <p>That's the entire pattern. The agent doesn't need full admin, just the license-issuer role. If it ever gets compromised, you revoke the scoped key in the admin UI and generate a new one in 30 seconds.</p>
<h2 id="not-exposed">What's NOT exposed to agents</h2> <h2 id="not-exposed">What's NOT exposed to agents</h2>
<p>Some operations are deliberately operator-only and not accessible to any scoped key, including <code>full-admin</code>:</p> <p>Some operations are deliberately operator-only and not accessible to any scoped key, including <code>full-admin</code>:</p>
@@ -306,5 +304,6 @@ def issue_comp_license(buyer_email: str, product_slug: str, reason: str) -> str:
</aside> </aside>
</div> </div>
<script src="docs.js"></script>
</body> </body>
</html> </html>
+49
View File
@@ -0,0 +1,49 @@
// Shared docs-site behaviors. Loaded at the bottom of every page.
//
// Sidebar active-pill sync: the .active class on sidebar links is
// baked into each HTML file as a baseline (page-level link only),
// which means clicking an in-page anchor link (e.g. "Discount codes"
// → index.html#discounts) leaves the pill stuck on whatever the
// statically-marked page link was. This script keeps the active
// state in sync with the current URL hash so the pill follows
// what the user clicked.
(function () {
var sidebarLinks = Array.prototype.slice.call(
document.querySelectorAll('aside.side a')
);
if (!sidebarLinks.length) return;
var currentFile = (location.pathname.split('/').pop() || 'index.html');
function findActive(hash) {
var desired = hash ? (currentFile + hash) : currentFile;
var match = null;
sidebarLinks.forEach(function (a) {
if (a.getAttribute('href') === desired) match = a;
});
if (match) return match;
// Fallback: bare current-file link when no hash matches
sidebarLinks.forEach(function (a) {
if (a.getAttribute('href') === currentFile) match = a;
});
return match;
}
function setActive(link) {
if (!link) return;
sidebarLinks.forEach(function (a) { a.classList.remove('active'); });
link.classList.add('active');
}
setActive(findActive(location.hash));
window.addEventListener('hashchange', function () {
setActive(findActive(location.hash));
});
sidebarLinks.forEach(function (a) {
a.addEventListener('click', function () {
setActive(a);
});
});
})();
+20 -21
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs Introduction</title> <title>Keysat Docs: Introduction</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css"> <link rel="stylesheet" href="docs.css">
</head> </head>
@@ -33,8 +33,6 @@
<div class="group"> <div class="group">
<div class="glabel">Reference</div> <div class="glabel">Reference</div>
<a href="wire-format.html">Wire format</a> <a href="wire-format.html">Wire format</a>
<a href="integrate.html#api">Admin API</a>
<a href="integrate.html#sdks">SDKs</a>
</div> </div>
<div class="group"> <div class="group">
<div class="glabel">Project</div> <div class="glabel">Project</div>
@@ -52,8 +50,8 @@
<main class="prose"> <main class="prose">
<div class="crumb">Get started · Introduction</div> <div class="crumb">Get started · Introduction</div>
<h1>Welcome to Keysat.</h1> <h1>Welcome to Keysat.</h1>
<p class="lead">Keysat lets independent software creators sell their work on their own terms. You ship software open source, closed source, free / paid versions, whatever fits and Keysat handles the buy page, payment via BTCPay, and a signed license for each buyer.</p> <p class="lead">Keysat lets independent software creators sell their work on their own terms. You ship software (open source, closed source, free / paid versions, whatever fits), and Keysat handles the buy page, payment via BTCPay, and a signed license for each buyer.</p>
<p>How you use that license inside your software is up to you: a one-time purchase to unlock the whole app, a free + paid split with specific paid features, a tip-jar style supporter badge all legitimate. The licensing layer is a primitive, not a script.</p> <p>How you use that license inside your software is up to you: a one-time purchase to unlock the whole app, a free + paid split with specific paid features, a tip-jar style supporter badge: all legitimate. The licensing layer is a primitive, not a script.</p>
<p>These docs cover both ends:</p> <p>These docs cover both ends:</p>
@@ -78,9 +76,9 @@
<h2 id="architecture">Architecture</h2> <h2 id="architecture">Architecture</h2>
<p>Keysat is the licensing layer sitting on top of your existing payments stack. Three boxes:</p> <p>Keysat is the licensing layer sitting on top of your existing payments stack. Three boxes:</p>
<ul> <ul>
<li><strong>BTCPay Server</strong> &mdash; takes the payment. On-chain Bitcoin or Lightning, settling to your wallet. Lives on your Start9.</li> <li><strong>BTCPay Server</strong>: takes the payment. On-chain Bitcoin or Lightning, settling to your wallet. Lives on your Start9.</li>
<li><strong>Keysat</strong> &mdash; your private licensing service. Holds the Ed25519 signing key. Hosts the public purchase URLs at <code>/buy/&lt;product&gt;</code>. Listens for BTCPay payment webhooks and issues a signed license on each settlement. Lives on your Start9.</li> <li><strong>Keysat</strong>: your private licensing service. Holds the Ed25519 signing key. Hosts the public purchase URLs at <code>/buy/&lt;product&gt;</code>. Listens for BTCPay payment webhooks and issues a signed license on each settlement. Lives on your Start9.</li>
<li><strong>Your software</strong> &mdash; the thing you sell. Ships with the Keysat <em>public</em> key embedded at compile time. On startup it reads the user&rsquo;s license and verifies the signature offline. No network call.</li> <li><strong>Your software</strong>: the thing you sell. Ships with the Keysat <em>public</em> key embedded at compile time. On startup it reads the user&rsquo;s license and verifies the signature offline. No network call.</li>
</ul> </ul>
<p>The key word is <em>offline</em>. Once a license is issued, your software does not need to phone home to verify it. The verification is a pure function of the license bytes and the public key. This is the same model used by signed JWTs, except wrapped in a small fixed-width format that&rsquo;s comfortable to print on a receipt.</p> <p>The key word is <em>offline</em>. Once a license is issued, your software does not need to phone home to verify it. The verification is a pure function of the license bytes and the public key. This is the same model used by signed JWTs, except wrapped in a small fixed-width format that&rsquo;s comfortable to print on a receipt.</p>
@@ -92,7 +90,7 @@
<h2 id="products-policies">Products &amp; policies</h2> <h2 id="products-policies">Products &amp; policies</h2>
<p>You declare two things in Keysat: products and policies.</p> <p>You declare two things in Keysat: products and policies.</p>
<p>A <strong>product</strong> is the thing you sell &mdash; "Bitcoin Ticker Pro", "Aurora Plugin", whatever. It has a slug, a display name, a description, and a price (sats / USD / EUR). Each product also carries an <strong>entitlements catalog</strong> &mdash; the typed list of feature slugs your software cares about, plus their display names and descriptions. Policies pick entitlements from this catalog.</p> <p>A <strong>product</strong> is the thing you sell: "Bitcoin Ticker Pro", "Aurora Plugin", whatever. It has a slug, a display name, a description, and a price (sats / USD / EUR). Each product also carries an <strong>entitlements catalog</strong>: the typed list of feature slugs your software cares about, plus their display names and descriptions. Policies pick entitlements from this catalog.</p>
<p>A <strong>policy</strong> is a license template attached to a product. It specifies:</p> <p>A <strong>policy</strong> is a license template attached to a product. It specifies:</p>
<table class="t"> <table class="t">
@@ -104,13 +102,13 @@
<tr><td><code>is_trial</code></td><td>Sets a <code>TRIAL</code> bit so your app can show a "trial" banner.</td></tr> <tr><td><code>is_trial</code></td><td>Sets a <code>TRIAL</code> bit so your app can show a "trial" banner.</td></tr>
<tr><td><code>is_recurring</code> + <code>renewal_period_days</code></td><td>Auto-renew on a cycle (weekly / monthly / annual / custom). The daemon mints a fresh invoice + signed license per cycle.</td></tr> <tr><td><code>is_recurring</code> + <code>renewal_period_days</code></td><td>Auto-renew on a cycle (weekly / monthly / annual / custom). The daemon mints a fresh invoice + signed license per cycle.</td></tr>
<tr><td><code>entitlements</code></td><td>Subset of the product&rsquo;s catalog this policy grants. Baked into the signed license.</td></tr> <tr><td><code>entitlements</code></td><td>Subset of the product&rsquo;s catalog this policy grants. Baked into the signed license.</td></tr>
<tr><td><code>metadata.marketing_bullets</code></td><td>Operator-authored ✓ items rendered on the buy-page tier card. Pure marketing copy &mdash; not enforced.</td></tr> <tr><td><code>metadata.marketing_bullets</code></td><td>Operator-authored ✓ items rendered on the buy-page tier card. Pure marketing copy. Not enforced.</td></tr>
<tr><td><code>metadata.hidden_entitlements</code></td><td>Slugs the license still grants but the buy-page card hides &mdash; useful when a higher tier uses "Everything in X, plus:" copy and doesn&rsquo;t want to repeat implied entitlements.</td></tr> <tr><td><code>metadata.hidden_entitlements</code></td><td>Slugs the license still grants but the buy-page card hides; useful when a higher tier uses "Everything in X, plus:" copy and doesn&rsquo;t want to repeat implied entitlements.</td></tr>
</tbody> </tbody>
</table> </table>
<p>A product can have <strong>one policy or many</strong>. Multi-tier ladders (think Basic / Pro / Max) are first-class: when a product has two or more public policies, the buy page renders a tier picker and the buyer chooses before paying. The displayed tier is selected from a <code>?policy=&lt;slug&gt;</code> URL hint, then the <code>highlighted</code> ("most popular") policy if any, then the cheapest. Tier ordering on the picker is operator-controlled via drag-and-drop in the admin UI (or <code>tier_rank</code> in the API).</p> <p>A product can have <strong>one policy or many</strong>. Multi-tier ladders (think Basic / Pro / Max) are first-class: when a product has two or more public policies, the buy page renders a tier picker and the buyer chooses before paying. The displayed tier is selected from a <code>?policy=&lt;slug&gt;</code> URL hint, then the <code>highlighted</code> ("most popular") policy if any, then the cheapest. Tier ordering on the picker is operator-controlled via drag-and-drop in the admin UI (or <code>tier_rank</code> in the API).</p>
<p>You can also attach <strong>private policies</strong> for manual issuance &mdash; e.g. a longer-duration "Lifetime" comp for conferences, a richer-entitlement "Internal" tier for support cases. Private policies don&rsquo;t appear on the buy page; the admin API issues them directly.</p> <p>You can also attach <strong>private policies</strong> for manual issuance, e.g. a longer-duration "Lifetime" comp for conferences, a richer-entitlement "Internal" tier for support cases. Private policies don&rsquo;t appear on the buy page; the admin API issues them directly.</p>
<h2 id="discounts">Discount codes</h2> <h2 id="discounts">Discount codes</h2>
<p>Four kinds:</p> <p>Four kinds:</p>
@@ -125,34 +123,34 @@
</tbody> </tbody>
</table> </table>
<p>Codes can be capped at N uses, dated to expire, restricted to one product (and optionally to a <strong>subset of policies</strong> on that product &mdash; e.g. "applies to Pro and Max but not Basic"), and tagged with a referrer label so you can see which campaign drove which sales in the audit log.</p> <p>Codes can be capped at N uses, dated to expire, restricted to one product (and optionally to a <strong>subset of policies</strong> on that product, e.g. "applies to Pro and Max but not Basic"), and tagged with a referrer label so you can see which campaign drove which sales in the audit log.</p>
<p>Codes can also be marked <strong>featured</strong> &mdash; a "launch special" mode. A featured code:</p> <p>Codes can also be marked <strong>featured</strong>: a "launch special" mode. A featured code:</p>
<ul> <ul>
<li>Renders a diagonal "LAUNCH SPECIAL" ribbon + struck-through original price on the matching tier cards on the buy page.</li> <li>Renders a diagonal "LAUNCH SPECIAL" ribbon + struck-through original price on the matching tier cards on the buy page.</li>
<li>Auto-applies for buyers who don&rsquo;t type any code, with the input pre-filled so they can see what&rsquo;s been applied.</li> <li>Auto-applies for buyers who don&rsquo;t type any code, with the input pre-filled so they can see what&rsquo;s been applied.</li>
<li>Stops surfacing once it hits its <code>max_uses</code> cap or expires &mdash; the ribbon disappears and pricing reverts to standard automatically.</li> <li>Stops surfacing once it hits its <code>max_uses</code> cap or expires: the ribbon disappears and pricing reverts to standard automatically.</li>
</ul> </ul>
<p>Operator-typed codes always take precedence: a buyer who pastes a non-featured code in the form gets that code instead of the auto-applied featured one.</p> <p>Operator-typed codes always take precedence: a buyer who pastes a non-featured code in the form gets that code instead of the auto-applied featured one.</p>
<h2 id="revocation">Revocation strategy</h2> <h2 id="revocation">Revocation strategy</h2>
<p>This is the one piece of the architecture that requires a design decision from you.</p> <p>This is the one piece of the architecture that requires a design decision from you.</p>
<p>Because verification is offline, a license that was once issued continues to verify forever &mdash; even if you mark it as revoked in the admin UI. The verifier in your app doesn&rsquo;t know about your admin actions.</p> <p>Because verification is offline, a license that was once issued continues to verify forever, even if you mark it as revoked in the admin UI. The verifier in your app doesn&rsquo;t know about your admin actions.</p>
<p>You have three options:</p> <p>You have three options:</p>
<ul> <ul>
<li><strong>Don&rsquo;t support revocation at all.</strong> Many indie developers do this. Once a key is sold, it stays valid. Refunds are still possible &mdash; you send sats back via BTCPay; the key still works but the customer agreed to stop using it.</li> <li><strong>Don&rsquo;t support revocation at all.</strong> Many indie developers do this. Once a key is sold, it stays valid. Refunds are still possible. You send sats back via BTCPay; the key still works but the customer agreed to stop using it.</li>
<li><strong>Periodic online check.</strong> Your app fetches a small revocation list from your Keysat (or a CDN you point at it) once a week / month. Adds a "soft-online" requirement.</li> <li><strong>Periodic online check.</strong> Your app fetches a small revocation list from your Keysat (or a CDN you point at it) once a week / month. Adds a "soft-online" requirement.</li>
<li><strong>Short-lived licenses with renewal.</strong> Issue 30-day licenses; the app fetches a fresh signed token before expiry. Recurring renewals are first-class in v0.2 &mdash; define a policy with <code>is_recurring=true</code> + <code>renewal_period_days</code> and Keysat handles the cycle (invoice → settle → re-sign → webhook).</li> <li><strong>Short-lived licenses with renewal.</strong> Issue 30-day licenses; the app fetches a fresh signed token before expiry. Recurring renewals are first-class in v0.2: define a policy with <code>is_recurring=true</code> + <code>renewal_period_days</code> and Keysat handles the cycle (invoice → settle → re-sign → webhook).</li>
</ul> </ul>
<div class="callout"> <div class="callout">
<i data-lucide="key-round"></i> <i data-lucide="key-round"></i>
<p><strong>You decide the policy.</strong> Keysat doesn&rsquo;t force a particular revocation model. The default is no revocation &mdash; that&rsquo;s the simplest, sovereign-by-default choice. If you need stronger guarantees, layer them on with the patterns above.</p> <p><strong>You decide the policy.</strong> Keysat doesn&rsquo;t force a particular revocation model. The default is no revocation. That&rsquo;s the simplest, sovereign-by-default choice. If you need stronger guarantees, layer them on with the patterns above.</p>
</div> </div>
<h2 id="operator-tiers">Operator tiers</h2> <h2 id="operator-tiers">Operator tiers</h2>
<p>Keysat itself ships under a tiered self-license. The daemon runs out of the box at the free <strong>Creator</strong> tier with caps that are generous for a solo developer; paid <strong>Pro</strong> and <strong>Patron</strong> tiers lift caps and unlock recurring billing + the Zaprite payment gateway. Caps are enforced by the daemon at create-time only &mdash; existing resources are always grandfathered if you downgrade.</p> <p>Keysat itself ships under a tiered self-license. The daemon runs out of the box at the free <strong>Creator</strong> tier with caps that are generous for a solo developer; paid <strong>Pro</strong> and <strong>Patron</strong> tiers lift caps and unlock recurring billing + the Zaprite payment gateway. Caps are enforced by the daemon at create-time only; existing resources are always grandfathered if you downgrade.</p>
<p>As of this writing, Creator caps at <strong>5 products / 5 policies per product / 10 active discount codes</strong>, and Pro / Patron are unlimited. The exact tier list, prices, entitlements, and any active launch-special discount are operator-controlled on the master Keysat and may change &mdash; the canonical sources are:</p> <p>As of this writing, Creator caps at <strong>5 products / 5 policies per product / 10 active discount codes</strong>, and Pro / Patron are unlimited. The exact tier list, prices, entitlements, and any active launch-special discount are operator-controlled on the master Keysat and may change. The canonical sources are:</p>
<ul> <ul>
<li>The live tier cards on <a href="https://keysat.xyz#tiers">keysat.xyz</a> (rendered dynamically from the master Keysat).</li> <li>The live tier cards on <a href="https://keysat.xyz#tiers">keysat.xyz</a> (rendered dynamically from the master Keysat).</li>
<li>The <a href="pricing.html">pricing page</a> on these docs for the human-readable breakdown.</li> <li>The <a href="pricing.html">pricing page</a> on these docs for the human-readable breakdown.</li>
@@ -193,5 +191,6 @@
<script src="https://unpkg.com/lucide@latest"></script> <script src="https://unpkg.com/lucide@latest"></script>
<script>lucide.createIcons();</script> <script>lucide.createIcons();</script>
<script src="docs.js"></script>
</body> </body>
</html> </html>
+20 -21
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs Install &amp; setup</title> <title>Keysat Docs: Install &amp; setup</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css"> <link rel="stylesheet" href="docs.css">
</head> </head>
@@ -33,8 +33,6 @@
<div class="group"> <div class="group">
<div class="glabel">Reference</div> <div class="glabel">Reference</div>
<a href="wire-format.html">Wire format</a> <a href="wire-format.html">Wire format</a>
<a href="integrate.html#api">Admin API</a>
<a href="integrate.html#sdks">SDKs</a>
</div> </div>
<div class="group"> <div class="group">
<div class="glabel">Project</div> <div class="glabel">Project</div>
@@ -61,7 +59,7 @@
<li>About 2 GB of free disk for Keysat itself; BTCPay&rsquo;s requirements are larger and depend on your Bitcoin node mode.</li> <li>About 2 GB of free disk for Keysat itself; BTCPay&rsquo;s requirements are larger and depend on your Bitcoin node mode.</li>
</ul> </ul>
<h2 id="install">Step 1 Install Keysat</h2> <h2 id="install">Step 1: Install Keysat</h2>
<p>Two ways. Either gets you to the same place.</p> <p>Two ways. Either gets you to the same place.</p>
<h3>Option A: from the Keysat marketplace (recommended)</h3> <h3>Option A: from the Keysat marketplace (recommended)</h3>
@@ -80,11 +78,11 @@
<p>BTCPay Server is declared as a required dependency. If you don&rsquo;t have it installed yet, StartOS will prompt you to install it as part of the same flow.</p> <p>BTCPay Server is declared as a required dependency. If you don&rsquo;t have it installed yet, StartOS will prompt you to install it as part of the same flow.</p>
<h2 id="operator-name">Step 2 Set your operator name</h2> <h2 id="operator-name">Step 2: Set your operator name</h2>
<p>Open the Keysat service page in StartOS. Go to <strong>Actions &rarr; Set operator name</strong>. Pick a short label that identifies <em>you</em> as the seller &mdash; e.g. "aurora-software", "northpath", "my-name". This shows up on the public purchase pages and in the audit log.</p> <p>Open the Keysat service page in StartOS. Go to <strong>Actions &rarr; Set operator name</strong>. Pick a short label that identifies <em>you</em> as the seller, e.g. "aurora-software", "northpath", "my-name". This shows up on the public purchase pages and in the audit log.</p>
<p>This change is live-reloaded; you don&rsquo;t need to restart the service.</p> <p>This change is live-reloaded; you don&rsquo;t need to restart the service.</p>
<h2 id="connect-btcpay">Step 3 Connect BTCPay</h2> <h2 id="connect-btcpay">Step 3: Connect BTCPay</h2>
<p>Make sure BTCPay Server is running and has at least one <strong>store</strong> with a configured <strong>payment method</strong> (on-chain wallet or Lightning node). Without a payment method, BTCPay will reject Keysat&rsquo;s invoice creation.</p> <p>Make sure BTCPay Server is running and has at least one <strong>store</strong> with a configured <strong>payment method</strong> (on-chain wallet or Lightning node). Without a payment method, BTCPay will reject Keysat&rsquo;s invoice creation.</p>
<p>In Keysat&rsquo;s service page, click <strong>Actions &rarr; Connect BTCPay</strong>. You&rsquo;ll be redirected to BTCPay&rsquo;s authorize page, where you grant Keysat the permissions it needs:</p> <p>In Keysat&rsquo;s service page, click <strong>Actions &rarr; Connect BTCPay</strong>. You&rsquo;ll be redirected to BTCPay&rsquo;s authorize page, where you grant Keysat the permissions it needs:</p>
@@ -117,7 +115,7 @@ payment_methods: <span class="s">[BTC-OnChain, BTC-LightningNetwork]</span></pre
<p>If <code>payment_methods</code> is empty, head back to BTCPay and configure at least one before continuing.</p> <p>If <code>payment_methods</code> is empty, head back to BTCPay and configure at least one before continuing.</p>
<h2 id="admin-key">Step 4 Get your admin API key</h2> <h2 id="admin-key">Step 4: Get your admin API key</h2>
<p>Go to <strong>Actions &rarr; Show admin API key</strong>. This reveals the 64-hex-character key that gates all <code>/v1/admin/*</code> endpoints, including the admin UI.</p> <p>Go to <strong>Actions &rarr; Show admin API key</strong>. This reveals the 64-hex-character key that gates all <code>/v1/admin/*</code> endpoints, including the admin UI.</p>
<div class="callout warn"> <div class="callout warn">
@@ -125,44 +123,44 @@ payment_methods: <span class="s">[BTC-OnChain, BTC-LightningNetwork]</span></pre
<p><strong>Treat this key like a password.</strong> Anyone with it can issue, revoke, or read every license you&rsquo;ve ever sold. Don&rsquo;t paste it into Slack. Don&rsquo;t check it into Git.</p> <p><strong>Treat this key like a password.</strong> Anyone with it can issue, revoke, or read every license you&rsquo;ve ever sold. Don&rsquo;t paste it into Slack. Don&rsquo;t check it into Git.</p>
</div> </div>
<h2 id="admin-ui">Step 5 Open the admin UI</h2> <h2 id="admin-ui">Step 5: Open the admin UI</h2>
<p>Click the <strong>Launch UI</strong> button on Keysat&rsquo;s service page. (StartOS surfaces this for any service that defines a <code>type: 'ui'</code> interface.) Paste the admin key from the previous step into the sign-in form.</p> <p>Click the <strong>Launch UI</strong> button on Keysat&rsquo;s service page. (StartOS surfaces this for any service that defines a <code>type: 'ui'</code> interface.) Paste the admin key from the previous step into the sign-in form.</p>
<p>From here on, you mostly work in the admin UI. The StartOS Actions tab is reserved for setup-only operations (operator name, BTCPay connect/disconnect/check, show admin key).</p> <p>From here on, you mostly work in the admin UI. The StartOS Actions tab is reserved for setup-only operations (operator name, BTCPay connect/disconnect/check, show admin key).</p>
<h2 id="first-product">Step 6 Define your first product</h2> <h2 id="first-product">Step 6: Define your first product</h2>
<p>In the admin UI, go to <strong>Products &rarr; Create a new product</strong> and fill in:</p> <p>In the admin UI, go to <strong>Products &rarr; Create a new product</strong> and fill in:</p>
<ul> <ul>
<li><strong>Slug</strong> &mdash; lowercase, hyphens, will appear in the public URL. e.g. <code>bitcoin-ticker-pro</code>.</li> <li><strong>Slug</strong>: lowercase, hyphens, will appear in the public URL. e.g. <code>bitcoin-ticker-pro</code>.</li>
<li><strong>Display name</strong> &mdash; shown on the buyer&rsquo;s purchase page and on receipts.</li> <li><strong>Display name</strong>: shown on the buyer&rsquo;s purchase page and on receipts.</li>
<li><strong>Description</strong> &mdash; one or two sentences; rendered as plain text.</li> <li><strong>Description</strong>: one or two sentences; rendered as plain text.</li>
<li><strong>Price</strong> &mdash; the currency picker accepts sats, USD, or EUR. For sats, enter an integer (e.g. <code>50000</code>). For USD/EUR, enter the amount in dollars/euros Keysat converts to BTC at invoice creation and the buyer pays the locked-in BTC amount.</li> <li><strong>Price</strong>: the currency picker accepts sats, USD, or EUR. For sats, enter an integer (e.g. <code>50000</code>). For USD/EUR, enter the amount in dollars/euros. Keysat converts to BTC at invoice creation and the buyer pays the locked-in BTC amount.</li>
</ul> </ul>
<p>The product is created with no policies attached. Next:</p> <p>The product is created with no policies attached. Next:</p>
<h2 id="first-policy">Step 7 Define one or more policies</h2> <h2 id="first-policy">Step 7: Define one or more policies</h2>
<p>Go to <strong>Policies &rarr; Create a new policy</strong>. Pick the product, then fill in:</p> <p>Go to <strong>Policies &rarr; Create a new policy</strong>. Pick the product, then fill in:</p>
<ul> <ul>
<li><strong>Slug</strong> &mdash; lowercase id (e.g. <code>basic</code>, <code>pro</code>, <code>annual</code>). Not "special" in any way; the buy page renders a tier picker when a product has two or more public policies, with the initial tier chosen by <code>?policy=&lt;slug&gt;</code> in the URL, then by the policy you mark "most popular", then by cheapest.</li> <li><strong>Slug</strong>: lowercase id (e.g. <code>basic</code>, <code>pro</code>, <code>annual</code>). Not "special" in any way; the buy page renders a tier picker when a product has two or more public policies, with the initial tier chosen by <code>?policy=&lt;slug&gt;</code> in the URL, then by the policy you mark "most popular", then by cheapest.</li>
<li><strong>Duration</strong>. Common choices: perpetual, 30 days (trial), 1 year. Recurring subscriptions are a separate toggle on the same form &mdash; flip "Recurring subscription" + set a renewal cadence and Keysat handles the cycle (invoice → settle → re-sign) automatically.</li> <li><strong>Duration</strong>. Common choices: perpetual, 30 days (trial), 1 year. Recurring subscriptions are a separate toggle on the same form. Flip "Recurring subscription" + set a renewal cadence and Keysat handles the cycle (invoice &rarr; settle &rarr; re-sign) automatically.</li>
<li><strong>Max devices</strong>. <code>1</code> for single-seat, <code>0</code> for unlimited.</li> <li><strong>Max devices</strong>. <code>1</code> for single-seat, <code>0</code> for unlimited.</li>
<li><strong>Entitlements</strong> &mdash; pick from the product's catalog (you set up the catalog when you created the product on the previous step). The picked entitlements are baked into the signed license and your app reads them at verify time. Optionally toggle the "hide on buy page" eye icon on any entitlement to drop it from the tier card without un-granting it &mdash; useful for higher tiers that use "Everything in Basic, plus:" marketing copy.</li> <li><strong>Entitlements</strong>: pick from the product's catalog (you set up the catalog when you created the product on the previous step). The picked entitlements are baked into the signed license and your app reads them at verify time. Optionally toggle the "hide on buy page" eye icon on any entitlement to drop it from the tier card without un-granting it. Useful for higher tiers that use "Everything in Basic, plus:" marketing copy.</li>
<li><strong>Marketing bullets</strong> (optional) &mdash; operator-authored ✓ items rendered on the tier card alongside the entitlements. Pure marketing copy, not enforced.</li> <li><strong>Marketing bullets</strong> (optional): operator-authored ✓ items rendered on the tier card alongside the entitlements. Pure marketing copy, not enforced.</li>
</ul> </ul>
<p>If you're selling a multi-tier product (e.g. Basic / Pro / Max), repeat this step for each tier. Drag the cards in the Policies grid to set the order shown to buyers.</p> <p>If you're selling a multi-tier product (e.g. Basic / Pro / Max), repeat this step for each tier. Drag the cards in the Policies grid to set the order shown to buyers.</p>
<h2 id="purchase-url">Step 8 Share your purchase URL</h2> <h2 id="purchase-url">Step 8: Share your purchase URL</h2>
<p>Your public purchase URL is now live at:</p> <p>Your public purchase URL is now live at:</p>
<pre class="code">https://&lt;your-keysat-host&gt;/buy/&lt;product-slug&gt;</pre> <pre class="code">https://&lt;your-keysat-host&gt;/buy/&lt;product-slug&gt;</pre>
<p>Buyers hit it, see your product, click "Pay", and BTCPay&rsquo;s checkout takes over. On payment confirmation, Keysat receives a webhook from BTCPay, signs a license, and emails it to the buyer (if they entered an email) and shows it on the receipt page.</p> <p>Buyers hit it, see your product, click "Pay", and BTCPay&rsquo;s checkout takes over. On payment confirmation, Keysat receives a webhook from BTCPay, signs a license, and emails it to the buyer (if they entered an email) and shows it on the receipt page.</p>
<p>Test it end-to-end by creating a free-license discount code and redeeming it &mdash; the same code path runs, just without the payment leg.</p> <p>Test it end-to-end by creating a free-license discount code and redeeming it: the same code path runs, just without the payment leg.</p>
<h2 id="next">What&rsquo;s next</h2> <h2 id="next">What&rsquo;s next</h2>
<div class="next-grid"> <div class="next-grid">
@@ -195,5 +193,6 @@ payment_methods: <span class="s">[BTC-OnChain, BTC-LightningNetwork]</span></pre
<script src="https://unpkg.com/lucide@latest"></script> <script src="https://unpkg.com/lucide@latest"></script>
<script>lucide.createIcons();</script> <script>lucide.createIcons();</script>
<script src="docs.js"></script>
</body> </body>
</html> </html>
+12 -13
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs Integrate the SDK</title> <title>Keysat Docs: Integrate the SDK</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css"> <link rel="stylesheet" href="docs.css">
</head> </head>
@@ -33,8 +33,6 @@
<div class="group"> <div class="group">
<div class="glabel">Reference</div> <div class="glabel">Reference</div>
<a href="wire-format.html">Wire format</a> <a href="wire-format.html">Wire format</a>
<a href="integrate.html#api">Admin API</a>
<a href="integrate.html#sdks">SDKs</a>
</div> </div>
<div class="group"> <div class="group">
<div class="glabel">Project</div> <div class="glabel">Project</div>
@@ -52,18 +50,18 @@
<main class="prose"> <main class="prose">
<div class="crumb">Get started · Integrate the SDK</div> <div class="crumb">Get started · Integrate the SDK</div>
<h1>Integrate the SDK.</h1> <h1>Integrate the SDK.</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. What you do with the result refuse to start without a license, unlock specific features, just show a "supporter" badge is your call. The SDK is the primitive; the business model is yours.</p> <p class="lead">Wire Keysat licenses into your software in under an afternoon. The verifier is pure-function, offline, and ships in five lines. What you do with the result (refuse to start without a license, unlock specific features, just show a "supporter" badge) is your call. The SDK is the primitive; the business model is yours.</p>
<h2 id="prereq">Prerequisites</h2> <h2 id="prereq">Prerequisites</h2>
<p>Before you start, you should have:</p> <p>Before you start, you should have:</p>
<ul> <ul>
<li>A Keysat installation running on your Start9 &mdash; see <a href="install.html">Install &amp; setup</a>.</li> <li>A Keysat installation running on your Start9; see <a href="install.html">Install &amp; setup</a>.</li>
<li>BTCPay Server connected to Keysat &mdash; ditto.</li> <li>BTCPay Server connected to Keysat; ditto.</li>
<li>At least one product defined in the admin UI.</li> <li>At least one product defined in the admin UI.</li>
</ul> </ul>
<h2 id="sdks">Pick an SDK</h2> <h2 id="sdks">Pick an SDK</h2>
<p>Four official SDKs ship today. They are wire-compatible &mdash; a license issued by your Keysat verifies identically in any of them. Cross-check fixtures in the daemon repo prove each SDK accepts the same bytes the daemon mints.</p> <p>Four official SDKs ship today. They are wire-compatible. A license issued by your Keysat verifies identically in any of them. Cross-check fixtures in the daemon repo prove each SDK accepts the same bytes the daemon mints.</p>
<div class="lang-tabs" role="tablist"> <div class="lang-tabs" role="tablist">
<button class="active" data-lang="ts">TypeScript</button> <button class="active" data-lang="ts">TypeScript</button>
@@ -88,11 +86,11 @@ poetry add keysat-licensing-client</pre>
<pre class="code lang-pane" data-lang="go" style="display:none"><span class="c">// go.mod</span> <pre class="code lang-pane" data-lang="go" style="display:none"><span class="c">// go.mod</span>
go get github.com/keysat-xyz/keysat-client-go go get github.com/keysat-xyz/keysat-client-go
<span class="c">// stdlib only no third-party Go dependencies</span></pre> <span class="c">// stdlib only: no third-party Go dependencies</span></pre>
<p>If your language isn&rsquo;t covered, see <a href="wire-format.html">Wire format</a>. The format is small and porting takes about an afternoon.</p> <p>If your language isn&rsquo;t covered, see <a href="wire-format.html">Wire format</a>. The format is small and porting takes about an afternoon.</p>
<h2 id="embed">Step 1 Embed your public key</h2> <h2 id="embed">Step 1: Embed your public key</h2>
<p>In the admin UI, open <strong>Overview</strong> and copy the issuer public key from the "Embed your public key" card. (Or fetch it from <code>GET /v1/issuer/public-key</code>.) Paste it into your application&rsquo;s source code as a compile-time constant.</p> <p>In the admin UI, open <strong>Overview</strong> and copy the issuer public key from the "Embed your public key" card. (Or fetch it from <code>GET /v1/issuer/public-key</code>.) Paste it into your application&rsquo;s source code as a compile-time constant.</p>
<pre class="code lang-pane" data-lang="ts"><span class="k">const</span> <span class="f">ISSUER_PEM</span> = <span class="s">`-----BEGIN PUBLIC KEY----- <pre class="code lang-pane" data-lang="ts"><span class="k">const</span> <span class="f">ISSUER_PEM</span> = <span class="s">`-----BEGIN PUBLIC KEY-----
@@ -110,7 +108,7 @@ MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
<p><strong>Embed it. Don&rsquo;t fetch it.</strong> The whole point of offline verification is that your software can&rsquo;t be tricked by a network-level attacker. If you fetch the public key at runtime, you&rsquo;re back to trusting a server.</p> <p><strong>Embed it. Don&rsquo;t fetch it.</strong> The whole point of offline verification is that your software can&rsquo;t be tricked by a network-level attacker. If you fetch the public key at runtime, you&rsquo;re back to trusting a server.</p>
</div> </div>
<h2 id="verify">Step 2 Verify a license at startup</h2> <h2 id="verify">Step 2: Verify a license at startup</h2>
<p>Read the user&rsquo;s license key from wherever you store it (a file in their data directory, the OS keychain, an env var) and verify it on application start.</p> <p>Read the user&rsquo;s license key from wherever you store it (a file in their data directory, the OS keychain, an env var) and verify it on application start.</p>
<pre class="code lang-pane" data-lang="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>; <pre class="code lang-pane" data-lang="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>;
@@ -167,7 +165,7 @@ result = verifier.<span class="f">verify</span>(license_key_from_user)
</tbody> </tbody>
</table> </table>
<h2 id="errors">Step 3 Handle errors gracefully</h2> <h2 id="errors">Step 3: Handle errors gracefully</h2>
<p>Verification can fail for benign reasons (the user hasn&rsquo;t pasted a license yet) or hostile ones (someone tampered with a license file). Distinguish them in your UX:</p> <p>Verification can fail for benign reasons (the user hasn&rsquo;t pasted a license yet) or hostile ones (someone tampered with a license file). Distinguish them in your UX:</p>
<pre class="code lang-pane" data-lang="ts"><span class="k">try</span> { <pre class="code lang-pane" data-lang="ts"><span class="k">try</span> {
@@ -198,7 +196,7 @@ result = verifier.<span class="f">verify</span>(license_key_from_user)
show_input_error()</pre> show_input_error()</pre>
<h2 id="renewals">Renewals &amp; revocation</h2> <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 in your app &mdash; that&rsquo;s the trade-off for offline.</p> <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 in your app. That&rsquo;s the trade-off for offline.</p>
<p>If you need revocation, ship a thin <em>online</em> check that runs on a cadence (e.g. once a week) against your Keysat&rsquo;s revocation feed:</p> <p>If you need revocation, ship a thin <em>online</em> check that runs on a cadence (e.g. once a week) against your Keysat&rsquo;s revocation feed:</p>
@@ -232,7 +230,7 @@ result = verifier.<span class="f">verify</span>(license_key_from_user)
<div class="callout"> <div class="callout">
<i data-lucide="key-round"></i> <i data-lucide="key-round"></i>
<p><strong>You decide the policy.</strong> Many indie developers ship no revocation at all. Once a key is sold, it stays valid &mdash; refunds happen offline via BTCPay. That&rsquo;s perfectly reasonable.</p> <p><strong>You decide the policy.</strong> Many indie developers ship no revocation at all. Once a key is sold, it stays valid. Refunds happen offline via BTCPay. That&rsquo;s perfectly reasonable.</p>
</div> </div>
<h2 id="api">Admin API</h2> <h2 id="api">Admin API</h2>
@@ -284,5 +282,6 @@ result = verifier.<span class="f">verify</span>(license_key_from_user)
b.addEventListener('click', () => setLang(b.dataset.lang)); b.addEventListener('click', () => setLang(b.dataset.lang));
}); });
</script> </script>
<script src="docs.js"></script>
</body> </body>
</html> </html>
+15 -16
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs License</title> <title>Keysat Docs: License</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css"> <link rel="stylesheet" href="docs.css">
</head> </head>
@@ -33,8 +33,11 @@
<div class="group"> <div class="group">
<div class="glabel">Reference</div> <div class="glabel">Reference</div>
<a href="wire-format.html">Wire format</a> <a href="wire-format.html">Wire format</a>
<a href="integrate.html#api">Admin API</a> </div>
<a href="integrate.html#sdks">SDKs</a> <div class="group">
<div class="glabel">Project</div>
<a href="pricing.html">Pricing</a>
<a href="license.html" class="active">License</a>
</div> </div>
<div class="group"> <div class="group">
<div class="glabel">Operate</div> <div class="glabel">Operate</div>
@@ -42,11 +45,6 @@
<a href="operate.html#migrate">Migrate hardware</a> <a href="operate.html#migrate">Migrate hardware</a>
<a href="operate.html#troubleshooting">Troubleshooting</a> <a href="operate.html#troubleshooting">Troubleshooting</a>
</div> </div>
<div class="group">
<div class="glabel">Project</div>
<a href="pricing.html">Pricing</a>
<a href="license.html" class="active">License</a>
</div>
</aside> </aside>
<main class="prose"> <main class="prose">
@@ -79,28 +77,28 @@
<h2 id="why">Why source-available for the daemon?</h2> <h2 id="why">Why source-available for the daemon?</h2>
<p>Two reasons, both pragmatic:</p> <p>Two reasons, both pragmatic:</p>
<ol> <ol>
<li><strong>The work has real cost.</strong> Building Keysat takes time. The source-available model lets the project be funded by operators on the Pro / Patron tiers who get value from a maintained, evolving daemon without forcing every operator onto a paid tier.</li> <li><strong>The work has real cost.</strong> Building Keysat takes time. The source-available model lets the project be funded by operators on the Pro / Patron tiers who get value from a maintained, evolving daemon, without forcing every operator onto a paid tier.</li>
<li><strong>The "AWS-hosts-our-open-source" failure mode.</strong> Fully open-source self-hosted projects routinely get strip-mined by cloud providers who host them as a managed service and capture the revenue. The daemon license forbids this specific pattern. Everything else running your own instance, modifying it, auditing the code, selling licenses for your own products through it — is permitted.</li> <li><strong>The "AWS-hosts-our-open-source" failure mode.</strong> Fully open-source self-hosted projects routinely get strip-mined by cloud providers who host them as a managed service and capture the revenue. The daemon license forbids this specific pattern. Everything else is permitted: running your own instance, modifying it, auditing the code, selling licenses for your own products through it.</li>
</ol> </ol>
<p>The SDKs are MIT because they sit inside <em>your</em> software. License compatibility there is critical and the MIT license is the modern default for libraries you embed.</p> <p>The SDKs are MIT because they sit inside <em>your</em> software. License compatibility there is critical and the MIT license is the modern default for libraries you embed.</p>
<h2 id="permitted">What you can do (daemon)</h2> <h2 id="permitted">What you can do (daemon)</h2>
<ul> <ul>
<li><strong>Audit the source.</strong> Read every line; understand the cryptography, the storage, the API surface.</li> <li><strong>Audit the source.</strong> Read every line; understand the cryptography, the storage, the API surface.</li>
<li><strong>Run an instance on infrastructure you control.</strong> A Start9 box at home, a VPS, a cloud instance anywhere you deploy it.</li> <li><strong>Run an instance on infrastructure you control.</strong> A Start9 box at home, a VPS, a cloud instance: anywhere you deploy it.</li>
<li><strong>Modify it for your needs.</strong> Add features, change defaults, integrate it more deeply with your StartOS package. Modifications remain under the same license.</li> <li><strong>Modify it for your needs.</strong> Add features, change defaults, integrate it more deeply with your StartOS package. Modifications remain under the same license.</li>
<li><strong>Operate it as your private licensing service</strong> to issue signed license keys for software products <em>you</em> sell or distribute. This is the intended use case Keysat exists for this.</li> <li><strong>Operate it as your private licensing service</strong> to issue signed license keys for software products <em>you</em> sell or distribute. This is the intended use case. Keysat exists for this.</li>
<li><strong>Maintain a public fork.</strong> Forks on GitHub are fine as long as they carry the license unchanged and don't enable any of the prohibited uses below.</li> <li><strong>Maintain a public fork.</strong> Forks on GitHub are fine as long as they carry the license unchanged and don't enable any of the prohibited uses below.</li>
</ul> </ul>
<h2 id="forbidden">What you can't do without prior permission (daemon)</h2> <h2 id="forbidden">What you can't do without prior permission (daemon)</h2>
<ul> <ul>
<li><strong>Distribute compiled binaries to third parties.</strong> Including free of charge. The intent is that operators run Keysat themselves; they don't hand pre-built copies to others.</li> <li><strong>Distribute compiled binaries to third parties.</strong> Including free of charge. The intent is that operators run Keysat themselves; they don't hand pre-built copies to others.</li>
<li><strong>Provide Keysat as a hosted / managed service to third parties.</strong> "Keysat-as-a-Service" run by a cloud provider for a fee, or by anyone other than the operator using it for their own products, is the one pattern explicitly forbidden. Your own customers receiving signed license keys from your instance are <em>not</em> a hosted service — that's the daemon's intended use case.</li> <li><strong>Provide Keysat as a hosted / managed service to third parties.</strong> "Keysat-as-a-Service" run by a cloud provider for a fee, or by anyone other than the operator using it for their own products, is the one pattern explicitly forbidden. Your own customers receiving signed license keys from your instance are <em>not</em> a hosted service. That's the daemon's intended use case.</li>
<li><strong>Sell, sublicense, lease, or rent the daemon software itself.</strong> Distinct from selling licenses <em>through</em> the daemon, which is allowed.</li> <li><strong>Sell, sublicense, lease, or rent the daemon software itself.</strong> Distinct from selling licenses <em>through</em> the daemon, which is allowed.</li>
<li><strong>Remove copyright notices or this license text.</strong></li> <li><strong>Remove copyright notices or this license text.</strong></li>
</ul> </ul>
<p>If you have a use case that crosses one of these lines commercial redistribution, white-label deployment, a managed-service offering email <a href="mailto:licensing@keysat.xyz">licensing@keysat.xyz</a>. The license isn't designed to be a wall; it's designed to make commercial expansion an explicit conversation rather than an implicit one.</p> <p>If you have a use case that crosses one of these lines (commercial redistribution, white-label deployment, a managed-service offering), email <a href="mailto:licensing@keysat.xyz">licensing@keysat.xyz</a>. The license isn't designed to be a wall; it's designed to make commercial expansion an explicit conversation rather than an implicit one.</p>
<h2 id="contributions">Contributions</h2> <h2 id="contributions">Contributions</h2>
<p>By submitting code, documentation, designs, or other contributions to the upstream daemon repo, you grant Keysat a perpetual, worldwide, non-exclusive, royalty-free license to use, modify, relicense, and redistribute your contribution under the same license (or any later version). You retain ownership of your contribution; this is a license-back, not an assignment. The full text is in <a href="https://github.com/keysat-xyz/keysat/blob/main/LICENSE">LICENSE Section 4</a>.</p> <p>By submitting code, documentation, designs, or other contributions to the upstream daemon repo, you grant Keysat a perpetual, worldwide, non-exclusive, royalty-free license to use, modify, relicense, and redistribute your contribution under the same license (or any later version). You retain ownership of your contribution; this is a license-back, not an assignment. The full text is in <a href="https://github.com/keysat-xyz/keysat/blob/main/LICENSE">LICENSE Section 4</a>.</p>
@@ -109,7 +107,7 @@
<p>The authoritative text lives at <a href="https://github.com/keysat-xyz/keysat/blob/main/LICENSE">github.com/keysat-xyz/keysat/blob/main/LICENSE</a>. This page is a plain-English summary; the LICENSE file is what governs in any conflict.</p> <p>The authoritative text lives at <a href="https://github.com/keysat-xyz/keysat/blob/main/LICENSE">github.com/keysat-xyz/keysat/blob/main/LICENSE</a>. This page is a plain-English summary; the LICENSE file is what governs in any conflict.</p>
<h2 id="sdks">SDK licenses</h2> <h2 id="sdks">SDK licenses</h2>
<p>Each SDK ships under the MIT License included verbatim in the <code>LICENSE</code> file of each repo:</p> <p>Each SDK ships under the MIT License, included verbatim in the <code>LICENSE</code> file of each repo:</p>
<ul> <ul>
<li><a href="https://github.com/keysat-xyz/keysat-client-rust/blob/main/LICENSE">keysat-client-rust</a></li> <li><a href="https://github.com/keysat-xyz/keysat-client-rust/blob/main/LICENSE">keysat-client-rust</a></li>
<li><a href="https://github.com/keysat-xyz/keysat-client-ts/blob/main/LICENSE">keysat-client-ts</a></li> <li><a href="https://github.com/keysat-xyz/keysat-client-ts/blob/main/LICENSE">keysat-client-ts</a></li>
@@ -117,7 +115,7 @@
<li><a href="https://github.com/keysat-xyz/keysat-client-go/blob/main/LICENSE">keysat-client-go</a></li> <li><a href="https://github.com/keysat-xyz/keysat-client-go/blob/main/LICENSE">keysat-client-go</a></li>
<li><a href="https://github.com/keysat-xyz/keysat-activate-template/blob/main/LICENSE">keysat-activate-template</a></li> <li><a href="https://github.com/keysat-xyz/keysat-activate-template/blob/main/LICENSE">keysat-activate-template</a></li>
</ul> </ul>
<p>You can use these in any software open-source, closed-source, commercial, free, anything. The only obligation MIT imposes is preserving the copyright notice when you redistribute the SDK source itself.</p> <p>You can use these in any software: open-source, closed-source, commercial, free, anything. The only obligation MIT imposes is preserving the copyright notice when you redistribute the SDK source itself.</p>
<h2 id="contact">Commercial inquiries</h2> <h2 id="contact">Commercial inquiries</h2>
<p>For commercial redistribution, resale, hosted-service rights, white-label deployment, or any other use not expressly granted by the source-available license: <a href="mailto:licensing@keysat.xyz">licensing@keysat.xyz</a>.</p> <p>For commercial redistribution, resale, hosted-service rights, white-label deployment, or any other use not expressly granted by the source-available license: <a href="mailto:licensing@keysat.xyz">licensing@keysat.xyz</a>.</p>
@@ -136,5 +134,6 @@
</aside> </aside>
</div> </div>
<script src="docs.js"></script>
</body> </body>
</html> </html>
+9 -10
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs Operate</title> <title>Keysat Docs: Operate</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css"> <link rel="stylesheet" href="docs.css">
</head> </head>
@@ -33,8 +33,6 @@
<div class="group"> <div class="group">
<div class="glabel">Reference</div> <div class="glabel">Reference</div>
<a href="wire-format.html">Wire format</a> <a href="wire-format.html">Wire format</a>
<a href="integrate.html#api">Admin API</a>
<a href="integrate.html#sdks">SDKs</a>
</div> </div>
<div class="group"> <div class="group">
<div class="glabel">Project</div> <div class="glabel">Project</div>
@@ -55,7 +53,7 @@
<p class="lead">Backups, migration, recovery, and the things that go wrong. The "you didn&rsquo;t expect to need this page until you needed it" page.</p> <p class="lead">Backups, migration, recovery, and the things that go wrong. The "you didn&rsquo;t expect to need this page until you needed it" page.</p>
<h2 id="backups">Backups</h2> <h2 id="backups">Backups</h2>
<p>StartOS handles backups for you. By default, every service in your StartOS install is included in the same backup snapshot &mdash; you set the destination once (encrypted external drive, S3-compatible cloud, etc.) and StartOS schedules nightly snapshots.</p> <p>StartOS handles backups for you. By default, every service in your StartOS install is included in the same backup snapshot. You set the destination once (encrypted external drive, S3-compatible cloud, etc.) and StartOS schedules nightly snapshots.</p>
<p>The Keysat backup payload is intentionally tiny. It contains:</p> <p>The Keysat backup payload is intentionally tiny. It contains:</p>
@@ -82,20 +80,20 @@
<li>On the new Start9, complete first-time setup with a fresh password. Don&rsquo;t install any services yet.</li> <li>On the new Start9, complete first-time setup with a fresh password. Don&rsquo;t install any services yet.</li>
<li>StartOS &rarr; Settings &rarr; Backups &rarr; Restore. Point at the same destination. Pick the most recent snapshot.</li> <li>StartOS &rarr; Settings &rarr; Backups &rarr; Restore. Point at the same destination. Pick the most recent snapshot.</li>
<li>StartOS restores all services in dependency order. Keysat will restore alongside BTCPay and Bitcoin Core. Bitcoin will need to re-sync if you&rsquo;re using Bitcoin Core (consider <a href="https://utxo.live">utxo.live</a> for assumeutxo to skip IBD).</li> <li>StartOS restores all services in dependency order. Keysat will restore alongside BTCPay and Bitcoin Core. Bitcoin will need to re-sync if you&rsquo;re using Bitcoin Core (consider <a href="https://utxo.live">utxo.live</a> for assumeutxo to skip IBD).</li>
<li>Once Keysat is running on the new box, your purchase URLs change &mdash; the LAN/Tor hostnames are different. Update any links you&rsquo;ve published.</li> <li>Once Keysat is running on the new box, your purchase URLs change: the LAN/Tor hostnames are different. Update any links you&rsquo;ve published.</li>
</ol> </ol>
<p>The signing keypair restores along with the database, so all previously-issued licenses verify identically against the same public key. You don&rsquo;t need to re-distribute the public key to your customers.</p> <p>The signing keypair restores along with the database, so all previously-issued licenses verify identically against the same public key. You don&rsquo;t need to re-distribute the public key to your customers.</p>
<h2 id="signing-key">Rotating the signing key</h2> <h2 id="signing-key">Rotating the signing key</h2>
<p>You generally don&rsquo;t want to rotate the signing key &mdash; doing so invalidates every license you&rsquo;ve ever issued. There is no admin-UI affordance for rotation today; the key is generated once on first start (and persisted to the <code>server_keys</code> SQLite table) and stays there for the life of the instance.</p> <p>You generally don&rsquo;t want to rotate the signing key. Doing so invalidates every license you&rsquo;ve ever issued. There is no admin-UI affordance for rotation today; the key is generated once on first start (and persisted to the <code>server_keys</code> SQLite table) and stays there for the life of the instance.</p>
<p>If you absolutely need to rotate (e.g. you suspect the keypair has leaked off the box):</p> <p>If you absolutely need to rotate (e.g. you suspect the keypair has leaked off the box):</p>
<ol> <ol>
<li>Stop Keysat.</li> <li>Stop Keysat.</li>
<li>Drop the row in the <code>server_keys</code> table (or move the database aside entirely if you also want to start clean).</li> <li>Drop the row in the <code>server_keys</code> table (or move the database aside entirely if you also want to start clean).</li>
<li>Restart Keysat &mdash; it will generate a fresh keypair on first run.</li> <li>Restart Keysat. It will generate a fresh keypair on first run.</li>
<li>Re-issue all active licenses to existing customers using the new key. The admin UI doesn&rsquo;t support bulk re-issuance yet; this is a manual SQL + scripted-API exercise.</li> <li>Re-issue all active licenses to existing customers using the new key. The admin UI doesn&rsquo;t support bulk re-issuance yet; this is a manual SQL + scripted-API exercise.</li>
<li>Push a software update that swaps the embedded public key in your downstream apps.</li> <li>Push a software update that swaps the embedded public key in your downstream apps.</li>
</ol> </ol>
@@ -112,7 +110,7 @@
<p>BTCPay rejects the invoice request because the store has no configured wallet. Open BTCPay, find your store, and configure either an on-chain wallet or a Lightning node before retrying.</p> <p>BTCPay rejects the invoice request because the store has no configured wallet. Open BTCPay, find your store, and configure either an on-chain wallet or a Lightning node before retrying.</p>
<h3 id="t-webhook">Webhook deliveries failing</h3> <h3 id="t-webhook">Webhook deliveries failing</h3>
<p>In the admin UI go to <strong>Webhooks</strong> &mdash; failed deliveries past the 10-attempt retry budget land in the "Failed" filter (the DLQ), with the response status and an inline "Retry" button. The audit log is a secondary source. Common causes:</p> <p>In the admin UI go to <strong>Webhooks</strong>. Failed deliveries past the 10-attempt retry budget land in the "Failed" filter (the DLQ), with the response status and an inline "Retry" button. The audit log is a secondary source. Common causes:</p>
<ul> <ul>
<li>Endpoint URL no longer reachable. Hit it manually with <code>curl</code> from your laptop to confirm.</li> <li>Endpoint URL no longer reachable. Hit it manually with <code>curl</code> from your laptop to confirm.</li>
<li>Endpoint rejecting on signature mismatch. Verify your endpoint is HMAC-validating against the secret you registered with.</li> <li>Endpoint rejecting on signature mismatch. Verify your endpoint is HMAC-validating against the secret you registered with.</li>
@@ -120,13 +118,13 @@
</ul> </ul>
<h3 id="t-db-locked">"database is locked" errors in logs</h3> <h3 id="t-db-locked">"database is locked" errors in logs</h3>
<p>Almost always a sign that two daemon instances are racing on the same SQLite file &mdash; usually because of a misconfigured supervisor. Confirm only one Keysat container is running. If you&rsquo;re seeing this on a fresh install with no customizations, file a bug report against the package version you&rsquo;re running.</p> <p>Almost always a sign that two daemon instances are racing on the same SQLite file, usually because of a misconfigured supervisor. Confirm only one Keysat container is running. If you&rsquo;re seeing this on a fresh install with no customizations, file a bug report against the package version you&rsquo;re running.</p>
<h3 id="t-time-skew">Licenses verifying as "expired" immediately after issue</h3> <h3 id="t-time-skew">Licenses verifying as "expired" immediately after issue</h3>
<p>Clock skew. Either the issuing host or the verifying host has the wrong time. Run NTP. StartOS keeps your Start9 in sync automatically; the issue is usually on the verifier side (e.g. an air-gapped buyer machine).</p> <p>Clock skew. Either the issuing host or the verifying host has the wrong time. Run NTP. StartOS keeps your Start9 in sync automatically; the issue is usually on the verifier side (e.g. an air-gapped buyer machine).</p>
<h2 id="logs">Reading the logs</h2> <h2 id="logs">Reading the logs</h2>
<p>Keysat logs to stdout, captured by StartOS. Tail them from the StartOS dashboard &mdash; Service page &rarr; Logs &rarr; Live tail.</p> <p>Keysat logs to stdout, captured by StartOS. Tail them from the StartOS dashboard: Service page &rarr; Logs &rarr; Live tail.</p>
<p>Useful log lines to grep for:</p> <p>Useful log lines to grep for:</p>
@@ -163,5 +161,6 @@
<script src="https://unpkg.com/lucide@latest"></script> <script src="https://unpkg.com/lucide@latest"></script>
<script>lucide.createIcons();</script> <script>lucide.createIcons();</script>
<script src="docs.js"></script>
</body> </body>
</html> </html>
+51 -28
View File
@@ -3,10 +3,11 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs Pricing</title> <title>Keysat Docs: Pricing</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css"> <link rel="stylesheet" href="docs.css">
<style> <style>
/* Page-local: tier-card grid (only used here). */
.tier-grid { display:grid; grid-template-columns:repeat(3, 1fr); gap:18px; margin:24px 0; } .tier-grid { display:grid; grid-template-columns:repeat(3, 1fr); gap:18px; margin:24px 0; }
@media (max-width:760px) { .tier-grid { grid-template-columns:1fr; } } @media (max-width:760px) { .tier-grid { grid-template-columns:1fr; } }
.tier-card { .tier-card {
@@ -78,25 +79,45 @@
<div class="layout"> <div class="layout">
<aside class="side"> <aside class="side">
<a href="#overview">Overview</a> <div class="group">
<a href="#tiers">The three tiers</a> <div class="glabel">Get started</div>
<a href="#what-counts">What the caps count</a> <a href="index.html">Introduction</a>
<a href="#changing-tiers">Switching tiers</a> <a href="install.html">Install &amp; setup</a>
<a href="#unlicensed">Running unlicensed</a> <a href="integrate.html">Integrate the SDK</a>
<a href="agent.html">Agent integration</a>
</div>
<div class="group">
<div class="glabel">Concepts</div>
<a href="index.html#architecture">Architecture</a>
<a href="index.html#products-policies">Products &amp; policies</a>
<a href="index.html#discounts">Discount codes</a>
<a href="index.html#revocation">Revocation strategy</a>
</div>
<div class="group">
<div class="glabel">Reference</div>
<a href="wire-format.html">Wire format</a>
</div>
<div class="group">
<div class="glabel">Project</div>
<a href="pricing.html" class="active">Pricing</a>
<a href="license.html">License</a>
</div>
<div class="group">
<div class="glabel">Operate</div>
<a href="operate.html#backups">Backups</a>
<a href="operate.html#migrate">Migrate hardware</a>
<a href="operate.html#troubleshooting">Troubleshooting</a>
</div>
</aside> </aside>
<main class="main"> <main class="prose">
<div class="crumb">Project &middot; Pricing</div>
<h1 id="overview">Pricing.</h1>
<p class="lead">Keysat dogfoods its own licensing. The Keysat daemon is itself licensed by a Keysat instance running at <a href="https://licensing.keysat.xyz">licensing.keysat.xyz</a>. The same primitive operators use to gate features in their own software gates a few Keysat features behind paid tiers. The <strong>free tier is genuinely useful</strong>: most hobbyist operators will never need to upgrade.</p>
<h1 id="overview">Pricing</h1> <h2 id="tiers">The three tiers</h2>
<p class="lede">
Keysat dogfoods its own licensing — the Keysat daemon is itself licensed by a Keysat
instance running at <a href="https://licensing.keysat.xyz">licensing.keysat.xyz</a>.
The same primitive operators use to gate features in their own software gates a few
Keysat features behind paid tiers. The <strong>free tier is genuinely useful</strong>
most hobbyist operators will never need to upgrade.
</p>
<div id="tiers" class="tier-grid"> <div class="tier-grid">
<div class="tier-card"> <div class="tier-card">
<h3>Creator</h3> <h3>Creator</h3>
@@ -121,8 +142,8 @@
<div class="frequency">per year (recurring)</div> <div class="frequency">per year (recurring)</div>
<ul> <ul>
<li>Unlimited products / policies / codes</li> <li>Unlimited products / policies / codes</li>
<li>Recurring subscriptions trials, grace, auto-renew</li> <li>Recurring subscriptions: trials, grace, auto-renew</li>
<li>Zaprite payments — accept BTC + cards <em>(shipping in v0.3)</em></li> <li>Zaprite payments (expanded payment options including card payment capabilities)</li>
<li>In-place tier upgrades (proration handled)</li> <li>In-place tier upgrades (proration handled)</li>
<li>Everything in Creator</li> <li>Everything in Creator</li>
</ul> </ul>
@@ -135,7 +156,7 @@
<div class="frequency">one-time, perpetual</div> <div class="frequency">one-time, perpetual</div>
<ul> <ul>
<li>Everything in Pro</li> <li>Everything in Pro</li>
<li>Perpetual license one-time, never renews</li> <li>Perpetual license: one-time, never renews</li>
<li>Direct one-on-one support</li> <li>Direct one-on-one support</li>
<li>"Patron" badge in your admin UI</li> <li>"Patron" badge in your admin UI</li>
<li>Listed on the Patrons page at keysat.xyz</li> <li>Listed on the Patrons page at keysat.xyz</li>
@@ -159,10 +180,11 @@
<strong>What's gated.</strong> Capacity caps (products / policies-per-product / <strong>What's gated.</strong> Capacity caps (products / policies-per-product /
active discount codes) are enforced at create-time on the Creator tier. Pro active discount codes) are enforced at create-time on the Creator tier. Pro
unlocks the <code>recurring_billing</code> entitlement (auto-renewing unlocks the <code>recurring_billing</code> entitlement (auto-renewing
subscriptions) and will unlock <code>zaprite_payments</code> (card payments subscriptions) and the <code>zaprite_payments</code> entitlement
via Zaprite) when that lands in v0.3. Patron differs from Pro by being a (expanded payment options including card payment capabilities).
one-time perpetual license rather than an annual subscription, plus direct Patron differs from Pro in that it is a perpetual license (never
one-on-one support not a feature gate, a different ownership model. expires or renews), plus direct one-on-one support. It's not a feature
gate, it's a different ownership model.
</div> </div>
<h2 id="what-counts">What the caps count</h2> <h2 id="what-counts">What the caps count</h2>
@@ -173,16 +195,16 @@
just stops them from creating a 6th. just stops them from creating a 6th.
</p> </p>
<ul> <ul>
<li><strong>Products</strong>: counts every product row (active + inactive). Operators don't get to evade the cap by toggling old rows inactive.</li> <li><strong>Products</strong>: counts every product row in the database. Free up a slot by deleting products you no longer offer (the admin UI exposes delete; there's no "soft-disable" affordance for products).</li>
<li><strong>Policies per product</strong>: counts policies on a single product, regardless of public/active state.</li> <li><strong>Policies per product</strong>: counts policies on a single product, regardless of public/active state.</li>
<li><strong>Active discount codes</strong>: counts only <code>active=true</code> codes. Disable old codes to free up slots without deleting them — disabled codes don't function but stay in the audit trail.</li> <li><strong>Active discount codes</strong>: counts only <code>active=true</code> codes. Disable old codes to free up slots without deleting them. Disabled codes don't function but stay in the audit trail.</li>
</ul> </ul>
<h2 id="changing-tiers">Switching tiers</h2> <h2 id="changing-tiers">Switching tiers</h2>
<p> <p>
Buy a higher-tier license at <a href="https://licensing.keysat.xyz/buy/keysat">licensing.keysat.xyz/buy/keysat</a>, Buy a higher-tier license at <a href="https://licensing.keysat.xyz/buy/keysat">licensing.keysat.xyz/buy/keysat</a>,
then activate it via StartOS → Keysat → Actions <em>Activate Keysat license</em>. then activate it via StartOS &rarr; Keysat &rarr; Actions &rarr; <em>Activate Keysat license</em>.
The daemon picks up the new entitlements on next request — no restart needed. The daemon picks up the new entitlements on next request. No restart needed.
The persistent banner in your admin sidebar always shows your current tier The persistent banner in your admin sidebar always shows your current tier
and the next-tier CTA. and the next-tier CTA.
</p> </p>
@@ -193,7 +215,7 @@
<h2 id="unlicensed">Running unlicensed</h2> <h2 id="unlicensed">Running unlicensed</h2>
<p> <p>
Keysat works without any license at all — you'll see "Unlicensed" in the Keysat works without any license at all. You'll see "Unlicensed" in the
sidebar and get the same caps as a Creator-tier operator sidebar and get the same caps as a Creator-tier operator
(5 products / 5 policies per product / 10 active discount codes). The (5 products / 5 policies per product / 10 active discount codes). The
Creator tier is free either way; the self-license flow exists primarily so Creator tier is free either way; the self-license flow exists primarily so
@@ -205,5 +227,6 @@
</main> </main>
</div> </div>
<script src="docs.js"></script>
</body> </body>
</html> </html>
+11 -12
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs Wire format reference</title> <title>Keysat Docs: Wire format reference</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css"> <link rel="stylesheet" href="docs.css">
</head> </head>
@@ -33,8 +33,6 @@
<div class="group"> <div class="group">
<div class="glabel">Reference</div> <div class="glabel">Reference</div>
<a href="wire-format.html" class="active">Wire format</a> <a href="wire-format.html" class="active">Wire format</a>
<a href="integrate.html#api">Admin API</a>
<a href="integrate.html#sdks">SDKs</a>
</div> </div>
<div class="group"> <div class="group">
<div class="glabel">Project</div> <div class="glabel">Project</div>
@@ -61,9 +59,9 @@
<p>Three parts, separated by single dashes:</p> <p>Three parts, separated by single dashes:</p>
<ul> <ul>
<li><code>LIC1</code> &mdash; literal envelope tag. Future format revisions get a new tag (<code>LIC2</code> etc.). Parsers MUST reject unknown tags.</li> <li><code>LIC1</code>: literal envelope tag. Future format revisions get a new tag (<code>LIC2</code> etc.). Parsers MUST reject unknown tags.</li>
<li><code>&lt;base32 payload&gt;</code> &mdash; the signed payload bytes, RFC 4648 base32 without padding (case-insensitive on decode). Variable length depending on payload version and number of entitlements.</li> <li><code>&lt;base32 payload&gt;</code>: the signed payload bytes, RFC 4648 base32 without padding (case-insensitive on decode). Variable length depending on payload version and number of entitlements.</li>
<li><code>&lt;base32 signature&gt;</code> &mdash; the 64-byte Ed25519 signature over the <em>raw payload bytes</em>, base32-encoded the same way.</li> <li><code>&lt;base32 signature&gt;</code>: the 64-byte Ed25519 signature over the <em>raw payload bytes</em>, base32-encoded the same way.</li>
</ul> </ul>
<p>To verify: split on <code>-</code>, validate the tag is <code>LIC1</code>, base32-decode both chunks (case-fold to upper), parse the payload, and verify the signature bytes against the raw payload bytes using the issuer&rsquo;s Ed25519 public key.</p> <p>To verify: split on <code>-</code>, validate the tag is <code>LIC1</code>, base32-decode both chunks (case-fold to upper), parse the payload, and verify the signature bytes against the raw payload bytes using the issuer&rsquo;s Ed25519 public key.</p>
@@ -71,7 +69,7 @@
<p>Keysat ships two payload versions today. v2 is the current default that the daemon issues; v1 verifiers stay in the SDKs forever so legacy keys keep verifying.</p> <p>Keysat ships two payload versions today. v2 is the current default that the daemon issues; v1 verifiers stay in the SDKs forever so legacy keys keep verifying.</p>
<h3>v1 (legacy, fixed 74 bytes)</h3> <h3>v1 (legacy, fixed 74 bytes)</h3>
<p>Issued by the very early daemon builds. No expiry, no entitlements &mdash; perpetual only, fingerprint binding optional. Still accepted on parse so old customer keys don&rsquo;t break.</p> <p>Issued by the very early daemon builds. No expiry, no entitlements. Perpetual only, fingerprint binding optional. Still accepted on parse so old customer keys don&rsquo;t break.</p>
<table class="t"> <table class="t">
<thead><tr><th>Offset</th><th>Length</th><th>Field</th><th>Notes</th></tr></thead> <thead><tr><th>Offset</th><th>Length</th><th>Field</th><th>Notes</th></tr></thead>
<tbody> <tbody>
@@ -85,7 +83,7 @@
</table> </table>
<h3>v2 (current default, variable length)</h3> <h3>v2 (current default, variable length)</h3>
<p>83-byte fixed head + variable-length entitlements table. v2 adds expiry, trial flag, and entitlements &mdash; all signed so offline verifiers can gate features without contacting the server (a stripped entitlement or pushed-back expiry would have to match a valid signature, which the attacker can&rsquo;t produce).</p> <p>83-byte fixed head + variable-length entitlements table. v2 adds expiry, trial flag, and entitlements, all signed so offline verifiers can gate features without contacting the server (a stripped entitlement or pushed-back expiry would have to match a valid signature, which the attacker can&rsquo;t produce).</p>
<table class="t"> <table class="t">
<thead><tr><th>Offset</th><th>Length</th><th>Field</th><th>Notes</th></tr></thead> <thead><tr><th>Offset</th><th>Length</th><th>Field</th><th>Notes</th></tr></thead>
<tbody> <tbody>
@@ -102,7 +100,7 @@
</table> </table>
<h2 id="signature">Signature</h2> <h2 id="signature">Signature</h2>
<p>The signature is computed over the <strong>raw payload bytes</strong> &mdash; the binary head plus any entitlements table, without the version tag, without base32 encoding, without dashes. The two base32 chunks in the wire format are encoded <em>independently</em>; concatenating them and base32-decoding the whole would be wrong.</p> <p>The signature is computed over the <strong>raw payload bytes</strong>: the binary head plus any entitlements table, without the version tag, without base32 encoding, without dashes. The two base32 chunks in the wire format are encoded <em>independently</em>; concatenating them and base32-decoding the whole would be wrong.</p>
<p>Verify with the issuer&rsquo;s Ed25519 public key (PEM-encoded, SubjectPublicKeyInfo). The SDKs ship the public key bundled in your app at build time; they don&rsquo;t fetch it at runtime. (The whole point of offline verification is that a network-level attacker can&rsquo;t hand your software a different key.)</p> <p>Verify with the issuer&rsquo;s Ed25519 public key (PEM-encoded, SubjectPublicKeyInfo). The SDKs ship the public key bundled in your app at build time; they don&rsquo;t fetch it at runtime. (The whole point of offline verification is that a network-level attacker can&rsquo;t hand your software a different key.)</p>
<h2 id="encoding">Base32 alphabet</h2> <h2 id="encoding">Base32 alphabet</h2>
@@ -132,7 +130,7 @@ MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
<li>Implement RFC 4648 base32 decode (most languages have this in stdlib).</li> <li>Implement RFC 4648 base32 decode (most languages have this in stdlib).</li>
<li>Implement the binary unmarshal for both v1 and v2 payloads (~80 lines total, mostly big-endian integer reads).</li> <li>Implement the binary unmarshal for both v1 and v2 payloads (~80 lines total, mostly big-endian integer reads).</li>
<li>Wire it up to your language&rsquo;s Ed25519 verifier from a vetted crypto library (libsodium, ring, ed25519-dalek, the Node/Python stdlib, etc.).</li> <li>Wire it up to your language&rsquo;s Ed25519 verifier from a vetted crypto library (libsodium, ring, ed25519-dalek, the Node/Python stdlib, etc.).</li>
<li>Run the cross-check tests &mdash; if all three vector cases pass byte-for-byte, you&rsquo;re wire-compatible.</li> <li>Run the cross-check tests. If all three vector cases pass byte-for-byte, you&rsquo;re wire-compatible.</li>
</ol> </ol>
<p>The four official SDKs (Rust, TypeScript, Python, Go) all sit on top of these same fixtures and the daemon&rsquo;s test suite asserts each implementation round-trips them identically before a release ships.</p> <p>The four official SDKs (Rust, TypeScript, Python, Go) all sit on top of these same fixtures and the daemon&rsquo;s test suite asserts each implementation round-trips them identically before a release ships.</p>
@@ -142,8 +140,8 @@ MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
<ul> <ul>
<li>Never silently changing an existing layout. Any field-shape change ⇒ new version byte.</li> <li>Never silently changing an existing layout. Any field-shape change ⇒ new version byte.</li>
<li>Maintaining v1 + v2 verifier support indefinitely &mdash; if v3 ever ships, your existing customer keys still verify against the daemon and the SDKs they shipped with.</li> <li>Maintaining v1 + v2 verifier support indefinitely. If v3 ever ships, your existing customer keys still verify against the daemon and the SDKs they shipped with.</li>
<li>The wire-envelope tag (<code>LIC1-…</code>) bumps only on a breaking envelope change &mdash; new payload versions live inside the same envelope tag as long as the split-on-dash structure stays the same.</li> <li>The wire-envelope tag (<code>LIC1-…</code>) bumps only on a breaking envelope change. New payload versions live inside the same envelope tag as long as the split-on-dash structure stays the same.</li>
<li>Publishing test vectors for every payload version under <code>tests/crosscheck/</code> in the daemon repo. All five implementations (daemon, Rust SDK, TypeScript SDK, Python SDK, Go SDK) are required to round-trip the same vectors byte-for-byte before a release ships.</li> <li>Publishing test vectors for every payload version under <code>tests/crosscheck/</code> in the daemon repo. All five implementations (daemon, Rust SDK, TypeScript SDK, Python SDK, Go SDK) are required to round-trip the same vectors byte-for-byte before a release ships.</li>
</ul> </ul>
</main> </main>
@@ -162,5 +160,6 @@ MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
<script src="https://unpkg.com/lucide@latest"></script> <script src="https://unpkg.com/lucide@latest"></script>
<script>lucide.createIcons();</script> <script>lucide.createIcons();</script>
<script src="docs.js"></script>
</body> </body>
</html> </html>