// Action: "Activate license" — buyer pastes a license key, we verify it (both // offline against the embedded issuer public key, and online against the // licensing server if reachable) and persist it in the package store. // // After this runs successfully, your main process can read // `store.license_key` and inject it into your app. import { sdk } from '../sdk' import { Verifier, PublicKey, Client } from '@keysat/licensing-client' import { LICENSING_BASE_URL, LICENSING_BASE_URL_TOR, PRODUCT_SLUG, ISSUER_PUBKEY_PEM, PRODUCT_DISPLAY_NAME, } from '../licensing/config' const input = sdk.InputSpec.of({ license_key: { type: 'text', name: 'License key', description: `Paste the license key you received after purchasing ${PRODUCT_DISPLAY_NAME}. ` + `It looks like "LIC1-...-...". Case-sensitive; no surrounding whitespace.`, required: true, default: null, masked: false, }, }) export const activateLicense = sdk.Action.withInput( 'activate-license', async ({ effects }) => ({ name: 'Activate license', description: `Verify and save a license key for ${PRODUCT_DISPLAY_NAME}. ` + `The key is verified cryptographically on-device, then cross-checked ` + `with the issuing server if it is reachable.`, warning: null, allowedStatuses: 'any', group: 'License', visibility: 'enabled', }), input, async ({ effects, input: form }) => { const key = (form.license_key ?? '').trim() if (!key) throw new Error('License key is empty.') // -- 1. Offline signature check (always runs). let offline try { const verifier = new Verifier(PublicKey.fromPem(ISSUER_PUBKEY_PEM)) offline = verifier.verify(key) } catch (e) { throw new Error( `This key's signature did not verify against the embedded issuer ` + `public key. It is either corrupt, truncated, or not issued by ` + `${PRODUCT_DISPLAY_NAME}.\n\nDetails: ${errMsg(e)}`, ) } // -- 2. Online check (best-effort). Catches revocation and fingerprint // mismatches from a previously-seen device. Failure here is NOT fatal // — the buyer may be offline. We record the last-known status. const fingerprint = await getMachineFingerprint(effects) const onlineStatus = await tryOnlineValidate(key, fingerprint) if (onlineStatus.reachable && !onlineStatus.ok) { throw new Error( `The licensing server rejected this key: ${onlineStatus.reason ?? 'unknown reason'}.\n\n` + `Most common causes: key was revoked; key is bound to a different ` + `machine; wrong product.`, ) } // -- 3. Persist. await sdk.store.getOwn(effects, sdk.StorePath).merge({ license_key: key, license_activated_at: new Date().toISOString(), license_last_status: onlineStatus.reachable ? 'valid (online-checked)' : 'valid (offline-only)', license_pending_invoice_id: null, }) return { message: `License activated.\n\n` + `Product: ${offline.productId}\n` + `License id: ${offline.licenseId}\n` + `Status: ${onlineStatus.reachable ? 'valid (confirmed with issuing server)' : 'valid (signature only — server not reachable)'}\n\n` + `You may need to restart ${PRODUCT_DISPLAY_NAME} for the change to take effect.`, } }, ) // --------------------------------------------------------------------------- async function tryOnlineValidate( key: string, fingerprint: string, ): Promise<{ reachable: boolean; ok?: boolean; reason?: string }> { const urls = [LICENSING_BASE_URL, LICENSING_BASE_URL_TOR].filter(Boolean) as string[] for (const base of urls) { try { const client = new Client(base) const r = await client.validate(key, PRODUCT_SLUG, fingerprint) return { reachable: true, ok: r.ok, reason: r.reason } } catch (_) { // try next URL } } return { reachable: false } } /** * Derive a stable per-machine fingerprint. Start9 packages have a host UUID; * if that's unavailable we fall back to the operating system's machine-id. * The fingerprint is hashed by the SDK before being sent to the server. */ async function getMachineFingerprint(effects: unknown): Promise { // Prefer: Start9-provided device identifier if available. // Fallback: /etc/machine-id on the host. try { const { readFile } = await import('fs/promises') const id = (await readFile('/etc/machine-id', 'utf8')).trim() if (id) return `machine-id:${id}` } catch (_) {} return 'fingerprint:unknown' } function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e) }