# SDK de TypeScript

> En camino a npm. El contrato de @emisso/connect, el cliente tipado y sus reglas de reintento, documentados para cuando se publique.



<Callout type="warn" title="En camino: el paquete todavía no está publicado en npm">
  `pnpm add @emisso/connect` aún no funciona fuera de este repositorio. Hoy la integración real es HTTP
  directo: los mismos endpoints de [tu primera llamada](/docs/empezar/primera-llamada) y de la
  [API de control](/docs/api-control), con `Authorization: Bearer connect_sk_…`. Esta página documenta el
  contrato del SDK tal como existe en el código, para que la migración sea directa cuando se publique.
</Callout>

`@emisso/connect` es el SDK oficial en TypeScript para Connect: habla REST por debajo (los mismos endpoints de [tu primera llamada](/docs/empezar/primera-llamada)) y expone encima un **accessor tipado**, generado desde el registro de tools, sin escribir a mano los tipos de cada `connector.resource.verb`.

## Instalación [#instalación]

```bash
pnpm add @emisso/connect
```

Requiere Node 20 o superior (usa `fetch` nativo; puedes reemplazarlo con `opts.fetch`).

## Primeros pasos [#primeros-pasos]

```ts
import { createClient } from "@emisso/connect";

const connect = createClient({ apiKey: "connect_sk_..." });

const now = await connect.tools.core.timestamp.now({ timezone: "America/Santiago" });
// { iso: "2026-07-21T15:04:00.000Z", unix: 1784646240, timezone: "America/Santiago" }
```

La API key es la misma que creas desde `/api-keys` en el dashboard: el SDK no introduce un modelo de credenciales distinto. El detalle de autenticación y scopes está en [Autenticación](/docs/empezar/autenticacion).

## `createClient(options)` [#createclientoptions]

