136 lines
4.6 KiB
Markdown
136 lines
4.6 KiB
Markdown
# StartOS activate-license template
|
|
|
|
A drop-in folder that adds Bitcoin-native 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.
|
|
- **"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.
|
|
|
|
## Install
|
|
|
|
Copy these folders into your buyer-side package's `startos/` directory:
|
|
|
|
```
|
|
startos/licensing/ ← config, store shape, runtime gate
|
|
startos/actions/ ← five 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 three constants to set: `LICENSING_BASE_URL`, `PRODUCT_SLUG`, and
|
|
`ISSUER_PUBKEY_PEM`. Each is documented inline.
|
|
|
|
### 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.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 four 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.
|