# Changelog

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

## [2.0.1] — sin publicar

### 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] — sin publicar

### 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.
