Add 'Set Recap License' StartOS action + s/Keysat license/Recap license/

Two related changes:

1. New StartOS action: 'Set Recap License'

   Symmetric with the existing 'Set Gemini API Key' action — paste a
   LIC1-... key into the StartOS Actions menu and it gets persisted.
   Added because some users prefer the StartOS form for credentials
   over the in-app activation modal.

   Implementation:
     • startos/file-models/config.json.ts: schema gains recap_license_key
     • startos/actions/setLicense.ts: input form (masked, regex-checks
       for the LIC1- prefix), persists via configFile.merge()
     • startos/actions/index.ts: registers the new action
     • server/license.js: readLicenseString() falls back to
       startos-config.json after the legacy license.txt path. Resolution
       order: env → license.txt → startos-config.json
     • server/license-middleware.js: faster license-file poll (30 s,
       env-overridable RECAP_LICENSE_FILE_POLL_MS) re-runs checkLicense
       so action-set keys take effect within seconds, not the 6 h online
       cycle. If the new key parses as 'licensed', kicks an immediate
       online check to confirm.

2. Copy fix: 'Keysat license' → 'Recap license' in user-facing text

   Keysat is the licensing system underneath, but customers buy a
   'Recap license'. Updated:
     • Activation screen subtitle (public/index.html)
     • 402 message in the activation gate (server/license-middleware.js)

   Internal references (PRODUCT_SLUG, KEYSAT_BASE_URL, the issuer.pub
   filename, the 'Issuer: licensing.keysat.xyz' display in the
   activation card) stay as Keysat — those are accurate.

Smoke tested locally: starting the server with no license, then
writing a fake LIC1-... key into startos-config.json, the
license-file poll picks it up within ~2 s and transitions state from
'unlicensed' to 'invalid' (since the fake key fails Ed25519
verification, as expected). With a real key, the same path would land
in 'licensed'.
This commit is contained in:
Keysat
2026-05-09 07:06:21 -05:00
parent 85cb641044
commit 29282f8dcc
6 changed files with 103 additions and 7 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
import { sdk } from '../sdk'
import { setApiKey } from './setApiKey'
import { setLicense } from './setLicense'
export const actions = sdk.Actions.of().addAction(setApiKey)
export const actions = sdk.Actions.of()
.addAction(setApiKey)
.addAction(setLicense)
+50
View File
@@ -0,0 +1,50 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
recap_license_key: Value.text({
name: 'Recap License Key',
description:
'Paste your Recap license key here. Keys start with "LIC1-..." — get one from your Recap seller. (Keys are also accepted via the web UI activation screen.)',
required: true,
default: null,
masked: true,
minLength: 1,
maxLength: 1024,
patterns: [
{
regex: '^LIC1-.+',
description: 'License keys start with "LIC1-".',
},
],
}),
})
export const setLicense = sdk.Action.withInput(
'set-license',
async ({ effects }) => ({
name: 'Set Recap License',
description:
'Activate a Recap license to unlock paid features (saved library, channel & podcast subscriptions, auto-queue).',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
return { recap_license_key: config?.recap_license_key || undefined }
},
async ({ effects, input }) => {
const trimmed = (input.recap_license_key || '').trim()
await configFile.merge(effects, { recap_license_key: trimmed })
return null
},
)