You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Go to file
dev 3abd7bfee8
Release 0.3.0
24 hours ago
.claude Inspector page: static SvelteKit site with /inspect (plan step 5) 2 weeks ago
scripts Specification 0.14, approved: SPEC_VERSION 0.14, testdata at spec-v0.14, one drand scheme and tlock_steps.json 1 day ago
src Release 0.3.0 24 hours ago
static Inspector page: static SvelteKit site with /inspect (plan step 5) 2 weeks ago
testdata Specification 0.14, approved: SPEC_VERSION 0.14, testdata at spec-v0.14, one drand scheme and tlock_steps.json 1 day ago
.gitattributes Start the DateKeys TypeScript project 2 weeks ago
.gitignore Start the DateKeys TypeScript project 2 weeks ago
.npmrc Pin the development tooling 2 weeks ago
CHANGELOG.md Release 0.3.0 24 hours ago
LICENSE Record the phase 2 decisions and license App under Apache-2.0 2 weeks ago
README.md Release 0.3.0 24 hours ago
package-lock.json Release 0.3.0 24 hours ago
package.json Release 0.3.0 24 hours ago
svelte.config.js /inspect asks drand for the release when the person asks 7 days ago
tsconfig.json Inspector page: static SvelteKit site with /inspect (plan step 5) 2 weeks ago
tsconfig.lib.json TypeScript implementation of DateKeys v0.8.2, steps 1 to 8 2 weeks ago
vite.config.ts The public note on /inspect, plain errors, and no reload on the first opening 1 day ago
vitest.config.ts Author keys of alg 1 and Ed25519 signing in own code, as Go's authorkey 1 day ago

README.md

datekeys-ts

