Anonymous Bitcoin-only fashion e-commerce with: - Vue 3 + Tailwind 4 frontend with glassmorphism dark/light design system - Express 5 + SQLite backend with BTCPay Server integration - Nostr identity (NIP-07/keypair) for anonymous purchase tracking - ChaCha20-Poly1305 encrypted shipping addresses - Admin panel with order/product/stock management - SVG logo splash animation with clip-path reveal - 5 seeded products across 4 categories Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
51 lines
2.4 KiB
TypeScript
51 lines
2.4 KiB
TypeScript
import crypto from 'node:crypto'
|
|
|
|
const TIMEOUT = 10_000
|
|
|
|
function getConfig() {
|
|
const url = process.env.BTCPAY_URL
|
|
const apiKey = process.env.BTCPAY_API_KEY
|
|
const storeId = process.env.BTCPAY_STORE_ID
|
|
if (!url || !apiKey || !storeId) throw new Error('BTCPay configuration missing: BTCPAY_URL, BTCPAY_API_KEY, BTCPAY_STORE_ID required')
|
|
return { url: url.replace(/\/$/, ''), apiKey, storeId }
|
|
}
|
|
|
|
async function btcpayFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
|
const { url, apiKey } = getConfig()
|
|
const controller = new AbortController()
|
|
const timer = setTimeout(() => controller.abort(), TIMEOUT)
|
|
try {
|
|
return await fetch(`${url}${path}`, { ...options, signal: controller.signal, headers: { Authorization: `token ${apiKey}`, 'Content-Type': 'application/json', ...options.headers } })
|
|
} finally { clearTimeout(timer) }
|
|
}
|
|
|
|
export interface CreateInvoiceParams { amountSats: number; orderId: string; redirectUrl: string }
|
|
export interface BtcPayInvoice { id: string; checkoutLink: string; status: string; amount: string; currency: string }
|
|
|
|
export async function createInvoice(params: CreateInvoiceParams): Promise<BtcPayInvoice> {
|
|
const { storeId } = getConfig()
|
|
const res = await btcpayFetch(`/api/v1/stores/${storeId}/invoices`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ amount: String(params.amountSats), currency: 'SATS', metadata: { orderId: params.orderId }, checkout: { redirectURL: params.redirectUrl, redirectAutomatically: true } }),
|
|
})
|
|
if (!res.ok) { const text = await res.text(); throw new Error(`BTCPay invoice creation failed: ${res.status} ${text}`) }
|
|
return res.json() as Promise<BtcPayInvoice>
|
|
}
|
|
|
|
export async function getInvoice(invoiceId: string): Promise<BtcPayInvoice> {
|
|
const { storeId } = getConfig()
|
|
const res = await btcpayFetch(`/api/v1/stores/${storeId}/invoices/${invoiceId}`)
|
|
if (!res.ok) throw new Error(`BTCPay invoice fetch failed: ${res.status}`)
|
|
return res.json() as Promise<BtcPayInvoice>
|
|
}
|
|
|
|
export function validateWebhookSignature(body: string, signature: string): boolean {
|
|
const secret = process.env.BTCPAY_WEBHOOK_SECRET
|
|
if (!secret) return false
|
|
const expected = crypto.createHmac('sha256', secret).update(body).digest('hex')
|
|
const sigBuf = Buffer.from(signature.replace('sha256=', ''), 'hex')
|
|
const expectedBuf = Buffer.from(expected, 'hex')
|
|
if (sigBuf.length !== expectedBuf.length) return false
|
|
return crypto.timingSafeEqual(sigBuf, expectedBuf)
|
|
}
|