@happyvertical/payments 0.90.0 → 0.91.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 +57 -0
- package/dist/adapters/btcpay.d.ts +170 -0
- package/dist/adapters/btcpay.d.ts.map +1 -0
- package/dist/adapters/btcpay.js +308 -0
- package/dist/adapters/btcpay.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
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":"AAYA,OAAO,EAA6B,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,cAAc,CAAC;AAExD,gDAAgD;AAChD,MAAM,MAAM,mBAAmB,GAC3B,KAAK,GACL,YAAY,GACZ,SAAS,GACT,SAAS,GACT,SAAS,CAAC;AAEd,+DAA+D;AAC/D,MAAM,MAAM,6BAA6B,GACrC,MAAM,GACN,UAAU,GACV,aAAa,GACb,QAAQ,GACR,SAAS,GACT,UAAU,CAAC;AAEf;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,aAAa,GACb,gBAAgB,GAChB,UAAU,CAAC;AAEf,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,oFAAoF;IACpF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,QAAQ,CAAC,EAAE,4BAA4B,CAAC;IACxC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAAC;IACrC,gBAAgB,EAAE,6BAA6B,GAAG,MAAM,CAAC;IACzD,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,cAAc,CAAC,EAAE,IAAI,CAAC;IACtB,oBAAoB,CAAC,EAAE,IAAI,CAAC;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAC;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,IAAI,CAAC;IACpB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,GAAG,MAAM,CAAC;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,mEAAmE;IACnE,eAAe,EAAE,MAAM,CAAC;IACxB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,qBAAa,cAAe,SAAQ,oBAAoB;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;gBAGxB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;KAAO;CAY7D;AAID,qBAAa,YAAY;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,OAAO,EAAE,mBAAmB;IAcxC;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,CAAC;IAmB5E,2DAA2D;IACrD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAS3D,uEAAuE;IACjE,YAAY,CAChB,KAAK,GAAE,uBAA4B,GAClC,OAAO,CAAC,aAAa,EAAE,CAAC;IA8B3B,2EAA2E;IACrE,wBAAwB,CAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,0BAA0B,EAAE,CAAC;IAexC,OAAO,CAAC,SAAS;YAIH,OAAO;CAgEtB;AAMD,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,MAAM,EAAE,MAAM,GACb,OAAO,CAQT;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CA+BzE"}
|
|
@@ -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`,
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.91.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"
|