@vitrindigital/node 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,426 @@
1
+ // src/errors.ts
2
+ var VitrinError = class extends Error {
3
+ statusCode;
4
+ body;
5
+ requestId;
6
+ constructor(message, opts = {}) {
7
+ super(message);
8
+ this.name = this.constructor.name;
9
+ this.statusCode = opts.statusCode;
10
+ this.body = opts.body;
11
+ this.requestId = opts.requestId;
12
+ }
13
+ };
14
+ var VitrinAuthError = class extends VitrinError {
15
+ };
16
+ var VitrinValidationError = class extends VitrinError {
17
+ fieldErrors;
18
+ constructor(message, opts = {}) {
19
+ super(message, opts);
20
+ this.fieldErrors = opts.fieldErrors;
21
+ }
22
+ };
23
+ var VitrinRateLimitError = class extends VitrinError {
24
+ };
25
+ var VitrinNotFoundError = class extends VitrinError {
26
+ };
27
+ var VitrinServerError = class extends VitrinError {
28
+ };
29
+ var VitrinNetworkError = class extends VitrinError {
30
+ };
31
+
32
+ // src/client.ts
33
+ var DEFAULT_BASE_URL = "https://api.vitrin.digital/api/v1";
34
+ var SDK_VERSION = "0.1.0";
35
+ var VitrinClient = class {
36
+ baseUrl;
37
+ apiKey;
38
+ timeout;
39
+ maxRetries;
40
+ constructor(opts) {
41
+ if (!opts.apiKey) {
42
+ throw new Error("apiKey \xE9 obrigat\xF3rio");
43
+ }
44
+ this.apiKey = opts.apiKey;
45
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
46
+ this.timeout = opts.timeout ?? 3e4;
47
+ this.maxRetries = opts.maxRetries ?? 3;
48
+ }
49
+ /** Helper público pra resources. */
50
+ async request(path, options = {}) {
51
+ const { method = "GET", body, query, idempotencyKey } = options;
52
+ const url = this.buildUrl(path, query);
53
+ const headers = {
54
+ Authorization: `Bearer ${this.apiKey}`,
55
+ Accept: "application/json",
56
+ "User-Agent": `vitrindigital-node/${SDK_VERSION} node/${process.version}`
57
+ };
58
+ if (body !== void 0) headers["Content-Type"] = "application/json";
59
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
60
+ let lastError = null;
61
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
62
+ const controller = new AbortController();
63
+ const timer = setTimeout(() => controller.abort(), this.timeout);
64
+ let res;
65
+ try {
66
+ res = await fetch(url, {
67
+ method,
68
+ headers,
69
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
70
+ signal: controller.signal
71
+ });
72
+ } catch (err2) {
73
+ clearTimeout(timer);
74
+ const netErr = err2;
75
+ lastError = new VitrinNetworkError(
76
+ netErr.name === "AbortError" ? `Timeout ap\xF3s ${this.timeout}ms` : netErr.message || "Falha de rede"
77
+ );
78
+ if (attempt < this.maxRetries) {
79
+ await sleep(backoffMs(attempt));
80
+ continue;
81
+ }
82
+ throw lastError;
83
+ }
84
+ clearTimeout(timer);
85
+ const requestId = res.headers.get("x-request-id") || void 0;
86
+ if (res.status >= 200 && res.status < 300) {
87
+ if (res.status === 204) return void 0;
88
+ const text2 = await res.text();
89
+ return text2 ? JSON.parse(text2) : void 0;
90
+ }
91
+ const text = await res.text().catch(() => "");
92
+ let parsed = text;
93
+ try {
94
+ parsed = text ? JSON.parse(text) : null;
95
+ } catch {
96
+ }
97
+ const err = mapErrorResponse(res.status, parsed, requestId);
98
+ const retriable = res.status === 429 || res.status >= 500;
99
+ if (retriable && attempt < this.maxRetries) {
100
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
101
+ await sleep(retryAfter ?? backoffMs(attempt));
102
+ lastError = err;
103
+ continue;
104
+ }
105
+ throw err;
106
+ }
107
+ throw lastError ?? new VitrinError("Esgotou as tentativas sem erro identificado");
108
+ }
109
+ buildUrl(path, query) {
110
+ const url = new URL(this.baseUrl + (path.startsWith("/") ? path : `/${path}`));
111
+ if (query) {
112
+ for (const [k, v] of Object.entries(query)) {
113
+ if (v !== void 0 && v !== null && v !== "") {
114
+ url.searchParams.set(k, String(v));
115
+ }
116
+ }
117
+ }
118
+ return url.toString();
119
+ }
120
+ };
121
+ function mapErrorResponse(status, body, requestId) {
122
+ const message = extractErrorMessage(body) || `HTTP ${status}`;
123
+ const opts = { statusCode: status, body, requestId };
124
+ if (status === 401 || status === 403) return new VitrinAuthError(message, opts);
125
+ if (status === 404) return new VitrinNotFoundError(message, opts);
126
+ if (status === 429) return new VitrinRateLimitError(message, opts);
127
+ if (status >= 500) return new VitrinServerError(message, opts);
128
+ if (status === 400 || status === 422) {
129
+ const fieldErrors = extractFieldErrors(body);
130
+ return new VitrinValidationError(message, { ...opts, fieldErrors });
131
+ }
132
+ return new VitrinError(message, opts);
133
+ }
134
+ function extractErrorMessage(body) {
135
+ if (!body || typeof body !== "object") return void 0;
136
+ const b = body;
137
+ if (typeof b.error === "string") return b.error;
138
+ if (typeof b.detail === "string") return b.detail;
139
+ for (const v of Object.values(b)) {
140
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
141
+ }
142
+ return void 0;
143
+ }
144
+ function extractFieldErrors(body) {
145
+ if (!body || typeof body !== "object") return void 0;
146
+ const out = {};
147
+ for (const [k, v] of Object.entries(body)) {
148
+ if (Array.isArray(v) && v.every((item) => typeof item === "string")) {
149
+ out[k] = v;
150
+ }
151
+ }
152
+ return Object.keys(out).length > 0 ? out : void 0;
153
+ }
154
+ function parseRetryAfter(header) {
155
+ if (!header) return null;
156
+ const seconds = Number(header);
157
+ if (!Number.isNaN(seconds)) return seconds * 1e3;
158
+ return null;
159
+ }
160
+ function backoffMs(attempt) {
161
+ const base = 250 * Math.pow(2, attempt);
162
+ return base + Math.floor(Math.random() * base * 0.5);
163
+ }
164
+ function sleep(ms) {
165
+ return new Promise((resolve) => setTimeout(resolve, ms));
166
+ }
167
+
168
+ // src/resources/charges.ts
169
+ var Charges = class {
170
+ constructor(client) {
171
+ this.client = client;
172
+ }
173
+ client;
174
+ /** Cria uma cobrança avulsa (PIX/Boleto/Cartão). */
175
+ create(params) {
176
+ const { idempotencyKey, ...body } = params;
177
+ return this.client.request("/charges/", {
178
+ method: "POST",
179
+ body,
180
+ idempotencyKey
181
+ });
182
+ }
183
+ retrieve(id) {
184
+ return this.client.request(`/transactions/${id}/`);
185
+ }
186
+ list(params = {}) {
187
+ return this.client.request("/reports/transactions/", {
188
+ query: params
189
+ });
190
+ }
191
+ refund(id, params) {
192
+ return this.client.request(`/payments/${id}/refund/`, {
193
+ method: "POST",
194
+ body: params
195
+ });
196
+ }
197
+ cancel(id, pin) {
198
+ return this.client.request(`/payments/${id}/cancel/`, {
199
+ method: "POST",
200
+ body: { pin }
201
+ });
202
+ }
203
+ /** Status atual da transação (consultado no provedor). */
204
+ status(id) {
205
+ return this.client.request(
206
+ `/payments/${id}/status/`
207
+ );
208
+ }
209
+ };
210
+
211
+ // src/resources/customers.ts
212
+ var Customers = class {
213
+ constructor(client) {
214
+ this.client = client;
215
+ }
216
+ client;
217
+ create(params) {
218
+ return this.client.request("/customers/", {
219
+ method: "POST",
220
+ body: params
221
+ });
222
+ }
223
+ retrieve(id) {
224
+ return this.client.request(`/customers/${id}/`);
225
+ }
226
+ list(params = {}) {
227
+ return this.client.request("/customers/", {
228
+ query: params
229
+ });
230
+ }
231
+ update(id, params) {
232
+ return this.client.request(`/customers/${id}/`, {
233
+ method: "PATCH",
234
+ body: params
235
+ });
236
+ }
237
+ delete(id) {
238
+ return this.client.request(`/customers/${id}/`, { method: "DELETE" });
239
+ }
240
+ };
241
+
242
+ // src/resources/plans.ts
243
+ var Plans = class {
244
+ constructor(client) {
245
+ this.client = client;
246
+ }
247
+ client;
248
+ create(params) {
249
+ return this.client.request("/plans/", { method: "POST", body: params });
250
+ }
251
+ retrieve(id) {
252
+ return this.client.request(`/plans/${id}/`);
253
+ }
254
+ list(params = {}) {
255
+ return this.client.request("/plans/", { query: params });
256
+ }
257
+ update(id, params) {
258
+ return this.client.request(`/plans/${id}/`, {
259
+ method: "PATCH",
260
+ body: params
261
+ });
262
+ }
263
+ delete(id) {
264
+ return this.client.request(`/plans/${id}/`, { method: "DELETE" });
265
+ }
266
+ };
267
+
268
+ // src/resources/subscriptions.ts
269
+ var Subscriptions = class {
270
+ constructor(client) {
271
+ this.client = client;
272
+ }
273
+ client;
274
+ create(params) {
275
+ return this.client.request("/subscriptions/", {
276
+ method: "POST",
277
+ body: params
278
+ });
279
+ }
280
+ retrieve(id) {
281
+ return this.client.request(`/subscriptions/${id}/`);
282
+ }
283
+ list(params = {}) {
284
+ return this.client.request("/subscriptions/", {
285
+ query: params
286
+ });
287
+ }
288
+ cancel(id, pin) {
289
+ return this.client.request(`/subscriptions/${id}/cancel/`, {
290
+ method: "POST",
291
+ body: { pin }
292
+ });
293
+ }
294
+ };
295
+
296
+ // src/resources/products.ts
297
+ var Products = class {
298
+ constructor(client) {
299
+ this.client = client;
300
+ }
301
+ client;
302
+ create(params) {
303
+ return this.client.request("/products/", {
304
+ method: "POST",
305
+ body: params
306
+ });
307
+ }
308
+ retrieve(id) {
309
+ return this.client.request(`/products/${id}/`);
310
+ }
311
+ list(params = {}) {
312
+ return this.client.request("/products/", { query: params });
313
+ }
314
+ update(id, params) {
315
+ return this.client.request(`/products/${id}/`, {
316
+ method: "PATCH",
317
+ body: params
318
+ });
319
+ }
320
+ delete(id) {
321
+ return this.client.request(`/products/${id}/`, { method: "DELETE" });
322
+ }
323
+ };
324
+
325
+ // src/resources/balance.ts
326
+ var BalanceAPI = class {
327
+ constructor(client) {
328
+ this.client = client;
329
+ }
330
+ client;
331
+ /** Saldo atual: disponível, pendente e total. */
332
+ retrieve() {
333
+ return this.client.request("/balance/");
334
+ }
335
+ /** Cronograma de recebíveis futuros (Pix D+1, Boleto D+2, Cartão D+30·N). */
336
+ scheduled(daysAhead = 90) {
337
+ return this.client.request("/balance/scheduled/", {
338
+ query: { days_ahead: daysAhead }
339
+ });
340
+ }
341
+ };
342
+
343
+ // src/webhooks.ts
344
+ import { createHmac, timingSafeEqual } from "crypto";
345
+ var Webhooks = {
346
+ /** Valida a assinatura e retorna o evento parseado. Lança em qualquer falha. */
347
+ constructEvent(opts) {
348
+ const { payload, signature, timestamp, eventType, eventId, secret, toleranceMs = 3e5 } = opts;
349
+ if (!signature) {
350
+ throw new VitrinError("Header X-Vitrin-Signature ausente.");
351
+ }
352
+ if (!secret) {
353
+ throw new VitrinError("webhook secret \xE9 obrigat\xF3rio.");
354
+ }
355
+ const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
356
+ const expected = createHmac("sha256", secret).update(raw).digest("hex");
357
+ const got = signature.trim();
358
+ if (expected.length !== got.length || !timingSafeEqual(Buffer.from(expected, "hex"), bufferFromHex(got))) {
359
+ throw new VitrinError("Assinatura inv\xE1lida \u2014 payload pode ter sido adulterado.");
360
+ }
361
+ if (toleranceMs > 0 && timestamp) {
362
+ const tsSec = Number(timestamp);
363
+ if (!Number.isFinite(tsSec)) {
364
+ throw new VitrinError("X-Vitrin-Timestamp inv\xE1lido.");
365
+ }
366
+ const tsMs = tsSec * 1e3;
367
+ const drift = Math.abs(Date.now() - tsMs);
368
+ if (drift > toleranceMs) {
369
+ throw new VitrinError(
370
+ `Webhook fora da janela de toler\xE2ncia (drift ${drift}ms > ${toleranceMs}ms).`
371
+ );
372
+ }
373
+ }
374
+ let data;
375
+ try {
376
+ data = JSON.parse(raw.toString("utf8"));
377
+ } catch {
378
+ throw new VitrinError("Payload n\xE3o \xE9 JSON v\xE1lido.");
379
+ }
380
+ return {
381
+ type: eventType || "",
382
+ id: eventId || "",
383
+ timestampMs: timestamp ? Number(timestamp) * 1e3 : Date.now(),
384
+ data
385
+ };
386
+ }
387
+ };
388
+ function bufferFromHex(hex) {
389
+ if (hex.length % 2 !== 0) return Buffer.alloc(0);
390
+ return Buffer.from(hex, "hex");
391
+ }
392
+
393
+ // src/index.ts
394
+ var Vitrin = class {
395
+ client;
396
+ charges;
397
+ customers;
398
+ plans;
399
+ subscriptions;
400
+ products;
401
+ balance;
402
+ constructor(opts) {
403
+ this.client = new VitrinClient(opts);
404
+ this.charges = new Charges(this.client);
405
+ this.customers = new Customers(this.client);
406
+ this.plans = new Plans(this.client);
407
+ this.subscriptions = new Subscriptions(this.client);
408
+ this.products = new Products(this.client);
409
+ this.balance = new BalanceAPI(this.client);
410
+ }
411
+ /** Acesso bruto pro caso de endpoint não coberto pelos resources. */
412
+ request(path, options) {
413
+ return this.client.request(path, options);
414
+ }
415
+ };
416
+ export {
417
+ Vitrin,
418
+ VitrinAuthError,
419
+ VitrinError,
420
+ VitrinNetworkError,
421
+ VitrinNotFoundError,
422
+ VitrinRateLimitError,
423
+ VitrinServerError,
424
+ VitrinValidationError,
425
+ Webhooks
426
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@vitrindigital/node",
3
+ "version": "0.1.0",
4
+ "description": "SDK oficial Node.js para a API da Vitrin Digital",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": ["dist", "README.md"],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "lint": "tsc --noEmit"
25
+ },
26
+ "keywords": ["vitrin", "vitrin-digital", "pagamentos", "pix", "boleto", "checkout"],
27
+ "author": "Vitrin Digital",
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "@types/node": "^22.10.0",
31
+ "tsup": "^8.3.5",
32
+ "typescript": "^5.7.2",
33
+ "vitest": "^2.1.8"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }