# Changelog

Todos los cambios notables de `@flouviahq/elements` se documentan aquí.

## [2.3.0] — 2026-11-02

### Added

- **`cord:error`** con `code`: `not_found` (el documento no existe), `expired` (venció), `frame_blocked` (el navegador bloqueó el iframe: el dominio no está en la lista o `baseUrl` no es Cord) y `timeout` (no llegó `cord:ready` a tiempo). Llega como `onError` en React, `@error` en Vue y `cord:error` en el Web Component y el loader. La página de un token que no existe, la de una organización suspendida y la de un token ilegible se lo avisan al SDK; antes el iframe se revelaba vacío y la consola no decía nada. Tipos `CordEmbedErrorCode` y `CordEmbedErrorDetail`.
- **`readyTimeout`** (milisegundos para recibir `cord:ready`, 10000 por defecto, `0` lo desactiva) y **`loading: 'eager' | 'lazy'`** en `mountCotizador`, `<CordCotizador>`, `<CordInvoice>` y Vue; `loading="eager"` en el Web Component y `data-cord-loading="eager"` en el loader. Con `lazy` el tiempo cuenta desde que el iframe se ve; `eager` sirve dentro de un modal oculto.
- **`controller.update({ appearance, theme, minHeight })`**: cambia la appearance, el tema o el alto sin recargar el iframe, así que lo que el cliente escribía se conserva. Viaja por `postMessage` y el iframe lo valida con el mismo `sanitizeAppearance` que el parámetro de la URL. React, Vue y la propiedad `appearance` del Web Component lo usan en vez de remontar. Tipo `CordUpdateOptions`.
- **`cord:ready` trae `livemode`** (`false` en un documento de prueba, también sin dominios permitidos) y `cord:change` el estado en vivo (`onChange` en React, `@change` en Vue).
- **`QuoteViewState.invoiced`, `livemode` y `error`**: si ya se emitió la factura (pagada o no), si es de prueba y el último `cord:error`.
- **Tipos de eventos para cada entorno**: `CordElementEventMap` y `HTMLElementEventMap` para `addEventListener`, los elementos en el JSX de React 19 y `GlobalComponents` de Vue; los `emits` de Vue tienen su payload tipado y `<CordInvoice>` declara sus props.
- **`./react/viewer`**: `CordCotizador` y `CordInvoice` sin el Builder, el formulario fiscal ni sus diccionarios. Es lo que monta el componente de Framer.
- **El Builder calcula con el redondeo del servidor.** `elements/config` trae `decimales` (los de cada divisa de `monedas`: 0 en JPY y CLP, 3 en KWD) e `impuestos.redondeo` (`'line' | 'document'`, `document` en Chile), con el tipo `CordTaxRounding`, y el Builder los pasa a `calculateDocumentTotals`: el total que ve el comprador es el de la cotización que se crea.
- **Catálogo completo**: `client.products({ signal, onPage })` recorre todas las páginas de `/productos` por cursor (antes solo llegaban 50), y el Builder busca en todo el catálogo. `builder.setCatalog(products | null)` y la prop `catalog` de `useQuoteBuilder` son reactivas. Tipos `CordRequestOptions` y `CordProductsOptions`.
- **Ciclo de vida del Builder**: `builder.load()` y `builder.destroy()` (cancela carga, catálogo, envío e IA con `AbortController`), y `autoLoad: false` para cargar tú; React lo hace en un efecto, así que StrictMode ya no duplica los fetch, y Vue llama `destroy()` al desmontar. Una petición cancelada se rechaza con `CordError` de código `aborted`, nunca como error de red.
- **`CordErrorCode` completo** con `CORD_ERROR_CODES` y `CordKnownErrorCode`: todos los códigos que el camino de Elements recibe del servidor (también `ai_unavailable`, `ai_quota_exceeded`, `no_items` y `unknown_tax_rate`) más los del SDK; uno que esta versión no conoce llega como `string`. Los `issues` por campo del servidor llegan a `state.issues` (`CordFieldIssue`).
- **Crear la cotización devuelve `subtotal`, `total` y `moneda`** ya con cupón y descuento (`CreateQuoteResponse`).
- `<cord-fiscal-form required>`: como un `<input>`, sin `required` vacío es válido; `setValues({ country })` vuelve a dibujar los campos del país y un `<fieldset disabled>` lo deshabilita.
- **`CordIntegrationError`** (con `code: CordIntegrationErrorCode` y `docUrl`) en `@flouviahq/elements`, `/react`, `/vue` y `/headless`. Todo aviso de consola y todo error de integración del paquete sale de un solo catálogo en es, en, pt, fr y de, con el formato `[Cord] <code>: <texto> <url>`; el enlace lleva a su código en "Errores de integración" del resumen de Elements, en el idioma del `<CordProvider>` o de la página.
- **`nonce`** en `configureCord()`, `<CordProvider>`, `<CordCotizador>` de `/react/viewer` y de Vue y `CordElementOptions`: va en cada `<style>` y `<link>` que el SDK agrega a la página. `embed.js` y `webflow.js` lo toman de su propio `<script nonce>`. La política mínima de cada superficie está en "CSP de tu página" de Seguridad.
- **`loadFonts: false`** en `configureCord()` y `<CordProvider>`: no agrega los `<link>` de `appearance.fonts`; la familia se sigue aplicando.
- **Framer**: documento (cotización o factura), tema, color principal, radio, fuente, `appearance` desde código y los eventos `onReady`, `onChange`, `onError`, `onApproved` y `onPaid`; el aviso sin token sale en el idioma elegido.
- **Ejemplos ejecutables** con su `package.json`: Next.js 15 con App Router (`params` como Promise), Vite + React, Vite + Vue y HTML sin bundler. En las docs se abren en StackBlitz, y `apariencia` tiene un editor visual que genera el objeto `appearance` listo para copiar.
- `/embed/demo` sirve la cotización de demostración, de solo lectura, para probar el visor sin cuenta.
- **`quote.milestone_completed`** en el contrato de webhooks (`WEBHOOK_EVENT_TYPES`, `isWebhookEventType`) y su tipo **`CordWebhookMilestoneCompletedData`**, exportado en `@flouviahq/elements/server`: el negocio marcó cumplido un hito del plan de pagos y ya se puede cobrar. Trae `cobro_id`, `numero` (el orden del hito), `etiqueta`, `monto` en `moneda` y `vence` (`'YYYY-MM-DD'`, en la zona del negocio).
- **`payment.partial` de un plan por hitos**: `tipo` acepta `'hito'`, `numero_cuota` es el orden del hito y `etiqueta` trae el nombre del pago ("Entrega del diseño"); `null` en anticipo, saldo y cuota.
- **`facturas` en `CordWebhookDisputeData`** (`dispute.created`, `dispute.closed`): los ids de las facturas que pagó el cobro disputado. Su saldo se reabre mientras el dinero esté retenido o si el contracargo se pierde; un arreglo vacío si el cobro no pagó facturas.
- **El sobre de cada webhook dice su modo y su versión**: `CordWebhookEvent` (y `CordWebhookPingEvent`) traen `mode` (`'live'` o `'test'`, `test` si lo emitió una organización de prueba), `api_version` (la versión de la API con la que se armó `data`, la fijada del endpoint) y `test?: boolean`, solo `true` en un evento simulado.
- **Campos que el servidor ya mandaba y el contrato no tipaba**: `language` en `CordWebhookClientData` (con el tipo `CordLanguage`), `documento` en `CordWebhookInvoiceData`, `factura_id` en `CordWebhookTaskData`, y `tipo: 'contraoferta'` y `propuesta` en `CordWebhookCommentData`. Nuevos en el servidor: `cliente_id` en `CordWebhookInvoiceData` y `moneda` en `CordWebhookProductData`, la divisa de `precio_lista` (la de la organización).
- **`sin_descuento` en las líneas de `calculateDocumentTotals`**: la línea no recibe el descuento de documento ni cuenta para su porcentaje o para el tope del monto fijo. Es la línea de intereses de un plan de pagos, que no puede cambiar después de que el cliente la aceptó. Sin la opción, el motor da exactamente lo mismo que antes.

