@happyvertical/payments 0.90.0 → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,6 +30,9 @@ import { StripeAdapter } from '@happyvertical/payments/stripe';
30
30
  addresses, x402 proof verification, and injected signer payouts.
31
31
  - `BtcAdapter`: BTCPay Server invoice, polling/webhook status, tiered
32
32
  confirmation policy, and unsigned PSBT payout creation.
33
+ - `BtcpayClient` (`@happyvertical/payments/btcpay`): a stateless BTCPay
34
+ Greenfield client for billing integrations — see
35
+ [BTCPay Greenfield client](#btcpay-greenfield-client).
33
36
  - `StripeAdapter`: Stripe Checkout URL settlement, webhook verification,
34
37
  refunds, Stripe Connect transfers, saved payment methods / card-on-file
35
38
  (`createSetupSession` / `getSetupResult`), and the manual-capture card
@@ -38,6 +41,60 @@ import { StripeAdapter } from '@happyvertical/payments/stripe';
38
41
  The package does not depend on SMRT or a database. Consumers own quote
39
42
  persistence, webhook routing, and operational policy.
40
43
 
44
+ ### BTCPay Greenfield client
45
+
46
+ `BtcpayClient` is a stateless client for one BTCPay Server store (Greenfield
47
+ 2.x). It creates, reads, and lists invoices (filterable by `orderId`), reads an
48
+ invoice's payment methods and payments, and verifies and parses webhooks.
49
+ Decimal amounts stay strings; Greenfield unix-second timestamps become `Date`s.
50
+ BTCPay has no idempotency keys, so callers make creation idempotent by
51
+ listing by `orderId` first.
52
+
53
+ ```ts
54
+ import {
55
+ BtcpayClient,
56
+ isValidBtcpayWebhookSignature,
57
+ parseBtcpayWebhook,
58
+ } from '@happyvertical/payments/btcpay';
59
+
60
+ const btcpay = new BtcpayClient({ baseUrl, apiKey, storeId });
61
+ const [existing] = await btcpay.listInvoices({ orderId, status: ['New', 'Processing', 'Settled'] });
62
+ const invoice =
63
+ existing ??
64
+ (await btcpay.createInvoice({
65
+ amount: '25.00',
66
+ currency: 'CAD',
67
+ orderId,
68
+ checkout: {
69
+ speedPolicy: 'MediumSpeed', // 1 confirmation
70
+ paymentMethods: ['BTC-CHAIN'],
71
+ expirationMinutes: 15,
72
+ paymentTolerance: 0,
73
+ redirectURL: successUrl,
74
+ },
75
+ }));
76
+ redirect(invoice.checkoutLink);
77
+
78
+ // webhook route
79
+ if (!isValidBtcpayWebhookSignature(rawBody, request.headers.get('BTCPay-Sig'), webhookSecret)) {
80
+ return new Response(null, { status: 400 });
81
+ }
82
+ const delivery = parseBtcpayWebhook(rawBody); // persist delivery.deliveryId to dedupe
83
+ const current = await btcpay.getInvoice(delivery.invoiceId!); // act on re-read state
84
+ ```
85
+
86
+ Invoice payments do not report confirmation counts; BTCPay settles an
87
+ invoice by its `speedPolicy` (`HighSpeed` 0, `MediumSpeed` 1,
88
+ `LowMediumSpeed` 2, `LowSpeed` 6 confirmations — no other values exist). An
89
+ on-chain payment's `transactionId` is parsed from its `<txid>-<vout>` id.
90
+
91
+ Errors are `BtcpayApiError` (`status`, Greenfield `apiCode`, `retryable` for
92
+ 429 and for reads that failed on the network or with 5xx). A `createInvoice`
93
+ that failed on the network or with 5xx may have created the invoice, so it is
94
+ never `retryable`: look it up by `orderId` before creating another. Messages never include the API key. A store
95
+ API key needs only `btcpay.store.canviewinvoices` and
96
+ `btcpay.store.cancreateinvoice` for these calls.
97
+
41
98
  ### Save a card (setup) → charge later
42
99
 
43
100
  ```ts
@@ -0,0 +1,170 @@
1
+ import { PaymentProviderError } from '../errors.js';
2
+ import { FetchLike } from '../shared.js';
3
+ /** Invoice status as reported by Greenfield. */
4
+ export type BtcpayInvoiceStatus = 'New' | 'Processing' | 'Expired' | 'Invalid' | 'Settled';
5
+ /** Greenfield's `additionalStatus` qualifier on an invoice. */
6
+ export type BtcpayInvoiceAdditionalStatus = 'None' | 'PaidLate' | 'PaidPartial' | 'Marked' | 'Invalid' | 'PaidOver';
7
+ /**
8
+ * Confirmations before BTCPay settles an on-chain payment: `HighSpeed` 0
9
+ * (1 for RBF-signalling transactions), `MediumSpeed` 1, `LowMediumSpeed` 2,
10
+ * `LowSpeed` 6. These are the only values BTCPay supports.
11
+ */
12
+ export type BtcpaySpeedPolicy = 'HighSpeed' | 'MediumSpeed' | 'LowMediumSpeed' | 'LowSpeed';
13
+ export interface BtcpayInvoiceCheckoutOptions {
14
+ speedPolicy?: BtcpaySpeedPolicy;
15
+ /** Payment method ids, for example `['BTC-CHAIN']` or `['BTC-CHAIN', 'BTC-LN']`. */
16
+ paymentMethods?: string[];
17
+ defaultPaymentMethod?: string;
18
+ /** Minutes the rate is locked and the invoice accepts new payments. */
19
+ expirationMinutes?: number;
20
+ /** Minutes after expiry during which late payments are still tracked. */
21
+ monitoringMinutes?: number;
22
+ /** Percentage underpayment BTCPay treats as paid (0–100). */
23
+ paymentTolerance?: number;
24
+ redirectURL?: string;
25
+ redirectAutomatically?: boolean;
26
+ defaultLanguage?: string;
27
+ }
28
+ export interface BtcpayCreateInvoiceInput {
29
+ /** Decimal amount in `currency`, for example `'25.00'`. */
30
+ amount: string;
31
+ currency: string;
32
+ /** Merchant order id; stored as `metadata.orderId` and filterable. */
33
+ orderId?: string;
34
+ metadata?: Record<string, unknown>;
35
+ checkout?: BtcpayInvoiceCheckoutOptions;
36
+ additionalSearchTerms?: string[];
37
+ }
38
+ export interface BtcpayInvoice {
39
+ id: string;
40
+ storeId?: string;
41
+ status: BtcpayInvoiceStatus | string;
42
+ additionalStatus: BtcpayInvoiceAdditionalStatus | string;
43
+ /** Decimal amount in `currency`. */
44
+ amount: string;
45
+ currency: string;
46
+ checkoutLink?: string;
47
+ createdTime?: Date;
48
+ expirationTime?: Date;
49
+ monitoringExpiration?: Date;
50
+ archived: boolean;
51
+ metadata: Record<string, unknown>;
52
+ checkout: Record<string, unknown>;
53
+ raw: Record<string, unknown>;
54
+ }
55
+ export interface BtcpayListInvoicesInput {
56
+ orderId?: string | string[];
57
+ status?: BtcpayInvoiceStatus | BtcpayInvoiceStatus[];
58
+ textSearch?: string;
59
+ skip?: number;
60
+ take?: number;
61
+ }
62
+ export interface BtcpayInvoicePayment {
63
+ /** On-chain payments use `<txid>-<vout>`. */
64
+ id: string;
65
+ /** The transaction id of an on-chain payment (parsed from `id`). */
66
+ transactionId?: string;
67
+ receivedDate?: Date;
68
+ /** Decimal amount in the payment method's currency (BTC). */
69
+ value: string;
70
+ /** Decimal network/payment-method fee. */
71
+ fee: string;
72
+ status: 'Invalid' | 'Processing' | 'Settled' | string;
73
+ destination?: string;
74
+ }
75
+ export interface BtcpayInvoicePaymentMethod {
76
+ /** `BTC-CHAIN`, `BTC-LN`, … (Greenfield 2.x `paymentMethodId`). */
77
+ paymentMethodId: string;
78
+ /** The payment method's currency, for example `BTC`. */
79
+ currency?: string;
80
+ destination?: string;
81
+ paymentLink?: string;
82
+ /** Invoice-currency price of one unit of the payment currency, if reported. */
83
+ rate?: string;
84
+ /** Decimal amount due in the payment currency when created. */
85
+ amount: string;
86
+ /** Decimal amount paid through this method. */
87
+ paymentMethodPaid: string;
88
+ /** Decimal amount paid through all methods, in this method's currency. */
89
+ totalPaid: string;
90
+ /**
91
+ * Decimal amount still due, in this method's currency. Undefined when
92
+ * BTCPay did not report it — never read a missing value as "nothing due".
93
+ */
94
+ due?: string;
95
+ payments: BtcpayInvoicePayment[];
96
+ raw: Record<string, unknown>;
97
+ }
98
+ export interface BtcpayClientOptions {
99
+ /** Server origin, for example `https://btcpay.example.com` (no path). */
100
+ baseUrl: string;
101
+ /** Greenfield API key (`Authorization: token …`). */
102
+ apiKey: string;
103
+ storeId: string;
104
+ fetch?: FetchLike;
105
+ /** Per-request timeout (default 30 seconds). */
106
+ timeoutMs?: number;
107
+ }
108
+ /**
109
+ * A Greenfield request failed. Never carries the API key.
110
+ *
111
+ * `retryable` is true only when repeating the same call cannot duplicate a
112
+ * side effect: a read (`GET`) that failed on the network, with 429, or with
113
+ * 5xx, or any request refused with 429. A `createInvoice` that failed on the
114
+ * network or with 5xx may still have created the invoice, so it is not
115
+ * retryable: resolve it with `listInvoices({ orderId })` instead.
116
+ */
117
+ export declare class BtcpayApiError extends PaymentProviderError {
118
+ readonly status: number;
119
+ readonly apiCode?: string;
120
+ constructor(message: string, status: number, apiCode?: string, options?: {
121
+ cause?: unknown;
122
+ method?: 'GET' | 'POST';
123
+ });
124
+ }
125
+ export declare class BtcpayClient {
126
+ readonly baseUrl: string;
127
+ readonly storeId: string;
128
+ private readonly apiKey;
129
+ private readonly fetch;
130
+ private readonly timeoutMs;
131
+ constructor(options: BtcpayClientOptions);
132
+ /**
133
+ * `POST /api/v1/stores/{storeId}/invoices`. Not idempotent at BTCPay: after
134
+ * a network or 5xx failure the invoice may exist, so the error is not
135
+ * `retryable` — look it up with `listInvoices({ orderId })` before creating
136
+ * another.
137
+ */
138
+ createInvoice(input: BtcpayCreateInvoiceInput): Promise<BtcpayInvoice>;
139
+ /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}`. */
140
+ getInvoice(invoiceId: string): Promise<BtcpayInvoice>;
141
+ /** `GET /api/v1/stores/{storeId}/invoices` with Greenfield filters. */
142
+ listInvoices(input?: BtcpayListInvoicesInput): Promise<BtcpayInvoice[]>;
143
+ /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods`. */
144
+ getInvoicePaymentMethods(invoiceId: string): Promise<BtcpayInvoicePaymentMethod[]>;
145
+ private storePath;
146
+ private request;
147
+ }
148
+ /** A parsed BTCPay webhook delivery (signature must be verified first). */
149
+ export interface BtcpayWebhookDelivery {
150
+ deliveryId: string;
151
+ webhookId?: string;
152
+ /** Set on a redelivery: the id of the delivery it repeats. */
153
+ originalDeliveryId?: string;
154
+ isRedelivery: boolean;
155
+ /** `InvoiceSettled`, `InvoiceExpired`, … */
156
+ type: string;
157
+ timestamp?: Date;
158
+ storeId?: string;
159
+ invoiceId?: string;
160
+ metadata?: Record<string, unknown>;
161
+ raw: Record<string, unknown>;
162
+ }
163
+ /**
164
+ * Check a `BTCPay-Sig` header (`sha256=<hex>`) against the raw request body
165
+ * in constant time. Returns false for any missing or malformed input.
166
+ */
167
+ export declare function isValidBtcpayWebhookSignature(rawBody: string, header: string | null | undefined, secret: string): boolean;
168
+ /** Parse a webhook body. Throws `PaymentProviderError` when it is malformed. */
169
+ export declare function parseBtcpayWebhook(rawBody: string): BtcpayWebhookDelivery;
170
+ //# sourceMappingURL=btcpay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"btcpay.d.ts","sourceRoot":"","sources":["../../src/adapters/btcpay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,308 @@
1
+ import { r as PaymentProviderError, t as PaymentConfigurationError } from "../chunks/errors-feZrwnTy.js";
2
+ import { o as getFetch } from "../chunks/shared-DVl6UzD9.js";
3
+ import { createHmac, timingSafeEqual } from "node:crypto";
4
+ //#region src/adapters/btcpay.ts
5
+ /**
6
+ * A small, stateless client for the BTCPay Server Greenfield API (2.x).
7
+ *
8
+ * It covers what billing integrations need: store invoices (create, read,
9
+ * list by `orderId`), an invoice's payment methods, and webhook signature
10
+ * verification and parsing. It keeps no state between calls — idempotency and
11
+ * webhook de-duplication belong to the caller's durable storage.
12
+ *
13
+ * Decimal amounts stay strings end to end; callers convert them to their own
14
+ * minor units.
15
+ */
16
+ /**
17
+ * A Greenfield request failed. Never carries the API key.
18
+ *
19
+ * `retryable` is true only when repeating the same call cannot duplicate a
20
+ * side effect: a read (`GET`) that failed on the network, with 429, or with
21
+ * 5xx, or any request refused with 429. A `createInvoice` that failed on the
22
+ * network or with 5xx may still have created the invoice, so it is not
23
+ * retryable: resolve it with `listInvoices({ orderId })` instead.
24
+ */
25
+ var BtcpayApiError = class extends PaymentProviderError {
26
+ status;
27
+ apiCode;
28
+ constructor(message, status, apiCode, options = {}) {
29
+ const { method = "GET", ...rest } = options;
30
+ super(message, {
31
+ ...rest,
32
+ retryable: status === 429 || method === "GET" && (status === 0 || status >= 500)
33
+ });
34
+ this.name = "BtcpayApiError";
35
+ this.status = status;
36
+ this.apiCode = apiCode;
37
+ }
38
+ };
39
+ var DEFAULT_TIMEOUT_MS = 3e4;
40
+ var BtcpayClient = class {
41
+ baseUrl;
42
+ storeId;
43
+ apiKey;
44
+ fetch;
45
+ timeoutMs;
46
+ constructor(options) {
47
+ this.baseUrl = normalizeBaseUrl(options.baseUrl);
48
+ this.apiKey = requireString(options.apiKey, "BtcpayClient apiKey");
49
+ this.storeId = requireString(options.storeId, "BtcpayClient storeId");
50
+ this.fetch = getFetch(options.fetch);
51
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
52
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new PaymentConfigurationError("BtcpayClient timeoutMs must be a positive number.");
53
+ this.timeoutMs = timeoutMs;
54
+ }
55
+ /**
56
+ * `POST /api/v1/stores/{storeId}/invoices`. Not idempotent at BTCPay: after
57
+ * a network or 5xx failure the invoice may exist, so the error is not
58
+ * `retryable` — look it up with `listInvoices({ orderId })` before creating
59
+ * another.
60
+ */
61
+ async createInvoice(input) {
62
+ const amount = requirePositiveDecimal(input.amount, "BTCPay invoice amount");
63
+ const currency = requireString(input.currency, "BTCPay invoice currency");
64
+ const metadata = { ...input.metadata ?? {} };
65
+ if (input.orderId !== void 0) metadata.orderId = requireString(input.orderId, "BTCPay orderId");
66
+ const body = {
67
+ amount,
68
+ currency,
69
+ metadata
70
+ };
71
+ if (input.checkout) body.checkout = compact({ ...input.checkout });
72
+ if (input.additionalSearchTerms?.length) body.additionalSearchTerms = input.additionalSearchTerms;
73
+ return toInvoice(await this.request("POST", this.storePath("/invoices"), body));
74
+ }
75
+ /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}`. */
76
+ async getInvoice(invoiceId) {
77
+ const id = requireString(invoiceId, "BTCPay invoiceId");
78
+ return toInvoice(await this.request("GET", this.storePath(`/invoices/${encodeURIComponent(id)}`)));
79
+ }
80
+ /** `GET /api/v1/stores/{storeId}/invoices` with Greenfield filters. */
81
+ async listInvoices(input = {}) {
82
+ const query = new URLSearchParams();
83
+ for (const orderId of toList(input.orderId)) query.append("orderId", requireString(orderId, "BTCPay orderId"));
84
+ for (const status of toList(input.status)) query.append("status", status);
85
+ if (input.textSearch) query.set("textSearch", input.textSearch);
86
+ if (input.skip !== void 0) query.set("skip", String(requireCount(input.skip, "skip")));
87
+ if (input.take !== void 0) query.set("take", String(requireCount(input.take, "take")));
88
+ const search = query.toString();
89
+ const suffix = search ? `?${search}` : "";
90
+ const raw = await this.request("GET", this.storePath(`/invoices${suffix}`));
91
+ const items = Array.isArray(raw) ? raw : Array.isArray(raw?.items) ? raw.items : null;
92
+ if (!items) throw new BtcpayApiError("BTCPay invoice list response was not an array.", 200);
93
+ return items.map(toInvoice);
94
+ }
95
+ /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods`. */
96
+ async getInvoicePaymentMethods(invoiceId) {
97
+ const id = requireString(invoiceId, "BTCPay invoiceId");
98
+ const raw = await this.request("GET", this.storePath(`/invoices/${encodeURIComponent(id)}/payment-methods`));
99
+ if (!Array.isArray(raw)) throw new BtcpayApiError("BTCPay payment-methods response was not an array.", 200);
100
+ return raw.filter(isRecord).map(toPaymentMethod);
101
+ }
102
+ storePath(suffix) {
103
+ return `/api/v1/stores/${encodeURIComponent(this.storeId)}${suffix}`;
104
+ }
105
+ async request(method, path, body) {
106
+ const headers = new Headers();
107
+ headers.set("Authorization", `token ${this.apiKey}`);
108
+ headers.set("Accept", "application/json");
109
+ if (body !== void 0) headers.set("Content-Type", "application/json");
110
+ let response;
111
+ try {
112
+ response = await this.fetch(`${this.baseUrl}${path}`, {
113
+ method,
114
+ headers,
115
+ body: body === void 0 ? void 0 : JSON.stringify(body),
116
+ signal: AbortSignal.timeout(this.timeoutMs)
117
+ });
118
+ } catch (error) {
119
+ throw new BtcpayApiError(`BTCPay ${method} ${redactPath(path)} failed: ${errorName(error)}.`, 0, void 0, {
120
+ cause: error,
121
+ method
122
+ });
123
+ }
124
+ let text;
125
+ try {
126
+ text = await response.text();
127
+ } catch (error) {
128
+ throw new BtcpayApiError(`BTCPay ${method} ${redactPath(path)} failed reading the response: ${errorName(error)}.`, 0, void 0, {
129
+ cause: error,
130
+ method
131
+ });
132
+ }
133
+ let parsed;
134
+ if (text) try {
135
+ parsed = JSON.parse(text);
136
+ } catch (error) {
137
+ if (response.ok) throw new BtcpayApiError(`BTCPay ${method} ${redactPath(path)} returned invalid JSON.`, response.status, void 0, { cause: error });
138
+ }
139
+ if (!response.ok) {
140
+ const { code, message } = readGreenfieldError(parsed);
141
+ throw new BtcpayApiError(`BTCPay ${method} ${redactPath(path)} failed with HTTP ${response.status}` + (message ? `: ${message}` : "."), response.status, code, { method });
142
+ }
143
+ return parsed;
144
+ }
145
+ };
146
+ /**
147
+ * Check a `BTCPay-Sig` header (`sha256=<hex>`) against the raw request body
148
+ * in constant time. Returns false for any missing or malformed input.
149
+ */
150
+ function isValidBtcpayWebhookSignature(rawBody, header, secret) {
151
+ if (typeof rawBody !== "string" || typeof header !== "string") return false;
152
+ if (typeof secret !== "string" || !secret) return false;
153
+ const match = /^sha256=([a-f0-9]{64})$/i.exec(header.trim());
154
+ if (!match) return false;
155
+ const expected = createHmac("sha256", secret).update(rawBody).digest();
156
+ const actual = Buffer.from(match[1], "hex");
157
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
158
+ }
159
+ /** Parse a webhook body. Throws `PaymentProviderError` when it is malformed. */
160
+ function parseBtcpayWebhook(rawBody) {
161
+ let raw;
162
+ try {
163
+ raw = JSON.parse(rawBody);
164
+ } catch (error) {
165
+ throw new PaymentProviderError("BTCPay webhook body is not JSON.", { cause: error });
166
+ }
167
+ if (!isRecord(raw)) throw new PaymentProviderError("BTCPay webhook body is not an object.");
168
+ const deliveryId = readString(raw, "deliveryId");
169
+ const type = readString(raw, "type");
170
+ if (!deliveryId || !type) throw new PaymentProviderError("BTCPay webhook body needs deliveryId and type.");
171
+ return {
172
+ deliveryId,
173
+ webhookId: readString(raw, "webhookId"),
174
+ originalDeliveryId: readString(raw, "originalDeliveryId"),
175
+ isRedelivery: raw.isRedelivery === true,
176
+ type,
177
+ timestamp: readDate(raw.timestamp),
178
+ storeId: readString(raw, "storeId"),
179
+ invoiceId: readString(raw, "invoiceId"),
180
+ metadata: isRecord(raw.metadata) ? raw.metadata : void 0,
181
+ raw
182
+ };
183
+ }
184
+ function toInvoice(value) {
185
+ if (!isRecord(value)) throw new BtcpayApiError("BTCPay invoice response was not an object.", 200);
186
+ const id = readString(value, "id");
187
+ if (!id) throw new BtcpayApiError("BTCPay invoice response has no id.", 200);
188
+ return {
189
+ id,
190
+ storeId: readString(value, "storeId"),
191
+ status: readString(value, "status") ?? "New",
192
+ additionalStatus: readString(value, "additionalStatus") ?? "None",
193
+ amount: readDecimal(value.amount) ?? "0",
194
+ currency: readString(value, "currency") ?? "",
195
+ checkoutLink: readString(value, "checkoutLink"),
196
+ createdTime: readDate(value.createdTime),
197
+ expirationTime: readDate(value.expirationTime),
198
+ monitoringExpiration: readDate(value.monitoringExpiration),
199
+ archived: value.archived === true,
200
+ metadata: isRecord(value.metadata) ? value.metadata : {},
201
+ checkout: isRecord(value.checkout) ? value.checkout : {},
202
+ raw: value
203
+ };
204
+ }
205
+ function toPaymentMethod(value) {
206
+ return {
207
+ paymentMethodId: readString(value, "paymentMethodId") ?? readString(value, "paymentMethod") ?? "",
208
+ currency: readString(value, "currency") ?? readString(value, "cryptoCode"),
209
+ destination: readString(value, "destination"),
210
+ paymentLink: readString(value, "paymentLink"),
211
+ rate: readDecimal(value.rate),
212
+ amount: readDecimal(value.amount) ?? "0",
213
+ paymentMethodPaid: readDecimal(value.paymentMethodPaid) ?? "0",
214
+ totalPaid: readDecimal(value.totalPaid) ?? "0",
215
+ due: readDecimal(value.due),
216
+ payments: Array.isArray(value.payments) ? value.payments.filter(isRecord).map((payment) => ({
217
+ id: readString(payment, "id") ?? "",
218
+ transactionId: onChainTransactionId(readString(payment, "id")),
219
+ receivedDate: readDate(payment.receivedDate),
220
+ value: readDecimal(payment.value) ?? "0",
221
+ fee: readDecimal(payment.fee) ?? "0",
222
+ status: readString(payment, "status") ?? "",
223
+ destination: readString(payment, "destination")
224
+ })) : [],
225
+ raw: value
226
+ };
227
+ }
228
+ function readGreenfieldError(body) {
229
+ if (Array.isArray(body)) return {
230
+ code: "validation-error",
231
+ message: body.filter(isRecord).map((item) => [readString(item, "path"), readString(item, "message")].filter(Boolean).join(": ")).filter(Boolean).join("; ")
232
+ };
233
+ if (isRecord(body)) return {
234
+ code: readString(body, "code"),
235
+ message: readString(body, "message")
236
+ };
237
+ return {};
238
+ }
239
+ /** Greenfield paths carry only store/invoice ids; strip the query string. */
240
+ function redactPath(path) {
241
+ return path.split("?")[0];
242
+ }
243
+ function errorName(error) {
244
+ if (error instanceof Error) return error.name === "TimeoutError" ? "timed out" : error.name;
245
+ return "network error";
246
+ }
247
+ function normalizeBaseUrl(value) {
248
+ const raw = requireString(value, "BtcpayClient baseUrl");
249
+ let parsed;
250
+ try {
251
+ parsed = new URL(raw);
252
+ } catch (error) {
253
+ throw new PaymentConfigurationError("BtcpayClient baseUrl must be an absolute URL.", { cause: error });
254
+ }
255
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new PaymentConfigurationError("BtcpayClient baseUrl must use http or https.");
256
+ if (parsed.search || parsed.hash || parsed.username || parsed.password || !["", "/"].includes(parsed.pathname)) throw new PaymentConfigurationError("BtcpayClient baseUrl must be an origin without path, query, fragment, or credentials.");
257
+ return parsed.origin;
258
+ }
259
+ function requireString(value, label) {
260
+ if (typeof value !== "string" || !value.trim()) throw new PaymentConfigurationError(`${label} is required.`);
261
+ return value.trim();
262
+ }
263
+ function requirePositiveDecimal(value, label) {
264
+ if (typeof value !== "string" || !/^\d+(\.\d+)?$/.test(value.trim()) || !/[1-9]/.test(value)) throw new PaymentConfigurationError(`${label} must be a positive decimal string.`);
265
+ return value.trim();
266
+ }
267
+ function requireCount(value, label) {
268
+ if (!Number.isSafeInteger(value) || value < 0) throw new PaymentConfigurationError(`BTCPay ${label} must be a non-negative integer.`);
269
+ return value;
270
+ }
271
+ function toList(value) {
272
+ if (value === void 0) return [];
273
+ return Array.isArray(value) ? value : [value];
274
+ }
275
+ function compact(value) {
276
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
277
+ }
278
+ function isRecord(value) {
279
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
280
+ }
281
+ function readString(record, key) {
282
+ const value = record[key];
283
+ if (typeof value === "string" && value.trim()) return value.trim();
284
+ }
285
+ /** Keep decimals as strings; accept numbers Greenfield may emit. */
286
+ function readDecimal(value) {
287
+ if (typeof value === "string" && value.trim()) return value.trim();
288
+ if (typeof value === "number" && Number.isFinite(value)) {
289
+ const text = String(value);
290
+ return /e/i.test(text) ? value.toFixed(20).replace(/0+$/, "").replace(/\.$/, "") : text;
291
+ }
292
+ }
293
+ function onChainTransactionId(id) {
294
+ const match = id ? /^([0-9a-f]{64})-\d+$/i.exec(id) : null;
295
+ return match ? match[1].toLowerCase() : void 0;
296
+ }
297
+ /** Greenfield timestamps are unix seconds; ISO strings are accepted too. */
298
+ function readDate(value) {
299
+ if (typeof value === "number" && Number.isFinite(value)) return /* @__PURE__ */ new Date(value * 1e3);
300
+ if (typeof value === "string" && value.trim()) {
301
+ const date = new Date(value);
302
+ return Number.isNaN(date.getTime()) ? void 0 : date;
303
+ }
304
+ }
305
+ //#endregion
306
+ export { BtcpayApiError, BtcpayClient, isValidBtcpayWebhookSignature, parseBtcpayWebhook };
307
+
308
+ //# sourceMappingURL=btcpay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"btcpay.js","names":[],"sources":["../../src/adapters/btcpay.ts"],"sourcesContent":["/**\n * A small, stateless client for the BTCPay Server Greenfield API (2.x).\n *\n * It covers what billing integrations need: store invoices (create, read,\n * list by `orderId`), an invoice's payment methods, and webhook signature\n * verification and parsing. It keeps no state between calls — idempotency and\n * webhook de-duplication belong to the caller's durable storage.\n *\n * Decimal amounts stay strings end to end; callers convert them to their own\n * minor units.\n */\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport { PaymentConfigurationError, PaymentProviderError } from '../errors.js';\nimport { type FetchLike, getFetch } from '../shared.js';\n\n/** Invoice status as reported by Greenfield. */\nexport type BtcpayInvoiceStatus =\n | 'New'\n | 'Processing'\n | 'Expired'\n | 'Invalid'\n | 'Settled';\n\n/** Greenfield's `additionalStatus` qualifier on an invoice. */\nexport type BtcpayInvoiceAdditionalStatus =\n | 'None'\n | 'PaidLate'\n | 'PaidPartial'\n | 'Marked'\n | 'Invalid'\n | 'PaidOver';\n\n/**\n * Confirmations before BTCPay settles an on-chain payment: `HighSpeed` 0\n * (1 for RBF-signalling transactions), `MediumSpeed` 1, `LowMediumSpeed` 2,\n * `LowSpeed` 6. These are the only values BTCPay supports.\n */\nexport type BtcpaySpeedPolicy =\n | 'HighSpeed'\n | 'MediumSpeed'\n | 'LowMediumSpeed'\n | 'LowSpeed';\n\nexport interface BtcpayInvoiceCheckoutOptions {\n speedPolicy?: BtcpaySpeedPolicy;\n /** Payment method ids, for example `['BTC-CHAIN']` or `['BTC-CHAIN', 'BTC-LN']`. */\n paymentMethods?: string[];\n defaultPaymentMethod?: string;\n /** Minutes the rate is locked and the invoice accepts new payments. */\n expirationMinutes?: number;\n /** Minutes after expiry during which late payments are still tracked. */\n monitoringMinutes?: number;\n /** Percentage underpayment BTCPay treats as paid (0–100). */\n paymentTolerance?: number;\n redirectURL?: string;\n redirectAutomatically?: boolean;\n defaultLanguage?: string;\n}\n\nexport interface BtcpayCreateInvoiceInput {\n /** Decimal amount in `currency`, for example `'25.00'`. */\n amount: string;\n currency: string;\n /** Merchant order id; stored as `metadata.orderId` and filterable. */\n orderId?: string;\n metadata?: Record<string, unknown>;\n checkout?: BtcpayInvoiceCheckoutOptions;\n additionalSearchTerms?: string[];\n}\n\nexport interface BtcpayInvoice {\n id: string;\n storeId?: string;\n status: BtcpayInvoiceStatus | string;\n additionalStatus: BtcpayInvoiceAdditionalStatus | string;\n /** Decimal amount in `currency`. */\n amount: string;\n currency: string;\n checkoutLink?: string;\n createdTime?: Date;\n expirationTime?: Date;\n monitoringExpiration?: Date;\n archived: boolean;\n metadata: Record<string, unknown>;\n checkout: Record<string, unknown>;\n raw: Record<string, unknown>;\n}\n\nexport interface BtcpayListInvoicesInput {\n orderId?: string | string[];\n status?: BtcpayInvoiceStatus | BtcpayInvoiceStatus[];\n textSearch?: string;\n skip?: number;\n take?: number;\n}\n\nexport interface BtcpayInvoicePayment {\n /** On-chain payments use `<txid>-<vout>`. */\n id: string;\n /** The transaction id of an on-chain payment (parsed from `id`). */\n transactionId?: string;\n receivedDate?: Date;\n /** Decimal amount in the payment method's currency (BTC). */\n value: string;\n /** Decimal network/payment-method fee. */\n fee: string;\n status: 'Invalid' | 'Processing' | 'Settled' | string;\n destination?: string;\n}\n\nexport interface BtcpayInvoicePaymentMethod {\n /** `BTC-CHAIN`, `BTC-LN`, … (Greenfield 2.x `paymentMethodId`). */\n paymentMethodId: string;\n /** The payment method's currency, for example `BTC`. */\n currency?: string;\n destination?: string;\n paymentLink?: string;\n /** Invoice-currency price of one unit of the payment currency, if reported. */\n rate?: string;\n /** Decimal amount due in the payment currency when created. */\n amount: string;\n /** Decimal amount paid through this method. */\n paymentMethodPaid: string;\n /** Decimal amount paid through all methods, in this method's currency. */\n totalPaid: string;\n /**\n * Decimal amount still due, in this method's currency. Undefined when\n * BTCPay did not report it — never read a missing value as \"nothing due\".\n */\n due?: string;\n payments: BtcpayInvoicePayment[];\n raw: Record<string, unknown>;\n}\n\nexport interface BtcpayClientOptions {\n /** Server origin, for example `https://btcpay.example.com` (no path). */\n baseUrl: string;\n /** Greenfield API key (`Authorization: token …`). */\n apiKey: string;\n storeId: string;\n fetch?: FetchLike;\n /** Per-request timeout (default 30 seconds). */\n timeoutMs?: number;\n}\n\n/**\n * A Greenfield request failed. Never carries the API key.\n *\n * `retryable` is true only when repeating the same call cannot duplicate a\n * side effect: a read (`GET`) that failed on the network, with 429, or with\n * 5xx, or any request refused with 429. A `createInvoice` that failed on the\n * network or with 5xx may still have created the invoice, so it is not\n * retryable: resolve it with `listInvoices({ orderId })` instead.\n */\nexport class BtcpayApiError extends PaymentProviderError {\n readonly status: number;\n readonly apiCode?: string;\n\n constructor(\n message: string,\n status: number,\n apiCode?: string,\n options: { cause?: unknown; method?: 'GET' | 'POST' } = {},\n ) {\n const { method = 'GET', ...rest } = options;\n super(message, {\n ...rest,\n retryable:\n status === 429 || (method === 'GET' && (status === 0 || status >= 500)),\n });\n this.name = 'BtcpayApiError';\n this.status = status;\n this.apiCode = apiCode;\n }\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport class BtcpayClient {\n readonly baseUrl: string;\n readonly storeId: string;\n private readonly apiKey: string;\n private readonly fetch: FetchLike;\n private readonly timeoutMs: number;\n\n constructor(options: BtcpayClientOptions) {\n this.baseUrl = normalizeBaseUrl(options.baseUrl);\n this.apiKey = requireString(options.apiKey, 'BtcpayClient apiKey');\n this.storeId = requireString(options.storeId, 'BtcpayClient storeId');\n this.fetch = getFetch(options.fetch);\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new PaymentConfigurationError(\n 'BtcpayClient timeoutMs must be a positive number.',\n );\n }\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * `POST /api/v1/stores/{storeId}/invoices`. Not idempotent at BTCPay: after\n * a network or 5xx failure the invoice may exist, so the error is not\n * `retryable` — look it up with `listInvoices({ orderId })` before creating\n * another.\n */\n async createInvoice(input: BtcpayCreateInvoiceInput): Promise<BtcpayInvoice> {\n const amount = requirePositiveDecimal(\n input.amount,\n 'BTCPay invoice amount',\n );\n const currency = requireString(input.currency, 'BTCPay invoice currency');\n const metadata: Record<string, unknown> = { ...(input.metadata ?? {}) };\n if (input.orderId !== undefined) {\n metadata.orderId = requireString(input.orderId, 'BTCPay orderId');\n }\n const body: Record<string, unknown> = { amount, currency, metadata };\n if (input.checkout) body.checkout = compact({ ...input.checkout });\n if (input.additionalSearchTerms?.length) {\n body.additionalSearchTerms = input.additionalSearchTerms;\n }\n const raw = await this.request('POST', this.storePath('/invoices'), body);\n return toInvoice(raw);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}`. */\n async getInvoice(invoiceId: string): Promise<BtcpayInvoice> {\n const id = requireString(invoiceId, 'BTCPay invoiceId');\n const raw = await this.request(\n 'GET',\n this.storePath(`/invoices/${encodeURIComponent(id)}`),\n );\n return toInvoice(raw);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices` with Greenfield filters. */\n async listInvoices(\n input: BtcpayListInvoicesInput = {},\n ): Promise<BtcpayInvoice[]> {\n const query = new URLSearchParams();\n for (const orderId of toList(input.orderId)) {\n query.append('orderId', requireString(orderId, 'BTCPay orderId'));\n }\n for (const status of toList(input.status)) query.append('status', status);\n if (input.textSearch) query.set('textSearch', input.textSearch);\n if (input.skip !== undefined) {\n query.set('skip', String(requireCount(input.skip, 'skip')));\n }\n if (input.take !== undefined) {\n query.set('take', String(requireCount(input.take, 'take')));\n }\n const search = query.toString();\n const suffix = search ? `?${search}` : '';\n const raw = await this.request('GET', this.storePath(`/invoices${suffix}`));\n const items = Array.isArray(raw)\n ? raw\n : Array.isArray((raw as { items?: unknown })?.items)\n ? (raw as { items: unknown[] }).items\n : null;\n if (!items) {\n throw new BtcpayApiError(\n 'BTCPay invoice list response was not an array.',\n 200,\n );\n }\n return items.map(toInvoice);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods`. */\n async getInvoicePaymentMethods(\n invoiceId: string,\n ): Promise<BtcpayInvoicePaymentMethod[]> {\n const id = requireString(invoiceId, 'BTCPay invoiceId');\n const raw = await this.request(\n 'GET',\n this.storePath(`/invoices/${encodeURIComponent(id)}/payment-methods`),\n );\n if (!Array.isArray(raw)) {\n throw new BtcpayApiError(\n 'BTCPay payment-methods response was not an array.',\n 200,\n );\n }\n return raw.filter(isRecord).map(toPaymentMethod);\n }\n\n private storePath(suffix: string): string {\n return `/api/v1/stores/${encodeURIComponent(this.storeId)}${suffix}`;\n }\n\n private async request(\n method: 'GET' | 'POST',\n path: string,\n body?: unknown,\n ): Promise<unknown> {\n const headers = new Headers();\n headers.set('Authorization', `token ${this.apiKey}`);\n headers.set('Accept', 'application/json');\n if (body !== undefined) headers.set('Content-Type', 'application/json');\n let response: Response;\n try {\n response = await this.fetch(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (error) {\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed: ${errorName(error)}.`,\n 0,\n undefined,\n { cause: error, method },\n );\n }\n let text: string;\n try {\n text = await response.text();\n } catch (error) {\n // The server answered, so a POST may already have taken effect.\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed reading the response: ${errorName(error)}.`,\n 0,\n undefined,\n { cause: error, method },\n );\n }\n let parsed: unknown;\n if (text) {\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n if (response.ok) {\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} returned invalid JSON.`,\n response.status,\n undefined,\n { cause: error },\n );\n }\n }\n }\n if (!response.ok) {\n const { code, message } = readGreenfieldError(parsed);\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed with HTTP ${response.status}` +\n (message ? `: ${message}` : '.'),\n response.status,\n code,\n { method },\n );\n }\n return parsed;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Webhooks\n// ---------------------------------------------------------------------------\n\n/** A parsed BTCPay webhook delivery (signature must be verified first). */\nexport interface BtcpayWebhookDelivery {\n deliveryId: string;\n webhookId?: string;\n /** Set on a redelivery: the id of the delivery it repeats. */\n originalDeliveryId?: string;\n isRedelivery: boolean;\n /** `InvoiceSettled`, `InvoiceExpired`, … */\n type: string;\n timestamp?: Date;\n storeId?: string;\n invoiceId?: string;\n metadata?: Record<string, unknown>;\n raw: Record<string, unknown>;\n}\n\n/**\n * Check a `BTCPay-Sig` header (`sha256=<hex>`) against the raw request body\n * in constant time. Returns false for any missing or malformed input.\n */\nexport function isValidBtcpayWebhookSignature(\n rawBody: string,\n header: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof rawBody !== 'string' || typeof header !== 'string') return false;\n if (typeof secret !== 'string' || !secret) return false;\n const match = /^sha256=([a-f0-9]{64})$/i.exec(header.trim());\n if (!match) return false;\n const expected = createHmac('sha256', secret).update(rawBody).digest();\n const actual = Buffer.from(match[1], 'hex');\n return actual.length === expected.length && timingSafeEqual(actual, expected);\n}\n\n/** Parse a webhook body. Throws `PaymentProviderError` when it is malformed. */\nexport function parseBtcpayWebhook(rawBody: string): BtcpayWebhookDelivery {\n let raw: unknown;\n try {\n raw = JSON.parse(rawBody);\n } catch (error) {\n throw new PaymentProviderError('BTCPay webhook body is not JSON.', {\n cause: error,\n });\n }\n if (!isRecord(raw)) {\n throw new PaymentProviderError('BTCPay webhook body is not an object.');\n }\n const deliveryId = readString(raw, 'deliveryId');\n const type = readString(raw, 'type');\n if (!deliveryId || !type) {\n throw new PaymentProviderError(\n 'BTCPay webhook body needs deliveryId and type.',\n );\n }\n return {\n deliveryId,\n webhookId: readString(raw, 'webhookId'),\n originalDeliveryId: readString(raw, 'originalDeliveryId'),\n isRedelivery: raw.isRedelivery === true,\n type,\n timestamp: readDate(raw.timestamp),\n storeId: readString(raw, 'storeId'),\n invoiceId: readString(raw, 'invoiceId'),\n metadata: isRecord(raw.metadata) ? raw.metadata : undefined,\n raw,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction toInvoice(value: unknown): BtcpayInvoice {\n if (!isRecord(value)) {\n throw new BtcpayApiError('BTCPay invoice response was not an object.', 200);\n }\n const id = readString(value, 'id');\n if (!id) {\n throw new BtcpayApiError('BTCPay invoice response has no id.', 200);\n }\n return {\n id,\n storeId: readString(value, 'storeId'),\n status: readString(value, 'status') ?? 'New',\n additionalStatus: readString(value, 'additionalStatus') ?? 'None',\n amount: readDecimal(value.amount) ?? '0',\n currency: readString(value, 'currency') ?? '',\n checkoutLink: readString(value, 'checkoutLink'),\n createdTime: readDate(value.createdTime),\n expirationTime: readDate(value.expirationTime),\n monitoringExpiration: readDate(value.monitoringExpiration),\n archived: value.archived === true,\n metadata: isRecord(value.metadata) ? value.metadata : {},\n checkout: isRecord(value.checkout) ? value.checkout : {},\n raw: value,\n };\n}\n\nfunction toPaymentMethod(\n value: Record<string, unknown>,\n): BtcpayInvoicePaymentMethod {\n // Greenfield 1.x used `paymentMethod` / `cryptoCode`; 2.x uses\n // `paymentMethodId` / `currency`.\n const paymentMethodId =\n readString(value, 'paymentMethodId') ??\n readString(value, 'paymentMethod') ??\n '';\n return {\n paymentMethodId,\n currency: readString(value, 'currency') ?? readString(value, 'cryptoCode'),\n destination: readString(value, 'destination'),\n paymentLink: readString(value, 'paymentLink'),\n rate: readDecimal(value.rate),\n amount: readDecimal(value.amount) ?? '0',\n paymentMethodPaid: readDecimal(value.paymentMethodPaid) ?? '0',\n totalPaid: readDecimal(value.totalPaid) ?? '0',\n due: readDecimal(value.due),\n payments: Array.isArray(value.payments)\n ? value.payments.filter(isRecord).map((payment) => ({\n id: readString(payment, 'id') ?? '',\n transactionId: onChainTransactionId(readString(payment, 'id')),\n receivedDate: readDate(payment.receivedDate),\n value: readDecimal(payment.value) ?? '0',\n fee: readDecimal(payment.fee) ?? '0',\n status: readString(payment, 'status') ?? '',\n destination: readString(payment, 'destination'),\n }))\n : [],\n raw: value,\n };\n}\n\nfunction readGreenfieldError(body: unknown): {\n code?: string;\n message?: string;\n} {\n if (Array.isArray(body)) {\n // Validation errors: [{ path, message }]\n const messages = body\n .filter(isRecord)\n .map((item) =>\n [readString(item, 'path'), readString(item, 'message')]\n .filter(Boolean)\n .join(': '),\n )\n .filter(Boolean);\n return { code: 'validation-error', message: messages.join('; ') };\n }\n if (isRecord(body)) {\n return {\n code: readString(body, 'code'),\n message: readString(body, 'message'),\n };\n }\n return {};\n}\n\n/** Greenfield paths carry only store/invoice ids; strip the query string. */\nfunction redactPath(path: string): string {\n return path.split('?')[0];\n}\n\nfunction errorName(error: unknown): string {\n if (error instanceof Error) {\n return error.name === 'TimeoutError' ? 'timed out' : error.name;\n }\n return 'network error';\n}\n\nfunction normalizeBaseUrl(value: string): string {\n const raw = requireString(value, 'BtcpayClient baseUrl');\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch (error) {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must be an absolute URL.',\n { cause: error },\n );\n }\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must use http or https.',\n );\n }\n if (\n parsed.search ||\n parsed.hash ||\n parsed.username ||\n parsed.password ||\n !['', '/'].includes(parsed.pathname)\n ) {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must be an origin without path, query, fragment, or credentials.',\n );\n }\n return parsed.origin;\n}\n\nfunction requireString(value: unknown, label: string): string {\n if (typeof value !== 'string' || !value.trim()) {\n throw new PaymentConfigurationError(`${label} is required.`);\n }\n return value.trim();\n}\n\nfunction requirePositiveDecimal(value: unknown, label: string): string {\n if (\n typeof value !== 'string' ||\n !/^\\d+(\\.\\d+)?$/.test(value.trim()) ||\n !/[1-9]/.test(value)\n ) {\n throw new PaymentConfigurationError(\n `${label} must be a positive decimal string.`,\n );\n }\n return value.trim();\n}\n\nfunction requireCount(value: number, label: string): number {\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new PaymentConfigurationError(\n `BTCPay ${label} must be a non-negative integer.`,\n );\n }\n return value;\n}\n\nfunction toList<T>(value: T | T[] | undefined): T[] {\n if (value === undefined) return [];\n return Array.isArray(value) ? value : [value];\n}\n\nfunction compact<T extends Record<string, unknown>>(value: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(value).filter(([, item]) => item !== undefined),\n ) as Partial<T>;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction readString(\n record: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = record[key];\n if (typeof value === 'string' && value.trim()) return value.trim();\n return undefined;\n}\n\n/** Keep decimals as strings; accept numbers Greenfield may emit. */\nfunction readDecimal(value: unknown): string | undefined {\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number' && Number.isFinite(value)) {\n const text = String(value);\n return /e/i.test(text)\n ? value.toFixed(20).replace(/0+$/, '').replace(/\\.$/, '')\n : text;\n }\n return undefined;\n}\n\nfunction onChainTransactionId(id: string | undefined): string | undefined {\n const match = id ? /^([0-9a-f]{64})-\\d+$/i.exec(id) : null;\n return match ? match[1].toLowerCase() : undefined;\n}\n\n/** Greenfield timestamps are unix seconds; ISO strings are accepted too. */\nfunction readDate(value: unknown): Date | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value * 1000);\n }\n if (typeof value === 'string' && value.trim()) {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? undefined : date;\n }\n return undefined;\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * The default entrypoint exports only provider contracts, shared errors, and
5
5
  * the dynamic factory. Adapter implementations live behind subpath exports:
