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