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:
@@ -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,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user