@getmandato/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,403 @@
1
+ // src/config.ts
2
+ var DEFAULTS = {
3
+ baseUrl: "https://api.getmandato.dev",
4
+ timeout: 3e4,
5
+ maxRetries: 3
6
+ };
7
+
8
+ // src/errors.ts
9
+ var MandatoError = class extends Error {
10
+ /** HTTP status code */
11
+ status;
12
+ /** Error type from the API response */
13
+ type;
14
+ /** Request ID for support reference */
15
+ requestId;
16
+ constructor(message, status, type, requestId) {
17
+ super(message);
18
+ this.name = "MandatoError";
19
+ this.status = status;
20
+ this.type = type;
21
+ this.requestId = requestId;
22
+ }
23
+ };
24
+ var ValidationError = class extends MandatoError {
25
+ details;
26
+ constructor(message, details = [], requestId) {
27
+ super(message, 400, "validation_error", requestId);
28
+ this.name = "ValidationError";
29
+ this.details = details;
30
+ }
31
+ };
32
+ var AuthenticationError = class extends MandatoError {
33
+ constructor(message = "Invalid API key", requestId) {
34
+ super(message, 401, "authentication_error", requestId);
35
+ this.name = "AuthenticationError";
36
+ }
37
+ };
38
+ var ForbiddenError = class extends MandatoError {
39
+ constructor(message = "Forbidden", requestId) {
40
+ super(message, 403, "forbidden_error", requestId);
41
+ this.name = "ForbiddenError";
42
+ }
43
+ };
44
+ var NotFoundError = class extends MandatoError {
45
+ constructor(message = "Resource not found", requestId) {
46
+ super(message, 404, "not_found_error", requestId);
47
+ this.name = "NotFoundError";
48
+ }
49
+ };
50
+ var ConflictError = class extends MandatoError {
51
+ constructor(message = "Idempotency conflict", requestId) {
52
+ super(message, 409, "conflict_error", requestId);
53
+ this.name = "ConflictError";
54
+ }
55
+ };
56
+ var RateLimitError = class extends MandatoError {
57
+ /** Requests per minute allowed */
58
+ limit;
59
+ /** Seconds until the rate limit resets */
60
+ retryAfter;
61
+ constructor(message, limit, retryAfter, requestId) {
62
+ super(message, 429, "rate_limit_error", requestId);
63
+ this.name = "RateLimitError";
64
+ this.limit = limit;
65
+ this.retryAfter = retryAfter;
66
+ }
67
+ };
68
+ var ServerError = class extends MandatoError {
69
+ constructor(message = "Internal server error", status = 500, requestId) {
70
+ super(message, status, "server_error", requestId);
71
+ this.name = "ServerError";
72
+ }
73
+ };
74
+
75
+ // src/http.ts
76
+ var MandatoHttpClient = class {
77
+ apiKey;
78
+ baseUrl;
79
+ timeout;
80
+ maxRetries;
81
+ constructor(config) {
82
+ this.apiKey = config.apiKey;
83
+ this.baseUrl = (config.baseUrl ?? DEFAULTS.baseUrl).replace(/\/+$/, "");
84
+ this.timeout = config.timeout ?? DEFAULTS.timeout;
85
+ this.maxRetries = config.maxRetries ?? DEFAULTS.maxRetries;
86
+ }
87
+ /** Make a GET request */
88
+ async get(path, params) {
89
+ const url = this.buildUrl(path, params);
90
+ return this.request("GET", url);
91
+ }
92
+ /** Make a POST request */
93
+ async post(path, body, headers) {
94
+ const url = this.buildUrl(path);
95
+ return this.request("POST", url, body, headers);
96
+ }
97
+ /** Make a PATCH request */
98
+ async patch(path, body) {
99
+ const url = this.buildUrl(path);
100
+ return this.request("PATCH", url, body);
101
+ }
102
+ /** Make a DELETE request */
103
+ async delete(path) {
104
+ const url = this.buildUrl(path);
105
+ return this.request("DELETE", url);
106
+ }
107
+ /** Build full URL with optional query params */
108
+ buildUrl(path, params) {
109
+ const url = new URL(`${this.baseUrl}${path}`);
110
+ if (params) {
111
+ for (const [key, value] of Object.entries(params)) {
112
+ if (value !== void 0) {
113
+ url.searchParams.set(key, String(value));
114
+ }
115
+ }
116
+ }
117
+ return url.toString();
118
+ }
119
+ /** Execute an HTTP request with retries and error mapping */
120
+ async request(method, url, body, extraHeaders) {
121
+ let lastError;
122
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
123
+ if (attempt > 0) {
124
+ const delay = Math.min(500 * 2 ** (attempt - 1), 1e4);
125
+ await sleep(delay);
126
+ }
127
+ try {
128
+ const controller = new AbortController();
129
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
130
+ const headers = {
131
+ Authorization: `Bearer ${this.apiKey}`,
132
+ "Content-Type": "application/json",
133
+ ...extraHeaders
134
+ };
135
+ const init = {
136
+ method,
137
+ headers,
138
+ signal: controller.signal
139
+ };
140
+ if (body !== void 0 && method !== "GET") {
141
+ init.body = JSON.stringify(body);
142
+ }
143
+ const response = await fetch(url, init);
144
+ clearTimeout(timeoutId);
145
+ if (response.ok) {
146
+ return await response.json();
147
+ }
148
+ const errorBody = await this.parseErrorBody(response);
149
+ const requestId = errorBody?.error?.requestId;
150
+ if (response.status === 400) {
151
+ throw new ValidationError(
152
+ errorBody?.error?.message ?? "Validation error",
153
+ errorBody?.error?.details,
154
+ requestId
155
+ );
156
+ }
157
+ if (response.status === 401) {
158
+ throw new AuthenticationError(errorBody?.error?.message, requestId);
159
+ }
160
+ if (response.status === 403) {
161
+ throw new ForbiddenError(errorBody?.error?.message, requestId);
162
+ }
163
+ if (response.status === 404) {
164
+ throw new NotFoundError(errorBody?.error?.message, requestId);
165
+ }
166
+ if (response.status === 409) {
167
+ throw new ConflictError(errorBody?.error?.message, requestId);
168
+ }
169
+ if (response.status === 429) {
170
+ const limitHeader = response.headers.get("X-RateLimit-Limit");
171
+ const retryAfter = response.headers.get("Retry-After");
172
+ lastError = new RateLimitError(
173
+ errorBody?.error?.message ?? "Rate limit exceeded",
174
+ limitHeader ? Number(limitHeader) : void 0,
175
+ retryAfter ? Number(retryAfter) : void 0,
176
+ requestId
177
+ );
178
+ continue;
179
+ }
180
+ if (response.status >= 500) {
181
+ lastError = new ServerError(
182
+ errorBody?.error?.message ?? "Server error",
183
+ response.status,
184
+ requestId
185
+ );
186
+ continue;
187
+ }
188
+ throw new MandatoError(
189
+ errorBody?.error?.message ?? `HTTP ${response.status}`,
190
+ response.status,
191
+ errorBody?.error?.type ?? "unknown_error",
192
+ requestId
193
+ );
194
+ } catch (error) {
195
+ if (error instanceof MandatoError && !(error instanceof RateLimitError) && !(error instanceof ServerError)) {
196
+ throw error;
197
+ }
198
+ if (error instanceof Error && error.name === "AbortError") {
199
+ lastError = new MandatoError("Request timeout", 0, "timeout_error");
200
+ continue;
201
+ }
202
+ if (error instanceof MandatoError) {
203
+ lastError = error;
204
+ continue;
205
+ }
206
+ lastError = error instanceof Error ? error : new Error(String(error));
207
+ }
208
+ }
209
+ throw lastError ?? new MandatoError("Request failed after retries", 0, "retry_exhausted");
210
+ }
211
+ /** Safely parse error response body */
212
+ async parseErrorBody(response) {
213
+ try {
214
+ return await response.json();
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+ };
220
+ function sleep(ms) {
221
+ return new Promise((resolve) => setTimeout(resolve, ms));
222
+ }
223
+
224
+ // src/resources/api-keys.ts
225
+ var ApiKeys = class {
226
+ constructor(http) {
227
+ this.http = http;
228
+ }
229
+ /** List API keys (masked) */
230
+ async list() {
231
+ return this.http.get("/v1/api-keys");
232
+ }
233
+ /** Create a new API key (full key returned once) */
234
+ async create(data) {
235
+ return this.http.post("/v1/api-keys", data);
236
+ }
237
+ /** Revoke an API key */
238
+ async revoke(id) {
239
+ return this.http.delete(`/v1/api-keys/${id}`);
240
+ }
241
+ };
242
+
243
+ // src/resources/companies.ts
244
+ var Companies = class {
245
+ constructor(http) {
246
+ this.http = http;
247
+ }
248
+ /** Create a company */
249
+ async create(data) {
250
+ return this.http.post("/v1/companies", data);
251
+ }
252
+ /** List all companies */
253
+ async list() {
254
+ return this.http.get("/v1/companies");
255
+ }
256
+ /** Get a single company by ID */
257
+ async get(id) {
258
+ return this.http.get(`/v1/companies/${id}`);
259
+ }
260
+ /** Update a company */
261
+ async update(id, data) {
262
+ return this.http.patch(`/v1/companies/${id}`, data);
263
+ }
264
+ /** Delete a company */
265
+ async delete(id) {
266
+ return this.http.delete(`/v1/companies/${id}`);
267
+ }
268
+ };
269
+
270
+ // src/resources/connections.ts
271
+ var Connections = class {
272
+ constructor(http) {
273
+ this.http = http;
274
+ }
275
+ /** Initialize a government connection */
276
+ async create(data) {
277
+ return this.http.post("/v1/connections", data);
278
+ }
279
+ /** List all connections */
280
+ async list() {
281
+ return this.http.get("/v1/connections");
282
+ }
283
+ /** Get a single connection by ID */
284
+ async get(id) {
285
+ return this.http.get(`/v1/connections/${id}`);
286
+ }
287
+ /** Remove a connection */
288
+ async delete(id) {
289
+ return this.http.delete(`/v1/connections/${id}`);
290
+ }
291
+ };
292
+
293
+ // src/resources/invoices.ts
294
+ var Invoices = class {
295
+ constructor(http) {
296
+ this.http = http;
297
+ }
298
+ /** Create and submit an invoice */
299
+ async create(data, idempotencyKey) {
300
+ const headers = {};
301
+ if (idempotencyKey) {
302
+ headers["X-Idempotency-Key"] = idempotencyKey;
303
+ }
304
+ return this.http.post("/v1/invoices", data, headers);
305
+ }
306
+ /** List invoices with optional filters */
307
+ async list(params) {
308
+ return this.http.get(
309
+ "/v1/invoices",
310
+ params
311
+ );
312
+ }
313
+ /** Get a single invoice by ID */
314
+ async get(id) {
315
+ return this.http.get(`/v1/invoices/${id}`);
316
+ }
317
+ /** Dry-run validation without submission */
318
+ async validate(data) {
319
+ return this.http.post("/v1/invoices/validate", data);
320
+ }
321
+ /** Get invoice event timeline */
322
+ async getEvents(id) {
323
+ return this.http.get(`/v1/invoices/${id}/events`);
324
+ }
325
+ };
326
+
327
+ // src/resources/webhooks.ts
328
+ var Webhooks = class {
329
+ constructor(http) {
330
+ this.http = http;
331
+ }
332
+ /** Register a webhook URL */
333
+ async create(data) {
334
+ return this.http.post("/v1/webhooks", data);
335
+ }
336
+ /** Get current webhook configuration */
337
+ async get() {
338
+ return this.http.get("/v1/webhooks");
339
+ }
340
+ /** Remove webhook */
341
+ async delete() {
342
+ return this.http.delete("/v1/webhooks");
343
+ }
344
+ };
345
+
346
+ // src/client.ts
347
+ var MandatoClient = class {
348
+ /** Invoice operations */
349
+ invoices;
350
+ /** Company operations */
351
+ companies;
352
+ /** Connection operations */
353
+ connections;
354
+ /** Webhook operations */
355
+ webhooks;
356
+ /** API key operations */
357
+ apiKeys;
358
+ http;
359
+ constructor(config) {
360
+ this.http = new MandatoHttpClient(config);
361
+ this.invoices = new Invoices(this.http);
362
+ this.companies = new Companies(this.http);
363
+ this.connections = new Connections(this.http);
364
+ this.webhooks = new Webhooks(this.http);
365
+ this.apiKeys = new ApiKeys(this.http);
366
+ }
367
+ /** Check API health */
368
+ async health() {
369
+ return this.http.get("/v1/health");
370
+ }
371
+ };
372
+
373
+ // src/webhooks.ts
374
+ import { createHmac, timingSafeEqual } from "crypto";
375
+ function verifyWebhookSignature(payload, signature, secret) {
376
+ const expected = createHmac("sha256", secret).update(payload).digest("hex");
377
+ const sigBuffer = Buffer.from(signature, "utf-8");
378
+ const expectedBuffer = Buffer.from(expected, "utf-8");
379
+ if (sigBuffer.length !== expectedBuffer.length) {
380
+ return false;
381
+ }
382
+ return timingSafeEqual(sigBuffer, expectedBuffer);
383
+ }
384
+ function constructWebhookEvent(payload, signature, secret) {
385
+ if (!verifyWebhookSignature(payload, signature, secret)) {
386
+ throw new Error("Invalid webhook signature");
387
+ }
388
+ return JSON.parse(payload);
389
+ }
390
+ export {
391
+ AuthenticationError,
392
+ ConflictError,
393
+ ForbiddenError,
394
+ MandatoClient,
395
+ MandatoError,
396
+ MandatoHttpClient,
397
+ NotFoundError,
398
+ RateLimitError,
399
+ ServerError,
400
+ ValidationError,
401
+ constructWebhookEvent,
402
+ verifyWebhookSignature
403
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@getmandato/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official Node.js SDK for the Mandato EU e-invoicing API",
5
+ "license": "MIT",
6
+ "author": "Andrei Cotaga",
7
+ "homepage": "https://getmandato.dev",
8
+ "bugs": "https://github.com/andreicotaga/mandato/issues",
9
+ "type": "module",
10
+ "main": "dist/index.cjs",
11
+ "module": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ ".": {
16
+ "import": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "require": {
21
+ "types": "./dist/index.d.cts",
22
+ "default": "./dist/index.cjs"
23
+ }
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "!dist/**/CLAUDE.md",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "keywords": [
33
+ "mandato",
34
+ "e-invoicing",
35
+ "eu",
36
+ "anaf",
37
+ "romania",
38
+ "ubl",
39
+ "invoice",
40
+ "api",
41
+ "sdk"
42
+ ],
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/andreicotaga/mandato.git",
46
+ "directory": "packages/sdk"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
56
+ "test": "vitest run",
57
+ "typecheck": "tsc --noEmit",
58
+ "prepublishOnly": "pnpm build"
59
+ }
60
+ }