# StartOS activate-license template A drop-in folder that adds Keysat licensing to your existing Start9 (StartOS 0.4.0.x) package. Your buyers get: - **"Buy license"** — pay with Bitcoin/Lightning on your BTCPay, never copy/paste the key. Optional discount/referral code field. - **"Finish license purchase"** — after the buyer pays, fetch the issued key and persist it locally. - **"Redeem free license"** — paste a free-license code from the seller; key is issued immediately, no payment. - **"Activate license"** — paste a key you got out-of-band. - **"Check license status"** — re-verify on demand. - **"Deactivate license"** — wipe the key locally. All actions work offline for the common case (boot-time signature check) and reach out to your Keysat instance for revocation and fingerprint enforcement only when the device is online. ## Prerequisites 1. You (the seller) are running [`Keysat`](https://github.com/keysat-xyz/keysat) on your own Start9, with BTCPay connected. 2. You have created a product in your Keysat and have its **slug**. 3. You know your Keysat's **public URL** (clearnet, .onion, or both). 4. You have your Keysat's **Ed25519 public key in PEM form**. Get it from the "Show admin credentials" action on your Start9, or hit `GET /v1/pubkey` on your service. 5. The action files import `sdk` from `'../sdk'` — a module you must provide. Point that path at (or alias it to) your package's own `@start9labs/start-sdk` singleton; otherwise the template won't compile. ## Install Copy these folders into your buyer-side package's `startos/` directory: ``` startos/licensing/ ← config, store shape, runtime gate startos/actions/ ← six actions (merge with your existing actions/) ``` Add the SDK to your package's `package.json`: ```bash npm install @keysat/licensing-client ``` ## Wire-up, in 4 edits ### 1. Fill in `startos/licensing/config.ts` There are four constants to set: `LICENSING_BASE_URL`, `PRODUCT_SLUG`, `ISSUER_PUBKEY_PEM`, and `PRODUCT_DISPLAY_NAME`. Each is documented inline. `PRODUCT_DISPLAY_NAME` is interpolated into every user-visible string, so leaving it at the default is a real mistake, not a cosmetic one. ### 2. Extend your store shape In the file where you call `sdk.setupStore(...)` (usually `startos/init/index.ts`): ```ts import { licensingStoreDefaults, type LicensingStoreFields } from '../licensing/store' export interface StoreShape extends LicensingStoreFields { // ...your existing fields... } export const initStore: StoreShape = { // ...your existing defaults... ...licensingStoreDefaults, } ``` ### 3. Register the actions In `startos/actions/index.ts`: ```ts import { licensingActions } from './licensing/actions' // or wherever you dropped them export const actions = sdk.Actions.of() // ...your own actions... .addAction(licensingActions.activateLicense) .addAction(licensingActions.buyLicense) .addAction(licensingActions.finishLicensePurchase) .addAction(licensingActions.redeemFreeLicense) .addAction(licensingActions.checkLicense) .addAction(licensingActions.deactivateLicense) ``` ### 4. Gate your app at startup In `startos/main.ts`, before you launch your daemon: ```ts import { checkLicenseGate } from './licensing/gate' export const main = sdk.setupMain(async ({ effects, started }) => { const store = await sdk.store.getOwn(effects, sdk.StorePath).const() const gate = checkLicenseGate(store) if (!gate.licensed) { // Pick your poison: // (a) hard-fail: throw new Error( `No valid license. Run the "Buy license" or "Activate license" action. ` + `(${gate.reason})`, ) // (b) OR boot in trial mode by passing an env flag to your daemon, // e.g. LICENSE_STATE=trial. } // ...normal startup... }) ``` ## What the buyer sees 1. Installs your package on their Start9. 2. Sees six new actions in the dashboard under the "License" group. 3. Either: - Clicks **Buy license** → opens the returned URL → pays → clicks **Finish license purchase** → done. - Pastes a key they got from you into **Activate license** → done. 4. Your app boots. No typing long keys. No copying bearer tokens. Keys live in the Start9 store, which means they are included in the buyer's regular Start9 backups. ## Revocation and fingerprint binding The **online** path (any time the buyer's Start9 can reach your licensing server) does three things the offline path cannot: - **Revocation**: if you revoke a leaked key server-side, that rejection propagates to the buyer on their next online check. - **Fingerprint binding**: the first device to check in with a key binds it. The same key replayed from a different machine is rejected. - **Audit trail**: your admin dashboard sees the check-in. The offline path (signature-only) is the fallback. It always succeeds as long as the key was legitimately issued and the signature is intact. This is the right tradeoff: a buyer with a revoked key who never goes online can still run the app, which is an edge case; a buyer whose internet drops should not have their paid software held hostage. ## License of this template MIT.