@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/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
|
+
}
|