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