@validapay/tokenize 1.1.0 → 1.3.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/package.json CHANGED
@@ -1,21 +1,26 @@
1
1
  {
2
2
  "name": "@validapay/tokenize",
3
- "version": "1.1.0",
4
- "description": "SDK de tokenização de cartão ValidaPay",
3
+ "version": "1.3.0",
4
+ "description": "Tokenização de cartão e identificação do dispositivo para antifraude ValidaPay",
5
5
  "type": "module",
6
- "main": "index.js",
6
+ "main": "src/index.js",
7
7
  "exports": {
8
- ".": "./index.js"
8
+ ".": "./src/index.js",
9
+ "./browser": "./src/browser.js"
9
10
  },
10
11
  "files": [
11
- "index.js"
12
+ "src"
12
13
  ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
13
17
  "publishConfig": {
14
18
  "access": "public"
15
19
  },
16
20
  "keywords": [
17
21
  "validapay",
18
22
  "tokenize",
23
+ "antifraude",
19
24
  "cartao",
20
25
  "pagamento",
21
26
  "sdk"
package/readme.md ADDED
@@ -0,0 +1,83 @@
1
+ # @validapay/tokenize
2
+
3
+ Salva o cartão do comprador na ValidaPay e identifica o dispositivo para a análise antifraude.
4
+
5
+ ```bash
6
+ npm install @validapay/tokenize
7
+ ```
8
+
9
+ Funciona de dois jeitos, conforme onde o cartão é recebido.
10
+
11
+ ## No servidor, com `clientId` e `clientSecret`
12
+
13
+ ```js
14
+ import { tokenize } from "@validapay/tokenize";
15
+
16
+ const paymentMethod = await tokenize({
17
+ clientId: process.env.VALIDAPAY_CLIENT_ID,
18
+ clientSecret: process.env.VALIDAPAY_CLIENT_SECRET,
19
+ card: { number: "4111111111111111", holderName: "MARIA SILVA", cvv: "123", expiration: "12/2030" },
20
+ customer: { name: "Maria Silva", document: "12345678901", email: "maria@exemplo.com" },
21
+ });
22
+ ```
23
+
24
+ - Devolve o cartão salvo com o `cardToken`, que vale 5 minutos. Integrações que já usavam o `paymentMethodId` continuam recebendo o campo; as novas recebem só o `cardToken`.
25
+ - `dev: true` usa o ambiente de desenvolvimento.
26
+ - `card.cardHolderName` continua aceito no lugar de `card.holderName`.
27
+ - O `clientSecret` nunca deve ir para o navegador.
28
+
29
+ ## No navegador, com `publicId`
30
+
31
+ ```js
32
+ import { tokenize } from "@validapay/tokenize/browser";
33
+
34
+ const { cardToken, cardBrand, cardLastFour } = await tokenize({
35
+ publicId: "3f1c2a8e-8a4b-4c1e-9d3a-2b7e6f5a4c10",
36
+ card,
37
+ customer,
38
+ });
39
+ ```
40
+
41
+ - O cartão vai do navegador direto para a ValidaPay, sem passar pelo seu servidor.
42
+ - Devolve só `cardToken`, `cardTokenExpiresAt`, `cardBrand` e `cardLastFour`.
43
+ - `environment`: `production` (padrão), `sandbox` ou `development`.
44
+
45
+ ## Identificação do dispositivo (antifraude)
46
+
47
+ Roda no navegador. Chame assim que a página de checkout abrir, uma vez por pedido: a identificação precisa desse tempo antes do pagamento.
48
+
49
+ ```js
50
+ import { collectDevice } from "@validapay/tokenize/browser";
51
+
52
+ const { deviceId } = await collectDevice({ publicId: "3f1c2a8e-8a4b-4c1e-9d3a-2b7e6f5a4c10" });
53
+ ```
54
+
55
+ - `deviceId` é `null` quando a conta não usa análise antifraude. Pode chamar sempre.
56
+ - Não coleta dados pessoais nem dados do cartão.
57
+ - Vale para as duas formas de tokenizar: se o cartão vai para o seu servidor, envie o `deviceId` junto.
58
+
59
+ ## Com autenticação do portador
60
+
61
+ Se usar `@validapay/3ds`, passe o resultado em `authentication`. Vale para as duas formas:
62
+
63
+ ```js
64
+ const paymentMethod = await tokenize({ ..., authentication });
65
+ ```
66
+
67
+ ## Cobrança
68
+
69
+ Seu servidor cria a cobrança com o `cardToken` em até 5 minutos:
70
+
71
+ ```http
72
+ POST /v1/charges
73
+ { "paymentMethod": "creditcard", "amount": 149.9, "cardToken": "ctk_…", "customer": { … } }
74
+ ```
75
+
76
+ Inclua também `deviceId` e, se usar `@validapay/3ds`, `authentication` no mesmo corpo:
77
+
78
+ ```http
79
+ POST /v1/charges
80
+ { "paymentMethod": "creditcard", "amount": 149.9, "cardToken": "ctk_…", "deviceId": "…", "authentication": { … }, "customer": { … } }
81
+ ```
82
+
83
+ - Depois de 5 minutos o `cardToken` vence e a cobrança responde `CARD_TOKEN_EXPIRED`: tokenize o cartão de novo.
package/src/browser.js ADDED
@@ -0,0 +1,52 @@
1
+ import { ValidaPayError } from "./shared/errors.js";
2
+ import { openPaymentSession, postJson, resolveApiUrl } from "./shared/api.js";
3
+ import { assertBrowser, assertRequired, holderNameOf, onlyDigits } from "./shared/runtime.js";
4
+ import { mountFingerprint } from "./fingerprint.js";
5
+
6
+ function assertInput({ publicId, card, customer }) {
7
+ assertRequired([
8
+ ["publicId", publicId],
9
+ ["card.number", card?.number],
10
+ ["card.holderName", holderNameOf(card)],
11
+ ["card.cvv", card?.cvv],
12
+ ["card.expiration", card?.expiration],
13
+ ["customer.document", customer?.document],
14
+ ["customer.name", customer?.name],
15
+ ["customer.email", customer?.email],
16
+ ]);
17
+ }
18
+
19
+ function toRequestBody({ publicId, card, customer, authentication }) {
20
+ return {
21
+ publicId,
22
+ cardHolderName: holderNameOf(card),
23
+ number: onlyDigits(card.number),
24
+ cvv: card.cvv,
25
+ expiration: card.expiration,
26
+ customer: { document: onlyDigits(customer.document), name: customer.name, email: customer.email },
27
+ ...(authentication?.cardId && { threeDsCardId: authentication.cardId }),
28
+ };
29
+ }
30
+
31
+ export async function collectDevice({ publicId, environment = "production" }) {
32
+ assertBrowser("@validapay/tokenize/browser");
33
+ assertRequired([["publicId", publicId]]);
34
+
35
+ const session = await openPaymentSession(environment, publicId);
36
+ mountFingerprint(session.fingerprint);
37
+
38
+ return { deviceId: session.fingerprint?.dfpId ?? null };
39
+ }
40
+
41
+ export async function tokenize({ publicId, environment = "production", card, customer, authentication = null }) {
42
+ assertBrowser("@validapay/tokenize/browser");
43
+ assertInput({ publicId, card, customer });
44
+
45
+ return postJson(
46
+ `${resolveApiUrl(environment)}/v1/payment-sessions/payment-methods`,
47
+ toRequestBody({ publicId, card, customer, authentication }),
48
+ "Não foi possível salvar o cartão"
49
+ );
50
+ }
51
+
52
+ export { ValidaPayError };
@@ -0,0 +1,36 @@
1
+ const FINGERPRINT_BASE_URL = "https://h.online-metrix.net/fp";
2
+ const HIDDEN_FRAME_STYLE = "width:100px;height:100px;border:0;position:absolute;top:-5000px;";
3
+ const MOUNTED_ATTRIBUTE = "data-validapay-fingerprint";
4
+
5
+ function fingerprintQuery({ orgId, merchantId, dfpId }) {
6
+ return new URLSearchParams({ org_id: orgId, session_id: `${merchantId}${dfpId}` }).toString();
7
+ }
8
+
9
+ function isMounted(dfpId) {
10
+ return Boolean(document.querySelector(`[${MOUNTED_ATTRIBUTE}="${dfpId}"]`));
11
+ }
12
+
13
+ function appendScript(query, dfpId) {
14
+ const script = document.createElement("script");
15
+ script.src = `${FINGERPRINT_BASE_URL}/tags.js?${query}`;
16
+ script.async = true;
17
+ script.setAttribute(MOUNTED_ATTRIBUTE, dfpId);
18
+ document.head.appendChild(script);
19
+ }
20
+
21
+ function appendFallbackFrame(query, dfpId) {
22
+ const frame = document.createElement("iframe");
23
+ frame.src = `${FINGERPRINT_BASE_URL}/tags?${query}`;
24
+ frame.style.cssText = HIDDEN_FRAME_STYLE;
25
+ frame.setAttribute("aria-hidden", "true");
26
+ frame.setAttribute(MOUNTED_ATTRIBUTE, `${dfpId}-frame`);
27
+ document.body.appendChild(frame);
28
+ }
29
+
30
+ export function mountFingerprint(fingerprint) {
31
+ if (!fingerprint || isMounted(fingerprint.dfpId)) return;
32
+
33
+ const query = fingerprintQuery(fingerprint);
34
+ appendScript(query, fingerprint.dfpId);
35
+ appendFallbackFrame(query, fingerprint.dfpId);
36
+ }
package/src/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import { ValidaPayError, toApiError } from "./shared/errors.js";
2
+ import { assertRequired, holderNameOf } from "./shared/runtime.js";
3
+
4
+ const OAUTH_URL_PRD = "https://oauth2.validapay.com.br/auth/token";
5
+ const OAUTH_URL_DEV = "https://oauth2-dev.validapay.com.br/auth/token";
6
+ const API_URL_PRD = "https://api.validapay.com.br";
7
+ const API_URL_DEV = "https://dev.validapay.com.br";
8
+ const TOKENIZE_SCOPE = "payment.methods/write";
9
+
10
+ async function fetchToken({ clientId, clientSecret, dev }) {
11
+ const response = await fetch(dev ? OAUTH_URL_DEV : OAUTH_URL_PRD, {
12
+ method: "POST",
13
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
14
+ body: new URLSearchParams({
15
+ grant_type: "client_credentials",
16
+ client_id: clientId,
17
+ client_secret: clientSecret,
18
+ scope: TOKENIZE_SCOPE,
19
+ }).toString(),
20
+ });
21
+
22
+ if (!response.ok) throw await toApiError(response, "Falha ao obter token");
23
+
24
+ const data = await response.json();
25
+ return data.access_token;
26
+ }
27
+
28
+ function assertInput({ clientId, clientSecret, card, customer }) {
29
+ assertRequired([
30
+ ["clientId", clientId],
31
+ ["clientSecret", clientSecret],
32
+ ["card.number", card?.number],
33
+ ["card.cardHolderName", holderNameOf(card)],
34
+ ["card.cvv", card?.cvv],
35
+ ["card.expiration", card?.expiration],
36
+ ["customer.document", customer?.document],
37
+ ["customer.name", customer?.name],
38
+ ["customer.email", customer?.email],
39
+ ]);
40
+ }
41
+
42
+ export async function tokenize({ clientId, clientSecret, dev = false, card, customer, authentication = null }) {
43
+ assertInput({ clientId, clientSecret, card, customer });
44
+
45
+ const token = await fetchToken({ clientId, clientSecret, dev });
46
+
47
+ const response = await fetch(`${dev ? API_URL_DEV : API_URL_PRD}/v1/payment-methods/tokenize`, {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ Authorization: `Bearer ${token}`,
52
+ },
53
+ body: JSON.stringify({
54
+ cardHolderName: holderNameOf(card),
55
+ number: card.number,
56
+ cvv: card.cvv,
57
+ expiration: card.expiration,
58
+ customer,
59
+ ...(authentication?.cardId && { threeDsCardId: authentication.cardId }),
60
+ }),
61
+ });
62
+
63
+ if (!response.ok) throw await toApiError(response, "Erro ao tokenizar cartão");
64
+
65
+ return response.json();
66
+ }
67
+
68
+ export { ValidaPayError };
@@ -0,0 +1,31 @@
1
+ import { ValidaPayError, toApiError } from "./errors.js";
2
+
3
+ const API_URLS = {
4
+ production: "https://api.validapay.com.br",
5
+ sandbox: "https://sandbox.validapay.com.br",
6
+ development: "https://dev.validapay.com.br",
7
+ };
8
+
9
+ export function resolveApiUrl(environment) {
10
+ const apiUrl = API_URLS[environment];
11
+ if (!apiUrl) {
12
+ throw new ValidaPayError(`environment inválido: use ${Object.keys(API_URLS).join(", ")}`, "INVALID_ENVIRONMENT");
13
+ }
14
+ return apiUrl;
15
+ }
16
+
17
+ export async function postJson(url, body, fallbackMessage) {
18
+ const response = await fetch(url, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify(body),
22
+ });
23
+
24
+ if (!response.ok) throw await toApiError(response, fallbackMessage);
25
+
26
+ return response.json();
27
+ }
28
+
29
+ export function openPaymentSession(environment, publicId) {
30
+ return postJson(`${resolveApiUrl(environment)}/v1/payment-sessions`, { publicId }, "Não foi possível iniciar o pagamento");
31
+ }
@@ -0,0 +1,19 @@
1
+ export class ValidaPayError extends Error {
2
+ constructor(message, code, details = null) {
3
+ super(message);
4
+ this.name = "ValidaPayError";
5
+ this.code = code;
6
+ this.details = details;
7
+ }
8
+ }
9
+
10
+ export async function toApiError(response, fallbackMessage) {
11
+ const body = await response.json().catch(() => null);
12
+ const error = body?.error ?? body;
13
+
14
+ return new ValidaPayError(
15
+ error?.message || fallbackMessage,
16
+ error?.code || `HTTP_${response.status}`,
17
+ error?.details ?? null
18
+ );
19
+ }
@@ -0,0 +1,16 @@
1
+ import { ValidaPayError } from "./errors.js";
2
+
3
+ export function assertBrowser(packageName) {
4
+ if (typeof window === "undefined" || typeof document === "undefined") {
5
+ throw new ValidaPayError(`${packageName} só roda no navegador`, "BROWSER_ONLY");
6
+ }
7
+ }
8
+
9
+ export function assertRequired(fields) {
10
+ const missing = fields.find(([, value]) => value === undefined || value === null || String(value).trim() === "");
11
+ if (missing) throw new ValidaPayError(`${missing[0]} é obrigatório`, "MISSING_FIELD");
12
+ }
13
+
14
+ export const onlyDigits = (value) => String(value ?? "").replace(/\D/g, "");
15
+
16
+ export const holderNameOf = (card) => card?.holderName ?? card?.cardHolderName;
package/index.js DELETED
@@ -1,64 +0,0 @@
1
- const OAUTH_URL_PRD = "https://oauth2.validapay.com.br/auth/token";
2
- const OAUTH_URL_DEV = "https://oauth2-dev.validapay.com.br/auth/token";
3
- const API_URL_PRD = "https://api.validapay.com.br";
4
- const API_URL_DEV = "https://dev.validapay.com.br";
5
-
6
- async function fetchToken({ clientId, clientSecret, dev }) {
7
- const oauthUrl = dev ? OAUTH_URL_DEV : OAUTH_URL_PRD;
8
-
9
- const res = await fetch(oauthUrl, {
10
- method: "POST",
11
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
12
- body: new URLSearchParams({
13
- grant_type: "client_credentials",
14
- client_id: clientId,
15
- client_secret: clientSecret,
16
- scope: "payment.methods/write",
17
- }).toString(),
18
- });
19
-
20
- if (!res.ok) {
21
- const error = await res.json().catch(() => ({ message: res.statusText }));
22
- throw new Error(error?.message || "Falha ao obter token");
23
- }
24
-
25
- const data = await res.json();
26
- return data.access_token;
27
- }
28
-
29
- export async function tokenize({ clientId, clientSecret, dev = false, card, customer }) {
30
- if (!clientId) throw new Error("clientId é obrigatório");
31
- if (!clientSecret) throw new Error("clientSecret é obrigatório");
32
- if (!card?.number) throw new Error("card.number é obrigatório");
33
- if (!card?.name) throw new Error("card.name é obrigatório");
34
- if (!card?.cvv) throw new Error("card.cvv é obrigatório");
35
- if (!card?.expiration) throw new Error("card.expiration é obrigatório");
36
- if (!customer?.document) throw new Error("customer.document é obrigatório");
37
- if (!customer?.name) throw new Error("customer.name é obrigatório");
38
- if (!customer?.email) throw new Error("customer.email é obrigatório");
39
-
40
- const token = await fetchToken({ clientId, clientSecret, dev });
41
- const apiUrl = dev ? API_URL_DEV : API_URL_PRD;
42
-
43
- const res = await fetch(`${apiUrl}/v1/payment-methods/tokenize`, {
44
- method: "POST",
45
- headers: {
46
- "Content-Type": "application/json",
47
- "Authorization": `Bearer ${token}`,
48
- },
49
- body: JSON.stringify({
50
- name: card.name,
51
- number: card.number,
52
- cvv: card.cvv,
53
- expiration: card.expiration,
54
- customer,
55
- }),
56
- });
57
-
58
- if (!res.ok) {
59
- const error = await res.json().catch(() => ({ message: res.statusText }));
60
- throw new Error(error?.message || "Erro ao tokenizar cartão");
61
- }
62
-
63
- return res.json();
64
- }