| Opción           | Tipo                              | Default                            | Notas                                                                                                                                     |
| ---------------- | --------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `apiKey`         | `string`                          |                                    | Tu clave `connect_sk_…`. Provee esta **o** `apiKeyProvider`.                                                                              |
| `apiKeyProvider` | `() => string \| Promise<string>` |                                    | Fuente asíncrona de la clave (por ejemplo un vault).                                                                                      |
| `baseUrl`        | `string`                          | `https://connect.emisso.ai/api/v1` | Se preserva el subpath; se recorta el `/` final.                                                                                          |
| `maxAttempts`    | `number`                          | `3`                                | Intentos totales por llamada, el primero incluido. Aplica **solo a GET**.                                                                 |
| `onUnauthorized` | `() => Promise<string \| null>`   |                                    | Seam de refresh OAuth: ante un 401 se llama una vez para obtener un token nuevo y se reintenta la llamada (cualquier verbo).              |
| `fetch`          | `typeof fetch`                    | `globalThis.fetch`                 | Reemplaza la implementación de fetch.                                                                                                     |
| `clientTraceId`  | `string`                          |                                    | Trace id por defecto para toda llamada; se sobreescribe por llamada desde `CallOptions`. Ver [Trazabilidad](#trazabilidad-clienttraceid). |

Los errores de configuración (falta `fetch`, falta `apiKey`/`apiKeyProvider`) se lanzan de forma síncrona desde el propio `createClient(...)`, antes de cualquier llamada.

## Accessor tipado [#accessor-tipado]

Cada tool del registro queda disponible como `connect.tools.<connector>.<resource>.<verbo>(input?, opts?)`, con el tipo de `input`/`output` derivado del registro. No hay `any` en el camino:

```ts
// connect.tools.<connector>.<resource>.<verbo>(input?, opts?)
const uf = await connect.tools.indicadores.valor.actual({ codigo: "UF" });
// { codigo: "UF", fecha: "2026-08-08", valor: 39487.23, unidad: "CLP", antiguedadDias: -1 }

// .withResponse(...) devuelve el envelope completo { data, requestId, meta, pagination }
const res = await connect.tools.core.timestamp.now.withResponse();
console.log(res.requestId, res.data);

// escape hatch genérico, útil si el id de la tool es dinámico
const data = await connect.tools.execute("core.timestamp.now", {});
```

La llamada por defecto (sin `.withResponse`) devuelve directamente el `data` de salida. Para el `request_id`, `meta` o `pagination` completos, usa `.withResponse(...)`. El catálogo completo de tools, con la ficha de cada una, vive en [la referencia](/docs/referencia).

## Selección de conexión [#selección-de-conexión]

Toda tool de un conector **con conexión** (el SII, los bancos) exige `connectionId` en `opts`. Siempre, incluso si tienes una sola conexión de ese sistema: no hay resolución implícita.

```ts
await connect.tools.sii.conexion.sincronizar(
  { periodo: "2026-06", alcances: ["rcv"] },
  { connectionId: "conn_..." },
);
```

Sin él la llamada falla con `400 validation_error` y un `suggestedFix` que dice de dónde sacar el id.

<Callout type="info" title="Por qué no se elige sola">
  La conexión **es la empresa**: dos conexiones de un banco son dos RUT distintos. Una elección implícita que hoy acierta porque hay una sola, mañana (al conectar la segunda empresa) acierta distinto sin que nadie haya cambiado una línea. Es exactamente el tipo de cambio silencioso que no queremos en plata ajena.
</Callout>

Los conectores **sin** conexión quedan exentos, porque no hay nada que elegir: `core`, `indicadores` y `conexiones` no tienen fila en `connections`. `conexiones.enlace.crear` es el caso que lo hace obvio: todavía no existe la conexión que crearía.

Para obtener los `conn_…` de tu organización, pregúntaselos al propio gateway:

```ts
const { conexiones } = await connect.tools.conexiones.estado.consultar({});
for (const cx of conexiones) {
  console.log(cx.id, cx.sistema, cx.nombre, cx.alcances, cx.datosListos);
}
```

`datosListos` responde «¿ya puedo leer?»: un listado vacío no es lo mismo que «no hay nada», puede ser que todavía no sincronizó. El porqué de esa separación está en [sincronizar y consultar](/docs/conceptos/sincronizar-consultar).

`opts: CallOptions = { signal?, connectionId?, clientTraceId? }`. `connectionId` viaja como el header `X-Connect-Connection`; `signal` es un `AbortSignal` estándar para cancelar la llamada; `clientTraceId` se explica abajo.

## Trazabilidad: `clientTraceId` [#trazabilidad-clienttraceid]

Una API key autentica como **un** agente. Si tu aplicación sirve a varias personas con la misma clave, la bitácora las ve a todas como el mismo actor y «quién pidió esto» queda sin respuesta. `clientTraceId` es la vía para decirlo:

```ts
const connect = createClient({ apiKey, clientTraceId: "miapp:tenant-42" });

// o por llamada, que gana sobre el del cliente
await connect.tools.sii.rcv.consultar({ limit: 10 }, { connectionId: "conn_...", clientTraceId: "miapp:tenant-42:hilo-9" });
```

Viaja como el header `x-client-trace-id` y se guarda en `bitacora_execution.client_trace_id`, en su propia columna y explícitamente no confiable: nunca se confunde con el `request_id` que emite el servidor. El SDK lo sanea antes de mandarlo: descarta caracteres de control y recorta a 200 caracteres, para que un valor construido concatenando texto de un tercero no pueda inyectar cabeceras.

## Errores [#errores]

`ConnectError` se lanza para fallas a nivel del gateway (una respuesta HTTP no-ok) y para errores de configuración del cliente. Los de configuración salen del propio `createClient(...)`, así que el `try/catch` de abajo, alrededor de una llamada a una tool, no los cubre:

```ts
import { ConnectError } from "@emisso/connect";

try {
  await connect.tools.core.timestamp.now();
} catch (e) {
  if (e instanceof ConnectError) {
    console.error(e.code, e.status, e.requestId, e.suggestedFix);
  }
}
```

`code` es un valor del [catálogo de errores](/docs/operar/errores) compartido (`@emisso/contracts`), o `"transport_error"` cuando el cuerpo del error no trae un `error.code` parseable. Todo error del gateway trae `requestId` y un `suggestedFix` pensado para que un agente LLM pueda actuar sobre él directamente. Cuando el servidor adjunta detalle estructurado (por ejemplo los issues campo a campo de un `validation_error`), llega en `e.details`.

Una falla de red que nunca llega al gateway (DNS, conexión rechazada, una request abortada) se propaga como el rechazo nativo de `fetch`; **no** se envuelve en un `ConnectError`.

## Reintentos [#reintentos]

Los `GET` se reintentan hasta `maxAttempts` cuando el error es retryable según el catálogo. Si la respuesta trae `Retry-After` (en segundos o como fecha HTTP), se respeta; si no, el backoff es exponencial, con un techo de 8 segundos entre intentos.

<Callout type="warn" title="POST/execute nunca se reintenta solo">
  Toda llamada a una tool es un `POST` (`execute`), y el SDK **nunca la reintenta automáticamente**: una acción regulada (un giro bancario, una declaración SII) no puede arriesgarse a un doble filing. La única excepción es el refresh OAuth: un único 401→refresh→reintento vía `onUnauthorized`, para cualquier verbo, fuera del presupuesto de `maxAttempts`.
</Callout>

Un `Idempotency-Key` viaja en cada llamada como preparación a futuro (hoy es un no-op en el servidor) y se mantiene **estable** a través del reintento por refresh OAuth: cuando el servidor active el almacén de idempotencia, un POST reintentado tras renovar el token deduplicará contra su primer intento en vez de ejecutarse dos veces.

## Tipos exportados [#tipos-exportados]

Además de `createClient` y `ConnectError`, el paquete exporta `ClientOptions`, `ConnectClient` (el tipo del cliente ya construido, útil para pasarlo entre funciones) y `ConnectToolIO` (el mapa `id → { input, output }` de todas las tools, el mismo del que se deriva el accessor).
