Initial public commit

This commit is contained in:
Keysat
2026-05-07 10:41:57 -05:00
commit 8c9bc75e24
12 changed files with 906 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
// Action: "Activate license" — buyer pastes a license key, we verify it (both
// offline against the embedded issuer public key, and online against the
// licensing server if reachable) and persist it in the package store.
//
// After this runs successfully, your main process can read
// `store.license_key` and inject it into your app.
import { sdk } from '../sdk'
import { Verifier, PublicKey, Client } from '@keysat/licensing-client'
import {
LICENSING_BASE_URL,
LICENSING_BASE_URL_TOR,
PRODUCT_SLUG,
ISSUER_PUBKEY_PEM,
PRODUCT_DISPLAY_NAME,
} from '../licensing/config'
const input = sdk.InputSpec.of({
license_key: {
type: 'text',
name: 'License key',
description:
`Paste the license key you received after purchasing ${PRODUCT_DISPLAY_NAME}. ` +
`It looks like "LIC1-...-...". Case-sensitive; no surrounding whitespace.`,
required: true,
default: null,
masked: false,
},
})
export const activateLicense = sdk.Action.withInput(
'activate-license',
async ({ effects }) => ({
name: 'Activate license',
description:
`Verify and save a license key for ${PRODUCT_DISPLAY_NAME}. ` +
`The key is verified cryptographically on-device, then cross-checked ` +
`with the issuing server if it is reachable.`,
warning: null,
allowedStatuses: 'any',
group: 'License',
visibility: 'enabled',
}),
input,
async ({ effects, input: form }) => {
const key = (form.license_key ?? '').trim()
if (!key) throw new Error('License key is empty.')
// -- 1. Offline signature check (always runs).
let offline
try {
const verifier = new Verifier(PublicKey.fromPem(ISSUER_PUBKEY_PEM))
offline = verifier.verify(key)
} catch (e) {
throw new Error(
`This key's signature did not verify against the embedded issuer ` +
`public key. It is either corrupt, truncated, or not issued by ` +
`${PRODUCT_DISPLAY_NAME}.\n\nDetails: ${errMsg(e)}`,
)
}
// -- 2. Online check (best-effort). Catches revocation and fingerprint
// mismatches from a previously-seen device. Failure here is NOT fatal
// — the buyer may be offline. We record the last-known status.
const fingerprint = await getMachineFingerprint(effects)
const onlineStatus = await tryOnlineValidate(key, fingerprint)
if (onlineStatus.reachable && !onlineStatus.ok) {
throw new Error(
`The licensing server rejected this key: ${onlineStatus.reason ?? 'unknown reason'}.\n\n` +
`Most common causes: key was revoked; key is bound to a different ` +
`machine; wrong product.`,
)
}
// -- 3. Persist.
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_key: key,
license_activated_at: new Date().toISOString(),
license_last_status: onlineStatus.reachable
? 'valid (online-checked)'
: 'valid (offline-only)',
license_pending_invoice_id: null,
})
return {
message:
`License activated.\n\n` +
`Product: ${offline.productId}\n` +
`License id: ${offline.licenseId}\n` +
`Status: ${onlineStatus.reachable ? 'valid (confirmed with issuing server)' : 'valid (signature only — server not reachable)'}\n\n` +
`You may need to restart ${PRODUCT_DISPLAY_NAME} for the change to take effect.`,
}
},
)
// ---------------------------------------------------------------------------
async function tryOnlineValidate(
key: string,
fingerprint: string,
): Promise<{ reachable: boolean; ok?: boolean; reason?: string }> {
const urls = [LICENSING_BASE_URL, LICENSING_BASE_URL_TOR].filter(Boolean) as string[]
for (const base of urls) {
try {
const client = new Client(base)
const r = await client.validate(key, PRODUCT_SLUG, fingerprint)
return { reachable: true, ok: r.ok, reason: r.reason }
} catch (_) {
// try next URL
}
}
return { reachable: false }
}
/**
* Derive a stable per-machine fingerprint. Start9 packages have a host UUID;
* if that's unavailable we fall back to the operating system's machine-id.
* The fingerprint is hashed by the SDK before being sent to the server.
*/
async function getMachineFingerprint(effects: unknown): Promise<string> {
// Prefer: Start9-provided device identifier if available.
// Fallback: /etc/machine-id on the host.
try {
const { readFile } = await import('fs/promises')
const id = (await readFile('/etc/machine-id', 'utf8')).trim()
if (id) return `machine-id:${id}`
} catch (_) {}
return 'fingerprint:unknown'
}
function errMsg(e: unknown): string {
return e instanceof Error ? e.message : String(e)
}
+181
View File
@@ -0,0 +1,181 @@
// Action: "Buy license" — starts a purchase against the issuing licensing
// server and returns a checkout URL. The operator opens the URL, pays with
// Bitcoin on the seller's BTCPay, and the license key is automatically
// captured by the companion action "Finish license purchase" — no copy/paste.
//
// Flow:
// 1. Buyer clicks "Buy license" → this action calls POST /v1/purchase on
// the seller's licensing server and returns the checkout URL.
// 2. Buyer opens that URL, pays the invoice.
// 3. Buyer clicks "Finish license purchase" → pollPurchase() → key arrives,
// we verify it offline, and we persist it. No typing, no copy/paste.
import { sdk } from '../sdk'
import { Client, Verifier, PublicKey } from '@keysat/licensing-client'
import {
LICENSING_BASE_URL,
LICENSING_BASE_URL_TOR,
PRODUCT_SLUG,
ISSUER_PUBKEY_PEM,
PRODUCT_DISPLAY_NAME,
} from '../licensing/config'
// --- "Buy license" ---------------------------------------------------------
const buyInput = sdk.InputSpec.of({
buyer_email: {
type: 'text',
name: 'Email (optional)',
description:
'If you would like a receipt or recovery email from the seller, enter it here. Otherwise leave blank.',
required: false,
default: null,
},
code: {
type: 'text',
name: 'Discount / referral code (optional)',
description:
'If the seller gave you a code (e.g. "FOUNDERS50"), enter it here. ' +
'It will be applied to the BTCPay invoice. For free-license codes, ' +
'use the "Redeem free license" action instead.',
required: false,
default: null,
},
})
export const buyLicense = sdk.Action.withInput(
'buy-license',
async ({ effects }) => ({
name: 'Buy license',
description:
`Start a Bitcoin-paid purchase of a ${PRODUCT_DISPLAY_NAME} license. ` +
`You will get a checkout URL to open in your browser; after you pay, ` +
`run "Finish license purchase" to have the key automatically ` +
`captured — you never need to copy/paste anything.`,
warning: null,
allowedStatuses: 'any',
group: 'License',
visibility: 'enabled',
}),
buyInput,
async ({ effects, input: form }) => {
const client = await firstReachableClient()
// Tell BTCPay to land the buyer on the licensing server's "thank you"
// page after a successful payment. That page reminds the buyer to
// return to their StartOS dashboard and run "Finish license purchase".
// We use the same base URL the client just probed as reachable so it
// matches the network path the buyer's browser can actually open
// (clearnet vs Tor).
const redirectUrl = `${client.baseUrl()}/thank-you`
const session = await client.startPurchase(PRODUCT_SLUG, {
buyerEmail: form.buyer_email ?? undefined,
code: form.code ?? undefined,
redirectUrl,
})
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_pending_invoice_id: session.invoiceId,
})
const codeApplied = form.code
? `\nCode applied: ${form.code} (final amount above reflects the discount)`
: ''
return {
message:
`Open this URL in your browser to pay:\n\n${session.checkoutUrl}\n\n` +
`Amount: ${session.amountSats} satoshis${codeApplied}\n` +
`Invoice id: ${session.invoiceId}\n\n` +
`After payment confirms on-chain or on Lightning, run the ` +
`"Finish license purchase" action — we will fetch the key for you.`,
}
},
)
// --- "Finish license purchase" --------------------------------------------
export const finishLicensePurchase = sdk.Action.withoutInput(
'finish-license-purchase',
async ({ effects }) => ({
name: 'Finish license purchase',
description:
`Check on a pending purchase started by "Buy license". If payment has ` +
`settled, the license key is fetched, verified, and stored ` +
`automatically.`,
warning: null,
allowedStatuses: 'any',
group: 'License',
visibility: 'enabled',
}),
async ({ effects }) => {
const store = await sdk.store.getOwn(effects, sdk.StorePath).const()
const invoiceId = (store as { license_pending_invoice_id: string | null })
.license_pending_invoice_id
if (!invoiceId) {
throw new Error(
`No pending purchase found. Run "Buy license" first to start one.`,
)
}
const client = await firstReachableClient()
const poll = await client.pollPurchase(invoiceId)
if (poll.status === 'expired' || poll.status === 'invalid') {
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_pending_invoice_id: null,
})
throw new Error(
`This invoice ${poll.status}. Run "Buy license" again to start over.`,
)
}
if (!poll.licenseKey) {
return {
message:
`Payment has not settled yet. Current status: ${poll.status}. ` +
`Try again in a minute — Bitcoin confirmations can take a few ` +
`minutes, Lightning is near-instant.`,
}
}
// Paid and key issued — verify and store.
const verifier = new Verifier(PublicKey.fromPem(ISSUER_PUBKEY_PEM))
const ok = verifier.verify(poll.licenseKey)
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_key: poll.licenseKey,
license_activated_at: new Date().toISOString(),
license_last_status: 'valid (issued from purchase)',
license_pending_invoice_id: null,
})
return {
message:
`Payment settled — license captured and activated.\n\n` +
`Product: ${ok.productId}\n` +
`License id: ${ok.licenseId}\n\n` +
`You may need to restart ${PRODUCT_DISPLAY_NAME} for the change to ` +
`take effect. Keep your Start9 backup up to date — that is where your ` +
`license now lives.`,
}
},
)
// ---------------------------------------------------------------------------
/** Return a Client pinned to the first reachable base URL, or throw. */
async function firstReachableClient(): Promise<Client> {
const urls = [LICENSING_BASE_URL, LICENSING_BASE_URL_TOR].filter(Boolean) as string[]
for (const base of urls) {
const client = new Client(base)
try {
// /v1/pubkey is the cheapest health-check that still proves the service
// is responsive and talking to us.
await client.fetchPubkeyPem()
return client
} catch (_) {}
}
throw new Error(
`Could not reach the licensing server at any configured URL. ` +
`Check your network (and Tor, if applicable) and try again.`,
)
}
+93
View File
@@ -0,0 +1,93 @@
// Action: "Check license status" — re-verifies the stored key (offline +
// online) and refreshes `license_last_status`. Useful for diagnosing a
// suddenly-rejecting app.
import { sdk } from '../sdk'
import { Verifier, PublicKey, Client } from '@keysat/licensing-client'
import {
LICENSING_BASE_URL,
LICENSING_BASE_URL_TOR,
PRODUCT_SLUG,
ISSUER_PUBKEY_PEM,
PRODUCT_DISPLAY_NAME,
} from '../licensing/config'
export const checkLicense = sdk.Action.withoutInput(
'check-license',
async ({ effects }) => ({
name: 'Check license status',
description:
`Re-run signature and server-side checks against the currently ` +
`stored license key for ${PRODUCT_DISPLAY_NAME}.`,
warning: null,
allowedStatuses: 'any',
group: 'License',
visibility: 'enabled',
}),
async ({ effects }) => {
const store = await sdk.store.getOwn(effects, sdk.StorePath).const()
const key = (store as { license_key: string | null }).license_key
if (!key) {
return {
message:
`No license key is stored. Run "Activate license" or "Buy license" first.`,
}
}
// Offline
let offline
try {
offline = new Verifier(PublicKey.fromPem(ISSUER_PUBKEY_PEM)).verify(key)
} catch (e) {
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_last_status: `offline-invalid: ${errMsg(e)}`,
})
return {
message:
`The stored key no longer verifies cryptographically. This means ` +
`the key was edited, truncated, or the issuer public key was ` +
`rotated by the seller. Contact the seller.\n\nDetails: ${errMsg(e)}`,
}
}
// Online
let onlineLine: string
try {
const client = await firstReachableClient()
const r = await client.validate(key, PRODUCT_SLUG, undefined)
onlineLine = r.ok
? 'Server says: OK'
: `Server rejected the key: ${r.reason ?? 'unknown'}`
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_last_status: r.ok ? 'valid (online-checked)' : `rejected: ${r.reason ?? 'unknown'}`,
})
} catch (_) {
onlineLine = 'Server: unreachable (offline — relying on signature only)'
}
return {
message:
`Signature: OK\n` +
`${onlineLine}\n\n` +
`Product: ${offline.productId}\n` +
`License id: ${offline.licenseId}\n` +
`Issued at: ${offline.payload.issuedAt}`,
}
},
)
async function firstReachableClient(): Promise<Client> {
const urls = [LICENSING_BASE_URL, LICENSING_BASE_URL_TOR].filter(Boolean) as string[]
for (const base of urls) {
const client = new Client(base)
try {
await client.fetchPubkeyPem()
return client
} catch (_) {}
}
throw new Error('no reachable URL')
}
function errMsg(e: unknown): string {
return e instanceof Error ? e.message : String(e)
}
+34
View File
@@ -0,0 +1,34 @@
// Action: "Deactivate license" — clears the stored key locally. Does NOT
// revoke the key server-side (only the seller can revoke, from their licensing
// server's admin dashboard). The operator is reminded of this.
import { sdk } from '../sdk'
import { PRODUCT_DISPLAY_NAME } from '../licensing/config'
export const deactivateLicense = sdk.Action.withoutInput(
'deactivate-license',
async ({ effects }) => ({
name: 'Deactivate license',
description:
`Remove the stored license key from this ${PRODUCT_DISPLAY_NAME} ` +
`install. The key itself is NOT revoked — to revoke, contact the seller.`,
warning:
`This only clears the key from this device. If you want the key ` +
`disabled everywhere (e.g. because it leaked), ask the seller to ` +
`revoke it server-side.`,
allowedStatuses: 'any',
group: 'License',
visibility: 'enabled',
}),
async ({ effects }) => {
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_key: null,
license_activated_at: null,
license_last_status: 'deactivated',
license_pending_invoice_id: null,
})
return {
message: `License cleared. ${PRODUCT_DISPLAY_NAME} will no longer consider itself licensed on next start.`,
}
},
)
+28
View File
@@ -0,0 +1,28 @@
// Register the licensing template's actions.
//
// In your package's top-level actions registry, spread these in:
//
// import { licensingActions } from './licensing/actions'
// 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)
import { activateLicense } from './activateLicense'
import { buyLicense, finishLicensePurchase } from './buyLicense'
import { checkLicense } from './checkLicense'
import { deactivateLicense } from './deactivateLicense'
import { redeemFreeLicense } from './redeemFreeLicense'
export const licensingActions = {
activateLicense,
buyLicense,
finishLicensePurchase,
redeemFreeLicense,
checkLicense,
deactivateLicense,
}
+123
View File
@@ -0,0 +1,123 @@
// Action: "Redeem free license code" — for codes the seller created with
// kind = 'free_license'. Bypasses BTCPay entirely: the buyer enters the
// code, we hit POST /v1/redeem on the seller's licensing server, get a
// signed key back immediately, verify it, and store it. No invoice, no
// payment, no polling.
import { sdk } from '../sdk'
import { Verifier, PublicKey, Client } from '@keysat/licensing-client'
import {
LICENSING_BASE_URL,
LICENSING_BASE_URL_TOR,
PRODUCT_SLUG,
ISSUER_PUBKEY_PEM,
PRODUCT_DISPLAY_NAME,
} from '../licensing/config'
const input = sdk.InputSpec.of({
code: {
type: 'text',
name: 'Free-license code',
description:
`The code the seller of ${PRODUCT_DISPLAY_NAME} gave you. Codes are ` +
`case-insensitive. Only "free license" codes work here; for paid ` +
`discount codes, use the "Buy license" action and enter the code there.`,
required: true,
default: null,
masked: false,
},
buyer_email: {
type: 'text',
name: 'Email (optional)',
description:
'Optional email — the seller may use this for support or recovery.',
required: false,
default: null,
},
})
export const redeemFreeLicense = sdk.Action.withInput(
'redeem-free-license',
async ({ effects: _effects }) => ({
name: 'Redeem free license',
description:
`Redeem a free-license code from the seller of ${PRODUCT_DISPLAY_NAME}. ` +
`The license key is issued immediately — no payment required. The ` +
`seller controls how many of these codes exist and how long they are valid.`,
warning: null,
allowedStatuses: 'any',
group: 'License',
visibility: 'enabled',
}),
input,
async ({ effects, input: form }) => {
const code = (form.code ?? '').trim()
if (!code) throw new Error('Code is empty.')
// 1. Hit /v1/redeem on the first reachable licensing-server URL.
const urls = [LICENSING_BASE_URL, LICENSING_BASE_URL_TOR].filter(Boolean) as string[]
let lastErr: unknown = null
let result: {
licenseId: string
licenseKey: string
invoiceId: string
redemptionId: string
} | null = null
for (const base of urls) {
try {
const client = new Client(base)
result = await client.redeemFreeLicense(PRODUCT_SLUG, code, {
buyerEmail: form.buyer_email ?? undefined,
})
break
} catch (e) {
lastErr = e
}
}
if (!result) {
throw new Error(
`Could not reach the licensing server, or the code was rejected. ` +
`Last error: ${errMsg(lastErr)}`,
)
}
// 2. Offline-verify the returned key against the embedded issuer pubkey,
// same as the activation flow. This catches a misconfigured server
// that's somehow returning keys signed by a different issuer.
let offline
try {
const verifier = new Verifier(PublicKey.fromPem(ISSUER_PUBKEY_PEM))
offline = verifier.verify(result.licenseKey)
} catch (e) {
throw new Error(
`Server returned a key but its signature did not verify against the ` +
`embedded issuer public key. This usually means the buyer-side ` +
`package was built with the wrong ISSUER_PUBKEY_PEM, or the ` +
`licensing server's key was rotated.\n\nDetails: ${errMsg(e)}`,
)
}
// 3. Persist.
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_key: result.licenseKey,
license_activated_at: new Date().toISOString(),
license_last_status: 'valid (issued from free code)',
license_pending_invoice_id: null,
})
return {
message:
`Code redeemed — license issued and activated.\n\n` +
`Product: ${offline.productId}\n` +
`License id: ${offline.licenseId}\n` +
`Code: ${code.toUpperCase()}\n\n` +
`You may need to restart ${PRODUCT_DISPLAY_NAME} for the change to ` +
`take effect. Keep your Start9 backup up to date — that is where your ` +
`license now lives.`,
}
},
)
function errMsg(e: unknown): string {
return e instanceof Error ? e.message : String(e)
}