@onebooks/node 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OneBooks
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,360 @@
1
- # Temporary Holding Version
1
+ # @onebooks/node
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
+ Server-side SDK for the OneBooks API: an authenticated HTTP client with
4
+ sane retries, OAuth 2.0 helpers (PKCE, code exchange, refresh, RFC 8693
5
+ session-token exchange), ES256 App Bridge session-token verification (via
6
+ JWKS), and webhook signature verification.
7
+
8
+ ESM, TypeScript, Node ≥ 20. One runtime dependency: [`jose`](https://github.com/panva/jose)
9
+ (JWKS fetching + ES256 verification).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @onebooks/node
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```ts
20
+ import { OneBooksClient } from '@onebooks/node';
21
+
22
+ const client = new OneBooksClient({ accessToken: process.env.ONEBOOKS_ACCESS_TOKEN! });
23
+
24
+ const invoice = await client.get(`/invoices/${invoiceId}`);
25
+
26
+ // A write the API de-duplicates by Idempotency-Key — and that the SDK may
27
+ // therefore safely retry after a dropped connection (see "Retries").
28
+ const created = await client.post('/invoices', { customerId, lineItems }, {
29
+ idempotencyKey: crypto.randomUUID(),
30
+ });
31
+
32
+ // List endpoints are page-based and return their own shape:
33
+ const { invoices, total } = await client.get<{ invoices: unknown[]; total: number }>('/invoices', {
34
+ query: { page: 1, limit: 50 },
35
+ });
36
+
37
+ // The event log is cursor-paginated — iterate it without managing cursors:
38
+ for await (const event of client.events.iterate({ types: ['invoice.paid'] })) {
39
+ console.log(event.type, event.resourceId);
40
+ }
41
+ ```
42
+
43
+ ## `OneBooksClient`
44
+
45
+ ```ts
46
+ new OneBooksClient({
47
+ accessToken?: string;
48
+ getAccessToken?: () => string | Promise<string>;
49
+ apiBase?: string; // default "https://api.getonebooks.com"
50
+ apiVersion?: string; // sent as the "OneBooks-Version" header
51
+ maxRetries?: number; // default 3
52
+ fetch?: typeof fetch; // override for tests / custom runtimes
53
+ })
54
+ ```
55
+
56
+ Provide **either** `accessToken` (a static token) **or** `getAccessToken`
57
+ (called before every request — use this if you refresh tokens elsewhere, a
58
+ token store, per-installation tokens, etc). Exactly one is required; the
59
+ constructor throws otherwise.
60
+
61
+ ### `request(method, path, options?)`
62
+
63
+ ```ts
64
+ client.request<T>(method: 'GET'|'POST'|'PUT'|'PATCH'|'DELETE', path: string, options?: {
65
+ query?: Record<string, string | number | boolean | Array<string|number> | null | undefined>;
66
+ body?: unknown;
67
+ idempotencyKey?: string;
68
+ headers?: Record<string, string>;
69
+ signal?: AbortSignal;
70
+ }): Promise<T>
71
+ ```
72
+
73
+ Also exposed as `client.get(path, options?)`, `client.delete(path, options?)`
74
+ and `client.post/put/patch(path, body?, options?)`.
75
+
76
+ ### Retries
77
+
78
+ Up to `maxRetries` (default 3) extra attempts, by this rule:
79
+
80
+ | Failure | Retried for |
81
+ |---|---|
82
+ | `429 Too Many Requests` | **Every method.** A rate-limited request was never processed, so replaying it is always safe. Honors `Retry-After` (seconds or an HTTP date). |
83
+ | Network error (connection reset, timeout, DNS), `502`, `503`, `504` | **`GET`/`HEAD` only — or any method when you pass `idempotencyKey`.** Jittered exponential backoff. |
84
+ | Anything else (`400`, `401`, `403`, `404`, `409`, `422`, `500`, …) | Never. |
85
+
86
+ Why writes are different: after a network error or a gateway `5xx` the API
87
+ may already have processed the request — only the response was lost. Blindly
88
+ re-sending a `POST /customers` would create a second customer. With an
89
+ `idempotencyKey` the SDK re-sends the **same** `Idempotency-Key` header, and
90
+ the API returns the original result instead of acting twice.
91
+
92
+ Only these endpoints de-duplicate on `Idempotency-Key` today:
93
+ `POST /invoices`, `POST /invoices/:id/payments`, `POST /payments`,
94
+ `POST /payments/allocate` and `POST /journal`. Elsewhere the header is
95
+ accepted but ignored — setting a key there makes the SDK retry, yet the
96
+ server cannot de-duplicate, so only set one where it is honored (or where a
97
+ duplicate is harmless). A write without a key fails fast instead: catch the
98
+ error and reconcile (for example, look the record up) before trying again.
99
+
100
+ On exhausting retries, or on any non-retried failure, the promise rejects
101
+ with `OneBooksApiError` (a network error is re-thrown as-is):
102
+
103
+ ```ts
104
+ class OneBooksApiError extends Error {
105
+ status: number;
106
+ code?: string; // machine-readable code; absent for class-validator array messages and OAuth endpoints
107
+ body: unknown; // the full parsed (or raw text) response body
108
+ oauthError?: OAuthErrorCode; // OAuth helpers only: the RFC 6749 `error` code, e.g. "invalid_grant"
109
+ oauthErrorDescription?: string; // OAuth helpers only: `error_description`, when sent
110
+ retryAfter?: number; // seconds, from the response's Retry-After header (every 429 has one)
111
+ }
112
+ ```
113
+
114
+ Branch on `.code`, not `.message` — see the [errors doc](../../developer-web/src/content/docs/errors.mdx).
115
+ Errors from the [OAuth helpers](#errors-from-the-oauth-helpers) carry
116
+ `.oauthError` instead.
117
+
118
+ ### `paginate(path, query?, options?)`
119
+
120
+ ```ts
121
+ for await (const event of client.paginate<OneBooksEventEnvelope>('/events', { types: 'invoice.paid' }, { cursorParam: 'after' })) { ... }
122
+ ```
123
+
124
+ Async-iterates every item of a **cursor-paginated** endpoint — one that
125
+ returns `{ data, hasMore, nextCursor }`. `options.cursorParam` names the
126
+ query parameter the endpoint reads the cursor from (default `"cursor"`).
127
+
128
+ Today the only cursor-paginated partner endpoint is `GET /events` (cursor
129
+ parameter `after`) — prefer `client.events.iterate()`, which wraps exactly
130
+ this. Resource lists such as `GET /invoices` are **page-based** and return
131
+ their own shape (`{ invoices, total }`, with `page`/`limit` query
132
+ parameters), so `paginate()` does not apply to them; page them with
133
+ `client.get()`.
134
+
135
+ ### `appData` — Contract 6 partner app-data API
136
+
137
+ ```ts
138
+ await client.appData.definitions(); // -> AppDataDefinition[]
139
+ await client.appData.get({ resourceType: 'INVOICE', resourceId: 'inv_1' }); // -> AppDataValue[]
140
+ await client.appData.get({ resourceType: 'INVOICE', resourceIds: ['a', 'b'] }); // -> AppDataValue[]
141
+ await client.appData.set([{ resourceType: 'INVOICE', resourceId: 'inv_1', key: 'tracking_number', value: '1Z...' }]); // up to 25; value: null deletes
142
+ await client.appData.delete(valueId);
143
+ ```
144
+
145
+ ### `events` — Contract 6 partner events API
146
+
147
+ ```ts
148
+ const page = await client.events.list({ types: ['invoice.paid'], limit: 50 });
149
+ const event = await client.events.get(eventId);
150
+
151
+ for await (const event of client.events.iterate({ after: lastSeenEventId })) {
152
+ // process, then persist event.id as your new "after" cursor
153
+ }
154
+ ```
155
+
156
+ ## OAuth helpers (`oauth`)
157
+
158
+ Standalone functions — you need these *before* you have a client/token, so
159
+ they don't hang off `OneBooksClient`:
160
+
161
+ ```ts
162
+ import { oauth } from '@onebooks/node';
163
+
164
+ const { codeVerifier, codeChallenge } = await oauth.createPkcePair();
165
+
166
+ const authorizeUrl = oauth.buildAuthorizeUrl({
167
+ clientId, redirectUri, scopes: ['invoices:read'], state, codeChallenge,
168
+ });
169
+ // -> redirect the user's browser here
170
+
171
+ // ... on your callback route:
172
+ const tokens = await oauth.exchangeCode({
173
+ clientId, clientSecret /* confidential only */, code, redirectUri, codeVerifier,
174
+ });
175
+
176
+ const refreshed = await oauth.refresh({ clientId, clientSecret, refreshToken: tokens.refresh_token! });
177
+
178
+ await oauth.revoke({ clientId, clientSecret, token: tokens.refresh_token!, tokenTypeHint: 'refresh_token' });
179
+
180
+ const introspection = await oauth.introspect({ clientId, clientSecret, token: tokens.access_token });
181
+ ```
182
+
183
+ A **public** (PKCE) client omits `clientSecret` everywhere above — the SDK
184
+ sends `client_id` in the request body instead of an `Authorization: Basic`
185
+ header, matching the [authentication doc](../../developer-web/src/content/docs/authentication.mdx).
186
+
187
+ ### Errors from the OAuth helpers
188
+
189
+ `/oauth/token`, `/oauth/revoke` and `/oauth/introspect` answer errors the
190
+ standard OAuth way (RFC 6749 §5.2, RFC 7009, RFC 7662), not in the OneBooks
191
+ `{ statusCode, message, code }` shape the rest of the API uses:
192
+
193
+ ```json
194
+ { "error": "invalid_grant", "error_description": "refresh token reuse detected" }
195
+ ```
196
+
197
+ Every helper rejects with `OneBooksApiError`, with the code in
198
+ `.oauthError` and the optional description in `.oauthErrorDescription`.
199
+ `.message` reads `"invalid_grant: refresh token reuse detected"` (just the
200
+ code when there is no description), and `.code` stays unset. Branch on
201
+ `.oauthError`:
202
+
203
+ ```ts
204
+ import { OneBooksApiError, oauth } from '@onebooks/node';
205
+
206
+ try {
207
+ tokens = await oauth.refresh({ clientId, clientSecret, refreshToken });
208
+ } catch (err) {
209
+ if (err instanceof OneBooksApiError && err.oauthError === 'invalid_grant') {
210
+ // expired, revoked, reused, or the business uninstalled you: re-authorize
211
+ } else if (err instanceof OneBooksApiError && err.status === 429) {
212
+ // rate limited: wait err.retryAfter seconds before the next token request
213
+ } else {
214
+ throw err;
215
+ }
216
+ }
217
+ ```
218
+
219
+ - `invalid_client` (HTTP `401`) is a configuration problem — a wrong or
220
+ missing secret, or an app that is suspended or deactivated. Fix it rather
221
+ than retrying: repeated failures lock the caller out for a minute.
222
+ - The helpers **don't retry**, a `429` included. Its body is
223
+ `{ "error": "temporarily_unavailable", … }`, and `.retryAfter` holds the
224
+ `Retry-After` seconds to wait.
225
+ - A `5xx` answers `server_error`. A proxy's non-JSON error page still
226
+ rejects with `OneBooksApiError`: `.message` is the HTTP status text and
227
+ `.body` the raw text.
228
+ - The helpers also read the code from `message`, where these endpoints put
229
+ it before 2026-09-23 (`{ "statusCode": 400, "message": "invalid_grant: expired", … }`),
230
+ so `.oauthError` is filled against an older server too.
231
+
232
+ ### `exchangeSessionToken` — Contract 3 (embedded apps)
233
+
234
+ For an app embedded via App Bridge: trade the short-lived session token
235
+ (`app.sessionToken()` on the frontend) for a normal access/refresh token pair
236
+ scoped to that installation. **Confidential clients only.**
237
+
238
+ ```ts
239
+ const tokens = await oauth.exchangeSessionToken({
240
+ clientId: process.env.ONEBOOKS_CLIENT_ID!,
241
+ clientSecret: process.env.ONEBOOKS_CLIENT_SECRET!,
242
+ sessionToken, // from your frontend, e.g. a header your embedded page sent you
243
+ });
244
+ ```
245
+
246
+ **Exchange once, cache the pair; verify — don't exchange — on every
247
+ request.** Each session token can be exchanged exactly **once**: the token
248
+ endpoint records its `jti`, and a second exchange of the same token fails
249
+ with `invalid_grant`. App Bridge deliberately reuses one session token for
250
+ up to ~50 seconds across all of a page's `app.fetch()` calls, so a backend
251
+ that exchanges on every request breaks on its second request. The pattern:
252
+
253
+ ```ts
254
+ const claims = await verifySessionToken(sessionToken, { clientId }); // every request
255
+ const key = `${claims.bid}:${claims.sub}`;
256
+ let pair = await store.get(key);
257
+ if (!pair) {
258
+ pair = await oauth.exchangeSessionToken({ clientId, clientSecret, sessionToken }); // once
259
+ await store.set(key, pair);
260
+ } else if (expiresSoon(pair)) {
261
+ pair = await oauth.refresh({ clientId, clientSecret, refreshToken: pair.refresh_token! });
262
+ await store.set(key, pair);
263
+ }
264
+ // If the refresh fails (tokens revoked — e.g. uninstall, suspension) or the
265
+ // API answers 401, drop the stored pair and exchange a FRESH session token.
266
+ ```
267
+
268
+ Run that exchange/refresh **one at a time per key**: a page's parallel
269
+ requests carry the same session token, so when two of them find no stored
270
+ pair and both exchange it, the second gets `invalid_grant`. Let concurrent
271
+ requests wait for the exchange or refresh already in flight.
272
+
273
+ The starter template (`developer-sdk/templates/embedded-node/lib/session-tokens.mjs`,
274
+ wired up in `lib/onebooks.mjs`) implements exactly this, including the
275
+ fallback and the one-at-a-time rule.
276
+
277
+ ## `verifySessionToken(token, options)`
278
+
279
+ Verifies an App Bridge session token server-side (ES256, JWKS from
280
+ `GET /.well-known/jwks.json`) — use this in **every** backend route your
281
+ embedded app calls, to authenticate the (business, user) making the request
282
+ (verification is local and repeatable; only the exchange is single-use):
283
+
284
+ ```ts
285
+ import { verifySessionToken } from '@onebooks/node';
286
+
287
+ const claims = await verifySessionToken(token, { clientId: process.env.ONEBOOKS_CLIENT_ID! });
288
+ // claims: { iss, aud, sub (userId), bid (businessId), role, locale, jti, iat, nbf, exp, typ }
289
+ ```
290
+
291
+ Checks performed: ES256 signature against the live JWKS (fetched once,
292
+ cached, and refreshed automatically by `jose` on a `kid` it hasn't seen),
293
+ `aud === clientId`, `iss === apiBase`'s origin, `exp`/`nbf` within
294
+ `clockToleranceSec` (default 30s), and the `typ` claim equals
295
+ `"onebooks.app_session"` — a token minted for some other purpose but
296
+ sharing the same signing keys must not be accepted here. Throws (a `jose`
297
+ error, e.g. `JWTExpired`) on any failure.
298
+
299
+ Options: `{ clientId, apiBase? (default the OneBooks API), jwksUrl? (default
300
+ "<apiBase origin>/.well-known/jwks.json"), clockToleranceSec? (default 30) }`.
301
+
302
+ ## `verifyWebhook(options)`
303
+
304
+ ```ts
305
+ import { verifyWebhook } from '@onebooks/node';
306
+ import { createServer } from 'node:http';
307
+
308
+ const server = createServer(async (req, res) => {
309
+ const chunks: Buffer[] = [];
310
+ for await (const chunk of req) chunks.push(chunk as Buffer);
311
+ const rawBody = Buffer.concat(chunks); // MUST verify against the raw bytes, not a parsed/re-serialized body
312
+
313
+ try {
314
+ const event = verifyWebhook({
315
+ rawBody,
316
+ signatureHeader: req.headers['x-webhook-signature'] as string,
317
+ secrets: [process.env.ONEBOOKS_WEBHOOK_SECRET!], // pass [new, old] while rotating
318
+ });
319
+ // event: { id, eventId, type, apiVersion, businessId, created, data }
320
+ res.writeHead(200).end();
321
+ void handle(event);
322
+ } catch {
323
+ res.writeHead(400).end();
324
+ }
325
+ });
326
+ ```
327
+
328
+ Verifies `X-Webhook-Signature: t=<unix>,v1=<hex>[,v1=<hex>]` — HMAC-SHA256 of
329
+ `` `${t}.${rawBody}` `` — with a constant-time comparison, and rejects a
330
+ timestamp older than `toleranceSec` (default 300) to blunt replay of a
331
+ captured delivery. **Accepts the delivery if ANY provided secret matches ANY
332
+ `v1` value**, so passing both your current and previous secret during a
333
+ rotation window (`secrets: [current, previous]`) verifies deliveries signed
334
+ with either. Throws on any failure; on success, returns the parsed event —
335
+ never a boolean, so a caller can't accidentally ignore a failed check.
336
+
337
+ `parseWebhookEvent(rawBody)` parses the envelope **without** verifying
338
+ anything — only use it on a body you already ran through `verifyWebhook()`.
339
+
340
+ ## Development
341
+
342
+ ```bash
343
+ npm install
344
+ npm run build # tsc -> dist/
345
+ npm test # node:test via tsx — no network, no real timers beyond real retry backoff
346
+ npm run typecheck
347
+ ```
348
+
349
+ Tests cover: webhook verification (valid / tampered / stale timestamp /
350
+ secret rotation with two `v1` values), session-token verification against a
351
+ locally generated ES256 key and a mocked JWKS fetch (aud mismatch, expired,
352
+ wrong `typ`, unknown signing key, clock tolerance) — **no real network
353
+ call** — the client's retry rule against a fake `fetch` (including that
354
+ a write without an idempotency key is never replayed), and the OAuth
355
+ helpers' error parsing for both the RFC 6749 body and an older server's
356
+ `message` body, `Retry-After` on a `429`, and a non-JSON gateway page.
357
+
358
+ ## License
359
+
360
+ MIT
@@ -0,0 +1,107 @@
1
+ import type { QueryParams } from './types.js';
2
+ interface RequestClient {
3
+ get<T>(path: string, options?: {
4
+ query?: QueryParams;
5
+ }): Promise<T>;
6
+ put<T>(path: string, body?: unknown): Promise<T>;
7
+ delete<T>(path: string): Promise<T>;
8
+ }
9
+ /** `en` is required; other locales are filled in as the app's listing supports them. */
10
+ export interface LocalizedText {
11
+ en: string;
12
+ [locale: string]: string | undefined;
13
+ }
14
+ /** Matches Prisma's `AppDataResourceType` enum exactly (`schema.prisma`). */
15
+ export type AppDataResourceType = 'BUSINESS' | 'CUSTOMER' | 'SUPPLIER' | 'ITEM' | 'INVOICE' | 'QUOTE' | 'SALES_RETURN' | 'PURCHASE' | 'PURCHASE_RETURN' | 'PAYMENT' | 'SUPPLIER_PAYMENT' | 'EXPENSE';
16
+ /** Matches Prisma's `AppDataType` enum exactly (`schema.prisma`). */
17
+ export type AppDataFieldType = 'TEXT' | 'MULTILINE_TEXT' | 'INTEGER' | 'DECIMAL' | 'BOOLEAN' | 'DATE' | 'DATETIME' | 'URL' | 'CHOICE' | 'JSON';
18
+ /** Matches Prisma's `AppDataUpdatedVia` enum exactly. */
19
+ export type AppDataUpdatedVia = 'APP' | 'MERCHANT';
20
+ export interface AppDataValidationRules {
21
+ /** TEXT / MULTILINE_TEXT only. */
22
+ maxLength?: number;
23
+ /** INTEGER / DECIMAL only. */
24
+ min?: number;
25
+ /** INTEGER / DECIMAL only. */
26
+ max?: number;
27
+ }
28
+ /**
29
+ * A field your app has declared via the developer console (Contract 6:
30
+ * `GET /app-data/definitions`).
31
+ *
32
+ * Server-side validation per `type` (`app-data-values.ts` — informational;
33
+ * this SDK does not re-validate client-side, the server is authoritative):
34
+ * `TEXT` — string, no line breaks, ≤ `validation.maxLength` (default 255).
35
+ * `MULTILINE_TEXT` — string, ≤ `validation.maxLength` (default 5000).
36
+ * `INTEGER` — a safe integer, within `validation.min`/`max`.
37
+ * `DECIMAL` — a finite number with ≤ 6 decimal places, within `validation.min`/`max`.
38
+ * `BOOLEAN` — `true`/`false`.
39
+ * `DATE` — `"YYYY-MM-DD"`, a real calendar date.
40
+ * `DATETIME` — an ISO-8601 date-time string; stored normalized to `Date#toISOString()`.
41
+ * `URL` — an absolute **https** URL, ≤ 2048 characters.
42
+ * `CHOICE` — a string that is one of this definition's `choices`.
43
+ * `JSON` — an object or array that serializes to ≤ 16 KB.
44
+ * In every case, `value: null` deletes the stored value regardless of type.
45
+ */
46
+ export interface AppDataDefinition {
47
+ id: string;
48
+ resourceType: AppDataResourceType;
49
+ key: string;
50
+ type: AppDataFieldType;
51
+ name: LocalizedText;
52
+ description: LocalizedText | null;
53
+ choices: string[];
54
+ validation: AppDataValidationRules | null;
55
+ merchantVisible: boolean;
56
+ merchantEditable: boolean;
57
+ position: number;
58
+ createdAt: string;
59
+ updatedAt: string;
60
+ }
61
+ export interface AppDataValue {
62
+ /** Null when this entry is the echo of a delete (a `set()` call with `value: null`). */
63
+ id: string | null;
64
+ resourceType: AppDataResourceType;
65
+ resourceId: string;
66
+ key: string;
67
+ value: unknown;
68
+ updatedVia: AppDataUpdatedVia;
69
+ updatedAt: string;
70
+ }
71
+ export interface AppDataGetParams {
72
+ resourceType: AppDataResourceType;
73
+ resourceId?: string;
74
+ /** At most 50. */
75
+ resourceIds?: string[];
76
+ }
77
+ export interface AppDataSetInput {
78
+ resourceType: AppDataResourceType;
79
+ /** At most 255 characters. */
80
+ resourceId: string;
81
+ /** Must match an existing definition key for this app + resourceType (`/^[a-z][a-z0-9_]{0,63}$/`). */
82
+ key: string;
83
+ /** The typed value (shape depends on the field's type — see {@link AppDataDefinition}). `null` deletes the value. */
84
+ value: unknown;
85
+ }
86
+ /** Partner app-data API (Contract 6): `client.appData.*`. */
87
+ export declare class AppDataResource {
88
+ private readonly client;
89
+ constructor(client: RequestClient);
90
+ /** `GET /app-data/definitions` — every field your app has declared. */
91
+ definitions(): Promise<AppDataDefinition[]>;
92
+ /**
93
+ * `GET /app-data?resourceType=…&resourceId=…` (or `&resourceIds=a,b`, at
94
+ * most 50). Provide exactly one of `resourceId` / `resourceIds`.
95
+ */
96
+ get(params: AppDataGetParams): Promise<AppDataValue[]>;
97
+ /**
98
+ * `PUT /app-data { values }` — 1 to 25 per call (the server rejects an
99
+ * empty array; this SDK short-circuits to `[]` instead of making that
100
+ * call); `value: null` deletes that value.
101
+ */
102
+ set(values: AppDataSetInput[]): Promise<AppDataValue[]>;
103
+ /** `DELETE /app-data/:id`. */
104
+ delete(id: string): Promise<void>;
105
+ }
106
+ export {};
107
+ //# sourceMappingURL=app-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-data.d.ts","sourceRoot":"","sources":["../src/app-data.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ const MAX_SET_BATCH = 25;
2
+ /** Partner app-data API (Contract 6): `client.appData.*`. */
3
+ export class AppDataResource {
4
+ client;
5
+ constructor(client) {
6
+ this.client = client;
7
+ }
8
+ /** `GET /app-data/definitions` — every field your app has declared. */
9
+ async definitions() {
10
+ const res = await this.client.get('/app-data/definitions');
11
+ return res.data;
12
+ }
13
+ /**
14
+ * `GET /app-data?resourceType=…&resourceId=…` (or `&resourceIds=a,b`, at
15
+ * most 50). Provide exactly one of `resourceId` / `resourceIds`.
16
+ */
17
+ async get(params) {
18
+ if (!params.resourceId && !params.resourceIds) {
19
+ throw new Error('appData.get(): provide either "resourceId" or "resourceIds".');
20
+ }
21
+ const query = { resourceType: params.resourceType };
22
+ if (params.resourceId)
23
+ query.resourceId = params.resourceId;
24
+ if (params.resourceIds)
25
+ query.resourceIds = params.resourceIds.join(',');
26
+ const res = await this.client.get('/app-data', { query });
27
+ return res.data;
28
+ }
29
+ /**
30
+ * `PUT /app-data { values }` — 1 to 25 per call (the server rejects an
31
+ * empty array; this SDK short-circuits to `[]` instead of making that
32
+ * call); `value: null` deletes that value.
33
+ */
34
+ async set(values) {
35
+ if (values.length === 0)
36
+ return [];
37
+ if (values.length > MAX_SET_BATCH) {
38
+ throw new RangeError(`appData.set(): at most ${MAX_SET_BATCH} values per call (got ${values.length}).`);
39
+ }
40
+ const res = await this.client.put('/app-data', { values });
41
+ return res.data;
42
+ }
43
+ /** `DELETE /app-data/:id`. */
44
+ async delete(id) {
45
+ await this.client.delete(`/app-data/${encodeURIComponent(id)}`);
46
+ }
47
+ }
48
+ //# sourceMappingURL=app-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-data.js","sourceRoot":"","sources":["../src/app-data.ts"],"names":[],"mappings":"AAuHA,MAAM,aAAa,GAAG,EAAE,CAAC;AAEzB,6DAA6D;AAC7D,MAAM,OAAO,eAAe;IACG;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAEtD,uEAAuE;IACvE,KAAK,CAAC,WAAW;QACf,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAgC,uBAAuB,CAAC,CAAC;QAC1F,OAAO,GAAG,CAAC,IAAI,CAAC;IAClB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAC,MAAwB;QAChC,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;QAClF,CAAC;QACD,MAAM,KAAK,GAAgB,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC;QACjE,IAAI,MAAM,CAAC,UAAU;YAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QAC5D,IAAI,MAAM,CAAC,WAAW;YAAE,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAA2B,WAAW,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,OAAO,GAAG,CAAC,IAAI,CAAC;IAClB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAG,CAAC,MAAyB;QACjC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACnC,IAAI,MAAM,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;YAClC,MAAM,IAAI,UAAU,CAAC,0BAA0B,aAAa,yBAAyB,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QAC1G,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAA2B,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACrF,OAAO,GAAG,CAAC,IAAI,CAAC;IAClB,CAAC;IAED,8BAA8B;IAC9B,KAAK,CAAC,MAAM,CAAC,EAAU;QACrB,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAO,aAAa,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;CACF"}
@@ -0,0 +1,58 @@
1
+ import { AppDataResource } from './app-data.js';
2
+ import { EventsResource } from './events.js';
3
+ import type { HttpMethod, OneBooksClientOptions, QueryParams, RequestOptions } from './types.js';
4
+ /**
5
+ * The retry rule, in one place:
6
+ * - `429` is retried for EVERY method — a rate-limited request was never
7
+ * processed, so replaying it can't duplicate anything.
8
+ * - A network error or `502`/`503`/`504` is retried only for `GET`/`HEAD`, or
9
+ * for any other method when the call set an `idempotencyKey` — otherwise a
10
+ * POST that the API processed before the connection dropped would be sent
11
+ * twice (a duplicate customer, expense, quote, …).
12
+ * - Nothing else is ever retried.
13
+ */
14
+ export declare function canReplayAfterAmbiguousFailure(method: HttpMethod, idempotencyKey: string | undefined): boolean;
15
+ /**
16
+ * Server-side OneBooks API client: authenticated `request()` with retries,
17
+ * plus `appData`/`events` resource helpers. OAuth flow helpers (PKCE, code
18
+ * exchange, session-token exchange, …) are standalone — see `oauth.ts` — since
19
+ * you need them before a client (and its token) exists.
20
+ */
21
+ export declare class OneBooksClient {
22
+ readonly apiBase: string;
23
+ readonly apiVersion?: string;
24
+ readonly appData: AppDataResource;
25
+ readonly events: EventsResource;
26
+ private readonly maxRetries;
27
+ private readonly fetchImpl;
28
+ private readonly staticToken?;
29
+ private readonly getAccessTokenFn?;
30
+ constructor(options: OneBooksClientOptions);
31
+ private resolveAccessToken;
32
+ private buildUrl;
33
+ /**
34
+ * Issues one authenticated API request. Retries (up to `maxRetries`) a
35
+ * `429` for any method, honoring `Retry-After`; retries a network error or
36
+ * `502`/`503`/`504` (jittered backoff) only for `GET`/`HEAD` or when
37
+ * `options.idempotencyKey` is set — see `canReplayAfterAmbiguousFailure`.
38
+ * Never retries any other status. Resolves the parsed JSON body (or
39
+ * `undefined` for a `204`); throws `OneBooksApiError` on a non-retried
40
+ * failure (or rethrows the network error).
41
+ */
42
+ request<T = unknown>(method: HttpMethod, path: string, options?: RequestOptions): Promise<T>;
43
+ get<T = unknown>(path: string, options?: Omit<RequestOptions, 'body'>): Promise<T>;
44
+ post<T = unknown>(path: string, body?: unknown, options?: Omit<RequestOptions, 'body'>): Promise<T>;
45
+ put<T = unknown>(path: string, body?: unknown, options?: Omit<RequestOptions, 'body'>): Promise<T>;
46
+ patch<T = unknown>(path: string, body?: unknown, options?: Omit<RequestOptions, 'body'>): Promise<T>;
47
+ delete<T = unknown>(path: string, options?: Omit<RequestOptions, 'body'>): Promise<T>;
48
+ /**
49
+ * Async-iterates every item of a `{ data, hasMore, nextCursor }` endpoint,
50
+ * paging automatically. `cursorParam` names the query parameter the
51
+ * endpoint reads the cursor from (default `"cursor"`; the events endpoint
52
+ * uses `"after"` — see `events.iterate()`, built on this).
53
+ */
54
+ paginate<T = unknown>(path: string, query?: QueryParams, paginateOptions?: {
55
+ cursorParam?: string;
56
+ }): AsyncGenerator<T, void, undefined>;
57
+ }
58
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EACV,UAAU,EACV,qBAAqB,EAErB,WAAW,EACX,cAAc,EACf,MAAM,YAAY,CAAC;AAWpB;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE9G;AA2CD;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAEhC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAiC;gBAEvD,OAAO,EAAE,qBAAqB;YAe5B,kBAAkB;IAMhC,OAAO,CAAC,QAAQ;IAiBhB;;;;;;;;OAQG;IACG,OAAO,CAAC,CAAC,GAAG,OAAO,EACvB,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,CAAC,CAAC;IAkDb,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIlF,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAInG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIlG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIrF;;;;;OAKG;IACI,QAAQ,CAAC,CAAC,GAAG,OAAO,EACzB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,WAAgB,EACvB,eAAe,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC7C,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC;CAYtC"}