From 33693cb00af74c54d3355eac6fb01247f854ea8a Mon Sep 17 00:00:00 2001 From: Mijhael Samaniego <67170467+mijhaels@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:20:21 -0300 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20preparar=20el=20README=20para=20el?= =?UTF-8?q?=20repo=20p=C3=BAblico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agrega instalación y configuración, define la variable que faltaba en el ejemplo, separa las filas ambiguas de la tabla de operaciones y ordena cada párrafo con el motivo antes de la instrucción. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6863e7b..3aa42e0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,17 @@ webhooks y calculá totales con las mismas reglas que la API. - Tipos generados desde el OpenAPI de la API. - Reintentos seguros: cada operación tributaria viaja con `Idempotency-Key`. -> Usalo sólo del lado del servidor. La API key no puede viajar al navegador ni dentro de una app distribuida. +## Instalación + +```bash +npm install @sifende/sdk +``` + +Necesitás una API key de Sifende. El prefijo de la clave fija el ambiente: `sk_live_`, `sk_test_` o `sk_sandbox_`. La +referencia completa de la API está en [sifende.com.py/docs](https://sifende.com.py/docs). + +> La API key emite documentos a nombre del contribuyente: usá el SDK solo en el servidor, nunca en el navegador ni +> dentro de una app distribuida. ## Uso @@ -15,6 +25,7 @@ webhooks y calculá totales con las mismas reglas que la API. import { Sifende, calcularTotales } from '@sifende/sdk'; const sifende = new Sifende({ apiKey: process.env.SIFENDE_API_KEY! }); +const pedidoId = '1001'; // el id de la venta en tu sistema const items = [ { codigo: 'REM-01', descripcion: 'Remera', cantidad: 2, unidadMedida: 'UNI', precioUnitario: 150_000, afectacionTributaria: 'GRAVADO', tasaIVA: 10 }, @@ -41,14 +52,27 @@ const documento = await sifende.documentos.emitir( }, items: [...items], }, - { idempotencyKey: `pedido-${pedido.id}` }, + { idempotencyKey: `pedido-${pedidoId}` }, ); -console.log(documento.cdc, documento.estado); // PENDIENTE hasta que SIFEN responda +console.log(documento.cdc, documento.estado); // 'PENDIENTE' ``` -`calcularTotales` reproduce el redondeo oficial de la SET (múltiplo de 50 hacia abajo en guaraníes): en una operación -al contado, `montoPago` tiene que coincidir con ese total. +El documento queda en `PENDIENTE` hasta que SIFEN lo procesa. El resultado llega por [webhook](#webhooks) o con +`documentos.consultarEstado(cdc)`. + +En una operación al contado, `montoPago` tiene que coincidir con el total redondeado. `calcularTotales` aplica el +redondeo oficial de la SET: en guaraníes, al múltiplo de 50 inferior. + +## Configuración + +| Opción | Por defecto | | +|---|---|---| +| `apiKey` | | Obligatoria. | +| `timeoutMs` | `30000` | Tiempo máximo por intento. | +| `maxReintentos` | `2` | Reintentos ante fallas transitorias. `0` los desactiva. | +| `baseUrl` | `https://api.sifende.com.py` | | +| `fetch` | `globalThis.fetch` | Para usar otro cliente HTTP o simular la API en tests. | ## Operaciones @@ -56,17 +80,25 @@ al contado, `montoPago` tiene que coincidir con ese total. |---|---| | `documentos.emitir(documento, { idempotencyKey })` | `POST /api/v1/documento-electronico` | | `documentos.consultarEstado(cdc)` | `GET /api/v1/documento-electronico/status/{cdc}` | -| `documentos.descargarKude(cdc)` | `GET /api/v1/documento-electronico/{cdc}/kude`; espera mientras el PDF se prepara | +| `documentos.descargarKude(cdc)` | `GET /api/v1/documento-electronico/{cdc}/kude` | | `documentos.cancelar(cdc, { motivo })` | `POST /api/v1/documento-electronico/{cdc}/cancelar` | | `documentos.nominar(cdc, { motivo, receptor })` | `POST /api/v1/documento-electronico/{cdc}/nominar` | | `documentos.inutilizar(rango)` | `POST /api/v1/documento-electronico/inutilizar` | -| `documentos.listarEventos(filtro)` / `consultarEvento(id)` | `GET /api/v1/documento-electronico/eventos` | -| `contribuyente.consultar()` / `consultarConsumoPlan()` | `GET /api/v1/contribuyente`, `/plan-consumo` | +| `documentos.listarEventos(filtro)` | `GET /api/v1/documento-electronico/eventos` | +| `documentos.consultarEvento(id)` | `GET /api/v1/documento-electronico/eventos/{id}` | +| `contribuyente.consultar()` | `GET /api/v1/contribuyente` | +| `contribuyente.consultarConsumoPlan()` | `GET /api/v1/contribuyente/plan-consumo` | | `padron.consultar(rucOCedula)` | `GET /api/v1/padron/{documento}` | -| `catalogos.enumeraciones()`, `departamentos()`, `distritos(id)`, `ciudades(id)` | Catálogos públicos | +| `catalogos.enumeraciones()` | `GET /api/v1/public/enums` | +| `catalogos.departamentos()`, `distritos(id)`, `ciudades(id)` | `GET /api/v1/geografia/…` | + +Los catálogos son públicos y no envían la API key. + +`descargarKude` espera hasta 3 minutos a que el PDF esté listo; si no lo está, lanza `KudePendienteError`. Cambiá la +espera con `{ esperarMs }`. -Si no pasás `idempotencyKey`, el SDK genera un UUID y lo reusa en sus reintentos. Para poder recuperar una operación -después de un corte, generá vos la clave y guardala antes de enviar. +Para poder retomar una operación después de un corte, generá vos la `idempotencyKey` y guardala antes de enviar. Si no +la pasás, el SDK genera un UUID que solo reusa en sus propios reintentos. ## Errores @@ -86,14 +118,19 @@ try { } ``` -El SDK reintenta solo los casos que la API marca como seguros: cortes de conexión con clave de idempotencia, -`409 idempotency-in-progress`, `503 idempotency-upstream-unknown`, `503 evento-nominacion-error`, `429`, y 500, 502, -503 o 504 en consultas. Respeta `Retry-After`. Un corte o un timeout mientras llega la respuesta cuenta como corte de -conexión. +`SifendeTimeoutError` extiende `SifendeConexionError`: un timeout, incluso a mitad de la respuesta, se trata como un +corte de conexión. + +El SDK reintenta solo los casos que la API marca como seguros, y respeta `Retry-After`: + +- Cortes de conexión en consultas y en operaciones con clave de idempotencia. +- `409 idempotency-in-progress`, `503 idempotency-upstream-unknown` y `503 evento-nominacion-error`. +- `429`. +- `500`, `502`, `503` y `504` en consultas. ## Webhooks -Pasá el cuerpo crudo, antes de cualquier parser JSON: +La firma se calcula sobre el cuerpo tal como llegó. Pasá el texto crudo, antes de cualquier parser JSON: ```ts import { FirmaWebhookInvalidaError, verificarWebhook } from '@sifende/sdk'; @@ -116,8 +153,10 @@ export async function action({ request }: { request: Request }) { } ``` -Durante una rotación de secreto podés pasar `secreto: [nuevo, anterior]`. Una entrega puede repetirse: deduplicá por -`evento.id`. +Los tipos de evento son `documento.aprobado`, `documento.rechazado`, `documento.cancelado` y `lote.procesado`. + +Una entrega puede repetirse: deduplicá por `evento.id`. `verificarWebhook` rechaza una entrega si su +`X-Sifende-Timestamp` difiere más de 5 minutos de tu reloj; ajustalo con `toleranciaSegundos`. Durante una rotación de secreto, pasá `secreto: [nuevo, anterior]`. ## Desarrollo From dd60b7865163168fc16d17664f793dbc99bc7a19 Mon Sep 17 00:00:00 2001 From: Mijhael Samaniego <67170467+mijhaels@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:23:20 -0300 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20agregar=20pol=C3=ADtica=20de=20segu?= =?UTF-8?q?ridad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- SECURITY.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..68d77af --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,10 @@ +# Seguridad + +Para que una vulnerabilidad no quede expuesta antes de corregirla, reportala en privado, no en un issue público. + +Usá **Report a vulnerability** en la pestaña [Security](https://github.com/ithdev/sifende-node/security) del repo. +Incluí la versión del SDK, los pasos para reproducirla y el impacto que ves. + +Solo la última versión publicada de `@sifende/sdk` recibe correcciones de seguridad. + +Si la vulnerabilidad está en la API de Sifende y no en el SDK, reportala por el mismo canal.