### Changed

- **Un solo vocabulario de eventos `cord:*`.** El Web Component emite `cord:ready`, `cord:approved`, `cord:change`… como el loader. Los nombres sin prefijo de la 2.2 (`ready`, `approved`, `statechange`…) siguen saliendo como alias deprecados hasta la 3.0, salvo `message`, que ya no sale. En Vue `@state` queda deprecado en favor de `@change`.
- **`state.paid` solo es verdadero con el estado `paid` o `cord:paid`.** Una cotización facturada sin pagar ya no cuenta como pagada; usa `state.invoiced` para saber si se facturó.
- **`QuoteStatus`** tiene sus ocho estados: `draft`, `sent`, `viewed`, `approved`, `rejected`, `expired`, `paid` e `invoiced`.
- `onReady` recibe el `detail` de `cord:ready`. `CordEventDetail.token` queda deprecado: ningún evento lo manda. `CordViewedDetail` no trae datos.
- El tema `auto` y el tema oscuro se aplican por CSS con `data-cord-theme` y `prefers-color-scheme`, sin JavaScript: el HTML del servidor y el del cliente coinciden. `flat` y `shadowCard` se respetan en los componentes de React.
- React usa `useSyncExternalStore` y genera los ids con `useId`: dos Builders en la misma página ya no comparten `id`.
- Tamaño: el presupuesto de cada entrypoint es lo medido más 0.75 KB y no se sube sin una razón escrita. `./react/viewer` y `framer` bajan; `react` e `index` suben de 1.2 a 2.7 KB gzip por lo de esta versión.
- `mountCotizador()`, `useCordContext()`, `useBuilderContext()`, `useQuoteBuilder()`, `createCordClient()` y `<CordProvider>` lanzan `CordIntegrationError` en vez de `Error`. Sigue siendo `instanceof Error`; cambia el texto de `message`. `missing_key` de `useCreateQuote()` y `clients_require_proxy` de `useCordClients()` siguen llegando como `CordError`, con el texto en el idioma del Provider y `docUrl`.
- `<cord-quote>`, `<cord-invoice>`, `<cord-fiscal-form>` y la barra de depuración ponen sus estilos como una hoja construida compartida, sin `<style>` en el Shadow DOM, con respaldo `<style nonce>` donde no hay hojas construidas. El skeleton ya no usa `innerHTML` (Trusted Types).
- El error de `<CordProvider>` con una llave que no es `pk_` ya no repite sus primeros caracteres.
- `api-types.md` congela los tipos públicos de las ocho entradas: `check-exports` falla si un `.d.ts` cambia sin actualizarlo. Los bloques `ts` y `tsx` del README y de las docs compilan en CI contra el paquete (`check-docs`), los enlaces de CDN llevan versión y CI prueba con React 18 y 19.

### Fixed

