@eva_solutions/whatsapp-sdk 0.0.0-stage → 1.0.0-dev-20261107051104

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 (68) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +264 -2
  4. package/dist/admin.cjs +816 -0
  5. package/dist/admin.cjs.map +1 -0
  6. package/dist/admin.d.cts +552 -0
  7. package/dist/admin.d.ts +552 -0
  8. package/dist/admin.js +811 -0
  9. package/dist/admin.js.map +1 -0
  10. package/dist/clasificar-evento-2MomTU6G.cjs +293 -0
  11. package/dist/clasificar-evento-2MomTU6G.cjs.map +1 -0
  12. package/dist/clasificar-evento-C2xB8ozq.js +246 -0
  13. package/dist/clasificar-evento-C2xB8ozq.js.map +1 -0
  14. package/dist/construir-http-client-662I0iNf.js +563 -0
  15. package/dist/construir-http-client-662I0iNf.js.map +1 -0
  16. package/dist/construir-http-client-I_8KGDdG.cjs +586 -0
  17. package/dist/construir-http-client-I_8KGDdG.cjs.map +1 -0
  18. package/dist/crear-http-client-con-token-B30t9PwL.js +16 -0
  19. package/dist/crear-http-client-con-token-B30t9PwL.js.map +1 -0
  20. package/dist/crear-http-client-con-token-CgQpiAB-.cjs +21 -0
  21. package/dist/crear-http-client-con-token-CgQpiAB-.cjs.map +1 -0
  22. package/dist/error-configuracion-BQCU2kjo.d.ts +67 -0
  23. package/dist/error-configuracion-BpKBUJ1E.d.cts +67 -0
  24. package/dist/index-Bd9BFAky.d.cts +323 -0
  25. package/dist/index-BqiB3Jx5.d.cts +810 -0
  26. package/dist/index-BxqcB-KI.d.ts +810 -0
  27. package/dist/index-CpPVftY8.d.ts +323 -0
  28. package/dist/index.cjs +739 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +3 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +735 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/internal.cjs +112 -0
  35. package/dist/internal.cjs.map +1 -0
  36. package/dist/internal.d.cts +3 -0
  37. package/dist/internal.d.ts +3 -0
  38. package/dist/internal.js +107 -0
  39. package/dist/internal.js.map +1 -0
  40. package/dist/iterar-paginas-B7ERHbF0.cjs +72 -0
  41. package/dist/iterar-paginas-B7ERHbF0.cjs.map +1 -0
  42. package/dist/iterar-paginas-qCd5Mq1U.js +55 -0
  43. package/dist/iterar-paginas-qCd5Mq1U.js.map +1 -0
  44. package/dist/result-B5IXEaX2.cjs +41 -0
  45. package/dist/result-B5IXEaX2.cjs.map +1 -0
  46. package/dist/result-DOH5m3oG.js +24 -0
  47. package/dist/result-DOH5m3oG.js.map +1 -0
  48. package/dist/schemas-comunes-B77W8Y1H.cjs +111 -0
  49. package/dist/schemas-comunes-B77W8Y1H.cjs.map +1 -0
  50. package/dist/schemas-comunes-Bp_JPbJ9.js +52 -0
  51. package/dist/schemas-comunes-Bp_JPbJ9.js.map +1 -0
  52. package/dist/validar-base-url-CnH9I5I5.cjs +42 -0
  53. package/dist/validar-base-url-CnH9I5I5.cjs.map +1 -0
  54. package/dist/validar-base-url-bFYhAddQ.js +37 -0
  55. package/dist/validar-base-url-bFYhAddQ.js.map +1 -0
  56. package/dist/webhooks.cjs +231 -0
  57. package/dist/webhooks.cjs.map +1 -0
  58. package/dist/webhooks.d.cts +71 -0
  59. package/dist/webhooks.d.ts +71 -0
  60. package/dist/webhooks.js +221 -0
  61. package/dist/webhooks.js.map +1 -0
  62. package/dist/ws.cjs +275 -0
  63. package/dist/ws.cjs.map +1 -0
  64. package/dist/ws.d.cts +112 -0
  65. package/dist/ws.d.ts +112 -0
  66. package/dist/ws.js +265 -0
  67. package/dist/ws.js.map +1 -0
  68. package/package.json +102 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # @eva_solutions/whatsapp-sdk
