wyzecash 0.0.0-stage → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Mudanças
2
+
3
+ ## 0.2.0
4
+
5
+ **Mudanças que pedem ajuste no seu código:**
6
+
7
+ - Saem `pix.send`, `pix.list` e `pix.retrieve`: enviar Pix e sacar são o mesmo Pix que sai, então use `withdrawals.create` (com `destination` ou `copyPaste`). A rota antiga `/v1/pix/payments` continua respondendo por enquanto, mas vai ser removida.
8
+ - Sai `meds.downloadAttachment`: as provas do MED são internas da WyzeCash e não se baixam pela API (a rota já recusava).
9
+
10
+ **Novidades:**
11
+
12
+ - `meds.respond(id, { defense, evidence })`: responde um MED com a defesa e até 5 provas (em base64), num pedido só. Precisa da permissão `/meds/respond` na chave.
13
+ - O endereço padrão passa a ser `https://api.wyzecash.com.br`. O antigo continua funcionando, e quem passa `baseUrl` não muda nada.
14
+ - `payments.retrieve` e `withdrawals.retrieve` explicam no editor que, para saber quando a cobrança é paga ou o saque conclui, o caminho é o webhook (ou o WebSocket), não a consulta em sequência, que gasta o limite de 100 pedidos por minuto da conta. O README ganhou a seção "Saber quando foi pago".
15
+
16
+ ## 0.1.0
17
+
18
+ Primeira versão.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WyzeCash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,162 @@
1
- # Temporary Holding Version
1
+ # wyzecash
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ SDK oficial da [WyzeCash](https://docs.wyzecash.com.br) para Node.js 20+, Bun e Deno: cobranças por Pix, cartão e cripto, links de pagamento, saldo e extrato, saques, estornos, MED, subcontas e webhooks. Sem dependências.
4
+
5
+ ```bash
6
+ npm install wyzecash
7
+ ```
8
+
9
+ ## Começar
10
+
11
+ ```ts
12
+ import WyzeCash from "wyzecash";
13
+
14
+ const wyzecash = new WyzeCash(process.env.WYZECASH_API_KEY);
15
+
16
+ const cobranca = await wyzecash.payments.create({
17
+ amount: 15000, // R$ 150,00, sempre em centavos
18
+ description: "Pedido #1052",
19
+ });
20
+
21
+ console.log(cobranca.copyPaste); // Pix Copia e Cola
22
+ ```
23
+
24
+ A chave de API (`bk_live_…`) é criada no painel, em Integrações e API, com uma permissão por rota. Sem passar a chave, o SDK lê a variável `WYZECASH_API_KEY`.
25
+
26
+ Não existe ambiente de testes: todo pedido é de verdade. Para testar, faça uma cobrança pequena.
27
+
28
+ ## Recursos
29
+
30
+ | Recurso | Métodos |
31
+ |---|---|
32
+ | `payments` | `create`, `methods`, `list`, `retrieve` |
33
+ | `paymentLinks` | `create`, `list`, `retrieve`, `update`, `delete`, `listPayments` |
34
+ | `customers` | `create`, `list`, `retrieve`, `update`, `delete` |
35
+ | `balance` | `retrieve` |
36
+ | `fees` | `list`, `preview` |
37
+ | `statement` | `list` |
38
+ | `transactions` | `list`, `retrieve` |
39
+ | `search` | `query` |
40
+ | `receipts` | `retrieve` |
41
+ | `meds` | `list`, `retrieve`, `respond` |
42
+ | `withdrawals` | `create`, `list`, `retrieve` |
43
+ | `refunds` | `create`, `list`, `retrieve` |
44
+ | `subaccounts` | `create`, `list`, `retrieve`, `update`, `delete`, `retrieveSettings`, `updateSettings`, `previewSettings`, `listEntries`, `fund`, `transfer`, `returnToAccount`, `withdraw` |
45
+
46
+ Todos os campos e respostas têm tipos e a descrição em português (o editor mostra ao passar o mouse).
47
+
48
+ ## Listas
49
+
50
+ `await` traz a primeira página; `for await` passa por todas, buscando a próxima só quando precisa.
51
+
52
+ ```ts
53
+ const pagina = await wyzecash.payments.list({ status: "paid", limit: 50 });
54
+
55
+ for await (const cobranca of wyzecash.payments.list({ status: "paid" })) {
56
+ console.log(cobranca.id, cobranca.amount);
57
+ }
58
+ ```
59
+
60
+ ## Idempotency-Key
61
+
62
+ Criar cobrança, saque, Pix enviado, estorno e os envios das subcontas pedem a `Idempotency-Key`. Passe o código do pedido no seu sistema: repetir a mesma chave devolve o mesmo resultado, sem fazer de novo.
63
+
64
+ ```ts
65
+ await wyzecash.withdrawals.create(
66
+ { amount: 10000, destination: { keyType: "email", key: "financeiro@exemplo.com" } },
67
+ { idempotencyKey: "saque-1052" },
68
+ );
69
+ ```
70
+
71
+ Sem ela, o SDK cria uma chave por chamada.
72
+
73
+ ## Erros e novas tentativas
74
+
75
+ ```ts
76
+ import WyzeCash, { WyzeCashError } from "wyzecash";
77
+
78
+ try {
79
+ await wyzecash.payments.create({ amount: 15000 });
80
+ } catch (erro) {
81
+ if (erro instanceof WyzeCashError) console.log(erro.status, erro.code, erro.message, erro.requestId);
82
+ }
83
+ ```
84
+
85
+ - O `code` é estável (`VALIDATION`, `INSUFFICIENT_FUNDS`, `RATE_LIMITED`...) e a `message` está em português.
86
+ - `429` (passou de 100 pedidos por minuto) volta a tentar sozinho, esperando o `Retry-After`.
87
+ - Erro de rede e `5xx` voltam a tentar só nas consultas e na criação de cobrança.
88
+ - O que tira dinheiro (saque, Pix enviado, estorno, envios das subcontas) **nunca** é repetido sozinho: se a rede cair, confira pela consulta ou pelo webhook antes de pedir de novo.
89
+
90
+ Opções do cliente: `new WyzeCash(chave, { maxRetries: 2, timeout: 60000, baseUrl, fetch })`.
91
+
92
+ ## Saber quando foi pago
93
+
94
+ **Use os webhooks ou o tempo real (WebSocket), não `payments.retrieve` em sequência.**
95
+
96
+ Chamar a consulta da cobrança em sequência para ver se ela foi paga gasta o limite de **100 pedidos por minuto da conta** (somando todas as rotas e todas as chaves). Uma tela consultando a cada 2 segundos gasta 30 por minuto; com quatro abertas, **todo** pedido da conta, até criar cobrança, volta `429 RATE_LIMITED` até liberar. Os avisos do webhook e do WebSocket não contam no limite.
97
+
98
+ - O seu sistema tem um endereço HTTPS: use os [webhooks](#webhooks) (`payment.paid`, `payment.expired`, `withdrawal.completed`...).
99
+ - Não tem (bot de Discord ou Telegram, programa num VPS): use o [tempo real](#tempo-real-websocket), de todas as cobranças ou só de uma (`ws.subscribePayment("BKY-…")`).
100
+ - A tela de "aguardando pagamento" pergunta ao **seu** servidor (que o webhook ou o WebSocket atualizou), nunca direto à WyzeCash: a chave de API não vai para o navegador nem para o app.
101
+ - `payments.retrieve` só para conferir uma vez (o webhook não chegou em alguns minutos). Sem webhook nem WebSocket, consulte no máximo a cada 5 segundos e pare na primeira situação final (`paid`, `expired` ou `refunded`).
102
+
103
+ ## Webhooks
104
+
105
+ Cada aviso chega com o cabeçalho `X-Bukio-Signature`. Confira sempre com o corpo cru, como chegou:
106
+
107
+ ```ts
108
+ import { verifyWebhook } from "wyzecash";
109
+
110
+ const evento = await verifyWebhook(corpoCru, request.headers["x-bukio-signature"], process.env.WYZECASH_WEBHOOK_SECRET);
111
+ if (evento.type === "payment.paid") {
112
+ // marque o pedido como pago (use evento.id para não processar duas vezes)
113
+ }
114
+ ```
115
+
116
+ Aviso com assinatura errada, sem assinatura ou assinado há mais de 5 minutos lança `WyzeCashWebhookError`.
117
+
118
+ ### Fastify
119
+
120
+ ```ts
121
+ import Fastify from "fastify";
122
+ import { wyzecashWebhooks } from "wyzecash/fastify";
123
+
124
+ const app = Fastify({ logger: true });
125
+
126
+ await app.register(wyzecashWebhooks, {
127
+ path: "/webhooks/wyzecash",
128
+ secret: process.env.WYZECASH_WEBHOOK_SECRET,
129
+ async onEvent(evento) {
130
+ if (evento.type === "payment.paid") {
131
+ // marque o pedido como pago
132
+ }
133
+ },
134
+ });
135
+
136
+ await app.listen({ port: 3000 });
137
+ ```
138
+
139
+ O plugin lê o corpo cru só na rota dele (o resto da aplicação continua lendo JSON como sempre) e responde `200` quando deu certo, `400` quando o aviso não é da WyzeCash e `500` quando o `onEvent` falha (a WyzeCash manda de novo mais tarde).
140
+
141
+ ## Tempo real (WebSocket)
142
+
143
+ Os mesmos eventos dos webhooks, na hora, sem precisar de um endereço HTTPS seu. Use só no servidor (a chave de API vai junto):
144
+
145
+ ```ts
146
+ const ws = wyzecash.realtime({ events: ["payment.*"] });
147
+
148
+ ws.on("payment.paid", (evento) => {
149
+ // marque o pedido como pago (use evento.id para não processar duas vezes)
150
+ });
151
+ ws.on("error", (erro) => console.error(erro.code, erro.message));
152
+ ```
153
+
154
+ - `events`: `*`, `payment.*`, `withdrawal.*`, `pix.*`, `transfer.*`, `subaccount.*` ou um evento só (`payment.paid`). A chave só recebe o que ela pode consultar; sem permissão chega o erro `FORBIDDEN`.
155
+ - `payments: ["BKY-…"]` ou `ws.subscribePayment("BKY-…")`: só os eventos de uma cobrança, por 30 minutos ou até ela ser paga ou vencer.
156
+ - Caiu? Conecta de novo sozinho e pede o que perdeu desde o último evento (`ws.lastEventId`, até 24 horas). Guarde esse código e passe em `since` quando o seu sistema reiniciar.
157
+ - Chave apagada ou inválida fecha a conexão (código 1008) e não reconecta. `ws.close()` fecha de vez.
158
+ - No Node 20, que não tem WebSocket, passe o do pacote `ws`: `wyzecash.realtime({ WebSocket })` com `import WebSocket from "ws"`.
159
+
160
+ ## Documentação
161
+
162
+ Referência completa da API: https://docs.wyzecash.com.br
@@ -0,0 +1,42 @@
1
+ import type { Requester, RequestOptions } from "./core.js";
2
+ import { type OperationId, Resources } from "./generated.js";
3
+ import { ListPromise } from "./pagination.js";
4
+ import { RealtimeConnection, type RealtimeOptions } from "./realtime.js";
5
+ import { Webhooks } from "./webhooks.js";
6
+ export declare const DEFAULT_BASE_URL = "https://api.wyzecash.com.br";
7
+ export interface ClientOptions {
8
+ /** A chave de API (`bk_live_…`). Sem ela, vale a variável de ambiente `WYZECASH_API_KEY`. */
9
+ apiKey?: string;
10
+ /** Endereço da API (padrão: o de produção). */
11
+ baseUrl?: string;
12
+ /** Quantas vezes tentar de novo (padrão 2). */
13
+ maxRetries?: number;
14
+ /** Tempo máximo de cada pedido, em milissegundos (padrão 60 000). */
15
+ timeout?: number;
16
+ /** Outro fetch (para testes ou proxy). */
17
+ fetch?: typeof fetch;
18
+ }
19
+ export declare class WyzeCash extends Resources implements Requester {
20
+ /** Conferir a assinatura dos webhooks. */
21
+ static readonly webhooks: Webhooks;
22
+ readonly webhooks: Webhooks;
23
+ private readonly apiKey;
24
+ private readonly baseUrl;
25
+ private readonly maxRetries;
26
+ private readonly timeout;
27
+ private readonly fetchImpl;
28
+ constructor(apiKeyOrOptions?: string | ClientOptions, maybeOptions?: ClientOptions);
29
+ /**
30
+ * Eventos na hora pelo WebSocket (os mesmos dos webhooks), sem endereço HTTPS seu. Só no servidor.
31
+ *
32
+ * const ws = wyzecash.realtime({ events: ["payment.*"] });
33
+ * ws.on("payment.paid", (e) => liberarPedido(e.data));
34
+ */
35
+ realtime(options?: RealtimeOptions): RealtimeConnection;
36
+ request<T>(id: OperationId, pathArgs: string[], params: object | undefined, options?: RequestOptions): Promise<T>;
37
+ list<P extends {
38
+ items: unknown[];
39
+ nextCursor?: string | null;
40
+ }, T>(id: OperationId, pathArgs: string[], params: object | undefined, options?: RequestOptions): ListPromise<P, T>;
41
+ private send;
42
+ }
package/dist/client.js ADDED
@@ -0,0 +1,143 @@
1
+ // O cliente da API da WyzeCash: `new WyzeCash(chave)` e os recursos (`wyzecash.payments.create(...)`).
2
+ // Sem dependências: usa o fetch do Node 20+, Bun, Deno e servidores na borda.
3
+ //
4
+ // Novas tentativas: 429 (passou do limite) sempre volta a tentar, esperando o `Retry-After`, porque
5
+ // a API não fez nada. Erro de rede, tempo esgotado e 5xx só voltam a tentar nas consultas e na
6
+ // criação de cobrança (com a mesma Idempotency-Key); o que tira dinheiro (saque, Pix enviado,
7
+ // estorno, envios das subcontas) nunca é repetido sozinho: confira pela consulta ou pelo webhook.
8
+ import { WyzeCashConnectionError, WyzeCashError } from "./errors.js";
9
+ import { OPERATIONS, Resources } from "./generated.js";
10
+ import { ListPromise } from "./pagination.js";
11
+ import { RealtimeConnection } from "./realtime.js";
12
+ import { VERSION } from "./version.js";
13
+ import { Webhooks } from "./webhooks.js";
14
+ export const DEFAULT_BASE_URL = "https://api.wyzecash.com.br";
15
+ const env = (name) => globalThis.process?.env?.[name];
16
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
17
+ export class WyzeCash extends Resources {
18
+ /** Conferir a assinatura dos webhooks. */
19
+ static webhooks = new Webhooks();
20
+ webhooks = WyzeCash.webhooks;
21
+ apiKey;
22
+ baseUrl;
23
+ maxRetries;
24
+ timeout;
25
+ fetchImpl;
26
+ constructor(apiKeyOrOptions, maybeOptions = {}) {
27
+ super();
28
+ const options = typeof apiKeyOrOptions === "string" ? { ...maybeOptions, apiKey: apiKeyOrOptions } : (apiKeyOrOptions ?? {});
29
+ const apiKey = options.apiKey ?? env("WYZECASH_API_KEY");
30
+ if (!apiKey)
31
+ throw new Error("Passe a chave de API: new WyzeCash(process.env.WYZECASH_API_KEY).");
32
+ this.apiKey = apiKey;
33
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
34
+ this.maxRetries = options.maxRetries ?? 2;
35
+ this.timeout = options.timeout ?? 60_000;
36
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
37
+ }
38
+ /**
39
+ * Eventos na hora pelo WebSocket (os mesmos dos webhooks), sem endereço HTTPS seu. Só no servidor.
40
+ *
41
+ * const ws = wyzecash.realtime({ events: ["payment.*"] });
42
+ * ws.on("payment.paid", (e) => liberarPedido(e.data));
43
+ */
44
+ realtime(options = {}) {
45
+ return new RealtimeConnection(`${this.baseUrl.replace(/^http/, "ws")}/v1/ws`, this.apiKey, options);
46
+ }
47
+ request(id, pathArgs, params, options = {}) {
48
+ return this.send(id, pathArgs, params, options);
49
+ }
50
+ list(id, pathArgs, params, options = {}) {
51
+ const base = { ...(params ?? {}) };
52
+ const start = typeof base.cursor === "string" ? base.cursor : undefined;
53
+ delete base.cursor;
54
+ return new ListPromise((cursor) => this.send(id, pathArgs, cursor ? { ...base, cursor } : base, options), start);
55
+ }
56
+ async send(id, pathArgs, params, options) {
57
+ const op = OPERATIONS[id];
58
+ let path = op.path;
59
+ for (const arg of pathArgs) {
60
+ if (!arg)
61
+ throw new Error(`Faltou o código na rota ${op.path}.`);
62
+ path = path.replace(/\{[A-Za-z]+\}/, encodeURIComponent(arg));
63
+ }
64
+ const url = new URL(this.baseUrl + path);
65
+ const body = {};
66
+ for (const [name, value] of Object.entries(params ?? {})) {
67
+ if (value === undefined)
68
+ continue;
69
+ if (op.query.includes(name)) {
70
+ if (value !== null)
71
+ url.searchParams.set(name, String(value));
72
+ }
73
+ else
74
+ body[name] = value;
75
+ }
76
+ const headers = {
77
+ authorization: `Bearer ${this.apiKey}`,
78
+ accept: op.binary ? "*/*" : "application/json",
79
+ "user-agent": `wyzecash-node/${VERSION}`,
80
+ };
81
+ if (op.idempotent)
82
+ headers["idempotency-key"] = options.idempotencyKey ?? globalThis.crypto.randomUUID();
83
+ const payload = op.body ? JSON.stringify(body) : undefined;
84
+ if (payload !== undefined)
85
+ headers["content-type"] = "application/json";
86
+ const safeToRepeat = op.method === "GET" || op.method === "DELETE" || (op.idempotent && !op.moneyOut);
87
+ const maxRetries = options.maxRetries ?? this.maxRetries;
88
+ for (let attempt = 0;; attempt++) {
89
+ let response;
90
+ const controller = new AbortController();
91
+ const onAbort = () => controller.abort(options.signal?.reason);
92
+ options.signal?.addEventListener("abort", onAbort, { once: true });
93
+ const timer = setTimeout(() => controller.abort(new WyzeCashConnectionError("A API não respondeu a tempo.", "TIMEOUT")), options.timeout ?? this.timeout);
94
+ try {
95
+ response = await this.fetchImpl(url, { method: op.method, headers, body: payload, signal: controller.signal });
96
+ }
97
+ catch (error) {
98
+ clearTimeout(timer);
99
+ options.signal?.removeEventListener("abort", onAbort);
100
+ if (options.signal?.aborted)
101
+ throw error;
102
+ const failure = error instanceof WyzeCashConnectionError ? error : new WyzeCashConnectionError(`Não deu para falar com a API: ${error?.message ?? error}`);
103
+ if (safeToRepeat && attempt < maxRetries) {
104
+ await wait(backoff(attempt));
105
+ continue;
106
+ }
107
+ throw failure;
108
+ }
109
+ clearTimeout(timer);
110
+ options.signal?.removeEventListener("abort", onAbort);
111
+ if (response.ok) {
112
+ if (op.binary)
113
+ return (await response.arrayBuffer());
114
+ const text = await response.text();
115
+ return (text ? JSON.parse(text) : null);
116
+ }
117
+ const retryable = response.status === 429 || (response.status >= 500 && safeToRepeat);
118
+ if (retryable && attempt < maxRetries) {
119
+ const after = Number(response.headers.get("retry-after"));
120
+ await wait(Number.isFinite(after) && after > 0 ? Math.min(after, 60) * 1000 : backoff(attempt));
121
+ continue;
122
+ }
123
+ throw await toError(response);
124
+ }
125
+ }
126
+ }
127
+ const backoff = (attempt) => Math.min(8000, 500 * 2 ** attempt) * (0.75 + Math.random() * 0.5);
128
+ async function toError(response) {
129
+ let json = null;
130
+ try {
131
+ json = await response.json();
132
+ }
133
+ catch {
134
+ json = null;
135
+ }
136
+ return new WyzeCashError({
137
+ status: response.status,
138
+ code: json?.error?.code ?? `HTTP_${response.status}`,
139
+ message: json?.error?.message ?? `A API respondeu ${response.status}.`,
140
+ requestId: json?.requestId ?? response.headers.get("x-request-id"),
141
+ details: json?.error?.details ?? null,
142
+ });
143
+ }
package/dist/core.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { OperationId } from "./generated.js";
2
+ import type { ListPromise } from "./pagination.js";
3
+ export interface RequestOptions {
4
+ /**
5
+ * Chave do pedido nas rotas que pedem `Idempotency-Key` (criar cobrança, saque, Pix enviado,
6
+ * estorno e os envios das subcontas). Use o código do pedido no seu sistema: repetir a mesma
7
+ * chave devolve o mesmo resultado. Sem ela, o SDK cria uma por chamada.
8
+ */
9
+ idempotencyKey?: string;
10
+ /** Tempo máximo do pedido, em milissegundos. */
11
+ timeout?: number;
12
+ /** Quantas vezes tentar de novo (vale por cima do padrão do cliente). */
13
+ maxRetries?: number;
14
+ signal?: AbortSignal;
15
+ }
16
+ export interface Requester {
17
+ request<T>(op: OperationId, pathArgs: string[], params: object | undefined, options?: RequestOptions): Promise<T>;
18
+ list<P extends {
19
+ items: unknown[];
20
+ nextCursor?: string | null;
21
+ }, T>(op: OperationId, pathArgs: string[], params: object | undefined, options?: RequestOptions): ListPromise<P, T>;
22
+ }
package/dist/core.js ADDED
@@ -0,0 +1,2 @@
1
+ // O que os recursos gerados (generated.ts) pedem ao cliente.
2
+ export {};
@@ -0,0 +1,24 @@
1
+ export declare class WyzeCashError extends Error {
2
+ /** Situação HTTP (0 quando o pedido nem chegou à API). */
3
+ readonly status: number;
4
+ /** Código estável do erro (ex.: `VALIDATION`, `INSUFFICIENT_FUNDS`, `RATE_LIMITED`). */
5
+ readonly code: string;
6
+ /** Código do pedido: mande para o suporte quando precisar de ajuda. */
7
+ readonly requestId: string | null;
8
+ readonly details: Record<string, unknown> | null;
9
+ constructor(input: {
10
+ status: number;
11
+ code: string;
12
+ message: string;
13
+ requestId?: string | null;
14
+ details?: Record<string, unknown> | null;
15
+ });
16
+ }
17
+ /** O pedido não chegou à API ou a resposta não voltou (rede, tempo esgotado). */
18
+ export declare class WyzeCashConnectionError extends WyzeCashError {
19
+ constructor(message: string, code?: "CONNECTION_ERROR" | "TIMEOUT");
20
+ }
21
+ /** O aviso (webhook) não passou na conferência da assinatura. */
22
+ export declare class WyzeCashWebhookError extends WyzeCashError {
23
+ constructor(message: string, code: "MISSING_SIGNATURE" | "INVALID_SIGNATURE" | "TIMESTAMP_OUT_OF_RANGE" | "INVALID_PAYLOAD");
24
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,33 @@
1
+ // Erros do SDK. A API responde `{ "error": { "code", "message", "details" }, "requestId" }`: o
2
+ // `code` é estável (use no seu sistema) e a `message` está em português.
3
+ export class WyzeCashError extends Error {
4
+ /** Situação HTTP (0 quando o pedido nem chegou à API). */
5
+ status;
6
+ /** Código estável do erro (ex.: `VALIDATION`, `INSUFFICIENT_FUNDS`, `RATE_LIMITED`). */
7
+ code;
8
+ /** Código do pedido: mande para o suporte quando precisar de ajuda. */
9
+ requestId;
10
+ details;
11
+ constructor(input) {
12
+ super(input.message);
13
+ this.name = "WyzeCashError";
14
+ this.status = input.status;
15
+ this.code = input.code;
16
+ this.requestId = input.requestId ?? null;
17
+ this.details = input.details ?? null;
18
+ }
19
+ }
20
+ /** O pedido não chegou à API ou a resposta não voltou (rede, tempo esgotado). */
21
+ export class WyzeCashConnectionError extends WyzeCashError {
22
+ constructor(message, code = "CONNECTION_ERROR") {
23
+ super({ status: 0, code, message });
24
+ this.name = "WyzeCashConnectionError";
25
+ }
26
+ }
27
+ /** O aviso (webhook) não passou na conferência da assinatura. */
28
+ export class WyzeCashWebhookError extends WyzeCashError {
29
+ constructor(message, code) {
30
+ super({ status: 400, code, message });
31
+ this.name = "WyzeCashWebhookError";
32
+ }
33
+ }
@@ -0,0 +1,14 @@
1
+ import type { FastifyInstance, FastifyRequest } from "fastify";
2
+ import { type WebhookEvent } from "./webhooks.js";
3
+ export interface WyzeCashWebhooksOptions {
4
+ /** Caminho da rota (padrão `/webhooks/wyzecash`). */
5
+ path?: string;
6
+ /** Segredo do webhook. Sem ele, vale a variável de ambiente `WYZECASH_WEBHOOK_SECRET`. */
7
+ secret?: string;
8
+ /** Quantos segundos o aviso vale depois de assinado (padrão 300). */
9
+ tolerance?: number;
10
+ /** Chamado com cada aviso conferido. Use `evento.id` para não processar o mesmo duas vezes. */
11
+ onEvent: (event: WebhookEvent, request: FastifyRequest) => unknown | Promise<unknown>;
12
+ }
13
+ export declare function wyzecashWebhooks(fastify: FastifyInstance, options: WyzeCashWebhooksOptions): Promise<void>;
14
+ export default wyzecashWebhooks;
@@ -0,0 +1,50 @@
1
+ // Plugin do Fastify para receber os webhooks da WyzeCash:
2
+ //
3
+ // import { wyzecashWebhooks } from "wyzecash/fastify";
4
+ // await app.register(wyzecashWebhooks, {
5
+ // path: "/webhooks/wyzecash",
6
+ // secret: process.env.WYZECASH_WEBHOOK_SECRET,
7
+ // async onEvent(evento) { if (evento.type === "payment.paid") ... },
8
+ // });
9
+ //
10
+ // A rota lê o corpo cru (a assinatura é conferida nos bytes como chegaram). O plugin não é
11
+ // "fastify-plugin" de propósito: a troca do leitor de JSON fica só dentro dele e o resto da sua
12
+ // aplicação continua lendo JSON como sempre. Responde 200 quando deu certo, 400 quando o aviso não
13
+ // é da WyzeCash e 500 quando o seu onEvent falha (aí a WyzeCash manda de novo mais tarde).
14
+ import { WyzeCashWebhookError } from "./errors.js";
15
+ import { SIGNATURE_HEADER, verifyWebhook } from "./webhooks.js";
16
+ const env = (name) => globalThis.process?.env?.[name];
17
+ export async function wyzecashWebhooks(fastify, options) {
18
+ const secret = options.secret ?? env("WYZECASH_WEBHOOK_SECRET");
19
+ if (!secret)
20
+ throw new Error("wyzecashWebhooks: passe o `secret` (ou a variável WYZECASH_WEBHOOK_SECRET).");
21
+ if (typeof options.onEvent !== "function")
22
+ throw new Error("wyzecashWebhooks: passe a função `onEvent`.");
23
+ fastify.removeContentTypeParser(["application/json"]);
24
+ fastify.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => done(null, body));
25
+ fastify.post(options.path ?? "/webhooks/wyzecash", async (request, reply) => {
26
+ const raw = request.body;
27
+ if (!(raw instanceof Uint8Array))
28
+ return reply.code(400).send({ error: "Corpo do aviso vazio ou fora do JSON." });
29
+ let event;
30
+ try {
31
+ event = await verifyWebhook(raw, request.headers[SIGNATURE_HEADER], secret, { tolerance: options.tolerance });
32
+ }
33
+ catch (error) {
34
+ if (error instanceof WyzeCashWebhookError) {
35
+ request.log.warn({ code: error.code }, "WyzeCash: aviso recusado");
36
+ return reply.code(400).send({ error: error.message });
37
+ }
38
+ throw error;
39
+ }
40
+ try {
41
+ await options.onEvent(event, request);
42
+ }
43
+ catch (error) {
44
+ request.log.error({ err: error, event: event.id, type: event.type }, "WyzeCash: o onEvent falhou");
45
+ return reply.code(500).send({ error: "Não deu para processar o aviso agora." });
46
+ }
47
+ return reply.code(200).send({ received: true });
48
+ });
49
+ }
50
+ export default wyzecashWebhooks;