@billkit-eu/sdk 0.1.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/CHANGELOG.md +71 -0
- package/LICENSE +201 -0
- package/README.md +299 -0
- package/dist/index.cjs +996 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +911 -0
- package/dist/index.d.ts +911 -0
- package/dist/index.js +977 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
- package/src/client.ts +88 -0
- package/src/errors.ts +168 -0
- package/src/index.ts +103 -0
- package/src/logging.ts +72 -0
- package/src/pagination.ts +77 -0
- package/src/resources.ts +993 -0
- package/src/retry.ts +61 -0
- package/src/transport.ts +300 -0
- package/src/version.ts +1 -0
- package/src/webhooks.ts +171 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/pagination.ts
|
|
4
|
+
async function* paginate(listFn, options = {}) {
|
|
5
|
+
const { pageSize, filters } = options;
|
|
6
|
+
const cleanFilters = {};
|
|
7
|
+
if (filters) {
|
|
8
|
+
for (const [k, v] of Object.entries(filters)) {
|
|
9
|
+
if (v !== void 0) cleanFilters[k] = v;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
let cursor;
|
|
13
|
+
for (; ; ) {
|
|
14
|
+
const page = await listFn({
|
|
15
|
+
...cleanFilters,
|
|
16
|
+
limit: pageSize,
|
|
17
|
+
starting_after: cursor
|
|
18
|
+
});
|
|
19
|
+
const items = page.data ?? [];
|
|
20
|
+
for (const item of items) yield item;
|
|
21
|
+
if (!page.has_more || items.length === 0) return;
|
|
22
|
+
const last = items[items.length - 1];
|
|
23
|
+
cursor = last?.id;
|
|
24
|
+
if (cursor === void 0) return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/resources.ts
|
|
29
|
+
function dropUndefined(obj) {
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
32
|
+
if (v !== void 0) out[k] = v;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function splitIdempotency(params) {
|
|
37
|
+
const { idempotencyKey, ...rest } = params;
|
|
38
|
+
return { body: dropUndefined(rest), idempotencyKey };
|
|
39
|
+
}
|
|
40
|
+
var BaseResource = class {
|
|
41
|
+
constructor(t) {
|
|
42
|
+
this.t = t;
|
|
43
|
+
}
|
|
44
|
+
t;
|
|
45
|
+
get(path, query) {
|
|
46
|
+
return this.t.request({ method: "GET", path, query });
|
|
47
|
+
}
|
|
48
|
+
post(path, params) {
|
|
49
|
+
const { body, idempotencyKey } = splitIdempotency(params);
|
|
50
|
+
return this.t.request({ method: "POST", path, body, idempotencyKey });
|
|
51
|
+
}
|
|
52
|
+
/** POST with no body, used by lifecycle verbs (cancel, resume, revoke ...). */
|
|
53
|
+
postEmpty(path, params = {}) {
|
|
54
|
+
return this.t.request({
|
|
55
|
+
method: "POST",
|
|
56
|
+
path,
|
|
57
|
+
idempotencyKey: params.idempotencyKey
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** POST with a fixed body and no idempotency stripping (used by
|
|
61
|
+
* endpoints whose body is fully specified by the caller's args
|
|
62
|
+
* and not optional, e.g. `preview_update`). */
|
|
63
|
+
postFixed(path, body, params = {}) {
|
|
64
|
+
return this.t.request({
|
|
65
|
+
method: "POST",
|
|
66
|
+
path,
|
|
67
|
+
body,
|
|
68
|
+
idempotencyKey: params.idempotencyKey
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
del(path, params = {}) {
|
|
72
|
+
return this.t.request({
|
|
73
|
+
method: "DELETE",
|
|
74
|
+
path,
|
|
75
|
+
idempotencyKey: params.idempotencyKey
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var Customers = class extends BaseResource {
|
|
80
|
+
/** Create a tenant-scoped buyer record. */
|
|
81
|
+
create(params = {}) {
|
|
82
|
+
return this.post("/v1/customers", params);
|
|
83
|
+
}
|
|
84
|
+
retrieve(id) {
|
|
85
|
+
return this.get(`/v1/customers/${id}`);
|
|
86
|
+
}
|
|
87
|
+
update(id, params = {}) {
|
|
88
|
+
return this.post(`/v1/customers/${id}`, params);
|
|
89
|
+
}
|
|
90
|
+
delete(id, params = {}) {
|
|
91
|
+
return this.del(`/v1/customers/${id}`, params);
|
|
92
|
+
}
|
|
93
|
+
list(params = {}) {
|
|
94
|
+
return this.get("/v1/customers", params);
|
|
95
|
+
}
|
|
96
|
+
/** Walk every page of `list()` and yield each customer. */
|
|
97
|
+
iter(options = {}) {
|
|
98
|
+
return paginate((p) => this.get("/v1/customers", p), { pageSize: options.pageSize });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Attach or replace the customer's VAT number; triggers server-side
|
|
102
|
+
* VIES validation. The response carries `vat_number_validated`
|
|
103
|
+
* reflecting whether VIES confirmed the number.
|
|
104
|
+
*/
|
|
105
|
+
setVatNumber(id, params) {
|
|
106
|
+
return this.post(`/v1/customers/${id}/vat_number`, params);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Hard-purge a customer's PII for GDPR erasure. Distinct from
|
|
110
|
+
* `delete()` (soft delete): purge nulls email/name/country/VAT/
|
|
111
|
+
* metadata, sets `purged_at`, and is irreversible.
|
|
112
|
+
*
|
|
113
|
+
* The server requires `confirmed: true` as a fat-finger guard; the
|
|
114
|
+
* SDK defaults it to `true` so callers don't have to opt in twice.
|
|
115
|
+
*/
|
|
116
|
+
purge(id, params = {}) {
|
|
117
|
+
const { confirmed = true, idempotencyKey } = params;
|
|
118
|
+
return this.postFixed(`/v1/customers/${id}/purge`, { confirmed }, { idempotencyKey });
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var Products = class extends BaseResource {
|
|
122
|
+
/** Create a catalog Product, then attach one or more Prices to it. */
|
|
123
|
+
create(params) {
|
|
124
|
+
return this.post("/v1/products", params);
|
|
125
|
+
}
|
|
126
|
+
retrieve(id) {
|
|
127
|
+
return this.get(`/v1/products/${id}`);
|
|
128
|
+
}
|
|
129
|
+
/** Patch mutable Product fields. */
|
|
130
|
+
update(id, params) {
|
|
131
|
+
return this.post(`/v1/products/${id}`, params);
|
|
132
|
+
}
|
|
133
|
+
/** Archive a Product. */
|
|
134
|
+
delete(id, params = {}) {
|
|
135
|
+
return this.del(`/v1/products/${id}`, params);
|
|
136
|
+
}
|
|
137
|
+
list(params = {}) {
|
|
138
|
+
return this.get("/v1/products", params);
|
|
139
|
+
}
|
|
140
|
+
iter(options = {}) {
|
|
141
|
+
return paginate((p) => this.get("/v1/products", p), { pageSize: options.pageSize });
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var Prices = class extends BaseResource {
|
|
145
|
+
/** Create immutable billing terms for an existing Product. */
|
|
146
|
+
create(params) {
|
|
147
|
+
return this.post("/v1/prices", params);
|
|
148
|
+
}
|
|
149
|
+
retrieve(id) {
|
|
150
|
+
return this.get(`/v1/prices/${id}`);
|
|
151
|
+
}
|
|
152
|
+
list(params = {}) {
|
|
153
|
+
return this.get("/v1/prices", params);
|
|
154
|
+
}
|
|
155
|
+
iter(options = {}) {
|
|
156
|
+
const filter = options.product_id === void 0 ? {} : { product_id: options.product_id };
|
|
157
|
+
return paginate((p) => this.get("/v1/prices", { ...filter, ...p }), {
|
|
158
|
+
pageSize: options.pageSize
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var CheckoutSessions = class extends BaseResource {
|
|
163
|
+
create(params) {
|
|
164
|
+
return this.post("/v1/checkout/sessions", params);
|
|
165
|
+
}
|
|
166
|
+
retrieve(id) {
|
|
167
|
+
return this.get(`/v1/checkout/sessions/${id}`);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var OneShotPayments = class extends BaseResource {
|
|
171
|
+
/** Create a one-off charge; returns the object with a `redirect_url`. */
|
|
172
|
+
create(params) {
|
|
173
|
+
return this.post("/v1/checkout/one_shot", params);
|
|
174
|
+
}
|
|
175
|
+
retrieve(id) {
|
|
176
|
+
return this.get(`/v1/checkout/one_shot/${id}`);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var Subscriptions = class extends BaseResource {
|
|
180
|
+
retrieve(id) {
|
|
181
|
+
return this.get(`/v1/subscriptions/${id}`);
|
|
182
|
+
}
|
|
183
|
+
list(params = {}) {
|
|
184
|
+
return this.get("/v1/subscriptions", params);
|
|
185
|
+
}
|
|
186
|
+
iter(options = {}) {
|
|
187
|
+
return paginate((p) => this.get("/v1/subscriptions", p), { pageSize: options.pageSize });
|
|
188
|
+
}
|
|
189
|
+
cancel(id, params = {}) {
|
|
190
|
+
return this.postEmpty(`/v1/subscriptions/${id}/cancel`, params);
|
|
191
|
+
}
|
|
192
|
+
pause(id, params = {}) {
|
|
193
|
+
return this.postEmpty(`/v1/subscriptions/${id}/pause`, params);
|
|
194
|
+
}
|
|
195
|
+
resume(id, params = {}) {
|
|
196
|
+
return this.postEmpty(`/v1/subscriptions/${id}/resume`, params);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Reactivate a canceled-but-still-in-period subscription.
|
|
200
|
+
*
|
|
201
|
+
* Distinct from `resume()` (paused → active): `reactivate()` flips
|
|
202
|
+
* `canceled` back to `active` for the remainder of the current
|
|
203
|
+
* period, so the customer keeps service without a new checkout.
|
|
204
|
+
* Returns `409` if the period has already elapsed.
|
|
205
|
+
*/
|
|
206
|
+
reactivate(id, params = {}) {
|
|
207
|
+
return this.postEmpty(`/v1/subscriptions/${id}/reactivate`, params);
|
|
208
|
+
}
|
|
209
|
+
previewUpdate(id, params) {
|
|
210
|
+
return this.postFixed(`/v1/subscriptions/${id}/preview_update`, {
|
|
211
|
+
target_price_id: params.target_price_id
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
update(id, params) {
|
|
215
|
+
return this.postFixed(
|
|
216
|
+
`/v1/subscriptions/${id}/update`,
|
|
217
|
+
{ target_price_id: params.target_price_id },
|
|
218
|
+
{ idempotencyKey: params.idempotencyKey }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
reauthorizePaymentMethod(id, params) {
|
|
222
|
+
return this.postFixed(
|
|
223
|
+
`/v1/subscriptions/${id}/reauthorize_payment_method`,
|
|
224
|
+
{ return_url: params.return_url },
|
|
225
|
+
{ idempotencyKey: params.idempotencyKey }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var Refunds = class extends BaseResource {
|
|
230
|
+
create(params) {
|
|
231
|
+
return this.post("/v1/refunds", params);
|
|
232
|
+
}
|
|
233
|
+
retrieve(id) {
|
|
234
|
+
return this.get(`/v1/refunds/${id}`);
|
|
235
|
+
}
|
|
236
|
+
list(params = {}) {
|
|
237
|
+
return this.get("/v1/refunds", params);
|
|
238
|
+
}
|
|
239
|
+
iter(options = {}) {
|
|
240
|
+
return paginate((p) => this.get("/v1/refunds", p), { pageSize: options.pageSize });
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
var Disputes = class extends BaseResource {
|
|
244
|
+
retrieve(id) {
|
|
245
|
+
return this.get(`/v1/disputes/${id}`);
|
|
246
|
+
}
|
|
247
|
+
list(params = {}) {
|
|
248
|
+
return this.get("/v1/disputes", params);
|
|
249
|
+
}
|
|
250
|
+
iter(options = {}) {
|
|
251
|
+
return paginate((p) => this.get("/v1/disputes", p), { pageSize: options.pageSize });
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var WebhookEndpoints = class extends BaseResource {
|
|
255
|
+
create(params) {
|
|
256
|
+
return this.post("/v1/webhook_endpoints", params);
|
|
257
|
+
}
|
|
258
|
+
retrieve(id) {
|
|
259
|
+
return this.get(`/v1/webhook_endpoints/${id}`);
|
|
260
|
+
}
|
|
261
|
+
update(id, params) {
|
|
262
|
+
return this.post(`/v1/webhook_endpoints/${id}`, params);
|
|
263
|
+
}
|
|
264
|
+
delete(id, params = {}) {
|
|
265
|
+
return this.del(`/v1/webhook_endpoints/${id}`, params);
|
|
266
|
+
}
|
|
267
|
+
/** Rotate the signing secret. The new `whsec_...` is returned once. */
|
|
268
|
+
rotateSecret(id, params = {}) {
|
|
269
|
+
return this.postEmpty(`/v1/webhook_endpoints/${id}/rotate_secret`, params);
|
|
270
|
+
}
|
|
271
|
+
list(params = {}) {
|
|
272
|
+
return this.get("/v1/webhook_endpoints", params);
|
|
273
|
+
}
|
|
274
|
+
iter(options = {}) {
|
|
275
|
+
return paginate((p) => this.get("/v1/webhook_endpoints", p), {
|
|
276
|
+
pageSize: options.pageSize
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* List per-attempt delivery records for one endpoint.
|
|
281
|
+
*
|
|
282
|
+
* Useful when a tenant's receiver is failing. Surfaces the status
|
|
283
|
+
* code, response body excerpt, error, and next-attempt timestamp
|
|
284
|
+
* for each event × endpoint pair.
|
|
285
|
+
*/
|
|
286
|
+
listDeliveries(endpointId, params = {}) {
|
|
287
|
+
return this.get(
|
|
288
|
+
`/v1/webhook_endpoints/${endpointId}/deliveries`,
|
|
289
|
+
params
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
/** Walk every page of `listDeliveries()` for one endpoint. */
|
|
293
|
+
iterDeliveries(endpointId, options = {}) {
|
|
294
|
+
return paginate(
|
|
295
|
+
(p) => this.get(`/v1/webhook_endpoints/${endpointId}/deliveries`, p),
|
|
296
|
+
{ pageSize: options.pageSize }
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
/** Fetch one delivery row for inspection before deciding to redeliver. */
|
|
300
|
+
getDelivery(endpointId, deliveryId) {
|
|
301
|
+
return this.get(`/v1/webhook_endpoints/${endpointId}/deliveries/${deliveryId}`);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Re-enqueue a delivery row for the dispatcher.
|
|
305
|
+
*
|
|
306
|
+
* Idempotent: a row already in `delivered` returns unchanged. A
|
|
307
|
+
* `pending` / `failed` row flips to `pending` with
|
|
308
|
+
* `next_attempt_at = now()`; `attempt_count` is preserved.
|
|
309
|
+
*/
|
|
310
|
+
redeliver(endpointId, deliveryId, params = {}) {
|
|
311
|
+
return this.postEmpty(
|
|
312
|
+
`/v1/webhook_endpoints/${endpointId}/deliveries/${deliveryId}/redeliver`,
|
|
313
|
+
params
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var Events = class extends BaseResource {
|
|
318
|
+
retrieve(id) {
|
|
319
|
+
return this.get(`/v1/events/${id}`);
|
|
320
|
+
}
|
|
321
|
+
list(params = {}) {
|
|
322
|
+
return this.get("/v1/events", params);
|
|
323
|
+
}
|
|
324
|
+
/** Walk every page of `list()`. Pass `type` to filter at the server. */
|
|
325
|
+
iter(options = {}) {
|
|
326
|
+
return paginate((p) => this.get("/v1/events", p), {
|
|
327
|
+
pageSize: options.pageSize,
|
|
328
|
+
filters: { type: options.type }
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
var Tenant = class extends BaseResource {
|
|
333
|
+
/** Cached Mollie profile shape (enabled methods, country, currency). */
|
|
334
|
+
capabilities() {
|
|
335
|
+
return this.get("/v1/tenant/capabilities");
|
|
336
|
+
}
|
|
337
|
+
/** Current portal branding row (business name, theme, capability flags). */
|
|
338
|
+
portalBranding() {
|
|
339
|
+
return this.get("/v1/tenant/portal_branding");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Partial-update the portal branding row.
|
|
343
|
+
*
|
|
344
|
+
* Only fields you set are sent. Pass `undefined` to leave a field
|
|
345
|
+
* untouched; sending an empty string explicitly clears it.
|
|
346
|
+
*/
|
|
347
|
+
setPortalBranding(params = {}) {
|
|
348
|
+
return this.post("/v1/tenant/portal_branding", params);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Rotate the encrypted provider credential for this tenant.
|
|
352
|
+
*
|
|
353
|
+
* The new `api_key` is encrypted server-side; nothing is logged.
|
|
354
|
+
* `mode` defaults to the calling key's mode; prefix-mismatch
|
|
355
|
+
* (`test_...` under live, `live_...` under test) is rejected at the
|
|
356
|
+
* API boundary.
|
|
357
|
+
*/
|
|
358
|
+
rotateProviderCredential(params) {
|
|
359
|
+
return this.post(
|
|
360
|
+
"/v1/tenant/provider_credential",
|
|
361
|
+
params
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
var Coupons = class extends BaseResource {
|
|
366
|
+
create(params) {
|
|
367
|
+
return this.post("/v1/coupons", params);
|
|
368
|
+
}
|
|
369
|
+
retrieve(id) {
|
|
370
|
+
return this.get(`/v1/coupons/${id}`);
|
|
371
|
+
}
|
|
372
|
+
update(id, params) {
|
|
373
|
+
return this.post(`/v1/coupons/${id}`, params);
|
|
374
|
+
}
|
|
375
|
+
delete(id, params = {}) {
|
|
376
|
+
return this.del(`/v1/coupons/${id}`, params);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Server-side dry-run of a coupon redemption.
|
|
380
|
+
*
|
|
381
|
+
* Returns the discount math without atomically claiming the coupon,
|
|
382
|
+
* which is useful for "preview before checkout" UX.
|
|
383
|
+
*/
|
|
384
|
+
validate(params) {
|
|
385
|
+
const body = { code: params.code };
|
|
386
|
+
if (params.price_id !== void 0) body["price_id"] = params.price_id;
|
|
387
|
+
if (params.amount_cents !== void 0) body["amount_cents"] = params.amount_cents;
|
|
388
|
+
return this.postFixed("/v1/coupons/validate", body);
|
|
389
|
+
}
|
|
390
|
+
list(params = {}) {
|
|
391
|
+
return this.get("/v1/coupons", params);
|
|
392
|
+
}
|
|
393
|
+
iter(options = {}) {
|
|
394
|
+
return paginate((p) => this.get("/v1/coupons", p), { pageSize: options.pageSize });
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
var TaxRates = class extends BaseResource {
|
|
398
|
+
create(params) {
|
|
399
|
+
return this.post("/v1/tax_rates", params);
|
|
400
|
+
}
|
|
401
|
+
retrieve(id) {
|
|
402
|
+
return this.get(`/v1/tax_rates/${id}`);
|
|
403
|
+
}
|
|
404
|
+
update(id, params) {
|
|
405
|
+
return this.post(`/v1/tax_rates/${id}`, params);
|
|
406
|
+
}
|
|
407
|
+
delete(id, params = {}) {
|
|
408
|
+
return this.del(`/v1/tax_rates/${id}`, params);
|
|
409
|
+
}
|
|
410
|
+
list(params = {}) {
|
|
411
|
+
return this.get("/v1/tax_rates", params);
|
|
412
|
+
}
|
|
413
|
+
iter(options = {}) {
|
|
414
|
+
return paginate((p) => this.get("/v1/tax_rates", p), { pageSize: options.pageSize });
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
var Invoices = class extends BaseResource {
|
|
418
|
+
retrieve(id) {
|
|
419
|
+
return this.get(`/v1/invoices/${id}`);
|
|
420
|
+
}
|
|
421
|
+
list(params = {}) {
|
|
422
|
+
return this.get("/v1/invoices", params);
|
|
423
|
+
}
|
|
424
|
+
iter(options = {}) {
|
|
425
|
+
return paginate((p) => this.get("/v1/invoices", p), { pageSize: options.pageSize });
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
var AuditLogs = class extends BaseResource {
|
|
429
|
+
retrieve(id) {
|
|
430
|
+
return this.get(`/v1/audit_logs/${id}`);
|
|
431
|
+
}
|
|
432
|
+
list(params = {}) {
|
|
433
|
+
return this.get("/v1/audit_logs", params);
|
|
434
|
+
}
|
|
435
|
+
iter(options = {}) {
|
|
436
|
+
return paginate((p) => this.get("/v1/audit_logs", p), {
|
|
437
|
+
pageSize: options.pageSize,
|
|
438
|
+
filters: {
|
|
439
|
+
action: options.action,
|
|
440
|
+
resource_type: options.resource_type,
|
|
441
|
+
actor_id: options.actor_id
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var Payments = class extends BaseResource {
|
|
447
|
+
retrieve(id) {
|
|
448
|
+
return this.get(`/v1/payments/${id}`);
|
|
449
|
+
}
|
|
450
|
+
list(params = {}) {
|
|
451
|
+
return this.get("/v1/payments", params);
|
|
452
|
+
}
|
|
453
|
+
iter(options = {}) {
|
|
454
|
+
return paginate((p) => this.get("/v1/payments", p), { pageSize: options.pageSize });
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
var BillingPortalSessions = class extends BaseResource {
|
|
458
|
+
create(params) {
|
|
459
|
+
return this.postFixed(
|
|
460
|
+
"/v1/billing_portal/sessions",
|
|
461
|
+
{
|
|
462
|
+
subscription_id: params.subscription_id,
|
|
463
|
+
return_url: params.return_url
|
|
464
|
+
},
|
|
465
|
+
{ idempotencyKey: params.idempotencyKey }
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
/** Kill an in-the-wild portal session. Idempotent. */
|
|
469
|
+
revoke(id, params = {}) {
|
|
470
|
+
return this.postEmpty(`/v1/billing_portal/sessions/${id}/revoke`, params);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// src/errors.ts
|
|
475
|
+
var BillKitError = class extends Error {
|
|
476
|
+
name = "BillKitError";
|
|
477
|
+
type;
|
|
478
|
+
code;
|
|
479
|
+
param;
|
|
480
|
+
statusCode;
|
|
481
|
+
requestId;
|
|
482
|
+
rawBody;
|
|
483
|
+
constructor(message, options = {}) {
|
|
484
|
+
super(message);
|
|
485
|
+
this.type = options.type;
|
|
486
|
+
this.code = options.code;
|
|
487
|
+
this.param = options.param;
|
|
488
|
+
this.statusCode = options.statusCode;
|
|
489
|
+
this.requestId = options.requestId;
|
|
490
|
+
this.rawBody = options.rawBody;
|
|
491
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
var APIConnectionError = class extends BillKitError {
|
|
495
|
+
name = "APIConnectionError";
|
|
496
|
+
};
|
|
497
|
+
var APIError = class extends BillKitError {
|
|
498
|
+
name = "APIError";
|
|
499
|
+
};
|
|
500
|
+
var ServerError = class extends APIError {
|
|
501
|
+
name = "ServerError";
|
|
502
|
+
};
|
|
503
|
+
var AuthenticationError = class extends BillKitError {
|
|
504
|
+
name = "AuthenticationError";
|
|
505
|
+
};
|
|
506
|
+
var PermissionError = class extends BillKitError {
|
|
507
|
+
name = "PermissionError";
|
|
508
|
+
};
|
|
509
|
+
var ResourceMissingError = class extends BillKitError {
|
|
510
|
+
name = "ResourceMissingError";
|
|
511
|
+
};
|
|
512
|
+
var InvalidRequestError = class extends BillKitError {
|
|
513
|
+
name = "InvalidRequestError";
|
|
514
|
+
};
|
|
515
|
+
var ConflictError = class extends BillKitError {
|
|
516
|
+
name = "ConflictError";
|
|
517
|
+
};
|
|
518
|
+
var RateLimitError = class extends BillKitError {
|
|
519
|
+
name = "RateLimitError";
|
|
520
|
+
retryAfter;
|
|
521
|
+
constructor(message, options = {}) {
|
|
522
|
+
super(message, options);
|
|
523
|
+
this.retryAfter = options.retryAfter;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
var TYPE_TO_CLASS = {
|
|
527
|
+
api_connection_error: APIConnectionError,
|
|
528
|
+
// ``api_error`` is the Stripe-convention type for 5xx, so surface it as
|
|
529
|
+
// ServerError (a subclass of APIError) so `catch (e instanceof
|
|
530
|
+
// ServerError)` works without false negatives.
|
|
531
|
+
api_error: ServerError,
|
|
532
|
+
authentication_error: AuthenticationError,
|
|
533
|
+
permission_error: PermissionError,
|
|
534
|
+
invalid_request_error: InvalidRequestError,
|
|
535
|
+
idempotency_error: ConflictError,
|
|
536
|
+
conflict: ConflictError,
|
|
537
|
+
rate_limit_error: RateLimitError
|
|
538
|
+
};
|
|
539
|
+
function fallbackType(status) {
|
|
540
|
+
if (status === 401) return "authentication_error";
|
|
541
|
+
if (status === 403) return "permission_error";
|
|
542
|
+
if (status === 404) return "invalid_request_error";
|
|
543
|
+
if (status === 409) return "conflict";
|
|
544
|
+
if (status === 429) return "rate_limit_error";
|
|
545
|
+
if (status >= 500) return "api_error";
|
|
546
|
+
return "invalid_request_error";
|
|
547
|
+
}
|
|
548
|
+
function fallbackClass(status) {
|
|
549
|
+
if (status === 401) return AuthenticationError;
|
|
550
|
+
if (status === 403) return PermissionError;
|
|
551
|
+
if (status === 404) return ResourceMissingError;
|
|
552
|
+
if (status === 409) return ConflictError;
|
|
553
|
+
if (status === 429) return RateLimitError;
|
|
554
|
+
if (status >= 500) return ServerError;
|
|
555
|
+
return InvalidRequestError;
|
|
556
|
+
}
|
|
557
|
+
function errorFromResponse(args) {
|
|
558
|
+
const { status, body, requestId, retryAfter } = args;
|
|
559
|
+
const envelope = typeof body === "object" && body !== null && "error" in body ? body.error ?? {} : {};
|
|
560
|
+
const type = envelope.type ?? fallbackType(status);
|
|
561
|
+
const message = envelope.message ?? `BillKit API returned HTTP ${status} with no error body.`;
|
|
562
|
+
let cls = TYPE_TO_CLASS[type] ?? fallbackClass(status);
|
|
563
|
+
if (status === 404 && cls === InvalidRequestError) cls = ResourceMissingError;
|
|
564
|
+
if (status === 409 && cls === InvalidRequestError) cls = ConflictError;
|
|
565
|
+
const options = {
|
|
566
|
+
type,
|
|
567
|
+
code: envelope.code,
|
|
568
|
+
param: envelope.param,
|
|
569
|
+
statusCode: status,
|
|
570
|
+
requestId,
|
|
571
|
+
rawBody: typeof body === "object" && body !== null ? body : void 0
|
|
572
|
+
};
|
|
573
|
+
if (cls === RateLimitError) {
|
|
574
|
+
options.retryAfter = retryAfter;
|
|
575
|
+
}
|
|
576
|
+
return new cls(message, options);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/logging.ts
|
|
580
|
+
var NOOP_LOGGER = {
|
|
581
|
+
debug() {
|
|
582
|
+
},
|
|
583
|
+
warn() {
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/retry.ts
|
|
588
|
+
var DEFAULT_RETRY_POLICY = {
|
|
589
|
+
maxAttempts: 4,
|
|
590
|
+
initialBackoffMs: 500,
|
|
591
|
+
backoffMultiplier: 2,
|
|
592
|
+
maxBackoffMs: 8e3,
|
|
593
|
+
maxRetryAfterMs: 3e4,
|
|
594
|
+
jitter: 0.25
|
|
595
|
+
};
|
|
596
|
+
function backoffForMs(attempt, policy) {
|
|
597
|
+
const base = policy.initialBackoffMs * policy.backoffMultiplier ** (attempt - 2);
|
|
598
|
+
const capped = Math.min(base, policy.maxBackoffMs);
|
|
599
|
+
const jitterRange = capped * policy.jitter;
|
|
600
|
+
const jittered = capped + (Math.random() * 2 - 1) * jitterRange;
|
|
601
|
+
return Math.max(0, jittered);
|
|
602
|
+
}
|
|
603
|
+
function shouldRetry(status, attempt, policy, retryAfterMs) {
|
|
604
|
+
if (attempt >= policy.maxAttempts) return false;
|
|
605
|
+
if (status === null) return true;
|
|
606
|
+
if (status === 429) {
|
|
607
|
+
if (retryAfterMs === void 0 || retryAfterMs < 0) return false;
|
|
608
|
+
return policy.maxRetryAfterMs === void 0 || retryAfterMs <= policy.maxRetryAfterMs;
|
|
609
|
+
}
|
|
610
|
+
return status >= 500;
|
|
611
|
+
}
|
|
612
|
+
function sleep(ms) {
|
|
613
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/version.ts
|
|
617
|
+
var VERSION = "0.1.0";
|
|
618
|
+
|
|
619
|
+
// src/transport.ts
|
|
620
|
+
var DEFAULT_BASE_URL = "https://api.billkit.eu";
|
|
621
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
622
|
+
function userAgent() {
|
|
623
|
+
return `billkit-node/${VERSION}`;
|
|
624
|
+
}
|
|
625
|
+
function autoIdempotencyKey(method, supplied) {
|
|
626
|
+
if (method === "GET") return void 0;
|
|
627
|
+
if (supplied !== void 0) return supplied;
|
|
628
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
629
|
+
if (uuid === void 0) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
"BillKit: crypto.randomUUID() is unavailable, so a safe Idempotency-Key cannot be generated. Use Node 20+, Bun, Deno, or Cloudflare Workers, or pass your own `idempotencyKey` on this call."
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
return `sdk-${uuid}`;
|
|
635
|
+
}
|
|
636
|
+
function logSafeUrl(baseUrl, path) {
|
|
637
|
+
const normalised = path.startsWith("/") ? path : `/${path}`;
|
|
638
|
+
return baseUrl.replace(/\/$/, "") + normalised;
|
|
639
|
+
}
|
|
640
|
+
function buildUrl(baseUrl, path, query) {
|
|
641
|
+
const normalised = path.startsWith("/") ? path : `/${path}`;
|
|
642
|
+
const url = new URL(baseUrl.replace(/\/$/, "") + normalised);
|
|
643
|
+
if (query) {
|
|
644
|
+
for (const [k, v] of Object.entries(query)) {
|
|
645
|
+
if (v !== null && v !== void 0) {
|
|
646
|
+
url.searchParams.set(k, String(v));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return url.toString();
|
|
651
|
+
}
|
|
652
|
+
function buildHeaders(apiKey, hasBody, idempotencyKey, extra) {
|
|
653
|
+
const headers = new Headers({
|
|
654
|
+
Authorization: `Bearer ${apiKey}`,
|
|
655
|
+
"User-Agent": userAgent(),
|
|
656
|
+
Accept: "application/json"
|
|
657
|
+
});
|
|
658
|
+
if (hasBody) headers.set("Content-Type", "application/json");
|
|
659
|
+
if (idempotencyKey) headers.set("Idempotency-Key", idempotencyKey);
|
|
660
|
+
if (extra) {
|
|
661
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
662
|
+
headers.set(k, v);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return headers;
|
|
666
|
+
}
|
|
667
|
+
async function parseJson(response) {
|
|
668
|
+
const text = await response.text();
|
|
669
|
+
if (!text) return null;
|
|
670
|
+
try {
|
|
671
|
+
return JSON.parse(text);
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function parseRetryAfterMs(header) {
|
|
677
|
+
if (!header) return void 0;
|
|
678
|
+
const n = Number.parseFloat(header);
|
|
679
|
+
if (Number.isFinite(n) && n >= 0) return n * 1e3;
|
|
680
|
+
const retryAt = Date.parse(header);
|
|
681
|
+
if (Number.isNaN(retryAt)) return void 0;
|
|
682
|
+
return Math.max(0, retryAt - Date.now());
|
|
683
|
+
}
|
|
684
|
+
function retryDelayMs(status, attempt, policy, retryAfterMs) {
|
|
685
|
+
if (status === 429 && retryAfterMs !== void 0) return retryAfterMs;
|
|
686
|
+
return backoffForMs(attempt + 1, policy);
|
|
687
|
+
}
|
|
688
|
+
function connectionError(err, timeoutMs) {
|
|
689
|
+
const e = err;
|
|
690
|
+
if (e?.name === "TimeoutError" || e?.name === "AbortError") {
|
|
691
|
+
return new APIConnectionError(`BillKit request timed out after ${timeoutMs}ms.`);
|
|
692
|
+
}
|
|
693
|
+
return new APIConnectionError(e?.message ?? "Network request failed.");
|
|
694
|
+
}
|
|
695
|
+
var Transport = class {
|
|
696
|
+
apiKey;
|
|
697
|
+
baseUrl;
|
|
698
|
+
timeoutMs;
|
|
699
|
+
retryPolicy;
|
|
700
|
+
fetchFn;
|
|
701
|
+
logger;
|
|
702
|
+
constructor(config) {
|
|
703
|
+
if (!config.apiKey) {
|
|
704
|
+
throw new Error("BillKit: an API key is required (config.apiKey or BILLKIT_API_KEY env).");
|
|
705
|
+
}
|
|
706
|
+
this.apiKey = config.apiKey;
|
|
707
|
+
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
708
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
709
|
+
this.retryPolicy = config.retryPolicy ?? DEFAULT_RETRY_POLICY;
|
|
710
|
+
this.logger = config.logger ?? NOOP_LOGGER;
|
|
711
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
712
|
+
if (!fetchFn) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
"BillKit: no global fetch implementation found. Use Node 20+, Bun, Deno, Cloudflare Workers, or pass { fetch } in the client options."
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
this.fetchFn = fetchFn.bind(globalThis);
|
|
718
|
+
}
|
|
719
|
+
async request(options) {
|
|
720
|
+
const idempotencyKey = autoIdempotencyKey(options.method, options.idempotencyKey);
|
|
721
|
+
const url = buildUrl(this.baseUrl, options.path, options.query);
|
|
722
|
+
const headers = buildHeaders(
|
|
723
|
+
this.apiKey,
|
|
724
|
+
options.body !== void 0,
|
|
725
|
+
idempotencyKey,
|
|
726
|
+
options.extraHeaders
|
|
727
|
+
);
|
|
728
|
+
const body = options.body !== void 0 ? JSON.stringify(options.body) : void 0;
|
|
729
|
+
const loggedUrl = logSafeUrl(this.baseUrl, options.path);
|
|
730
|
+
let lastError = null;
|
|
731
|
+
for (let attempt = 1; attempt <= this.retryPolicy.maxAttempts; attempt++) {
|
|
732
|
+
this.logger.debug("BillKit request", {
|
|
733
|
+
method: options.method,
|
|
734
|
+
url: loggedUrl,
|
|
735
|
+
attempt,
|
|
736
|
+
maxAttempts: this.retryPolicy.maxAttempts
|
|
737
|
+
});
|
|
738
|
+
const startedAt = Date.now();
|
|
739
|
+
let response;
|
|
740
|
+
let parsedBody;
|
|
741
|
+
try {
|
|
742
|
+
const init = {
|
|
743
|
+
method: options.method,
|
|
744
|
+
headers,
|
|
745
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
746
|
+
};
|
|
747
|
+
if (body !== void 0) init.body = body;
|
|
748
|
+
response = await this.fetchFn(url, init);
|
|
749
|
+
parsedBody = await parseJson(response);
|
|
750
|
+
} catch (err) {
|
|
751
|
+
lastError = connectionError(err, this.timeoutMs);
|
|
752
|
+
if (!shouldRetry(null, attempt, this.retryPolicy)) throw lastError;
|
|
753
|
+
const delayMs2 = retryDelayMs(null, attempt, this.retryPolicy);
|
|
754
|
+
this.logger.warn("BillKit retrying", {
|
|
755
|
+
method: options.method,
|
|
756
|
+
url: loggedUrl,
|
|
757
|
+
reason: err?.name ?? "network error",
|
|
758
|
+
attempt,
|
|
759
|
+
delayMs: delayMs2
|
|
760
|
+
});
|
|
761
|
+
await sleep(delayMs2);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const requestId = response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? void 0;
|
|
765
|
+
this.logger.debug("BillKit response", {
|
|
766
|
+
method: options.method,
|
|
767
|
+
url: loggedUrl,
|
|
768
|
+
status: response.status,
|
|
769
|
+
durationMs: Date.now() - startedAt,
|
|
770
|
+
requestId: requestId ?? null
|
|
771
|
+
});
|
|
772
|
+
if (response.ok) {
|
|
773
|
+
return parsedBody ?? void 0;
|
|
774
|
+
}
|
|
775
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
776
|
+
const error = errorFromResponse({
|
|
777
|
+
status: response.status,
|
|
778
|
+
body: parsedBody,
|
|
779
|
+
requestId,
|
|
780
|
+
retryAfter: retryAfterMs === void 0 ? void 0 : retryAfterMs / 1e3
|
|
781
|
+
});
|
|
782
|
+
if (!shouldRetry(response.status, attempt, this.retryPolicy, retryAfterMs)) {
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
lastError = error;
|
|
786
|
+
const delayMs = retryDelayMs(response.status, attempt, this.retryPolicy, retryAfterMs);
|
|
787
|
+
this.logger.warn("BillKit retrying", {
|
|
788
|
+
method: options.method,
|
|
789
|
+
url: loggedUrl,
|
|
790
|
+
reason: `HTTP ${response.status}`,
|
|
791
|
+
attempt,
|
|
792
|
+
delayMs
|
|
793
|
+
});
|
|
794
|
+
await sleep(delayMs);
|
|
795
|
+
}
|
|
796
|
+
if (lastError) throw lastError;
|
|
797
|
+
throw new APIConnectionError("Retry budget exhausted with no recorded error.");
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// src/client.ts
|
|
802
|
+
function resolveApiKey(supplied) {
|
|
803
|
+
if (supplied) return supplied;
|
|
804
|
+
const env = globalThis.process?.env?.["BILLKIT_API_KEY"];
|
|
805
|
+
if (env) return env;
|
|
806
|
+
throw new Error(
|
|
807
|
+
"BillKit: missing API key. Pass { apiKey } or set BILLKIT_API_KEY in the environment."
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
var BillKit = class {
|
|
811
|
+
customers;
|
|
812
|
+
products;
|
|
813
|
+
prices;
|
|
814
|
+
checkoutSessions;
|
|
815
|
+
oneShotPayments;
|
|
816
|
+
subscriptions;
|
|
817
|
+
refunds;
|
|
818
|
+
disputes;
|
|
819
|
+
webhookEndpoints;
|
|
820
|
+
events;
|
|
821
|
+
tenant;
|
|
822
|
+
coupons;
|
|
823
|
+
taxRates;
|
|
824
|
+
invoices;
|
|
825
|
+
auditLogs;
|
|
826
|
+
payments;
|
|
827
|
+
billingPortalSessions;
|
|
828
|
+
constructor(options = {}) {
|
|
829
|
+
const transport = new Transport({
|
|
830
|
+
...options,
|
|
831
|
+
apiKey: resolveApiKey(options.apiKey)
|
|
832
|
+
});
|
|
833
|
+
this.customers = new Customers(transport);
|
|
834
|
+
this.products = new Products(transport);
|
|
835
|
+
this.prices = new Prices(transport);
|
|
836
|
+
this.checkoutSessions = new CheckoutSessions(transport);
|
|
837
|
+
this.oneShotPayments = new OneShotPayments(transport);
|
|
838
|
+
this.subscriptions = new Subscriptions(transport);
|
|
839
|
+
this.refunds = new Refunds(transport);
|
|
840
|
+
this.disputes = new Disputes(transport);
|
|
841
|
+
this.webhookEndpoints = new WebhookEndpoints(transport);
|
|
842
|
+
this.events = new Events(transport);
|
|
843
|
+
this.tenant = new Tenant(transport);
|
|
844
|
+
this.coupons = new Coupons(transport);
|
|
845
|
+
this.taxRates = new TaxRates(transport);
|
|
846
|
+
this.invoices = new Invoices(transport);
|
|
847
|
+
this.auditLogs = new AuditLogs(transport);
|
|
848
|
+
this.payments = new Payments(transport);
|
|
849
|
+
this.billingPortalSessions = new BillingPortalSessions(transport);
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// src/webhooks.ts
|
|
854
|
+
var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
855
|
+
var WebhookVerificationError = class extends Error {
|
|
856
|
+
name = "WebhookVerificationError";
|
|
857
|
+
constructor(message) {
|
|
858
|
+
super(message);
|
|
859
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
var textEncoder = new TextEncoder();
|
|
863
|
+
var V1_HEX_RE = /^[0-9a-fA-F]{64}$/;
|
|
864
|
+
function toBytes(payload) {
|
|
865
|
+
return typeof payload === "string" ? textEncoder.encode(payload) : payload;
|
|
866
|
+
}
|
|
867
|
+
function constantTimeEqual(a, b) {
|
|
868
|
+
if (a.length !== b.length) return false;
|
|
869
|
+
let diff = 0;
|
|
870
|
+
for (let i = 0; i < a.length; i++) {
|
|
871
|
+
diff |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
872
|
+
}
|
|
873
|
+
return diff === 0;
|
|
874
|
+
}
|
|
875
|
+
function hexToBytes(hex) {
|
|
876
|
+
if (!V1_HEX_RE.test(hex)) return null;
|
|
877
|
+
const out = new Uint8Array(hex.length / 2);
|
|
878
|
+
for (let i = 0; i < out.length; i++) {
|
|
879
|
+
const byte = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
880
|
+
if (Number.isNaN(byte)) return null;
|
|
881
|
+
out[i] = byte;
|
|
882
|
+
}
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
function parseSignatureHeader(header) {
|
|
886
|
+
let tsRaw;
|
|
887
|
+
const v1List = [];
|
|
888
|
+
for (const chunk of header.split(",")) {
|
|
889
|
+
const idx = chunk.indexOf("=");
|
|
890
|
+
if (idx < 0) continue;
|
|
891
|
+
const key = chunk.slice(0, idx).trim();
|
|
892
|
+
const value = chunk.slice(idx + 1).trim();
|
|
893
|
+
if (key === "t") tsRaw = value;
|
|
894
|
+
else if (key === "v1") v1List.push(value);
|
|
895
|
+
}
|
|
896
|
+
if (!tsRaw || v1List.length === 0) {
|
|
897
|
+
throw new WebhookVerificationError(`Malformed BillKit-Signature header: ${header}`);
|
|
898
|
+
}
|
|
899
|
+
const ts = Number.parseInt(tsRaw, 10);
|
|
900
|
+
if (Number.isNaN(ts) || ts <= 0) {
|
|
901
|
+
throw new WebhookVerificationError(`Malformed timestamp in BillKit-Signature: ${tsRaw}`);
|
|
902
|
+
}
|
|
903
|
+
return { ts, v1List };
|
|
904
|
+
}
|
|
905
|
+
function toArrayBuffer(view) {
|
|
906
|
+
const out = new ArrayBuffer(view.byteLength);
|
|
907
|
+
new Uint8Array(out).set(view);
|
|
908
|
+
return out;
|
|
909
|
+
}
|
|
910
|
+
async function computeHmac(secret, signed) {
|
|
911
|
+
const subtle = globalThis.crypto?.subtle;
|
|
912
|
+
if (!subtle) {
|
|
913
|
+
throw new WebhookVerificationError(
|
|
914
|
+
"No SubtleCrypto available. The BillKit SDK requires Node 20+, Bun, Deno, Cloudflare Workers, or any runtime that exposes globalThis.crypto.subtle."
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
const key = await subtle.importKey(
|
|
918
|
+
"raw",
|
|
919
|
+
toArrayBuffer(textEncoder.encode(secret)),
|
|
920
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
921
|
+
false,
|
|
922
|
+
["sign"]
|
|
923
|
+
);
|
|
924
|
+
const signature = await subtle.sign("HMAC", key, toArrayBuffer(signed));
|
|
925
|
+
return new Uint8Array(signature);
|
|
926
|
+
}
|
|
927
|
+
async function verifyWebhookSignature(options) {
|
|
928
|
+
const {
|
|
929
|
+
payload,
|
|
930
|
+
signatureHeader,
|
|
931
|
+
secret,
|
|
932
|
+
toleranceSeconds = DEFAULT_WEBHOOK_TOLERANCE_SECONDS,
|
|
933
|
+
nowMs = Date.now()
|
|
934
|
+
} = options;
|
|
935
|
+
if (signatureHeader === null || signatureHeader === void 0) {
|
|
936
|
+
throw new WebhookVerificationError("Missing BillKit-Signature header.");
|
|
937
|
+
}
|
|
938
|
+
const { ts, v1List } = parseSignatureHeader(signatureHeader);
|
|
939
|
+
if (Math.abs(nowMs / 1e3 - ts) > toleranceSeconds) {
|
|
940
|
+
throw new WebhookVerificationError(
|
|
941
|
+
`Signature timestamp outside \xB1${toleranceSeconds}s tolerance.`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const payloadBytes = toBytes(payload);
|
|
945
|
+
const signed = new Uint8Array(payloadBytes.length + textEncoder.encode(`${ts}.`).length);
|
|
946
|
+
const prefix = textEncoder.encode(`${ts}.`);
|
|
947
|
+
signed.set(prefix, 0);
|
|
948
|
+
signed.set(payloadBytes, prefix.length);
|
|
949
|
+
const expected = await computeHmac(secret, signed);
|
|
950
|
+
let sawValidHex = false;
|
|
951
|
+
let matched = false;
|
|
952
|
+
for (const v1 of v1List) {
|
|
953
|
+
const received = hexToBytes(v1);
|
|
954
|
+
if (!received) continue;
|
|
955
|
+
sawValidHex = true;
|
|
956
|
+
if (constantTimeEqual(expected, received)) matched = true;
|
|
957
|
+
}
|
|
958
|
+
if (!sawValidHex) {
|
|
959
|
+
throw new WebhookVerificationError(
|
|
960
|
+
`Malformed v1 hex in BillKit-Signature: ${v1List.join(",")}`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
if (!matched) {
|
|
964
|
+
throw new WebhookVerificationError("Signature mismatch.");
|
|
965
|
+
}
|
|
966
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
967
|
+
const text = decoder.decode(payloadBytes);
|
|
968
|
+
try {
|
|
969
|
+
return JSON.parse(text);
|
|
970
|
+
} catch (err) {
|
|
971
|
+
throw new WebhookVerificationError(
|
|
972
|
+
`Webhook body is not valid JSON: ${err.message}`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
exports.APIConnectionError = APIConnectionError;
|
|
978
|
+
exports.APIError = APIError;
|
|
979
|
+
exports.AuthenticationError = AuthenticationError;
|
|
980
|
+
exports.BillKit = BillKit;
|
|
981
|
+
exports.BillKitError = BillKitError;
|
|
982
|
+
exports.ConflictError = ConflictError;
|
|
983
|
+
exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
|
|
984
|
+
exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
|
|
985
|
+
exports.InvalidRequestError = InvalidRequestError;
|
|
986
|
+
exports.NOOP_LOGGER = NOOP_LOGGER;
|
|
987
|
+
exports.PermissionError = PermissionError;
|
|
988
|
+
exports.RateLimitError = RateLimitError;
|
|
989
|
+
exports.ResourceMissingError = ResourceMissingError;
|
|
990
|
+
exports.ServerError = ServerError;
|
|
991
|
+
exports.VERSION = VERSION;
|
|
992
|
+
exports.WebhookVerificationError = WebhookVerificationError;
|
|
993
|
+
exports.paginate = paginate;
|
|
994
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
995
|
+
//# sourceMappingURL=index.cjs.map
|
|
996
|
+
//# sourceMappingURL=index.cjs.map
|