- `<cord-quote>` despachaba un evento `message` que llegaba a cada `window.addEventListener('message', …)` de la página como un `CustomEvent` sin `data`.
- `cord:resize` se re-emite al anfitrión, como decían las docs.
- Una `className` que cambia (Tailwind condicional, la prop `style` de Framer) ya no borra las clases del iframe: el SDK monta dentro de un contenedor propio.
- Vue ya no recarga el iframe cada vez que cambia la identidad de `appearance`.
- `mountCotizador` agregaba una hoja de estilos en cada render del Web Component y sus temporizadores sobrevivían a `destroy()`.
- `--cord-shadow-card: default` quitaba la sombra en React.
- El selector de productos sigue el patrón de combobox del APG: se elige con flechas y Enter, con `aria-controls` y `aria-activedescendant`, y el interruptor de impuesto tiene foco visible.
- `<cord-fiscal-form>` bloqueaba siempre el `<form>` que lo contiene y emitía `fiscalvalid` en cada sincronización.
- Soltar un archivo en la IA mientras se leía otro mostraba "Error de red".

### Docs

- README: el motor es `calculateDocumentTotals` (`calculateTotals` queda como camino heredado), la tabla de Appearance sale de `appearance.ts` (`flat`, `night`, las variables, `rules`, `layout`, `fonts`, `elements` y `baseTheme`) y los eventos usan el vocabulario `cord:*`. El CDN se fija a `@2.3.0`.

## [2.2.0] — 2026-10-13

### Added

- **Elements en portugués, francés y alemán.** El `locale` de `<CordProvider>` y de `<CordFiscalForm>`, el atributo `lang` de `<cord-fiscal-form>`, `fiscalText()` y `formatMoney()` aceptan `'es' | 'en' | 'pt' | 'fr' | 'de'`, con o sin región (`pt-BR`, `fr-FR`, `de-DE`). El Builder, el formulario fiscal y la barra de depuración tienen todos sus textos en los cinco idiomas, y los importes y porcentajes siguen la convención de cada uno (`1.234,50 €`, `USt. 19 %`). Sin `locale` el default sigue siendo `'es'`; un idioma que no se reconoce cae a inglés, nunca a español.
- El tipo **`CordLocale`**, exportado en `@flouviahq/elements`, `/headless` y `/react`. `FiscalLocale` pasa a ser su alias.
- `networkError` en los mensajes de `CordProvider`: un error de red al crear la cotización o al leer un pedido con IA se muestra en el idioma del Provider, no en el del cliente HTTP.
- **`validateTaxId(country, value, { locale?, persona? })`** y **`TAX_ID_COUNTRIES`** en `@flouviahq/elements/headless`: validan el identificador fiscal de los doce mercados (RFC, NIF/NIE/CIF, EIN, SIREN/SIRET/TVA, USt-IdNr./Steuernummer, VAT/UTR, Business Number/GST/HST, CPF/CNPJ —incluido el CNPJ alfanumérico de julio de 2026—, NIT, CUIT, RUT y RUC) con su dígito verificador. Devuelven el valor normalizado y el tipo detectado, o un motivo que nombra el dato local en español, inglés, portugués, francés o alemán (`locale: 'es' | 'en' | 'pt' | 'fr' | 'de'`; uno desconocido cae a español). Un país sin validador nunca bloquea.
- **`calculateDocumentTotals(lines, { roundLines })`**: con `roundLines` (los decimales de la divisa del documento: 2 en MXN o EUR, 0 en JPY o CLP) cada línea se redondea antes de sumar —base e impuesto— y los totales son la suma de importes ya redondeados. Es lo que exige un comprobante fiscal: el CFDI 4.0 rechaza un `Total` que no sea la suma de sus conceptos, y una factura en JPY o CLP no puede llevar decimales. Sin la opción, el motor se comporta igual que antes.
- **Impuesto redondeado por documento (`taxRounding: 'document'`)** en `calculateInvoiceTotals` y `calculateDocumentTotals`, con los tipos `TaxRounding` y `RoundingOptions`. Con `roundLines`, el impuesto de cada tasa es el redondeo de (suma de las bases de esa tasa × tasa) en vez de la suma de impuestos ya redondeados por línea: es la regla del DTE chileno (IVA = Monto neto × tasa, montos enteros; Formato DTE v2.2 del SII, campos 107 y 112). El impuesto de cada tasa se reparte entre sus líneas por mayor residuo, así que la suma por línea sigue siendo exactamente el impuesto del documento y cada línea queda a lo más a una unidad de `round(base × tasa)`. Con precios con impuesto incluido, la base de cada tasa se desagrega una sola vez sobre la suma de sus importes. `'line'` sigue siendo el default: sin la opción, el motor da exactamente lo mismo que antes.
- **Retención sobre la base gravada (`base: 'gravado'`)** y el tipo `RetencionBase`: la retención se calcula solo sobre los conceptos que llevan impuesto. Es la Retención de IVA mexicana (LIVA art. 1-A), que no alcanza a un concepto exento; `CordElementsConfig.impuestos.retenciones[].base` puede traer este valor. Las bases `subtotal` e `impuesto` no cambian.
- **Descuento de documento** (`descuento: { tipo: 'porcentaje' | 'monto', valor }`) en `calculateInvoiceTotals` y `calculateDocumentTotals`, con el tipo `DescuentoInput`. Se aplica antes de impuestos y se reparte entre las líneas en proporción a su importe bruto; con `roundLines` el reparto es en unidades mínimas por mayor residuo, así que la suma por línea es exactamente el descuento del documento y ninguna línea queda negativa. Un monto se topa en el bruto del documento; con precios que incluyen impuesto rebaja lo que el cliente paga y la base se desagrega después. Cada línea trae `descuento` (lo que bajó su base) y los totales `descuentoTotal`, ambos antes de impuestos; las retenciones se calculan sobre las bases ya descontadas. Sin la opción, el motor da exactamente lo mismo que antes.
- **Cupones en el Quote Builder headless**: `setCupon(codigo)` y `cupon` en el estado. El código viaja al crear la cotización y el servidor lo valida y aplica; el total que devuelve la API ya lo incluye.

