Harden onion boot flow and deepen site surfaces

Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation.

Made-with: Cursor
This commit is contained in:
drjones
2026-04-07 21:35:52 -07:00
parent 52432dccfa
commit 78a071ba02
162 changed files with 21692 additions and 39 deletions

121
app/api/btc/verify/route.ts Normal file
View File

@@ -0,0 +1,121 @@
import { NextResponse } from "next/server";
const MEMPOOL = "https://mempool.space/api";
const MIN_CONFIRMATIONS = 1;
function merchantAddress(): string {
return (
process.env.MERCHANT_BTC_ADDRESS ||
process.env.NEXT_PUBLIC_MERCHANT_BTC_ADDRESS ||
""
).trim();
}
export async function POST(req: Request) {
const merchant = merchantAddress();
if (!merchant) {
return NextResponse.json(
{ ok: false, error: "Server not configured: set MERCHANT_BTC_ADDRESS" },
{ status: 503 },
);
}
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
}
const txid =
typeof body === "object" &&
body !== null &&
"txid" in body &&
typeof (body as { txid: unknown }).txid === "string"
? (body as { txid: string }).txid.trim()
: "";
if (!/^[a-fA-F0-9]{64}$/.test(txid)) {
return NextResponse.json({ ok: false, error: "Invalid transaction id" }, { status: 400 });
}
const txRes = await fetch(`${MEMPOOL}/tx/${txid}`, { cache: "no-store" });
if (!txRes.ok) {
return NextResponse.json(
{ ok: false, error: "Transaction not found on network yet" },
{ status: 404 },
);
}
const tx = (await txRes.json()) as {
vout?: Array<{ value?: number; scriptpubkey_address?: string }>;
status?: { confirmed?: boolean; block_height?: number };
};
const merchantLower = merchant.toLowerCase();
let sats = 0;
for (const vout of tx.vout || []) {
const addr = vout.scriptpubkey_address;
if (addr && addr.toLowerCase() === merchantLower && typeof vout.value === "number") {
sats += vout.value;
}
}
if (sats <= 0) {
return NextResponse.json(
{
ok: false,
error: "This transaction does not pay your configured merchant Bitcoin address",
},
{ status: 400 },
);
}
let confirmations = 0;
if (tx.status?.confirmed && typeof tx.status.block_height === "number") {
const tipRes = await fetch(`${MEMPOOL}/blocks/tip/height`, { cache: "no-store" });
if (tipRes.ok) {
const tip = Number(await tipRes.text());
if (Number.isFinite(tip)) {
confirmations = tip - tx.status.block_height + 1;
}
}
}
if (confirmations < MIN_CONFIRMATIONS) {
return NextResponse.json({
ok: false,
error: `Need at least ${MIN_CONFIRMATIONS} confirmation(s); currently ${confirmations}`,
confirmations,
sats,
});
}
const priceRes = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
{ next: { revalidate: 60 } },
);
if (!priceRes.ok) {
return NextResponse.json(
{ ok: false, error: "Could not fetch BTC/USD rate; try again shortly" },
{ status: 502 },
);
}
const priceJson = (await priceRes.json()) as { bitcoin?: { usd?: number } };
const btcUsd = priceJson.bitcoin?.usd;
if (!btcUsd || !Number.isFinite(btcUsd)) {
return NextResponse.json({ ok: false, error: "Invalid rate response" }, { status: 502 });
}
const btc = sats / 1e8;
const creditedUsd = Math.round(btc * btcUsd * 100) / 100;
return NextResponse.json({
ok: true,
creditedUsd,
sats,
btc,
btcUsd,
confirmations,
});
}