// Action: "Redeem free license code" — for codes the seller created with // kind = 'free_license'. Bypasses BTCPay entirely: the buyer enters the // code, we hit POST /v1/redeem on the seller's licensing server, get a // signed key back immediately, verify it, and store it. No invoice, no // payment, no polling. 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({ code: { type: 'text', name: 'Free-license code', description: `The code the seller of ${PRODUCT_DISPLAY_NAME} gave you. Codes are ` + `case-insensitive. Only "free license" codes work here; for paid ` + `discount codes, use the "Buy license" action and enter the code there.`, required: true, default: null, masked: false, }, buyer_email: { type: 'text', name: 'Email (optional)', description: 'Optional email — the seller may use this for support or recovery.', required: false, default: null, }, }) export const redeemFreeLicense = sdk.Action.withInput( 'redeem-free-license', async ({ effects: _effects }) => ({ name: 'Redeem free license', description: `Redeem a free-license code from the seller of ${PRODUCT_DISPLAY_NAME}. ` + `The license key is issued immediately — no payment required. The ` + `seller controls how many of these codes exist and how long they are valid.`, warning: null, allowedStatuses: 'any', group: 'License', visibility: 'enabled', }), input, async ({ effects, input: form }) => { const code = (form.code ?? '').trim() if (!code) throw new Error('Code is empty.') // 1. Hit /v1/redeem on the first reachable licensing-server URL. const urls = [LICENSING_BASE_URL, LICENSING_BASE_URL_TOR].filter(Boolean) as string[] let lastErr: unknown = null let result: { licenseId: string licenseKey: string invoiceId: string redemptionId: string } | null = null for (const base of urls) { try { const client = new Client(base) result = await client.redeemFreeLicense(PRODUCT_SLUG, code, { buyerEmail: form.buyer_email ?? undefined, }) break } catch (e) { lastErr = e } } if (!result) { throw new Error( `Could not reach the licensing server, or the code was rejected. ` + `Last error: ${errMsg(lastErr)}`, ) } // 2. Offline-verify the returned key against the embedded issuer pubkey, // same as the activation flow. This catches a misconfigured server // that's somehow returning keys signed by a different issuer. let offline try { const verifier = new Verifier(PublicKey.fromPem(ISSUER_PUBKEY_PEM)) offline = verifier.verify(result.licenseKey) } catch (e) { throw new Error( `Server returned a key but its signature did not verify against the ` + `embedded issuer public key. This usually means the buyer-side ` + `package was built with the wrong ISSUER_PUBKEY_PEM, or the ` + `licensing server's key was rotated.\n\nDetails: ${errMsg(e)}`, ) } // 3. Persist. await sdk.store.getOwn(effects, sdk.StorePath).merge({ license_key: result.licenseKey, license_activated_at: new Date().toISOString(), license_last_status: 'valid (issued from free code)', license_pending_invoice_id: null, }) return { message: `Code redeemed — license issued and activated.\n\n` + `Product: ${offline.productId}\n` + `License id: ${offline.licenseId}\n` + `Code: ${code.toUpperCase()}\n\n` + `You may need to restart ${PRODUCT_DISPLAY_NAME} for the change to ` + `take effect. Keep your Start9 backup up to date — that is where your ` + `license now lives.`, } }, ) function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e) }