### Changed

- **`CordElementsConfig.org.locale` puede traer `pt`, `fr` o `de`.** El servidor ya no convierte esos idiomas a inglés: el negocio recibe su idioma y las etiquetas de impuestos de su catálogo en él (Isento, Exonéré, Steuerfrei). Es un valor nuevo del mismo campo; el nombre del impuesto sigue saliendo del país.
- `<cord-fiscal-form>`, la barra de depuración y el título accesible del iframe (`Quote`, `Devis`, `Rechnung`…) ya no caen a español cuando la página está en otro idioma: siguen su `lang` y, si no lo reconocen, usan inglés.
- En portugués, francés y alemán los plazos llevan los días ("Net 30 · 30 Tage") y el contado es "À vista", "Paiement à réception" y "Sofort fällig".
- Tamaño: los textos de los tres idiomas viajan en el bundle, sin carga diferida, para que el Builder se dibuje en el idioma correcto desde el primer render y también con SSR. Suman ~3.9 KB gzip en `react` y `framer`, ~1.8 KB en el entry principal, ~1.4 KB en `headless` (incluye los motivos de `validateTaxId` en cinco idiomas), ~0.9 KB en `vue` y ~0.9 KB en el loader (`embed.js`, `webflow`).

### Fixed

- **El Fiscal Element valida el identificador de los doce mercados**: fuera de México, España y Estados Unidos solo se comprobaba que el dato existiera; ahora `validateFiscalReceptor` rechaza un dígito verificador que no cuadra y guarda el valor normalizado. `unsupported_country` queda para países fuera de `TAX_ID_COUNTRIES`.
- **NIF-IVA español**: `validSpainTaxId("ESB12345674")` era falso (leía la E como letra de CIF); el prefijo `ES` se acepta y se quita. Los NIF K, L y M usan la letra del DNI, no el control del CIF, y `validCif` ya no los acepta.
- **EIN**: `validEin` rechaza los prefijos que el IRS nunca asigna (00, 07–09, 17–19, 28, 29, 49, 69, 70, 78, 79, 89, 96, 97).
- **Fiscal Element nombra el identificador fiscal de los doce mercados**: además de RFC, NIF/CIF y EIN, ahora etiqueta BN (Canadá), CNPJ/CPF, VAT reg. no./UTR, Steuernummer/USt-IdNr., SIREN/N° de TVA, NIT, CUIT, RUT y RUC en vez de un "Identificador fiscal" genérico.

## [2.1.0] — 2026-10-08

### Added

- **Más términos de pago**: `Terminos` y `CordTerminos` aceptan `net7`, `net15`, `net45` y `net90` además de `contado`, `net30` y `net60`. La regla es `net<N>` = N días naturales de crédito desde la fecha del documento. El selector de `<CordQuoteBuilder>` los rotula en español e inglés (`net7`…`net90` en los mensajes de `CordProvider`) y solo ofrece los que el servidor declara en `config.terminos`.
- `CORD_TERMINOS` en el contrato de configuración: la lista ordenada de plazos que acepta Cord.

## [2.0.2] — 2026-10-07

### Docs

- El README abre con un bloque para asistentes de IA que apunta a `llms.txt`, `llms-full.txt` y el OpenAPI, y resume lo que Cord ya tiene (llaves restringidas, registro de peticiones, versionado, paginación por cursor).

## [2.0.1] — 2026-10-06

### Changed

- **Barra de depuración en inglés**: sigue el `locale` de `<CordProvider>` o, sin él, el `lang` de tu página. Los avisos de appearance en consola también.

## [2.0.0] — 2026-10-05

Elements pasa a ser headless de punta a punta: todo el estado vive en un núcleo
sin framework (`@flouviahq/elements/headless`) y React, Vue, los Web Components y
el loader de una línea son consumidores de ese mismo núcleo. El SDK de servidor
se separa en `@flouviahq/node`.

### Breaking changes — tabla de migración

