# DateKeys App
Implementación en TypeScript del protocolo DateKeys v0.8.2 y página de prueba en el navegador. Sustituye al prototipo, archivado en `../AppOld` (API Quicknet en Go, CLI tlock y cliente Svelte, commit `4d2b0a1`).
La implementación de referencia es la librería Go `g.activething.com/go/DateKeys`, en `../datekeys-go`. El plan de trabajo está en [docs/PLAN_codec_cbor_y_pagina_svelte.md](docs/PLAN_codec_cbor_y_pagina_svelte.md).
## Contenido
| Parte | Ubicación | Paso del plan | Estado |
|---|---|---|---|
| Codec CBOR del subconjunto, parsers de schema, DateKey, `inspect` | `src/lib/dkc/` | 4 | hecho |
| Página inspector, sin red | SvelteKit estático: `src/routes/`, `src/lib/inspector/`, `src/lib/components/` | 5 | hecho |
| Cifrado y descifrado en el navegador | fase 2: [docs/PLAN_fase2_ibe_noble2.md](docs/PLAN_fase2_ibe_noble2.md) | 6 | en curso: dependencias y guardas hechas (paso 2 de la fase) |
## Versiones
Hay tres números de versión, cada uno con su significado, como en la referencia Go:
| Versión | Dónde | Cambia cuando |
|---|---|---|
| Formato | Dentro de los objetos: la versión de framing de DKC1 y DKK1 y la de schema de la clave 1, hoy todas 1 | Cambia el formato. Un lector rechaza una versión que no conoce (§70) |
| Especificación | `SPEC_VERSION` de `src/lib/dkc/version.ts`, hoy `0.8.2`: la del tag `spec-v0.8.2` de `datekeys-go` | Cambia el texto normativo |
| Librería | `VERSION` de `src/lib/dkc/version.ts`, igual al campo `version` de `package.json` | Cambia la API o el comportamiento. Versionado semántico, sin promesa de estabilidad antes de 1.0.0 |
`version.test.ts` comprueba que `VERSION` coincide con `package.json` y con su lockfile, y que `SPEC_VERSION` es la versión que nombran los vectores y fixtures compartidos; `vectors.test.ts` exige esa versión a cada fichero. El pie de la página muestra las dos.
La versión actual es `0.1.0-dev`. Será `0.1.0` cuando la fase 2 añada la apertura de cápsulas. Cubre:
- la especificación 0.8.2, con versiones de formato 1;
- solo el scheme de Quicknet (`bls-unchained-g1-rfc9380`): un perfil de otro scheme se inspecciona, pero su release no se verifica (decisión 3 del plan de la fase 2);
- la inspección de los pasos 1 a 8, el IBE de tlock (`ibe.ts`) y la verificación de releases (`release.ts`). La apertura (pasos 9 a 18) llega en la fase 2 y el cifrado en la fase 3;
- todos los vectores y fixtures compartidos de `datekeys-go` en `9ac9cd9` (`spec-v0.8.2`);
- navegadores con Web Crypto y Node 20 o posterior.
[CHANGELOG.md](CHANGELOG.md) recoge los cambios de cada versión.
## `src/lib/dkc`
Lo que hay hoy (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegadores y en Node 20+: solo usa `Uint8Array`, `DataView`, `TextEncoder`/`TextDecoder`, `BigInt` y `crypto.subtle` (SHA-256). La fase 2 añade las dependencias de ejecución de su sección, y noble solo lo importarán `ibe.ts` y `release.ts`.
`crypto.subtle` solo existe en contextos seguros: `https`, o `http` en `localhost`. La página del paso 5 servida por `http` desde una IP de la red local (por ejemplo `vite --host` para probar en un móvil) no lo tiene, y `inspect` rechaza entonces con un `Error` que lo dice (`SHA-256 needs Web Crypto (crypto.subtle), …`) en vez de dar un veredicto. El registro por defecto no memoriza ese fallo: la siguiente llamada lo vuelve a intentar.
| Fichero | Contenido | Equivale en Go (`f6f2e9f`) |
|---|---|---|
| `errors.ts` | `DateKeysError` con el código normativo de §69 (`ERR_*`); mensajes con la forma `contexto: CÓDIGO` de Go | `errors.go` |
| `bytes.ts` | Hex, UTF-8 estricto, `goQuote` (el `%q` de Go, con la tabla de `strconv.IsPrint` de Go 1.26 fijada en el código) y `sha256` (Web Crypto) | `strconv`, `unicode/utf8` |
| `cbor.ts` | El codec propio de la referencia, con las mismas lecturas, las mismas comprobaciones en el mismo orden y los mismos textos de error: `Encoder` con error persistente; `Decoder`, cursor estricto (`map`/`key`/`endMap`, `array`, `uint`/`uint64`, `bstr`, `text`, `done`); `unmarshal` (decodifica, reencodifica y compara; `onReject` para borrar secretos); `peek`/`checkSchema` (capa 2 de §69.1: tipo y versión antes que nada) y `walk` (lector genérico acotado en profundidad y longitud) | `codec` |
| `schema.ts` | Lo que comparten los decodificadores de PUBLIC_HEADER, CONTROL_CBOR y el cuerpo de la `.dkk`: `key N: ` en los errores, claves obligatorias y arrays de extensiones | `capsule/framing.go`, `accesskey` |
| `extension.ts` | Arrays de extensiones, leídos con su objeto (capa 3 de §69.1). Registros con ubicación opcional (`registeredIn`): una extensión conocida fuera de los objetos y arrays de su registro cuenta allí como desconocida (§54, §72), como `extension.Placement` en Go. Reglas del array: de 1 a 64, en orden estrictamente ascendente de los bytes UTF-8 de `extension_id` (nunca por unidades UTF-16), `extension_version` hasta 2³² − 1, `data` ausente o `bstr` no vacío, ningún id en los dos arrays; registros, críticas y no críticas (capa 4) | `extension` |
| `profile.ts` | Provider Profile: CBOR exacto, `profile_hash`, reglas 1 a 4 de §12.1 en su orden (el límite de `period` de §74 en la capa del esquema; alfabetos, clave pública del grupo del scheme y fórmula de `chain_hash`), registro pinneado; Quicknet fijado por su CBOR y su hash | `profile` |
| `bls12381.ts` | Pertenencia de claves públicas BLS12-381 comprimidas (G1 y G2) al subgrupo, como `FromCompressed` de kilic | `kyber-bls12381` |
| `ibe.ts` | IBE-CCA de tlock sobre G2 para Quicknet (§63 paso 11): `decryptOnG2`, con la puerta de codificación canónica de `bls12381.ts` sobre la firma y U; H2 sobre GT serializado en el orden de kilic (nunca `Fp12.toBytes` de noble), H3 y H4; `roundIdentity`; el cuerpo `U ‖ V ‖ W` de 128 bytes del stanza. Errores `IbeError` con motivo (`length`, `encoding`, `identity`, `proof`) y texto fijos, sin ningún valor del cálculo; borra sigma y los hashes derivados. Sobre `@noble/curves` 2.4.0; lleva el aviso MIT de `tlock-js`, cuya estructura sigue. Aún no lo reexporta `index.ts`: lo usará la apertura (paso 5 de la fase 2) | `encrypt/ibe` de drand/kyber (`DecryptCCAonG2`), `tlock.BytesToCiphertext` y `TimeUnlock` |
| `release.ts` | Verificación local del release (§17, §51, §63 paso 10), en el orden y con los textos de `provider.Verify`:
1. el rango de la ronda (`ERR_DATEKEY_INVALID`);
2. la ronda del release antes que la firma (`ERR_ROUND_MISMATCH`);
3. la longitud de la firma;
4. la clave pinneada (`ERR_UNKNOWN_PROFILE`);
5. la firma: codificación canónica de un punto de G1 que no sea el infinito, y firma BLS válida de la ronda sobre `@noble/curves` 2.4.0, con el DST de RFC 9380 para G1 (`ERR_RELEASE_INVALID`).
Nada de noble se copia a los errores. Solo verifica el scheme de Quicknet: un perfil de otro scheme falla con `ERR_UNKNOWN_PROFILE` tras las comprobaciones de ronda, donde la referencia sí lo verificaría (decisión 3 del plan de la fase 2). También define `ReleaseSource`, con su contrato de fuentes de red y de la corrección 6, y `suppliedRelease`, el release que entrega quien llama. Aún no lo reexporta `index.ts` | `provider` (`Verify`, `ReleaseSource`) |
| `datekey.ts` | `dk1_` canónico con las reglas de lectura de §19 (CR, LF y todo carácter fuera del alfabeto fallan el paso 1; números JSON por su valor decimal exacto), ronda desde una fecha con precisión de nanosegundos y cota de 9999-12-31T23:59:59Z (§15), parser RFC 3339 equivalente a `time.Parse(time.RFC3339Nano, …)` | `datekey` |
| `header.ts`, `control.ts`, `accesskey.ts` | PUBLIC_HEADER, CONTROL_CBOR y `.dkk` (cuerpo y trama), decodificar y codificar, con las capas de §69.1 | `capsule`, `accesskey` |
| `framing.ts` | Prelude DKC1 (16 bytes) y DKK1 (12 bytes) en el orden de §23 y §40, longitudes de 1 byte hasta los límites de §57, y troceo de secciones | `capsule/framing.go` |
| `age.ts` | Parser estricto de la cabecera `age` v1 (§28.1) sobre los ficheros binarios, con los textos de error de `age`; reglas de stanzas; `MAX_AGE_HEADER_LEN` (2 MiB), el límite que usa la página para leer solo el prefijo de un `.dkc` grande | `agewrap`, `filippo.io/age/internal/format` |
| `inspect.ts` | Pasos 1 a 8 de §63 y la vista JSON de `datekeys inspect -json` (`inspectView`, `inspectJSON`) | `capsule/inspect.go`, `internal/inspectview` |
| `index.ts` | Reexporta todo, salvo `ibe.ts` y `release.ts` por ahora | |
| `testing/` | Solo para tests: lectura de `testdata/` y de sus formatos (`vectors.ts`: ediciones, vectores), constructores de CBOR en hex, cirugía de cápsulas | |
Los tests (`*.test.ts`) están junto a cada fichero.
### Equivalencia con la referencia Go
El comportamiento se contrastó con la librería Go en `3820066` (`692cf87` solo cambia la regla de UTF-8 de `dk1_` descrita abajo) mediante un oráculo diferencial fuera del repositorio: 483 527 entradas de tres semillas. Son mutaciones de los cinco `.dkc` oficiales de todas las clases (bits, bytes, truncados, inserciones, borrados, longitudes y campos del prelude, cabeceras reescritas con cambios de CBOR, DateKeys, arrays de extensiones con y sin registro de extensiones, cabeceras `age` de SEALED_CONTROL y de PAYLOAD_AGE, argumentos del stanza tlock, rondas, secciones cambiadas de sitio y combinaciones de varios defectos); CBOR de cada esquema (PUBLIC_HEADER, CONTROL_CBOR, cuerpo y fichero `.dkk`, Provider Profile, arrays de extensiones); `walk`, `peek` y `checkSchema`; cabeceras `age`, preludes, cadenas `dk1_`, rondas y fechas. En todas coinciden el veredicto, el código y el paso, y también el texto del error, el valor decodificado y la salida entera de `datekeys inspect -json`, byte a byte. Un segundo diferencial con otro generador, de 407 196 entradas, se repitió contra `f6f2e9f` (la enmienda de canonicidad de puntos) con el mismo resultado, textos incluidos.
Como la referencia desde `f6f2e9f`, los errores no copian el texto de una librería: una cabecera `age` que no se puede leer da siempre `agewrap: not an age v1 header: malformed, truncated or beyond the parser limits` (`ERR_INTEGRITY`). El motivo del parser, con las palabras de `age`, queda en `cause` del error, fuera del mensaje, y los tests lo comparan con los textos de `age` para comprobar que se rechaza por la misma razón.
Precedencia de errores (§69.1): primero la trama; después el tipo y la versión de esquema (`checkSchema`); después el perfil CBOR y el CDDL, con los límites de implementación de §74, en una sola decodificación (`unmarshal` con el decodificador del esquema, que ya comprueba tipos, tamaños, rangos, `access_policy`, `extension_version`, el máximo de 64 extensiones y su orden); y solo entonces los campos con código propio, en orden de clave: DateKey, perfil fijado y extensiones críticas en PUBLIC_HEADER; `access_type` y `access_material` en la `.dkk`; los campos y la autocomprobación de `chain_hash` en el Provider Profile. Entre pasos decide el orden de §63. Un PUBLIC_HEADER que rompe a la vez el CDDL y la DateKey da `ERR_NON_CANONICAL_CBOR`, como en la referencia de `3820066`; la de `afb44a3` leía `access_policy` y `extension_version` como `uint64` y los acotaba después de la DateKey, y la vía `wideUint` que lo imitaba ya no existe.
Enteros: el decodificador lee cada entero hasta 2⁶⁴ − 1 (un `bigint` por encima de 2⁵³ − 1) y lo compara con el máximo de su campo, con el texto de la referencia (`unsigned integer 9007199254740992 above 9007199254740991`). Ningún campo de un objeto del protocolo admite más de 2⁵³ − 1; `walk` admite cualquier `uint64`, como `codec.Walk`.
`peek` lee la capa 2 de §69.1 como `codec.Peek`: una cabecera de mapa de longitud definida que anuncia al menos dos entradas y no más de la mitad de los bytes que la siguen, la clave 0 con un texto de hasta 64 bytes y la clave 1 con un entero de hasta 2⁵³ − 1, todo en su forma más corta. No lee nada más: una versión 2 es `ERR_UNSUPPORTED_VERSION` sea lo que sea lo que venga detrás, y cualquier otra cosa en esas posiciones, `ERR_NON_CANONICAL_CBOR`.
UTF-8 inválido en el JSON de un `dk1_` hace fallar el paso 2 de §19 (`ERR_DATEKEY_INVALID`) en las dos implementaciones, también en un miembro que un nombre repetido reemplaza después. Hasta `692cf87` la referencia Go lo sustituía por U+FFFD (`encoding/json`) y en ese caso daba `ERR_DATEKEY_NON_CANONICAL`; el diferencial de esta implementación lo encontró y la referencia se corrigió para seguir al spec. `datekey.test.ts` y el vector *invalid UTF-8 in a member a repeated name overwrites* de `dk1.json` lo fijan.
`goQuote` escribe como Go 1.26 (Unicode 15.0.0) los textos entre comillas de los detalles de fallo: usa una tabla de `strconv.IsPrint` generada con Go y no `\p{…}`, porque cada motor JavaScript trae su propia versión de Unicode (V8 en Node 24 ya imprime runas de Unicode 16 que Go escapa, como U+31E4). `bytes.test.ts` fija el SHA-256 del conjunto completo de runas imprimibles. Si el toolchain de la referencia cambia de versión de Unicode, se regenera con `go run scripts/go-isprint-table.go` y se actualizan la tabla y el hash.
Secretos: `access_material` de un `.dkk` e `I_PAYLOAD` de CONTROL_CBOR se borran en todos los caminos, también cuando la decodificación falla a medias o `unmarshal` rechaza el valor, como el `clear` diferido de Go.
## Página inspector
Sitio SvelteKit estático (`@sveltejs/adapter-static`, `strict`): las dos páginas se prerenderizan a HTML y no hay código de servidor.
| Ruta | Contenido |
|---|---|
| `/` | Qué es el inspector y qué garantiza; enlaza con `/inspect` |
| `/inspect` | El inspector |
`/inspect` carga un `.dkc` con el selector de ficheros, soltándolo en cualquier parte de la página o desde la lista de fixtures oficiales, y ejecuta `inspect` (pasos 1 a 8 de §63) sin registro de extensiones, como `datekeys inspect`. Muestra:
- cada paso con su número, su nombre de la CLI, `superado` o el código normativo, y el detalle (los caracteres invisibles o de control se escriben como `\uXXXX`);
- el veredicto, `capsule_id`, la DateKey compacta y decodificada (red y ronda), el perfil fijado, la fecha de apertura en UTC y en la hora local del navegador, `access_policy`, los campos del prelude, los argumentos del stanza `tlock` frente al perfil fijado y el número y tipo de stanzas de OUTER_TIME_AGE y PAYLOAD_AGE;
- las extensiones de PUBLIC_HEADER según el contrato del plan §8: id (entre comillas y escapado si tiene caracteres no imprimibles), versión, crítica o no, conocida o no, longitud y hex (plegado si pasa de 64 bytes); texto si los bytes son UTF-8 imprimible; vista CBOR con `walk` si son un ítem del perfil de §58, marcada "informativo, no validado por el protocolo"; y el aviso de que son públicas, de que nada las vincula al resto de la cápsula antes del paso 15 y de que ni entonces prueban autoría (§55.1);
- **Copiar JSON**, que copia exactamente la salida de `datekeys inspect -json` (`cliJSON`: el `json.Encoder` de Go con sangría de dos espacios, `<`, `>`, `&`, U+2028 y U+2029 escapados y salto de línea final). `file` es el nombre del fichero.
No pide ni acepta secretos. Todo el texto leído de la cápsula pasa por interpolación de texto de Svelte (nunca `{@html}`), y ni las entradas de mapas CBOR ni los identificadores de extensión se usan nunca como claves de objetos o `Map` de JavaScript.
| Fichero | Contenido |
|---|---|
| `src/lib/inspector/report.ts` | `buildReport`: el modelo de la página a partir de `Inspection`, sin DOM ni reloj |
| `src/lib/inspector/format.ts` | Nombres y glosas de pasos, códigos y políticas; texto imprimible y escapado; números y fechas en español; `cliJSON` |
| `src/lib/inspector/diagnostic.ts` | Notación de diagnóstico CBOR (RFC 8949 §8) de `walk`, acotada a 16 384 caracteres |
| `src/lib/inspector/load.ts` | Lectura por prefijo: de un `.dkc` grande solo se leen 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN + 2 MiB + 1 bytes, y solo 16 si los pasos 1 y 2 rechazan el prelude (otro tipo de fichero, un `.dkk`, longitudes fuera de §57), siempre con el mismo resultado que el fichero entero (lo comprueba `load.test.ts`) |
| `src/lib/inspector/fixtures.ts` | Los fixtures oficiales, empaquetados desde `testdata/fixtures` |
| `src/lib/components/` | `InspectionReport`, `StepList`, `ExtensionList`, `DataView`, `Mark` |
| `src/routes/` | Layout, portada e inspector |
### Fixtures
`fixtures.ts` importa con `import.meta.glob` los `.dkc` de `testdata/fixtures` como URL (`?url`) y, de cada registro JSON, solo el campo `description`. `testdata/` sigue siendo la única fuente: Vite copia cada `.dkc` como fichero con hash en `_app/immutable/assets/` y nunca lo incrusta como `data:` (`assetsInlineLimit: 0`), y no se copia nada más. Los `.dkk`, los textos en claro y los demás campos de los registros (`payload_identity`, `control_cbor`…) no llegan al sitio; `check-build.mjs` lo comprueba. En desarrollo, `server.fs.allow` deja que Vite sirva `testdata/fixtures`.
### Sin red: la Content-Security-Policy
`kit.csp` (`svelte.config.js`, modo `hash`) pone en cada página prerenderizada, como primer elemento que carga algo, un ``:
```
default-src 'self'; frame-src 'none'; worker-src 'none'; connect-src 'self'; font-src 'self';
img-src 'self'; manifest-src 'self'; object-src 'none'; script-src 'self' 'sha256-…';
style-src 'self'; style-src-attr 'unsafe-hashes' 'sha256-…'; base-uri 'none'; form-action 'none'
```
- `connect-src 'self'`: `fetch` solo llega al propio origen, y solo se usa para los fixtures.
- `script-src`: los módulos del sitio y el hash SHA-256 del único script en línea, el arranque de SvelteKit (los nonces no sirven en HTML prerenderizado).
- `style-src 'self'`: solo hojas de estilo del sitio; sin fuentes web ni CDN, con las fuentes del sistema.
- `style-src-attr`: solo el atributo `style` del anunciador de rutas de SvelteKit, por su hash (`ANNOUNCER_STYLE_HASH`, válido para `@sveltejs/kit` 2.70.3; `app.css` lo oculta también si el navegador bloquea el atributo).
`npm run build` ejecuta después `scripts/check-build.mjs` (`postbuild`; también `npm run build:check`), que falla si una ruta no tiene su HTML prerenderizado; si una página no tiene exactamente esa política, con la etiqueta antes de cualquier elemento que cargue recursos; si un script en línea no está en `script-src` o sobra un hash; si `style-src-attr` no coincide con los atributos `style` del bundle; si hay estilos en línea, manejadores de eventos en atributos, `@import` o URL a otro origen; si algún `.dkc` oficial no está byte a byte; si aparece en el sitio algún secreto de los fixtures (`.dkk`, textos en claro, identidades, `payload_identity`, `access_material`, `control_cbor`); o si el bundle del cliente contiene `tlock-js`, `drand-client` o helpers de Babel, o una copia anidada de un paquete que no sea la de noble bajo `@noble/post-quantum`. `vite.config.ts` registra los módulos de cada chunk en `.svelte-kit/output/client-modules.json`, fuera del sitio. Al terminar informa del JavaScript que carga cada página, en bytes y con gzip, y de los paquetes npm que lleva el bundle.
En un hosting estático basta con servir `build/`. Las directivas que solo funcionan como cabecera HTTP (`frame-ancestors`, `sandbox`, `report-to`) quedan para el servidor que la aloje. `crypto.subtle` exige contexto seguro: `https`, o `http` en `localhost`.
## Reglas
- Los fixtures y vectores del Go son la verdad. Este proyecto nunca genera fixtures propios: `testdata/` es una copia exacta de un commit de la librería Go.
- Dependencias de ejecución: solo `age`, `drand`, `tlock` y lo que ellas arrastran; hoy, las de la sección «Dependencias de ejecución». El sitio lleva compilado además el runtime de cliente de Svelte y SvelteKit, el tooling que el plan elige para la página (sección 13).
- Tooling de desarrollo: solo el de la lista siguiente. Cualquier otra dependencia se propone por escrito y no se instala sin aprobación.
## Comandos
```bash
npm test # vitest, todos los tests
npm run coverage # tests con cobertura v8; falla por debajo de los umbrales
npm run typecheck # svelte-kit sync y tsc sobre todo y sobre la librería sin tipos de Node
npm run check # svelte-kit sync y svelte-check (componentes y rutas), falla con avisos
npm run dev # servidor de desarrollo: http://localhost:5173/inspect
npm run build # sitio estático en build/ y, después, scripts/check-build.mjs
npm run preview # sirve build/: http://localhost:4173/inspect
npm run build:check # solo la comprobación del sitio ya construido
npm run verify # check, typecheck, coverage y build (con su comprobación)
```
Umbrales de cobertura (`vitest.config.ts`): `cbor.ts`, `ibe.ts` y `release.ts` al 100 % en líneas, ramas, funciones y sentencias; el conjunto de `src/lib/dkc` al 95/90/95/95, y el de `src/lib/inspector` también.
`vitest.config.ts` es la configuración de los tests; `vite.config.ts`, la del sitio con el plugin de SvelteKit. Vitest prefiere la primera, así que los tests de `src/lib` corren sin SvelteKit, y `src/lib/inspector` importa la librería por rutas relativas, sin el alias `$lib`. `tsconfig.json` extiende el que genera `svelte-kit sync` (por eso `typecheck` y `check` lo ejecutan antes, y `npm install` también, con `prepare`).
`npm run typecheck` pasa dos veces: `tsconfig.json` (todo, con tipos de Node para los tests) y `tsconfig.lib.json` (solo la librería y sin tipos de Node, para que no se cuele ninguna API que no exista en el navegador).
### Vectores y fixtures
- `testdata/fixtures/*.json`: cada `.dkc` pasa los pasos 1 a 8 con los valores registrados (prelude, PUBLIC_HEADER, DateKey, `capsule_id`, política, `unlock_at`, extensiones con sus bytes exactos, stanzas, `header_binding`, CONTROL_CBOR); cada `.dkk` se decodifica campo a campo y se reencodifica byte a byte. Los fixtures nuevos se recogen solos.
- `testdata/fixtures/*.inspect.json`: la vista de `inspect` de cada `.dkc`, escrita con `inspectJSON` como la imprime la CLI (`file` incluido), es idéntica byte a byte al fichero, también el texto de cada paso.
- `testdata/vectors/dk1.json` (con los tres vectores de los refinamientos de §19: LF dentro del Base64, CR y LF después, y la versión `1.0000000000000001`), `quicknet_rounds.json` y `profile_quicknet.json`: se ejecutan todos.
- `testdata/vectors/cbor.json`: cada vector genérico (`accept` y `reject`) pasa por `walk` con los `max_depth` y `max_len` del fichero; los enteros aceptados comparan su `value` (número o, por encima de 2⁵³ − 1, `bigint`), y los rechazados «above max_len» o «above max_depth» se aceptan sin ese límite. Cada vector de `schemas` pasa por el decodificador de su esquema (`decodeProfile`, `decodeHeader`, `decodeControl`, `decodeAccessKeyBody`) con el código exacto, y un objeto aceptado se reescribe a los mismos bytes.
- `testdata/vectors/tlock_ibe.json`: el vector de H2 del IBE de tlock (§63 paso 11). Hasta que llegue `ibe.ts` (fase 2), el test lo recalcula con `@noble/curves` 2.4.0: los puntos son canónicos para `bls12381.ts`, el pairing serializado en el orden de kilic es el GT del vector y su H2 coincide; el orden propio de noble (`Fp12.toBytes`) da otro hash.
- `testdata/vectors/mutations.json`: se leen los 65 casos enteros (ediciones sobre un fixture o hex congelado, release, reloj, registro, extensiones, `.dkk` e identidades). Los 31 de los pasos 1 a 8 pasan por `inspect` con su registro y sus extensiones, y dan el mismo código y el mismo paso; los 34 de los pasos 9 a 18 (entre ellos las 10 mutaciones de la enmienda de canonicidad de puntos, en los pasos 10 y 11) necesitan `open` (fase 2) y se saltan uno a uno con ese motivo, y un test fija los dos recuentos. Las `.dkk` ofrecidas se decodifican.
- `testdata/vectors/inspect_differential.json`: las 1 825 mutaciones dan el mismo veredicto, código y paso que Go; los `bases` se comprueban por su SHA-256.
- `src/lib/dkc/testing/ibe-vectors.json`: los valores de referencia de `ibe.ts`. Los escribe `scripts/ibe-go-vectors.go` con kyber, tlock y `age`, las librerías de la referencia Go, y `ibe.test.ts` los comprueba todos:
- el GT de e(G1, G2) y de su cuadrado, con H2 de 16 y 32 bytes;
- H3 y H4 sobre entradas fijas, entre ellas una H3 aceptada en la segunda iteración y otra en la tercera;
- la identidad de varias rondas;
- para el stanza tlock de cada fixture oficial, el pairing, sigma, r y la file key. La file key es la que devuelve `tlock.TimeUnlock`, y con ella `age` abre el `OUTER_TIME_AGE`. El test lo repite con `age-encryption`: el MAC de la cabecera y STREAM verifican, y el contenido es el `control_cbor` del registro o un `INNER_ACCESS_AGE`;
- mensajes de 0, 1, 16 y 32 bytes cifrados por `EncryptCCAonG2` de kyber;
- el veredicto de `DecryptCCAonG2` sobre copias editadas del stanza de `time_only`: U con c0 + p, en el infinito o negado, V o W alterados, la firma de otra ronda, negada o en el infinito, y longitudes erróneas.
H2, H3 y H4 no son públicas en kyber: el script las reescribe con sus etiquetas y las comprueba en cada fixture contra la file key de tlock y contra U = r·G2. Los cifrados de kyber usan un sigma aleatorio, así que el fichero se genera una vez y se congela. Para regenerarlo, desde un módulo Go temporal que requiera la referencia (`replace g.activething.com/go/DateKeys => ../datekeys-go`, `GOFLAGS=-mod=mod`): `go run ibe-go-vectors.go ../App/testdata/fixtures > ibe-vectors.json`.
- Todo se lee con los formatos de `testdata/README.md` (`testing/vectors.ts`): una clave desconocida o que falta, un valor de otro tipo, un código que no es de §69 o una edición fuera de su base hacen fallar el fichero con su motivo; nada se salta en silencio.
- Todo fichero de `testdata/` tiene que ejecutarlo algún test: un nombre nuevo exportado por Go (otro `vectors/*.json`, un fichero de fixture que ningún JSON nombra) hace fallar `testdata/ holds no file that no test runs` hasta que se le añade su bloque.
## Dependencias de ejecución
Aprobadas en el plan de la fase 2 (sección 3 y decisión 5) e instaladas con su versión exacta. `ibe.ts` importa noble desde el paso 3, pero la página todavía no lo usa, así que el sitio no cambia hasta que llegue la apertura.
| Paquete | Versión | Licencia | Uso |
|---|---|---|---|
| `age-encryption` | 0.3.1 | BSD-3-Clause | las tres envolturas `age` (§28), con `Identity` y `Recipient` propios para el stanza `tlock` |
| `@noble/curves` | 2.4.0 | MIT | BLS12-381 del núcleo IBE y de la verificación de releases; también el oráculo de `bls12381.contrast.test.ts` |
| `@noble/hashes` | 2.4.0 | MIT | los hashes del IBE; se declara porque se importa directamente |
`age-encryption` arrastra `@noble/ciphers` 2.4.0, `@scure/base` 2.4.0 y `@noble/post-quantum` 0.5.4, todos con licencia MIT. `@noble/post-quantum` fija `@noble/curves` y `@noble/hashes` a `~2.0.0` y trae su propia copia 2.0.1, que usa para el ML-KEM híbrido. La decisión 5 la acepta, sin overrides de npm.
Guardas de `src/lib/dependencies.test.ts`, en cada `npm test`:
- `package.json` declara exactamente estas tres dependencias, con versión exacta;
- `package-lock.json` no contiene `tlock-js` ni `drand-client`, ningún noble 1.x, ni más copias 2.x de `@noble/curves` o `@noble/hashes` que la 2.4.0 de la raíz y la 2.0.1 bajo `@noble/post-quantum`;
- ningún fichero de `src/` importa `tlock-js` ni `drand-client`;
- solo `ibe.ts`, `release.ts` y los tests nombran `@noble/`, siempre con subrutas de `@noble/curves` y `@noble/hashes` que resuelven a la copia 2.4.0 de la raíz;
- cada comprobación se ejecuta también sobre entradas malas, así que una guarda que dejara de detectar algo fallaría.
`check-build.mjs` hace la misma comprobación sobre el bundle del cliente.
Coste medido en el bundle el 28-09-2026, con una compilación de prueba de Vite 8 minificada (gzip de nivel 9):
| Qué se importa | Minificado | gzip |
|---|---|---|
| `Decrypter` de `age-encryption` | 153 645 B | 48 032 B |
| `Decrypter` y `Encrypter` | 183 650 B | 55 915 B |
| `bls12_381` y `sha256` de noble | 92 637 B | 28 090 B |
| Todo lo anterior | 239 454 B | 72 783 B |
`age-encryption` importa de forma estática sus recipients ML-KEM híbridos, P-256 y scrypt. Por eso el `Decrypter` arrastra `@noble/post-quantum` y la copia 2.0.1 de noble, aunque DateKeys no los use: son unos 99 KB de los 212 KB de código antes de minificar. Como referencia, `/inspect` carga hoy unos 157 KB de JavaScript (58,7 KB con gzip). La cifra exacta cambia unos bytes en cada compilación, por la versión que SvelteKit incrusta.
`npm audit --omit=dev` no encuentra vulnerabilidades. El `npm audit` completo encuentra 2 de gravedad baja en el tooling: `cookie` < 0.7.0 (GHSA-pxg6-pf52-xh8x), que llega a través de `@sveltejs/kit` 2.70.3. Esa es la última versión y sigue pidiendo `cookie` ^0.6.0. Solo afecta a la gestión de cookies del servidor de SvelteKit, que un sitio estático no usa.
## Tooling de desarrollo
| Paquete | Versión | Estado |
|---|---|---|
| `typescript` | 5.9.3 | en `package.json`; ya usado en el prototipo |
| `vitest` | 5.0.1 | en `package.json`; ya usado en el prototipo |
| `@vitest/coverage-v8` | 5.0.1 | en `package.json`; cobertura del 100 % del codec |
| `@types/node` | 24.13.6 | en `package.json`; tests que leen `testdata/` desde disco |
| `vite` | 8.3.0 | en `package.json`; dependencia peer obligatoria de `vitest` 5.0.1 y base del paso 5; la versión del prototipo |
| `svelte` | 5.57.1 | en `package.json`; paso 5 |
| `@sveltejs/kit` | 2.70.3 | en `package.json`; paso 5 |
| `@sveltejs/vite-plugin-svelte` | 7.3.0 | en `package.json`; paso 5 |
| `svelte-check` | 4.7.6 | en `package.json`; paso 5, `npm run check` |
| `@sveltejs/adapter-static` | 3.0.10 | en `package.json`; paso 5: la página es estática y no necesita servidor |
`@noble/curves` 2.4.0 estaba en esta lista como oráculo de `bls12381.contrast.test.ts` y ha pasado a dependencia de ejecución en la fase 2.
Todas las versiones se fijan exactas y `package-lock.json` se versiona. `.npmrc` activa `legacy-peer-deps` porque npm 11.5.2 falla al resolver los peers opcionales de `vitest` 5.0.1 (`Cannot read properties of null (reading 'edgesOut')`); con esa opción npm no instala peers, así que el peer obligatorio `vite` está declarado explícitamente.
## testdata
```bash
npm run testdata:sync
```
Copia `testdata/` de `../datekeys-go` en `HEAD`, leyendo los blobs con git para no arrastrar cambios sin commit, y escribe `testdata/SOURCE.json` con el commit completo y el SHA-256 de cada fichero. Para fijar otro commit: `node scripts/sync-testdata.mjs sync --commit `.
```bash
npm run testdata:check
```
Comprueba que los ficheros coinciden con `SOURCE.json`, sin faltantes ni sobrantes, y vuelve a leerlos del repositorio Go en el commit registrado. Sin la opción `--against`, `node scripts/sync-testdata.mjs check` verifica solo la copia local. Ambos comandos usan solo Node y git.
`.gitattributes` marca `testdata/**` como binario para que git no altere ningún byte.
Copia actual: la de `testdata/SOURCE.json` (rama `v0.8.2`).
## Licencia
Apache-2.0 ([LICENSE](LICENSE)), como la librería Go de referencia. El código que se derive de terceros conserva su aviso de copyright y licencia en el propio fichero; el primero previsto es el núcleo IBE de la fase 2, derivado de `tlock-js` (Apache-2.0 OR MIT).