# AutoFirma Client

[![CI](https://github.com/erseco/autofirma-client/actions/workflows/ci.yml/badge.svg)](https://github.com/erseco/autofirma-client/actions/workflows/ci.yml)
[![Codecov](https://codecov.io/gh/erseco/autofirma-client/graph/badge.svg)](https://codecov.io/gh/erseco/autofirma-client)
[![npm version](https://img.shields.io/npm/v/%40erseco%2Fautofirma-client)](https://www.npmjs.com/package/@erseco/autofirma-client)
[![License](https://img.shields.io/github/license/erseco/autofirma-client)](LICENSE)

Cliente TypeScript para usar la API oficial **AutoScript** con promesas,
tipado estricto, conversión de datos, errores normalizados y dobles de prueba.

> [!IMPORTANT]
> Este proyecto no pertenece al Gobierno de España, no incluye AutoFirma y no
> sustituye la validación criptográfica en servidor.

## Estado

La API pública es estable dentro de la línea `1.9` y se publica bajo `latest`;
lo que va entrando en `main` sale antes por el canal `canary`. El mayor y el
menor del paquete siguen la línea del repositorio oficial
`ctt-gob-es/clienteafirma`, y el parche versiona el wrapper (ver
[ADR-0010](docs/arquitectura/adr/ADR-0010-parche-propio.md)).
La versión de AutoScript realmente empaquetada, y sus constantes, son las que
registra `autoscript.lock.json`, la única fuente de verdad al respecto: hoy
fija el tag `v1.9.2`, que corresponde a AutoScript `1.9.0`, y lo empaqueta
la versión `1.9.3` del paquete. Consulta la
política de compatibilidad en
[AGENTS.md](AGENTS.md#compatibilidad-y-versiones) antes de actualizar.

## Instalación

Instala el paquete desde el [registro público de npm](https://www.npmjs.com/package/@erseco/autofirma-client):

```bash
npm install @erseco/autofirma-client
```

No necesitas clonar el repositorio ni configurar GitHub Packages.

El paquete incluye una copia fijada y verificada por hash de `autoscript.js`
en `vendor/autoscript.js`; no hace falta descargarla aparte. Ese fichero no es
un módulo: es un script clásico que no declara UMD ni exporta nada, sino que
confía en que su `var` de nivel superior se convierta en global. Por eso no
puede importarse con un empaquetador y hay que insertarlo como etiqueta
`<script>`. `loadAutoScript(url)` hace justo eso: sirve `vendor/autoscript.js`
desde tu propia infraestructura estática y pásale esa URL.

```ts
import { AutoFirmaClient, loadAutoScript } from "@erseco/autofirma-client";

const autoScript = await loadAutoScript("/vendor/autoscript.js");
const client = new AutoFirmaClient({ autoScript });
```

### Si lo sirves desde otro origen

El paquete fija `autoscript.js` por sha256 (ver
[ADR-0005](docs/arquitectura/adr/ADR-0005-empaquetar-autoscript.md)), pero esa
garantía termina donde empieza la red: si lo sirves desde un CDN o un dominio de
estáticos, quien controle ese origen controla el fichero. `loadAutoScript`
acepta los atributos de Subresource Integrity para recuperarla.

```ts
const autoScript = await loadAutoScript(
  "https://cdn.example.org/autoscript.js",
  {
    integrity: "sha256-…",
    crossOrigin: "anonymous",
  },
);
```

`autoscript.lock.json` guarda la huella en hexadecimal y SRI la quiere en base64
con prefijo `sha256-`:

```bash
printf 'sha256-%s\n' "$(node -p "require('./autoscript.lock.json').sha256" \
  | xxd -r -p | base64)"
```

## Uso

```ts
import { AutoFirmaClient } from "@erseco/autofirma-client";

const client = new AutoFirmaClient();
client.initialize();

const file = document.querySelector<HTMLInputElement>("#document")?.files?.[0];

if (!file) {
  throw new Error("Selecciona un documento.");
}

const result = await client.sign({
  data: file,
  format: "PAdES",
  parameters: {
    mode: "implicit",
    signingReason: "Aprobación del documento",
  },
});

console.log(result.signature);
```

Las cadenas se interpretan como Base64. Para datos sin codificar usa `File`,
`Blob`, `ArrayBuffer` o `Uint8Array`.

### Servicios intermedios

```ts
const client = new AutoFirmaClient({
  storageUrl: "https://example.org/storage",
  retrieveUrl: "https://example.org/retrieve",
});
```

Estos servicios pertenecen al protocolo de transporte de AutoFirma. No son un
repositorio documental.

### Guardar ficheros y comprobar el reloj

```ts
await client.saveDataToFile({
  data: base64Data,
  title: "Guardar firma",
  filename: "firma",
  extension: "csig",
  description: "Firma electrónica (*.csig)",
});
```

`saveDataToFile` pide a AutoFirma que guarde datos en un fichero elegido por
la persona usuaria; rechaza si la versión fijada de AutoScript no expone la
operación.

```ts
await client.checkTime({ checkType: "CT_RECOMMENDED" });
```

`checkTime` no informa del resultado a través de la promesa que devuelve:
AutoScript hace una petición **síncrona** que bloquea la página mientras
espera respuesta y solo avisa de un desfase con un `alert()` nativo;
cualquier error (incluido uno de red) se silencia. Con
`checkType: "CT_OBLIGATORY"` puede además impedir en silencio que una llamada
posterior a `initialize()` cargue AutoFirma, así que el orden de las llamadas
importa. El docblock de `checkTime` en [`src/client.ts`](src/client.ts)
documenta el detalle completo.

### Ficheros de ejemplo

Para probar una integración local sin usar documentos reales:

- [Texto para CAdES](docs/ejemplos/files/ejemplo.txt)
- [XML para XAdES](docs/ejemplos/files/ejemplo.xml)
- [PDF para PAdES](docs/ejemplos/files/ejemplo.pdf)

Contienen datos ficticios. Una firma generada con ellos no demuestra que la
integración valide certificados, cadenas de confianza o revocación.

## Firma local de varios PDF

```typescript
const batch = await client.signBatch({
  documents: files.map((data, index) => ({ id: String(index), data })),
  format: "PAdES",
  parameters: {
    layer2Text: "Firmado por $$SUBJECTCN$$",
    signaturePage: 1,
    signaturePositionOnPageLowerLeftX: 40,
    signaturePositionOnPageLowerLeftY: 40,
    signaturePositionOnPageUpperRightX: 260,
    signaturePositionOnPageUpperRightY: 110,
  },
});
for (const item of batch.signs) {
  if (item.result === "DONE_AND_SAVED" && item.signature) {
    // Descargar o guardar item.signature (Base64), asociándolo por item.id.
  } else {
    console.error(item.id, item.result, item.description);
  }
}
```

El formato y el algoritmo son comunes a todo el lote, y los parámetros también
salvo que un documento traiga los suyos: AutoFirma usa entonces esos **en lugar
de** los del lote —no los mezcla—, así que tienen que ir completos. Sirve para
que cada PDF lleve el sello en su página y su rectángulo, que no tienen por qué
coincidir de un documento a otro:

```typescript
documents: models.map(({ id, data, page, box }) => ({
  id,
  data,
  parameters: { layer2Text: "Firmado por $$SUBJECTCN$$", signaturePage: page, ...box },
})),
```

Sin `parameters` propios, el sello usa el mismo texto, página y rectángulo en
todos: comprueba que esa página y posición existen en cada documento. La demo
permite seleccionar varios PDF.

Es un lote JSON **local**, mediante `signBatchProcess`, sin servicios de prefirma
ni postfirma. No es la operación XML `AutoScript.signBatch`. Se devuelven los
estados nativos por ID (`DONE_AND_SAVED`, `ERROR_PRE`, `SKIPPED`); un callback
correcto no implica que todos los PDF se hayan firmado, y `DONE_AND_SAVED` no
significa que se haya guardado nada en WordPress u otro servidor.

`stopOnError` vale `false` por defecto para conservar los éxitos parciales.
Con `true`, AutoFirma puede descartar incluso las firmas anteriores al error.
`certificateFilters` admite parámetros para seleccionar el certificado.
El cliente rechaza lotes vacíos, IDs vacíos o repetidos y respuestas sin
correspondencia exacta con los documentos enviados.

Las operaciones asíncronas del wrapper sobre un mismo AutoScript no se pueden
solapar (error `OPERATION_IN_PROGRESS`), ni siquiera creando dos clientes.
Las llamadas directas a `raw` quedan fuera de esa protección. Los documentos y
resultados viajan completos en memoria; los límites del transporte intermedio
se aplican al mensaje agregado y codificado. El soporte está contrastado con el
AutoScript fijado; la firma real y el soporte móvil dependen de la aplicación
nativa instalada y deben probarse en el entorno de destino.

## Errores

Todo fallo llega como `AutoFirmaError`, que conserva intactos `nativeType` y
`nativeMessage` tal y como los devolvió AutoFirma. El campo `code` clasifica los
casos sobre los que se puede actuar:

| `code`                   | Cuándo                                                                 |
| ------------------------ | ---------------------------------------------------------------------- |
| `USER_CANCELLED`         | La persona usuaria canceló la operación o la selección de certificado. |
| `DATA_TOO_LARGE`         | El fichero excede la memoria disponible de AutoFirma.                  |
| `NATIVE_TIMEOUT`         | AutoFirma no respondió: no está instalada o no llegó a abrirse.        |
| `NATIVE_ERROR`           | Cualquier otro fallo de AutoFirma, incluidos sus códigos `SAF_xx`.     |
| `UNSUPPORTED_OPERATION`  | La versión fijada de AutoScript no expone esa operación.               |
| `AUTOSCRIPT_UNAVAILABLE` | `window.AutoScript` no existe o no es la API de AutoScript.            |

No existe un tamaño máximo declarado en ninguna parte. El fichero viaja entero
en una sola pieza codificado en Base64, que lo agranda un tercio, y el límite
real es la memoria de la aplicación nativa: al superarla responde `MEMORY_ERROR`
y el cliente lo entrega como `DATA_TOO_LARGE`.

## Qué aporta

- API basada en `Promise`.
- Tipos para PAdES, CAdES, XAdES y FacturaE.
- Entradas `Blob`, `File`, `ArrayBuffer`, `Uint8Array` y Base64.
- Serialización segura de parámetros en el formato de AutoScript.
- Errores normalizados sin perder el tipo y mensaje nativos.
- `MockAutoFirmaClient` para pruebas de aplicaciones consumidoras.
- Acceso a `client.raw` para funciones oficiales no cubiertas todavía.

## Seguridad y privacidad

La librería transforma los datos en memoria y los entrega al objeto AutoScript
de la página. **No incluye telemetría, backend, cookies ni almacenamiento**. Que
un documento salga o no del equipo depende de la configuración de AutoScript,
de los servicios intermedios configurados por la aplicación y del código de la
aplicación consumidora.

No confíes en los datos devueltos por el navegador para decisiones jurídicas o
de autorización. Valida en servidor la firma, la integridad del documento, el
certificado, la cadena de confianza y, cuando proceda, la revocación.

## Desarrollo

```bash
make install
make check
```

La CI ejecuta formato, TypeScript, tests con cobertura y construcción en Node
20, 22 y 24.

### Symlinks en Windows

Los procedimientos recurrentes del proyecto viven como skills en
`.agents/skills/`, la ruta que leen Codex y Grok. Como Claude Code y GitHub
Copilot buscan en `.claude/skills/` y `.github/skills/`, esos dos directorios
contienen enlaces simbólicos a los mismos ficheros en vez de copias.

Git guarda esos enlaces como tales, pero **en Windows solo los recrea al clonar
si el soporte está activado**; de lo contrario aparecen como ficheros de texto
que contienen la ruta de destino, y ni Claude Code ni Copilot encontrarán las
skills. La copia real de `.agents/skills/` sigue funcionando en cualquier caso.

```bash
git config --global core.symlinks true
```

Hay que fijarlo **antes de clonar**: en un clon ya hecho no basta con activarlo,
hay que volver a clonar o restaurar esas rutas. Además, Windows exige modo de
desarrollador o privilegios de administrador para crear enlaces simbólicos.

`prepare` construye el wrapper e incluye el AutoScript fijado también al instalar
desde un commit Git. El comando `vendor` compila su script con el `tsup` ya
instalado antes de ejecutarlo, por lo que funciona también con Node 20 y no
necesita borrado nativo de tipos. La carpeta temporal `.vendor-script/` se ignora.

Los objetivos rápidos son:

- `make lint`: comprueba Prettier y TypeScript.
- `make fix`: aplica Prettier.
- `make test`: ejecuta Vitest con cobertura.
- `make build`: genera ESM, CommonJS y tipos.
- `make check`: ejecuta lint, tests y build.

## Publicación

Cada tag `v*` ejecuta `.github/workflows/publish.yml`: comprueba que el tag
coincide con la versión de `package.json` y que esta comparte mayor y menor
con el tag fijado en `autoscript.lock.json`, repite todos los controles, verifica que el tarball
generado contiene `vendor/autoscript.js` con el `sha256` que registra el
lock y publica ese mismo tarball ya verificado (`npm publish artifacts/*.tgz`,
sin volver a empaquetar) en tres sitios: el registro público de npm, el gestor
de paquetes de GitHub y GitHub Releases.

La publicación en npm usa **npm Trusted Publishing con OIDC**, sin tokens. En
los ajustes del paquete hay que registrar como publicador de confianza el
repositorio `erseco/autofirma-client`, el fichero `publish.yml` y el entorno
`npm`. Un paquete admite **un solo publicador**, y por eso release y canary
comparten workflow: en ficheros distintos, uno de los dos fallaría siempre.

Ese mismo límite impide que OIDC cree un paquete que no existe todavía, así que
la primera publicación de un paquete nuevo tiene que hacerse a mano.

### Canal canary

Cada push a `main` publica una preversión bajo el dist-tag **`canary`**:

```bash
npm install @erseco/autofirma-client@canary
```

Su número parte del **patch siguiente** al de `package.json`, no del actual: una
preversión ordena por debajo de su versión, de modo que sobre el patch actual
ocuparía el hueco de la que está por salir. Los rangos habituales, como
`^1.9.3`, no resuelven preversiones: quien quiera la canary tiene que pedirla
expresamente.

GitHub Packages no admite Trusted Publishing, así que su publicación utiliza
el `GITHUB_TOKEN` efímero del workflow y requiere `packages: write`. Para
instalar el paquete, utiliza el registro público de npm como se indica en
[Instalación](#instalación), sin autenticación de GitHub Packages.

## Documentación

Una página con una demo ejecutable se publica en
<https://erseco.github.io/autofirma-client/>, generada con
`npm run build:web` a partir de `web/`. Las decisiones de arquitectura (ADR y
SDD) están en [`docs/arquitectura/`](docs/arquitectura/).

## Licencia

GPL-2.0-or-later. AutoFirma y AutoScript conservan sus propias licencias y
marcas.
