v0.1.0:24 — Keysat licensing service end-to-end

Daemon, StartOS wrapper, admin SPA, public buy/thank-you pages,
discount codes, free-license redemption, Apply-discount UX,
self-licensing, and v0.1.0 release notes.
This commit is contained in:
Grant
2026-05-07 10:33:39 -05:00
parent 432250bffc
commit 6ac118ae70
90 changed files with 14896 additions and 524 deletions
+145 -15
View File
@@ -14,11 +14,12 @@
// The operator never sees or types an API key, store id, or webhook secret.
import { sdk } from '../sdk'
import { store } from '../fileModels/store'
import { adminCall, LICENSING_URL } from '../utils'
export const configureBtcpay = sdk.Action.withoutInput(
'configureBtcpay',
async ({ effects }) => ({
'configure-btcpay',
async () => ({
name: 'Connect BTCPay',
description:
"One-click connect to your BTCPay Server. Opens a consent page in " +
@@ -29,11 +30,12 @@ export const configureBtcpay = sdk.Action.withoutInput(
group: 'BTCPay',
visibility: 'enabled',
}),
async ({ effects }) => {
const store = await sdk.store.getOwn(effects, sdk.StorePath).const()
async () => {
const storeData = await store.read().once()
if (!storeData) throw new Error('Store not initialized — restart the service.')
const resp = await adminCall(
LICENSING_URL,
store.admin_api_key,
storeData.admin_api_key,
'/v1/admin/btcpay/connect',
{ method: 'POST' },
)
@@ -43,22 +45,102 @@ export const configureBtcpay = sdk.Action.withoutInput(
const body = (await resp.json()) as { authorize_url: string }
return {
version: '1',
title: 'Approve on BTCPay to finish connecting',
message:
'Open the URL below in your browser. You will be taken to your ' +
'BTCPay Server, where you click "Authorize". After that BTCPay ' +
'sends the API key back to Keysat automatically — you do not ' +
'need to copy anything.\n\n' +
body.authorize_url +
'\n\nYou can confirm the connection succeeded with the "Check BTCPay ' +
'connection" action once approval is complete.',
'need to copy anything.\n\nYou can confirm the connection succeeded ' +
'with the "Check BTCPay connection" action once approval is complete.',
result: {
type: 'single',
value: body.authorize_url,
copyable: true,
qr: false,
masked: false,
},
}
},
)
/** Replay id used by init/index.ts when surfacing the BTCPay setup task. */
const BTCPAY_SETUP_TASK_ID = 'btcpay-initial-setup'
/** Disconnect BTCPay — clean revocation path for re-authorize cases. */
export const disconnectBtcpay = sdk.Action.withoutInput(
'disconnect-btcpay',
async () => ({
name: 'Disconnect BTCPay',
description:
'Disconnect Keysat from your BTCPay Server: revoke the API key, ' +
'delete the registered webhook, and clear local connection state. ' +
"Run this before 'Connect BTCPay' if you want to re-authorize " +
'(e.g., to switch stores or rotate the API key). Existing license ' +
'keys, products, and policies are unaffected.',
warning:
'Until you re-run "Connect BTCPay" after this, new purchases will ' +
'return 503 (BTCPay not configured). Already-issued license keys ' +
'continue to validate normally.',
allowedStatuses: 'only-running',
group: 'BTCPay',
visibility: 'enabled',
}),
async ({ effects: _effects }) => {
const storeData = await store.read().once()
if (!storeData) throw new Error('Store not initialized — restart the service.')
const resp = await adminCall(
LICENSING_URL,
storeData.admin_api_key,
'/v1/admin/btcpay/disconnect',
{ method: 'POST' },
)
if (!resp.ok) {
throw new Error(`Disconnect failed: HTTP ${resp.status}${await resp.text()}`)
}
const body = (await resp.json()) as
| { ok: true; noop: true; message: string }
| {
ok: true
noop: false
store_id: string | null
webhook_id: string | null
warnings: string[]
}
if ('noop' in body && body.noop) {
return {
version: '1',
title: 'Already disconnected',
message: body.message,
result: null,
}
}
const b = body as {
ok: true
noop: false
store_id: string | null
webhook_id: string | null
warnings: string[]
}
const warningsBlock = b.warnings.length > 0
? `\n\nWarnings:\n${b.warnings.map((w) => `${w}`).join('\n')}`
: ''
return {
version: '1',
title: 'BTCPay disconnected',
message:
`Local BTCPay connection cleared. ` +
`Store id was ${b.store_id ?? '(unknown)'}, webhook id was ${b.webhook_id ?? '(none)'}. ` +
`You can now run "Connect BTCPay" again to re-authorize.${warningsBlock}`,
result: null,
}
},
)
/** Optional companion action: show current BTCPay connection state. */
export const btcpayStatus = sdk.Action.withoutInput(
'btcpayStatus',
async ({ effects }) => ({
'btcpay-status',
async () => ({
name: 'Check BTCPay connection',
description: 'Shows whether BTCPay is currently connected, and the store id.',
warning: null,
@@ -67,10 +149,11 @@ export const btcpayStatus = sdk.Action.withoutInput(
visibility: 'enabled',
}),
async ({ effects }) => {
const store = await sdk.store.getOwn(effects, sdk.StorePath).const()
const storeData = await store.read().once()
if (!storeData) throw new Error('Store not initialized — restart the service.')
const resp = await adminCall(
LICENSING_URL,
store.admin_api_key,
storeData.admin_api_key,
'/v1/admin/btcpay/status',
{ method: 'GET' },
)
@@ -83,15 +166,62 @@ export const btcpayStatus = sdk.Action.withoutInput(
if (!body.connected) {
return {
version: '1',
title: 'Not connected',
message: 'BTCPay is not connected yet. Run the "Connect BTCPay" action to authorize.',
result: null,
}
}
// BTCPay is connected — clear the install-time setup task so it
// disappears from the dashboard. clearTask is idempotent and
// tolerates being called when no such task exists, so this is safe
// every time btcpayStatus is run.
try {
await sdk.action.clearTask(effects, BTCPAY_SETUP_TASK_ID)
} catch (_) {
// Non-fatal — we still report status.
}
// Also check whether BTCPay's store has any payment methods (wallet
// / Lightning) configured. A connected store with zero payment
// methods can't actually issue invoices — that's the trap that
// surfaces as "BTC-CHAIN: Payment method unavailable" when buyers
// try to purchase. Surface the situation here so the operator
// discovers it BEFORE a customer hits a broken purchase flow.
let walletNote = ''
try {
const pmResp = await adminCall(
LICENSING_URL,
storeData.admin_api_key,
'/v1/admin/btcpay/payment-methods',
{ method: 'GET' },
)
if (pmResp.ok) {
const pmBody = (await pmResp.json()) as { count: number }
if (pmBody.count === 0) {
walletNote =
`\n\n⚠ NO WALLET CONFIGURED on this BTCPay store. Buyers won't ` +
`be able to pay until you set one up.\n` +
`Open your BTCPay store settings (${body.base_url.replace(/^http:\/\//, 'http://').replace(/^https:\/\//, 'https://')}/stores/${body.store_id}) ` +
`→ Wallets / Lightning, then come back and re-run "Check BTCPay connection".`
} else {
walletNote = `\n\n✓ ${pmBody.count} payment method(s) configured.`
}
}
} catch (_) {
// Non-fatal: payment-method check is informational.
}
return {
version: '1',
title: 'BTCPay is connected',
message:
`BTCPay is connected.\n` +
`Store id: ${body.store_id}\n` +
`Webhook id: ${body.webhook_id ?? '(not registered — check BTCPay manually)'}\n` +
`Base URL: ${body.base_url}`,
`Base URL: ${body.base_url}` +
walletNote,
result: null,
}
},
)