2
+
3
+ ## 1.0.0-dev-20261107051104
4
+
5
+ ### Major Changes
6
+
7
+ - 1437b62: **Cambio incompatible.** `./internal` y `./admin` dejan de firmar con HMAC S2S y se autentican con un token Bearer de servicio emitido por Auth (identidad fase 1).
8
+
9
+ - `crearClienteInterno` y `crearClienteAdmin` reciben `obtenerToken: () => Promise<string>` e `invalidarToken?: (tokenRechazado: string) => void` en lugar de `clientId` y `secretoHex`, y envían `Authorization: Bearer <token>`. El SDK no depende del auth-sdk: el consumidor pasa su proveedor de tokens.
10
+ - Ante un `401` se invoca `invalidarToken(tokenRechazado)` (puede ser directamente `proveedor.invalidate` del auth-sdk) y la request se reintenta **una sola vez**, también los `POST` (el `401` significa que la API no procesó la request). Sin `invalidarToken` no hay reintento. Un segundo `401` se devuelve como error. Si `obtenerToken` o `invalidarToken` lanzan, el SDK devuelve un `Resultado` de falla (el `500` local expone solo un `code` OAuth no sensible en `detalles`).
11
+ - `./admin` `serviceClients` refleja el registro local de Whatsapp-api sin secreto ni permisos: `ServiceClient` = `{ id, clientId, name, enabled, allowedSourcePhones, createdAt, updatedAt }`, donde `id` es el `sub` del token (uuid de Auth) y `clientId` el slug informativo. Las rutas y los inputs se identifican por `id` (antes `slug`). Se retiran `rotarSecret`, `secret` y `scopes`.
12
+ - Se retiran `crearHttpClientS2S`, `OpcionesClienteS2S`, la firma HMAC y `soportaMultipart`. Nuevos: `crearHttpClientConToken` y `OpcionesClienteConToken` (exportados por `./admin` e `./internal`).
13
+ - Un `403` en `./internal` indica permiso insuficiente en el token, `sub` sin registro habilitado o `sourcePhone` fuera de la allowlist; un `401`, token inválido o vencido.
14
+
15
+ ### Minor Changes
16
+
17
+ - c03ac0f: `ErrorResultado` incluye `origen` (`"api" | "transporte" | "local"`) para distinguir un 503 real de la API de un fallo de red/timeout/cancelación (resultado ambiguo en un POST) y de un rechazo local. Un body `FormData` con el cliente S2S (`./admin`, `./internal`) se rechaza con 400 local en vez de enviar una firma inválida, y el `Content-Type` manual del consumidor se descarta con `FormData`. `./ws` ya no arrastra el cliente REST. `sideEffects: false`.
18
+ - c03ac0f: Primera versión del SDK de Whatsapp-api, con 5 subpaths ESM/CJS (Node >= 22):
19
+
20
+ - `.`: `crearCliente` (x-api-key) con mensajes (texto, imagen, video, gif, PDF, ubicación y lote), sesión (estado y QR), programados, webhooks fallidos, buffer de eventos WebSocket, billing, seguridad y secreto de webhook.
21
+ - `./webhooks`: verificación de la firma HMAC (`verificarFirmaWebhook`, `verificarWebhookDesdeRequest`) y eventos tipados.
22
+ - `./ws`: `conectarWs`, cliente de eventos en tiempo real (`/v1/ws/events`) con reconexión con backoff, heartbeat y eventos tipados.
23
+ - `./admin`: `crearClienteAdmin` (firma S2S) con service clients y su allowlist de teléfonos, sesiones, QR del sistema, billing, pagos, exención de pago y auditoría.
24
+ - `./internal`: `crearClienteInterno` (firma S2S) para el envío interno de mensajes.
25
+
26
+ Núcleo común: Result Pattern (nunca lanza por fallos HTTP), timeout por cliente y por operación (35 s por defecto), reintentos con backoff y `Retry-After` (los POST y los DELETE con efecto no se reintentan; un fallo por timeout o red en un envío es ambiguo), validación de entradas con Zod y respuestas tolerantes a valores nuevos de la API.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EVA Solutions
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
13
+ all 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
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,265 @@
1
- # Temporary Holding Version
1
+ # @eva_solutions/whatsapp-sdk
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 TypeScript para consumir el servicio Whatsapp-api. Simple por defecto, con control avanzado opcional.
4
+
5
+ ```
6
+ ✓ Runtime Node 22+ (ESM y CJS)
7
+ ✓ Seguridad apiKey, tokens y secretos solo en el servidor
8
+ ✓ Errores Result Pattern: nunca lanza por un fallo HTTP o de validación
9
+ ✓ Config baseUrl en runtime: un solo paquete para todos los entornos
10
+ ✓ Superficies 5 subpaths: x-api-key, webhooks, WebSocket, admin (Bearer), internal (Bearer)
11
+ ✓ Dependencias zod, nada más
12
+ ```
13
+
14
+ **No browser:** la `apiKey`, los tokens de servicio y el secreto del webhook son secretos y deben quedarse en tu servidor.
15
+
16
+ **Licencia:** MIT. **Versiones y cambios:** `CHANGELOG.md` lo genera Changesets al publicar cada release.
17
+
18
+ ## Subpaths
19
+
20
+ | Subpath | Qué expone | Autenticación |
21
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
22
+ | `@eva_solutions/whatsapp-sdk` | `crearCliente`: mensajes, sesión, programados, webhooks fallidos, buffer WS, billing, seguridad, secreto de webhook | `x-api-key` |
23
+ | `@eva_solutions/whatsapp-sdk/webhooks` | Verificación de la firma HMAC de los webhooks y eventos tipados | secreto del webhook |
24
+ | `@eva_solutions/whatsapp-sdk/ws` | `conectarWs`: eventos en tiempo real con reconexión | `x-api-key` (en `Sec-WebSocket-Protocol`) |
25
+ | `@eva_solutions/whatsapp-sdk/admin` | `crearClienteAdmin`: service clients, sesiones, QR del sistema, billing, pagos, exención, auditoría | token Bearer de servicio (Auth) |
26
+ | `@eva_solutions/whatsapp-sdk/internal` | `crearClienteInterno`: envío interno de mensajes | token Bearer de servicio (Auth) |
27
+
28
+ Cada subpath existe en ESM y CJS. Quien no usa un subpath no lo carga.
29
+
30
+ ## Instalación
31
+
32
+ ```bash
33
+ pnpm add @eva_solutions/whatsapp-sdk
34
+ # o: npm install @eva_solutions/whatsapp-sdk
35
+ ```
36
+
37
+ > Requiere Node 22 o superior (`engines.node`).
38
+
39
+ Según el entorno del consumidor:
40
+
41
+ | Entorno | Instalación |
42
+ | ------------- | -------------------------------------------------------------------------------------------------------- |
43
+ | `production` | `pnpm add @eva_solutions/whatsapp-sdk` (estable, dist-tag `latest`; sale al crear una GitHub Release) |
44
+ | `development` | `pnpm add @eva_solutions/whatsapp-sdk@dev` (snapshot `X.Y.Z-dev-<timestamp>`) |
45
+ | `local` | `pnpm build` aquí y `pnpm link <ruta a este checkout>` en el consumidor; no commitees `file:` ni `link:` |
46
+
47
+ Detalle en [CONTRIBUTING.md](./CONTRIBUTING.md#entornos-y-publicación-mantenedores).
48
+
49
+ ## Quickstart
50
+
51
+ ### `.` — API pública con `x-api-key`
52
+
53
+ ```typescript
54
+ import { crearCliente } from "@eva_solutions/whatsapp-sdk";
55
+
56
+ const cliente = crearCliente({
57
+ apiKey: process.env.EVA_WHATSAPP_API_KEY!,
58
+ baseUrl: process.env.EVA_WHATSAPP_BASE_URL!, // origin, ej. https://whatsapp.example.com
59
+ });
60
+
61
+ const r = await cliente.mensajes.enviarTexto({ phone: "59171234567", message: "Hola" });
62
+
63
+ if (!r.success) {
64
+ console.error(`Error ${r.codigo}: ${r.error}`);
65
+ return;
66
+ }
67
+ console.log(r.data.message);
68
+ ```
69
+
70
+ `apiKey` y `baseUrl` son obligatorios. Si falta alguno o no es válido, `crearCliente` lanza `ErrorConfiguracion` de forma síncrona (los mensajes nunca incluyen sus valores).
71
+
72
+ ### `./webhooks` — verificar la firma
73
+
74
+ ```typescript
75
+ import { verificarWebhookDesdeRequest } from "@eva_solutions/whatsapp-sdk/webhooks";
76
+
77
+ export async function POST(request: Request) {
78
+ const r = await verificarWebhookDesdeRequest(request, process.env.WEBHOOK_SECRET!);
79
+ if (!r.success) {
80
+ return new Response(r.error, { status: r.codigo }); // 401, 413 o 422
81
+ }
82
+
83
+ const evento = r.data.evento;
84
+ if (evento.conocido === false) {
85
+ return new Response("ok"); // tipo nuevo de la API: ignóralo o regístralo
86
+ }
87
+ if (evento.type === "message") {
88
+ console.log(evento.data.from, evento.data.messageType);
89
+ }
90
+ return new Response("ok");
91
+ }
92
+ ```
93
+
94
+ Si tu framework ya leyó el body (Hono, Workers), pásalo con `{ cuerpoCrudo }` como tercer argumento. Ver [docs/api.md](./docs/api.md#3-webhooks-webhooks).
95
+
96
+ ### `./ws` — eventos en tiempo real
97
+
98
+ ```typescript
99
+ import { conectarWs } from "@eva_solutions/whatsapp-sdk/ws";
100
+
101
+ const ws = conectarWs({
102
+ baseUrl: process.env.EVA_WHATSAPP_BASE_URL!,
103
+ apiKey: process.env.EVA_WHATSAPP_API_KEY!,
104
+ // webSocketImpl: WebSocket de la librería 'ws', solo si tu runtime no tiene WebSocket global
105
+ });
106
+
107
+ ws.on("message", (evento) => console.log(evento.data.messageType));
108
+ ws.on("desconocido", (evento) => console.log("tipo nuevo:", evento.type));
109
+ ws.on("cerrado", ({ codigo, reconectara }) => console.log(codigo, reconectara));
110
+ ws.on("error", (e) => console.error(e.motivo, e.mensaje));
111
+
112
+ // al apagar el proceso:
113
+ ws.desconectar();
114
+ ```
115
+
116
+ Usa el `WebSocket` global (Node 22+, Bun, Deno). Para otro runtime, o en tests, inyecta `webSocketImpl`.
117
+
118
+ ### `./admin` — administración (token de servicio)
119
+
120
+ ```typescript
121
+ import { crearClienteAdmin } from "@eva_solutions/whatsapp-sdk/admin";
122
+
123
+ const admin = crearClienteAdmin({
124
+ baseUrl: process.env.EVA_WHATSAPP_BASE_URL!,
125
+ obtenerToken: () => proveedor.getToken(), // token de servicio emitido por Auth
126
+ invalidarToken: proveedor.invalidate, // opcional: recibe el token rechazado
127
+ });
128
+
129
+ const r = await admin.serviceClients.listar();
130
+ ```
131
+
132
+ El SDK **no** obtiene ni renueva tokens y no depende del auth-sdk: tú pasas un proveedor (por ejemplo `createServiceTokenProvider` del auth-sdk, con `audience: "whatsapp-api"`) que cachea y renueva. `obtenerToken` se invoca en cada intento de request. El token necesita `aud: whatsapp-api` y el permiso `admin`.
133
+
134
+ `invalidarToken(tokenRechazado)` recibe el token que provocó el `401`; el `invalidate` del auth-sdk lo acepta directamente y solo descarta el caché si sigue siendo ese token (varias requests concurrentes con un `401` no provocan una estampida de tokens nuevos). Ante un `401` el SDK llama a `invalidarToken` y reintenta **una sola vez** con un token nuevo, también los `POST` (un `401` significa que la API no procesó la request). Un segundo `401` se devuelve como error. **Sin `invalidarToken` no hay reintento ante `401`**: saldría con el mismo token cacheado. Si `obtenerToken` o `invalidarToken` lanzan, el SDK no propaga la excepción: devuelve un `Resultado` de falla.
135
+
136
+ ### `./internal` — envío interno (token de servicio)
137
+
138
+ ```typescript
139
+ import { crearClienteInterno } from "@eva_solutions/whatsapp-sdk/internal";
140
+
141
+ const interno = crearClienteInterno({
142
+ baseUrl: process.env.EVA_WHATSAPP_BASE_URL!,
143
+ obtenerToken: () => proveedor.getToken(),
144
+ invalidarToken: proveedor.invalidate,
145
+ });
146
+
147
+ const r = await interno.mensajes.enviar({
148
+ phone: "59171234567",
149
+ message: "Hola",
150
+ sourcePhone: "59170000000", // debe estar en la allowlist del service client
151
+ });
152
+ ```
153
+
154
+ El token necesita `aud: whatsapp-api` y el permiso del tipo de mensaje (`messages:send:text` o `messages:send:media`). Su `sub` debe tener un registro habilitado en Whatsapp-api (`admin.serviceClients.crear`), si no la API responde `403`.
155
+
156
+ ## Result Pattern y códigos
157
+
158
+ Toda operación retorna `Promise<Resultado<T>>` y no lanza por fallos de negocio:
159
+
160
+ ```typescript
161
+ type Resultado<T> = { success: true; data: T } | ErrorResultado;
162
+ // ErrorResultado: { success: false; error: string; codigo: CodigoErrorHttp;
163
+ // requestId?: string; detalles?: unknown; codigoApi?: string; retryAfter?: number /* solo 429 */ }
164
+ ```
165
+
166
+ | `codigo` | Significa |
167
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
168
+ | `400` | La API rechazó la petición; o el SDK rechazó la ruta o el cuerpo antes de enviarlo |
169
+ | `401` / `403` | Token inválido o vencido (`401`) / sin permiso o sin registro habilitado (`403`); `codigoApi` trae el code cuando existe |
170
+ | `402` | Pago requerido |
171
+ | `404` / `408` / `409` / `413` | El de la API (`408`: el QR no llegó a tiempo; `409`: sesión ya conectada) |
172
+ | `422` | Validación. **Local** (el SDK validó tu entrada y no envió nada) o **de la API**. `detalles` trae `{ formErrors, fieldErrors }` |
173
+ | `429` | Rate limit. `retryAfter` en segundos |
174
+ | `500` / `502` | Error del servicio; `502` también si la respuesta no tiene la forma esperada |
175
+ | `503` | Servicio no disponible **o fallo de red, timeout o cancelación sin respuesta** (mira `origen`) |
176
+
177
+ > **`503` es ambiguo en un envío.** Si un `POST` de envío termina en `503` por red o timeout, el mensaje pudo haber salido. El SDK no reintenta los `POST` justamente para no duplicarlo: consulta antes de reenviar o acepta el riesgo de duplicado.
178
+
179
+ `origen` distingue quién produjo el error: `"api"` (la API respondió, incluido un `503` real), `"transporte"` (no hubo respuesta: red, DNS, TLS, timeout, cancelación o redirect; en un `POST` es el caso ambiguo) y `"local"` (el SDK lo rechazó antes de enviar: validación `422`, ruta o cuerpo inválidos, o fallo al obtener el token). Para decidir si reenviar, mira `origen === "transporte"`, no solo `codigo === 503`.
180
+
181
+ Helpers: `esExitoso(r)` y `esFallido(r)`. Excepciones al patrón: `ErrorConfiguracion` al construir un cliente, los iteradores `iterar…` y el cliente WebSocket (que reporta por eventos). Detalle en [docs/api.md](./docs/api.md#5-result-pattern-y-códigos).
182
+
183
+ ## Timeouts
184
+
185
+ - Default: **35 s por intento** (cubre la espera del QR de la API, hasta 30 s).
186
+ - Por cliente: `crearCliente({ timeout })`. Por operación: `cliente.sesion.obtenerQr(input, { timeout })`.
187
+ - Cancelación: `signal` del cliente y `signal` por operación, combinadas con el timeout. Un abort del consumidor responde `503 "Operación cancelada"`; un timeout, `503 "Tiempo de espera agotado"`.
188
+
189
+ ## Reintentos
190
+
191
+ Solo se reintentan las lecturas (`GET`) sin efecto, ante error de red/timeout, 5xx y 429, con backoff exponencial y respetando `Retry-After` (default: 2 reintentos, base 250 ms, tope 10 s).
192
+
193
+ - Nunca se reintentan `POST`, `PUT` ni `PATCH`, ni los `DELETE`, ni los `GET` con efecto (QR, secreto de webhook). Única excepción: en `./admin` e `./internal`, un `401` se reintenta **una vez** con un token nuevo, en cualquier método (la API no procesó la request).
194
+ - Si la API pide esperar más que `esperaMaximaMs`, no se reintenta y el `429` llega con `retryAfter`.
195
+ - Ajuste global: `reintentos: { maximo, esperaBaseMs, esperaMaximaMs }` (`maximo: 0` los desactiva).
196
+ - El override por petición `reintentar: true | false` existe en el cliente HTTP de bajo nivel (`crearHttpClientConToken` de `./admin` e `./internal`), no en las opciones de las operaciones (`signal` y `timeout`). Ver [docs/configuration.md](./docs/configuration.md#reintentos).
197
+
198
+ ## Paginación
199
+
200
+ Cada listado paginado tiene un `listar` manual y un `iterar…` que pide las páginas a demanda:
201
+
202
+ ```typescript
203
+ try {
204
+ for await (const programado of cliente.programados.iterar({ limit: 50 })) {
205
+ console.log(programado.id);
206
+ }
207
+ } catch (err) {
208
+ const fallo = (err as Error).cause as { codigo: number } | undefined; // el Resultado de falla
209
+ }
210
+ ```
211
+
212
+ Los iteradores son la excepción al Result Pattern: ceden los items directos y, ante un fallo, lanzan `Error` con el `Resultado` en `cause`.
213
+
214
+ ## Eventos desconocidos (forward-compat)
215
+
216
+ La API puede agregar tipos de evento sin que sea un error. Un evento con `type` nuevo, o un `message` con `messageType` nuevo, llega con `conocido: false` (`EventoDesconocido`); en el WebSocket se emite como evento `desconocido`. Los valores nuevos en enums de respuesta también se toleran. Un evento de tipo **conocido** con forma inválida sí es un fallo (`422`).
217
+
218
+ ## Seguridad
219
+
220
+ - **Servidor únicamente:** `apiKey`, los tokens de servicio y el secreto del webhook nunca van al navegador ni a logs.
221
+ - **Webhooks:** la firma no incluye timestamp, así que un evento reenviado sigue siendo válido. Deduplica en tu handler (por ejemplo con `data.messageId` cuando el evento lo trae, o con `type` + `phone` + `timestamp`).
222
+ - **Redirects:** el SDK los rechaza (`redirect: "error"`) para no filtrar credenciales a otro host.
223
+ - **`baseUrl`:** solo origin (`https` o `http` en loopback), sin credenciales, path, query ni fragmento.
224
+
225
+ Modelo de amenazas completo en [docs/security.md](./docs/security.md).
226
+
227
+ ## Qué NO cubre el SDK
228
+
229
+ | Ruta de la API | Por qué |
230
+ | ------------------------------------------------------------- | ---------------------------------------------------------------------- |
231
+ | `/v1/me/api-keys`, `/v1/me/usuario` | Se autentican con la cookie de sesión del panel, no con `x-api-key` |
232
+ | Webhook entrante de pagos (`/v1/admin/billing/webhook/pagos`) | Lo llama el procesador de pagos, no es una operación de administración |
233
+ | Obtención y renovación de tokens de servicio | Es del proveedor que pasas en `obtenerToken` (auth-sdk), no del SDK |
234
+
235
+ ## Desarrollo (contribuidores)
236
+
237
+ Requisitos: Node 22+ y pnpm `11.28.2` (declarado en `packageManager`).
238
+
239
+ ```bash
240
+ pnpm install --frozen-lockfile
241
+ pnpm build # tsdown: dist/ con ESM + CJS para los 5 subpaths
242
+ pnpm typecheck
243
+ pnpm lint # oxlint
244
+ pnpm test:run
245
+ pnpm format # oxfmt
246
+ pnpm changeset # registra un cambio para el próximo release
247
+ ```
248
+
249
+ Guía completa en [CONTRIBUTING.md](./CONTRIBUTING.md).
250
+
251
+ ## Documentación
252
+
253
+ | Documento | Contenido |
254
+ | ------------------------------------------------ | --------------------------------------------------- |
255
+ | [docs/api.md](./docs/api.md) | Referencia de la API pública por subpath |
256
+ | [docs/architecture.md](./docs/architecture.md) | Capas, flujo HTTP, tokens de servicio, WebSocket |
257
+ | [docs/configuration.md](./docs/configuration.md) | Opciones, `baseUrl`, timeouts, reintentos, runtimes |
258
+ | [docs/conventions.md](./docs/conventions.md) | Naming, Result Pattern, imports, tests |
259
+ | [docs/decisions.md](./docs/decisions.md) | ADRs |
260
+ | [docs/security.md](./docs/security.md) | Modelo de amenazas y recomendaciones |
261
+ | [SECURITY.md](./SECURITY.md) | Cómo reportar una vulnerabilidad |
262
+
263
+ ## Licencia
264
+
265
+ MIT © EVA Solutions. Ver [`LICENSE`](./LICENSE).