v0.1.0:24 — Keysat licensing service end-to-end

Daemon, StartOS wrapper, admin SPA, public buy/thank-you pages,
discount codes, free-license redemption, Apply-discount UX,
self-licensing, and v0.1.0 release notes.
This commit is contained in:
Grant
2026-05-07 10:33:39 -05:00
parent 432250bffc
commit 6ac118ae70
90 changed files with 14896 additions and 524 deletions
+274 -120
View File
@@ -1,128 +1,282 @@
# keysat-startos
<p align="center">
<img src="icon.png" alt="Keysat" width="128" />
</p>
StartOS 0.4.0.x wrapper package for [Keysat](../licensing-service) (the Rust daemon in `../licensing-service/`). This directory turns the upstream Rust daemon into an installable `.s9pk`.
<h1 align="center">Keysat</h1>
The source directory is still called `licensing-service/` on disk for continuity; the binary it produces, the manifest id, and all operator-visible strings use the new name **Keysat**.
<p align="center">
Self-hosted, Bitcoin-paid software-licensing service for Start9.
</p>
## Prerequisites
> **About this README.** Keysat is a from-scratch service authored for
> StartOS — there is no upstream project to differ from. The canonical
> implementation is this package and the Rust daemon it wraps
> (`licensing-service/`). Where this README would normally explain
> "differences from upstream," it instead documents the architecture
> directly. Anything that isn't documented here matches the source.
- A working StartOS 0.4.0.x development environment (see [docs.start9.com](https://docs.start9.com)).
- `start-cli` installed, with `~/.startos/developer.key.pem` initialized.
- Node.js and npm (the StartOS SDK is TypeScript).
- Docker (via buildx) for the multi-arch image build.
## Table of Contents
## Getting the shared build logic
- [What Keysat is](#what-keysat-is)
- [Image and Container Runtime](#image-and-container-runtime)
- [Volume and Data Layout](#volume-and-data-layout)
- [Installation and First-Run Flow](#installation-and-first-run-flow)
- [Configuration Management](#configuration-management)
- [Network Access and Interfaces](#network-access-and-interfaces)
- [Actions (StartOS UI)](#actions-startos-ui)
- [Backups and Restore](#backups-and-restore)
- [Health Checks](#health-checks)
- [Dependencies](#dependencies)
- [Limitations and Differences](#limitations-and-differences)
- [What Is Unchanged from Upstream](#what-is-unchanged-from-upstream)
- [Contributing](#contributing)
- [YAML Quick Reference](#yaml-quick-reference)
The `Makefile` includes `s9pk.mk`, which is shared build boilerplate maintained by the Start9 team. Fetch it once:
## What Keysat is
```bash
curl -o s9pk.mk https://raw.githubusercontent.com/Start9Labs/hello-world-startos/master/s9pk.mk
Keysat lets a software seller issue, validate, and revoke license keys for
their own product, with payment in Bitcoin via BTCPay Server. The seller
runs Keysat on their own Start9, declares one or more products, and shares
a public purchase URL with their customers. Buyers pay in Bitcoin and
receive a signed license key whose authenticity their software can verify
offline against the seller's embedded public key. Keys can be capped to
specific machines, time-limited, suspended, revoked, or marked as trial.
Discount and referral codes (paid and free-license) are first-class
primitives. Free-license codes bypass BTCPay entirely and issue a key
directly via a public redemption endpoint — useful for press passes,
comp keys, beta access, or "first N users free" launch promos.
## Image and Container Runtime
Built from the local `Dockerfile` via `images.main.source.dockerBuild`,
with build context set to the parent directory so the Dockerfile can
`COPY` from the sibling `licensing-service/` source tree. The Rust binary
is statically linked against musl (target
`*-unknown-linux-musl`) so the runtime image is a `scratch`-based final
stage with no shared-library dependencies. Architectures: `x86_64` and
`aarch64`.
`start-cli s9pk pack` ingests the resulting OCI image, converts it to a
squashfs filesystem image, and embeds that in the `.s9pk`. At runtime
StartOS extracts the squashfs and runs the service in its own container
runtime.
## Volume and Data Layout
Keysat declares a single persistent volume:
| Volume | Mount | Contents |
|--------|--------|---------------------------------------------------------|
| `main` | `/data`| SQLite database (`keysat.db`); contains the Ed25519 signing keypair, products, policies, licenses, machines, invoices, redemptions, audit log, and BTCPay credentials. |
Loss of this volume invalidates every issued license, since the signing
keypair is regenerated on first boot. Treat StartOS-managed backups as
mandatory.
## Installation and First-Run Flow
1. Install Keysat via the marketplace (or sideload the `.s9pk`).
2. Resolve the auto-created **critical task** "Connect BTCPay" by
running the **Connect BTCPay** action. This opens a one-click
authorize page on your local BTCPay; after approval, Keysat
auto-detects your store and registers an inbound webhook. No API
keys to copy.
3. Run **Check BTCPay connection** to confirm — the install task clears
automatically.
4. Set your **operator name** (shown on the public homepage and in
buyer-facing receipts).
5. Create one or more **products** — each represents something you sell.
6. Create at least one **policy** per product. The policy slugged
`default` is consumed by the standard public purchase flow; other
slugs are used for manual issuance. Policies define duration, grace
period, seat cap, entitlements, trial flag, and price overrides.
7. Optionally create **discount / referral / free-license codes** (see
`Create discount code` action).
8. Share the public service URL with buyers.
## Configuration Management
All configuration is performed through StartOS actions; there is no
on-disk config file the operator should edit. Environment variables
passed to the daemon at startup (`main.ts`) are derived from the
package-local store (operator name, admin API key) and from the
declared BTCPay dependency hostname.
For advanced operators, the `/v1/admin/*` HTTP API exposes everything
the actions do plus bulk-list operations not yet surfaced in the UI.
Retrieve the admin API key via the **Show admin credentials** action.
## Network Access and Interfaces
Keysat exposes one logical port (8080 HTTP) split across two service
interfaces for clarity:
| 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. |
| `webhook` | api | `/btcpay` | BTCPay webhook landing endpoint. Registered automatically during Connect BTCPay; not for human use. |
StartOS terminates TLS at the platform edge. Inside the container every
request arrives as plain HTTP. For browser-facing URLs (e.g., the public
purchase page) hardcode `https://`.
## Actions (StartOS UI)
Grouped as displayed in the dashboard.
**General**
- *Set operator name* — your public-facing brand.
**BTCPay**
- *Connect BTCPay* — one-click authorize against your BTCPay; auto-detects store and registers webhook.
- *Check BTCPay connection* — confirm BTCPay state; clears the install task on success.
**Credentials**
- *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, Nostr npub, 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
Keysat opts into StartOS's default volume backup via `setupBackups` /
`Backups.ofVolumes('main')`. The single `main` volume contains all
state — signing key included — so a backup is sufficient to fully
recover the service. On restore, the install-time **Connect BTCPay**
task re-surfaces in case the BTCPay credentials in the restored DB are
stale.
Treat backups as mandatory: losing the signing keypair invalidates every
key Keysat ever issued, with no recovery path.
## Health Checks
A single port-listening check on port 8080 (`sdk.healthCheck.checkPortListening`).
StartOS reports the service as healthy once the daemon is binding the
port. The daemon exposes `GET /healthz` for richer external monitoring.
## Dependencies
| Dependency | Version range | Required | Purpose |
|-------------|---------------|----------|---------------------------------------------------------------|
| `btcpayserver` | `>=1.11.0` | Yes | Required to receive Bitcoin payments and confirm settlement. |
The dependency is `kind: 'running'`, so Keysat will not start until
BTCPay is running. The `btcpayserver.startos` hostname is provided to
the container automatically.
## Limitations and Differences
Known v0.1 limitations:
- **No buyer self-service portal.** Buyers cannot log in to view their licenses, transfer to a new machine, or recover a lost key without contacting the operator. Use *Search licenses* to recover.
- **No recurring subscriptions.** Time-limited licenses expire and require a manual repurchase. BTCPay supports recurring billing but Keysat does not yet model auto-renewal.
- **No license tier upgrade in place.** A buyer who got Standard cannot be upgraded to Pro on the existing key — they need a new key.
- **No bulk / volume licensing.** "Buy 10 keys at once with discount" is not built in.
- **No in-dashboard list views.** Operators query large datasets via the admin API key rather than a paginated UI.
- **Webhook delivery retries are bounded.** A subscriber down past the retry window will miss events. 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.
## What Is Unchanged from Upstream
Not applicable — Keysat is authored fresh for Start9 and has no upstream.
The canonical implementation IS this package + the Rust daemon at
`licensing-service/`.
## Contributing
For commercial redistribution or resale rights, or to discuss white-label
deployment, contact `licensing@keysat.xyz`. Source-available license
terms are in the package's `LICENSE` file: you may run, audit, modify
for self-hosting; you may not redistribute, resell, or publicly host for
others.
## YAML Quick Reference
Structured summary for AI consumers and automated package introspection.
```yaml
service:
id: keysat
title: Keysat
category: bitcoin
license: source-available (LicenseRef-Proprietary)
marketingUrl: https://keysat.xyz
image:
source: dockerBuild
baseImage: scratch (musl-static Rust binary)
arches: [x86_64, aarch64]
volumes:
- id: main
mountpoint: /data
contents: SQLite DB + Ed25519 signing keypair
network:
interfaces:
- id: api
type: api
port: 8080
protocol: http
pathPrefix: /
audience: public
- id: webhook
type: api
port: 8080
protocol: http
pathPrefix: /btcpay
audience: btcpay
dependencies:
btcpayserver:
required: true
versionRange: ">=1.11.0"
kind: running
healthChecks:
- id: api
method: portListening
port: 8080
backups:
mode: full-volume
volumes: [main]
firstRun:
tasks:
- id: btcpay-initial-setup
severity: critical
runs: configureBtcpay
features:
paymentRail: btcpay-server
signing: ed25519
offlineVerification: true
multiSeat: true
trialFlag: true
expiry: true
gracePeriod: true
entitlements: true
discountCodes: [percent, fixed_sats, free_license]
outboundWebhooks: true
auditLog: true
selfLicensingTier: stub-v0.1
```
(Or copy it from any other 0.4.0.x package you have locally.)
## Installing dependencies
```bash
npm install
```
## Building and installing
```bash
# Build for all supported architectures
make
# Or just the architecture of your dev StartOS box
make arm # for Raspberry Pi / Apple Silicon StartOS
make x86 # for an x86 StartOS server
# Push to your StartOS server (requires the developer key)
make install
```
`make install` will prompt for your StartOS password the first time; subsequent installs use the cached session.
## Project layout
This follows the standard 0.4.0.x layout:
```
keysat-startos/
├── Dockerfile # multi-stage Rust build
├── Makefile # delegates to s9pk.mk
├── s9pk.mk # (fetch from hello-world-startos)
├── package.json / tsconfig.json
├── icon.png # 512×512 StartOS tile
├── assets/
│ ├── ABOUT.md
│ └── keysat-thumbnail.png # 1024×1024 marketing hero
└── startos/
├── manifest/index.ts # setupManifest()
├── manifest/i18n.ts # descriptions, translatable
├── main.ts # daemon definition
├── interfaces.ts # network exposure (API on 8080)
├── dependencies.ts # requires BTCPay Server
├── actions/ # user-facing StartOS buttons
│ ├── configureBtcpay.ts # one-click BTCPay authorize
│ ├── createPolicy.ts # reusable license template
│ ├── createProduct.ts
│ ├── deactivateMachine.ts # force-kick an install
│ ├── issueLicense.ts # comp / press keys
│ ├── listMachines.ts # inspect a license's seats
│ ├── listWebhooks.ts
│ ├── registerWebhook.ts # outbound event subscriber
│ ├── revokeLicense.ts # one-way permanent block
│ ├── searchLicenses.ts # lost-key recovery
│ ├── setOperatorName.ts
│ ├── showCredentials.ts
│ ├── suspendLicense.ts # reversible lockout
│ ├── unsuspendLicense.ts
│ └── viewAuditLog.ts
├── fileModels/store.ts # persistent wrapper state
├── init/index.ts # first-boot setup
├── versions/ # migration history
│ ├── index.ts
│ └── v0.1.0.ts
├── backups.ts # volume backup declaration
├── sdk.ts # manifest-bound SDK instance
├── utils.ts # small helpers
└── index.ts # ties everything together
```
## Dockerfile notes
The Dockerfile expects the `licensing-service/` source to be available at the parent directory (`..`). The manifest sets `images.main.source.dockerBuild.workdir` to `'..'` so `start-cli s9pk pack` runs `docker build` with the parent `Licensing/` directory as the context — Docker then sees the licensing-service source alongside this wrapper. A `.dockerignore` at the parent level keeps the uploaded context small.
If you're laying out the repositories differently — e.g., separate GitHub repos for service and wrapper — you'll want to add a git submodule or adjust the `workdir`/`COPY` paths accordingly.
## Operator workflow after install
1. Open the service in your StartOS dashboard.
2. **Set operator name** → your display name, shown on the public homepage.
3. **Connect BTCPay** → one-click authorize flow. Opens BTCPay's consent page in your browser; after you approve, the daemon auto-detects your store and registers its inbound webhook. No API keys to copy.
4. **Check BTCPay connection** to confirm the authorize succeeded.
5. **Create product** once per thing you want to sell.
6. **Create policy** at least once per product, slugged `default`, to set the shape of keys issued through the public purchase flow (duration, grace period, entitlements, seat cap).
7. Share the public service URL with buyers. That's enough for the standard purchase flow.
### Customer support
- **Search licenses** — look up a buyer by email, Nostr npub, or BTCPay invoice id.
- **Suspend license** / **Unsuspend license** — reversible lockout (e.g., for payment disputes).
- **Revoke license** — permanent, one-way kill.
- **Issue license manually** — comp / press / grandfathered keys.
- **List machines** — see which installs are bound to a license.
- **Deactivate machine** — force-kick a specific install, freeing a seat.
### Integrations & operations
- **Register webhook endpoint** — POST signed event notifications to an HTTPS URL you control (license.issued, license.revoked, machine.activated, etc.). HMAC-SHA256 in `X-Keysat-Signature: sha256=<hex>`.
- **List webhook endpoints** — see what's subscribed.
- **View audit log** — most recent admin mutations, filterable by action slug. Useful for compliance and debugging.
- **Show admin API key** — only needed if you want to script against `/v1/admin/*` from outside the box; every built-in action already carries the key for you.
## Limitations in v0.1
- No in-dashboard list view for invoices/products/licenses — use `/v1/admin/...` via the admin API key if you need a bulk view beyond what the built-in actions surface.
- Webhook delivery retries are bounded; if a subscriber is down past the retry window, the event is dropped. Invoice reconciliation runs as a background task so dropped BTCPay webhooks get replayed.