| Antes | Ahora | Qué hacer |
|---|---|---|
| `proxyUrl` era el endpoint de crear (`POST {proxyUrl}`) y `{proxyUrl}/clientes` | `proxyUrl` es una base con la forma de `/api/v1`: `GET {proxyUrl}/elements/config`, `GET {proxyUrl}/productos`, `POST {proxyUrl}/cotizaciones`, `GET {proxyUrl}/clientes` | Monta `createElementsProxy()` de `@flouviahq/node` en `/api/cord/[...path]`, o mueve tu handler de crear a `{proxyUrl}/cotizaciones`. |
| `useQuoteBuilder()` devolvía `cliente`/`setCliente(string)`, `items` sin llave, `updateItem(idx, …)`, `ivaIncluido`, `subtotal`/`iva`/`total` | Devuelve el estado headless: `cliente` (objeto), `items` con `key`, `builder.updateItem(key, …)`, `precios_incluyen_impuesto`, `totals` (con `porTasa` y `retenciones`), `issues`, `status` | Lee `state.totals.total` en vez de `total`; llama `builder.setCliente({ empresa })`; usa `item.key` en vez del índice. |
| `ivaPct` en `<CordProvider>` / `useQuoteBuilder` | Deprecado y sin efecto: cada línea elige su tasa del catálogo real de la organización | Quítalo. Si lo dejas, solo verás un aviso en desarrollo. |
| Divisas fijas MXN/USD, `$` con formato es-MX, una sola tasa, etiqueta "IVA" | Divisas, tasas por línea, retenciones, términos y nombre del impuesto vienen de `GET /api/v1/elements/config`; montos con `Intl` en la divisa elegida | Nada. Si dependías del texto "IVA", ahora dice el nombre del país (VAT, GST, Sales tax). |
| `<CordProvider appearance>` escribía en `:root` y ponía `data-cord-theme` en `<html>` | Las variables `--cord-*` se aplican solo en la raíz de cada componente | Si estilizabas `html[data-cord-theme]`, cambia a la clase `.cord-builderRoot`. |
| `<cord-cotizador>` en el light DOM con estilos en `<head>` | `<cord-quote>` (y `<cord-cotizador>` como alias) en Shadow DOM; personaliza con `::part(frame)`, `::part(skeleton)` y `--cord-*` | Si apuntabas a `.cord-embed iframe` desde tu CSS, usa `cord-quote::part(frame)`. |
| `./server` (`CordAPI`, `CordWebhooks`) | Deprecado; `@flouviahq/node` lo reemplaza con la API v1 completa y anti-replay obligatorio | `new Cord(sk)` y `await constructEvent(rawBody, headers, secret)`. |

### Added

- **`@flouviahq/elements/headless`**: `createCordClient` (idempotencia automática, reintentos con `Retry-After`, timeouts, caché de configuración con ETag), `createQuoteBuilder`, `createFiscalForm`, `reduceQuoteView`, `createStore`, el módulo fiscal y el validador de appearance. Sin efectos al importar.
- **`<cord-invoice>`**: la factura de `/i/{token}` embebida, con `<CordInvoice>` en React y Vue y `data-cord-document="invoice"` en `embed.js`. El pago se abre en una ventana de Cord; el embed emite `status_changed` y `paid` cuando cambia el saldo. Misma política de dominios permitidos que la cotización.
- **Appearance v2**: temas `flat` y `night` (alias de `dark`), 22 variables que el embed y el Builder consumen de verdad (campos, foco, botones, superficies, sombra, espaciado), `rules` por componente público (`Button`, `Input:focus`, `Total`, `Bubble--mine`...) con propiedades de una lista cerrada, y `layout` (`compact`, `hideChat`, `hideNotes`). El tema oscuro ahora cambia la tarjeta completa. Todo pasa por el mismo validador en el SDK y en el servidor; un objeto de más de 8 KB se ignora y el SDK lo avisa.
- **Fiscal Element**: `<cord-fiscal-form>` (asociado a formularios, aporta su valor y bloquea el envío con errores), `<CordFiscalForm>` en React, `useCordFiscalForm` en Vue y `<CordBuilder fiscal>`. RFC con dígito verificador del SAT, régimen y uso de CFDI filtrados por tipo de persona, 616/S01 para público en general, aviso del régimen societario en el nombre; NIF/NIE/CIF y EIN. El servidor valida con el mismo código.
- **Estado en vivo**: `controller.state`, la propiedad `state` y el evento `statechange` de `<cord-quote>`, `onStateChange` en React y el evento `state` en Vue. Eventos nuevos `cord:updated` y `cord:status_changed`; `cord:ready` trae estado, folio, total y divisa (solo con dominios permitidos).
- **Doble clic seguro**: el Builder y `useCreateQuote` usan una sola clave de idempotencia por intento de envío.
- `public/embed.js` y `./webflow` salen del mismo `src/loader.ts`; CI verifica que el archivo publicado coincide con el build.
- Toda petición manda `Cord-Version: 2026-10-01` (`CORD_API_VERSION`): actualizar Cord no cambia la forma de las respuestas que lee el SDK.
- **Barra de depuración** (`debug` en `<CordProvider>`, `configureCord`, el atributo `debug` de `<cord-quote>` o `data-cord-debug`): eventos del iframe, peticiones con su request id y avisos de configuración, en un Shadow DOM. Solo con llaves de prueba; sin llave, solo en `localhost` o con `?cord_debug=1`.

### Security

- El relay del iframe solo acepta los eventos conocidos y acota la altura.
- Todo appearance pasa por el validador por gramática antes de llegar a CSS.

## [1.1.0] — nunca publicada

Estos cambios no salieron como 1.1.0: se publicaron dentro de la 2.0.0. Se conservan aquí como registro de qué cambió y por qué.

### Security

- **Embed sin dominios permitidos = solo lectura.** Si tu organización no configuró
  dominios en Ajustes › Elements, cualquier sitio podía enmarcar la cotización con los
  botones de aprobar, firmar y pagar (clickjacking). Ahora el embed muestra la propuesta
  y un botón que abre `/q/{token}` en una pestaña nueva, en el dominio de Cord. Con
  dominios configurados, todo sigue igual. En solo lectura los eventos llegan sin `detail`.
- **El pago nunca corre dentro del iframe.** Antes el botón navegaba el propio iframe a
  `/q/{token}/pay`, que el navegador bloquea al enmarcarse. Ahora abre una ventana de
  Cord y `cord:pay` trae la URL **absoluta** (antes era relativa: `window.location.assign`
  en tu sitio la resolvía contra tu dominio).
