diff --git a/lib/coinpay.ts b/lib/coinpay.ts index abc5fad..94caee9 100644 --- a/lib/coinpay.ts +++ b/lib/coinpay.ts @@ -33,6 +33,21 @@ export function payUrl(id: string): string { export type CreatedPayment = { id: string; status: string; payUrl: string }; +const SETUP_AMOUNT_RE = /^(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d{1,2})?$/; + +export function setupAmountUsd(value: unknown = SETUP_FEE_USD): number { + if (typeof value === "number") { + if (!Number.isFinite(value) || value <= 0) throw new Error("setup payment amount must be positive"); + return Math.round(value * 100) / 100; + } + + const text = String(value ?? "").trim().replace(/\s+/g, ""); + if (!SETUP_AMOUNT_RE.test(text)) throw new Error("setup payment amount must be a positive dollar amount"); + const amount = Number(text.replace(/,/g, "")); + if (!Number.isFinite(amount) || amount <= 0) throw new Error("setup payment amount must be positive"); + return amount; +} + export async function createSetupPayment(opts: { accountId: string; email: string; @@ -44,7 +59,7 @@ export async function createSetupPayment(opts: { // module is imported even when CoinPay isn't configured (offline dev mode). const coinpay = createCoinPayClient({ apiKey: API_KEY, baseUrl: ISSUER }); const { paymentId, payment } = await coinpay.createCheckout({ - amountUsd: Number(opts.amount || SETUP_FEE_USD), + amountUsd: setupAmountUsd(opts.amount || SETUP_FEE_USD), currency: PAY_CHAIN.toLowerCase(), paymentMethod: "crypto", description: `moshcoding account setup — ${opts.domain}`, diff --git a/tests/coinpay-amount.test.mjs b/tests/coinpay-amount.test.mjs new file mode 100644 index 0000000..f1bc97e --- /dev/null +++ b/tests/coinpay-amount.test.mjs @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { setupAmountUsd } from "../lib/coinpay.ts"; + +test("setupAmountUsd accepts ordinary positive dollar amounts", () => { + assert.equal(setupAmountUsd("1.00"), 1); + assert.equal(setupAmountUsd(" 1,234.56 "), 1234.56); + assert.equal(setupAmountUsd(2.345), 2.35); +}); + +test("setupAmountUsd rejects malformed or unsafe payment amounts", () => { + assert.throws(() => setupAmountUsd(""), /positive dollar amount/); + assert.throws(() => setupAmountUsd("1e2"), /positive dollar amount/); + assert.throws(() => setupAmountUsd("0x10"), /positive dollar amount/); + assert.throws(() => setupAmountUsd("-1"), /positive dollar amount/); + assert.throws(() => setupAmountUsd(Number.NaN), /positive/); + assert.throws(() => setupAmountUsd(Number.POSITIVE_INFINITY), /positive/); +});