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:
@@ -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
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user