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>
<meta charset="utf-8">
<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="stylesheet" href="docs.css">
</head>
@@ -33,8 +33,6 @@
<div class="group">
<div class="glabel">Reference</div>
<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 class="group">
<div class="glabel">Project</div>
@@ -53,7 +51,7 @@
<div class="crumb">Get started · Agent integration</div>
<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>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>
<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 "Content-Type: application/json" \
-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
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>
<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>
<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>
</tbody>
</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>
<p>Two complementary discovery mechanisms.</p>
<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>
<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>
@@ -177,7 +175,7 @@ curl https://your-keysat-host/v1/admin/licenses?status=active \
<h3>Find a license by email</h3>
<pre><code>curl "$KS/v1/admin/licenses?buyer_email=alice@example.com" \
-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>
<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><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>
<table>
<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>
<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>
<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>
<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>
<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>
<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
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"]
# 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>
<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>
</div>
<script src="docs.js"></script>
</body>
</html>