@bitavu/client 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bitavu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # Bitavu for Node.js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Generate PDF invoices, reports, and other documents from a saved [Bitavu](https://bitavu.com) template and JSON data. The client includes TypeScript declarations and runs on Node.js 22.13 or newer in the Node.js 22, 24, and 26 release lines. It has no runtime dependencies.
4
+
5
+ ```sh
6
+ npm install @bitavu/client
7
+ ```
8
+
9
+ ## Render a PDF
10
+
11
+ In [Bitavu Studio](https://studio.bitavu.com/), save a template revision. Use **Integrations → Prepare a template request** to get its ID, revision, and example JSON. Create an API key with **Generate PDFs** permission for that template.
12
+
13
+ Load the key from your application's server environment. Keep it in an ignored local environment file during development and in your hosting provider's secret store in production.
14
+
15
+ ```ts
16
+ import { randomUUID } from 'node:crypto';
17
+ import { writeFile } from 'node:fs/promises';
18
+ import { BitavuClient } from '@bitavu/client';
19
+
20
+ const apiKey = process.env.BITAVU_API_KEY;
21
+ if (!apiKey) throw new Error('Configure BITAVU_API_KEY.');
22
+
23
+ const client = new BitavuClient({ apiKey });
24
+ const operationId = randomUUID();
25
+
26
+ const pdf = await client.renderPdf({
27
+ templateId: 'your-template-id',
28
+ revision: 1,
29
+ data: {
30
+ invoice: { number: 'INV-1001' },
31
+ items: [{ description: 'Design consultation', quantity: 1, unitPrice: 240 }],
32
+ },
33
+ idempotencyKey: operationId,
34
+ });
35
+
36
+ await writeFile('invoice.pdf', pdf.bytes);
37
+ console.log('Rendered revision', pdf.revision);
38
+ ```
39
+
40
+ Replace the template ID, revision, and data with the values for your template. The client preserves the property names in your JSON. It returns the PDF bytes as a `Uint8Array` and includes the resolved revision, page count, and request ID when the API supplies them.
41
+
42
+ ## Choose a revision
43
+
44
+ Set `revision` to a number to render that saved revision. Set `revision: 'latest-published'` and pass `baseRevision` to follow the current publication while checking compatibility against a revision you tested. If you omit `baseRevision`, Bitavu compares the current publication against the first published revision.
45
+
46
+ ```ts
47
+ const pdf = await client.renderPdf({
48
+ templateId,
49
+ revision: 'latest-published',
50
+ baseRevision: testedRevision,
51
+ data,
52
+ idempotencyKey: operationId,
53
+ }, { signal });
54
+ ```
55
+
56
+ If the published revision is incompatible, the API returns HTTP 409 with `JSON_COMPATIBILITY`. If there is no publication, it returns HTTP 404 with `NOT_PUBLISHED`. You can pass `mappingId` in either mode when you use a mapping saved in Studio.
57
+
58
+ ## Read templates and save examples
59
+
60
+ | Method | Returns |
61
+ | --- | --- |
62
+ | `checkCredentials()` | Completes if the key is valid. |
63
+ | `listTemplates()` | The accessible templates, including each folder path, latest saved revision, and Studio link. |
64
+ | `getTemplateVersions(templateId, { page, pageSize, revision })` | A page of saved-version summaries with the latest revision and an optional selected revision. |
65
+ | `getTemplate(templateId, revision)` | A saved revision's Studio link, data bindings, example JSON, and input review. |
66
+ | `saveTemplateExample({ templateId, expectedRevision, exampleJson })` | The new saved revision and its updated input review. |
67
+
68
+ Template reads require **Read templates**. Saving examples requires **Save template examples**. Use the latest saved revision as `expectedRevision`. On a conflict, fetch the latest revision and check its bindings before deciding whether to save again.
69
+
70
+ Fixed response fields are typed. Template data can be your own TypeScript interface. When you call `getTemplate<YourData>(…)`, the type argument describes the JSON shape you expect. Validate the saved example in your app if its shape is uncertain.
71
+
72
+ ```ts
73
+ interface InvoiceData {
74
+ invoice: { number: string };
75
+ items: { description: string; quantity: number; unitPrice: number }[];
76
+ }
77
+
78
+ const { exampleJson, inputReview, studioUrl } =
79
+ await client.getTemplate<InvoiceData>(templateId, testedRevision);
80
+ ```
81
+
82
+ `inputReview.missing` lists bindings absent from the saved example. `inputReview.unused` lists paths the template does not use. Both `exampleJson` and `inputReview` are `null` if the revision has no saved example.
83
+
84
+ ## Errors and retries
85
+
86
+ `BitavuApiError` contains the HTTP status, API code, request ID, and retry delay when available. `BitavuTransportError` identifies timeouts and network failures. Pass an `AbortSignal` to cancel a request.
87
+
88
+ A request uses a deadline of two minutes by default. The caller controls retries. For the same render, reuse the operation ID with the same template, revision selection, and data. Respect `retryAfterMs` if returned. Use a new operation ID for a different render.
89
+
90
+ ## Node.js and Next.js guide
91
+
92
+ Follow the [Bitavu Node.js and Next.js guide](https://bitavu.com/support/nodejs) to generate an invoice PDF with the Node.js client and return it as a download from a Next.js App Router Route Handler.
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,17 @@
1
+ import type { BitavuClientOptions, JsonObject, RenderedPdf, RenderPdfInput, RequestOptions, SaveTemplateExampleInput, TemplateCatalog, TemplateRevision, TemplateVersionOptions, TemplateVersionPage } from './types.js';
2
+ export declare class BitavuClient {
3
+ #private;
4
+ constructor({ apiKey, apiOrigin, timeoutMs, fetch }: BitavuClientOptions);
5
+ /** Checks authentication without reading templates or using render credits. */
6
+ checkCredentials(options?: RequestOptions): Promise<void>;
7
+ /** Lists accessible templates and their latest saved revisions. */
8
+ listTemplates(options?: RequestOptions): Promise<TemplateCatalog>;
9
+ /** Returns metadata, bindings and the saved example for the selected revision. */
10
+ getTemplate<TExample extends object = JsonObject>(templateId: string, revision: number, options?: RequestOptions): Promise<TemplateRevision<TExample>>;
11
+ /** Returns a page of saved-version summaries, newest first. */
12
+ getTemplateVersions(templateId: string, { page, pageSize, revision, signal }?: TemplateVersionOptions): Promise<TemplateVersionPage>;
13
+ /** Saves example data as a new revision, using the latest saved revision as a precondition. */
14
+ saveTemplateExample<TExample extends object>(input: SaveTemplateExampleInput<TExample>, options?: RequestOptions): Promise<TemplateRevision<TExample>>;
15
+ /** Renders a PDF using an explicit revision or the latest compatible publication. */
16
+ renderPdf<TData extends object>(input: RenderPdfInput<TData>, options?: RequestOptions): Promise<RenderedPdf>;
17
+ }
package/dist/client.js ADDED
@@ -0,0 +1,255 @@
1
+ import { isCatalog, isRecord, isTemplateRevision, isVersionPage } from './contracts.js';
2
+ import { BitavuApiError, BitavuTransportError } from './errors.js';
3
+ const productionOrigin = 'https://api.bitavu.com';
4
+ const stagingOrigin = 'https://api-staging.bitavu.com';
5
+ const maxErrorBytes = 65_536;
6
+ const pdfSignature = [0x25, 0x50, 0x44, 0x46, 0x2d]; // %PDF-
7
+ export class BitavuClient {
8
+ #apiKey;
9
+ #apiOrigin;
10
+ #timeoutMs;
11
+ #fetch;
12
+ constructor({ apiKey, apiOrigin = productionOrigin, timeoutMs = 120_000, fetch = globalThis.fetch }) {
13
+ if (typeof apiKey !== 'string' || !/^[A-Za-z0-9_.~+/-]+=*(?![\s\S])/.test(apiKey)) {
14
+ throw new TypeError('Provide a valid Bitavu API key.');
15
+ }
16
+ if (apiOrigin !== productionOrigin && apiOrigin !== stagingOrigin) {
17
+ throw new TypeError('Use a supported Bitavu API origin.');
18
+ }
19
+ assertInteger(timeoutMs, 'timeoutMs', 2_147_483_647);
20
+ this.#apiKey = apiKey;
21
+ this.#apiOrigin = apiOrigin;
22
+ this.#timeoutMs = timeoutMs;
23
+ this.#fetch = fetch;
24
+ }
25
+ /** Checks authentication without reading templates or using render credits. */
26
+ async checkCredentials(options = {}) {
27
+ await this.#json('/v1/credentials/check', options, isAuthenticated);
28
+ }
29
+ /** Lists accessible templates and their latest saved revisions. */
30
+ listTemplates(options = {}) {
31
+ return this.#json('/v1/templates', options, isCatalog);
32
+ }
33
+ /** Returns metadata, bindings and the saved example for the selected revision. */
34
+ async getTemplate(templateId, revision, options = {}) {
35
+ assertInteger(revision, 'revision');
36
+ const result = await this.#json(templatePath(templateId) + '?revision=' + revision, options, isTemplateRevision);
37
+ return result;
38
+ }
39
+ /** Returns a page of saved-version summaries, newest first. */
40
+ async getTemplateVersions(templateId, { page = 1, pageSize = 10, revision, signal } = {}) {
41
+ assertInteger(page, 'page', 10_000);
42
+ assertInteger(pageSize, 'pageSize', 50);
43
+ const query = new URLSearchParams({ summary: 'true', page: String(page), pageSize: String(pageSize) });
44
+ if (revision !== undefined) {
45
+ assertInteger(revision, 'revision');
46
+ query.set('revision', String(revision));
47
+ }
48
+ return this.#json(templatePath(templateId) + '/versions?' + query, signal ? { signal } : {}, isVersionPage);
49
+ }
50
+ /** Saves example data as a new revision, using the latest saved revision as a precondition. */
51
+ async saveTemplateExample(input, options = {}) {
52
+ const path = templatePath(input.templateId) + '/example';
53
+ assertInteger(input.expectedRevision, 'expectedRevision', Number.MAX_SAFE_INTEGER - 1);
54
+ const exampleJson = serializeObject(input.exampleJson, 'exampleJson');
55
+ const result = await this.#json(path, options, isTemplateRevision, {
56
+ expectedRevision: input.expectedRevision,
57
+ exampleJson,
58
+ });
59
+ return result;
60
+ }
61
+ /** Renders a PDF using an explicit revision or the latest compatible publication. */
62
+ async renderPdf(input, options = {}) {
63
+ templatePath(input.templateId);
64
+ if (input.revision === 'latest-published') {
65
+ if (input.baseRevision !== undefined)
66
+ assertInteger(input.baseRevision, 'baseRevision');
67
+ }
68
+ else {
69
+ assertInteger(input.revision, 'revision');
70
+ if (input.baseRevision !== undefined) {
71
+ throw new TypeError('baseRevision is only valid with latest-published.');
72
+ }
73
+ }
74
+ if (typeof input.idempotencyKey !== 'string'
75
+ || !/^[A-Za-z0-9_-]{16,128}(?![\s\S])/.test(input.idempotencyKey)) {
76
+ throw new TypeError('Use 16 to 128 ASCII letters, digits, underscores or hyphens for idempotencyKey.');
77
+ }
78
+ if (input.mappingId !== undefined && (typeof input.mappingId !== 'string' || !input.mappingId.trim())) {
79
+ throw new TypeError('mappingId must be a non-empty string.');
80
+ }
81
+ const data = serializeObject(input.data, 'data');
82
+ return this.#request('/v1/render', options, 'application/pdf', async (response) => {
83
+ if (!hasMediaType(response, 'application/pdf'))
84
+ throw this.#unexpectedResponse(response);
85
+ const bytes = new Uint8Array(await response.arrayBuffer());
86
+ if (!pdfSignature.every((byte, index) => bytes[index] === byte)) {
87
+ throw this.#unexpectedResponse(response);
88
+ }
89
+ return {
90
+ bytes,
91
+ revision: positiveHeader(response, 'X-Bitavu-Revision'),
92
+ pageCount: positiveHeader(response, 'X-Bitavu-Pages'),
93
+ requestId: this.#requestId(response),
94
+ };
95
+ }, {
96
+ templateId: input.templateId,
97
+ revision: input.revision,
98
+ baseRevision: input.baseRevision,
99
+ mappingId: input.mappingId,
100
+ data,
101
+ }, input.idempotencyKey);
102
+ }
103
+ #json(path, options, isExpected, body) {
104
+ return this.#request(path, options, 'application/json', async (response) => {
105
+ if (!hasMediaType(response, 'application/json'))
106
+ throw this.#unexpectedResponse(response);
107
+ const text = await response.text();
108
+ let value;
109
+ try {
110
+ value = JSON.parse(text);
111
+ }
112
+ catch {
113
+ throw this.#unexpectedResponse(response);
114
+ }
115
+ if (!isExpected(value))
116
+ throw this.#unexpectedResponse(response);
117
+ return value;
118
+ }, body);
119
+ }
120
+ async #request(path, { signal }, accept, read, body, idempotencyKey) {
121
+ signal?.throwIfAborted();
122
+ const deadline = AbortSignal.timeout(this.#timeoutMs);
123
+ const headers = new Headers({ Accept: accept, Authorization: 'Bearer ' + this.#apiKey });
124
+ if (idempotencyKey !== undefined)
125
+ headers.set('Idempotency-Key', idempotencyKey);
126
+ const init = {
127
+ method: 'GET',
128
+ headers,
129
+ redirect: 'error',
130
+ cache: 'no-store',
131
+ signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
132
+ };
133
+ if (body !== undefined) {
134
+ init.method = 'POST';
135
+ headers.set('Content-Type', 'application/json');
136
+ init.body = JSON.stringify(body);
137
+ }
138
+ let response;
139
+ try {
140
+ response = await this.#fetch(this.#apiOrigin + path, init);
141
+ if (!response.ok) {
142
+ const retryAfterMs = parseRetryAfter(response);
143
+ throw new BitavuApiError({
144
+ status: response.status,
145
+ code: await readErrorCode(response, this.#apiKey),
146
+ requestId: this.#requestId(response),
147
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
148
+ });
149
+ }
150
+ return await read(response);
151
+ }
152
+ catch (error) {
153
+ signal?.throwIfAborted();
154
+ if (deadline.aborted)
155
+ throw new BitavuTransportError('TIMEOUT');
156
+ if (error instanceof BitavuApiError)
157
+ throw error;
158
+ throw new BitavuTransportError('NETWORK_ERROR');
159
+ }
160
+ finally {
161
+ if (!response?.bodyUsed)
162
+ void response?.body?.cancel().catch(() => { });
163
+ }
164
+ }
165
+ #requestId(response) {
166
+ const value = response.headers.get('X-Bitavu-Request-Id');
167
+ return value && value !== this.#apiKey
168
+ && /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value) ? value : null;
169
+ }
170
+ #unexpectedResponse(response) {
171
+ return new BitavuApiError({
172
+ status: response.status, code: null, requestId: this.#requestId(response), invalidResponse: true,
173
+ });
174
+ }
175
+ }
176
+ function templatePath(id) {
177
+ if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,80}(?![\s\S])/.test(id)) {
178
+ throw new TypeError('Use 1 to 80 ASCII letters, digits, underscores or hyphens for templateId.');
179
+ }
180
+ return '/v1/templates/' + id;
181
+ }
182
+ function assertInteger(value, name, maximum = Number.MAX_SAFE_INTEGER) {
183
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
184
+ throw new RangeError(name + ' must be an integer from 1 to ' + maximum + '.');
185
+ }
186
+ }
187
+ function serializeObject(value, name) {
188
+ let serialized;
189
+ if (isRecord(value)) {
190
+ try {
191
+ serialized = JSON.parse(JSON.stringify(value));
192
+ }
193
+ catch {
194
+ // Use a fixed message even when a user-defined serializer throws.
195
+ }
196
+ }
197
+ if (!isRecord(serialized))
198
+ throw new TypeError(name + ' must be serializable as a JSON object.');
199
+ return serialized;
200
+ }
201
+ function isAuthenticated(value) {
202
+ return isRecord(value) && value.authenticated === true;
203
+ }
204
+ function hasMediaType(response, expected) {
205
+ return response.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase() === expected;
206
+ }
207
+ function positiveHeader(response, name) {
208
+ const value = response.headers.get(name);
209
+ if (!value || !/^\d+$/.test(value))
210
+ return null;
211
+ const number = Number(value);
212
+ return Number.isSafeInteger(number) && number > 0 ? number : null;
213
+ }
214
+ function parseRetryAfter(response) {
215
+ const value = response.headers.get('Retry-After');
216
+ if (!value)
217
+ return undefined;
218
+ const ms = /^\d+$/.test(value) ? Number(value) * 1000 : Math.max(0, Date.parse(value) - Date.now());
219
+ return Number.isSafeInteger(ms) ? ms : undefined;
220
+ }
221
+ async function readErrorCode(response, apiKey) {
222
+ if (!hasMediaType(response, 'application/json') || !response.body)
223
+ return null;
224
+ const length = response.headers.get('Content-Length');
225
+ if (length && Number(length) > maxErrorBytes)
226
+ return null;
227
+ const reader = response.body.getReader();
228
+ const decoder = new TextDecoder();
229
+ let size = 0;
230
+ let text = '';
231
+ try {
232
+ while (true) {
233
+ const chunk = await reader.read();
234
+ if (chunk.done)
235
+ break;
236
+ size += chunk.value.byteLength;
237
+ if (size > maxErrorBytes) {
238
+ void reader.cancel().catch(() => { });
239
+ return null;
240
+ }
241
+ text += decoder.decode(chunk.value, { stream: true });
242
+ }
243
+ }
244
+ finally {
245
+ reader.releaseLock();
246
+ }
247
+ try {
248
+ const value = JSON.parse(text + decoder.decode());
249
+ const code = isRecord(value) ? value.code : undefined;
250
+ return typeof code === 'string' && code !== apiKey && /^[A-Z][A-Z0-9_]{1,63}$/.test(code) ? code : null;
251
+ }
252
+ catch {
253
+ return null;
254
+ }
255
+ }
@@ -0,0 +1,5 @@
1
+ import type { TemplateCatalog, TemplateRevision, TemplateVersionPage } from './types.js';
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ export declare function isCatalog(value: unknown): value is TemplateCatalog;
4
+ export declare function isVersionPage(value: unknown): value is TemplateVersionPage;
5
+ export declare function isTemplateRevision(value: unknown): value is TemplateRevision;
@@ -0,0 +1,45 @@
1
+ export function isRecord(value) {
2
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
3
+ }
4
+ function isString(value) {
5
+ return typeof value === 'string';
6
+ }
7
+ function isList(value, check) {
8
+ return Array.isArray(value) && value.every(check);
9
+ }
10
+ function isInteger(value, minimum) {
11
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;
12
+ }
13
+ function isRevisionSummary(value) {
14
+ return isRecord(value)
15
+ && isString(value.id) && isString(value.name) && isString(value.studioUrl)
16
+ && isInteger(value.revision, 1)
17
+ && (value.createdAt === null || isInteger(value.createdAt, 0))
18
+ && (value.updatedAt === null || isInteger(value.updatedAt, 0));
19
+ }
20
+ export function isCatalog(value) {
21
+ return isRecord(value) && isList(value.templates, item => isRecord(item)
22
+ && isString(item.id) && isString(item.name) && isString(item.studioUrl)
23
+ && isList(item.path, isString) && isInteger(item.latestRevision, 1));
24
+ }
25
+ export function isVersionPage(value) {
26
+ return isRecord(value)
27
+ && isList(value.versions, version => isRevisionSummary(version)
28
+ && (version.title === null || isString(version.title)))
29
+ && isRevisionSummary(value.latest)
30
+ && (value.selected === null || isRevisionSummary(value.selected))
31
+ && isInteger(value.page, 1) && isInteger(value.pageSize, 1)
32
+ && isInteger(value.total, 0) && isInteger(value.pages, 0);
33
+ }
34
+ function isMissingInput(value) {
35
+ return isRecord(value) && isString(value.path) && typeof value.required === 'boolean';
36
+ }
37
+ export function isTemplateRevision(value) {
38
+ if (!isRevisionSummary(value))
39
+ return false;
40
+ const review = value.inputReview;
41
+ return isList(value.bindings, item => isMissingInput(item) && isString(item.type))
42
+ && (value.exampleJson === null || isRecord(value.exampleJson))
43
+ && (review === null || (isRecord(review)
44
+ && isList(review.missing, isMissingInput) && isList(review.unused, isString)));
45
+ }
@@ -0,0 +1,19 @@
1
+ export declare class BitavuApiError extends Error {
2
+ readonly status: number;
3
+ readonly code: string | null;
4
+ readonly requestId: string | null;
5
+ readonly retryAfterMs: number | null;
6
+ /** @internal */
7
+ constructor(options: {
8
+ status: number;
9
+ code: string | null;
10
+ requestId: string | null;
11
+ retryAfterMs?: number;
12
+ invalidResponse?: boolean;
13
+ });
14
+ }
15
+ export declare class BitavuTransportError extends Error {
16
+ readonly code: 'TIMEOUT' | 'NETWORK_ERROR';
17
+ /** @internal */
18
+ constructor(code: 'TIMEOUT' | 'NETWORK_ERROR');
19
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,28 @@
1
+ export class BitavuApiError extends Error {
2
+ status;
3
+ code;
4
+ requestId;
5
+ retryAfterMs;
6
+ /** @internal */
7
+ constructor(options) {
8
+ super(options.invalidResponse
9
+ ? 'Bitavu returned an unexpected response.'
10
+ : 'Bitavu API returned HTTP ' + options.status + '.');
11
+ this.name = 'BitavuApiError';
12
+ this.status = options.status;
13
+ this.code = options.code;
14
+ this.requestId = options.requestId;
15
+ this.retryAfterMs = options.retryAfterMs ?? null;
16
+ }
17
+ }
18
+ export class BitavuTransportError extends Error {
19
+ code;
20
+ /** @internal */
21
+ constructor(code) {
22
+ super(code === 'TIMEOUT'
23
+ ? 'The Bitavu request timed out.'
24
+ : 'The Bitavu request could not be completed.');
25
+ this.name = 'BitavuTransportError';
26
+ this.code = code;
27
+ }
28
+ }
@@ -0,0 +1,3 @@
1
+ export { BitavuClient } from './client.js';
2
+ export { BitavuApiError, BitavuTransportError } from './errors.js';
3
+ export type * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { BitavuClient } from './client.js';
2
+ export { BitavuApiError, BitavuTransportError } from './errors.js';
@@ -0,0 +1,99 @@
1
+ export type JsonValue = string | number | boolean | null | JsonObject | readonly JsonValue[];
2
+ export interface JsonObject {
3
+ readonly [key: string]: JsonValue;
4
+ }
5
+ export interface BitavuClientOptions {
6
+ /** An API key loaded by your application from server-side configuration. */
7
+ apiKey: string;
8
+ /** Defaults to the public Bitavu API. Use a key for the selected environment. */
9
+ apiOrigin?: 'https://api.bitavu.com' | 'https://api-staging.bitavu.com';
10
+ /** Deadline for the HTTP request and response body, in milliseconds. Defaults to 120000. */
11
+ timeoutMs?: number;
12
+ /** Custom transport. This function receives the authenticated request. */
13
+ fetch?: typeof fetch;
14
+ }
15
+ export interface RequestOptions {
16
+ signal?: AbortSignal;
17
+ }
18
+ export interface TemplateListItem {
19
+ readonly id: string;
20
+ readonly name: string;
21
+ readonly path: readonly string[];
22
+ readonly latestRevision: number;
23
+ readonly studioUrl: string;
24
+ }
25
+ export interface TemplateCatalog {
26
+ readonly templates: readonly TemplateListItem[];
27
+ }
28
+ export interface TemplateRevisionSummary {
29
+ readonly id: string;
30
+ readonly name: string;
31
+ readonly revision: number;
32
+ /** Unix time in milliseconds. */
33
+ readonly createdAt: number | null;
34
+ /** Unix time in milliseconds. */
35
+ readonly updatedAt: number | null;
36
+ readonly studioUrl: string;
37
+ }
38
+ export interface TemplateVersion extends TemplateRevisionSummary {
39
+ readonly title: string | null;
40
+ }
41
+ export interface TemplateVersionPage {
42
+ readonly versions: readonly TemplateVersion[];
43
+ readonly latest: TemplateRevisionSummary;
44
+ readonly selected: TemplateRevisionSummary | null;
45
+ readonly page: number;
46
+ readonly pageSize: number;
47
+ readonly total: number;
48
+ readonly pages: number;
49
+ }
50
+ export interface TemplateVersionOptions extends RequestOptions {
51
+ page?: number;
52
+ pageSize?: number;
53
+ /** Ask the server to include the summary of this revision, even on another page. */
54
+ revision?: number;
55
+ }
56
+ export interface DataBinding {
57
+ readonly path: string;
58
+ readonly type: string;
59
+ readonly required: boolean;
60
+ }
61
+ export interface MissingInput {
62
+ readonly path: string;
63
+ readonly required: boolean;
64
+ }
65
+ export interface InputReview {
66
+ readonly missing: readonly MissingInput[];
67
+ readonly unused: readonly string[];
68
+ }
69
+ export interface TemplateRevision<TExample extends object = JsonObject> extends TemplateRevisionSummary {
70
+ readonly bindings: readonly DataBinding[];
71
+ /** The JSON shape is determined by the template. A supplied type is a caller assertion. */
72
+ readonly exampleJson: TExample | null;
73
+ readonly inputReview: InputReview | null;
74
+ }
75
+ export interface SaveTemplateExampleInput<TExample extends object = JsonObject> {
76
+ templateId: string;
77
+ expectedRevision: number;
78
+ exampleJson: TExample;
79
+ }
80
+ export type RenderSelection = {
81
+ revision: number;
82
+ baseRevision?: never;
83
+ } | {
84
+ revision: 'latest-published';
85
+ baseRevision?: number;
86
+ };
87
+ export type RenderPdfInput<TData extends object = JsonObject> = RenderSelection & {
88
+ templateId: string;
89
+ data: TData;
90
+ /** Reuse this key only when retrying the same render with the same input. */
91
+ idempotencyKey: string;
92
+ mappingId?: string;
93
+ };
94
+ export interface RenderedPdf {
95
+ readonly bytes: Uint8Array<ArrayBuffer>;
96
+ readonly revision: number | null;
97
+ readonly pageCount: number | null;
98
+ readonly requestId: string | null;
99
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@bitavu/client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Generate PDFs from saved Bitavu visual templates and workflow data",
5
+ "license": "MIT",
6
+ "author": {"name":"bitavu.com","email":"integration+n8n@bitavu.com","url":"https://bitavu.com"},
7
+ "homepage": "https://bitavu.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/bitavu/bitavu-node.git"
11
+ },
12
+ "keywords": ["bitavu", "pdf", "pdf-generation", "invoice", "templates", "typescript"],
13
+ "type": "module",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "node": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": ["dist"],
22
+ "engines": {
23
+ "node": "^22.13.0 || ^24.0.0 || >=26.0.0"
24
+ },
25
+ "publishConfig": {
26
+ "registry": "https://registry.npmjs.org/",
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "test": "npm run build && node --test",
32
+ "prepack": "npm run build",
33
+ "check:package": "npm run build && node scripts/check-package.mjs"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "5.9.3",
37
+ "@types/node": "22.19.19"
38
+ }
39
+ }