@radardata/sdk 0.0.0-stage → 0.1.1

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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -2
  3. package/dist/cjs/configuracao.js +28 -0
  4. package/dist/cjs/configuracao.js.map +1 -0
  5. package/dist/cjs/dados.js +44 -0
  6. package/dist/cjs/dados.js.map +1 -0
  7. package/dist/cjs/erros.js +86 -0
  8. package/dist/cjs/erros.js.map +1 -0
  9. package/dist/cjs/index.js +69 -0
  10. package/dist/cjs/index.js.map +1 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/recursos/analises.js +53 -0
  13. package/dist/cjs/recursos/analises.js.map +1 -0
  14. package/dist/cjs/recursos/catalogo.js +21 -0
  15. package/dist/cjs/recursos/catalogo.js.map +1 -0
  16. package/dist/cjs/recursos/consultas.js +33 -0
  17. package/dist/cjs/recursos/consultas.js.map +1 -0
  18. package/dist/cjs/recursos/verify.js +39 -0
  19. package/dist/cjs/recursos/verify.js.map +1 -0
  20. package/dist/cjs/recursos/webhooks.js +45 -0
  21. package/dist/cjs/recursos/webhooks.js.map +1 -0
  22. package/dist/cjs/transporte.js +128 -0
  23. package/dist/cjs/transporte.js.map +1 -0
  24. package/dist/cjs/webhooks/assinatura.js +65 -0
  25. package/dist/cjs/webhooks/assinatura.js.map +1 -0
  26. package/dist/esm/configuracao.d.ts +27 -0
  27. package/dist/esm/configuracao.d.ts.map +1 -0
  28. package/dist/esm/configuracao.js +23 -0
  29. package/dist/esm/configuracao.js.map +1 -0
  30. package/dist/esm/dados.d.ts +59 -0
  31. package/dist/esm/dados.d.ts.map +1 -0
  32. package/dist/esm/dados.js +38 -0
  33. package/dist/esm/dados.js.map +1 -0
  34. package/dist/esm/erros.d.ts +53 -0
  35. package/dist/esm/erros.d.ts.map +1 -0
  36. package/dist/esm/erros.js +73 -0
  37. package/dist/esm/erros.js.map +1 -0
  38. package/dist/esm/index.d.ts +41 -0
  39. package/dist/esm/index.d.ts.map +1 -0
  40. package/dist/esm/index.js +47 -0
  41. package/dist/esm/index.js.map +1 -0
  42. package/dist/esm/recursos/analises.d.ts +25 -0
  43. package/dist/esm/recursos/analises.d.ts.map +1 -0
  44. package/dist/esm/recursos/analises.js +49 -0
  45. package/dist/esm/recursos/analises.js.map +1 -0
  46. package/dist/esm/recursos/catalogo.d.ts +11 -0
  47. package/dist/esm/recursos/catalogo.d.ts.map +1 -0
  48. package/dist/esm/recursos/catalogo.js +17 -0
  49. package/dist/esm/recursos/catalogo.js.map +1 -0
  50. package/dist/esm/recursos/consultas.d.ts +22 -0
  51. package/dist/esm/recursos/consultas.d.ts.map +1 -0
  52. package/dist/esm/recursos/consultas.js +29 -0
  53. package/dist/esm/recursos/consultas.js.map +1 -0
  54. package/dist/esm/recursos/verify.d.ts +25 -0
  55. package/dist/esm/recursos/verify.d.ts.map +1 -0
  56. package/dist/esm/recursos/verify.js +35 -0
  57. package/dist/esm/recursos/verify.js.map +1 -0
  58. package/dist/esm/recursos/webhooks.d.ts +27 -0
  59. package/dist/esm/recursos/webhooks.d.ts.map +1 -0
  60. package/dist/esm/recursos/webhooks.js +41 -0
  61. package/dist/esm/recursos/webhooks.js.map +1 -0
  62. package/dist/esm/transporte.d.ts +35 -0
  63. package/dist/esm/transporte.d.ts.map +1 -0
  64. package/dist/esm/transporte.js +123 -0
  65. package/dist/esm/transporte.js.map +1 -0
  66. package/dist/esm/webhooks/assinatura.d.ts +32 -0
  67. package/dist/esm/webhooks/assinatura.d.ts.map +1 -0
  68. package/dist/esm/webhooks/assinatura.js +61 -0
  69. package/dist/esm/webhooks/assinatura.js.map +1 -0
  70. package/package.json +47 -4
  71. package/src/configuracao.ts +50 -0
  72. package/src/dados.ts +93 -0
  73. package/src/erros.ts +70 -0
  74. package/src/index.ts +66 -0
  75. package/src/recursos/analises.ts +61 -0
  76. package/src/recursos/catalogo.ts +17 -0
  77. package/src/recursos/consultas.ts +32 -0
  78. package/src/recursos/verify.ts +40 -0
  79. package/src/recursos/webhooks.ts +43 -0
  80. package/src/transporte.ts +145 -0
  81. package/src/webhooks/assinatura.ts +72 -0
