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:
Grant
2026-05-07 10:33:39 -05:00
parent 432250bffc
commit 6ac118ae70
90 changed files with 14896 additions and 524 deletions
+50 -7
View File
@@ -1,24 +1,38 @@
// First-boot initialization.
//
// On fresh install:
// - Generate an admin API key (stored in the StartOS store; user can
// retrieve it via an action if they need to script against the API).
// - Generate an admin API key (stored in the StartOS package-local store;
// user can retrieve it via the `showCredentials` action if they need to
// script against the API).
// - Surface "Connect BTCPay" as a critical task so the operator sees a
// clear "do this next" prompt in the StartOS dashboard. Cleared by the
// btcpayStatus action once BTCPay reports connected (see
// ../actions/configureBtcpay.ts).
//
// The BTCPay webhook secret is no longer stored here — the daemon generates
// and persists it in its own DB during the one-click "Connect BTCPay" flow.
// The field is kept in the store shape for backward compatibility with
// installs made before v0.1.0; it is not used.
// installs made before the authorize flow; it is not authoritative.
//
// On subsequent boots this is a no-op (keys already exist).
//
// SDK 0.4.0 note: InitFn signature is `(effects, kind)` positional — NOT the
// 0.3.x `({effects})` object destructure. `setupOnInit` wraps the function
// into an InitScript so it can be composed with `setupInit(...)`.
import { sdk } from '../sdk'
import { store } from '../fileModels/store'
import { generateSecret } from '../utils'
import { configureBtcpay } from '../actions/configureBtcpay'
export const initFn = sdk.setupOnInit(async ({ effects }) => {
const current = await sdk.store.getOwn(effects, sdk.StorePath).const()
/** Replay id used to dedupe + later-clear the BTCPay setup task. */
export const BTCPAY_SETUP_TASK_ID = 'btcpay-initial-setup'
export const initFn = sdk.setupOnInit(async (effects, kind) => {
const current = await store.read().once()
if (!current || current.schema_version === 0 || current.schema_version === undefined) {
await sdk.store.setOwn(effects, sdk.StorePath, {
await store.write(effects, {
admin_api_key: current?.admin_api_key || generateSecret(32),
// Kept in the shape for backcompat; no longer authoritative.
btcpay_webhook_secret: current?.btcpay_webhook_secret || '',
@@ -26,9 +40,38 @@ export const initFn = sdk.setupOnInit(async ({ effects }) => {
schema_version: 1,
})
}
// Surface BTCPay setup as a prominent task on first install and on
// restore (a backup older than the BTCPay-authorize flow may not have a
// valid BTCPay config). On regular updates / container rebuilds we
// skip — BTCPay should already be connected by then. createOwnTask is
// idempotent on the same replayId, so a re-run won't duplicate.
//
// Severity is 'important', not 'critical', because 'critical' blocks
// the service from STARTING until the task is completed — but the
// configureBtcpay action requires the service to BE running (it makes
// an HTTP call to the local daemon to kick off the authorize flow).
// 'critical' would deadlock: task blocks start, action needs running.
// 'important' shows the task prominently without blocking startup.
if (kind === 'install' || kind === 'restore') {
try {
await sdk.action.createOwnTask(effects, configureBtcpay, 'important', {
replayId: BTCPAY_SETUP_TASK_ID,
reason:
'Connect Keysat to your BTCPay Server to start selling licenses. ' +
'Your BTCPay instance on this Start9 is already a declared ' +
'dependency — Keysat just needs to authorize against it.',
})
} catch (e) {
// Don't block init on a task-create failure. Operators can still
// run "Connect BTCPay" manually.
// eslint-disable-next-line no-console
console.warn('createOwnTask(configureBtcpay) failed:', e)
}
}
})
export const uninitFn = sdk.setupOnUninit(async ({ effects }) => {
export const uninitFn = sdk.setupOnUninit(async (_effects, _target) => {
// Nothing to tear down at the StartOS level — the DB volume is handled by
// StartOS directly when the package is uninstalled.
})