@facturino/node 1.0.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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/cjs/client.d.ts +24 -0
  4. package/dist/cjs/client.js +203 -0
  5. package/dist/cjs/errors.d.ts +76 -0
  6. package/dist/cjs/errors.js +128 -0
  7. package/dist/cjs/index.d.ts +59 -0
  8. package/dist/cjs/index.js +77 -0
  9. package/dist/cjs/package.json +3 -0
  10. package/dist/cjs/pagination.d.ts +24 -0
  11. package/dist/cjs/pagination.js +68 -0
  12. package/dist/cjs/resources/account.d.ts +58 -0
  13. package/dist/cjs/resources/account.js +48 -0
  14. package/dist/cjs/resources/archives.d.ts +16 -0
  15. package/dist/cjs/resources/archives.js +17 -0
  16. package/dist/cjs/resources/billing.d.ts +30 -0
  17. package/dist/cjs/resources/billing.js +45 -0
  18. package/dist/cjs/resources/companies.d.ts +47 -0
  19. package/dist/cjs/resources/companies.js +45 -0
  20. package/dist/cjs/resources/creditNotes.d.ts +64 -0
  21. package/dist/cjs/resources/creditNotes.js +62 -0
  22. package/dist/cjs/resources/customers.d.ts +17 -0
  23. package/dist/cjs/resources/customers.js +36 -0
  24. package/dist/cjs/resources/ereporting.d.ts +12 -0
  25. package/dist/cjs/resources/ereporting.js +23 -0
  26. package/dist/cjs/resources/events.d.ts +11 -0
  27. package/dist/cjs/resources/events.js +20 -0
  28. package/dist/cjs/resources/exports.d.ts +20 -0
  29. package/dist/cjs/resources/exports.js +31 -0
  30. package/dist/cjs/resources/health.d.ts +13 -0
  31. package/dist/cjs/resources/health.js +18 -0
  32. package/dist/cjs/resources/invoices.d.ts +112 -0
  33. package/dist/cjs/resources/invoices.js +141 -0
  34. package/dist/cjs/resources/jobs.d.ts +10 -0
  35. package/dist/cjs/resources/jobs.js +24 -0
  36. package/dist/cjs/resources/payments.d.ts +16 -0
  37. package/dist/cjs/resources/payments.js +25 -0
  38. package/dist/cjs/resources/products.d.ts +15 -0
  39. package/dist/cjs/resources/products.js +32 -0
  40. package/dist/cjs/resources/quotes.d.ts +44 -0
  41. package/dist/cjs/resources/quotes.js +58 -0
  42. package/dist/cjs/resources/received-invoices.d.ts +14 -0
  43. package/dist/cjs/resources/received-invoices.js +29 -0
  44. package/dist/cjs/resources/recurringInvoices.d.ts +15 -0
  45. package/dist/cjs/resources/recurringInvoices.js +32 -0
  46. package/dist/cjs/resources/reference.d.ts +45 -0
  47. package/dist/cjs/resources/reference.js +57 -0
  48. package/dist/cjs/resources/reporting.d.ts +11 -0
  49. package/dist/cjs/resources/reporting.js +30 -0
  50. package/dist/cjs/resources/sandbox.d.ts +12 -0
  51. package/dist/cjs/resources/sandbox.js +21 -0
  52. package/dist/cjs/resources/usage.d.ts +20 -0
  53. package/dist/cjs/resources/usage.js +25 -0
  54. package/dist/cjs/resources/validate.d.ts +18 -0
  55. package/dist/cjs/resources/validate.js +23 -0
  56. package/dist/cjs/resources/webhookEndpoints.d.ts +20 -0
  57. package/dist/cjs/resources/webhookEndpoints.js +31 -0
  58. package/dist/cjs/types.d.ts +1178 -0
  59. package/dist/cjs/types.js +5 -0
  60. package/dist/cjs/webhooks.d.ts +14 -0
  61. package/dist/cjs/webhooks.js +63 -0
  62. package/dist/esm/client.d.ts +25 -0
  63. package/dist/esm/client.d.ts.map +1 -0
  64. package/dist/esm/client.js +200 -0
  65. package/dist/esm/client.js.map +1 -0
  66. package/dist/esm/errors.d.ts +77 -0
  67. package/dist/esm/errors.d.ts.map +1 -0
  68. package/dist/esm/errors.js +114 -0
  69. package/dist/esm/errors.js.map +1 -0
  70. package/dist/esm/index.d.ts +60 -0
  71. package/dist/esm/index.d.ts.map +1 -0
  72. package/dist/esm/index.js +61 -0
  73. package/dist/esm/index.js.map +1 -0
  74. package/dist/esm/pagination.d.ts +25 -0
  75. package/dist/esm/pagination.d.ts.map +1 -0
  76. package/dist/esm/pagination.js +65 -0
  77. package/dist/esm/pagination.js.map +1 -0
  78. package/dist/esm/resources/account.d.ts +59 -0
  79. package/dist/esm/resources/account.d.ts.map +1 -0
  80. package/dist/esm/resources/account.js +45 -0
  81. package/dist/esm/resources/account.js.map +1 -0
  82. package/dist/esm/resources/archives.d.ts +17 -0
  83. package/dist/esm/resources/archives.d.ts.map +1 -0
  84. package/dist/esm/resources/archives.js +14 -0
  85. package/dist/esm/resources/archives.js.map +1 -0
  86. package/dist/esm/resources/billing.d.ts +31 -0
  87. package/dist/esm/resources/billing.d.ts.map +1 -0
  88. package/dist/esm/resources/billing.js +42 -0
  89. package/dist/esm/resources/billing.js.map +1 -0
  90. package/dist/esm/resources/companies.d.ts +48 -0
  91. package/dist/esm/resources/companies.d.ts.map +1 -0
  92. package/dist/esm/resources/companies.js +42 -0
  93. package/dist/esm/resources/companies.js.map +1 -0
  94. package/dist/esm/resources/creditNotes.d.ts +65 -0
  95. package/dist/esm/resources/creditNotes.d.ts.map +1 -0
  96. package/dist/esm/resources/creditNotes.js +59 -0
  97. package/dist/esm/resources/creditNotes.js.map +1 -0
  98. package/dist/esm/resources/customers.d.ts +18 -0
  99. package/dist/esm/resources/customers.d.ts.map +1 -0
  100. package/dist/esm/resources/customers.js +33 -0
  101. package/dist/esm/resources/customers.js.map +1 -0
  102. package/dist/esm/resources/ereporting.d.ts +13 -0
  103. package/dist/esm/resources/ereporting.d.ts.map +1 -0
  104. package/dist/esm/resources/ereporting.js +20 -0
  105. package/dist/esm/resources/ereporting.js.map +1 -0
  106. package/dist/esm/resources/events.d.ts +12 -0
  107. package/dist/esm/resources/events.d.ts.map +1 -0
  108. package/dist/esm/resources/events.js +17 -0
  109. package/dist/esm/resources/events.js.map +1 -0
  110. package/dist/esm/resources/exports.d.ts +21 -0
  111. package/dist/esm/resources/exports.d.ts.map +1 -0
  112. package/dist/esm/resources/exports.js +28 -0
  113. package/dist/esm/resources/exports.js.map +1 -0
  114. package/dist/esm/resources/health.d.ts +14 -0
  115. package/dist/esm/resources/health.d.ts.map +1 -0
  116. package/dist/esm/resources/health.js +15 -0
  117. package/dist/esm/resources/health.js.map +1 -0
  118. package/dist/esm/resources/invoices.d.ts +113 -0
  119. package/dist/esm/resources/invoices.d.ts.map +1 -0
  120. package/dist/esm/resources/invoices.js +138 -0
  121. package/dist/esm/resources/invoices.js.map +1 -0
  122. package/dist/esm/resources/jobs.d.ts +11 -0
  123. package/dist/esm/resources/jobs.d.ts.map +1 -0
  124. package/dist/esm/resources/jobs.js +21 -0
  125. package/dist/esm/resources/jobs.js.map +1 -0
  126. package/dist/esm/resources/payments.d.ts +17 -0
  127. package/dist/esm/resources/payments.d.ts.map +1 -0
  128. package/dist/esm/resources/payments.js +22 -0
  129. package/dist/esm/resources/payments.js.map +1 -0
  130. package/dist/esm/resources/products.d.ts +16 -0
  131. package/dist/esm/resources/products.d.ts.map +1 -0
  132. package/dist/esm/resources/products.js +29 -0
  133. package/dist/esm/resources/products.js.map +1 -0
  134. package/dist/esm/resources/quotes.d.ts +45 -0
  135. package/dist/esm/resources/quotes.d.ts.map +1 -0
  136. package/dist/esm/resources/quotes.js +55 -0
  137. package/dist/esm/resources/quotes.js.map +1 -0
  138. package/dist/esm/resources/received-invoices.d.ts +15 -0
  139. package/dist/esm/resources/received-invoices.d.ts.map +1 -0
  140. package/dist/esm/resources/received-invoices.js +26 -0
  141. package/dist/esm/resources/received-invoices.js.map +1 -0
  142. package/dist/esm/resources/recurringInvoices.d.ts +16 -0
  143. package/dist/esm/resources/recurringInvoices.d.ts.map +1 -0
  144. package/dist/esm/resources/recurringInvoices.js +29 -0
  145. package/dist/esm/resources/recurringInvoices.js.map +1 -0
  146. package/dist/esm/resources/reference.d.ts +46 -0
  147. package/dist/esm/resources/reference.d.ts.map +1 -0
  148. package/dist/esm/resources/reference.js +54 -0
  149. package/dist/esm/resources/reference.js.map +1 -0
  150. package/dist/esm/resources/reporting.d.ts +12 -0
  151. package/dist/esm/resources/reporting.d.ts.map +1 -0
  152. package/dist/esm/resources/reporting.js +27 -0
  153. package/dist/esm/resources/reporting.js.map +1 -0
  154. package/dist/esm/resources/sandbox.d.ts +13 -0
  155. package/dist/esm/resources/sandbox.d.ts.map +1 -0
  156. package/dist/esm/resources/sandbox.js +18 -0
  157. package/dist/esm/resources/sandbox.js.map +1 -0
  158. package/dist/esm/resources/usage.d.ts +21 -0
  159. package/dist/esm/resources/usage.d.ts.map +1 -0
  160. package/dist/esm/resources/usage.js +22 -0
  161. package/dist/esm/resources/usage.js.map +1 -0
  162. package/dist/esm/resources/validate.d.ts +19 -0
  163. package/dist/esm/resources/validate.d.ts.map +1 -0
  164. package/dist/esm/resources/validate.js +20 -0
  165. package/dist/esm/resources/validate.js.map +1 -0
  166. package/dist/esm/resources/webhookEndpoints.d.ts +21 -0
  167. package/dist/esm/resources/webhookEndpoints.d.ts.map +1 -0
  168. package/dist/esm/resources/webhookEndpoints.js +28 -0
  169. package/dist/esm/resources/webhookEndpoints.js.map +1 -0
  170. package/dist/esm/types.d.ts +1179 -0
  171. package/dist/esm/types.d.ts.map +1 -0
  172. package/dist/esm/types.js +5 -0
  173. package/dist/esm/types.js.map +1 -0
  174. package/dist/esm/webhooks.d.ts +15 -0
  175. package/dist/esm/webhooks.d.ts.map +1 -0
  176. package/dist/esm/webhooks.js +60 -0
  177. package/dist/esm/webhooks.js.map +1 -0
  178. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Facturino
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 ADDED
@@ -0,0 +1,204 @@
1
+ # Facturino Node.js SDK
2
+
3
+ [![npm](https://img.shields.io/npm/v/@facturino/node)](https://www.npmjs.com/package/@facturino/node)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Official Node.js / TypeScript client for the [Facturino API](https://facturino.com/docs/api). Requires Node 18+.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @facturino/node
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```typescript
17
+ import Facturino from '@facturino/node'
18
+
19
+ const facturino = new Facturino('fac_test_xxx')
20
+
21
+ const invoice = await facturino.invoices.create({
22
+ customerId: 'cus_xxx',
23
+ buyer: {
24
+ companyName: 'Acme SAS',
25
+ siret: '55208131766522',
26
+ address: { line1: '10 rue de la Paix', postalCode: '75002', city: 'Paris', country: 'FR' },
27
+ },
28
+ lines: [{
29
+ description: 'Consulting',
30
+ quantity: '1', // decimal string
31
+ unit: 'flat_rate',
32
+ unitPrice: 10000, // 100.00 EUR (centimes)
33
+ vatRate: 2000, // 20.00% (centièmes de pourcent)
34
+ vatCode: 'S',
35
+ }],
36
+ dates: { issued: '2026-07-01', due: '2026-07-31' },
37
+ payment: { terms: 'Paiement à 30 jours', termsDays: 30, method: 'transfer', latePaymentRate: '10.00', collectionFee: '40.00' },
38
+ })
39
+
40
+ const finalized = await facturino.invoices.finalize(invoice.id)
41
+
42
+ // One-shot: pass `autoFinalize: true` (and optionally
43
+ // `autoSend: { email: true, pa: true }`) to `invoices.create(...)` to
44
+ // finalize — and deliver by email and/or to the PA — in a single call.
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ ```typescript
50
+ const facturino = new Facturino('fac_test_xxx', {
51
+ maxRetries: 3, // retries on 429/5xx
52
+ timeout: 30000, // ms
53
+ apiVersion: '2026-03-01',
54
+ })
55
+ ```
56
+
57
+ ## Amounts
58
+
59
+ Monetary values are integers in **centimes** (10000 = 100.00 EUR).
60
+ VAT rates are integers in **centipercent** (2000 = 20.00%).
61
+
62
+ ## Pagination
63
+
64
+ ```typescript
65
+ // Auto-paginate
66
+ for await (const inv of facturino.invoices.list({ limit: 25 })) {
67
+ console.log(inv.id)
68
+ }
69
+
70
+ // Single page
71
+ const page = await facturino.invoices.list({ status: 'draft' })
72
+ ```
73
+
74
+ ## Resources
75
+
76
+ ```typescript
77
+ // Invoices
78
+ facturino.invoices.create(params)
79
+ facturino.invoices.get('inv_xxx')
80
+ facturino.invoices.get('inv_xxx', { expand: ['customer', 'credit_notes'] })
81
+ facturino.invoices.update('inv_xxx', params)
82
+ facturino.invoices.del('inv_xxx')
83
+ facturino.invoices.finalize('inv_xxx')
84
+ facturino.invoices.send('inv_xxx')
85
+ facturino.invoices.getPdf('inv_xxx')
86
+ facturino.invoices.getFacturx('inv_xxx')
87
+ facturino.invoices.getXml('inv_xxx', 'cii')
88
+ facturino.invoices.list({ convertedFrom: 'quo_xxx' }) // invoices issued from a quote
89
+
90
+ // Payments (sub-resource)
91
+ facturino.invoices.payments.create('inv_xxx', { amount: 10000, method: 'transfer', paidAt: '...' })
92
+ facturino.invoices.payments.list('inv_xxx')
93
+ facturino.invoices.payments.cancel('inv_xxx', 'pay_xxx')
94
+
95
+ // Customers
96
+ facturino.customers.create(params)
97
+ facturino.customers.lookup({ siret: '73282932000074' })
98
+ // contacts[].role: 'billing' | 'technical' | 'main' (billing receives invoices)
99
+
100
+ // Products
101
+ facturino.products.list({ q: 'consult', category: 'services', active: true })
102
+
103
+ // Quotes
104
+ facturino.quotes.create(params)
105
+ facturino.quotes.send('quo_xxx')
106
+ facturino.quotes.accept('quo_xxx')
107
+ facturino.quotes.convert('quo_xxx') // -> draft invoice
108
+ facturino.quotes.clone('quo_xxx') // -> duplicated draft quote
109
+
110
+ // Credit Notes
111
+ facturino.creditNotes.create(params)
112
+ facturino.creditNotes.finalize('crn_xxx')
113
+
114
+ // Recurring Invoices
115
+ facturino.recurringInvoices.create(params)
116
+ facturino.recurringInvoices.pause('rec_xxx')
117
+ facturino.recurringInvoices.resume('rec_xxx')
118
+
119
+ // E-Reporting
120
+ facturino.ereporting.createDeclaration(params)
121
+ facturino.ereporting.submitDeclaration('erp_xxx')
122
+
123
+ // Exports
124
+ facturino.exports.generateFec({ period_start: '2026-01-01', period_end: '2026-12-31' })
125
+
126
+ // Jobs
127
+ facturino.jobs.poll('job_xxx') // wait for async completion
128
+
129
+ // Sandbox (test mode only)
130
+ facturino.sandbox.resetData()
131
+ facturino.sandbox.simulateStatus('inv_xxx', { status: 'approved' })
132
+
133
+ // Reference & health (public, no auth)
134
+ facturino.reference.listLegalForms({ search: 'SAS' })
135
+ facturino.reference.listPaProviders() // supported Plateformes Agréées (BYOPA)
136
+ facturino.health.check()
137
+
138
+ // Also: companies, events, webhookEndpoints, products
139
+ ```
140
+
141
+ > **Public token endpoints** — the recipient-facing portals (`/pay/:token`,
142
+ > `/portal/:token`, `/quote-portal/:token`) are intentionally not exposed by the
143
+ > SDK: they are opened by the end recipient through a hosted page, not called
144
+ > with an API key.
145
+
146
+ ## Webhooks
147
+
148
+ ```typescript
149
+ app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
150
+ try {
151
+ const event = facturino.webhooks.constructEvent(
152
+ req.body,
153
+ req.headers['facturino-signature'] as string,
154
+ 'whsec_xxx',
155
+ )
156
+ console.log(event.type, event.data.id)
157
+ res.json({ received: true })
158
+ } catch (err) {
159
+ res.status(400).send('Invalid signature')
160
+ }
161
+ })
162
+ ```
163
+
164
+ ## Idempotency
165
+
166
+ ```typescript
167
+ await facturino.invoices.create(params, { idempotencyKey: 'unique-id' })
168
+ ```
169
+
170
+ ## Errors
171
+
172
+ ```typescript
173
+ import { ApiError, RateLimitError, NotFoundError } from '@facturino/node'
174
+
175
+ try {
176
+ await facturino.invoices.get('inv_xxx')
177
+ } catch (err) {
178
+ if (err instanceof NotFoundError) { /* 404 */ }
179
+ if (err instanceof RateLimitError) { console.log(err.retryAfter) }
180
+ if (err instanceof ApiError) { console.log(err.code, err.requestId) }
181
+ }
182
+ ```
183
+
184
+ ## TypeScript
185
+
186
+ All types are exported:
187
+
188
+ ```typescript
189
+ import type { Invoice, Customer, WebhookEvent } from '@facturino/node'
190
+ ```
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ git clone https://github.com/facturino/facturino-node.git
196
+ cd facturino-node
197
+ npm install
198
+ npm test
199
+ npm run build
200
+ ```
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,24 @@
1
+ import type { FacturinoConfig, RequestOptions } from './types.js';
2
+ /** HTTP client with retries, exponential backoff, and structured errors. */
3
+ export declare class HttpClient {
4
+ private readonly apiKey;
5
+ private readonly baseUrl;
6
+ private readonly maxRetries;
7
+ private readonly timeout;
8
+ private readonly apiVersion;
9
+ constructor(apiKey: string, config?: FacturinoConfig);
10
+ request<T>(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<T>;
11
+ get<T>(path: string): Promise<T>;
12
+ post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
13
+ patch<T>(path: string, body: unknown): Promise<T>;
14
+ del<T>(path: string, body?: unknown): Promise<T>;
15
+ /**
16
+ * Map an HTTP status + error body to the most specific {@link ApiError}
17
+ * subclass. Both the status and the `error.type` from the body are used:
18
+ * the status decides the primary class (401 → Authentication, 429 →
19
+ * RateLimit…) but for 4xx codes that share a status — notably 422 vs
20
+ * 400 — we look at `body.error.type` to discriminate further.
21
+ */
22
+ private buildError;
23
+ private getRetryDelay;
24
+ }
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpClient = void 0;
4
+ const errors_js_1 = require("./errors.js");
5
+ const DEFAULT_BASE_URL = 'https://facturino.com/api';
6
+ const DEFAULT_MAX_RETRIES = 3;
7
+ const DEFAULT_TIMEOUT = 30000;
8
+ const DEFAULT_API_VERSION = '2026-03-01';
9
+ const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503]);
10
+ const INITIAL_RETRY_DELAY_MS = 500;
11
+ const MAX_RETRY_DELAY_MS = 30000;
12
+ const VERSION = '1.0.0';
13
+ /** HTTP client with retries, exponential backoff, and structured errors. */
14
+ class HttpClient {
15
+ constructor(apiKey, config = {}) {
16
+ if (!apiKey) {
17
+ throw new Error('No API key provided. Pass your key as the first argument: new Facturino("fac_test_...")');
18
+ }
19
+ if (!apiKey.startsWith('fac_test_') && !apiKey.startsWith('fac_live_')) {
20
+ throw new Error('Invalid API key format. Keys must start with "fac_test_" or "fac_live_".');
21
+ }
22
+ this.apiKey = apiKey;
23
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
24
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
25
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
26
+ this.apiVersion = config.apiVersion ?? DEFAULT_API_VERSION;
27
+ }
28
+ async request(method, path, body, options) {
29
+ const url = `${this.baseUrl}${path}`;
30
+ const headers = {
31
+ 'Authorization': `Bearer ${this.apiKey}`,
32
+ 'Content-Type': 'application/json',
33
+ 'Accept': 'application/json',
34
+ 'Facturino-Version': this.apiVersion,
35
+ 'User-Agent': `facturino-node/${VERSION}`,
36
+ };
37
+ if (options?.idempotencyKey) {
38
+ headers['Idempotency-Key'] = options.idempotencyKey;
39
+ }
40
+ let lastError = null;
41
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
42
+ if (attempt > 0) {
43
+ const delay = this.getRetryDelay(attempt, lastError);
44
+ await sleep(delay);
45
+ }
46
+ const controller = new AbortController();
47
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
48
+ try {
49
+ const fetchOptions = {
50
+ method,
51
+ headers,
52
+ signal: controller.signal,
53
+ };
54
+ if (body !== undefined && method !== 'GET') {
55
+ fetchOptions.body = JSON.stringify(body);
56
+ }
57
+ const response = await fetch(url, fetchOptions);
58
+ clearTimeout(timeoutId);
59
+ if (response.status === 204) {
60
+ return undefined;
61
+ }
62
+ let responseBody;
63
+ const contentType = response.headers.get('content-type') ?? '';
64
+ if (contentType.includes('application/json')) {
65
+ responseBody = await response.json();
66
+ }
67
+ else {
68
+ const text = await response.text();
69
+ responseBody = text;
70
+ }
71
+ if (response.ok) {
72
+ return responseBody;
73
+ }
74
+ const errorBody = responseBody;
75
+ const hasStructuredError = errorBody &&
76
+ typeof errorBody === 'object' &&
77
+ 'error' in errorBody &&
78
+ typeof errorBody.error === 'object';
79
+ if (!hasStructuredError) {
80
+ const syntheticError = {
81
+ error: {
82
+ type: 'api_error',
83
+ code: 'unknown',
84
+ message: typeof responseBody === 'string' ? responseBody : `HTTP ${response.status}`,
85
+ request_id: response.headers.get('x-request-id') ?? 'req_unknown',
86
+ },
87
+ };
88
+ lastError = this.buildError(response.status, syntheticError, response.headers);
89
+ }
90
+ else {
91
+ lastError = this.buildError(response.status, errorBody, response.headers);
92
+ }
93
+ if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
94
+ continue;
95
+ }
96
+ throw lastError;
97
+ }
98
+ catch (err) {
99
+ clearTimeout(timeoutId);
100
+ if (err instanceof errors_js_1.ApiError) {
101
+ throw err;
102
+ }
103
+ if (err instanceof Error && err.name === 'AbortError') {
104
+ lastError = new errors_js_1.ConnectionError(`Request to ${method} ${path} timed out after ${this.timeout}ms`);
105
+ if (attempt < this.maxRetries) {
106
+ continue;
107
+ }
108
+ throw lastError;
109
+ }
110
+ if (err instanceof TypeError && (err.message.includes('fetch') || err.message.includes('network'))) {
111
+ lastError = new errors_js_1.ConnectionError(`Network error on ${method} ${path}: ${err.message}`);
112
+ if (attempt < this.maxRetries) {
113
+ continue;
114
+ }
115
+ throw lastError;
116
+ }
117
+ throw err;
118
+ }
119
+ }
120
+ throw lastError ?? new errors_js_1.ConnectionError('Request failed after all retries');
121
+ }
122
+ async get(path) {
123
+ return this.request('GET', path);
124
+ }
125
+ async post(path, body, options) {
126
+ return this.request('POST', path, body, options);
127
+ }
128
+ async patch(path, body) {
129
+ return this.request('PATCH', path, body);
130
+ }
131
+ async del(path, body) {
132
+ return this.request('DELETE', path, body);
133
+ }
134
+ /**
135
+ * Map an HTTP status + error body to the most specific {@link ApiError}
136
+ * subclass. Both the status and the `error.type` from the body are used:
137
+ * the status decides the primary class (401 → Authentication, 429 →
138
+ * RateLimit…) but for 4xx codes that share a status — notably 422 vs
139
+ * 400 — we look at `body.error.type` to discriminate further.
140
+ */
141
+ buildError(status, body, headers) {
142
+ const type = body?.error?.type;
143
+ switch (status) {
144
+ case 400:
145
+ return new errors_js_1.InvalidRequestError(status, body);
146
+ case 401:
147
+ return new errors_js_1.AuthenticationError(status, body);
148
+ case 402:
149
+ return new errors_js_1.PlanLimitError(status, body);
150
+ case 403:
151
+ return new errors_js_1.PermissionError(status, body);
152
+ case 404:
153
+ return new errors_js_1.NotFoundError(status, body);
154
+ case 409:
155
+ return new errors_js_1.ConflictError(status, body);
156
+ case 422:
157
+ return new errors_js_1.ValidationError(status, body);
158
+ case 429: {
159
+ const retryAfterHeader = headers.get('retry-after');
160
+ const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : null;
161
+ return new errors_js_1.RateLimitError(status, body, Number.isNaN(retryAfter) ? null : retryAfter);
162
+ }
163
+ case 500:
164
+ case 502:
165
+ case 503:
166
+ case 504:
167
+ return new errors_js_1.ApiInternalError(status, body);
168
+ default:
169
+ // Use the body `type` as a fallback for non-standard status codes so
170
+ // a 418 with `type: 'rate_limit_error'` still maps to the right
171
+ // class instead of falling through to the generic `ApiError`.
172
+ if (type === 'validation_error')
173
+ return new errors_js_1.ValidationError(status, body);
174
+ if (type === 'authentication_error')
175
+ return new errors_js_1.AuthenticationError(status, body);
176
+ if (type === 'permission_error')
177
+ return new errors_js_1.PermissionError(status, body);
178
+ if (type === 'conflict_error')
179
+ return new errors_js_1.ConflictError(status, body);
180
+ if (type === 'plan_limit_error')
181
+ return new errors_js_1.PlanLimitError(status, body);
182
+ if (type === 'rate_limit_error')
183
+ return new errors_js_1.RateLimitError(status, body, null);
184
+ if (type === 'not_found_error')
185
+ return new errors_js_1.NotFoundError(status, body);
186
+ if (type === 'api_error')
187
+ return new errors_js_1.ApiInternalError(status, body);
188
+ return new errors_js_1.ApiError(status, body);
189
+ }
190
+ }
191
+ getRetryDelay(attempt, lastError) {
192
+ if (lastError instanceof errors_js_1.RateLimitError && lastError.retryAfter !== null) {
193
+ return Math.min(lastError.retryAfter * 1000, MAX_RETRY_DELAY_MS);
194
+ }
195
+ const baseDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
196
+ const jitter = baseDelay * 0.2 * Math.random();
197
+ return Math.min(baseDelay + jitter, MAX_RETRY_DELAY_MS);
198
+ }
199
+ }
200
+ exports.HttpClient = HttpClient;
201
+ function sleep(ms) {
202
+ return new Promise((resolve) => setTimeout(resolve, ms));
203
+ }
@@ -0,0 +1,76 @@
1
+ import type { ApiErrorBody } from './types.js';
2
+ /** Base SDK error. */
3
+ export declare class FacturinoError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /** Structured API error response. */
7
+ export declare class ApiError extends FacturinoError {
8
+ readonly status: number;
9
+ readonly type: string;
10
+ readonly code: string;
11
+ readonly param?: string;
12
+ readonly docUrl?: string;
13
+ readonly requestId: string;
14
+ readonly hint?: string;
15
+ constructor(status: number, body: ApiErrorBody);
16
+ }
17
+ /**
18
+ * 400 — request payload is malformed, a field is missing or invalid.
19
+ * Inspect `error.param` to identify the offending field.
20
+ */
21
+ export declare class InvalidRequestError extends ApiError {
22
+ constructor(status: number, body: ApiErrorBody);
23
+ }
24
+ /**
25
+ * 422 — Zod schema validation rejected the payload. The offending field is
26
+ * surfaced in `error.param`. Distinct from {@link InvalidRequestError} so
27
+ * callers can differentiate "shape malformed" (400) from "value out of
28
+ * range / wrong type" (422) when re-displaying validation feedback.
29
+ */
30
+ export declare class ValidationError extends ApiError {
31
+ constructor(status: number, body: ApiErrorBody);
32
+ }
33
+ /** 401 — invalid, revoked, or missing API key. */
34
+ export declare class AuthenticationError extends ApiError {
35
+ constructor(status: number, body: ApiErrorBody);
36
+ }
37
+ /**
38
+ * 403 — the API key authenticated successfully but lacks the scope required
39
+ * for the requested operation. The required scope is exposed in
40
+ * `error.param`.
41
+ */
42
+ export declare class PermissionError extends ApiError {
43
+ constructor(status: number, body: ApiErrorBody);
44
+ }
45
+ /** 404 — resource not found, or not visible with this key's livemode. */
46
+ export declare class NotFoundError extends ApiError {
47
+ constructor(status: number, body: ApiErrorBody);
48
+ }
49
+ /**
50
+ * 409 — the request conflicts with the current state of the resource
51
+ * (state-machine transition refused, finalized invoice cannot be edited,
52
+ * idempotency key reused with a different payload).
53
+ */
54
+ export declare class ConflictError extends ApiError {
55
+ constructor(status: number, body: ApiErrorBody);
56
+ }
57
+ /** 429 — rate limit exceeded. `retryAfter` is the recommended back-off in seconds. */
58
+ export declare class RateLimitError extends ApiError {
59
+ readonly retryAfter: number | null;
60
+ constructor(status: number, body: ApiErrorBody, retryAfter: number | null);
61
+ }
62
+ /** 402 — quota or plan limit exceeded. Upgrade or revoke resources to recover. */
63
+ export declare class PlanLimitError extends ApiError {
64
+ constructor(status: number, body: ApiErrorBody);
65
+ }
66
+ /**
67
+ * 500/503 — Facturino is experiencing an internal error. The API is monitored
68
+ * automatically; safe to retry with exponential back-off.
69
+ */
70
+ export declare class ApiInternalError extends ApiError {
71
+ constructor(status: number, body: ApiErrorBody);
72
+ }
73
+ /** Network or timeout error. Not surfaced by the API itself. */
74
+ export declare class ConnectionError extends FacturinoError {
75
+ constructor(message: string);
76
+ }
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionError = exports.ApiInternalError = exports.PlanLimitError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.PermissionError = exports.AuthenticationError = exports.ValidationError = exports.InvalidRequestError = exports.ApiError = exports.FacturinoError = void 0;
4
+ /** Base SDK error. */
5
+ class FacturinoError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'FacturinoError';
9
+ Object.setPrototypeOf(this, new.target.prototype);
10
+ }
11
+ }
12
+ exports.FacturinoError = FacturinoError;
13
+ /** Structured API error response. */
14
+ class ApiError extends FacturinoError {
15
+ constructor(status, body) {
16
+ const err = body.error;
17
+ super(err.message);
18
+ this.name = 'ApiError';
19
+ this.status = status;
20
+ this.type = err.type;
21
+ this.code = err.code;
22
+ this.param = err.param;
23
+ this.docUrl = err.doc_url;
24
+ this.requestId = err.request_id;
25
+ this.hint = err.hint;
26
+ }
27
+ }
28
+ exports.ApiError = ApiError;
29
+ /**
30
+ * 400 — request payload is malformed, a field is missing or invalid.
31
+ * Inspect `error.param` to identify the offending field.
32
+ */
33
+ class InvalidRequestError extends ApiError {
34
+ constructor(status, body) {
35
+ super(status, body);
36
+ this.name = 'InvalidRequestError';
37
+ }
38
+ }
39
+ exports.InvalidRequestError = InvalidRequestError;
40
+ /**
41
+ * 422 — Zod schema validation rejected the payload. The offending field is
42
+ * surfaced in `error.param`. Distinct from {@link InvalidRequestError} so
43
+ * callers can differentiate "shape malformed" (400) from "value out of
44
+ * range / wrong type" (422) when re-displaying validation feedback.
45
+ */
46
+ class ValidationError extends ApiError {
47
+ constructor(status, body) {
48
+ super(status, body);
49
+ this.name = 'ValidationError';
50
+ }
51
+ }
52
+ exports.ValidationError = ValidationError;
53
+ /** 401 — invalid, revoked, or missing API key. */
54
+ class AuthenticationError extends ApiError {
55
+ constructor(status, body) {
56
+ super(status, body);
57
+ this.name = 'AuthenticationError';
58
+ }
59
+ }
60
+ exports.AuthenticationError = AuthenticationError;
61
+ /**
62
+ * 403 — the API key authenticated successfully but lacks the scope required
63
+ * for the requested operation. The required scope is exposed in
64
+ * `error.param`.
65
+ */
66
+ class PermissionError extends ApiError {
67
+ constructor(status, body) {
68
+ super(status, body);
69
+ this.name = 'PermissionError';
70
+ }
71
+ }
72
+ exports.PermissionError = PermissionError;
73
+ /** 404 — resource not found, or not visible with this key's livemode. */
74
+ class NotFoundError extends ApiError {
75
+ constructor(status, body) {
76
+ super(status, body);
77
+ this.name = 'NotFoundError';
78
+ }
79
+ }
80
+ exports.NotFoundError = NotFoundError;
81
+ /**
82
+ * 409 — the request conflicts with the current state of the resource
83
+ * (state-machine transition refused, finalized invoice cannot be edited,
84
+ * idempotency key reused with a different payload).
85
+ */
86
+ class ConflictError extends ApiError {
87
+ constructor(status, body) {
88
+ super(status, body);
89
+ this.name = 'ConflictError';
90
+ }
91
+ }
92
+ exports.ConflictError = ConflictError;
93
+ /** 429 — rate limit exceeded. `retryAfter` is the recommended back-off in seconds. */
94
+ class RateLimitError extends ApiError {
95
+ constructor(status, body, retryAfter) {
96
+ super(status, body);
97
+ this.name = 'RateLimitError';
98
+ this.retryAfter = retryAfter;
99
+ }
100
+ }
101
+ exports.RateLimitError = RateLimitError;
102
+ /** 402 — quota or plan limit exceeded. Upgrade or revoke resources to recover. */
103
+ class PlanLimitError extends ApiError {
104
+ constructor(status, body) {
105
+ super(status, body);
106
+ this.name = 'PlanLimitError';
107
+ }
108
+ }
109
+ exports.PlanLimitError = PlanLimitError;
110
+ /**
111
+ * 500/503 — Facturino is experiencing an internal error. The API is monitored
112
+ * automatically; safe to retry with exponential back-off.
113
+ */
114
+ class ApiInternalError extends ApiError {
115
+ constructor(status, body) {
116
+ super(status, body);
117
+ this.name = 'ApiInternalError';
118
+ }
119
+ }
120
+ exports.ApiInternalError = ApiInternalError;
121
+ /** Network or timeout error. Not surfaced by the API itself. */
122
+ class ConnectionError extends FacturinoError {
123
+ constructor(message) {
124
+ super(message);
125
+ this.name = 'ConnectionError';
126
+ }
127
+ }
128
+ exports.ConnectionError = ConnectionError;