@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/src/index.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Official Node.js / TypeScript SDK for BillKit.
3
+ *
4
+ * Quick start:
5
+ *
6
+ * ```ts
7
+ * import { BillKit } from "@billkit-eu/sdk";
8
+ *
9
+ * const client = new BillKit({ apiKey: "sk_test_..." });
10
+ * const customer = await client.customers.create<{ id: string }>({
11
+ * email: "ada@example.com",
12
+ * });
13
+ * const product = await client.products.create<{ id: string }>({ name: "Pro" });
14
+ * const price = await client.prices.create<{ id: string }>({
15
+ * product_id: product.id,
16
+ * amount_cents: 999,
17
+ * currency: "EUR",
18
+ * interval: "month",
19
+ * });
20
+ * ```
21
+ *
22
+ * Auto-pagination via async iteration:
23
+ *
24
+ * ```ts
25
+ * for await (const customer of client.customers.iter()) {
26
+ * console.log(customer);
27
+ * }
28
+ * ```
29
+ *
30
+ * Webhook verification:
31
+ *
32
+ * ```ts
33
+ * try {
34
+ * const event = await verifyWebhookSignature({
35
+ * payload: rawBody,
36
+ * signatureHeader: req.headers["billkit-signature"],
37
+ * secret: process.env.BILLKIT_WEBHOOK_SECRET!,
38
+ * });
39
+ * } catch (err) {
40
+ * return new Response(null, { status: 400 });
41
+ * }
42
+ * ```
43
+ *
44
+ * Logging is off by default and never configures anything for you. Pass
45
+ * a logger to opt in:
46
+ *
47
+ * ```ts
48
+ * const client = new BillKit({ apiKey: "sk_test_...", logger: console });
49
+ * ```
50
+ *
51
+ * See {@link BillKitLogger} for what is logged and what is withheld
52
+ * (keys, bodies and query strings never reach it).
53
+ */
54
+
55
+ export { BillKit, type BillKitOptions } from "./client.js";
56
+ export {
57
+ APIConnectionError,
58
+ APIError,
59
+ AuthenticationError,
60
+ BillKitError,
61
+ ConflictError,
62
+ InvalidRequestError,
63
+ PermissionError,
64
+ RateLimitError,
65
+ ResourceMissingError,
66
+ ServerError,
67
+ } from "./errors.js";
68
+ export { NOOP_LOGGER, type BillKitLogger, type LogContext } from "./logging.js";
69
+ export { paginate, type ListResponseEnvelope, type PaginateOptions } from "./pagination.js";
70
+ export type {
71
+ AuditLogsListParams,
72
+ BaseListParams,
73
+ CreateBillingPortalSessionParams,
74
+ CreateCheckoutSessionParams,
75
+ CreateCouponParams,
76
+ CreateCustomerParams,
77
+ CreateOneShotPaymentParams,
78
+ CreatePriceParams,
79
+ CreateProductParams,
80
+ CreateRefundParams,
81
+ CreateTaxRateParams,
82
+ CreateWebhookEndpointParams,
83
+ EventsListParams,
84
+ IdempotencyOptions,
85
+ ListParams,
86
+ PricesListParams,
87
+ RotateProviderCredentialParams,
88
+ SetPortalBrandingParams,
89
+ UpdateCouponParams,
90
+ UpdateCustomerParams,
91
+ UpdateProductParams,
92
+ UpdateTaxRateParams,
93
+ UpdateWebhookEndpointParams,
94
+ ValidateCouponParams,
95
+ } from "./resources.js";
96
+ export { DEFAULT_RETRY_POLICY, type RetryPolicy } from "./retry.js";
97
+ export { VERSION } from "./version.js";
98
+ export {
99
+ DEFAULT_WEBHOOK_TOLERANCE_SECONDS,
100
+ type VerifyWebhookOptions,
101
+ WebhookVerificationError,
102
+ verifyWebhookSignature,
103
+ } from "./webhooks.js";
package/src/logging.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Opt-in logging for the BillKit SDK.
3
+ *
4
+ * A library has no business deciding where its host application's logs
5
+ * go, so this SDK ships no logger, no transport, and no destination. It
6
+ * accepts one from you and writes to a no-op until you do:
7
+ *
8
+ * ```ts
9
+ * const client = new BillKit({ logger: console });
10
+ * ```
11
+ *
12
+ * `console` satisfies {@link BillKitLogger} structurally, so that line
13
+ * works with no adapter. So does a pino/winston/bunyan child logger:
14
+ * their `debug(msg, ctx)` / `warn(msg, ctx)` signatures line up. If yours
15
+ * takes its arguments the other way round, wrap it:
16
+ *
17
+ * ```ts
18
+ * const logger = {
19
+ * debug: (m, c) => myLogger.debug(c, m),
20
+ * warn: (m, c) => myLogger.warn(c, m),
21
+ * };
22
+ * ```
23
+ *
24
+ * ## What gets logged
25
+ *
26
+ * - **debug**: one call per HTTP attempt and one per response, with
27
+ * `method`, `url`, `attempt`, `status`, `durationMs`, and `requestId`
28
+ * (quote that id to BillKit support).
29
+ * - **warn**: one call per retry, naming the reason and the delay before
30
+ * the next attempt. A retry is a real anomaly worth surfacing without
31
+ * being an error.
32
+ *
33
+ * ## What is deliberately never logged
34
+ *
35
+ * - The `Authorization` header or the API key, in any form.
36
+ * - Request and response **bodies**. They carry customer PII (emails,
37
+ * names, addresses) and billing detail; a payments SDK that quietly
38
+ * copies those into its user's log sink has manufactured a compliance
39
+ * problem on their behalf.
40
+ * - The **query string**. List filters routinely carry values like
41
+ * `email=ada@example.com`, so only the path is logged.
42
+ * - The **final failure**. Every exhausted call throws a typed
43
+ * `BillKitError` carrying the status, request id and retry-after;
44
+ * logging it here as well would produce a duplicate the caller never
45
+ * asked for and cannot suppress from their own sink.
46
+ */
47
+
48
+ /** Structured context attached to a log line. Never contains secrets. */
49
+ export type LogContext = Record<string, unknown>;
50
+
51
+ /**
52
+ * The minimum a logger must do for the SDK to use it. Deliberately two
53
+ * methods: the SDK has exactly two things to say, and a narrow interface
54
+ * is one almost every logger already satisfies without an adapter.
55
+ */
56
+ export interface BillKitLogger {
57
+ debug(message: string, context?: LogContext): void;
58
+ warn(message: string, context?: LogContext): void;
59
+ }
60
+
61
+ /**
62
+ * The default. Discards everything, so the SDK is silent until a logger
63
+ * is supplied, and costs nothing when it isn't.
64
+ */
65
+ export const NOOP_LOGGER: BillKitLogger = {
66
+ debug(): void {
67
+ /* intentionally empty */
68
+ },
69
+ warn(): void {
70
+ /* intentionally empty */
71
+ },
72
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Auto-pagination helper for list endpoints.
3
+ *
4
+ * The BillKit API returns Stripe-shape envelopes:
5
+ *
6
+ * { "object": "list", "data": [...], "has_more": bool }
7
+ *
8
+ * Cursor pagination is forward-only via the last item's `id` as
9
+ * `starting_after`. `paginate` walks every page and yields each row.
10
+ * Callers consume it via `for await`:
11
+ *
12
+ * for await (const customer of client.customers.iter()) {
13
+ * ...
14
+ * }
15
+ */
16
+
17
+ import type { QueryValue } from "./transport.js";
18
+
19
+ export interface ListResponseEnvelope<T = unknown> {
20
+ readonly data?: readonly T[];
21
+ readonly has_more?: boolean;
22
+ }
23
+
24
+ export interface PaginateOptions {
25
+ /** Maps to the API's `limit` parameter. `undefined` lets the
26
+ * server pick its default (10 today). */
27
+ pageSize?: number | undefined;
28
+ /** Extra filters forwarded on every page (e.g. `type` on events,
29
+ * `action` on audit logs). Values are pruned of `undefined` so
30
+ * callers can spread their full options object in. */
31
+ filters?: Readonly<Record<string, QueryValue>>;
32
+ }
33
+
34
+ type ListFn<T> = (params: {
35
+ limit?: number | undefined;
36
+ starting_after?: string | undefined;
37
+ [key: string]: QueryValue;
38
+ }) => Promise<ListResponseEnvelope<T>>;
39
+
40
+ /**
41
+ * Walk every page of `listFn` and yield each row.
42
+ *
43
+ * Three terminators, in priority order:
44
+ * 1. `has_more=false`: the server's authoritative signal (common case).
45
+ * 2. Empty `data` with `has_more=true`: shouldn't happen per the API
46
+ * contract, but if a future server bug or proxy misbehaviour
47
+ * produced it the iterator would loop forever. Belt-and-suspenders.
48
+ * 3. The last row has no `id`, so there is no cursor to advance with. The schema
49
+ * doesn't allow it today, but same defensive reasoning.
50
+ */
51
+ export async function* paginate<T>(
52
+ listFn: ListFn<T>,
53
+ options: PaginateOptions = {},
54
+ ): AsyncIterableIterator<T> {
55
+ const { pageSize, filters } = options;
56
+ const cleanFilters: Record<string, QueryValue> = {};
57
+ if (filters) {
58
+ for (const [k, v] of Object.entries(filters)) {
59
+ if (v !== undefined) cleanFilters[k] = v;
60
+ }
61
+ }
62
+
63
+ let cursor: string | undefined;
64
+ for (;;) {
65
+ const page = await listFn({
66
+ ...cleanFilters,
67
+ limit: pageSize,
68
+ starting_after: cursor,
69
+ });
70
+ const items = page.data ?? [];
71
+ for (const item of items) yield item;
72
+ if (!page.has_more || items.length === 0) return;
73
+ const last = items[items.length - 1] as { id?: string } | undefined;
74
+ cursor = last?.id;
75
+ if (cursor === undefined) return;
76
+ }
77
+ }