- **Appearance validado por gramática.** Claves enumeradas (`colorPrimary`,
  `colorBackground`, `colorText`, `colorTextSecondary`, `colorBorder`, `colorDanger`,
  `fontFamily`, `fontSize`, `borderRadius`), colores y longitudes por tipo, sin `url(`.
  Lo que no encaja se descarta.
- **Publishable keys en vivo exigen dominios permitidos** (`origin_allowlist_required`).
  Las `pk_test_` siguen funcionando desde cualquier origen.
- **Secret keys desde un navegador se rechazan**: el servidor responde
  `secret_key_in_browser` y `CordAPI` lanza si recibe una `sk_` en un navegador.

### Added

- **Los 41 eventos de webhook tipados**, con el `data` exacto de cada uno (factura,
  cliente, producto, tarea, promesa, contracargo, reembolso, depósito y cuenta). Antes
  el SDK declaraba 12. Los tipos salen del mismo contrato con el que la app emite
  (`src/contract/webhook-events.ts`), así que ya no pueden divergir. Nuevos exports en
  `./server`: `WEBHOOK_EVENT_TYPES`, `WEBHOOK_EVENT_OBJECTS`, `isWebhookEventType`.
- `CordWebhookQuoteData` agrega `moneda` y `cliente_id`, que el servidor ya mandaba.
- `CordError#requestId` y `CordError#docUrl`. Toda respuesta de `/api/v1` trae
  `Cord-Request-Id`; los errores lo repiten en el cuerpo junto a `doc_url`.
- `CordErrorCode` cubre todos los códigos reales del servidor (idempotencia, cuota,
  origen, sesión vencida, entre otros).
- CORS real en `/api/v1/productos` (GET) y `/api/v1/cotizaciones` (POST) para el modo
  publishable: antes no había preflight y el navegador bloqueaba la llamada cross-origin.

### Fixed

- `cord:ready` y `cord:viewed` se emitían dos veces por carga.

### Entrega de webhooks

Identidad de evento para los webhooks salientes — parte de llevar el motor de entrega
(`src/lib/webhook-delivery.ts`, no publicado, es código de la app) a un outbox durable con
reintentos con backoff exponencial. 100% aditivo, sin breaking changes.

#### Added

- **`CordWebhookEvent.id`** (`evt_…`) — cada entrega trae ahora un identificador estable,
  igual en el body y en el header `X-Cord-Event-Id`. Es el MISMO valor en reintentos
  automáticos y en un replay manual desde Ajustes › Developers (el payload se reenvía
  byte-idéntico) — úsalo para deduplicar del lado del receptor.
- Headers nuevos en cada entrega: `X-Cord-Event-Id` (= `id` del body), `X-Cord-Delivery-Id`
  (id del intento — cambia en cada replay, a diferencia de `X-Cord-Event-Id`),
  `X-Cord-Attempt` (número de intento, 1-indexado), `Idempotency-Key` (repite
  `X-Cord-Event-Id` — varios frameworks de servidor lo leen de ahí sin configuración extra).
- **Rotación de secreto sin downtime**: `CordWebhooks#constructEvent`/`constructEventAsync`
  ahora aceptan `X-Cord-Signature-V1` con **más de un** par `v1=` (uno por cada secreto
  vigente durante una rotación en curso) — basta con que UNO cuadre. Nuevo botón "Rotar
  secreto" en Ajustes › Developers con ventana de solape configurable (1h/24h/72h): durante
  esa ventana Cord firma con el secreto nuevo Y el viejo a la vez, así que **no hace falta
  desplegar tu verificador actualizado el mismo instante en que rotas** — puedes rotar y
  actualizar tu `endpointSecret` en cualquier momento dentro de la ventana sin perder ni una
  entrega. El header legacy `X-Cord-Signature` (un solo valor posible) firma con el secreto
  VIEJO durante la ventana y pasa al nuevo cuando cierra.
- **6 eventos nuevos**: `quote.updated` (una cotización ya enviada se editó y se reenvió —
  antes reusaba `quote.sent`, indistinguible de un envío inicial), `quote.expired` (venció su
  vigencia sin que el cliente decidiera), `quote.deleted` (se borró un borrador),
  `payment.partial` (cayó un anticipo/saldo/cuota sin cubrir el total — antes NINGÚN webhook
  avisaba de esto, solo te enterabas hasta que `quote.paid` cubría el 100%), `payment.failed`
  (un cobro recurrente de iguala falló). `CordWebhookEvent` pasó a **unión discriminada** por
  `event`: todos comparten `CordWebhookQuoteData`, excepto `payment.partial`, cuyo `data` trae
  además `tipo`/`monto`/`numero_cuota`/`saldo_pendiente`/`payment_method`
  (`CordWebhookPaymentPartialData`, nueva interfaz exportada) — revisa `event.event` antes de
  leer `event.data` para que TypeScript te dé el tipo correcto.
- **Motor de totales con impuesto por línea**, exportado desde la raíz del paquete:
  `calculateInvoiceTotals(items, { ivaIncluido })` (cada línea con su propia tasa y
  desglose `porTasa`) y `calculateDocumentTotals(items, { ivaIncluido, retenciones })`
  (lo mismo más retenciones: `total = subtotal + impuestos − retenciones`). Es el mismo
  motor con el que Cord calcula sus cotizaciones y facturas, así que un total que
  reproduces en tu servidor cuadra con el del link. `calculateTotals` (tasa única) se
  conserva sin cambios.

