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.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user