Implementación en TypeScript del protocolo DateKeys (formato 3 de la v0.10; de la v0.11, la firma de clave propia, la firma con certificados y el sello de tiempo, que lee y verifica con el perfil del certificado y los textos del borrador v0.12, y el escritor con el área de 32 KiB, la nota pública, las claves de autor y la firma y el sello al escribir, como los escribe Go; de la v0.12, el localizador de datekeys.capsule) y página de prueba en el navegador. Sustituye al prototipo, archivado en ../archive/prototype (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. Los planes y el estado del trabajo están en ../docs, el repositorio privado de documentación del proyecto.

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
Apertura de cápsulas en el navegador, y la acción "abrir" de la página fase 2 (PLAN_fase2_ibe_noble2.md, en ../docs) 6 hecho (pasos 2 a 8 de la fase 2)
Escritura de cápsulas en TypeScript fase 3 (PLAN_fase3_escritura.md, en ../docs) 7 hecho: la librería, su interoperabilidad con Go y la página /create (pasos 0 a 7 del plan)
Formato 3, especificación 0.10 (entrega 1) PLAN_formato3_ts.md, en ../docs, rama v0.10 — hecho: la lectura y la escritura del formato 3, las reglas de las rutas, el ZIP de la página, /inspect y /create con ficheros y carpetas (pasos 0 a 8 del plan)

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: el formato de la cápsula, el VERSION del prelude de DKC1, 1, 2 o 3 al leer, que fija también la versión de schema de CONTROL_CBOR; y 1 en la trama DKK1 y en el schema de los demás objetos Cambia el formato. Un lector rechaza una versión que no conoce (§22, §70)
Especificación SPEC_VERSION de src/lib/dkc/version.ts, hoy 0.14: la del tag spec-v0.14 de datekeys-go, que aprobó el autor el 6 de octubre de 2026. testdata está en ese tag (39b2033), y todos sus ficheros dicen 0.14 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.3.0, del 6 de octubre de 2026, con el tag v0.3.0. Cubre:

  • la especificación 0.14 (el tag spec-v0.14 de datekeys-go): lee los formatos de cápsula 1 a 3 y escribe el 3, y el 2 solo como generador de vectores (§62.1, regla 1);
  • la firma de autor de alg 1, con las claves dkauthor1…, y la de alg 2 con certificados, y el sello RFC 3161: las evalúa al abrir con los veredictos de Go y las escribe con los enganches del escritor;
  • la llave de palabras, la nota pública y el localizador de datekeys.capsule, con su sellado, su sobre y la comprobación de la IP a la que resuelve una dirección, NAT64 incluido;
  • solo el scheme de Quicknet (bls-unchained-g1-rfc9380), el único que admite §12.1 desde la v0.14: un perfil de otro scheme da ERR_UNKNOWN_PROFILE;
  • la inspección de los pasos 1 a 8 y la apertura de los pasos 9 a 18, desde un Uint8Array o un Blob y hacia memoria o hacia un stream de salida, y las páginas /inspect y /create;
  • todos los vectores y fixtures compartidos de datekeys-go en 39b2033 (spec-v0.14), tlock_steps.json incluido;
  • navegadores con Web Crypto y Node 20 o posterior.

La 0.2.0, del mismo día, con el tag v0.2.0, cubría lo mismo para la especificación 0.13. La anterior es 0.1.0, del 29 de septiembre de 2026, con el tag v0.1.0: la especificación 0.9, leer los formatos 1 y 2, y la página /inspect.

CHANGELOG.md recoge los cambios de cada versión.

src/lib/dkc

La inspección (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 apertura (fase 2) y la escritura usan las dependencias de ejecución de su sección: noble solo lo importan ageio.ts, author.ts, authorkey.ts, cms.ts, digest.ts, ed25519sign.ts, ed25519strict.ts, ibe.ts, release.ts y x25519.ts, y age-encryption solo agefile.ts, authorkey.ts, open.ts, tlock.ts y writer.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 (spec-v0.11)
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). checkWrite es la regla de los codificadores del §72 para las extensiones de la especificación (NOTE_ID, CAPSULE_ID): datekeys.note solo en el array no crítico de la cabecera y datekeys.capsule solo en el de una .dkk, con datos válidos; la aplican el escritor de cápsulas y el de .dkk extension (CheckWrite y el registro Standard)
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, el único scheme que admite la v0.14, bls-unchained-g1-rfc9380, con los textos de validateDrand de Go, clave pública de G2 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 y encryptOnG2RFC9380 (Qid = H(id) en G1 con el DST de RFC 9380, sigma aleatorio, U = r·G2), 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 (h3Base y h3Try, que desplaza el primer byte de cada intento un bit a la derecha, como kyber) y H4; roundIdentity y hashToG1, el hash a G1 de RFC 9380 que usan también release.ts y el cifrado; 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:
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, el único que admite un perfil desde la v0.14: un Profile de otro scheme, que validateProfile rechaza, falla aquí con ERR_UNKNOWN_PROFILE tras las comprobaciones de ronda (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:
- las credenciales y el release (paso 9), que cualquier fallo de la fuente convierte en ERR_RELEASE_UNAVAILABLE (corrección 6);
- la verificación del release (10);
- OUTER_TIME_AGE (11), la estructura frente a access_policy (12) e INNER_ACCESS_AGE (13);
- CONTROL_CBOR (14), header_binding (15), I_PAYLOAD (16), PAYLOAD_AGE (17) y el commit (18).
Lee los dos formatos (§22, §70). En el formato 2, INNER_ACCESS_AGE tiene exactamente 16 stanzas (paso 12); CONTROL_CBOR es de la versión de schema 2, con L y la regla de relleno (14); el paso 16 calcula P, y el 17 exige un texto en claro de exactamente P bytes con ceros tras el contenido, ERR_INTEGRITY en otro caso. Solo se entregan los L primeros bytes, nunca el relleno (§29.1, §56). Opened da el formato y, en los formatos 2 y 3, L, la regla y P.
En el formato 3, el paso 17 lo hace open3.ts, y los ficheros van a sink; sin él, open rechaza con un TypeError justo tras el paso 2, antes de pedir nada, como ErrSinkRequired. Opened da entonces el head, los veredictos del área de seguridad y el tamaño del área.
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.
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.
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)
encrypt.ts, writer.ts Los writers. encryptFiles(files, opts) escribe un .dkc de formato 3, como capsule.EncryptFiles: comprueba las rutas y los textos con las reglas del lector y con los textos de Go, pone los ficheros en el orden de los bytes de sus rutas, mide L con un head de sal y hashes a cero, lee cada fichero dos veces y falla si cambió entre las dos lecturas; el head, el control y el área de seguridad se decodifican antes de escribir. El área de seguridad mide 32 KiB sea lo que sea lo que guarde la cápsula (§62.1, regla 13), y va vacía o con la firma y el sello de los enganches de Go: authorKey firma con alg 1 (un AuthorKey de authorkey.ts o cualquier AuthorSigner), cmsSigner con alg 2, la firma con certificados, y sealer pide el sello de seal_type 2; largeArea deja ensanchar el área a 64 KiB solo si lo firmado no cabe en 32 KiB. Los enganches pueden ser asíncronos. Se comprueban como en newSealer de Go, en su orden y con sus textos: una sola firma, y con cmsSigner los sellos van dentro de cada firma. Se llaman cuando el control y el head ya son los finales y antes de escribir nada: la firma se compromete con ellos y el sello con la firma (§29.8, §29.11). El área se evalúa con el lector de la librería en el contexto de la cápsula antes de escribirla, como security de Go, y una firma que no daría F4 o F6, o un sello que no daría S4 o S5, la hace fallar con el texto de Go (reglas 17, 19 y 21). Lo que lanza cmsSigner o sealer llega con capsule: signing: o capsule: sealing: y su mensaje, y el error como cause. publicNote es la nota pública de la cabecera (§24.1), que se rechaza con los textos de extension.CheckNote tras capsule: , y nunca se corrige; las opciones se comprueban en el orden de newSealer de Go. Con un área de 512 bytes, que solo puede pedir un generador de vectores, reproduce byte a byte PRELUDE, PUBLIC_HEADER, CONTROL_CBOR y BODY de los cinco fixtures que escribió EncryptFiles en la v0.10. fileSource hace la fuente de un File.
El formato 2 solo lo escribe un generador de vectores (§62.1, regla 1). encrypt(src, opts) tiene la forma de capsule.Encrypt, pero sus opciones no pueden pedirlo, así que falla con el texto de Go. Lo que solo pide un generador, el formato 2 y otra área (TestVectors, como EncryptOptions.TestVectors de Go), solo lo pasan al núcleo los ayudantes de testing/encrypt.ts (encryptVectors, encryptWith y encryptFilesWith), que ninguna página puede cargar. Con ellos, las pruebas y los scripts escriben un .dkc de formato 2, sin head, área ni nota, y, si se pide, una .dkk portable (§61, §62, §62.1), en el orden y con los textos y códigos de capsule.Encrypt:
- el formato 2 siempre; L conocida de antemano (el tamaño de un Uint8Array o un Blob, o length con un ReadableStream), y una fuente que da más o menos bytes falla con los textos de Go;
- el relleno reforzado por defecto, o bloque256;
- de 1 a 16 credenciales, canónicas y no de orden bajo, un señuelo en cada hueco libre, cuyo escalar se borra al derivar su clave pública, y un orden uniforme de los 16 (random.ts);
- SEALED_CONTROL_LEN con la fórmula del §62.1, comprobada con el sellado real;
- las autocomprobaciones de la regla 11 y dos más: OUTER_TIME_AGE con las reglas del lector, y la cabecera de PAYLOAD_AGE, que I_PAYLOAD abre antes de escribir nada.
Nada se escribe hasta que todo lo anterior al contenido está comprobado. El contenido va en trozos de 64 KiB, seguido de los ceros del relleno, con presión inversa, hacia memoria (hasta MAX_MEMORY_DKC, 1 GiB) o hacia output, que se cierra solo con la cápsula completa y comprobada y se aborta ante cualquier fallo. Los errores de la fuente y de la salida se relanzan tal cual.
El núcleo, writer.ts, recibe la aleatoriedad de quien lo llama: encrypt.ts le da la de crypto.getRandomValues, y solo testing/encrypt.ts la fija, para reproducir los fixtures de Go
capsule.Encrypt, accesskey.Encode
tlock.ts timeRecipient, el Recipient de age-encryption para OUTER_TIME_AGE (§32, §35), como agewrap.TimeRecipient: cifra la file key con ibe.ts para una ronda de un perfil pinneado y escribe el stanza tlock <ronda> <chain hash> de tlock. Comprueba el perfil y luego el rango de la ronda, con los textos de NewTimeRecipient. age-encryption no tiene etiquetas, así que quien escriba OUTER_TIME_AGE (fase 3) lo añade como único recipient agewrap.TimeRecipient
lengths.ts El tamaño de un .dkc de formato 2 o 3 antes de escribirlo. Para el formato 3, bodyLength da L con headLength, que mide el head por los tamaños de sus elementos CBOR sin codificarlo ni cargar las tablas de Unicode, y mtimeSeconds y headComment dan la mtime y el comentario tal como el writer los guarda. Para los dos formatos: sealedControlLength, la fórmula de SEALED_CONTROL_LEN del §62.1 con la que el writer comprueba su sellado, y capsuleLength, el tamaño exacto que escriben los writers para una ronda, una política, L, el relleno, las extensiones y la nota pública, que la página muestra antes de cifrar porque cualquiera con el fichero lo ve (§55.2). Sin noble, age-encryption ni tablas de Unicode capsule.Encrypt, que mide un borrador sellado
padding.ts El relleno del formato 2 (§29.1): los códigos 1 (bloque256) y 2 (reforzado), paddedLength, exacta hasta L_MAX = 2⁵³ − 2⁴⁶ (bitlen con BigInt y los redondeos con ceil, exactos en doubles; nunca operaciones de 32 bits, Math.clz32 ni Math.log2), y la longitud de PAYLOAD_AGE capsule/padding.go
digest.ts SHA-256 incremental con @noble/hashes (sha256Hasher, sha256Stream), para el capsule_digest de un .dkc que no está en memoria o que se está escribiendo (Web Crypto solo calcula el hash de buffers enteros)
agefile.ts Ficheros age enteros con el Decrypter de age-encryption, compartidos por la apertura y las autocomprobaciones del writer: los errores de una identidad conservan su código, y cualquier otro fallo de age es ERR_INTEGRITY con el motivo fijo de su fase, cabecera o STREAM. Lo que se lee en memoria se borra trozo a trozo capsule.Open
recipient.ts Recipients age1… como los lee y escribe age 1.3.2, con sus textos, y las reglas de §37 y §62.1 (regla 3) con los de agewrap.CheckX25519Recipient: se rechazan los no canónicos (bit 255, u ≥ p) y los de orden bajo, comprobados con la lista de sus cinco coordenadas u, sin aritmética de curva; un punto del twist se acepta, como en Go. También lee la lista de recipients que escribe una persona, como un fichero -R de age algo más tolerante, con errores por número de línea que nunca citan su contenido. Sin noble, para que una página valide las líneas sin cargar el writer age (ParseX25519Recipient), agewrap.CheckX25519Recipient
random.ts Un índice uniforme sin sesgo (rechaza las palabras de 32 bits desde ⌊2³²/n⌋·n) y la permutación de Fisher–Yates, para el orden de los 16 huecos de INNER_ACCESS_AGE (§39) capsule.permute
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…, genera identidades nuevas en bytes crudos (newX25519Identity) y deriva su clave pública (x25519PublicKey) 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, …); compareInstants e isInstant; LONG_HORIZON_SECONDS e isLongHorizon, el umbral de 365 días de los avisos de §53 y §50, una política de producto 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. CONTROL_CBOR se lee y se escribe para un formato: versión de schema 1 sin las claves 6 y 7, o 2 y 3 con payload_length (8 bytes, hasta L_MAX) y padding (1 o 2); 103 bytes sin extensiones sea cual sea L capsule, accesskey
body.ts, security.ts, head.ts El formato 3 (§29.2 a §29.7): la trama de BODY (AREA_LEN, SECURITY_LEN y HEAD_LEN) y los ceros del área, ERR_INTEGRITY; el SECURITY_CBOR que escriben los writers, vacío o con la firma y el sello (encodeSecurityWith, encodeAuthorSignatureItem y encodeSealItem), y los veredictos de la v0.11 con sus textos y las líneas que los muestran, las de un certificado con los textos del borrador v0.12 (cada nombre entre « y », la autoridad del sello de cada firmante de F6 y el aviso de que DateKeys no comprueba quién emitió los sellos): X; F0 a F6, con la firma de alg 1 y la de alg 2 comprobadas en el contexto de la cápsula; y S0 a S5, con el sello de seal_type 2. evaluateSecurity nunca lanza: una excepción al evaluar la firma da F1, y una al evaluar el sello, S2, cada una sin tocar el otro veredicto. Y el head, con las capas de §69.1: R1 y R8 en el CDDL, R8 por los bytes UTF-8 y no por el orden UTF-16 de las cadenas de JavaScript, y luego el comentario, el autor declarado, las rutas, la maquetación, R7 y R9, todo ERR_HEAD_INVALID, y las extensiones críticas del objeto head capsule/format3.go, capsule/signature.go
authorkey.ts Las claves de autor de alg 1 (§29.9, §29.12), como el paquete authorkey de Go en spec-v0.12, con sus comprobaciones en su orden y sus textos byte a byte: AuthorKey (generate con una fuente de azar inyectable, fromSeed, publicKey, sign, clear, secret, y toString, toJSON y util.inspect que ocultan el secreto), authorPublicString, parseAuthorPublic (canónica, en la curva y no de orden pequeño), parseAuthorSecret y marshalAuthorKey. Las cadenas se leen como bytes de Go: la mayúscula y la minúscula son las de strings.ToLower y strings.ToUpper de Go, y los espacios de una línea los de strings.TrimSpace, con las tablas de gounicode.ts; un Uint8Array es una cadena de Go que puede no ser UTF-8. El fichero de clave: encryptAuthorKey lo escribe con el Encrypter de age-encryption y una frase de paso, scrypt con logN 16; readAuthorKey lee uno cifrado o en claro de hasta 64 KiB, con las líneas de bufio.Scanner, y del cifrado lee la cabecera con age.ts, comprueba el stanza scrypt como ScryptIdentity de Go con un factor máximo de 16, deja a age-encryption el scrypt y el MAC, y descifra el STREAM, todo con los textos de age de Go. A diferencia de Go, una clave borrada lanza al usarla. JavaScript no promete tiempo constante ni borrar la memoria: se borran las copias propias, no las del motor, age-encryption o noble, ni las cadenas. En Node 24.9, firmar tarda unos 6 ms, y escribir o leer un fichero de clave cifrado, unos 0,3 s, los del scrypt de 64 MiB authorkey
ed25519sign.ts La firma Ed25519 (RFC 8032, 5.1.5 y 5.1.6) en código propio: crypto_sign de TweetNaCl, como el port de Dart, con el SHA-512 de @noble/hashes. Aritmética exacta en Float64Array (16 miembros de 16 bits en el cuerpo, 64 de 8 bits para los escalares módulo ℓ); los secretos nunca pasan por BigInt, una rama o un índice que dependa de ellos. Da la firma de Go byte a byte, también si la clave pública que se le da es otra crypto/ed25519
gounicode.ts Las tablas de unicode.ToLower, unicode.ToUpper y unicode.IsSpace de Go 1.26 (Unicode 15.0.0) que usan strings.ToLower, strings.ToUpper y strings.TrimSpace, en tramos. Las genera scripts/go-unicode-tables.go con el SHA-256 de cada conjunto, que una prueba recalcula unicode
author.ts Lo que se firma y se sella (§29.8, §29.11): payload_commit, control_commit sobre CONTROL_SIG, head_digest, signers_digest, AUTHOR_MESSAGE (99 bytes de ASCII) y su código, que se toma byte a byte como en Go, SIG_PART y SEAL_SUBJECT. Nada se guarda: todo se recalcula de la cápsula abierta capsule/signature.go
ed25519strict.ts La verificación estricta de la firma de alg 1 (§29.9): las cuatro condiciones del perfil, con la aritmética del grupo de @noble/curves, cuyo verify usa la ecuación con cofactor y acepta lo que el perfil rechaza. Da la respuesta de Go en los 18 vectores de ed25519_strict.json internal/ed25519strict
der.ts La comprobación estricta de DER que hace la firma CMS antes de mirar dentro (§29.10): longitudes definidas y mínimas, BOOLEAN, INTEGER, NULL, OID y BIT STRING canónicos, UTCTime y GeneralizedTime en sus formas de X.690 y con una fecha que existe (parseTime los lee), solo los tipos universales que usan los certificados, las firmas y los tokens, los tipos de cadena restringidos entre ellos, y una profundidad de 32; setOfSorted para los SET OF cuyo esquema conoce quien llama, que pueden repetir un elemento internal/der
cms.ts La firma CMS (RFC 5652) de alg 2 y el token RFC 3161 del sello (§29.10, §29.11), con el orden de comprobaciones y los resultados de Go y la tabla cerrada de algoritmos: RSA PKCS #1 v1.5 y PSS con BigInt, de 2048 a 4096 bits y con un módulo impar, y ECDSA sobre P-256, P-384 y P-521 con la aritmética de @noble/curves, solo con el punto sin comprimir. Lee el certificado campo a campo con el perfil del §29.10 del borrador v0.12, como Go: a quién nombra (su givenName y su surname antes que su commonName), el emisor que dice (su commonName o su organizationName), su validez y su clave; nunca comprueba quién lo emitió ni si se revocó. Compara los OID por los bytes de su DER, acepta un SET OF que repite un elemento y cuenta como uno un certificado repetido, y un nombre conserva un U+FEFF inicial, como en Go internal/cms
securitycms.ts Los veredictos de una firma de alg 2, F1, F2, F5 y F6, con cada firmante requerido y ajeno nombrado como el §29.7 del borrador v0.12 (su nombre si cumple las reglas del autor declarado, tiene como mucho 64 puntos de código y no lleva dos espacios seguidos, y si no el SHA-256 del certificado; su emisor, o el SHA-256 de su Name; y la autoridad de su sello), y los de un sello, S1 a S5, con su autoridad y t. encodeSigners escribe SIGNERS para el escritor, ordenado y con los textos de Go capsule/signature2.go
note.ts La nota pública (§24.1): la extensión datekeys.note de la cabecera, de 1 a 1024 bytes de UTF-8 que cumplen las reglas del autor declarado, con los textos y el orden de extension.CheckNote. Una cadena con un sustituto suelto se rechaza: nunca se escribe con U+FFFD. checkNoteData comprueba los bytes de una nota en el orden de Go: la longitud, el UTF-8 y las reglas. publicNote la lee como Go, con un U+FEFF inicial que la deja inservible, y unusableNote distingue una nota inservible de ninguna extension (CheckNote, NewNote, Note), capsule (Header.UnusableNote)
pathrule.ts, pathrule-tables.ts Las reglas de las rutas y de los textos del head (§29.5, §29.6), de R1 a R10 con R4b, R6b, R6c y R9, NFD, el pliegue y la clave de R7, con los textos de error de Go. Nunca usa normalize, toLowerCase, localeCompare, Intl ni las clases \p{…}, cuya versión de Unicode cambia con el motor: las tablas de Unicode 18.0.0 y WindowsBestFit las genera datekeys-go (pathrule/gen -ts), y una prueba recalcula su digest internal/pathrule
open3.ts, sink.ts El paso 17 del formato 3 en sus subpasos 17.2 a 17.8, con la precedencia de Go: un fallo de age o un texto en claro que no mide P prevalecen, manda el primer subpaso que falla, y los códigos distintos de ERR_INTEGRITY solo se dan tras leer PAYLOAD_AGE hasta el final. Sink (begin, create, commit, abort) recibe los ficheros y solo los publica en el paso 18; MemorySink los guarda en memoria, que crece con los bytes recibidos y nunca con los tamaños que declara el head capsule/open3.go
framing.ts Prelude DKC1 (16 bytes), con el formato de la cápsula, de 1 a 3, 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, con los 16 de INNER_ACCESS_AGE en el formato 2 (ACCESS_SLOTS); 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), con la nota pública (public_note) o el aviso de que no se muestra (public_note_unusable). inspect lee la nota bajo demanda, solo si la cabecera lleva una, así que las tablas de Unicode nunca se cargan con /inspect capsule/inspect.go, internal/inspectview
prefix.ts Lecturas acotadas de un Blob: el prefijo de un .dkc que necesitan los pasos 1 a 8 (readCapsule) y, de una .dkk, como mucho 12 bytes + 16 MiB + 1 (readAccessKey), que dan el mismo resultado que el fichero entero
locator.ts, ipaddr.ts El localizador de datekeys.capsule sin criptografía (§43 a §44.1 de la v0.12), con los checks, el orden y los textos del paquete locator de Go en spec-v0.12:
- los datos de la extensión: parseInfo, con ERR_EXTENSION_DATA_INVALID como único código, e infoExtension, que relee lo que escribe (§72);
- standardExtensions, el extension.Standard de Go: con el validador por defecto comprueba los datos de datekeys.capsule como locator.Standard, y con null solo que haya datos;
- las direcciones: checkURI con cada regla del §44.1 de la v0.12, addressHost y usableAddresses. Una dirección se lee como sus bytes, y un sustituto suelto como los tres bytes de su punto de código (WTF-8), que no son UTF-8 y se rechazan como Go rechaza esos bytes. Los bloques de IANA se comparan byte a byte sobre los 4 o 16 bytes de la dirección, nunca como números (ipaddr.ts, que lee como netip.ParseAddr y escribe como su String);
- checkResolvedIp, la IP a la que resuelve un nombre, que Go no tiene: es la de datekeys-dart, con su texto, y rechaza hoy 64:ff9b::/96 como el §44.1 de la v0.12;
- el texto en claro: marshalLocator, unmarshalLocator y plaintextLength, con el relleno de la clave 6 hasta el menor múltiplo de 4096 que puede llenar;
- el resto: restIn y hide.
Los errores sin código son LocatorError, con el texto de Go
locator (locator.go, open.go y hide.go), net/netip
ageio.ts, envelope.ts La criptografía del localizador, con los textos de Go: openSealed (Open), que lee como mucho 1 MiB del texto como Go con io.LimitReader; openInfoLocator (Info.OpenLocator); openEnvelope; seal y newEnvelope, con una fuente inyectable de lo aleatorio (RandomFill, crypto.getRandomValues por defecto) que se lee en el orden de Go: con los mismos valores escriben los mismos bytes. ageio.ts lee y escribe ficheros age como filippo.io/age 1.3.2, con sus textos, porque locator.Open y OpenEnvelope los copian: la cabecera con el parser de age.ts, la identidad X25519, el MAC, el nonce y STREAM; la cápsula sigue leyendo y escribiendo los suyos con age-encryption. Solo el esquema de Quicknet, como la apertura locator (Seal, NewEnvelope, Open, OpenEnvelope), filippo.io/age
index.ts Reexporta todo salvo la fase 2 (ibe.ts, release.ts, open.ts, tlock.ts, x25519.ts, bech32.ts, digest.ts y agefile.ts), la fase 3 (encrypt.ts, writer.ts, recipient.ts y random.ts), el formato 3 (open3.ts, sink.ts, head.ts, pathrule.ts y sus tablas, que pesan 136 KB), la v0.11 (author.ts, ed25519strict.ts, der.ts, cms.ts, securitycms.ts y note.ts) el localizador (locator.ts, ipaddr.ts, ageio.ts y envelope.ts) y las claves de autor (authorkey.ts, ed25519sign.ts y gounicode.ts), y una guarda lo comprueba. La página importa index.ts, y reexportarlos metería noble, age-encryption o las tablas en la primera carga de /inspect aunque no los use, porque noble ejecuta código al cargarse. La página carga la apertura bajo demanda (src/lib/inspector/opener.ts)
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, firmas y tokens de prueba (cmsbuild.ts), el writer como generador de vectores (encrypt.ts), el único que escribe el formato 2 u otra área, y lo del localizador: la lectura de sus vectores (locator.ts), una fuente de lo aleatorio de semilla fija con ChaCha20 propio (seeded.ts) y las recetas de su interoperabilidad (locator-interop.ts). Solo lo importan los tests y testing/ mismo: una guarda de dependencies.test.ts lo comprueba en src/, y check-build.mjs en el bundle de las páginas

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.

