@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/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # @vitrindigital/node
2
+
3
+ SDK oficial Node.js para a [API da Vitrin Digital](https://api.vitrin.digital).
4
+
5
+ ```bash
6
+ npm install @vitrindigital/node
7
+ ```
8
+
9
+ > **Node 18+ requerido.** Usa `fetch` nativo, sem dependências externas em runtime.
10
+
11
+ ## Setup
12
+
13
+ ```ts
14
+ import { Vitrin } from '@vitrindigital/node';
15
+
16
+ const vitrin = new Vitrin({
17
+ apiKey: process.env.VITRIN_API_KEY!,
18
+ // Opcionais:
19
+ // baseUrl: 'https://api.vitrin.digital/api/v1',
20
+ // timeout: 30_000,
21
+ // maxRetries: 3,
22
+ });
23
+ ```
24
+
25
+ Use a chave `vd_test_*` em desenvolvimento e `vd_live_*` em produção.
26
+
27
+ ## Recursos
28
+
29
+ ### Clientes
30
+
31
+ ```ts
32
+ const customer = await vitrin.customers.create({
33
+ name: 'Maria Silva',
34
+ email: 'maria@example.com',
35
+ cpf_cnpj: '12345678901',
36
+ });
37
+
38
+ const list = await vitrin.customers.list({ page: 1 });
39
+ const updated = await vitrin.customers.update(customer.id, { phone: '11987654321' });
40
+ await vitrin.customers.delete(customer.id);
41
+ ```
42
+
43
+ ### Cobranças
44
+
45
+ ```ts
46
+ const charge = await vitrin.charges.create({
47
+ customer_id: customer.id,
48
+ amount: 99.90,
49
+ billing_type: 'PIX',
50
+ description: 'Mensalidade abril',
51
+ // Recomendado em fluxos com retry — evita duplo-débito
52
+ idempotencyKey: `mensalidade-${customer.id}-2026-04`,
53
+ });
54
+
55
+ console.log(charge.pix_qr_code);
56
+ console.log(charge.pix_copy_paste);
57
+
58
+ const refunded = await vitrin.charges.refund(charge.id, {
59
+ amount: 50.0, // omitir = total
60
+ pin: '123456',
61
+ });
62
+ ```
63
+
64
+ ### Planos & Assinaturas
65
+
66
+ ```ts
67
+ const plan = await vitrin.plans.create({
68
+ name: 'Pro Mensal',
69
+ price: 99.0,
70
+ billing_cycle: 'monthly',
71
+ });
72
+
73
+ const sub = await vitrin.subscriptions.create({
74
+ customer_id: customer.id,
75
+ plan_id: plan.id,
76
+ billing_type: 'CREDIT_CARD',
77
+ credit_card_token: 'tok_xxx',
78
+ });
79
+
80
+ await vitrin.subscriptions.cancel(sub.id, '123456');
81
+ ```
82
+
83
+ ### Saldo & Recebíveis
84
+
85
+ ```ts
86
+ const balance = await vitrin.balance.retrieve();
87
+ // → { available, total, pending, withdrawal_fees: { pix, ted } }
88
+
89
+ const scheduled = await vitrin.balance.scheduled(90);
90
+ // → cronograma 90 dias: PIX D+1, Boleto D+2, Cartão Nx D+30·n
91
+ ```
92
+
93
+ ## Webhooks
94
+
95
+ Valide a assinatura HMAC do webhook antes de processar:
96
+
97
+ ```ts
98
+ import { Webhooks, VitrinError } from '@vitrindigital/node';
99
+ import express from 'express';
100
+
101
+ const app = express();
102
+
103
+ // IMPORTANTE: aceite o body cru, NÃO json parsed
104
+ app.post('/webhooks/vitrin', express.raw({ type: 'application/json' }), (req, res) => {
105
+ try {
106
+ const event = Webhooks.constructEvent({
107
+ payload: req.body, // Buffer
108
+ signature: req.header('x-vitrin-signature'),
109
+ timestamp: req.header('x-vitrin-timestamp'),
110
+ eventType: req.header('x-vitrin-event'),
111
+ eventId: req.header('x-vitrin-event-id'),
112
+ secret: process.env.VITRIN_WEBHOOK_SECRET!,
113
+ });
114
+
115
+ console.log(event.type, event.id, event.data);
116
+ res.status(200).send();
117
+ } catch (err) {
118
+ if (err instanceof VitrinError) {
119
+ console.error('Webhook invalid:', err.message);
120
+ return res.status(400).send();
121
+ }
122
+ throw err;
123
+ }
124
+ });
125
+ ```
126
+
127
+ ## Tratamento de erros
128
+
129
+ Toda chamada lança subclasses de `VitrinError`:
130
+
131
+ ```ts
132
+ import {
133
+ VitrinError, VitrinAuthError, VitrinValidationError,
134
+ VitrinRateLimitError, VitrinNotFoundError, VitrinServerError,
135
+ } from '@vitrindigital/node';
136
+
137
+ try {
138
+ await vitrin.charges.create({ /* ... */ });
139
+ } catch (err) {
140
+ if (err instanceof VitrinValidationError) {
141
+ console.log('Campos inválidos:', err.fieldErrors);
142
+ } else if (err instanceof VitrinAuthError) {
143
+ console.log('Chave inválida ou sem permissão');
144
+ } else if (err instanceof VitrinRateLimitError) {
145
+ console.log('Aguarde antes de tentar de novo');
146
+ } else if (err instanceof VitrinError) {
147
+ console.log('Erro Vitrin:', err.statusCode, err.requestId, err.message);
148
+ } else {
149
+ throw err;
150
+ }
151
+ }
152
+ ```
153
+
154
+ O cliente faz **retry automático** em `429` e `5xx` com backoff exponencial
155
+ (default: 3 tentativas). Erros 4xx (exceto 429) não são retentados.
156
+
157
+ ## Idempotência
158
+
159
+ Inclua `idempotencyKey` em POSTs sensíveis. Se o request chegar duas vezes
160
+ (retry de rede, deploy etc), a Vitrin reconhece pela chave e devolve a mesma
161
+ resposta — sem cobrar duas vezes.
162
+
163
+ ```ts
164
+ await vitrin.charges.create({
165
+ customer_id: 'cus_1',
166
+ amount: 100,
167
+ billing_type: 'PIX',
168
+ idempotencyKey: `pedido-${orderId}`, // único por pedido
169
+ });
170
+ ```
171
+
172
+ ## Acesso bruto
173
+
174
+ Pra endpoints ainda não cobertos pelos resources:
175
+
176
+ ```ts
177
+ const data = await vitrin.request<MyType>('/some/path/', {
178
+ method: 'POST',
179
+ body: { foo: 'bar' },
180
+ });
181
+ ```
182
+
183
+ ## Licença
184
+
185
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,461 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Vitrin: () => Vitrin,
24
+ VitrinAuthError: () => VitrinAuthError,
25
+ VitrinError: () => VitrinError,
26
+ VitrinNetworkError: () => VitrinNetworkError,
27
+ VitrinNotFoundError: () => VitrinNotFoundError,
28
+ VitrinRateLimitError: () => VitrinRateLimitError,
29
+ VitrinServerError: () => VitrinServerError,
30
+ VitrinValidationError: () => VitrinValidationError,
31
+ Webhooks: () => Webhooks
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/errors.ts
36
+ var VitrinError = class extends Error {
37
+ statusCode;
38
+ body;
39
+ requestId;
40
+ constructor(message, opts = {}) {
41
+ super(message);
42
+ this.name = this.constructor.name;
43
+ this.statusCode = opts.statusCode;
44
+ this.body = opts.body;
45
+ this.requestId = opts.requestId;
46
+ }
47
+ };
48
+ var VitrinAuthError = class extends VitrinError {
49
+ };
50
+ var VitrinValidationError = class extends VitrinError {
51
+ fieldErrors;
52
+ constructor(message, opts = {}) {
53
+ super(message, opts);
54
+ this.fieldErrors = opts.fieldErrors;
55
+ }
56
+ };
57
+ var VitrinRateLimitError = class extends VitrinError {
58
+ };
59
+ var VitrinNotFoundError = class extends VitrinError {
60
+ };
61
+ var VitrinServerError = class extends VitrinError {
62
+ };
63
+ var VitrinNetworkError = class extends VitrinError {
64
+ };
65
+
66
+ // src/client.ts
67
+ var DEFAULT_BASE_URL = "https://api.vitrin.digital/api/v1";
68
+ var SDK_VERSION = "0.1.0";
69
+ var VitrinClient = class {
70
+ baseUrl;
71
+ apiKey;
72
+ timeout;
73
+ maxRetries;
74
+ constructor(opts) {
75
+ if (!opts.apiKey) {
76
+ throw new Error("apiKey \xE9 obrigat\xF3rio");
77
+ }
78
+ this.apiKey = opts.apiKey;
79
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
80
+ this.timeout = opts.timeout ?? 3e4;
81
+ this.maxRetries = opts.maxRetries ?? 3;
82
+ }
83
+ /** Helper público pra resources. */
84
+ async request(path, options = {}) {
85
+ const { method = "GET", body, query, idempotencyKey } = options;
86
+ const url = this.buildUrl(path, query);
87
+ const headers = {
88
+ Authorization: `Bearer ${this.apiKey}`,
89
+ Accept: "application/json",
90
+ "User-Agent": `vitrindigital-node/${SDK_VERSION} node/${process.version}`
91
+ };
92
+ if (body !== void 0) headers["Content-Type"] = "application/json";
93
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
94
+ let lastError = null;
95
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
96
+ const controller = new AbortController();
97
+ const timer = setTimeout(() => controller.abort(), this.timeout);
98
+ let res;
99
+ try {
100
+ res = await fetch(url, {
101
+ method,
102
+ headers,
103
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
104
+ signal: controller.signal
105
+ });
106
+ } catch (err2) {
107
+ clearTimeout(timer);
108
+ const netErr = err2;
109
+ lastError = new VitrinNetworkError(
110
+ netErr.name === "AbortError" ? `Timeout ap\xF3s ${this.timeout}ms` : netErr.message || "Falha de rede"
111
+ );
112
+ if (attempt < this.maxRetries) {
113
+ await sleep(backoffMs(attempt));
114
+ continue;
115
+ }
116
+ throw lastError;
117
+ }
118
+ clearTimeout(timer);
119
+ const requestId = res.headers.get("x-request-id") || void 0;
120
+ if (res.status >= 200 && res.status < 300) {
121
+ if (res.status === 204) return void 0;
122
+ const text2 = await res.text();
123
+ return text2 ? JSON.parse(text2) : void 0;
124
+ }
125
+ const text = await res.text().catch(() => "");
126
+ let parsed = text;
127
+ try {
128
+ parsed = text ? JSON.parse(text) : null;
129
+ } catch {
130
+ }
131
+ const err = mapErrorResponse(res.status, parsed, requestId);
132
+ const retriable = res.status === 429 || res.status >= 500;
133
+ if (retriable && attempt < this.maxRetries) {
134
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
135
+ await sleep(retryAfter ?? backoffMs(attempt));
136
+ lastError = err;
137
+ continue;
138
+ }
139
+ throw err;
140
+ }
141
+ throw lastError ?? new VitrinError("Esgotou as tentativas sem erro identificado");
142
+ }
143
+ buildUrl(path, query) {
144
+ const url = new URL(this.baseUrl + (path.startsWith("/") ? path : `/${path}`));
145
+ if (query) {
146
+ for (const [k, v] of Object.entries(query)) {
147
+ if (v !== void 0 && v !== null && v !== "") {
148
+ url.searchParams.set(k, String(v));
149
+ }
150
+ }
151
+ }
152
+ return url.toString();
153
+ }
154
+ };
155
+ function mapErrorResponse(status, body, requestId) {
156
+ const message = extractErrorMessage(body) || `HTTP ${status}`;
157
+ const opts = { statusCode: status, body, requestId };
158
+ if (status === 401 || status === 403) return new VitrinAuthError(message, opts);
159
+ if (status === 404) return new VitrinNotFoundError(message, opts);
160
+ if (status === 429) return new VitrinRateLimitError(message, opts);
161
+ if (status >= 500) return new VitrinServerError(message, opts);
162
+ if (status === 400 || status === 422) {
163
+ const fieldErrors = extractFieldErrors(body);
164
+ return new VitrinValidationError(message, { ...opts, fieldErrors });
165
+ }
166
+ return new VitrinError(message, opts);
167
+ }
168
+ function extractErrorMessage(body) {
169
+ if (!body || typeof body !== "object") return void 0;
170
+ const b = body;
171
+ if (typeof b.error === "string") return b.error;
172
+ if (typeof b.detail === "string") return b.detail;
173
+ for (const v of Object.values(b)) {
174
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
175
+ }
176
+ return void 0;
177
+ }
178
+ function extractFieldErrors(body) {
179
+ if (!body || typeof body !== "object") return void 0;
180
+ const out = {};
181
+ for (const [k, v] of Object.entries(body)) {
182
+ if (Array.isArray(v) && v.every((item) => typeof item === "string")) {
183
+ out[k] = v;
184
+ }
185
+ }
186
+ return Object.keys(out).length > 0 ? out : void 0;
187
+ }
188
+ function parseRetryAfter(header) {
189
+ if (!header) return null;
190
+ const seconds = Number(header);
191
+ if (!Number.isNaN(seconds)) return seconds * 1e3;
192
+ return null;
193
+ }
194
+ function backoffMs(attempt) {
195
+ const base = 250 * Math.pow(2, attempt);
196
+ return base + Math.floor(Math.random() * base * 0.5);
197
+ }
198
+ function sleep(ms) {
199
+ return new Promise((resolve) => setTimeout(resolve, ms));
200
+ }
201
+
202
+ // src/resources/charges.ts
203
+ var Charges = class {
204
+ constructor(client) {
205
+ this.client = client;
206
+ }
207
+ client;
208
+ /** Cria uma cobrança avulsa (PIX/Boleto/Cartão). */
209
+ create(params) {
210
+ const { idempotencyKey, ...body } = params;
211
+ return this.client.request("/charges/", {
212
+ method: "POST",
213
+ body,
214
+ idempotencyKey
215
+ });
216
+ }
217
+ retrieve(id) {
218
+ return this.client.request(`/transactions/${id}/`);
219
+ }
220
+ list(params = {}) {
221
+ return this.client.request("/reports/transactions/", {
222
+ query: params
223
+ });
224
+ }
225
+ refund(id, params) {
226
+ return this.client.request(`/payments/${id}/refund/`, {
227
+ method: "POST",
228
+ body: params
229
+ });
230
+ }
231
+ cancel(id, pin) {
232
+ return this.client.request(`/payments/${id}/cancel/`, {
233
+ method: "POST",
234
+ body: { pin }
235
+ });
236
+ }
237
+ /** Status atual da transação (consultado no provedor). */
238
+ status(id) {
239
+ return this.client.request(
240
+ `/payments/${id}/status/`
241
+ );
242
+ }
243
+ };
244
+
245
+ // src/resources/customers.ts
246
+ var Customers = class {
247
+ constructor(client) {
248
+ this.client = client;
249
+ }
250
+ client;
251
+ create(params) {
252
+ return this.client.request("/customers/", {
253
+ method: "POST",
254
+ body: params
255
+ });
256
+ }
257
+ retrieve(id) {
258
+ return this.client.request(`/customers/${id}/`);
259
+ }
260
+ list(params = {}) {
261
+ return this.client.request("/customers/", {
262
+ query: params
263
+ });
264
+ }
265
+ update(id, params) {
266
+ return this.client.request(`/customers/${id}/`, {
267
+ method: "PATCH",
268
+ body: params
269
+ });
270
+ }
271
+ delete(id) {
272
+ return this.client.request(`/customers/${id}/`, { method: "DELETE" });
273
+ }
274
+ };
275
+
276
+ // src/resources/plans.ts
277
+ var Plans = class {
278
+ constructor(client) {
279
+ this.client = client;
280
+ }
281
+ client;
282
+ create(params) {
283
+ return this.client.request("/plans/", { method: "POST", body: params });
284
+ }
285
+ retrieve(id) {
286
+ return this.client.request(`/plans/${id}/`);
287
+ }
288
+ list(params = {}) {
289
+ return this.client.request("/plans/", { query: params });
290
+ }
291
+ update(id, params) {
292
+ return this.client.request(`/plans/${id}/`, {
293
+ method: "PATCH",
294
+ body: params
295
+ });
296
+ }
297
+ delete(id) {
298
+ return this.client.request(`/plans/${id}/`, { method: "DELETE" });
299
+ }
300
+ };
301
+
302
+ // src/resources/subscriptions.ts
303
+ var Subscriptions = class {
304
+ constructor(client) {
305
+ this.client = client;
306
+ }
307
+ client;
308
+ create(params) {
309
+ return this.client.request("/subscriptions/", {
310
+ method: "POST",
311
+ body: params
312
+ });
313
+ }
314
+ retrieve(id) {
315
+ return this.client.request(`/subscriptions/${id}/`);
316
+ }
317
+ list(params = {}) {
318
+ return this.client.request("/subscriptions/", {
319
+ query: params
320
+ });
321
+ }
322
+ cancel(id, pin) {
323
+ return this.client.request(`/subscriptions/${id}/cancel/`, {
324
+ method: "POST",
325
+ body: { pin }
326
+ });
327
+ }
328
+ };
329
+
330
+ // src/resources/products.ts
331
+ var Products = class {
332
+ constructor(client) {
333
+ this.client = client;
334
+ }
335
+ client;
336
+ create(params) {
337
+ return this.client.request("/products/", {
338
+ method: "POST",
339
+ body: params
340
+ });
341
+ }
342
+ retrieve(id) {
343
+ return this.client.request(`/products/${id}/`);
344
+ }
345
+ list(params = {}) {
346
+ return this.client.request("/products/", { query: params });
347
+ }
348
+ update(id, params) {
349
+ return this.client.request(`/products/${id}/`, {
350
+ method: "PATCH",
351
+ body: params
352
+ });
353
+ }
354
+ delete(id) {
355
+ return this.client.request(`/products/${id}/`, { method: "DELETE" });
356
+ }
357
+ };
358
+
359
+ // src/resources/balance.ts
360
+ var BalanceAPI = class {
361
+ constructor(client) {
362
+ this.client = client;
363
+ }
364
+ client;
365
+ /** Saldo atual: disponível, pendente e total. */
366
+ retrieve() {
367
+ return this.client.request("/balance/");
368
+ }
369
+ /** Cronograma de recebíveis futuros (Pix D+1, Boleto D+2, Cartão D+30·N). */
370
+ scheduled(daysAhead = 90) {
371
+ return this.client.request("/balance/scheduled/", {
372
+ query: { days_ahead: daysAhead }
373
+ });
374
+ }
375
+ };
376
+
377
+ // src/webhooks.ts
378
+ var import_node_crypto = require("crypto");
379
+ var Webhooks = {
380
+ /** Valida a assinatura e retorna o evento parseado. Lança em qualquer falha. */
381
+ constructEvent(opts) {
382
+ const { payload, signature, timestamp, eventType, eventId, secret, toleranceMs = 3e5 } = opts;
383
+ if (!signature) {
384
+ throw new VitrinError("Header X-Vitrin-Signature ausente.");
385
+ }
386
+ if (!secret) {
387
+ throw new VitrinError("webhook secret \xE9 obrigat\xF3rio.");
388
+ }
389
+ const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
390
+ const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(raw).digest("hex");
391
+ const got = signature.trim();
392
+ if (expected.length !== got.length || !(0, import_node_crypto.timingSafeEqual)(Buffer.from(expected, "hex"), bufferFromHex(got))) {
393
+ throw new VitrinError("Assinatura inv\xE1lida \u2014 payload pode ter sido adulterado.");
394
+ }
395
+ if (toleranceMs > 0 && timestamp) {
396
+ const tsSec = Number(timestamp);
397
+ if (!Number.isFinite(tsSec)) {
398
+ throw new VitrinError("X-Vitrin-Timestamp inv\xE1lido.");
399
+ }
400
+ const tsMs = tsSec * 1e3;
401
+ const drift = Math.abs(Date.now() - tsMs);
402
+ if (drift > toleranceMs) {
403
+ throw new VitrinError(
404
+ `Webhook fora da janela de toler\xE2ncia (drift ${drift}ms > ${toleranceMs}ms).`
405
+ );
406
+ }
407
+ }
408
+ let data;
409
+ try {
410
+ data = JSON.parse(raw.toString("utf8"));
411
+ } catch {
412
+ throw new VitrinError("Payload n\xE3o \xE9 JSON v\xE1lido.");
413
+ }
414
+ return {
415
+ type: eventType || "",
416
+ id: eventId || "",
417
+ timestampMs: timestamp ? Number(timestamp) * 1e3 : Date.now(),
418
+ data
419
+ };
420
+ }
421
+ };
422
+ function bufferFromHex(hex) {
423
+ if (hex.length % 2 !== 0) return Buffer.alloc(0);
424
+ return Buffer.from(hex, "hex");
425
+ }
426
+
427
+ // src/index.ts
428
+ var Vitrin = class {
429
+ client;
430
+ charges;
431
+ customers;
432
+ plans;
433
+ subscriptions;
434
+ products;
435
+ balance;
436
+ constructor(opts) {
437
+ this.client = new VitrinClient(opts);
438
+ this.charges = new Charges(this.client);
439
+ this.customers = new Customers(this.client);
440
+ this.plans = new Plans(this.client);
441
+ this.subscriptions = new Subscriptions(this.client);
442
+ this.products = new Products(this.client);
443
+ this.balance = new BalanceAPI(this.client);
444
+ }
445
+ /** Acesso bruto pro caso de endpoint não coberto pelos resources. */
446
+ request(path, options) {
447
+ return this.client.request(path, options);
448
+ }
449
+ };
450
+ // Annotate the CommonJS export names for ESM import in node:
451
+ 0 && (module.exports = {
452
+ Vitrin,
453
+ VitrinAuthError,
454
+ VitrinError,
455
+ VitrinNetworkError,
456
+ VitrinNotFoundError,
457
+ VitrinRateLimitError,
458
+ VitrinServerError,
459
+ VitrinValidationError,
460
+ Webhooks
461
+ });