Initial public commit

This commit is contained in:
Keysat
2026-05-07 10:41:57 -05:00
commit 8c9bc75e24
12 changed files with 906 additions and 0 deletions
+181
View File
@@ -0,0 +1,181 @@
// Action: "Buy license" — starts a purchase against the issuing licensing
// server and returns a checkout URL. The operator opens the URL, pays with
// Bitcoin on the seller's BTCPay, and the license key is automatically
// captured by the companion action "Finish license purchase" — no copy/paste.
//
// Flow:
// 1. Buyer clicks "Buy license" → this action calls POST /v1/purchase on
// the seller's licensing server and returns the checkout URL.
// 2. Buyer opens that URL, pays the invoice.
// 3. Buyer clicks "Finish license purchase" → pollPurchase() → key arrives,
// we verify it offline, and we persist it. No typing, no copy/paste.
import { sdk } from '../sdk'
import { Client, Verifier, PublicKey } from '@keysat/licensing-client'
import {
LICENSING_BASE_URL,
LICENSING_BASE_URL_TOR,
PRODUCT_SLUG,
ISSUER_PUBKEY_PEM,
PRODUCT_DISPLAY_NAME,
} from '../licensing/config'
// --- "Buy license" ---------------------------------------------------------
const buyInput = sdk.InputSpec.of({
buyer_email: {
type: 'text',
name: 'Email (optional)',
description:
'If you would like a receipt or recovery email from the seller, enter it here. Otherwise leave blank.',
required: false,
default: null,
},
code: {
type: 'text',
name: 'Discount / referral code (optional)',
description:
'If the seller gave you a code (e.g. "FOUNDERS50"), enter it here. ' +
'It will be applied to the BTCPay invoice. For free-license codes, ' +
'use the "Redeem free license" action instead.',
required: false,
default: null,
},
})
export const buyLicense = sdk.Action.withInput(
'buy-license',
async ({ effects }) => ({
name: 'Buy license',
description:
`Start a Bitcoin-paid purchase of a ${PRODUCT_DISPLAY_NAME} license. ` +
`You will get a checkout URL to open in your browser; after you pay, ` +
`run "Finish license purchase" to have the key automatically ` +
`captured — you never need to copy/paste anything.`,
warning: null,
allowedStatuses: 'any',
group: 'License',
visibility: 'enabled',
}),
buyInput,
async ({ effects, input: form }) => {
const client = await firstReachableClient()
// Tell BTCPay to land the buyer on the licensing server's "thank you"
// page after a successful payment. That page reminds the buyer to
// return to their StartOS dashboard and run "Finish license purchase".
// We use the same base URL the client just probed as reachable so it
// matches the network path the buyer's browser can actually open
// (clearnet vs Tor).
const redirectUrl = `${client.baseUrl()}/thank-you`
const session = await client.startPurchase(PRODUCT_SLUG, {
buyerEmail: form.buyer_email ?? undefined,
code: form.code ?? undefined,
redirectUrl,
})
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_pending_invoice_id: session.invoiceId,
})
const codeApplied = form.code
? `\nCode applied: ${form.code} (final amount above reflects the discount)`
: ''
return {
message:
`Open this URL in your browser to pay:\n\n${session.checkoutUrl}\n\n` +
`Amount: ${session.amountSats} satoshis${codeApplied}\n` +
`Invoice id: ${session.invoiceId}\n\n` +
`After payment confirms on-chain or on Lightning, run the ` +
`"Finish license purchase" action — we will fetch the key for you.`,
}
},
)
// --- "Finish license purchase" --------------------------------------------
export const finishLicensePurchase = sdk.Action.withoutInput(
'finish-license-purchase',
async ({ effects }) => ({
name: 'Finish license purchase',
description:
`Check on a pending purchase started by "Buy license". If payment has ` +
`settled, the license key is fetched, verified, and stored ` +
`automatically.`,
warning: null,
allowedStatuses: 'any',
group: 'License',
visibility: 'enabled',
}),
async ({ effects }) => {
const store = await sdk.store.getOwn(effects, sdk.StorePath).const()
const invoiceId = (store as { license_pending_invoice_id: string | null })
.license_pending_invoice_id
if (!invoiceId) {
throw new Error(
`No pending purchase found. Run "Buy license" first to start one.`,
)
}
const client = await firstReachableClient()
const poll = await client.pollPurchase(invoiceId)
if (poll.status === 'expired' || poll.status === 'invalid') {
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_pending_invoice_id: null,
})
throw new Error(
`This invoice ${poll.status}. Run "Buy license" again to start over.`,
)
}
if (!poll.licenseKey) {
return {
message:
`Payment has not settled yet. Current status: ${poll.status}. ` +
`Try again in a minute — Bitcoin confirmations can take a few ` +
`minutes, Lightning is near-instant.`,
}
}
// Paid and key issued — verify and store.
const verifier = new Verifier(PublicKey.fromPem(ISSUER_PUBKEY_PEM))
const ok = verifier.verify(poll.licenseKey)
await sdk.store.getOwn(effects, sdk.StorePath).merge({
license_key: poll.licenseKey,
license_activated_at: new Date().toISOString(),
license_last_status: 'valid (issued from purchase)',
license_pending_invoice_id: null,
})
return {
message:
`Payment settled — license captured and activated.\n\n` +
`Product: ${ok.productId}\n` +
`License id: ${ok.licenseId}\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.`,
}
},
)
// ---------------------------------------------------------------------------
/** Return a Client pinned to the first reachable base URL, or throw. */
async function firstReachableClient(): Promise<Client> {
const urls = [LICENSING_BASE_URL, LICENSING_BASE_URL_TOR].filter(Boolean) as string[]
for (const base of urls) {
const client = new Client(base)
try {
// /v1/pubkey is the cheapest health-check that still proves the service
// is responsive and talking to us.
await client.fetchPubkeyPem()
return client
} catch (_) {}
}
throw new Error(
`Could not reach the licensing server at any configured URL. ` +
`Check your network (and Tor, if applicable) and try again.`,
)
}