@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/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