@@ -0,0 +1,145 @@
1
+ import { type ConfigResolvida, userAgent } from "./configuracao.js";
2
+ import { ErroRadarData, FalhaDeRede, erroDoStatus } from "./erros.js";
3
+
4
+ /**
5
+ * O transporte: fala HTTP com a `/api/v2` e traduz resposta em dado ou erro.
6
+ *
7
+ * Tres decisoes que valem o comentario, porque sao as que diferenciam um SDK
8
+ * de um `fetch` embrulhado:
9
+ *
10
+ * 1. RETRY SO ONDE E SEGURO. A api aceita `Idempotency-Key` em consulta,
11
+ * analise e operacao, e e so nessas que repetir e seguro: a mesma chave
12
+ * devolve a mesma resposta em vez de cobrar de novo. Criar sessao de
13
+ * Verify NAO tem idempotencia, entao repetir ali criaria uma segunda
14
+ * sessao faturavel. Por isso o retry nao e uma opcao global: ele e ligado
15
+ * por chamada, e so quando o SDK manda a chave.
16
+ * 2. A CHAVE E GERADA PELO SDK quando quem chama nao passa uma. Deixar isso
17
+ * para o integrador significa, na pratica, ninguem usar, e e justamente o
18
+ * caso em que a falha doi: timeout numa consulta paga.
19
+ * 3. FALHA SEM RESPOSTA NAO E ERRO DE SERVIDOR. `FalhaDeRede` existe porque
20
+ * nesse caso nao se sabe se a operacao aconteceu, e a acao correta e
21
+ * consultar pela chave, nao repetir as cegas.
22
+ */
23
+ export class Transporte {
24
+ constructor(private readonly config: ConfigResolvida) {}
25
+
26
+ async pedir<T = Record<string, unknown>>(
27
+ metodo: string,
28
+ caminho: string,
29
+ opcoes: {
30
+ corpo?: Record<string, unknown> | null;
31
+ query?: Record<string, string | number | undefined>;
32
+ chaveIdempotencia?: string;
33
+ } = {},
34
+ ): Promise<T> {
35
+ const { corpo, query, chaveIdempotencia } = opcoes;
36
+
37
+ const cabecalhos: Record<string, string> = {
38
+ Authorization: `Bearer ${this.config.token}`,
39
+ Accept: "application/json",
40
+ "User-Agent": userAgent(),
41
+ };
42
+
43
+ // Repetir so e seguro com a chave: sem ela, uma consulta cobrada duas
44
+ // vezes seria dinheiro do cliente.
45
+ const podeRepetir = typeof chaveIdempotencia === "string" && chaveIdempotencia !== "";
46
+ if (podeRepetir) cabecalhos["Idempotency-Key"] = chaveIdempotencia;
47
+ if (corpo != null) cabecalhos["Content-Type"] = "application/json";
48
+
49
+ let url = `${this.config.base}/${caminho.replace(/^\/+/, "")}`;
50
+ if (query) {
51
+ const q = new URLSearchParams();
52
+ for (const [k, v] of Object.entries(query)) if (v !== undefined) q.set(k, String(v));
53
+ if ([...q].length > 0) url += `?${q.toString()}`;
54
+ }
55
+
56
+ const tentativas = podeRepetir ? this.config.tentativas : 1;
57
+ let ultimo: ErroRadarData | null = null;
58
+
59
+ for (let i = 1; i <= tentativas; i++) {
60
+ const abortar = new AbortController();
61
+ const relogio = setTimeout(() => abortar.abort(), this.config.timeoutMs);
62
+
63
+ let resposta: Response;
64
+ try {
65
+ resposta = await this.config.fetch(url, {
66
+ method: metodo,
67
+ headers: cabecalhos,
68
+ body: corpo != null ? JSON.stringify(corpo) : undefined,
69
+ signal: abortar.signal,
70
+ });
71
+ } catch (e) {
72
+ ultimo = new FalhaDeRede(
73
+ `Não foi possível falar com o RadarData: ${e instanceof Error ? e.message : String(e)}`,
74
+ "falha_de_rede",
75
+ null,
76
+ {},
77
+ { cause: e },
78
+ );
79
+ if (i < tentativas) {
80
+ await this.esperar(i);
81
+ continue;
82
+ }
83
+ throw ultimo;
84
+ } finally {
85
+ clearTimeout(relogio);
86
+ }
87
+
88
+ // 429 e 5xx valem nova tentativa; o resto e resposta final.
89
+ if ((resposta.status === 429 || resposta.status >= 500) && i < tentativas) {
90
+ await this.esperar(i, resposta);
91
+ continue;
92
+ }
93
+
94
+ return (await this.interpretar(resposta)) as T;
95
+ }
96
+
97
+ throw ultimo ?? new ErroRadarData("Falha desconhecida no transporte.");
98
+ }
99
+
100
+ private async interpretar(resposta: Response): Promise<unknown> {
101
+ const cru = await resposta.text();
102
+ let json: unknown;
103
+ try {
104
+ json = cru === "" ? {} : JSON.parse(cru);
105
+ } catch {
106
+ json = {};
107
+ }
108
+ const corpo = (json ?? {}) as Record<string, unknown>;
109
+
110
+ if (resposta.ok) {
111
+ // `data` quando a rota embrulha (catalogo, saldo, analises) e o corpo
112
+ // inteiro quando nao (sessao de Verify). Devolver sempre o conteudo
113
+ // util poupa quem chama de saber qual rota faz o que.
114
+ return "data" in corpo && corpo.data !== null && typeof corpo.data === "object" ? corpo.data : corpo;
115
+ }
116
+
117
+ const erro = (corpo.error ?? {}) as { code?: string; message?: string };
118
+ const mensagem =
119
+ erro.message ?? (corpo.message as string | undefined) ?? `O RadarData respondeu ${resposta.status} sem detalhe.`;
120
+
121
+ throw erroDoStatus(resposta.status, mensagem, erro.code ?? null, corpo);
122
+ }
123
+
124
+ /** Espera entre tentativas, respeitando `Retry-After` quando ele vem. */
125
+ private async esperar(tentativa: number, resposta?: Response): Promise<void> {
126
+ const cabecalho = Number(resposta?.headers.get("Retry-After") ?? 0);
127
+
128
+ if (cabecalho > 0) {
129
+ await new Promise((r) => setTimeout(r, Math.min(cabecalho, 30) * 1000));
130
+ return;
131
+ }
132
+
133
+ // Recuo exponencial com um pouco de aleatorio, para muitos clientes
134
+ // falhando juntos nao voltarem todos no mesmo instante.
135
+ const base = Math.min(8, 2 ** (tentativa - 1));
136
+ await new Promise((r) => setTimeout(r, base * 1000 + Math.random() * 250));
137
+ }
138
+ }
139
+
140
+ /** Chave nova de idempotencia, no formato que a api aceita. */
141
+ export function chaveIdempotencia(): string {
142
+ const bytes = new Uint8Array(16);
143
+ globalThis.crypto.getRandomValues(bytes);
144
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
145
+ }
@@ -0,0 +1,72 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+
3
+ /**
4
+ * Confere que o webhook veio mesmo do RadarData.
5
+ *
6
+ * Mandamos `X-RadarData-Signature: sha256=<hmac do CORPO CRU com o seu
7
+ * webhook_secret>`, mais `X-RadarData-Event`, `X-RadarData-Delivery` e
8
+ * `X-RadarData-Timestamp`.
9
+ *
10
+ * Aceita `string` ou `Uint8Array`, e nao `Buffer`, de proposito: `Buffer` e um
11
+ * global do Node e poria `@types/node` na superficie publica do pacote, o que
12
+ * quebra o `tsc` de quem compila para borda, Deno ou navegador. `Buffer` E um
13
+ * `Uint8Array`, entao o que o `express.raw()` entrega continua servindo.
14
+ *
15
+ * O CORPO TEM DE SER O CRU. Em Express e `express.raw()`, nao `express.json()`;
16
+ * reserializar o objeto ja decodificado muda espaco e ordem de chave, o HMAC
17
+ * muda junto e a conferencia falha sem motivo aparente. E o erro mais comum
18
+ * nesta parte, em qualquer integracao.
19
+ *
20
+ * LIMITE QUE VOCE PRECISA CONHECER: o timestamp e o id de entrega viajam em
21
+ * CABECALHO, fora do corpo assinado, entao a assinatura prova a ORIGEM e o
22
+ * CONTEUDO, e nao a hora. Ela nao protege contra reenvio de um aviso antigo e
23
+ * autentico por quem o tenha interceptado. A defesa pratica e guardar o
24
+ * `X-RadarData-Delivery` e ignorar o que ja foi processado, que voce deve
25
+ * fazer de qualquer forma, porque reenviamos em caso de falha.
26
+ */
27
+ export function confereAssinatura(
28
+ corpoCru: string | Uint8Array,
29
+ assinatura: string | null | undefined,
30
+ segredo: string,
31
+ ): boolean {
32
+ if (!assinatura || !segredo) return false;
33
+
34
+ const esperada = `sha256=${createHmac("sha256", segredo).update(corpoCru).digest("hex")}`;
35
+
36
+ const a = Buffer.from(esperada, "utf8");
37
+ const b = Buffer.from(assinatura, "utf8");
38
+
39
+ // `timingSafeEqual` exige tamanhos iguais e lança quando diferem; comparar
40
+ // o tamanho antes não vaza nada, porque o tamanho da assinatura é fixo e
41
+ // público.
42
+ if (a.length !== b.length) return false;
43
+
44
+ // E não `===`: comparação normal vaza, pelo tempo que leva, quantos bytes
45
+ // iniciais bateram, e com isso dá para descobrir a assinatura byte a byte.
46
+ return timingSafeEqual(a, b);
47
+ }
48
+
49
+ /**
50
+ * Confere e devolve o evento decodificado, ou lança.
51
+ *
52
+ * @throws Error quando a assinatura nao confere
53
+ */
54
+ export function exigirAssinatura(
55
+ corpoCru: string | Uint8Array,
56
+ assinatura: string | null | undefined,
57
+ segredo: string,
58
+ ): Record<string, unknown> {
59
+ if (!confereAssinatura(corpoCru, assinatura, segredo)) {
60
+ throw new Error("Assinatura do webhook do RadarData não confere.");
61
+ }
62
+
63
+ try {
64
+ // `TextDecoder` e nao `Buffer.toString`: existe em Node, em navegador, em
65
+ // Deno e em Bun, e nao arrasta os tipos do Node para a superficie publica.
66
+ const texto = typeof corpoCru === "string" ? corpoCru : new TextDecoder().decode(corpoCru);
67
+ const dados = JSON.parse(texto);
68
+ return dados && typeof dados === "object" ? (dados as Record<string, unknown>) : {};
69
+ } catch {
70
+ return {};
71
+ }
72
+ }