Update docs to match the 0.2.0 daemon (admin-UI actions, runtime image, Zaprite, roles)

This commit is contained in:
Grant
2026-06-17 15:25:05 -05:00
parent 8c5cdb6468
commit eafdc6646e
6 changed files with 107 additions and 113 deletions
+29 -9
View File
@@ -57,6 +57,7 @@ API keys. Each carries a role that bounds what it can do. Format: `ks_<43 chars>
| `read-only` | List / get every resource. Mutate nothing. | | `read-only` | List / get every resource. Mutate nothing. |
| `license-issuer` | All `read-only` scopes + issue / revoke / suspend / change-tier on licenses. Cannot touch products, policies, or codes. | | `license-issuer` | All `read-only` scopes + issue / revoke / suspend / change-tier on licenses. Cannot touch products, policies, or codes. |
| `support` | All `license-issuer` scopes + cancel subscriptions + force-deactivate machines. | | `support` | All `license-issuer` scopes + cancel subscriptions + force-deactivate machines. |
| `merchant-onboard` | All `read-only` scopes + `products:write` + `policies:write` + `licenses:write` — the least-privilege credential for standing up a fresh catalog (create products, define policies/tiers, issue licenses against them) via the API. Deliberately excludes the support writes (subscriptions / machines) and every master-only gate. Tier caps still bound it. |
| `full-admin` | Every scope. Equivalent to the master key for most endpoints. | | `full-admin` | Every scope. Equivalent to the master key for most endpoints. |
Endpoints that touch settings (operator name, payment provider connections, Endpoints that touch settings (operator name, payment provider connections,
@@ -138,7 +139,7 @@ upgrade CTA without parsing message strings.
| `expired` | Past `expires_at` | | `expired` | Past `expires_at` |
| `fingerprint_mismatch` | Different machine than the one bound on first activate | | `fingerprint_mismatch` | Different machine than the one bound on first activate |
| `product_mismatch` | License is for a different product than the caller asserted | | `product_mismatch` | License is for a different product than the caller asserted |
| `machine_cap_exceeded` | Activating this fingerprint would exceed `max_machines` | | `too_many_machines` | Activating this fingerprint would exceed `max_machines` |
--- ---
@@ -173,7 +174,9 @@ curl -X POST $KS/v1/admin/licenses/$LICENSE_ID/revoke \
-d '{"reason":"customer request"}' -d '{"reason":"customer request"}'
``` ```
Idempotent. The next online validate from the buyer's app returns `reason: revoked`. The next online validate from the buyer's app returns `reason: revoked`. Not
idempotent — a second revoke of the same license returns `404 not_found` (treat
as success-equivalent on retry; see Idempotency below).
Scope required: `licenses:write`. Scope required: `licenses:write`.
@@ -277,10 +280,16 @@ A few patterns that work well in practice.
### Idempotency ### Idempotency
The daemon's mutation endpoints are idempotent where they can be. Revoke, The daemon's mutation endpoints are idempotent where they can be. Suspend,
suspend, unsuspend, archive, unarchive, subscription cancel — all return unsuspend, archive, unarchive, subscription cancel — all return success on the
success on the second call without changing state. Your agent can safely second call without changing state. Your agent can safely retry on network
retry on network errors. errors.
One exception: **revoke is not idempotent** — revoking an already-revoked
license returns `404 not_found` (the row no longer matches the
`status != 'revoked'` update guard). When retrying a revoke after an ambiguous
network failure, treat a `404` as success-equivalent: the license is already
revoked.
### Pagination ### Pagination
@@ -349,15 +358,26 @@ Some operations are deliberately operator-only and not accessible to any
scoped key, including `full-admin`: scoped key, including `full-admin`:
- Generating / revoking scoped API keys (`/v1/admin/api-keys`) - Generating / revoking scoped API keys (`/v1/admin/api-keys`)
- Connecting / disconnecting payment providers - Disconnecting a payment provider, and connecting *any* provider on a
production daemon
- Setting the operator name - Setting the operator name
- Activating the self-license (`/v1/admin/self-license`) - Activating the self-license (`/v1/admin/self-license`)
- Resetting the analytics install_uuid - Resetting the analytics install_uuid
- Changing the web UI password (StartOS Action only) - Changing the web UI password (StartOS Action only)
These all require the master `KEYSAT_ADMIN_API_KEY`. The reasoning: an agent These all require the master `KEYSAT_ADMIN_API_KEY`. The reasoning: an agent
that can rotate its own credentials, connect arbitrary payment processors, or that can rotate its own credentials, redirect settled payments, or change the
change the operator identity is no longer bounded by the role it was given. operator identity is no longer bounded by the role it was given.
**One narrow exception — agent-delegated payment connect.** A key granted the
à-la-carte `payment_providers:write` scope (never granted by any role —
operators add it explicitly per key) CAN initiate a BTCPay connect, but only
fail-closed under two gates: the daemon must be in **sandbox mode** (an outer
gate — scoped connect is refused outright on a production daemon, even for
regtest), and the target store must be **non-mainnet** (an inner gate enforced
after the OAuth round-trip). Disconnecting a provider, and any connect on a
production / mainnet daemon, remain master-only. This lets an integrating agent
wire up a throwaway sandbox without ever touching a live store's settlement.
--- ---
+37 -59
View File
@@ -80,10 +80,10 @@ comp keys, beta access, or "first N users free" launch promos.
Built from the local `Dockerfile` via `images.main.source.dockerBuild`, Built from the local `Dockerfile` via `images.main.source.dockerBuild`,
with build context set to the parent directory so the Dockerfile can with build context set to the parent directory so the Dockerfile can
`COPY` from the sibling `licensing-service/` source tree. The Rust binary `COPY` from the sibling `licensing-service/` source tree. The Rust binary
is statically linked against musl (target is statically compiled against musl (target `*-unknown-linux-musl`), and the
`*-unknown-linux-musl`) so the runtime image is a `scratch`-based final runtime stage is `debian:bookworm-slim` with `ca-certificates`, `tini` (init /
stage with no shared-library dependencies. Architectures: `x86_64` and signal handling), and `sqlite3` (an SQL shell for occasional admin tasks)
`aarch64`. installed. Architectures: `x86_64` and `aarch64`.
`start-cli s9pk pack` ingests the resulting OCI image, converts it to a `start-cli s9pk pack` ingests the resulting OCI image, converts it to a
squashfs filesystem image, and embeds that in the `.s9pk`. At runtime squashfs filesystem image, and embeds that in the `.s9pk`. At runtime
@@ -105,25 +105,24 @@ mandatory.
## Installation and First-Run Flow ## Installation and First-Run Flow
1. Install Keysat via the marketplace (or sideload the `.s9pk`). 1. Install Keysat via the marketplace (or sideload the `.s9pk`).
2. Resolve the auto-created **critical task** "Connect BTCPay" by 2. Resolve the auto-created **important task** "Connect BTCPay" — open
running the **Connect BTCPay** action. This opens a one-click the embedded admin web UI (**Settings → Payment providers**) and
authorize page on your local BTCPay; after approval, Keysat click **Connect BTCPay**. This opens a one-click authorize page on
auto-detects your store and registers an inbound webhook. No API your local BTCPay; after approval, Keysat auto-detects your store and
keys to copy. registers an inbound webhook. No API keys to copy. The install task
3. Run **Check BTCPay connection** to confirm — the install task clears clears automatically once BTCPay reports connected.
automatically. 3. Set your **operator name** (shown on the public homepage and in
4. Set your **operator name** (shown on the public homepage and in
buyer-facing receipts). buyer-facing receipts).
5. Create one or more **products** — each represents something you sell. 4. Create one or more **products** — each represents something you sell.
6. Create at least one **policy** per product. Multi-tier ladders 5. Create at least one **policy** per product. Multi-tier ladders
(Basic / Pro / Max) are first-class: when a product has two or more (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 public policies, the buy page renders a tier picker and the buyer
chooses before paying. Policies define duration, grace period, seat chooses before paying. Policies define duration, grace period, seat
cap, entitlements, recurring cadence, trial flag, price overrides, cap, entitlements, recurring cadence, trial flag, price overrides,
marketing bullets, and per-entitlement hide-on-buy-page toggles. marketing bullets, and per-entitlement hide-on-buy-page toggles.
7. Optionally create **discount / referral / free-license codes** (see 6. Optionally create **discount / referral / free-license codes** in the
`Create discount code` action). admin web UI.
8. Share the public service URL with buyers. 7. Share the public service URL with buyers.
## Configuration Management ## Configuration Management
@@ -145,7 +144,7 @@ interfaces for clarity:
| Interface | Type | Path prefix | Purpose | | Interface | Type | Path prefix | Purpose |
|-----------|------|-------------|------------------------------------------------------------------------------| |-----------|------|-------------|------------------------------------------------------------------------------|
| `api` | api | `/` | Public REST API for buyers (purchase, redeem) and licensed apps (validate, machine activation). Bake the URL into your software builds as the licensing endpoint. | | `api` | api | `/` | Public REST API for buyers (purchase, redeem) and licensed apps (validate, machine activation). Bake the URL into your software builds as the licensing endpoint. |
| `webhook` | api | `/btcpay` | BTCPay webhook landing endpoint. Registered automatically during Connect BTCPay; not for human use. | | `webhook` | api | `/btcpay` | BTCPay webhook landing endpoint. Registered automatically when you connect BTCPay in the admin web UI; not for human use. |
StartOS terminates TLS at the platform edge. Inside the container every StartOS terminates TLS at the platform edge. Inside the container every
request arrives as plain HTTP. For browser-facing URLs (e.g., the public request arrives as plain HTTP. For browser-facing URLs (e.g., the public
@@ -153,44 +152,23 @@ purchase page) hardcode `https://`.
## Actions (StartOS UI) ## Actions (StartOS UI)
Grouped as displayed in the dashboard. The StartOS Actions tab is intentionally minimal — only the four operations
that must happen outside the embedded admin web UI are registered as actions:
**General** - *Set web UI password* — set / recover the admin SPA login password (you
- *Set operator name* — your public-facing brand. can't reset it from inside the UI if you're locked out).
- *Show credentials* — reveal the admin API key on first install, before
you've logged into the admin UI.
- *Activate Keysat license* — first-install bootstrap for paid self-hosting
tiers, and recovery if `/data/keysat-license.txt` is lost.
- *Show license status* — sanity-check the self-license state without
logging into the admin UI.
**BTCPay** Everything else — connecting BTCPay (and Zaprite), operator name, products,
- *Connect BTCPay* — one-click authorize against your BTCPay; auto-detects store and registers webhook. policies, discount / referral / free-license codes, licenses, machines,
- *Check BTCPay connection* — confirm BTCPay state; clears the install task on success. outbound webhooks, scoped API keys, and the audit log — lives in the embedded
**admin web UI** (Settings tab + the workspace sidebar), not as StartOS
**Credentials** actions.
- *Show admin credentials* — admin API key for direct `/v1/admin/*` access.
**Products + Policies**
- *Create product* — declare something to sell.
- *Create policy* — license template for a product (duration, grace, seat cap, entitlements, trial flag, price override).
**Discount codes**
- *Create discount code* — percent-off / fixed-sats-off / free-license.
- *List discount codes* — usage stats.
- *Disable / enable discount code*.
**Licenses**
- *Issue license manually* — comp / press / grandfathered keys.
- *Search licenses* — by email or BTCPay invoice id.
- *Suspend license* — reversible lockout.
- *Unsuspend license*.
- *Revoke license* — terminal kill.
**Machines**
- *List machines* — installs bound to a license.
- *Deactivate machine* — free a seat.
**Webhooks (outbound)**
- *Register webhook endpoint* — POST signed events to your URL.
- *List webhook endpoints*.
**Diagnostics**
- *View audit log* — admin mutation history, filterable.
## Backups and Restore ## Backups and Restore
@@ -228,7 +206,7 @@ Known current limitations:
- **No bulk / volume licensing UI.** "Buy 10 keys at once with discount" is not built into the buy page. Operators can issue N comp licenses via the admin API in a loop. - **No bulk / volume licensing UI.** "Buy 10 keys at once with discount" is not built into the buy page. Operators can issue N comp licenses via the admin API in a loop.
- **Webhook delivery retries are bounded.** A subscriber down past the 10-attempt retry window lands in the dead-letter queue (visible in admin UI → Webhooks → Failed). BTCPay invoice reconciliation runs as a background poll so dropped *payment* webhooks are recovered. - **Webhook delivery retries are bounded.** A subscriber down past the 10-attempt retry window lands in the dead-letter queue (visible in admin UI → Webhooks → Failed). BTCPay invoice reconciliation runs as a background poll so dropped *payment* webhooks are recovered.
- **Hardware fingerprinting is client-supplied.** Keysat does not derive fingerprints itself; the buyer-side SDK passes whatever the integrator chose. The fingerprint is bound on first activate and enforced thereafter. - **Hardware fingerprinting is client-supplied.** Keysat does not derive fingerprints itself; the buyer-side SDK passes whatever the integrator chose. The fingerprint is bound on first activate and enforced thereafter.
- **Card payments not shipped.** The Zaprite payment provider is in design for v0.3 — operators on Pro / Patron will get a card-payment option alongside BTCPay. Until then, payments are BTC / Lightning only. - **Card payments via Zaprite are gated.** Zaprite ships as an optional second payment provider (card / fiat alongside BTCPay) but is gated by the `zaprite_payments` entitlement — operators on the tiers that grant it can connect Zaprite in the admin web UI. BTCPay remains the required provider; without the entitlement, payments are BTC / Lightning only.
## What Is Unchanged from Upstream ## What Is Unchanged from Upstream
@@ -257,7 +235,7 @@ service:
marketingUrl: https://keysat.xyz marketingUrl: https://keysat.xyz
image: image:
source: dockerBuild source: dockerBuild
baseImage: scratch (musl-static Rust binary) baseImage: debian:bookworm-slim (musl-compiled Rust binary; bundles ca-certificates, tini, sqlite3)
arches: [x86_64, aarch64] arches: [x86_64, aarch64]
volumes: volumes:
- id: main - id: main
@@ -292,10 +270,10 @@ backups:
firstRun: firstRun:
tasks: tasks:
- id: btcpay-initial-setup - id: btcpay-initial-setup
severity: critical severity: important
runs: configureBtcpay runs: configureBtcpay
features: features:
paymentRail: btcpay-server # zaprite planned for v0.3 (card payments) paymentProviders: [btcpay-server, zaprite] # btcpay required; zaprite optional, gated by the zaprite_payments entitlement
signing: ed25519 signing: ed25519
offlineVerification: true offlineVerification: true
multiSeat: true multiSeat: true
@@ -315,7 +293,7 @@ features:
outboundWebhooks: true outboundWebhooks: true
webhookDlq: true # failed deliveries retryable from admin UI webhookDlq: true # failed deliveries retryable from admin UI
auditLog: true auditLog: true
scopedApiKeys: [read-only, license-issuer, support, full-admin] scopedApiKeys: [read-only, license-issuer, support, merchant-onboard, full-admin]
openapiSpec: /v1/openapi.json openapiSpec: /v1/openapi.json
selfLicensingTier: [Creator, Pro, Patron] selfLicensingTier: [Creator, Pro, Patron]
sdks: sdks:
+20 -37
View File
@@ -56,44 +56,21 @@ See [`src/crypto/mod.rs`](src/crypto/mod.rs) for the exact byte layout.
## Project layout ## Project layout
``` The daemon source lives under `src/`, organized by subsystem (browse it for the current layout — the tree below has grown well past the v0.1 snapshot):
licensing-service/
├── Cargo.toml - `main.rs`, `config.rs`, `error.rs`, `models.rs` — entry point, env-driven config, error → HTTP mapping, shared domain types.
├── LICENSE # source-available; no redistribution - `crypto/` — the LIC1 license-key byte layout and Ed25519 sign/verify (the contract the four SDKs implement).
├── README.md - `db/` — SQLite pool, migrations, and `repo.rs` (all SQL). `migrations/` holds the numbered, additive schema (0001 through the latest; the schema has grown substantially since 0001).
├── .env.example # required env vars - `payment/` (`btcpay/`, `zaprite/`) + `merchant_profiles.rs` — the payment-provider abstraction and multi-profile routing.
├── migrations/ - `reconcile.rs`, `subscriptions.rs`, `upgrades.rs` — the background worker (invoice reconciliation, recurring renewals, tier upgrades).
│ └── 0001_initial.sql # SQLite schema - `api/` — the ~30 route modules: public (`products`, `purchase`, `validate`, `redeem`) and admin (`admin*`, scoped API keys, webhooks, etc.), plus the router and `AppState` in `api/mod.rs`.
├── src/ - `web/index.html` — the embedded admin SPA.
│ ├── main.rs # entry point: wires everything
│ ├── config.rs # env-driven config Deeper docs: [`docs/API.md`](docs/API.md), [`docs/INTEGRATION.md`](docs/INTEGRATION.md), [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
│ ├── error.rs # unified error → HTTP mapping
│ ├── models.rs # shared domain types
│ ├── crypto/
│ │ ├── mod.rs # license key format + sign/verify
│ │ └── keys.rs # server keypair lifecycle
│ ├── db/
│ │ ├── mod.rs # pool + migrations
│ │ └── repo.rs # all SQL queries
│ ├── btcpay/
│ │ ├── client.rs # Greenfield API client
│ │ └── webhook.rs # HMAC verification + event parsing
│ └── api/
│ ├── mod.rs # router + AppState
│ ├── products.rs # public product endpoints
│ ├── purchase.rs # buy + poll
│ ├── validate.rs # the hot path for downstream software
│ ├── webhook.rs # BTCPay landing
│ └── admin.rs # operator-only actions
└── docs/
├── API.md # full endpoint reference
├── INTEGRATION.md # for developers embedding a client
└── ARCHITECTURE.md # deeper design notes
```
## Running locally ## Running locally
Prerequisites: Rust 1.75+, a BTCPay Server instance you can point at (local or hosted). Prerequisites: Rust 1.88+ (the build toolchain; the crate's Cargo.toml still declares MSRV 1.75, but the dependency tree now requires a newer compiler), a BTCPay Server instance you can point at (local or hosted).
```bash ```bash
cp .env.example .env cp .env.example .env
@@ -143,7 +120,7 @@ curl -X POST http://localhost:8080/v1/validate \
## Deploying on Start9 ## Deploying on Start9
This repository ships the service only. To package as an `.s9pk` for the 0.4.0.x platform you'll need a separate wrapper repository following [docs.start9.com/packaging/0.4.0.x](https://docs.start9.com/packaging/0.4.0.x/). The service is designed to slot in cleanly: The StartOS wrapper lives in **this same repository** under `../startos/` (this `licensing-service/` directory is the daemon source it bundles). Build the `.s9pk` for the 0.4.0.x platform from the parent directory — see the build/release guide and `../Makefile`. The service is designed to slot in cleanly:
- **Declares a dependency** on BTCPay Server in the manifest. StartOS will make BTCPay reachable at a `.startos` hostname and supply the env vars from the wrapper's action handlers. - **Declares a dependency** on BTCPay Server in the manifest. StartOS will make BTCPay reachable at a `.startos` hostname and supply the env vars from the wrapper's action handlers.
- **Persists to `/data`**, so everything (SQLite DB including the signing key) is covered by one-click encrypted backups. - **Persists to `/data`**, so everything (SQLite DB including the signing key) is covered by one-click encrypted backups.
@@ -170,4 +147,10 @@ Commercial redistribution / resale rights: contact licensing@keysat.xyz.
## Status ## Status
v0.1 — minimal working implementation. Feature direction after this is expected to cover: SDK crates for Rust and TypeScript, s9pk wrapper repository, richer admin UI, invoice reconciliation job for dropped webhooks, per-product webhook endpoints for the operator. 0.2.0 — shipped and in production. The current feature set:
- **Four published SDKs** — TypeScript (npm), Rust (crates.io), Python (PyPI), and Go — all wire-compatible against the cross-check fixtures in `tests/crosscheck/`.
- **StartOS wrapper included in this repo** under `../startos/`; build the `.s9pk` from the parent directory (no separate wrapper repository).
- **Embedded admin SPA** (`web/index.html`) for all day-to-day operations.
- **Subscriptions** (recurring auto-renew with trials + grace), **policies / tiers** with per-policy entitlements, **discount / referral / free-license codes**, **outbound webhooks** with a dead-letter queue, and a background **invoice reconciliation** job that recovers dropped payment webhooks.
- **Payment providers**: BTCPay Server is required; Zaprite (card / fiat) is optional and gated by the `zaprite_payments` entitlement.
+3 -3
View File
@@ -6,7 +6,7 @@ All endpoints are JSON in / JSON out. Errors return a body of the form:
{ "ok": false, "error": "not_found", "message": "product 'xyz'" } { "ok": false, "error": "not_found", "message": "product 'xyz'" }
``` ```
Admin endpoints require `Authorization: Bearer $LICENSING_ADMIN_API_KEY`. Admin endpoints require `Authorization: Bearer $KEYSAT_ADMIN_API_KEY`.
--- ---
@@ -128,7 +128,7 @@ On failure:
{ "ok": false, "reason": "revoked" } { "ok": false, "reason": "revoked" }
``` ```
Possible `reason` values: `bad_format`, `bad_signature`, `not_found`, `revoked`, `product_mismatch`, `fingerprint_mismatch`. Possible `reason` values: `bad_format`, `bad_signature`, `not_found`, `revoked`, `suspended`, `expired`, `product_mismatch`, `fingerprint_mismatch`, `too_many_machines` (multi-seat cap reached).
### `POST /v1/btcpay/webhook` ### `POST /v1/btcpay/webhook`
@@ -138,7 +138,7 @@ Landing point for BTCPay Server webhook events. Only BTCPay should call this. We
## Admin endpoints ## Admin endpoints
All of these require `Authorization: Bearer $LICENSING_ADMIN_API_KEY`. All of these require `Authorization: Bearer $KEYSAT_ADMIN_API_KEY`.
### `POST /v1/admin/products` ### `POST /v1/admin/products`
+8 -4
View File
@@ -12,13 +12,18 @@
## Data model ## Data model
See [`migrations/0001_initial.sql`](../migrations/0001_initial.sql). Five tables: The schema lives in [`migrations/`](../migrations/) as numbered, additive
migrations (0001 through the latest — it has grown substantially past the
original five-table v0.1 schema, adding discount codes, tiered pricing,
multi-currency, subscriptions, tier upgrades, per-product entitlement catalogs,
scoped API keys, merchant profiles, and more). The core tables established in
[`0001_initial.sql`](../migrations/0001_initial.sql):
- `products` — what's for sale. Independent pricing per product. - `products` — what's for sale. Independent pricing per product.
- `invoices` — one per purchase attempt, keyed by BTCPay's invoice id. - `invoices` — one per purchase attempt, keyed by BTCPay's invoice id.
- `licenses` — one per successful payment (or manual issuance). Has optional `fingerprint` (machine bind) and `bound_identity` (user bind) columns. - `licenses` — one per successful payment (or manual issuance). Has optional `fingerprint` (machine bind) and `bound_identity` (user bind) columns. Later migrations add `expires_at`, entitlements, trial flag, and tier columns.
- `validation_log` — append-only audit log of every validate call. Useful for detecting abuse (same key, many fingerprints) and for rate-limiting layers above us. - `validation_log` — append-only audit log of every validate call. Useful for detecting abuse (same key, many fingerprints) and for rate-limiting layers above us.
- `server_keys` — singleton table holding the server's Ed25519 keypair. Generated on first boot, never rotated in v0.1 (rotation is a planned feature). - `server_keys` — singleton table holding the server's Ed25519 keypair. Generated on first boot.
## License key format ## License key format
@@ -63,7 +68,6 @@ Who might attack this?
- **Key rotation.** A single static signing key is fine for first launch. Rotation requires SDK multi-key support and a migration strategy; deferred. - **Key rotation.** A single static signing key is fine for first launch. Rotation requires SDK multi-key support and a migration strategy; deferred.
- **Trial periods / demos.** This is a pure paid-license server. Trials are the developer's responsibility in-app. - **Trial periods / demos.** This is a pure paid-license server. Trials are the developer's responsibility in-app.
- **Payment currencies other than BTC.** BTCPay supports Lightning, altcoins, and fiat; we only send BTC-denominated invoices. Adding Lightning is straightforward (BTCPay handles it transparently if the store has LN configured). - **Payment currencies other than BTC.** BTCPay supports Lightning, altcoins, and fiat; we only send BTC-denominated invoices. Adding Lightning is straightforward (BTCPay handles it transparently if the store has LN configured).
- **Subscription / time-limited licenses.** The payload has an `issued_at` field but no `expires_at`. Adding expiry is a later schema + payload change.
- **Multi-tenant / SaaS mode.** This is a *single-operator* server by design. Running multiple logical operators on one instance is a different product. - **Multi-tenant / SaaS mode.** This is a *single-operator* server by design. Running multiple logical operators on one instance is a different product.
- **Admin UI.** Everything is API-driven. Wrap it in whatever UI you like — or just use `curl`. - **Admin UI.** Everything is API-driven. Wrap it in whatever UI you like — or just use `curl`.
+10 -1
View File
@@ -25,9 +25,18 @@ curl -s https://license.example.com/v1/pubkey | jq -r .public_key_pem
Commit the resulting PEM into your client source tree. **Do not fetch it dynamically at runtime** — that would let an attacker who compromises your licensing server swap the key and re-sign forged licenses retroactively. A pinned public key is the whole point. Commit the resulting PEM into your client source tree. **Do not fetch it dynamically at runtime** — that would let an attacker who compromises your licensing server swap the key and re-sign forged licenses retroactively. A pinned public key is the whole point.
> **Official SDKs exist — use them first.** Four wire-compatible client SDKs
> are published: TypeScript (`@keysat/licensing-client` on npm), Rust
> (`keysat-licensing-client` on crates.io), Python (`keysat-licensing-client`
> on PyPI), and Go (`github.com/keysat-xyz/keysat-client-go`). Install commands
> are in the main README. The by-hand reference implementations below are a
> fallback for languages without an SDK, or for understanding exactly what the
> SDKs do under the hood.
## Reference integration in Rust ## Reference integration in Rust
This is what a Start9 package written in Rust might look like. No SDK crate yet — that's planned; here's what you'd write by hand: This is what a Start9 package written in Rust might look like if you verify by
hand instead of using the Rust SDK:
```rust ```rust
use anyhow::{Context, Result}; use anyhow::{Context, Result};