Con la v0.9 se contrastaron contra spec-v0.9 los 125 casos de mutations.json, en memoria y desde un Blob con salida, y coinciden el código, el paso y el texto del error de capsule.Open en todos. Desde la v0.10 ese contraste está en el repositorio: src/lib/dkc/testing/mutation-texts.json congela el texto de capsule.Open en spec-v0.11 para los 210 casos, y vectors.test.ts exige el mismo en las dos aperturas. El validador de extensiones del arnés da los textos de testkit.KnownExtensions, así que ya coinciden también los casos de extensiones conocidas con datos inválidos.

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 que no es la esperada es ERR_UNSUPPORTED_VERSION sea lo que sea lo que venga detrás (en CONTROL_CBOR, la esperada es el formato de la cápsula), 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.

El localizador

El localizador sigue al paquete locator de Go también en esto, que el autor decidió el 6 de octubre de 2026 dejar así:

  • openSealed lee como mucho 1 MiB del texto del localizador, y un defecto posterior queda oculto tras el error de unmarshalLocator.

parseInfo comprueba el localizador sellado tanto como se puede antes de la fecha, como Go desde 69dbb0c: la cadena del stanza en hexadecimal en minúsculas, la de Quicknet si la DateKey es de Quicknet, y un cuerpo que lleva un texto de 4096 bytes o un múltiplo, así que una cabecera sin cuerpo se rechaza; infoExtension rechaza además una DateKey de un perfil que la librería no fija. Un CID con un carácter de más cuyos bits son cero, y https://[[2000::]/, se rechazan desde que Go los rechaza (e801e03 de datekeys-go): el §44.1 de la v0.12 ya pedía el CID en su forma canónica y un literal IPv6 con un solo par de corchetes.

checkResolvedIp sigue la v0.13 (§44.1, cambio 1 del §76), como locator.CheckResolvedIP de Go: una dirección de NAT64 a la que resuelve un nombre, de 64:ff9b::/96 o del prefijo de la red que la aplicación le pasa en nat64, cuenta por la IPv4 que lleva dentro. vectors.test.ts corre los 42 casos de resolved_ip.json.

Diferencias de forma con Go, sin efecto en lo que se lee o se escribe:

  • openInfoLocator recibe el registro de perfiles, como Go, y sin él falla con el texto de Go para nil; la página usará defaultRegistry();
  • standardExtensions(null) es el extension.Standard de Go sin ValidateCapsule;
  • seal saca la etiqueta de 16 bytes que Go saca para que age no mezcle el recipient tlock con otros, y la descarta: este escritor tiene un recipient solo, y sacarla mantiene el orden de los valores de Go;
  • como en la apertura, solo se verifica el esquema de Quicknet: un perfil de otro esquema falla con ERR_UNKNOWN_PROFILE donde Go lo usaría.

