6ac118ae70
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.
168 lines
5.4 KiB
TypeScript
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,
|
|
}
|
|
}
|
|
},
|
|
)
|