diff --git a/instructions.md b/instructions.md new file mode 100644 index 0000000..3e4d160 --- /dev/null +++ b/instructions.md @@ -0,0 +1,87 @@ +# Keysat Licensing — Instructions + +Keysat is a Bitcoin-native, self-hosted licensing service for software +creators. You run your own instance, hold your own signing key, and issue +Ed25519-signed license keys that your software verifies offline. There is no +central authority and no shared database. + +## Before you start + +- **BTCPay Server is required.** Install and start BTCPay Server first — Keysat + uses it to take Bitcoin/Lightning payments and confirm settlement. StartOS + lists this dependency before it lets you install Keysat. +- **A clearnet domain is recommended if you sell to the public**, so buyers + anywhere can reach your checkout. LAN/Tor-only works for testing. +- **Zaprite is optional** (adds card payments). You connect it later from inside + the admin web UI; nothing to do up front. + +## First-time setup + +1. **Get your admin API key.** Open the **Actions** tab and run + **Show admin API key**. Copy it — you sign into the admin web UI with it the + first time. +2. **Open the admin dashboard.** Click **Launch UI** on the **Admin Web UI** + interface and paste the admin API key to sign in. +3. **(Recommended) Set a real password.** Run the **Set web UI password** action + (Actions tab, minimum 12 characters). After this the login page shows a + password field; the admin API key keeps working for automation. +4. **Connect your payment provider.** In the admin web UI's Settings, use the + one-click **Connect BTCPay** flow to authorize Keysat against your BTCPay + Server. (Optionally connect Zaprite here too.) +5. **Set your operator name** in the admin web UI — it appears on buyer-facing + checkout and receipts. +6. **Create what you sell.** Use **Create product** for each item, and + optionally **Create policy** to set per-product defaults (duration, grace + period, entitlements, seat cap, trial flag). A policy slugged `default` is the + one the public purchase flow uses. + +Activation is optional. Keysat runs out of the box at the free **Creator** tier +(up to 5 products, 5 policies per product, and 10 active discount codes). +Activating a license lifts those caps and unlocks recurring billing and Zaprite +(card) payments. To activate, get a key at +[registry.keysat.xyz](https://registry.keysat.xyz), run the **Activate Keysat +license** action, and confirm with **Show Keysat license status**. + +## Selling licenses + +Share your **Licensing API** URL with buyers and bake it into your software as +the validation endpoint. Buyers call `POST /v1/purchase`, pay via BTCPay, and +Keysat issues a signed license key. Your software validates keys against +`POST /v1/validate` — including revocation checks, which return +`ok: false` with `reason: "revoked"`. + +The same admin web UI covers manual license issuance (comps, press, trials), +suspension/unsuspension, revocation, machine management, discount codes, +outbound webhooks, and the audit log. + +## Interfaces and exposure + +- **Licensing API** (`/`) — public-facing. This is the URL you share with + customers and bake into your builds. +- **Admin Web UI** (`/admin`) — your dashboard. Restrict this interface to LAN or + Tor only; the public internet does not need to reach it. +- **BTCPay webhook endpoint** (`/btcpay`) — registered with BTCPay automatically + during the Connect BTCPay flow. Not for human use. + +## Backups and uninstalling + +Your data volume holds the SQLite database — which contains your server signing +key and every license record — and StartOS backs it up automatically. Your +self-license at `/data/keysat-license.txt` is included in the backup and +survives upgrades and reinstalls. + +**Uninstalling deletes your signing key and all license records.** Once it is +gone, previously issued license keys no longer validate against this server. Back +up first if you plan to reinstall. + +## Recovery + +- **Locked out of the admin UI?** Run **Set web UI password** to set a new one, + or **Show admin API key** to sign in with the key. +- **Lost your Keysat license?** Re-run **Activate Keysat license** with your key. + +## More + +Full developer and integration documentation lives in the upstream repository +(`README.md` and `KEYSAT_INTEGRATION.md`) and at +[keysat.xyz](https://keysat.xyz). diff --git a/startos/actions/activateLicense.ts b/startos/actions/activateLicense.ts index 7296dcc..315ff8f 100644 --- a/startos/actions/activateLicense.ts +++ b/startos/actions/activateLicense.ts @@ -6,12 +6,10 @@ // writes it to /data/keysat-license.txt, and swaps its runtime tier // to Licensed without a restart. // -// In permissive builds (the default for local `make x86`) the daemon -// will start regardless and this action just records the tier. In -// enforce builds (compiled with KEYSAT_LICENSE_ENFORCE=1, used for -// the marketplace .s9pk) the daemon refuses to start without a valid -// license, and this action is the bootstrap path: install Keysat, -// run this action with your activation key, then start the service. +// The daemon always boots regardless of license state (enforce mode was +// retired — see license_self.rs::check_at_boot). With no valid self-license +// it runs at the free Creator tier with Creator caps; this action records +// the license and lifts those caps without a restart. import { sdk } from '../sdk' import { store } from '../fileModels/store' @@ -36,9 +34,9 @@ export const activateLicense = sdk.Action.withInput( async () => ({ name: 'Activate Keysat license', description: - 'Activate this Keysat install. Required for marketplace builds; ' + - 'optional but recommended for source-built dev installs (signals support, ' + - 'and lets the admin UI show your tier).', + 'Activate this Keysat install. Optional — Keysat runs at the free ' + + 'Creator tier without it. Activating lifts the Creator caps, unlocks ' + + 'recurring billing + Zaprite payments, and shows your tier in the admin UI.', warning: null, allowedStatuses: 'only-running', group: 'License', @@ -80,7 +78,6 @@ export const activateLicense = sdk.Action.withInput( product_id?: string expires_at?: number entitlements?: string[] - mode: string } message: string } @@ -132,7 +129,6 @@ export const showLicenseStatus = sdk.Action.withoutInput( expires_at?: number entitlements?: string[] reason?: string - mode: string } if (j.tier === 'licensed') { @@ -146,20 +142,19 @@ export const showLicenseStatus = sdk.Action.withoutInput( message: `License id: ${j.license_id}\n` + `Expires: ${exp}\n` + - `Entitlements: ${ents}\n` + - `Build mode: ${j.mode}`, + `Entitlements: ${ents}`, result: null, } } else { return { version: '1', - title: 'Unlicensed', + title: 'Creator (free tier)', message: - `Reason: ${j.reason || 'no license configured'}\n` + - `Build mode: ${j.mode}\n\n` + - (j.mode === 'enforce' - ? 'This is a marketplace build that requires a valid license to run. Use the "Activate Keysat license" action to bootstrap.' - : 'This is a permissive (dev) build. The daemon will keep running. Activate a license to see your tier reflected here.'), + `This install is running at the free Creator tier.\n` + + `Reason: ${j.reason || 'no license configured'}\n\n` + + `Creator caps: 5 products, 5 policies per product, 10 active ` + + `discount codes. Activating a license lifts these caps and unlocks ` + + `recurring billing + Zaprite payments (the "Activate Keysat license" action).`, result: null, } } diff --git a/startos/actions/showCredentials.ts b/startos/actions/showCredentials.ts index 0c40ebf..24184f7 100644 --- a/startos/actions/showCredentials.ts +++ b/startos/actions/showCredentials.ts @@ -1,8 +1,9 @@ // Action: reveal the auto-generated admin API key. // -// The operator rarely needs this — every other action in StartOS already -// carries the key for them — but it's useful if they want to script against -// the admin HTTP API directly. +// The operator needs this on first install to sign into the admin web UI +// (until they set a web UI password); afterward it's mainly for scripting +// the admin HTTP API directly, since every other StartOS action already +// carries the key for them. // // The BTCPay webhook secret used to live in the StartOS store; it now lives // inside the daemon's own SQLite database, generated automatically during @@ -35,9 +36,11 @@ export const showCredentials = sdk.Action.withoutInput( version: '1', title: 'Admin API key', message: - `Used as 'Authorization: Bearer ' against /v1/admin/*. All ` + - `StartOS actions already supply this for you — only export it if ` + - `you intend to script against the admin API from outside the box.`, + `This is your admin API key — the 'Authorization: Bearer ' ` + + `credential for /v1/admin/*. Use it to sign into the admin web UI on ` + + `first install (until you set a web UI password). Every StartOS action ` + + `already supplies it for you, so you only need to export it to script ` + + `the admin API yourself.`, result: { type: 'single', value: storeData.admin_api_key, diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index 4ee80d9..257df2b 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -15,13 +15,15 @@ export const manifest = setupManifest({ id: 'keysat', title: 'Keysat Licensing', license: 'LicenseRef-Keysat-1.0', - packageRepo: 'https://github.com/keysat-xyz/keysat-startos', + // packageRepo (the s9pk wrapper source) and upstreamRepo (the daemon source) + // are the same URL: the StartOS wrapper and the Rust daemon share one monorepo. + packageRepo: 'https://github.com/keysat-xyz/keysat', upstreamRepo: 'https://github.com/keysat-xyz/keysat', marketingUrl: 'https://keysat.xyz', donationUrl: null, docsUrls: [ 'https://github.com/keysat-xyz/keysat/blob/main/README.md', - 'https://github.com/keysat-xyz/keysat/blob/main/docs/INTEGRATION.md', + 'https://github.com/keysat-xyz/keysat/blob/main/KEYSAT_INTEGRATION.md', ], description: { short, long }, // A single data volume holds the SQLite database (which in turn holds the diff --git a/startos/versions/v0.2.0.ts b/startos/versions/v0.2.0.ts index 9e8932e..d488eae 100644 --- a/startos/versions/v0.2.0.ts +++ b/startos/versions/v0.2.0.ts @@ -1,27 +1,8 @@ -// Draft of the v0.2.0 milestone version entry. -// -// NOT YET WIRED INTO `versions/index.ts` — this file sits ready to -// use when we cut v0.2.0:0 from the alpha-iteration line. To -// activate: -// 1. In `versions/index.ts`: -// import { v0_2_0 } from './v0.2.0' -// export const versions = VersionGraph.of({ -// current: v0_2_0, -// other: [v0_1_0], // ← so installs on 0.1.0:N can upgrade -// }) -// 2. Build the .s9pk (`make x86`). -// 3. Publish via `~/.keysat/publish.sh` (the version-changed gate -// will fire because `0.2.0:0` differs from the recorded -// `0.1.0:N`). -// -// Why this draft exists separately: -// - The cut is an irreversible release decision for already-installed -// operators (downgrade paths exist in StartOS but they're sticky). -// - Wiring it in changes how StartOS computes the upgrade dialog -// shown to operators on registry refresh — best to QA the -// release-notes content in this file before flipping the switch. -// - Lets us write the v0.2.0 release notes carefully and then ship -// them all at once, rather than amending mid-build. +// The v0.2.0 milestone version entry — the current, active version on +// the v0.2 line. Wired into `versions/index.ts` as `current: v0_2_0`, +// with `v0_1_0` in `other` so installs on 0.1.0:N can upgrade. Routine +// wrapper updates bump the downstream revision here (`0.2.0:N`) before +// each build/publish; see startos-packaging.md. // // Version-string format reminder: ExVer is `:`. // The `` bump from 0.1.0 → 0.2.0 marks the milestone; the