Página inspector

Sitio SvelteKit estático (@sveltejs/adapter-static, strict): las tres 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 y /create
/inspect El inspector
/create Crear una cápsula (fase 3, paso 7)

/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 con el formato de la cápsula (con un aviso en el veredicto si es el 1, que no oculta el número de credenciales ni la longitud exacta del contenido, §55.2), los argumentos del stanza tlock frente al perfil fijado y el número y tipo de stanzas de OUTER_TIME_AGE y PAYLOAD_AGE;
  • la nota pública, justo debajo del veredicto, como texto del creador sin comprobar, en la letra serif de la página y sobre el fondo de aviso, con el aviso de que antes de la fecha nadie puede comprobar quién creó la cápsula ni si va firmada, como showNote de la CLI de Go; si la cabecera lleva una nota que no cumple las reglas de texto, solo dice que no se muestra (§24.1);
  • 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.

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.

Abrir

Tras los pasos 1 a 8, si la cápsula es válida y su fecha de apertura ya pasó según el reloj del dispositivo, la página ofrece abrirla: pasos 9 a 18 de §63 con open (fase 2, paso 8). Antes de la fecha dice que nadie puede abrirla todavía y no pide nada.

  • El release lo da quien abre (decisión 4 del plan de la fase 2, confirmada el 28-09-2026): la página nunca lo pide a la red. Se pega la respuesta JSON de drand o la firma sola en hexadecimal (release-input.ts), y solo se leen round y signature: cualquier otro campo, como una clave pública, se ignora, porque la raíz de confianza es el perfil fijado (§11, §13). La página enlaza la URL de drand de esa ronda (https://api.drand.sh/<chain hash>/public/<ronda>, con rel="noopener noreferrer"), que abre la persona en otra pestaña. Es un release que suministra quien llama: el paso 10 lo verifica y da sus códigos (ERR_ROUND_MISMATCH, ERR_RELEASE_INVALID). En los fixtures oficiales el campo viene relleno con el release de su registro, que es el que publicó drand.

  • Credenciales, solo en time_and_key: una .dkk (se leen como mucho 16 MiB + 13 bytes, readAccessKey) o identidades AGE-SECRET-KEY-1…, una por línea, como un fichero de identidades de age. La página explica de dónde sale la identidad: del fichero que se creó con age-keygen, que se puede pegar entero. Un error de una línea se da por su número, nunca por su contenido, y el foco va al campo; las identidades se borran tras usarlas. En time_only la página no las pide y open no las usa.

  • El código de la apertura se carga bajo demanda: la página importa opener.ts con import() al pulsar "Abrir", y con él open.ts, noble y age-encryption. opening.ts, que construye lo que se muestra, solo importa tipos de open.ts. check-build.mjs comprueba que ninguna página carga noble, @scure/base ni age-encryption en la primera carga.

  • El texto en claro se muestra, cuando la cápsula se abre y es texto (plaintextPreview): UTF-8 imprimible, hasta 100 000 caracteres de sus primeros 128 KiB. Se muestra el CR LF de Windows como salto de línea y se omite el BOM, como hace un editor; la descarga conserva los bytes exactos. Lo que no es texto solo se ofrece para descargar. El de un fixture se abre en memoria, con su SHA-256 comparado con el del registro. El contenido va justo debajo del veredicto, antes de los pasos 9 a 18.

  • El nombre de la descarga. La cápsula no guarda el nombre del fichero que sella (§6, §55.2), así que se ofrece el del .dkc sin la extensión, como hace age con informe.pdf.age. Si ese nombre no tiene extensión propia, como el capsula-<fecha>.dkc de /create, toma la del contenido (contentExtension): .txt para un texto, y .pdf, .png, .jpg, .gif, .webp, .wav, .zip, .7z, .gz, .mp3, .mp4 u .ogg por sus primeros bytes. El de un fichero propio va a un fichero temporal del almacenamiento privado del navegador (OPFS, tempfile.ts), escrito con createWritable. El navegador guarda lo escrito en un fichero de intercambio que solo se confirma al cerrarlo, y open lo cierra tras el paso 18 y lo aborta ante cualquier fallo (§56). Después se ofrece para descargar, con el nombre del .dkc sin la extensión, como hace age, y cada carácter no imprimible cambiado por _, para que un carácter de control bidireccional no disfrace la extensión. Antes de empezar se compara el tamaño de PAYLOAD_AGE con la cuota libre (navigator.storage.estimate()).

  • El fichero temporal se borra al pulsar "Borrar", al abrir o cargar otra cápsula y al salir de la página (pagehide). Si el navegador terminó antes, se borra en la siguiente visita. Cada pestaña escribe en su propio directorio, datekeys-open/<id>, y tiene un Web Lock con ese nombre mientras existe. Así la limpieza de otra pestaña nunca borra un fichero en uso; sin Web Locks, solo borra lo que tiene más de un día. Si el navegador no tiene OPFS o createWritable, o los rechaza (una ventana privada, datos del sitio bloqueados), o la cuota no alcanza, la página abre en memoria hasta 64 MiB. Una apertura en curso se detiene si se carga otra cápsula: su salida deja de aceptar datos, open falla en el paso 17 y su fichero se borra. Si la página vuelve de la caché de atrás y adelante tras borrar el fichero, ya no lo ofrece.

  • El formato 3 guarda ficheros con sus rutas (§29.2 a §29.7). Van al mismo fichero temporal por un ZipSink: un fichero de un solo segmento, tal cual; en los demás casos, un ZIP propio, y cada fichero es un tramo de ese ZIP (apartado 5 del diseño del formato 3). Sin OPFS, van a la memoria de la página, y el ZIP se compone allí con zipOf. Lo que se muestra sale de files.ts, que se carga con la apertura porque compara claves de R7:

    • primero, las líneas de los veredictos del área de seguridad; después, el autor declarado y el comentario, como texto del creador sin comprobar, en un recuadro (§29.7);
    • cada ruta como texto, en un <bdi dir=auto>, con los invisibles escritos como \u…, su tamaño y la fecha que declaró el creador; hasta 500 en la lista, y todas en el ZIP;
    • los avisos de la CLI de la referencia, comparados por la clave de R7: un acceso directo (.lnk, .url, .library-ms, .searchConnector-ms), desktop.ini, una carpeta .git, un programa o un script, y un segmento que empieza por '-';
    • las descargas: el fichero, con su nombre pasado por safeFileName, si es el único, con el ZIP de su carpeta como segunda opción; si hay varios, el ZIP, llamado <primer segmento común>.zip o como el .dkc, y cada fichero por separado.

    Si el ZIP no cabe en el espacio que deja el navegador, ZipSink falla al empezar, con la longitud exacta, y la página lo dice. check-build.mjs comprueba que las tablas de pathrule-tables.ts no entran en la primera carga de ninguna página y sí en la carga bajo demanda de /inspect y /create.

  • La fecha se vuelve a mirar cuando llega y cuando la pestaña vuelve a verse, así que una cápsula inspeccionada antes de su fecha se puede abrir sin cargarla otra vez. open comprueba de nuevo el reloj en el paso 9.

  • El resultado muestra cada paso de 9 a 18 como muestra los de 1 a 8, con los checks que registra la referencia (desde la v0.9, también el 17 cuando se supera), el release verificado, las extensiones de CONTROL_CBOR, el tamaño y el SHA-256 del texto en claro y, en el formato 2, la regla de relleno y P. También avisa de que el texto está autenticado, pero no prueba quién lo escribió ni que sea el original si otros abrieron la cápsula antes (§55.1).

Medido en Chromium (el navegador de la app de escritorio) sobre la compilación de producción: los pasos 9 a 18 tardan 0,34 s en time_only, la primera apertura de la página, y 0,11 s en time_and_key_portable, la siguiente, sin contar la descarga del código y en el hilo principal (la política no permite workers).

Crear

/create (plan de la fase 3, sección 9, con las decisiones del paso 6, y apartados 3 y 5 del diseño del formato 3) cifra ficheros y carpetas propios, con un comentario y un autor declarado, en un .dkc de formato 3 (§62.1, regla 1) y, si se pide, en una .dkk portable, todo en el navegador y sin red.

  • Lo que guarda (create-files.ts, con la primera carga): ficheros elegidos con multiple, una carpeta con webkitdirectory o lo que se suelte en la página, carpetas incluidas, que se recorren con webkitGetAsEntry hasta el final. Un fichero va con su nombre, y el de una carpeta con el nombre de la carpeta delante, como webkitRelativePath y la CLI de la referencia. Las carpetas vacías no se guardan, y un recorrido se detiene en cuanto pasa de 65 535 ficheros. Una ruta que ya está en la lista se sustituye. Los ficheros que crean los sistemas dentro de una carpeta (.DS_Store, Thumbs.db y desktop.ini, comparados como strings.EqualFold, ._* y lo que hay bajo __MACOSX) quedan fuera, tachados, con una casilla para marcarlos, como los deja fuera collect.go; uno elegido suelto, no.
  • Las rutas se pueden editar, y se comprueban a medida que se escriben con las reglas del lector (create-check.ts, bajo demanda con las tablas de Unicode): cada problema en su fila, en español y con su regla, y un choque de R7 en las dos rutas. Una prueba de propiedad exige que la página no vea ningún problema exactamente cuando checkPath y checkTree aceptan las rutas. Una lista de más de 2000 ficheros se vuelve a comprobar tras una pausa al escribir, con el problema de cada ruta y la clave de cada segmento en memoria; se muestran los 500 primeros y los que han tenido un problema. Cada fila da el tamaño, la fecha y la ruta original si cambió.
  • El comentario y el autor declarado son opcionales, se comprueban igual (§29.6) y van cifrados en el head, con el comentario en LF. La casilla de la fecha de modificación, marcada por defecto, guarda la de cada fichero, que se omite si cae fuera de 1970 a 9999 (§62.1, regla 16). Una cápsula puede guardar solo un comentario (decisión 9).
  • El formulario (create-input.ts) mide los ficheros una vez por cambio de la lista (measureFiles), y con ellos da el tamaño exacto del .dkc a cada cambio del comentario, del autor o de la fecha. Después, el día y la hora, en la zona del dispositivo, en otra de las que conoce el navegador o en UTC. Y la política: «solo con la fecha» (time_only, la de por defecto) o «con la fecha y una clave» (time_and_key). Esta lleva destinatarios age1…, uno por línea y comprobados mientras se escriben (recipient.ts, sin noble), y la casilla de la clave portable, marcada por defecto; como mucho 16 credenciales. Una ayuda plegable explica qué es un destinatario de age y cómo se consigue: quien abrirá la cápsula crea su par con age-keygen y envía solo su age1…. Los campos se comprueban en su orden, y el foco va al campo del primer problema: una ruta que no vale lleva el foco a su fila.
  • La hora local (localtime.ts) se convierte al instante UTC con Intl y las reglas de zona que conoce hoy el navegador. Una hora que la zona se salta al adelantar los relojes se rechaza. De una que repite al atrasarlos se toma la más tardía, para no abrir nunca antes de lo querido. La cápsula no guarda la zona.
  • Antes de cifrar, la página muestra el instante efectivo, que es el de la ronda, en la zona elegida y en UTC, y cuánto cae después del pedido. También la ronda, la dk1_, la hora del dispositivo junto a la UTC, el contenido, el tamaño exacto del .dkc (capsuleLength con bodyLengthOf) y lo que la cápsula deja ver hasta la fecha (§55.2). Y los avisos: el de protocolo preliminar (§74), siempre; los de §53 y §50, con sus textos, si el instante efectivo está a más de 365 días (LONG_HORIZON_SECONDS); y uno informativo si está a menos de una hora. La hora del dispositivo se lee cada segundo mientras la pestaña se ve y al crear la cápsula; la página no la pregunta a ningún servidor. Si el navegador no conoce la zona del dispositivo (V8 da entonces Etc/Unknown), la página empieza en UTC.
  • El writer se carga bajo demanda: la página importa creator.ts con import() al pulsar «Crear la cápsula», y con él encrypt.ts, noble, age-encryption y las tablas de Unicode de las rutas. El relleno es siempre reforzado, y no hay extensiones. encryptFiles lee cada fichero dos veces, y la página muestra el progreso de las dos pasadas: la primera, en bytes de los ficheros, contada por creator.ts con un stream propio que lee a demanda; la segunda, en bytes del .dkc. «Cancelar» detiene cualquiera de las dos, y un fichero que cambia entre ellas hace fallar la escritura. Lo escrito no se ofrece si no mide lo que se mostró o si los pasos 1 a 8 lo rechazan, y ante cualquier fallo se borra la .dkk. Si la fecha llega mientras la página se prepara para escribir, el writer la rechaza con su propio reloj (§62.1, regla 2), y la página lo dice en el campo de la fecha.
  • El .dkc se escribe en un fichero temporal de OPFS, datekeys-create/<id>/capsule (tempfile.ts), que el navegador solo confirma cuando el writer lo cierra con la cápsula completa y comprobada. Si el navegador no tiene OPFS o lo rechaza, o la cuota no alcanza, se escribe en memoria, hasta 64 MiB. La cuota se comprueba con el tamaño exacto antes del primer byte. «Cancelar» detiene la escritura tras el trozo en curso: la salida se aborta y el fichero se borra. El fichero temporal se borra al pulsar «Borrar el fichero temporal», al crear otra cápsula y al salir de la página. Si el navegador terminó antes, se borra en la siguiente visita a /create o a /inspect, que limpian las dos zonas.
  • La .dkk (152 bytes sin extensiones) vive solo en la memoria de la página: nunca en OPFS, y nunca se muestra como AGE-SECRET-KEY-1…. Sus bytes se borran al pulsar «Olvidar la clave», al crear otra cápsula y al salir, y con ellos las URL de sus descargas. Junto a ella va el aviso de §7.4. Si la cápsula solo se abre con su .dkk y no se ha descargado, la página pide confirmación antes de borrarla, al olvidarla o al crear otra, y avisa antes de salir: el navegador pregunta al cerrar o recargar, y la página, al seguir un enlace del sitio. Salir de la página también cancela una escritura en curso.
  • Las descargas van por separado, con nombres que se pueden editar. Por defecto son capsula-<apertura en UTC>.dkc y .dkk, que no dicen cuándo se creó la cápsula. La URL blob: de cada descarga se revoca un minuto después del clic.
  • El resultado muestra el capsule_id, la política, el contenido, el tamaño y el relleno, y el informe de los pasos 1 a 8 del .dkc escrito, con el componente del inspector.

Comprobado el 30-09-2026 en Chromium, con el formato 3:

  • una lista de dos ficheros y una carpeta con .DS_Store, un fichero bajo __MACOSX, una ruta con «:», dos nombres que solo cambian en mayúsculas y una fecha de 1969: los dos del sistema, tachados; el problema de R4 y el choque de R7, en sus filas; la fecha de 1969, omitida;
  • con las rutas corregidas, el comentario y el autor, la cápsula midió los 2.084 bytes del plan y se escribió en 0,47 s sobre la compilación de producción;
  • pasada la fecha, datekeys decrypt de Go la abrió con los relays públicos: los seis ficheros con sus rutas y fechas, el autor y el comentario. /inspect la abrió con el release pegado de drand, con un ZIP y la descarga de cada fichero;
  • al crear, una ruta CON.txt lleva el foco a su fila, y después un autor con un espacio inicial, a su campo;
  • en el servidor de desarrollo, con el panel del navegador oculto, cada actualización del progreso retrasaba la escritura casi 2 s; la compilación de producción no lo hace.

Comprobado el 29-09-2026 en Chromium (el navegador de la app de escritorio), sobre la compilación de producción:

  • un fichero de 77 bytes se cifró en time_and_key, con clave portable, para dentro de cuatro minutos, sin ninguna petición fuera del origen, y el informe pasó los pasos 1 a 8 con el formato 2;
  • pasada la fecha, la cápsula se abrió en /inspect, con el release pegado de drand y la .dkk, y con datekeys decrypt de Go, con red y la .dkk: el mismo contenido, con el mismo SHA-256;
  • los avisos de §53 y §50 salen solo a partir de 365 días;
  • cancelar a mitad de 200 MB no deja fichero, y sin createWritable la cápsula se escribe en memoria;
  • la página avisa al salir sin la .dkk;
  • a 375 px ningún elemento desborda el ancho, ni con un nombre de fichero largo sin espacios.

Una revisión adversarial del 29-09-2026 encontró un fallo mayor: la .dkk que era la única credencial se podía borrar sin confirmación. Encontró también ocho menores, entre ellos una zona del dispositivo desconocida, el reloj de la página parado mientras la pestaña se veía, un mensaje equivocado con el reloj del dispositivo anterior a Quicknet y el foco durante la escritura. Todos se contrastaron y se corrigieron, y los del navegador se volvieron a comprobar en él. La revisión comparó además localToEpochMs con una búsqueda exhaustiva en las 418 zonas, sin ninguna diferencia en 26 114 horas locales. localtime.test.ts repite esa comparación en cada ejecución, alrededor de los 260 cambios de hora de 2030.

Rendimiento, informativo, en ese navegador con la ventana en segundo plano: una cápsula pequeña tarda 0,14 s en time_and_key y 0,2 s en time_only. 64 MiB tardan 5,4 s hacia OPFS (unos 12 MiB/s), y 60 MiB, 3,2 s en memoria; 1 GiB, 74,5 s hacia OPFS. Escribir 64 MiB en OPFS cuesta 1 s en trozos de 64 KiB, los del writer, y 0,55 s en trozos de 1 MiB.

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/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
src/lib/inspector/release-input.ts Lee el release pegado (respuesta de drand o firma sola) y construye la URL de drand de la ronda
src/lib/inspector/opener.ts La apertura, cargada bajo demanda: identidades, .dkk, open con el release suministrado, SHA-256 del texto en claro
src/lib/inspector/opening.ts buildOpenReport: el modelo de la apertura (pasos 9 a 18, release, extensiones de CONTROL_CBOR, texto en claro), sin DOM ni reloj; nombre del fichero descifrado
src/lib/inspector/tempfile.ts El fichero temporal de OPFS, con un directorio y un Web Lock por pestaña y por zona (la apertura y crear), la cuota libre y la limpieza de lo que quedó. Su writable acepta además trozos con posición (TempChunk), como FileSystemWritableFileStream, para parchear el CRC-32 del ZIP
src/lib/inspector/crc32.ts, zip.ts El CRC-32 de ZIP, por trozos, y la disposición de un ZIP cuyas entradas se conocen de antemano: almacenadas, con nombres UTF-8 (bit 11), sin descriptores de datos ni entradas de carpeta; la hora DOS en UTC entre 1980 y 2107, el campo NTFS 0x000A, que manda, y 0x5455 cuando cabe en 32 bits con signo; ZIP64 por tamaño, posición o número de entradas
src/lib/inspector/files.ts Lo que la página muestra de los ficheros de una cápsula de formato 3: cada ruta como texto, sus avisos por la clave de R7 con los textos de la CLI de la referencia, y el nombre y el orden de las descargas. Se carga bajo demanda con la apertura, porque trae las tablas de las rutas
src/lib/inspector/zipsink.ts El sumidero de la página para el formato 3 (apartado 5 del diseño): un fichero de un segmento va tal cual al fichero temporal, y los demás casos a un ZIP en ese fichero, con cada cabecera en su posición, el CRC-32 parcheado al cerrar cada fichero y el directorio central al hacer commit; cada fichero queda como un tramo continuo. zipOf hace el mismo ZIP en memoria, como un Blob de sus partes
src/lib/inspector/localtime.ts Una fecha y una hora locales de una zona como instante UTC, con Intl: las horas que no existen y las repetidas; la lista de zonas
src/lib/inspector/create-files.ts La lista de ficheros de /create, sin tablas: lo elegido y lo soltado, carpetas recorridas incluidas, los ficheros de un sistema fuera por defecto como en collect.go, las rutas editadas y lo que la cápsula guarda
src/lib/inspector/create-check.ts Las reglas de las rutas y de los textos (§29.5, §29.6) como las explica /create: todos los problemas de todas las rutas, en español, con los de pathrule.ts. Se carga bajo demanda, con las tablas
src/lib/inspector/create-input.ts El formulario de /create, sin DOM, reloj ni writer: planCapsule comprueba los campos en su orden y da lo que se muestra antes de cifrar, con los ficheros medidos una vez (chooseFiles); los destinatarios y los nombres de los ficheros
src/lib/inspector/creator.ts La escritura, cargada bajo demanda: encryptFiles con los ficheros, el comentario y el autor hacia el fichero temporal o la memoria, con el progreso de las dos lecturas, la cancelación y la cuota, y los pasos 1 a 8 de lo escrito
src/lib/components/ InspectionReport, OpenPanel, StepList, ExtensionList, DataView, Mark
src/routes/ Layout, portada, inspector y crear

Fixtures

fixtures.ts importa con import.meta.glob los .dkc de testdata/fixtures como URL (?url) y, de cada registro JSON, solo tres campos públicos: description, release (la ronda y la firma que publicó drand, con las que la página abre el fixture) y plaintext_sha256 (para comparar con lo descifrado). 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 <meta http-equiv="content-security-policy">:

default-src 'self'; frame-src 'none'; worker-src 'none'; connect-src 'self' https://api.drand.sh https://api2.drand.sh https://api3.drand.sh; 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: fetch llega al propio origen, para los fixtures, y a los tres relays públicos de drand que usa la CLI de la referencia, solo cuando la persona pulsa «Pedir la firma a drand» en /inspect (drand.ts: en carrera, 6 s, como mucho 8 KiB, sin redirecciones, y la aleatoriedad comprobada contra la firma, que el paso 10 verifica con la clave fijada). El relay ve la IP y la ronda pedida. Tampoco llega a URL blob:: la descarga del texto en claro es una navegación.
  • 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); 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; si contiene un test o un módulo de src/lib/dkc/testing/, cuyos ayudantes escriben lo que solo puede escribir un generador de vectores; si una página carga noble, @scure/base o age-encryption en su primera carga, o las claves de autor y su firma (authorkey.ts, ed25519sign.ts y gounicode.ts), que /inspect no carga ni bajo demanda, o si /inspect y /create no pueden cargar bajo demanda age-encryption, @noble/curves y @noble/ciphers, y /create también @noble/hashes; o si a licenses.txt le falta el aviso de un paquete del bundle, las líneas de copyright de un módulo de src/ derivado de otro proyecto o la licencia del sitio. 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, en la primera carga y bajo demanda, y de los paquetes npm que lleva el bundle.

