{"version":3,"file":"topup.mjs","names":[],"sources":["../../../../../src/astro/routes/api/billing/topup.ts"],"sourcesContent":["/**\n * Credit top-up endpoint.\n *\n * POST /_emdash/api/billing/topup  { amount: number }\n *\n * The instance itself holds no payment keys — only its hosting parent does. So\n * this proxies to the parent's checkout endpoint (recorded in `billing:*`\n * options at provision time), passing this instance's project id and the\n * amount, and returns the parent's hosted checkout URL for the browser to\n * redirect to. Admin-only.\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm } from \"#api/authorize.js\";\nimport { apiError, handleError } from \"#api/error.js\";\nimport { OptionsRepository } from \"#db/repositories/options.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ locals, request }) => {\n\tconst { emdash, user } = locals;\n\n\tconst denied = requirePerm(user, \"settings:manage\");\n\tif (denied) return denied;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\tlet amount = 0;\n\ttry {\n\t\tconst body = (await request.json()) as { amount?: unknown };\n\t\tamount = Number(body?.amount);\n\t} catch {\n\t\treturn apiError(\"BAD_REQUEST\", \"Expected a JSON body { amount }\", 400);\n\t}\n\tif (!Number.isFinite(amount) || amount <= 0) {\n\t\treturn apiError(\"BAD_REQUEST\", \"amount must be a positive number\", 400);\n\t}\n\n\tconst options = new OptionsRepository(emdash.db);\n\tconst [parentUrl, projectId, currency] = await Promise.all([\n\t\toptions.get<string>(\"billing:parent_url\"),\n\t\toptions.get<string>(\"billing:project_id\"),\n\t\toptions.getOrDefault<string>(\"billing:currency\", \"USD\"),\n\t]);\n\n\tif (!parentUrl || !projectId) {\n\t\treturn apiError(\n\t\t\t\"NOT_CONFIGURED\",\n\t\t\t\"This instance has no billing parent configured — top-ups are handled by its host.\",\n\t\t\t409,\n\t\t);\n\t}\n\n\ttry {\n\t\tconst res = await fetch(parentUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tprojectId,\n\t\t\t\tamount,\n\t\t\t\tcurrency,\n\t\t\t\treturnUrl: new URL(\"/_emdash/admin/billing\", request.url).toString(),\n\t\t\t}),\n\t\t});\n\t\tconst data = (await res.json().catch(() => ({}))) as { checkoutUrl?: string; error?: string };\n\t\tif (!res.ok || !data.checkoutUrl) {\n\t\t\treturn apiError(\n\t\t\t\t\"CHECKOUT_FAILED\",\n\t\t\t\tdata.error ?? `Checkout failed (${res.status})`,\n\t\t\t\t502,\n\t\t\t);\n\t\t}\n\t\treturn Response.json({ success: true, data: { checkoutUrl: data.checkoutUrl } });\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to start checkout\", \"CHECKOUT_ERROR\");\n\t}\n};\n"],"mappings":";;;;;;;AAkBA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,QAAQ,cAAc;CAC5D,MAAM,EAAE,QAAQ,SAAS;CAEzB,MAAM,SAAS,YAAY,MAAM,kBAAkB;AACnD,KAAI,OAAQ,QAAO;AAEnB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAGpE,IAAI,SAAS;AACb,KAAI;EACH,MAAM,OAAQ,MAAM,QAAQ,MAAM;AAClC,WAAS,OAAO,MAAM,OAAO;SACtB;AACP,SAAO,SAAS,eAAe,mCAAmC,IAAI;;AAEvE,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,UAAU,EACzC,QAAO,SAAS,eAAe,oCAAoC,IAAI;CAGxE,MAAM,UAAU,IAAI,kBAAkB,OAAO,GAAG;CAChD,MAAM,CAAC,WAAW,WAAW,YAAY,MAAM,QAAQ,IAAI;EAC1D,QAAQ,IAAY,qBAAqB;EACzC,QAAQ,IAAY,qBAAqB;EACzC,QAAQ,aAAqB,oBAAoB,MAAM;EACvD,CAAC;AAEF,KAAI,CAAC,aAAa,CAAC,UAClB,QAAO,SACN,kBACA,qFACA,IACA;AAGF,KAAI;EACH,MAAM,MAAM,MAAM,MAAM,WAAW;GAClC,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,MAAM,KAAK,UAAU;IACpB;IACA;IACA;IACA,WAAW,IAAI,IAAI,0BAA0B,QAAQ,IAAI,CAAC,UAAU;IACpE,CAAC;GACF,CAAC;EACF,MAAM,OAAQ,MAAM,IAAI,MAAM,CAAC,aAAa,EAAE,EAAE;AAChD,MAAI,CAAC,IAAI,MAAM,CAAC,KAAK,YACpB,QAAO,SACN,mBACA,KAAK,SAAS,oBAAoB,IAAI,OAAO,IAC7C,IACA;AAEF,SAAO,SAAS,KAAK;GAAE,SAAS;GAAM,MAAM,EAAE,aAAa,KAAK,aAAa;GAAE,CAAC;UACxE,OAAO;AACf,SAAO,YAAY,OAAO,4BAA4B,iBAAiB"}