@billkit-eu/sdk 0.1.0 → 0.2.1
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 +41 -2
- package/README.md +85 -11
- package/dist/index.cjs +182 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +244 -16
- package/dist/index.d.ts +244 -16
- package/dist/index.js +182 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +28 -22
- package/src/index.ts +5 -2
- package/src/resources.ts +255 -22
- package/src/retry.ts +31 -3
- package/src/transport.ts +58 -4
- package/src/version.ts +1 -1
package/src/transport.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface RequestOptions {
|
|
|
33
33
|
body?: Record<string, unknown> | undefined;
|
|
34
34
|
idempotencyKey?: string | undefined;
|
|
35
35
|
extraHeaders?: Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* How to read a **successful** response body. `"json"` (the default)
|
|
38
|
+
* parses it; `"binary"` hands back the raw `ArrayBuffer`, for
|
|
39
|
+
* endpoints that serve a document rather than a resource (the invoice
|
|
40
|
+
* PDF). Error responses are always read as JSON either way, so the
|
|
41
|
+
* typed error hierarchy behaves identically on both paths.
|
|
42
|
+
*/
|
|
43
|
+
responseType?: "json" | "binary";
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export interface TransportConfig {
|
|
@@ -126,8 +134,7 @@ function buildHeaders(
|
|
|
126
134
|
return headers;
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
const text = await response.text();
|
|
137
|
+
function parseJsonText(text: string): unknown {
|
|
131
138
|
if (!text) return null;
|
|
132
139
|
try {
|
|
133
140
|
return JSON.parse(text);
|
|
@@ -136,6 +143,32 @@ async function parseJson(response: Response): Promise<unknown> {
|
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
async function parseJson(response: Response): Promise<unknown> {
|
|
147
|
+
return parseJsonText(await response.text());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read the body once, as the caller asked for it.
|
|
152
|
+
*
|
|
153
|
+
* A `Response` body can only be consumed once, so the choice has to be
|
|
154
|
+
* made here rather than after the status check. On the binary path a
|
|
155
|
+
* *failed* response is still decoded as UTF-8 JSON: an error is an error
|
|
156
|
+
* envelope no matter which endpoint produced it, and losing that would
|
|
157
|
+
* mean the PDF call throwing a shapeless error where every other call
|
|
158
|
+
* throws a typed one.
|
|
159
|
+
*/
|
|
160
|
+
async function readBody(
|
|
161
|
+
response: Response,
|
|
162
|
+
responseType: "json" | "binary",
|
|
163
|
+
): Promise<{ parsed: unknown; binary: ArrayBuffer | undefined }> {
|
|
164
|
+
if (responseType !== "binary") {
|
|
165
|
+
return { parsed: await parseJson(response), binary: undefined };
|
|
166
|
+
}
|
|
167
|
+
const buffer = await response.arrayBuffer();
|
|
168
|
+
if (response.ok) return { parsed: null, binary: buffer };
|
|
169
|
+
return { parsed: parseJsonText(new TextDecoder().decode(buffer)), binary: undefined };
|
|
170
|
+
}
|
|
171
|
+
|
|
139
172
|
function parseRetryAfterMs(header: string | null): number | undefined {
|
|
140
173
|
if (!header) return undefined;
|
|
141
174
|
const n = Number.parseFloat(header);
|
|
@@ -198,7 +231,22 @@ export class Transport {
|
|
|
198
231
|
this.fetchFn = fetchFn.bind(globalThis);
|
|
199
232
|
}
|
|
200
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Fetch a binary document (currently only the invoice PDF).
|
|
236
|
+
*
|
|
237
|
+
* Same retry policy, same timeout, same typed errors as
|
|
238
|
+
* {@link Transport.request}; only the success-path decoding differs.
|
|
239
|
+
* `fetch` follows the storage adapter's `302` to the signed URL by
|
|
240
|
+
* itself, and the WHATWG spec drops the `Authorization` header on that
|
|
241
|
+
* cross-origin hop — which is correct, since a presigned URL carries
|
|
242
|
+
* its own credential and must not be handed BillKit's API key.
|
|
243
|
+
*/
|
|
244
|
+
requestBinary(options: Omit<RequestOptions, "responseType">): Promise<ArrayBuffer> {
|
|
245
|
+
return this.request<ArrayBuffer>({ ...options, responseType: "binary" });
|
|
246
|
+
}
|
|
247
|
+
|
|
201
248
|
async request<T = unknown>(options: RequestOptions): Promise<T> {
|
|
249
|
+
const responseType = options.responseType ?? "json";
|
|
202
250
|
const idempotencyKey = autoIdempotencyKey(options.method, options.idempotencyKey);
|
|
203
251
|
const url = buildUrl(this.baseUrl, options.path, options.query);
|
|
204
252
|
const headers = buildHeaders(
|
|
@@ -222,6 +270,7 @@ export class Transport {
|
|
|
222
270
|
const startedAt = Date.now();
|
|
223
271
|
let response: Response;
|
|
224
272
|
let parsedBody: unknown;
|
|
273
|
+
let binaryBody: ArrayBuffer | undefined;
|
|
225
274
|
try {
|
|
226
275
|
// ``body`` is only spread when present so a GET request goes
|
|
227
276
|
// out without a body field. Some hosts (Cloudflare Workers'
|
|
@@ -240,7 +289,7 @@ export class Transport {
|
|
|
240
289
|
};
|
|
241
290
|
if (body !== undefined) init.body = body;
|
|
242
291
|
response = await this.fetchFn(url, init);
|
|
243
|
-
parsedBody = await
|
|
292
|
+
({ parsed: parsedBody, binary: binaryBody } = await readBody(response, responseType));
|
|
244
293
|
} catch (err) {
|
|
245
294
|
lastError = connectionError(err, this.timeoutMs);
|
|
246
295
|
if (!shouldRetry(null, attempt, this.retryPolicy)) throw lastError;
|
|
@@ -267,6 +316,7 @@ export class Transport {
|
|
|
267
316
|
});
|
|
268
317
|
|
|
269
318
|
if (response.ok) {
|
|
319
|
+
if (responseType === "binary") return binaryBody as T;
|
|
270
320
|
return (parsedBody ?? undefined) as T;
|
|
271
321
|
}
|
|
272
322
|
|
|
@@ -278,7 +328,11 @@ export class Transport {
|
|
|
278
328
|
retryAfter: retryAfterMs === undefined ? undefined : retryAfterMs / 1000,
|
|
279
329
|
});
|
|
280
330
|
|
|
281
|
-
|
|
331
|
+
// `error.code` is what separates a transient
|
|
332
|
+
// `409 idempotency_in_progress` from every other (permanent) 409;
|
|
333
|
+
// see `IN_PROGRESS_CODE`. The key on the wire is unchanged across
|
|
334
|
+
// attempts, so the retry replays rather than re-charges.
|
|
335
|
+
if (!shouldRetry(response.status, attempt, this.retryPolicy, retryAfterMs, error.code)) {
|
|
282
336
|
throw error;
|
|
283
337
|
}
|
|
284
338
|
lastError = error;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1
|
|
1
|
+
export const VERSION = "0.2.1";
|