Avisos de licencia: licenses.txt

El JavaScript minimizado no conserva comentarios, así que el sitio publica licenses.txt, enlazado desde el pie de cada página. Lo escribe un plugin de vite.config.ts al compilar el cliente, con tres partes:

  • los módulos de src/ derivados de otros proyectos, con el aviso de su cabecera: ibe.ts (de tlock-js, MIT) y bech32.ts (de age, MIT);
  • el fichero de licencia de cada paquete npm con algún módulo en el bundle;
  • la licencia Apache-2.0 del sitio.

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

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), al 100 % en líneas, ramas, funciones y sentencias: de la librería, cbor.ts, ibe.ts, release.ts, x25519.ts, bech32.ts, digest.ts, tlock.ts, prefix.ts, recipient.ts, random.ts, agefile.ts, padding.ts, writer.ts, encrypt.ts y lengths.ts, los del formato 3, pathrule.ts, body.ts, security.ts, head.ts, open3.ts y sink.ts, los del localizador, locator.ts, ipaddr.ts, ageio.ts y envelope.ts, y los de las claves de autor, authorkey.ts, ed25519sign.ts y gounicode.ts; de la página, fixtures.ts, opener.ts, opening.ts, release-input.ts, tempfile.ts, localtime.ts, create-input.ts y creator.ts, y los del formato 3, crc32.ts, zip.ts, zipsink.ts, files.ts, create-files.ts y create-check.ts. 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, de los tres formatos, pasa los pasos 1 a 8 con los valores registrados (formato, prelude, PUBLIC_HEADER, DateKey, capsule_id, política, unlock_at, extensiones con sus bytes exactos, stanzas, header_binding, CONTROL_CBOR) y abre con open a su contenido; L, y en los formatos 2 y 3 la regla y P, cuadran con la longitud de PAYLOAD_AGE, y cada credencial abre justo el stanza que el registro le asigna. En el formato 3, BODY se recompone del registro: la trama, el área con sus veredictos, el head, que se decodifica y se reescribe a sus bytes, y cada fichero en su tramo con su SHA-256; open entrega esos ficheros a un MemorySink, con sus veredictos. 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 con el formato del vector, 1 si no lo trae, también el 3, decodeAccessKeyBody) con el código exacto, y un objeto aceptado se reescribe a los mismos bytes.

  • testdata/vectors/padding.json: P con las dos reglas y la longitud de PAYLOAD_AGE de cada L, incluidas las fronteras en que fallan las operaciones de 32 bits y un logaritmo en coma flotante, y las longitudes por encima de L_MAX, que se rechazan. padding.test.ts contrasta además paddedLength con el §29.1 escrito en BigInt sobre 20 000 longitudes de todo el rango.

  • 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/tlock_steps.json: los pasos 10 y 11 de §63 para Quicknet, valor a valor (v0.14), con el código de la librería: M con roundIdentity, H(M) con hashToG1, el release con verifyRelease y la ecuación de pairing; las tres partes del stanza con ciphertextFromBody, e(firma, U) con gtBytes, H2, sigma, H4 y la file key; la base de H3 con h3Base, cada intento con h3Try y su digest, el desplazamiento del primer byte y su aceptación, r con h3, y r·G2 = U con proofHolds; y decryptOnG2 sobre el cuerpo entero. También las lecturas erróneas que descarta el generador: el DST de G2 o la ronda sin SHA-256 no verifican, y poner a cero el bit más alto en vez de desplazar el byte da otra r, que U no prueba; una firma de otra ronda y un V o un W editados no descifran.

  • testdata/vectors/mutations.json: se leen los 218 casos enteros (ediciones sobre un fixture o hex congelado, release, reloj, registro, extensiones, .dkk, identidades y, en los once que abren, sus veredictos). Los 218 pasan por open con su release, su reloj, su registro, sus extensiones, su .dkk, sus identidades y un MemorySink, y dan el mismo código en el mismo paso que Go y el mismo texto que capsule.Open (testing/mutation-texts.json); los cuatro de seguridad del formato 3 abren con los veredictos X, F1, F2 y S1 del registro, y siete de la lista de la v0.11 con los suyos. También pasan como Blob con un stream de salida, que termina abortado en los 207 que fallan, y un sumidero que nunca se publica. Son los 178 del §64: las 33 mutaciones de las dos primeras listas en cada formato, las 23 de la lista del formato 2, las 48 de la del formato 3 y las 8 de la lista de la v0.11; y 40 más. Ninguno de los que fallan sin red pide un release. Los 57 de los pasos 1 a 8 pasan además por inspect, y un test fija los recuentos y el orden. Las .dkk ofrecidas se decodifican.

  • testdata/vectors/inspect_differential.json: las 5 110 mutaciones de los catorce fixtures de base dan el mismo veredicto, código y paso que Go; los bases se comprueban por su SHA-256.

  • testdata/vectors/paths.json y path_fold.json, con las tablas cuyo digest nombran: cada ruta pasa por las reglas de una entrada con el texto exacto, cada árbol por la decodificación de un head de ficheros de 0 bytes, y cada segmento da su NFD y su clave de R7.

  • testdata/vectors/head_schema.json y security.json: cada head pasa por decodeHead con el código de la primera capa que falla y el texto exacto de ERR_HEAD_INVALID, y un head válido se reescribe a sus bytes; cada área de seguridad da, en el contexto del fichero, sus veredictos y sus líneas.

  • testdata/vectors/security_cms.json: los 135 casos de alg 2 y de seal_type 2 dan, en su contexto, los veredictos, el resultado de cada firmante exigido y de cada ajeno, la autoridad y la hora del sello y las líneas de Go, byte a byte. ed25519_strict.json lo corre ed25519strict.test.ts.

  • testdata/vectors/note.json: cada nota pasa por checkNoteData, con su resultado y el texto exacto de la regla que incumple, y por publicNote, unusableNote y newNote.

  • testdata/vectors/locator.json: se corre entero, como TestLocatorVectors de Go, con los textos de Go de testing/locator-vectors.json y testing/locator-uris.json: la extensión se lee, el localizador se abre con el release de su ronda y da su texto en claro de 4096 bytes y sus campos, el resto se encuentra en el host en su desplazamiento y abre el sobre; las 36 bases de relleno; las 247 direcciones, con el veredicto del fichero y el texto de Go; el localizador mixto, del que se usa solo la dirección que se acepta y que un escritor no escribe; los 5 restos, los 8 datos de la extensión, con ERR_EXTENSION_DATA_INVALID como único código, y los 16 textos en claro.

  • src/lib/dkc/testing/locator-uris.json y locator-vectors.json: el localizador sin su escritura contra Go en spec-v0.12, que comprueban locator.test.ts y envelope.test.ts, todo con el resultado y el texto de Go. Los escribe scripts/locator-go-vectors.go, el generador de la etapa 7a de datekeys-dart con las semillas de este repositorio:

    • CheckURI y Host en las 247 direcciones de locator.json y en 2 800 hechas en cada borde del §44.1 y sacadas de una semilla, y CheckURI sobre 4 cadenas con un sustituto suelto; netip.ParseAddr y String en 1 700 cadenas; y publicIP en los bytes de 868 direcciones, que checkResolvedIp da igual;
    • PlaintextLength en cada base de −4 100 a 16 484; Unmarshal en 482 textos en claro de tres bases, válidos y rotos byte a byte y campo a campo; Marshal en 49 localizadores en cada límite y cada borde del relleno;
    • Open en 118 localizadores sellados de cuatro rondas, con los releases de otras, perfiles rotos y ficheros editados, y en 14 ficheros cuyo texto pasa de 1 MiB, que Go lee con io.LimitReader; OpenEnvelope en 34 sobres y restos; RestIn, Hide y 11 cortes de NewEnvelope; Info.Extension en 29 datos, Info.OpenLocator en 7 y ParseInfo en 127; el registro de locator.Standard con CheckNoncriticalIn, CheckCriticalIn y CheckWrite; y capsule.Open de un fixture cuya .dkk lleva datekeys.capsule, con sus checks.

    Son 6 531 casos, además de las 20 585 bases del relleno, y en todos coinciden el resultado, el código y el texto. Lo aleatorio de age, tlock y NewEnvelope sale de un ChaCha8 de semilla fija: el fichero sale igual en cada ejecución. El generador solo importa paquetes públicos de la referencia y llega a publicIP y headerEnd con go:linkname. Para regenerarlo, en una exportación de datekeys-go en spec-v0.12, para no leer cambios en curso: git -C ../datekeys-go archive spec-v0.12 | tar -x -C DIR, y desde DIR, go run .../scripts/locator-go-vectors.go -testdata .../testdata -out .../src/lib/dkc/testing.

  • src/lib/dkc/testing/locator-seal.json: lo que escriben Seal y NewEnvelope de Go mientras crypto/rand lee el keystream de una semilla (ChaCha20 bajo el SHA-256 de la semilla, nonce a cero), con cada valor que saca. Lo escribe scripts/locator-seal-go-vectors.go, y envelope.test.ts lo comprueba con seededFill de testing/seeded.ts, la misma fuente: seal y newEnvelope sacan los mismos valores en el mismo orden y escriben los mismos bytes en los 10 sellados, de uno a tres bloques y de la ronda 1 a la última de Quicknet, en los 8 sobres, de 0 bytes a 1 MiB, y en el camino entero; y rechazan las 12 entradas que rechaza Go, con su texto y antes de sacar nada. Se regenera como el anterior, con -source spec-v0.12.

  • src/lib/dkc/testing/locator-interop.json: Go abre lo que escribe esta librería. scripts/locator-ts-samples.mjs escribe los ficheros de las recetas de testing/locator-interop.ts, siete localizadores sellados con sus sobres, de un .dkc de 0 bytes a uno de 16 MiB y un byte, en el que el contador del nonce de STREAM pasa de un byte; scripts/locator-go-verdicts.go los abre con locator.Open, comprueba que Marshal da el texto sellado y que se usan todas sus direcciones, abre el sobre con OpenEnvelope y busca el resto escondido con Hide y RestIn. locator.interop.test.ts vuelve a escribir cada fichero de su receta, exige el SHA-256 que leyó Go y lo abre también. Para regenerarlo: node scripts/locator-ts-samples.mjs DIR, y desde la exportación de datekeys-go, go run .../scripts/locator-go-verdicts.go -source spec-v0.12 -testdata .../testdata -samples DIR > locator-interop.json.

  • src/lib/dkc/testing/zip-vectors.json: cómo lee archive/zip de Go los ZIP de la página. scripts/zip-ts-samples.mjs escribe con ZipSink las muestras de testing/zip.ts en un directorio: ficheros en carpetas con nombres fuera de ASCII y todas las clases de fecha (1970, 2³¹ − 1, 2³¹, 9999 y ninguna, que toma la hora de la ronda), un fichero dentro de una carpeta, y 65 535 ficheros, que hacen el ZIP64 por número de entradas. scripts/zip-go-read.go, solo con la biblioteca estándar, registra el SHA-256 de cada ZIP y una línea por entrada: nombre, bit 11, método, CRC-32, tamaños, fecha leída de los campos extra y SHA-256 del contenido, leído con el CRC comprobado. zipsink.test.ts vuelve a escribir cada muestra, exige su SHA-256 y calcula las líneas que Go tiene que dar. Para regenerarlo: node scripts/zip-ts-samples.mjs DIR > zip-samples.json y go run scripts/zip-go-read.go DIR zip-samples.json > zip-vectors.json.

  • src/lib/dkc/testing/mutation-texts.json: el texto del error de capsule.Open para cada caso de mutations.json, u ok. Lo escribe scripts/mutation-go-texts.go, que reproduce los casos como internal/testkit de la referencia, que un módulo de fuera no puede importar: el perfil de Quicknet o ninguno, una fuente con el release del caso, la .dkk ya decodificada, las identidades, las extensiones del caso con los textos de testkit.KnownExtensions y un sumidero que descarta. Comprueba cada código contra el corpus. Para regenerarlo, desde un módulo Go temporal como el de abajo: go run mutation-go-texts.go ../datekeys-ts/testdata > mutation-texts.json.

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

    El mismo tratamiento tiene src/lib/dkc/testing/tlock-vectors.json, los valores de referencia del cifrado (paso 7 de la fase 2), que escribe scripts/tlock-go-vectors.go y comprueba tlock.test.ts:

    • cifrados con sigma fijo de mensajes de 1, 16 y 32 bytes para las rondas 1000 y 1001. Go reescribe EncryptCCAonG2 porque kyber toma sigma de crypto/rand, y comprueba su reescritura descifrando con ibe.DecryptCCAonG2 y, en los de 16 bytes, con tlock.TimeUnlock. encryptOnG2WithSigma los reproduce byte a byte;
    • la interoperabilidad de TypeScript a Go. scripts/tlock-ts-samples.mjs cifra con esta librería un cuerpo IBE y un fichero age escrito con timeRecipient, para las rondas 1000 y 1001. Go abre los cuerpos con tlock.TimeUnlock y los ficheros con age.Decrypt y agewrap.NewTimeIdentity, la identidad del paso 11, y obtiene la misma file key y el mismo texto. Esas muestras son aleatorias, así que se congelan con el veredicto de Go.

    Para regenerarlo: node scripts/tlock-ts-samples.mjs > ts-samples.json, y desde el mismo módulo Go temporal, go run tlock-go-vectors.go ts-samples.json > tlock-vectors.json.

    En ibe-vectors.json, 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, y la directiva go de la referencia, para que se use su toolchain): go run ibe-go-vectors.go ../datekeys-ts/testdata/fixtures > ibe-vectors.json. Los siete fixtures del formato 2 se añadieron el 29-09-2026 así, sobre spec-v0.9, tomando solo el bloque fixtures: los valores de los cinco anteriores salieron idénticos, y el resto del fichero no cambió. Los nueve del formato 3 se añadieron igual el 30-09-2026, sobre spec-v0.10, con los doce anteriores idénticos. El 01-10-2026, sobre spec-v0.11, se añadieron format3_signed, format3_signed_cms y format3_sealed, y se rehicieron los dos de format3_signature_unsupported y format3_seal_unsupported, que Go regeneró con alg 4294967295; los demás salieron idénticos.

  • El writer (encrypt.test.ts, encrypt.stream.test.ts, encrypt.internal.test.ts, encrypt.property.test.ts):

    • con los valores de su registro, reproduce byte a byte el PRELUDE, PUBLIC_HEADER, header_binding y CONTROL_CBOR de los siete fixtures de formato 2 de Go, con las mismas longitudes; cada credencial cae en el hueco del registro y la .dkk sale igual, salvo su capsule_digest;
    • lo que escribe se abre con open: las dos políticas, de 1 a 16 credenciales, cada una sola y todas juntas; contenidos en todos los bordes de trozo y de relleno, hasta 5 000 000 de bytes, con las dos reglas; rondas 1000, 1001 y 2000;
    • las opciones inválidas dan los textos y códigos de capsule.Encrypt sin escribir nada y con la salida abortada;
    • streaming desde Uint8Array, Blob y ReadableStream con cualquier troceado; una fuente de otra longitud, una fuente o una salida que fallan y un progress que lanza dejan la salida abortada;
    • las autocomprobaciones, con entradas malas y con un age-encryption sustituido que falla, alarga, cambia o corta lo que sella;
    • un bucle de propiedades con opciones aleatorias, a veces inválidas: 50 semillas en cada ejecución y 500 con DATEKEYS_PROPERTY_SEEDS=500 (pasó el 29-09-2026, en 203 s).

    Rendimiento en Node 24.9, informativo: time_only, unos 120 ms por cápsula en caliente (470 ms la primera, con la carga del código); time_and_key con clave portable, unos 200 ms; 64 MiB desde un Blob hacia una salida, 50 MiB/s con el SHA-256 en la misma pasada.

  • src/lib/dkc/testing/authorkey-vectors.json: las claves de autor y la firma Ed25519, contra el paquete authorkey de Go y crypto/ed25519. Lo escribe scripts/authorkey-go-vectors.go, el generador del port de Dart con la salida de esta librería, y lo comprueban ed25519sign.test.ts y authorkey.test.ts:

    • 234 firmas: las líneas de sign.input de Go (SUPERCOP, con los tests 1 a 3 del RFC 8032, 7.1, y el de 1023 bytes), TEST SHA(abc), semillas y mensajes de 0 bytes a 1 MiB, y claves cuya mitad pública es otra, que Go usa tal cual;
    • la reducción módulo ℓ de 214 números de 64 bytes y 136 productos (a·b + c) mod ℓ, en las esquinas y al azar, con math/big;
    • NewFromSeed, Public, PublicString, Secret, Marshal y String de 24 semillas, y sus errores;
    • Generate y Encrypt con cada valor que lee crypto/rand, en su orden: encryptAuthorKey, con esos valores en crypto.getRandomValues, escribe el fichero de Go byte a byte, salvo la etiqueta al azar que saca el ScryptRecipient de Go y que age-encryption no saca;
    • ParsePublic y ParseSecret sobre 1 288 cadenas, como bytes: en otro caso o mezclado, de otras longitudes, con cada error de Bech32, otros prefijos, rellenos, claves no canónicas, fuera de la curva o de orden pequeño, y bytes que no son UTF-8;
    • 3 240 runas en los bordes de los conjuntos de unicode.ToLower, unicode.ToUpper y unicode.IsSpace de Go, en el prefijo y en los datos;
    • Read de 130 ficheros en claro y cifrados: comentarios, cada espacio de Go alrededor de la línea, bytes sueltos, los límites de 64 KiB y del bufio.Scanner, y ficheros cifrados con otra frase, otro factor, otro stanza, la cabecera editada campo a campo, el nonce o el STREAM cortados, o un MAC cambiado. Los ficheros grandes se guardan como receta, con los valores al azar de Go, y la prueba los vuelve a escribir con age-encryption.

    Para regenerarlo, en una exportación git archive de datekeys-go en el tag spec-v0.12, sin tocar el repositorio: las órdenes están en la cabecera del script. Todos los valores salen de Go, y cada ejecución escribe los mismos bytes.

  • src/lib/dkc/gounicode.ts lo escribe scripts/go-unicode-tables.go con el toolchain de Go 1.26 (go run scripts/go-unicode-tables.go -out src/lib/dkc/gounicode.ts), y comprueba sus tramos contra las funciones de Go en cada punto de código.

  • src/lib/dkc/testing/signing-vectors.json: los enganches del escritor contra capsule.EncryptFiles de Go, que comprueba encrypt.signing.test.ts. Lo escribe scripts/signing-go-vectors_test.go, que importa paquetes internos y corre como prueba en una exportación git archive de spec-v0.12:

    • 23 recetas de formato 3 en time_only, para la ronda 1000. Go escribe cada cápsula con crypto/rand leyendo un flujo ChaCha20 fijo, y guarda cada valor en su orden, y lo que recibió y devolvió cada enganche: una clave de autor de una semilla, y las firmas CMS y los tokens RFC 3161 de internal/cms/cmstest, cuyos ECDSA y RSA fija cryptotest.SetGlobalRandom;
    • con esos valores y esas firmas, encryptFiles escribe las ocho cápsulas de Go byte a byte: alg 1, alg 1 y un sello, un sello solo, uno posterior a la fecha (S5), alg 2 con dos firmantes sellados, alg 2 en un área de 64 KiB, largeArea sin ensanchar y un área de 512 bytes de generador de vectores. Pide la firma y el sello sobre los mismos mensajes, y lee en lo que escribe los veredictos y las líneas de capsule.Open, también con la clave guardada (F3). La prueba entrega a age-encryption y a ibe.ts los valores de Go por crypto.getRandomValues, sin el sellado de medida de Go, que esta librería calcula con una fórmula, ni las etiquetas al azar de sus recipients, que age-encryption no saca, y deja pasar el cegado de las multiplicaciones de noble, que no cambia ningún resultado;
    • las otras 15 fallan con el texto de Go: las exclusiones, el área de prueba con largeArea, una clave de 31 bytes, una firma de ceros (F2), una firma CMS sin un firmante exigido (F5, con el nombre), sin sellos o que no es una firma (F1), un área que no cabe en 32 KiB o en 64 KiB, SIGNERS vacío o repetido, y la aplicación de firma o la autoridad que fallan;
    • samples: cinco cápsulas que scripts/signing-ts-samples.mjs escribe con encryptFiles, sus propios valores al azar, un AuthorKey nuevo y los certificados, firmas y tokens de testing/cmsbuild.ts. capsule.Open de Go las abre a sus ficheros y les da los mismos veredictos y las mismas líneas que esta librería.

    Para regenerarlo: node scripts/signing-ts-samples.mjs > ts-signing.json, y la prueba de Go con -samples ts-signing.json; las órdenes están en la cabecera del script. Las cápsulas de Go salen igual en cada ejecución; las muestras son aleatorias y se congelan.

  • src/lib/dkc/testing/capsule-vectors.json: la interoperabilidad de los writers con Go a nivel de cápsula (plan de la fase 3, sección 8, punto 9, y paso 4 del plan del formato 3), que comprueba interop.test.ts. Se regeneró el 02-10-2026 con el escritor de la v0.11, contra spec-v0.11. scripts/capsule-ts-samples.mjs escribe con encryptVectors de testing/encrypt.ts, como generador de vectores, trece cápsulas de formato 2 para las rondas 1000, 1001 y 2000:

    • time_only de 0, 46, 65 536 y 78 000 bytes, con las dos reglas de relleno;
    • time_and_key con una clave portable, con tres recipients y una clave portable, y con dieciséis recipients;
    • una con extensiones en PUBLIC_HEADER, CONTROL_CBOR y la .dkk;
    • una para un instante un nanosegundo posterior a la ronda 1000.

    Y con encryptFiles, como lo llama cualquiera, ocho de formato 3, con el área de 32 KiB: un fichero con su mtime; un árbol de siete ficheros, uno sobre dos trozos STREAM, con rutas fuera de ASCII y el par U+FFFD y U+10000, que UTF-8 y UTF-16 ordenan al revés, con comentario y autor; un comentario sin ficheros; bloque256; time_and_key con tres destinatarios y una clave portable; extensiones del head; una nota pública fuera de ASCII; y una nota de 1024 bytes en time_and_key, junto a otra extensión no crítica de la cabecera.

    scripts/capsule-go-verdicts.go las pasa por capsule.Inspect y las abre con capsule.Open con cada credencial sola y con todas juntas: todas abren al mismo contenido, con el formato, L, la regla y P pedidos. En el formato 3 abre en un Sink, y los ficheros que recibe, con su SHA-256, el head reencodificado con capsule.EncodeHead, los veredictos y el tamaño del área son los de lo que se escribió. Además abre SEALED_CONTROL capa a capa con las identidades de agewrap, cuenta los 16 stanzas de INNER_ACCESS_AGE, reencodifica PUBLIC_HEADER, CONTROL_CBOR y la .dkk a los mismos bytes, y lee la nota pública con Header.PublicNote. El test comprueba esos veredictos y repite cada apertura con open sobre los bytes congelados, con el mismo resultado, y la lectura de la nota con publicNote. El fichero lleva también:

    • cuatro mezclas de dos cápsulas de la ronda 1000, que Go y open rechazan con el mismo código en el mismo paso: ERR_HEADER_BINDING en el 15 (dos de ellas), ERR_INTEGRITY en el 17 y ERR_POLICY_STRUCTURE_MISMATCH en el 12;
    • el diferencial de los codificadores: 500 entradas válidas sacadas de una semilla (testing/interop.ts), que capsule.EncodeHeader, capsule.EncodeControl y AccessKey.MarshalBody codifican a los mismos bytes que esta librería. Se congelan la semilla, el número de entradas y el SHA-256 de todas las codificaciones, que el test recalcula;
    • 22 cadenas de recipient, que parseX25519Recipient y checkX25519Recipient leen con los textos de age.ParseX25519Recipient y agewrap.CheckX25519Recipient;
    • las 22 opciones inválidas de encrypt que tienen equivalente en Go, una nota pública en el formato 2 entre ellas, con el texto de capsule.Encrypt como generador de vectores;
    • las 9 notas públicas que rechaza encryptFiles, con el texto de capsule.EncryptFiles. La de UTF-8 inválido, un sustituto suelto en TypeScript, no cabe en JSON; su texto lo fija note.test.ts.

    Las cápsulas son aleatorias, así que se generan una vez y se congelan con los veredictos de Go. Para regenerarlo: node scripts/capsule-ts-samples.mjs > ts-samples.json, y desde el mismo módulo Go temporal, go run capsule-go-verdicts.go ts-samples.json > capsule-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. La página solo las carga al abrir una cápsula (ver Abrir).

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; y el stanza scrypt del fichero de una clave de autor (setPassphrase, setScryptWorkFactor y addPassphrase), cuyo scrypt de @noble/hashes ya iba en el bundle: authorkey.ts no importa ningún módulo nuevo
@noble/curves 2.4.0 MIT BLS12-381 del núcleo IBE y de la verificación de releases, X25519 de los stanzas, la aritmética de Ed25519 de la firma de alg 1 (ed25519strict.ts) y ECDSA sobre P-256, P-384 y P-521 de las firmas y los sellos con certificados (cms.ts); también el oráculo de bls12381.contrast.test.ts
@noble/hashes 2.4.0 MIT los hashes del IBE, el HKDF de los stanzas X25519 y del STREAM de un fichero de clave de autor, el SHA-256 incremental de digest.ts, lo que se firma (author.ts), el SHA-512 de Ed25519, al verificar y al firmar (ed25519sign.ts), y SHA-1 y SHA-2 de CMS (cms.ts); se declara porque se importa directamente
@noble/ciphers 2.4.0 MIT el ChaCha20-Poly1305 de los stanzas X25519 (x25519.ts) y del STREAM de un fichero de clave de autor (authorkey.ts): la misma copia que usa age-encryption, así que no añade nada al bundle. Aprobada el 28-09-2026 para abrir los stanzas de uno en uno, como exige §36

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 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 ageio.ts, author.ts, authorkey.ts, cms.ts, digest.ts, ed25519sign.ts, ed25519strict.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;
  • solo agefile.ts, authorkey.ts, open.ts, tlock.ts, writer.ts y los tests importan age-encryption; solo encrypt.ts, testing/ y los tests importan writer.ts, cuyo núcleo recibe la aleatoriedad de quien lo llama (plan de la fase 3, decisiones 4 y 13); e index.ts no reexporta la apertura, el writer, los módulos de la v0.11 ni nada de testing/;
  • solo los tests y testing/ mismo importan testing/, cuyos ayudantes escriben el formato 2 y otra área, lo que solo puede escribir un generador de vectores;
  • 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, del que además excluye los tests y testing/.

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.