#### Notes

- Ningún header ni campo existente cambió de forma — `X-Cord-Signature`/`X-Cord-Signature-V1`
  siguen firmando exactamente el mismo body crudo; `constructEvent`/`constructEventAsync` no
  requieren cambios de tu parte, el campo `id` simplemente aparece en el objeto devuelto.
- Reintentos ahora corren con backoff exponencial (hasta 11 intentos en ~3.6 días) en vez de
  2 intentos fijos — no afecta el contrato del SDK, pero un endpoint caído puede recibir el
  mismo evento más veces de las que veía antes. Deduplicar por `id` es la forma correcta de
  manejarlo (nunca fue seguro asumir entrega exactly-once).

## [1.0.0] — 2026-07-16

Reescritura mayor para llevar el SDK al nivel de Stripe Elements / Clerk Elements.
Motivada por una auditoría real (integración en un cliente externo, "El Zarco") que
encontró: 3 `@ts-ignore` por tipos incompletos, un componente reescrito a mano (286
líneas) porque los estilos inline no se podían sobreescribir, un botón "Abrir en pestaña
nueva" porque el iframe perdía la marca sin `<CordProvider>`, datos de cliente perdidos en
silencio, y una respuesta de API cuyo campo `folio`/`token` siempre llegaba `undefined`.

**Ninguno de estos cambios requiere un paquete nuevo ni un scope nuevo** — sigue siendo
`@flouviahq/elements`, mismo nombre en npm, mismos entrypoints.

### ⚠️ Breaking changes — tabla de migración

| Antes | Ahora | Qué hacer |
|---|---|---|
| `types/*.d.ts` escritos a mano (10 de 11 exports de `./react` sin tipos) | Tipos **generados** desde `src/` vía `tsc` | Nada — si tenías `@ts-ignore` por esto, bórralos. |
| `CordAppearance.rules` (tipado, pero el servidor lo descartaba) | Campo eliminado | Si lo usabas, no tenía efecto — no hay migración real. |
| `CordCotizadorProps.onEvent?: (type: string, detail) => void` | `onEvent?: (event: CordEvent) => void` | `onEvent={(type, detail) => ...}` → `onEvent={(event) => { const { type, detail } = event; ... }}` |
| Inline styles en `<CordBuilder>` y sus slots | Clases estables `.cord-*` (en `@layer cord`) | Si dependías del look exacto anterior, pásalo vía `appearance.variables`/`appearance.elements`. |
| `useCordClients()` intentaba fetch en modo publishable (y recibía 403 en silencio) | Falla ruidoso y tipado (`CordError` con `code: 'clients_require_proxy'`), sin red | Pasa `clients` como prop, o usa `proxyUrl`. |
| `CordProviderProps` aceptaba `publishableKey` y `proxyUrl` a la vez | Unión discriminada — pasar ambos es error de compilación | Elige uno. Si tenías los dos "por si acaso" (como El Zarco), quita el que no usas de verdad. |
| `result.folio`/`result.token` de `useCreateQuote()`/`CordAPI.quotes.create()` | Desenvuelven `{ data }` — ya no son `undefined` | Si tenías un workaround leyendo `result.data.folio`, quítalo. |
| `CreateQuoteResponse.link_publico` relativo (`/q/abc`) | Absoluto (`https://cordhq.app/q/abc`) | Si concatenabas tu propio dominio, ya no hace falta. |
| `CordWebhooks#constructEvent` devolvía `{ type, data, created }` (mentira — `evt.type`/`evt.created` SIEMPRE `undefined`) | `{ event, created_at, data }` (la forma real) | `evt.type` → `evt.event`; `evt.created` → `evt.created_at`. |
| Un solo `X-Cord-Signature` sin timestamp (sin protección anti-replay) | Doble firma: `X-Cord-Signature` (intacta) + `X-Cord-Signature-V1` (con timestamp) | Nada si usas `constructEvent` del SDK. Si verificabas a mano, sigue funcionando con la legacy — considera migrar a V1. |
| `data-cord-cotizador` + `data-token` en `embed.js` | `data-cord-token` (mismo vocabulario que Webflow) | El par legacy sigue funcionando (embeds ya publicados no se rompen). |

### Added

- **Tipos generados, no escritos** (`tsc --emitDeclarationOnly`) + anti-deriva:
  `scripts/check-exports.mjs` compara los exports reales del bundle contra un snapshot
  committeado (`api-report.json`) — falla si divergen.
- `configureCord({ baseUrl, appearance, publishableKey })` — configuración global estilo
  `loadStripe`, para Vue/Web Component/vanilla sin `<CordProvider>`.
- `CordProviderProps.ivaPct` — configura el % de IVA una vez, en el Provider, en vez de
  por instancia del Builder (evita que el total del Builder diverja del real).
- `theme: 'dark' | 'auto'` funciona de verdad, tanto en el iframe (`CordCotizador`) como
  en los componentes nativos (`CordBuilder`), con paleta oscura default y reacción en vivo
  a `prefers-color-scheme` cuando es `'auto'`.
- `appearance.baseTheme: 'none'` — headless real: cero CSS de Cord, clases `.cord-*` intactas.
- `appearance.elements` (`CordElements`) — override por elemento interno, estilo
  `appearance.elements` de Clerk. Ver README.
- `useQuoteBuilder()` — el estado completo del Builder como hook standalone (sin
  `<CordBuilder>` de por medio). `<CordBuilder>` ahora es un consumidor delgado de este hook.
