open(dkc, opts) now takes a Uint8Array or a Blob, such as a File.
- Of a Blob it reads only the prefix that steps 1 to 8 need.
inspectedLength and readCapsule move from src/lib/inspector/load.ts to
src/lib/dkc/prefix.ts, and the page imports them from the library.
- The .dkk capsule_digest is computed over the Blob's stream with the
new src/lib/dkc/digest.ts, an incremental SHA-256 on @noble/hashes,
since Web Crypto hashes whole buffers only. digest.ts joins the noble
allowlist of the guards.
- PAYLOAD_AGE is decrypted in streaming.
The plaintext goes to memory, as before, or to opts.output, a
WritableStream. The output is written as age authenticates each chunk,
closed only after step 18, and aborted after any failure at any step,
even before step 17 (spec §56). A failure of the output is ERR_INTEGRITY
with its text, as Go keeps the error of the writer of the plaintext.
Tests:
- the fixtures from Blobs, to memory and to an output;
- a truncated two-chunk payload whose first chunk reached the output
before the abort;
- early failures that never write;
- write and close failures, and an abort that fails;
- a .dkk without capsule_digest;
- the whole mutation corpus again as Blobs into an output, aborted in
every case;
- digest.ts against Web Crypto.
Coverage of digest.ts is 100 % and a threshold.
Checked in the browser (dev server, real OPFS). time_only.dkc, two
STREAM chunks, opened from a Blob into FileSystemFileHandle.createWritable
gives 78,000 bytes with the SHA-256 of its sidecar. The same file
truncated fails at step 17, and the OPFS file keeps its previous
content. The quota check, the temporary file and the download belong to
the page, in step 8.
npm run verify is green: 2,560 tests. The site does not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ -18,11 +18,11 @@ Implementa la especificación DateKeys 0.8.2 (tag `spec-v0.8.2` de `datekeys-go`
- dependencias de ejecución (`age-encryption` 0.3.1, `@noble/curves` y `@noble/hashes` 2.4.0) con sus guardas;
- el IBE de tlock (`ibe.ts`) y la verificación local de releases (`release.ts`), contrastados con la referencia Go;
- la apertura, pasos 9 a 18 de §63 (`open.ts`), con el texto en claro en memoria. Las identidades estrictas de `agewrap` se apoyan en `x25519.ts`, que abre cada stanza X25519 por separado, y en `bech32.ts`. Los 65 casos del corpus de mutaciones pasan por `open` con el código y el paso de Go, y los cinco fixtures oficiales se abren a su texto en claro;
- `@noble/ciphers` 2.4.0 como dependencia directa, aprobada el 28-09-2026: la copia que ya trae `age-encryption`.
- `@noble/ciphers` 2.4.0 como dependencia directa, aprobada el 28-09-2026: la copia que ya trae `age-encryption`;
- la apertura en streaming. La entrada puede ser un `Blob`, del que se lee solo el prefijo de los pasos 1 a 8 (`prefix.ts`, antes en la página) y se descifra `PAYLOAD_AGE` en streaming. La salida puede ser un `WritableStream`, que se cierra solo tras el paso 18 y se aborta ante cualquier fallo. En el navegador, con un fichero OPFS, un fallo de STREAM deja intacto su contenido anterior.
- `VERSION` y `SPEC_VERSION`, también en el pie de la página.
### Pendiente para 0.1.0
- El descifrado de `PAYLOAD_AGE` en streaming a OPFS (fase 2, paso 5b).
- El cifrado a nivel de stanza y de fichero `age`, contrastado con Go (paso 7).
- La acción "abrir" en `/inspect`, cargada bajo demanda (paso 8).
- La acción "abrir" en `/inspect`, cargada bajo demanda, con el fichero temporal de OPFS, la cuota libre y la descarga (paso 8).
@ -27,7 +27,7 @@ Hay tres números de versión, cada uno con su significado, como en la referenci
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 y la apertura de los pasos 9 a 18 (`open.ts`), con el texto en claro en memoria. El descifrado en streaming a OPFS completa la fase 2, y el cifrado llega en la fase 3;
- la inspección de los pasos 1 a 8 y la apertura de los pasos 9 a 18 (`open.ts`), desde un `Uint8Array` o un `Blob` y hacia memoria o hacia un stream de salida, como el de un fichero OPFS. El cifrado llega 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.
@ -50,7 +50,8 @@ Lo que hay hoy (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegad
| `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. Lo usa la apertura (`open.ts`) | `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`:<br>1. el rango de la ronda (`ERR_DATEKEY_INVALID`);<br>2. la ronda del release antes que la firma (`ERR_ROUND_MISMATCH`);<br>3. la longitud de la firma;<br>4. la clave pinneada (`ERR_UNKNOWN_PROFILE`);<br>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`).<br>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 | `provider` (`Verify`, `ReleaseSource`) |
| `open.ts` | Los pasos 9 a 18 de §63 sobre los pasos 1 a 8 de `inspectWith`, con los checks, códigos y textos de `capsule.Open`:<br>- las credenciales y el release (paso 9), que cualquier fallo de la fuente convierte en `ERR_RELEASE_UNAVAILABLE` (corrección 6);<br>- la verificación del release (10);<br>- `OUTER_TIME_AGE` (11), la estructura frente a `access_policy` (12) e `INNER_ACCESS_AGE` (13);<br>- `CONTROL_CBOR` (14), `header_binding` (15), `I_PAYLOAD` (16), `PAYLOAD_AGE` (17) y el commit (18).<br>Abre los tres ficheros `age` con el `Decrypter` de `age-encryption` y con identidades propias que aplican las reglas de `agewrap`: la de tiempo, sobre `ibe.ts`; las de acceso y payload, sobre `x25519.ts`, stanza a stanza. Los fallos de `age` que no informa una identidad son `ERR_INTEGRITY` con el motivo fijo de su fase, cabecera o STREAM, sin copiar el texto de `age-encryption`. Por ahora el texto en claro se descifra en memoria y solo se entrega tras el paso 18 | `capsule.Open`, `agewrap` (`TimeIdentity`, `AccessIdentity`, `PayloadIdentity`) |
| `open.ts` | Los pasos 9 a 18 de §63 sobre los pasos 1 a 8 de `inspectWith`, con los checks, códigos y textos de `capsule.Open`:<br>- las credenciales y el release (paso 9), que cualquier fallo de la fuente convierte en `ERR_RELEASE_UNAVAILABLE` (corrección 6);<br>- la verificación del release (10);<br>- `OUTER_TIME_AGE` (11), la estructura frente a `access_policy` (12) e `INNER_ACCESS_AGE` (13);<br>- `CONTROL_CBOR` (14), `header_binding` (15), `I_PAYLOAD` (16), `PAYLOAD_AGE` (17) y el commit (18).<br>Abre los tres ficheros `age` con el `Decrypter` de `age-encryption` y con identidades propias que aplican las reglas de `agewrap`: la de tiempo, sobre `ibe.ts`; las de acceso y payload, sobre `x25519.ts`, stanza a stanza. Los fallos de `age` que no informa una identidad son `ERR_INTEGRITY` con el motivo fijo de su fase, cabecera o STREAM, sin copiar el texto de `age-encryption`.<br>La entrada puede ser un `Uint8Array` o un `Blob`, como un `File`. De un `Blob` solo se lee el prefijo de los pasos 1 a 8 (`prefix.ts`), el `capsule_digest` de la `.dkk` se calcula sobre su stream (`digest.ts`) y `PAYLOAD_AGE` se descifra en streaming.<br>El texto en claro va a memoria o a `output`, un `WritableStream`. Se escribe a medida que `age` autentica cada chunk, se cierra solo tras el paso 18 y se aborta ante cualquier fallo, en cualquier paso (§56). Un fallo del stream de salida es `ERR_INTEGRITY` con su texto, como en Go. El `WritableStream` de un fichero OPFS guarda lo escrito en un fichero de intercambio hasta el cierre: comprobado en el navegador, un fallo de STREAM deja intacto el contenido anterior | `capsule.Open`, `agewrap` (`TimeIdentity`, `AccessIdentity`, `PayloadIdentity`) |
| `digest.ts` | SHA-256 incremental de un stream, con `@noble/hashes`, para el `capsule_digest` de un `.dkc` que no está en memoria (Web Crypto solo calcula el hash de buffers enteros) | |
| `x25519.ts` | El stanza X25519 de `age`, abierto de uno en uno como `X25519Identity.Unwrap` de `age`: argumentos, share, acuerdo de claves, longitud del cuerpo y autenticación, en ese orden, con las primitivas que usa `age-encryption` (X25519 de `@noble/curves`, HKDF-SHA-256 de `@noble/hashes` y ChaCha20-Poly1305 de `@noble/ciphers`). También lee identidades `AGE-SECRET-KEY-1…` | `filippo.io/age` (`X25519Identity`), `agewrap` |
| `bech32.ts` | Bech32 (BIP 173) tal como `internal/bech32` de `age`, que la referencia copia como `codec/bech32`; conserva su aviso MIT | `codec/bech32` |
| `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` |
@ -58,7 +59,7 @@ Lo que hay hoy (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegad
| `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 la fase 2 (`ibe.ts`, `release.ts`, `open.ts`, `x25519.ts` y `bech32.ts`). La página importa `index.ts`, y reexportarlos metería noble en `/inspect` (de 58,7 a 84,9 KB con gzip) aunque no los use, porque noble ejecuta código al cargarse. El paso 8 cargará la apertura bajo demanda | |
| `index.ts` | Reexporta todo salvo la fase 2 (`ibe.ts`, `release.ts`, `open.ts`, `x25519.ts`, `bech32.ts` y `digest.ts`). La página importa `index.ts`, y reexportarlos metería noble en `/inspect` (de 58,7 a 84,9 KB con gzip) aunque no los use, porque noble ejecuta código al cargarse. El paso 8 cargará la apertura bajo demanda | |
| `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.
@ -104,7 +105,7 @@ No pide ni acepta secretos. Todo el texto leído de la cápsula pasa por interpo
| `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/dkc/prefix.ts` | Lectura por prefijo, que también usa la apertura: 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 `prefix.test.ts`) |
| `src/lib/inspector/fixtures.ts` | Los fixtures oficiales, empaquetados desde `testdata/fixtures` |
@ -152,7 +153,7 @@ 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`, `release.ts`, `x25519.ts` y `bech32.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.
Umbrales de cobertura (`vitest.config.ts`): `cbor.ts`, `ibe.ts`, `release.ts`, `x25519.ts`, `bech32.ts` y `digest.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`).
- `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 65 pasan por `open` con su release, su reloj, su registro, sus extensiones, su `.dkk` y sus identidades, y dan el mismo código en el mismo paso que Go. Entre ellos están los 34 de los pasos 9 a 18, con las 10 mutaciones de la enmienda de canonicidad de puntos en los pasos 10 y 11. Ninguno de los que fallan sin red pide un release. Los 31 de los pasos 1 a 8 pasan además por `inspect`, y un test fija los dos recuentos. Las `.dkk` ofrecidas se decodifican.
- `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 65 pasan por `open` con su release, su reloj, su registro, sus extensiones, su `.dkk` y sus identidades, y dan el mismo código en el mismo paso que Go. También pasan como `Blob` con un stream de salida, que termina abortado en los 65. Entre ellos están los 34 de los pasos 9 a 18, con las 10 mutaciones de la enmienda de canonicidad de puntos en los pasos 10 y 11. Ninguno de los que fallan sin red pide un release. Los 31 de los pasos 1 a 8 pasan además por `inspect`, 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;
@ -196,7 +197,7 @@ Guardas de `src/lib/dependencies.test.ts`, en cada `npm test`:
- `package.json` declara exactamente estas cuatro 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`, `x25519.ts` y los tests nombran `@noble/`, siempre con subrutas de `@noble/curves`, `@noble/hashes` y `@noble/ciphers` que resuelven a la copia 2.4.0 de la raíz;
- solo `digest.ts`, `ibe.ts`, `release.ts`, `x25519.ts` y los tests nombran `@noble/`, siempre con subrutas de `@noble/curves`, `@noble/hashes` y `@noble/ciphers` 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.
@ -161,7 +161,7 @@ La ruta `/inspect` gana una acción "abrir": con un fixture o un `.dkc` arrastra
| 2 | Dependencias y guardas | instaladas con versiones exactas; `npm audit --omit=dev` sin avisos; guardas en verde; en el lockfile, un solo noble 2.4.0 en la raíz, la copia 2.0.1 solo bajo `@noble/post-quantum` y ningún 1.x. Hecho el 28-09-2026: el README de `App` recoge las guardas, la medida del bundle y el resultado de `npm audit` |
| 3 | `ibe.ts` de descifrado desde la semilla, `roundIdentity`, escritura del stanza en `age.ts`, `ibe.test.ts` sin la parte de cifrado | los cinco fixtures dan la file key correcta; U no canónico e identidad rechazados; cobertura 100 %. Hecho el 28-09-2026: vectores de `scripts/ibe-go-vectors.go` en `src/lib/dkc/testing/ibe-vectors.json`; `ibe.ts` al 100 %, fijado como umbral. `ibe.ts` también pasa el cuerpo `U ‖ V ‖ W` a bytes; los argumentos del stanza y su paso al `Stanza` de `age-encryption`, que guarda el tipo en `args[0]`, van al paso 7, con el `Recipient` que los usa |
| 4 | `release.ts` y `release.test.ts` | ronda real válida, alias rechazados. Hecho el 28-09-2026:<br>- `verifyRelease` con el orden y los textos de `provider.Verify`;<br>- `ReleaseSource`, con el contrato de la sección 5, y `suppliedRelease`;<br>- los casos de `TestVerifyRejects` de Go y los 7 del corpus de mutaciones que fallan en el paso 10;<br>- las firmas publicadas de las rondas 1000, 1001, 2000 y 1004, esta última obtenida restando p a la codificación x + p del corpus;<br>- cobertura del 100 %, fijada como umbral |
| 5 | `open.ts` con la `Identity` propia, pasos 9 a 18, `open.test.ts` | los cinco fixtures se abren y el plaintext coincide con el sidecar; el corpus de mutaciones existente reproduce código y paso.<br>5a hecho el 28-09-2026, con el texto en claro en memoria:<br>- los 65 casos del corpus pasan por `open` con el código y el paso de Go;<br>- los cinco fixtures se abren con cada credencial;<br>- los textos siguen a `capsule.Open` y `agewrap`.<br>El paso 13 exige probar cada identity contra cada stanza X25519, y `age-encryption` no expone su `X25519Identity`. Por eso `x25519.ts` abre los stanzas de uno en uno, con ChaCha20-Poly1305 de `@noble/ciphers` 2.4.0, dependencia aprobada el 28-09-2026 porque es la copia que ya usa `age-encryption`. `bech32.ts` lee las identidades `AGE-SECRET-KEY-1…`.<br>`index.ts` aún no reexporta la apertura, que metería noble en `/inspect`: el paso 8 la cargará bajo demanda.<br>5b, pendiente: `PAYLOAD_AGE` en streaming a OPFS |
| 5 | `open.ts` con la `Identity` propia, pasos 9 a 18, `open.test.ts` | los cinco fixtures se abren y el plaintext coincide con el sidecar; el corpus de mutaciones existente reproduce código y paso.<br>5a hecho el 28-09-2026, con el texto en claro en memoria:<br>- los 65 casos del corpus pasan por `open` con el código y el paso de Go;<br>- los cinco fixtures se abren con cada credencial;<br>- los textos siguen a `capsule.Open` y `agewrap`.<br>El paso 13 exige probar cada identity contra cada stanza X25519, y `age-encryption` no expone su `X25519Identity`. Por eso `x25519.ts` abre los stanzas de uno en uno, con ChaCha20-Poly1305 de `@noble/ciphers` 2.4.0, dependencia aprobada el 28-09-2026 porque es la copia que ya usa `age-encryption`. `bech32.ts` lee las identidades `AGE-SECRET-KEY-1…`.<br>`index.ts` aún no reexporta la apertura, que metería noble en `/inspect`: el paso 8 la cargará bajo demanda.<br>5b hecho el 28-09-2026: `open` acepta un `Blob`, del que lee solo el prefijo de los pasos 1 a 8 (`prefix.ts`, movido de la página a la librería), calcula el `capsule_digest` en streaming (`digest.ts`) y descifra `PAYLOAD_AGE` en streaming. La salida puede ser un `WritableStream`, que se cierra tras el paso 18 y se aborta ante cualquier fallo. Los 65 casos del corpus pasan también así.<br>Comprobado en el navegador con un fichero OPFS (`createWritable`): `time_only` se abre con el SHA-256 del sidecar, y un fallo de STREAM deja intacto el contenido anterior del fichero.<br>La consulta de cuota (`navigator.storage.estimate()`), el fichero temporal y la descarga van con la página, en el paso 8 |
| 6 | Spec, mutaciones en Go, `testdata:sync`, reproducción en TypeScript (sección 7) | texto aprobado; vectores congelados en ambos repositorios; commits en Gitea |
| 7 | `encryptOnG2RFC9380`, `Recipient` propio, ida y vuelta, interoperabilidad TS → Go a nivel IBE y de fichero `age` (sección 8, puntos 4 y 5) | Go abre lo que TypeScript cifra; vectores congelados |
| 8 | Página (sección 9) | un fixture `time_and_key` se abre en el navegador sin red; `check-build` en verde; tamaño del bundle anotado en el README |
it('only ibe.ts, release.ts, x25519.ts and the tests import noble, only from @noble/curves, @noble/hashes and @noble/ciphers, and nothing imports tlock-js or drand-client',()=>{
it('only digest.ts, ibe.ts, release.ts, x25519.ts and the tests import noble, only from @noble/curves, @noble/hashes and @noble/ciphers, and nothing imports tlock-js or drand-client',()=>{