# Plan: codec CBOR propio y página de prueba en Svelte Estado: v2, 25 de septiembre de 2026. Sustituye a la v1 (conservada en `AppOld/docs`) con la revisión, el panel sobre `data` y las decisiones 2, 3 y 4. La decisión 1 queda pendiente de confirmar el límite N (sección 3). Alcance: sustituir la dependencia CBOR externa de `datekeys-go` por un codec propio, escribir el mismo codec en TypeScript en el proyecto nuevo `App`, y construir sobre él la página de prueba: primero un inspector de cápsulas, después cifrado y descifrado en el navegador. Regla de dependencias del proyecto: en ejecución, solo `age`, `drand`, `tlock` y lo que ellas arrastran, en cualquier lenguaje. El tooling de desarrollo queda fuera de la regla, pero solo se admite el de la lista de la sección 13. Toda dependencia nueva, de ejecución o de tooling, se propone por escrito y no se instala sin aprobación. --- ## 1. Situación de partida | Elemento | Estado verificado el 25-09-2026 | |---|---| | `datekeys-go`, paquete `codec` | Usa `github.com/fxamacker/cbor/v2` v2.9.4, que arrastra `github.com/x448/float16`. Es la única dependencia directa fuera de la regla. | | Uso de esa librería | `codec` (Marshal, Unmarshal, Peek, CheckSchema, Valid) y structs con etiquetas `cbor:"N,keyasint"` en `profile`, `capsule`, `accesskey`, `extension` e `internal/testkit`. `extension.Wire.Data` es `cbor.RawMessage`. | | Fixtures y vectores | Cinco cápsulas `.dkc`, dos `.dkk`, vectores de ronda, `dk1_` y `profile_hash`. Los vectores se regeneran byte a byte. Los `.dkc` se generaron una vez y están congelados: la aleatoriedad de `age` no se puede inyectar, así que regenerar una cápsula cambia `capsule_id`, las claves y todos sus bytes. | | Proyecto TypeScript | `App`, nuevo y separado. Tiene un esqueleto sin dependencias instaladas, los planes en `docs/` y `testdata/` copiado del commit `5719f6a` de `datekeys-go` con `scripts/sync-testdata.mjs`, que verifica cada fichero contra `testdata/SOURCE.json`. | | Prototipo | `AppOld`: API Quicknet en Go, CLI tlock y cliente Svelte (`web-client`), commit `4d2b0a1`. No se modifica. | | `web` | Landing estática de datekeys.com. Fuera de este plan. | Fallos de la librería actual que este plan corrige, todos reproducidos el 25-09-2026: | Fallo | Reproducción | Se corrige en | |---|---|---| | `CheckDisjoint` es cuadrático y `DecodeHeader` lo ejecuta antes de comprobar las extensiones críticas | una PUBLIC_HEADER de 880 KB con 40 000 + 40 000 extensiones se decodifica sin error en 8,3 s, sin red ni secretos | paso 1 (máximo de 64 por array) y paso 2a (fusión lineal) | | Asimetría entre sellar y abrir | `Encrypt` sella `data` de control con 14 o 15 arrays anidados; al llegar la fecha, `Open` la rechaza en el paso 14 (`exceeded max nested level 16`) y la cápsula ya no se puede abrir | paso 2a (`data` pasa a ser una hoja) y autocomprobación del encoder | | Veredicto no determinista | la misma cápsula de 810 bytes, con `data` de cabecera `{NaN:0, NaN:1}`, la acepta `Inspect` 437 veces de 500 y `Open` 82 de 100 | paso 2a (fxamacker sale del camino de `data`) | | `extension.New(id, v, nil)` emite `f6` | contradice §58.1 | paso 2a (`New` recibe `[]byte` y rechaza vacío) | --- ## 2. Subconjunto CBOR del protocolo Del CDDL `spec/datekeys.cddl` se deduce que el protocolo solo usa: | Tipo CBOR | Tipo mayor | Uso | |---|---|---| | Entero sin signo | 0 | versiones, claves de mapa, política, periodo, genesis, versión de extensión | | Cadena de bytes | 2 | identificadores, hashes, claves, `capsule_digest`, `data` de extensión | | Cadena de texto | 3 | etiquetas de tipo, ids, DateKey compacta, `access_type` | | Array | 4 | listas de extensiones | | Mapa con claves enteras | 5 | todos los objetos | El spec lo nombra en §58 como perfil del protocolo: RFC 8949 §4.2.1 restringido a los tipos mayores 0, 2, 3, 4 y 5, con claves enteras sin signo. Con claves enteras coinciden el orden por bytes, el orden por longitud y el orden numérico, así que no hay ambigüedad de perfil. Reglas que el codec impone al decodificar y al codificar: - enteros y longitudes en su forma más corta; - longitudes definidas; - claves de mapa en orden ascendente estricto, sin duplicados; - mapas cerrados: toda clave no prevista por el schema se rechaza; - texto UTF-8 válido; - tamaños acotados por el schema y por los límites de §57, que pasan a ser MUST (sección 9); - como mucho tres niveles de contenedores (objeto, array de extensiones y extensión). `data` es una hoja y el codec nunca desciende dentro. Se rechazan con `ERR_NON_CANONICAL_CBOR`: enteros negativos, flotantes, tags, valores simples, longitudes indefinidas y claves no enteras. Los límites de tamaño usan el código que decida la sección 9. Tras decodificar, el decodificador reencodifica y compara byte a byte con la entrada. Es una defensa independiente de la decodificación estricta, y ahora cubre también `data`. --- ## 3. Decisión normativa: `data` de las extensiones **Tipo: `bstr` no vacío.** Bytes opacos para el protocolo base. Un panel comparó cuatro opciones con tres jueces independientes (seguridad, interoperabilidad y gobernanza del spec) y un red team contra la ganadora: | Opción | Puntos (máx. 30) | |---|---| | A. `bstr` opaco no vacío | 25,5 (ganadora para los tres jueces) | | B. Subconjunto estructurado recursivo | 19 | | C. `any` fijado a un perfil preciso (CDE o dCBOR) | 11 | | D. `any` como hoy, fijando el comportamiento de fxamacker | 6,5 | A gana porque: - el protocolo base no analiza nada de `data`, sin superficie de parser ni recursión; - un lector nunca analiza la `data` de extensiones que no conoce; - la validez base es una comprobación de longitud, idéntica en todas las implementaciones. Es el diseño de X.509 (`extnValue` OCTET STRING) y TLS (`extension_data` opaco). B es la rival seria, pero: - obliga a cada lector a validar datos que §54 dice que no interpreta; - congela el espacio de valores; - sus ventajas (canonicidad interna, vista estructurada, comprobación al sellar) se recuperan en A mediante la regla de registro de la sección 9. El red team no refutó el tipo. **Justificación §76.** Se basa en casos reproducibles, no en el tamaño del código: 1. El CDDL se contradice: las líneas 12 a 14 dicen que `null` y los vacíos nunca representan ausencia, y la línea 82 declara `? 2 => any`. 2. `extension.New(id, v, nil)` emite `f6`. 3. El veredicto no determinista con claves NaN. 4. El fixture oficial `time_only_extensions` usa claves de texto y `true`. 5. La asimetría de profundidad: una cápsula sellada que no se abre tras la espera. Es el caso decisivo. **Límite de tamaño: pendiente de confirmar.** El panel propuso N = 65 535 (el límite de TLS). El red team mostró que ninguna evidencia sostiene un N concreto, y que un N uniforme de 64 KiB bloquearía el modo `opaque_onion` del Dead Man Switch de los borradores v0.1 y v0.2, si ese servicio llevara cada capa anterior en una extensión de control. Según una simulación del red team con la librería actual, crece unos 477 bytes por renovación y se bloquea en la renovación 139. El diseño del Dead Man Switch sigue abierto, así que el argumento es condicional. Opciones: 1. **Recomendada:** sin N propio. `data` queda acotada por la trama de su contenedor (§57, que pasa a MUST), y cada extensión registrada declara su máximo en §72. 2. Límites por contenedor: 65 535 en PUBLIC_HEADER (pública y leída antes del desbloqueo) y la trama en CONTROL_CBOR y `.dkk`. 3. N uniforme de 65 535, registrando en §76 que limita el `opaque_onion`. --- ## 4. Diseño del codec en Go Paquete `codec` reescrito sin reflexión y sin etiquetas de struct. ```go // Codificación: se acumula en un []byte. El primer error queda fijado y // Err lo devuelve; las llamadas posteriores no hacen nada. type Encoder struct{ buf []byte; err error } func (e *Encoder) Map(pairs int) func (e *Encoder) Array(items int) func (e *Encoder) Uint(v uint64) func (e *Encoder) Bstr(b []byte) func (e *Encoder) Text(s string) // rechaza UTF-8 inválido func (e *Encoder) Out() ([]byte, error) // Decodificación: cursor estricto sobre la entrada. El Decoder guarda la // última clave de cada mapa abierto y exige orden ascendente estricto. type Decoder struct{ /* entrada, posición, pila de últimas claves */ } func (d *Decoder) Map(max int) (pairs int, err error) func (d *Decoder) Key() (uint64, error) func (d *Decoder) EndMap() error func (d *Decoder) Array(max int) (items int, err error) func (d *Decoder) Uint(max uint64) (uint64, error) func (d *Decoder) Bstr(min, max int) ([]byte, error) // longitud comprobada contra lo que queda antes de copiar func (d *Decoder) Text(max int) (string, error) func (d *Decoder) Done() error // rechaza bytes sobrantes // Canonicidad: decodifica con decode, reencodifica con encode y compara. func Unmarshal(in []byte, decode func(*Decoder) error, encode func(*Encoder)) error // Lee las claves 0 y 1 de un mapa (tipo y versión) antes de la // decodificación estricta (spec §70). func Peek(in []byte) (typeTag string, version uint64, err error) // Recorredor genérico del subconjunto, con profundidad y tamaño acotados. // Es auxiliar: vectores compartidos, fuzzing, vista informativa del // inspector y extensiones registradas cuyo `data` sea CBOR. Nunca decide // la validez base de un objeto. func Walk(in []byte, maxDepth, maxLen int) error ``` Cada schema escribe su codificación y decodificación a mano, en su paquete, en sustitución de los structs `wire`: | Schema | Paquete | Claves | |---|---|---| | Provider Profile | `profile` | 0 a 10 | | PUBLIC_HEADER | `capsule` | 0 a 4, opcionales 5 y 6 | | CONTROL_CBOR | `capsule` | 0 a 3, opcionales 4 y 5 | | `.dkk` body | `accesskey` | 0 a 5, opcionales 6, 7 y 8 | | `verification_metadata` | `accesskey` | 0 | | extensión | `extension` | 0, 1, opcional 2 (`bstr` no vacío) | Reglas comunes: - una clave opcional ausente se omite; - un array vacío, un mapa vacío o un `h''` en una clave opcional es un error explícito, no un efecto de `omitempty`; - la etiqueta de tipo y la versión se comprueban antes de decodificar el resto; - los errores envuelven el sentinel de §69 que corresponda. Cambios en `extension`: - `New` recibe `[]byte` y rechaza vacío o fuera de límite; - el array se rechaza por su cabecera si supera 64 elementos, antes de iterar; - `CheckDisjoint` se sustituye por una fusión lineal de los dos arrays ordenados. Autocomprobación del encoder: `Encrypt`, `EncodeHeader`, `EncodeControl` y el encoder de `.dkk` decodifican lo que producen con el decodificador del lector antes de sellar o escribir. Hay un fuzz target que exige que todo encode correcto implique un decode correcto. `go.mod` pierde `github.com/fxamacker/cbor/v2` y `github.com/x448/float16`. No entra nada a cambio. --- ## 5. Diseño del codec en TypeScript Mismo diseño y misma API, en `App/src/lib/dkc/cbor.ts`, sobre `Uint8Array`, sin dependencias: - texto con `TextDecoder('utf-8', { fatal: true, ignoreBOM: true })`; sin `ignoreBOM`, un BOM inicial desaparece y TS rechaza lo que Go acepta; - `Text` del encoder comprueba `isWellFormed()` antes de codificar, porque `TextEncoder` sustituye en silencio un surrogate suelto por `efbfbd`; - los `extension_id` se comparan por sus bytes UTF-8, nunca como cadenas JavaScript, que comparan en orden UTF-16 (el par U+FF61 y U+10000 sale al revés); un port ingenuo divergió en 782 de 200 000 entradas; - enteros siempre como `number`: todos los `uint` del CDDL quedan acotados por debajo de 2⁵³ (sección 9), y el decodificador rechaza lo que supere `Number.MAX_SAFE_INTEGER`; - `data` se devuelve como `Uint8Array`; `undefined` significa clave ausente. Los parsers de schema van en el mismo directorio: `profile.ts`, `header.ts`, `control.ts`, `accesskey.ts`, `extension.ts`, `datekey.ts` e `inspect.ts`, que cubre los pasos 1 a 8 de §63. `datekey.ts` no porta el `roundForTime` del prototipo. Ese código usa `Date.parse`, que trunca a milisegundos, y falla el vector "genesis + 1ns" de `quicknet_rounds.json` (da la ronda 1 en vez de la 2). Hace falta un parser RFC 3339 con precisión de nanosegundos. La cabecera `age` de OUTER_TIME_AGE y PAYLOAD_AGE (pasos 5, 6 y 8) se analiza con un parser estricto según la gramática de `age`: stanzas, cuerpo base64 canónico, líneas de 64 columnas y línea MAC, sobre los ficheros binarios que localizan las longitudes del prelude. El filtro de líneas `-> ` del prototipo es demasiado laxo. --- ## 6. Vectores compartidos entre lenguajes Fichero `testdata/vectors/cbor.json` en `datekeys-go`, generado desde Go y congelado. `App` lo recibe con `sync-testdata`. Los casos genéricos se ejecutan con `Walk`; los de schema, con el decodificador de cada schema. Los enteros por encima de 2⁵³ van como cadena decimal. ```json { "accept": [ { "name": "uint 23 inline", "hex": "17", "value": 23 }, { "name": "uint 24 one byte", "hex": "1818", "value": 24 }, { "name": "uint 256 two bytes", "hex": "190100", "value": 256 }, { "name": "uint 2^32 eight bytes", "hex": "1b0000000100000000", "value": 4294967296 }, { "name": "empty bstr", "hex": "40" }, { "name": "map two sorted keys", "hex": "a200010101" }, { "name": "text with leading BOM", "hex": "64efbbbf61" } ], "reject": [ { "name": "uint 23 with one extra byte", "hex": "1817", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "bstr length not shortest", "hex": "5800", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "keys out of order", "hex": "a201000001", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "duplicate key", "hex": "a200000001", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "text key", "hex": "a1616100", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "indefinite array", "hex": "9f01ff", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "tag", "hex": "c101", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "float", "hex": "f97e00", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "true", "hex": "f5", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "null", "hex": "f6", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "negative int", "hex": "20", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "truncated uint", "hex": "1901", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "length beyond input", "hex": "5affffffff", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "trailing byte", "hex": "0100", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "invalid UTF-8", "hex": "61ff", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "overlong UTF-8", "hex": "62c080", "error": "ERR_NON_CANONICAL_CBOR" }, { "name": "UTF-8 surrogate", "hex": "63eda080", "error": "ERR_NON_CANONICAL_CBOR" } ] } ``` Además, un bloque por schema: objeto válido mínimo, clave desconocida, clave obligatoria ausente, tipo incorrecto y tamaño fuera de límite. Casos de extensión: - `data` con `40`, con `5801xx` y en el límite de la sección 3 y uno más; - `data` con tipo distinto de `bstr`: texto, `null`, `uint`, mapa, tag y longitud indefinida; - 64 y 65 extensiones en un array; - un `extension_id` que empieza por BOM; - el par de orden U+FF61 y U+10000; - `extension_version` en 2⁵³ − 1 y 2⁵³. --- ## 7. Tests 1. **Regresión byte a byte.** Tras el paso 2b, `profile_hash` de Quicknet, los hex de prelude, cabecera y control de las cinco cápsulas, y los dos `.dkk` completos tienen que ser idénticos a los fixtures congelados, sin ninguna excepción: el cambio de `data` y la regeneración de su fixture ocurren antes, en el paso 2a. 2. **Asertos sobre los bytes exactos de `data`** en los tests de conformidad, no solo sobre su número (`conformance_test.go:165` y `205`, `accesskey_test.go:54`). 3. **Vectores compartidos** de la sección 6, en Go y en TypeScript. 4. **Corpus de mutaciones exportado.** Los 45 casos de `capsule/mutation_test.go` (23 de ellos en los pasos 1 a 8 y sin red) se exportan como vectores congelados: bytes, código de error y paso. TypeScript tiene que reproducir el código y el paso de cada uno. Además, un corpus diferencial de mutaciones aleatorias con semilla fija y el veredicto de Go. 5. **Fuzzing en Go** de `Decoder`, de `Walk`, de cada decodificador de schema y de la propiedad "encode correcto implica decode correcto". El corpus de mutación existente sigue pasando. 6. **TypeScript con `vitest`.** Carga los fixtures de `App/testdata` y compara `inspect` con la salida congelada de `datekeys inspect -json` de cada fixture, generada en Go y guardada en `testdata`; los tests no ejecutan Go. Pasa `quicknet_rounds.json`, `dk1.json` y `profile_quicknet.json`. 7. **Cobertura** del codec al 100 % en ambos lenguajes. En TypeScript necesita `@vitest/coverage-v8` (sección 13). --- ## 8. La página de prueba en Svelte En el proyecto `App`, como sitio estático, sin servidor. **Fase 1, inspector.** Ruta `/inspect`. Carga un `.dkc` desde disco o uno de los fixtures, ejecuta los pasos 1 a 8 y muestra, como la CLI Go, el resultado de cada paso: prelude, cabecera, política, DateKey, fecha de apertura, argumentos del stanza `tlock` frente al perfil pinneado y recuento de stanzas de OUTER_TIME_AGE y PAYLOAD_AGE. Sin secretos. Una CSP con `connect-src 'self'` garantiza que la página no habla con ningún otro origen. Contrato de las extensiones: - id, versión, crítica o no, conocida o no, longitud y hex siempre; - una vista de texto si los bytes son UTF-8 imprimible; - una vista decodificada con `Walk` si los bytes son CBOR del subconjunto, marcada como informativa y no validada por el protocolo; - todo el texto escapado; - nunca se construyen objetos ni `Map` de JavaScript a partir de claves (`__proto__`, colisiones por encima de 2⁵³); - la `data` de PUBLIC_HEADER y `.dkk` se marca como pública y no autenticada hasta el paso 15. **Fase 2, cifrar y descifrar.** Solo cuando la fase 1 pase con todos los fixtures y el corpus exportado. Necesita `age-encryption` 0.3.1, la librería `age` oficial en TypeScript, que admite `Recipient` e `Identity` propios. Arrastra `@noble/ciphers`, `@noble/curves` 2, `@noble/hashes` 2, `@noble/post-quantum` y `@scure/base`. Como `tlock-js` 0.9 sigue en `@noble/curves` 1.9.7 y `@noble/hashes` 1.8.0, el bundle tendrá dos versiones mayores de noble. La aprobación se decide al empezar la fase 2 (decisión 4). Piezas de `tlock-js` y `drand-client`: sustituido el 26-09-2026 por `PLAN_fase2_ibe_noble2.md`. El bloque original nombraba `decryptOnG1` (para Quicknet es `decryptOnG2`) y daba a `drand-client` la verificación de releases; ninguno de los dos paquetes entra ya en el bundle: el núcleo IBE se escribe en el proyecto sobre `@noble/curves` 2.x y los releases se verifican con noble y el perfil pinneado. Las identidades `tlock` y X25519 aplican la cardinalidad de §63 (pasos 11, 13 y 17) dentro de `unwrapFileKey`, como en Go. La prueba de aceptación descifra un fixture `time_and_key` en el navegador con la firma embebida, sin red. --- ## 9. Cambios en la especificación y el CDDL Todos en un único cambio normativo, con la justificación §76 de la sección 3: - **Extensiones.** - CDDL: `extensions = [1*64 extension]` y `extension = { 0 => extension-id, 1 => extension-version, ? 2 => bstr .size (1..MAX) }`, con MAX según la opción elegida en la sección 3. - §31 y §54: `data` es una cadena de bytes opaca. El protocolo base nunca la decodifica ni la valida, y la validez de PUBLIC_HEADER, CONTROL_CBOR y `.dkk` nunca depende de su contenido. Una extensión sin datos omite la clave 2, y `h''` es inválido. - §31 y §54: se elimina la excepción de multiplicidad ("salvo que el schema registrado permita multiplicidad"), que contradice al CDDL y a la implementación de referencia. La multiplicidad va dentro de `data`. - §54: una extensión crítica conocida con `data` inválida se rechaza con un código definido (nuevo en §69, o uno existente nombrado expresamente). Una no crítica conocida con `data` inválida se marca como inutilizable y el objeto sigue siendo válido. - **Enteros.** `extension_version`, `period` y `genesis_time` acotados, como mucho a 2⁵³ − 1; para `extension_version` basta con 2³² − 1. En TypeScript, 2⁵³ + 1 se confunde con 2⁵³, y una versión crítica desconocida podría pasar por conocida. - **§57.** Los límites pasan de "V1 recomienda" a MUST para encoders y decoders. Un único código de error para toda violación de límites; la referencia usa hoy `ERR_INTEGRITY` para las tramas. - **§58.** Nombra el perfil de la sección 2, aplica también a la cabecera del `bstr` de `data` y nunca a su contenido. - **§72.** - Cada extensión registrada declara la codificación de su `data`, su forma canónica, su longitud máxima y sus vectores. - Si la codificación es CBOR, usa el perfil de §58 de forma recursiva, con contenedores no vacíos y sin `null`. - Sus lectores rechazan codificaciones no canónicas, y su encoder decodifica lo que produce antes de sellar. - Las extensiones de firma firman los bytes exactos de `data`. - **§64.** Mutaciones nuevas: `data` con tipo incorrecto, `data` vacía, `data` por encima del límite y 65 extensiones. - **§68.** Un fixture `.dkk` con una extensión. - **§74.** Se retira "formato exacto de extensiones" de lo provisional. - **Documentación.** Se actualizan `docs/traceability.md` (decisión 6) y `CHANGELOG.md`, que recoge la rotura de API: `extension.New` recibe `[]byte`, y `data` distinta de `bstr` o vacía deja de ser válida. El fixture se regenera en el mismo commit. --- ## 10. Orden de trabajo y criterios de aceptación | Paso | Contenido | Hecho cuando | |---|---|---| | 1 | Cambios de spec y CDDL de la sección 9 | texto aprobado | | 2a | Con fxamacker todavía dentro: `data` como `bstr`, `New([]byte)`, máximo de 64, fusión lineal, autocomprobación del encoder. Se regenera `time_only_extensions` con `-force` (cabecera: los bytes de "public label", que no son CBOR; control: un ejemplo CBOR del subconjunto) y se añade el `.dkk` con extensión. | tests verdes; los tres fallos de la sección 1 tienen test de regresión; commit en Gitea | | 2b | Codec propio en Go y baja de fxamacker | cero cambios en `testdata/`; regresión completa de la sección 7; `govulncheck` sin cambios; una noche de fuzzing limpia; commit en Gitea | | 3 | Vectores CBOR, corpus de mutaciones exportado y salidas de `inspect -json` congeladas | Go los genera y los pasa; ficheros congelados | | 4 | En `App`: `sync-testdata`, codec y parsers TypeScript, `inspect.ts` | `vitest` reproduce fixtures, vectores y corpus exportado; cobertura del codec al 100 % | | 5 | Ruta `/inspect` | abre los fixtures y un `.dkc` arrastrado; la CSP bloquea cualquier otro origen. `npm run verify`: `svelte-check`, `tsc`, cobertura de `src/lib/dkc` y `src/lib/inspector`, y `build` con `scripts/check-build.mjs` (CSP de cada página prerenderizada, fixtures byte a byte, ningún secreto en el sitio) | | 6 | Fase 2 | `age-encryption` aprobada; un fixture `time_and_key` se descifra en el navegador | --- ## 11. Riesgos - **Dos codecs propios** en vez de una librería auditada por muchos. Mitigación: subconjunto mínimo, `data` opaca, vectores compartidos, regresión byte a byte sin excepciones, corpus de mutaciones exportado y fuzzing. - **Desviación entre Go y TypeScript.** Los fixtures y vectores del Go son la verdad; TypeScript nunca genera fixtures propios, y `testdata/SOURCE.json` fija el commit de origen y el hash de cada fichero. - **Parser estricto de cabecera `age` en TypeScript.** Cubierto por las mutaciones de los pasos 5, 6 y 8. - **`data` opaca aplaza la validación a cada extensión.** Es el tipo de fallo de CVE-2020-1971. Mitigación: la regla de §72, el perfil con nombre y el recorredor compartido. - **Límite de `data`.** Si se elige un N uniforme, puede limitar servicios futuros, como el `opaque_onion` del Dead Man Switch. --- ## 12. Decisiones 1. **`data`:** `bstr` no vacío (recomendado, sección 3). Pendiente de confirmar el límite: la trama del contenedor (recomendado), límites por contenedor o un N uniforme de 65 535. 2. **Proyecto TypeScript:** aparte, en `App`. El prototipo queda en `AppOld`. Decidido. 3. **Regla de dependencias:** afecta a las de ejecución. El tooling de desarrollo se admite solo desde la lista explícita de la sección 13. Decidido. 4. **`age-encryption` 0.3.1:** se decide al empezar la fase 2. Decidido. --- ## 13. Tooling de desarrollo de `App` | 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` (sección 7) | | `@types/node` | 24.13.6 | en `package.json`; tests que leen `testdata/` | | `svelte`, `@sveltejs/kit`, `@sveltejs/vite-plugin-svelte`, `vite`, `svelte-check` | 5.57.1, 2.70.3, 7.3.0, 8.3.0, 4.7.6 | en `package.json`; paso 5 | | `@sveltejs/adapter-static` | 3.0.10 | en `package.json`; paso 5, sitio estático | | `@noble/curves` | 2.4.0 | solo desarrollo; oráculo auditado del test de contraste de `bls12381.ts` (decidido el 26-09-2026: se mantiene nuestra implementación y se contrasta en cada ejecución). En la fase 2 pasará a ser la dependencia BLS de ejecución, fijada a 2.3.0 o posterior | 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')`).