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