6
- * `@happyvertical/payments/base-usdc`, `@happyvertical/payments/btc`, and
6
+ * `@happyvertical/payments/base-usdc`, `@happyvertical/payments/btc`,
7
+ * `@happyvertical/payments/btcpay` (BTCPay Greenfield client), and
7
8
  * `@happyvertical/payments/stripe`.
8
9
  */
9
10
  export * from './errors.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAE3B,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAE3B,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/factory.ts","../src/index.ts"],"sourcesContent":["import type { BaseUsdcAdapterOptions } from './adapters/base-usdc.js';\nimport type { BtcAdapterOptions } from './adapters/btc.js';\nimport type { StripeAdapterOptions } from './adapters/stripe.js';\nimport { PaymentConfigurationError } from './errors.js';\nimport type { PaymentBackend } from './types.js';\n\nexport type PaymentBackendOptions =\n | ({ type: 'base-usdc' } & BaseUsdcAdapterOptions)\n | ({ type: 'btc' } & BtcAdapterOptions)\n | ({ type: 'stripe' } & StripeAdapterOptions);\n\nexport async function createPaymentBackend(\n options: PaymentBackendOptions,\n): Promise<PaymentBackend> {\n if (!options || typeof options !== 'object' || Array.isArray(options)) {\n throw new PaymentConfigurationError(\n 'Payment backend options must be an object.',\n );\n }\n\n const type = (options as { type?: unknown }).type;\n\n if (typeof type !== 'string' || !type.trim()) {\n throw new PaymentConfigurationError(\n 'Payment backend type must be a string.',\n );\n }\n\n const normalizedType = type.trim();\n const backendOptions = {\n ...options,\n type: normalizedType,\n } as PaymentBackendOptions;\n\n switch (backendOptions.type) {\n case 'base-usdc': {\n const { BaseUsdcAdapter } = await import('./adapters/base-usdc.js');\n return new BaseUsdcAdapter(backendOptions);\n }\n case 'btc': {\n const { BtcAdapter } = await import('./adapters/btc.js');\n return new BtcAdapter(backendOptions);\n }\n case 'stripe': {\n const { StripeAdapter } = await import('./adapters/stripe.js');\n return new StripeAdapter(backendOptions);\n }\n default:\n throw new PaymentConfigurationError(\n `Unknown payment backend type: ${normalizedType}`,\n );\n }\n}\n","/**\n * @happyvertical/payments - Multi-backend payment provider abstraction.\n *\n * The default entrypoint exports only provider contracts, shared errors, and\n * the dynamic factory. Adapter implementations live behind subpath exports:\n * `@happyvertical/payments/base-usdc`, `@happyvertical/payments/btc`, and\n * `@happyvertical/payments/stripe`.\n */\n\nexport * from './errors.js';\nexport * from './factory.js';\nexport * from './types.js';\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":";;AAWA,eAAsB,qBACpB,SACyB;CACzB,IAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,GAClE,MAAM,IAAI,0BACR,4CACF;CAGF,MAAM,OAAQ,QAA+B;CAE7C,IAAI,OAAO,SAAS,YAAY,CAAC,KAAK,KAAK,GACzC,MAAM,IAAI,0BACR,wCACF;CAGF,MAAM,iBAAiB,KAAK,KAAK;CACjC,MAAM,iBAAiB;EACrB,GAAG;EACH,MAAM;CACR;CAEA,QAAQ,eAAe,MAAvB;EACE,KAAK,aAAa;GAChB,MAAM,EAAE,oBAAoB,MAAM,OAAO;GACzC,OAAO,IAAI,gBAAgB,cAAc;EAC3C;EACA,KAAK,OAAO;GACV,MAAM,EAAE,eAAe,MAAM,OAAO;GACpC,OAAO,IAAI,WAAW,cAAc;EACtC;EACA,KAAK,UAAU;GACb,MAAM,EAAE,kBAAkB,MAAM,OAAO;GACvC,OAAO,IAAI,cAAc,cAAc;EACzC;EACA,SACE,MAAM,IAAI,0BACR,iCAAiC,gBACnC;CACJ;AACF;;;;ACtCA,IAAa,8BAA8B"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/factory.ts","../src/index.ts"],"sourcesContent":["import type { BaseUsdcAdapterOptions } from './adapters/base-usdc.js';\nimport type { BtcAdapterOptions } from './adapters/btc.js';\nimport type { StripeAdapterOptions } from './adapters/stripe.js';\nimport { PaymentConfigurationError } from './errors.js';\nimport type { PaymentBackend } from './types.js';\n\nexport type PaymentBackendOptions =\n | ({ type: 'base-usdc' } & BaseUsdcAdapterOptions)\n | ({ type: 'btc' } & BtcAdapterOptions)\n | ({ type: 'stripe' } & StripeAdapterOptions);\n\nexport async function createPaymentBackend(\n options: PaymentBackendOptions,\n): Promise<PaymentBackend> {\n if (!options || typeof options !== 'object' || Array.isArray(options)) {\n throw new PaymentConfigurationError(\n 'Payment backend options must be an object.',\n );\n }\n\n const type = (options as { type?: unknown }).type;\n\n if (typeof type !== 'string' || !type.trim()) {\n throw new PaymentConfigurationError(\n 'Payment backend type must be a string.',\n );\n }\n\n const normalizedType = type.trim();\n const backendOptions = {\n ...options,\n type: normalizedType,\n } as PaymentBackendOptions;\n\n switch (backendOptions.type) {\n case 'base-usdc': {\n const { BaseUsdcAdapter } = await import('./adapters/base-usdc.js');\n return new BaseUsdcAdapter(backendOptions);\n }\n case 'btc': {\n const { BtcAdapter } = await import('./adapters/btc.js');\n return new BtcAdapter(backendOptions);\n }\n case 'stripe': {\n const { StripeAdapter } = await import('./adapters/stripe.js');\n return new StripeAdapter(backendOptions);\n }\n default:\n throw new PaymentConfigurationError(\n `Unknown payment backend type: ${normalizedType}`,\n );\n }\n}\n","/**\n * @happyvertical/payments - Multi-backend payment provider abstraction.\n *\n * The default entrypoint exports only provider contracts, shared errors, and\n * the dynamic factory. Adapter implementations live behind subpath exports:\n * `@happyvertical/payments/base-usdc`, `@happyvertical/payments/btc`,\n * `@happyvertical/payments/btcpay` (BTCPay Greenfield client), and\n * `@happyvertical/payments/stripe`.\n */\n\nexport * from './errors.js';\nexport * from './factory.js';\nexport * from './types.js';\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":";;AAWA,eAAsB,qBACpB,SACyB;CACzB,IAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,GAClE,MAAM,IAAI,0BACR,4CACF;CAGF,MAAM,OAAQ,QAA+B;CAE7C,IAAI,OAAO,SAAS,YAAY,CAAC,KAAK,KAAK,GACzC,MAAM,IAAI,0BACR,wCACF;CAGF,MAAM,iBAAiB,KAAK,KAAK;CACjC,MAAM,iBAAiB;EACrB,GAAG;EACH,MAAM;CACR;CAEA,QAAQ,eAAe,MAAvB;EACE,KAAK,aAAa;GAChB,MAAM,EAAE,oBAAoB,MAAM,OAAO;GACzC,OAAO,IAAI,gBAAgB,cAAc;EAC3C;EACA,KAAK,OAAO;GACV,MAAM,EAAE,eAAe,MAAM,OAAO;GACpC,OAAO,IAAI,WAAW,cAAc;EACtC;EACA,KAAK,UAAU;GACb,MAAM,EAAE,kBAAkB,MAAM,OAAO;GACvC,OAAO,IAAI,cAAc,cAAc;EACzC;EACA,SACE,MAAM,IAAI,0BACR,iCAAiC,gBACnC;CACJ;AACF;;;;ACrCA,IAAa,8BAA8B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/payments",
3
- "version": "0.90.0",
3
+ "version": "0.92.0",
4
4
  "description": "Payment backend abstraction with adapters for Base USDC, BTCPay Server, and Stripe",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,6 +18,10 @@
18
18
  "types": "./dist/adapters/btc.d.ts",
19
19
  "import": "./dist/adapters/btc.js"
20
20
  },
21
+ "./btcpay": {
22
+ "types": "./dist/adapters/btcpay.d.ts",
23
+ "import": "./dist/adapters/btcpay.js"
24
+ },
21
25
  "./stripe": {
22
26
  "types": "./dist/adapters/stripe.d.ts",
23
27
  "import": "./dist/adapters/stripe.js"