/** * Credit top-up endpoint. * * POST /_emdash/api/billing/topup { amount: number } * * The instance itself holds no payment keys — only its hosting parent does. So * this proxies to the parent's checkout endpoint (recorded in `billing:*` * options at provision time), passing this instance's project id and the * amount, and returns the parent's hosted checkout URL for the browser to * redirect to. Admin-only. */ import type { APIRoute } from "astro"; import { requirePerm } from "#api/authorize.js"; import { apiError, handleError } from "#api/error.js"; import { OptionsRepository } from "#db/repositories/options.js"; export const prerender = false; export const POST: APIRoute = async ({ locals, request }) => { const { emdash, user } = locals; const denied = requirePerm(user, "settings:manage"); if (denied) return denied; if (!emdash?.db) { return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); } let amount = 0; try { const body = (await request.json()) as { amount?: unknown }; amount = Number(body?.amount); } catch { return apiError("BAD_REQUEST", "Expected a JSON body { amount }", 400); } if (!Number.isFinite(amount) || amount <= 0) { return apiError("BAD_REQUEST", "amount must be a positive number", 400); } const options = new OptionsRepository(emdash.db); const [parentUrl, projectId, currency] = await Promise.all([ options.get("billing:parent_url"), options.get("billing:project_id"), options.getOrDefault("billing:currency", "USD"), ]); if (!parentUrl || !projectId) { return apiError( "NOT_CONFIGURED", "This instance has no billing parent configured — top-ups are handled by its host.", 409, ); } try { const res = await fetch(parentUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ projectId, amount, currency, returnUrl: new URL("/_emdash/admin/billing", request.url).toString(), }), }); const data = (await res.json().catch(() => ({}))) as { checkoutUrl?: string; error?: string }; if (!res.ok || !data.checkoutUrl) { return apiError( "CHECKOUT_FAILED", data.error ?? `Checkout failed (${res.status})`, 502, ); } return Response.json({ success: true, data: { checkoutUrl: data.checkoutUrl } }); } catch (error) { return handleError(error, "Failed to start checkout", "CHECKOUT_ERROR"); } };