- Eventos tipados como unión discriminada (`CordEvent`) + callbacks nuevos: `onViewed`,
  `onSigned`, `onItemComment`.
- `CreateQuoteInput.cliente` — cotizar a un cliente NUEVO (find-or-create acotado: solo
  crea, nunca actualiza uno existente; queda marcado `origen: 'embed'` para tu revisión).
- Errores tipados (`CordError` con `.status`/`.code`/`.payload`, `CordErrorCode`) en todos
  los hooks y en `CordAPI`.
- `roundMoney()` en el motor de cálculo — redondeo SOLO para mostrar, nunca afecta
  `calculateTotals` (que sigue exacto a lo que guarda el servidor).
- Validación de prefijo: `<CordProvider>` lanza si `publishableKey` no empieza con `pk_`
  (atrapa el error más caro posible: pegar una `sk_` en el navegador).
- `CordWebhooks#constructEventAsync` — verifica con WebCrypto para runtimes edge/workers.
- `constructEvent`/`constructEventAsync` aceptan un `Headers` real o `Record<string,string>`
  (mejor DX que solo el string crudo del header).
- `.github/workflows/elements.yml` — CI: `tsc` → build → `check-exports` → `attw` → `publint`.
- Shims de compatibilidad `node10` (resolución clásica de TypeScript) para `./react`,
  `./vue`, `./framer`, `./webflow`, `./server`.

### Fixed

- **El sobre de respuesta nunca se abría**: toda ruta `/api/v1/*` envuelve en `{ data }`,
  pero `useCreateQuote`/`CordAPI.fetch` no lo desenvolvían — `result.folio` era SIEMPRE
  `undefined`, en el hook y en el Server SDK.
- `<CordCotizador>` sin `<CordProvider>` en el árbol **truena** (`useCordTranslations()`
  llamaba `useCordContext()`, que lanza sin Provider) — ahora funciona standalone de verdad.
- Appearance rancia: `<CordCotizador>` no reaccionaba a cambios de `appearance` después del
  primer render (las deps del `useEffect` no la incluían).
- `<CordProvider appearance>` no tenía `baseUrl` en sus props — imposible apuntar a
  self-host/staging desde el Provider (línea muerta: `props.baseUrl || (context?.proxyUrl ?
  undefined : undefined)` siempre daba `undefined`).
- 5 `baseUrl` hardcodeados a `https://cordhq.app` en distintos archivos — unificados
  en `resolveOrigin()`/`resolveApiBase()` (`config.ts`).
- `CordProduct.categoria` — referencia a un campo que el backend nunca devuelve (dead code
  en el dropdown de productos, eliminado).
- `class CordCotizadorElement extends HTMLElement` a nivel de módulo tronaba con
  `ReferenceError` al importar el paquete desde Node/SSR (sin DOM) — incluso sin instanciar
  nada. Afectaba el entrypoint `.` completo.
- `webflow.ts` ejecutaba `initWebflow()` (toca `document`) a nivel de módulo — mismo tipo de
  crash en SSR, ahora guardado por `typeof document !== 'undefined'`.
- `engine.ts`: `ivaPct` fuera de `[0,1]` (o `NaN`) ahora lanza `RangeError` en vez de
  producir un total silenciosamente incorrecto — este motor lo importa el servidor
  directamente para calcular dinero real.
- `postMessage(..., '*')` en `/embed/[token]` — ahora usa el `parentOrigin` real cuando
  coincide con la allowlist de `orgs.embed_domains` (mismo gate que ya protege
  `frame-ancestors`); sin allowlist o sin match, sigue en `'*'` (sin regresión).
- `.endsWith('/create')` para adivinar los endpoints de catálogo/clientes desde el proxy de
  creación — reemplazado por una política explícita: catálogo vía publishable key (endpoint
  público real), clientes vía `proxyUrl` como base o prop explícita.
- `cord:item_comment` se emitía en `QuoteCard` pero el relay del iframe no lo reenviaba —
  se perdía en silencio antes de llegar a ningún SDK.
- `X-Cord-Signature-V1`: anti-replay real con timestamp (antes el parámetro `tolerance` de
  `constructEvent` existía pero la validación estaba comentada — cualquier firma vieja capturada
  se podía re-enviar para siempre).

## [0.6.3] — 2026-07-06

### Fixed
- `CordBuilder`: reemplazado `alert()` nativo por un estado de error inline; ícono SVG en
  vez de emoji en el botón de eliminar línea.
- `useCordCatalog`/`useCordClients`: primer intento de golpear `/productos`/`/clientes` de
  forma más robusta (aún con el `.endsWith('/create')` frágil, corregido del todo en 1.0.0).
- `CordWebhooks`: parseo correcto del formato `sha256=<hmac>`; comparación de firma en
  tiempo constante vía `crypto.timingSafeEqual`.

## [0.5.0] y anteriores

Evolución del SDK desde un wrapper de iframe simple hasta una infraestructura con:
Web Component nativo + wrappers de React/Vue/Framer/Webflow, patrón compound (`CordBuilder`
con slots), hooks headless (`useCordCatalog`/`useCordClients`/`useCreateQuote`), llaves
publishable/secret (`pk_`/`sk_`), motor de cálculo compartido con el backend (`engine.ts`),
y Server SDK con verificación de webhooks. El detalle línea-por-línea de este tramo no se
reconstruyó aquí con precisión de commit — ver `docs/historial/README.md` del repo `flouvia-cord`
para el registro narrativo completo de esa etapa.