En el sitio, según check-build.mjs el 28-09-2026:

/inspect JavaScript gzip
Primera carga, antes del paso 8 unos 157 KB 58,7 KB
Primera carga, con la acción "abrir" 187 700 B 68 197 B
Bajo demanda, al abrir: opener.ts, open.ts, noble y age-encryption 183 747 B 66 882 B

Y según check-build.mjs el 29-09-2026, con la página de crear, y el 30-09-2026, con el formato 3:

/create JavaScript gzip
Primera carga, con el formulario y el informe de los pasos 1 a 8 208 755 B 76 942 B
Bajo demanda, al crear: creator.ts, encrypt.ts, noble y age-encryption 212 685 B 76 617 B
Primera carga, con la lista de ficheros (formato 3) 224 367 B 82 345 B
Bajo demanda (formato 3): lo anterior, create-check.ts y las tablas de las rutas 353 788 B 125 503 B

Los 9,5 KB con gzip que crece la primera carga de /inspect son la interfaz de la apertura (OpenPanel.svelte) y sus módulos sin noble. Las cifras exactas cambian 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

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

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 (el tag spec-v0.14 de datekeys-go, 39b2033), que se sincroniza con node scripts/sync-testdata.mjs sync --commit spec-v0.14.

Licencia

Apache-2.0 (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 núcleo IBE (ibe.ts), derivado de tlock-js (Apache-2.0 OR MIT, usado bajo MIT), y bech32.ts, portado de age (MIT). El sitio publica esos avisos y los de sus paquetes npm en licenses.txt.

Powered by TurnKey Linux.