@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 +185 -0
- package/dist/index.cjs +461 -0
- package/dist/index.d.cts +428 -0
- package/dist/index.d.ts +428 -0
- package/dist/index.js +426 -0
- package/package.json +38 -0
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
|
+
});
|