Files
keysat/startos/actions/activateLicense.ts
T
Grant 6ac118ae70 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.
2026-05-07 10:33:39 -05:00

168 lines
5.4 KiB
TypeScript

// Action: activate the Keysat package's own self-license.
//
// The daemon embeds the keysat.xyz master public key at compile time
// (see licensing-service/src/license_self.rs). The operator pastes a
// LIC1-… key here; the daemon verifies it against the master pubkey,
// 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.
import { sdk } from '../sdk'
import { store } from '../fileModels/store'
import { adminCall, LICENSING_URL } from '../utils'
const { InputSpec, Value } = sdk
const input = InputSpec.of({
license_key: Value.text({
name: 'License key',
description:
'Paste the LIC1-… license key issued for your Keysat install. ' +
'Buy or redeem one at registry.keysat.xyz.',
required: true,
default: null,
placeholder: 'LIC1-XXXXXXXXXXXX-XXXXXXXXXXXX',
}),
})
export const activateLicense = sdk.Action.withInput(
'activate-license',
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).',
warning: null,
allowedStatuses: 'only-running',
group: 'License',
visibility: 'enabled',
}),
input,
async () => null,
async ({ effects: _effects, input: formInput }) => {
const storeData = await store.read().once()
if (!storeData) throw new Error('Store not initialized — restart the service.')
const key = formInput.license_key.trim()
if (!key) {
throw new Error('License key is required.')
}
const resp = await adminCall(
LICENSING_URL,
storeData.admin_api_key,
'/v1/admin/self-license',
{ method: 'POST', body: JSON.stringify({ license_key: key }) },
)
if (!resp.ok) {
const body = await resp.text()
let detail = body
try {
const parsed = JSON.parse(body)
if (parsed.detail) detail = parsed.detail
else if (parsed.error) detail = parsed.error
} catch (_) {}
throw new Error(`Activation rejected (HTTP ${resp.status}): ${detail}`)
}
const result = (await resp.json()) as {
ok: boolean
tier: {
tier: 'licensed' | 'unlicensed'
license_id?: string
product_id?: string
expires_at?: number
entitlements?: string[]
mode: string
}
message: string
}
return {
version: '1',
title: 'Keysat license activated',
message:
result.message +
' The license is stored at /data/keysat-license.txt and survives upgrades and reinstalls (it is part of your StartOS backup set).',
result: null,
}
},
)
// Companion read-only action: surface the current self-license tier.
// Useful both as a sanity check after activation and as a way for the
// operator to see "am I running licensed or unlicensed?" without
// digging into logs.
export const showLicenseStatus = sdk.Action.withoutInput(
'show-license-status',
async () => ({
name: 'Show Keysat license status',
description:
'Reports whether this Keysat install is running licensed or unlicensed, ' +
'and which entitlements are active.',
warning: null,
allowedStatuses: 'only-running',
group: 'License',
visibility: 'enabled',
}),
async ({ effects: _effects }) => {
const storeData = await store.read().once()
if (!storeData) throw new Error('Store not initialized — restart the service.')
const resp = await adminCall(
LICENSING_URL,
storeData.admin_api_key,
'/v1/admin/self-license',
{ method: 'GET' },
)
if (!resp.ok) {
throw new Error(`Could not read license status: HTTP ${resp.status}${await resp.text()}`)
}
const j = (await resp.json()) as {
tier: 'licensed' | 'unlicensed'
license_id?: string
product_id?: string
expires_at?: number
entitlements?: string[]
reason?: string
mode: string
}
if (j.tier === 'licensed') {
const exp = !j.expires_at
? 'perpetual'
: new Date(j.expires_at * 1000).toISOString().slice(0, 10)
const ents = (j.entitlements || []).length === 0 ? '(none)' : (j.entitlements || []).join(', ')
return {
version: '1',
title: 'Licensed',
message:
`License id: ${j.license_id}\n` +
`Expires: ${exp}\n` +
`Entitlements: ${ents}\n` +
`Build mode: ${j.mode}`,
result: null,
}
} else {
return {
version: '1',
title: 'Unlicensed',
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.'),
result: null,
}
}
},
)