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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user