diff --git a/.claude/launch.json b/.claude/launch.json
new file mode 100644
index 0000000..5fd8c39
--- /dev/null
+++ b/.claude/launch.json
@@ -0,0 +1,17 @@
+{
+ "version": "0.0.1",
+ "configurations": [
+ {
+ "name": "inspector-preview",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": ["run", "preview", "--", "--port", "4173", "--strictPort"],
+ "port": 4173
+ },
+ {
+ "name": "inspector-dev",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": ["run", "dev", "--", "--port", "5188", "--strictPort"],
+ "port": 5188
+ }
+ ]
+}
diff --git a/README.md b/README.md
index a91298d..107173f 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ La implementación de referencia es la librería Go `g.activething.com/go/DateKe
| 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, ruta `/inspect` | 5 | pendiente |
+| Página inspector, sin red | SvelteKit estático: `src/routes/`, `src/lib/inspector/`, `src/lib/components/` | 5 | hecho |
| Cifrado y descifrado en el navegador | fase 2 | 6 | pendiente |
## `src/lib/dkc`
@@ -29,7 +29,7 @@ Sin dependencias de ejecución. Funciona en navegadores y en Node 20+: solo usa
| `datekey.ts` | `dk1_` canónico, ronda desde una fecha con precisión de nanosegundos, parser RFC 3339 equivalente a `time.Parse(time.RFC3339Nano, …)` | `datekey` |
| `header.ts`, `control.ts`, `accesskey.ts` | PUBLIC_HEADER, CONTROL_CBOR y `.dkk` (cuerpo y trama), decodificar y codificar | `capsule`, `accesskey` |
| `framing.ts` | Prelude DKC1 (16 bytes) y DKK1 (12 bytes), longitudes, límites de §57 y troceo de secciones | `capsule/framing.go` |
-| `age.ts` | Parser estricto de la cabecera `age` v1 sobre los ficheros binarios, con los textos de error de `age`; reglas de stanzas | `agewrap`, `filippo.io/age/internal/format` |
+| `age.ts` | Parser estricto de la cabecera `age` v1 sobre los ficheros binarios, con los textos de error de `age`; reglas de stanzas; `MAX_AGE_HEADER_LEN` (2 MiB), el límite que usa la página para leer solo el prefijo de un `.dkc` grande | `agewrap`, `filippo.io/age/internal/format` |
| `inspect.ts` | Pasos 1 a 8 de §63 y la vista JSON de `datekeys inspect -json` | `capsule/inspect.go`, `cmd/datekeys` |
| `index.ts` | Reexporta todo | |
| `testing/` | Solo para tests: lectura de `testdata/`, constructores de CBOR en hex, cirugía de cápsulas | |
@@ -53,10 +53,61 @@ El lector de schema (`peek`) reproduce a propósito lo que acepta `codec.Peek` d
Secretos: `access_material` de un `.dkk` e `I_PAYLOAD` de CONTROL_CBOR se borran en todos los caminos, también cuando la decodificación falla a medias o `unmarshal` rechaza el valor, como el `clear` diferido de Go.
+## Página inspector
+
+Sitio SvelteKit estático (`@sveltejs/adapter-static`, `strict`): las dos páginas se prerenderizan a HTML y no hay código de servidor.
+
+| Ruta | Contenido |
+|---|---|
+| `/` | Qué es el inspector y qué garantiza; enlaza con `/inspect` |
+| `/inspect` | El inspector |
+
+`/inspect` carga un `.dkc` con el selector de ficheros, soltándolo en cualquier parte de la página o desde la lista de fixtures oficiales, y ejecuta `inspect` (pasos 1 a 8 de §63) sin registro de extensiones, como `datekeys inspect`. Muestra:
+
+- cada paso con su número, su nombre de la CLI, `superado` o el código normativo, y el detalle (los caracteres invisibles o de control se escriben como `\uXXXX`);
+- el veredicto, `capsule_id`, la DateKey compacta y decodificada (red y ronda), el perfil fijado, la fecha de apertura en UTC y en la hora local del navegador, `access_policy`, los campos del prelude, los argumentos del stanza `tlock` frente al perfil fijado y el número y tipo de stanzas de OUTER_TIME_AGE y PAYLOAD_AGE;
+- las extensiones de PUBLIC_HEADER según el contrato del plan §8: id (entre comillas y escapado si tiene caracteres no imprimibles), versión, crítica o no, conocida o no, longitud y hex (plegado si pasa de 64 bytes); texto si los bytes son UTF-8 imprimible; vista CBOR con `walk` si son un ítem del perfil de §58, marcada "informativo, no validado por el protocolo"; y el aviso de que son públicas y no están autenticadas hasta el paso 15;
+- **Copiar JSON**, que copia exactamente la salida de `datekeys inspect -json` (`cliJSON`: el `json.Encoder` de Go con sangría de dos espacios, `<`, `>`, `&`, U+2028 y U+2029 escapados y salto de línea final). `file` es el nombre del fichero.
+
+No pide ni acepta secretos. Todo el texto leído de la cápsula pasa por interpolación de texto de Svelte (nunca `{@html}`), y ni las entradas de mapas CBOR ni los identificadores de extensión se usan nunca como claves de objetos o `Map` de JavaScript.
+
+| Fichero | Contenido |
+|---|---|
+| `src/lib/inspector/report.ts` | `buildReport`: el modelo de la página a partir de `Inspection`, sin DOM ni reloj |
+| `src/lib/inspector/format.ts` | Nombres y glosas de pasos, códigos y políticas; texto imprimible y escapado; números y fechas en español; `cliJSON` |
+| `src/lib/inspector/diagnostic.ts` | Notación de diagnóstico CBOR (RFC 8949 §8) de `walk`, acotada a 16 384 caracteres |
+| `src/lib/inspector/load.ts` | Lectura por prefijo: de un `.dkc` grande solo se leen 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN + 2 MiB + 1 bytes, y solo 16 si los pasos 1 y 2 rechazan el prelude (otro tipo de fichero, un `.dkk`, longitudes fuera de §57), siempre con el mismo resultado que el fichero entero (lo comprueba `load.test.ts`) |
+| `src/lib/inspector/fixtures.ts` | Los fixtures oficiales, empaquetados desde `testdata/fixtures` |
+| `src/lib/components/` | `InspectionReport`, `StepList`, `ExtensionList`, `DataView`, `Mark` |
+| `src/routes/` | Layout, portada e inspector |
+
+### Fixtures
+
+`fixtures.ts` importa con `import.meta.glob` los `.dkc` de `testdata/fixtures` como URL (`?url`) y, de cada registro JSON, solo el campo `description`. `testdata/` sigue siendo la única fuente: Vite copia cada `.dkc` como fichero con hash en `_app/immutable/assets/` y nunca lo incrusta como `data:` (`assetsInlineLimit: 0`), y no se copia nada más. Los `.dkk`, los textos en claro y los demás campos de los registros (`payload_identity`, `control_cbor`…) no llegan al sitio; `check-build.mjs` lo comprueba. En desarrollo, `server.fs.allow` deja que Vite sirva `testdata/fixtures`.
+
+### Sin red: la Content-Security-Policy
+
+`kit.csp` (`svelte.config.js`, modo `hash`) pone en cada página prerenderizada, como primer elemento que carga algo, un ``:
+
+```
+default-src 'self'; frame-src 'none'; worker-src 'none'; connect-src 'self'; font-src 'self';
+img-src 'self'; manifest-src 'self'; object-src 'none'; script-src 'self' 'sha256-…';
+style-src 'self'; style-src-attr 'unsafe-hashes' 'sha256-…'; base-uri 'none'; form-action 'none'
+```
+
+- `connect-src 'self'`: `fetch` solo llega al propio origen, y solo se usa para los fixtures.
+- `script-src`: los módulos del sitio y el hash SHA-256 del único script en línea, el arranque de SvelteKit (los nonces no sirven en HTML prerenderizado).
+- `style-src 'self'`: solo hojas de estilo del sitio; sin fuentes web ni CDN, con las fuentes del sistema.
+- `style-src-attr`: solo el atributo `style` del anunciador de rutas de SvelteKit, por su hash (`ANNOUNCER_STYLE_HASH`, válido para `@sveltejs/kit` 2.70.3; `app.css` lo oculta también si el navegador bloquea el atributo).
+
+`npm run build` ejecuta después `scripts/check-build.mjs` (`postbuild`; también `npm run build:check`), que falla si una ruta no tiene su HTML prerenderizado; si una página no tiene exactamente esa política, con la etiqueta antes de cualquier elemento que cargue recursos; si un script en línea no está en `script-src` o sobra un hash; si `style-src-attr` no coincide con los atributos `style` del bundle; si hay estilos en línea, manejadores de eventos en atributos, `@import` o URL a otro origen; si algún `.dkc` oficial no está byte a byte; o si aparece en el sitio algún secreto de los fixtures (`.dkk`, textos en claro, identidades, `payload_identity`, `access_material`, `control_cbor`).
+
+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. Ahora mismo no hay ninguna.
+- Dependencias de ejecución: solo `age`, `drand`, `tlock` y lo que ellas arrastran. Ahora mismo no hay ninguna: `package.json` solo tiene `devDependencies`. El sitio lleva compilado 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
@@ -64,11 +115,18 @@ Secretos: `access_material` de un `.dkk` e `I_PAYLOAD` de CONTROL_CBOR se borran
```bash
npm test # vitest, todos los tests
npm run coverage # tests con cobertura v8; falla por debajo de los umbrales
-npm run typecheck # tsc sobre todo y sobre la librería sin tipos de Node
+npm run typecheck # svelte-kit sync y tsc sobre todo y sobre la librería sin tipos de Node
+npm run check # svelte-kit sync y svelte-check (componentes y rutas), falla con avisos
+npm run dev # servidor de desarrollo: http://localhost:5173/inspect
+npm run build # sitio estático en build/ y, después, scripts/check-build.mjs
+npm run preview # sirve build/: http://localhost:4173/inspect
+npm run build:check # solo la comprobación del sitio ya construido
+npm run verify # check, typecheck, coverage y build (con su comprobación)
```
+Umbrales de cobertura (`vitest.config.ts`): `cbor.ts` al 100 % en líneas, ramas, funciones y sentencias; el conjunto de `src/lib/dkc` al 95/90/95/95, y el de `src/lib/inspector` también.
-Umbrales de cobertura (`vitest.config.ts`): `cbor.ts` al 100 % en líneas, ramas, funciones y sentencias; el conjunto de `src/lib/dkc` al 95/90/95/95.
+`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).
@@ -93,8 +151,11 @@ Umbrales de cobertura (`vitest.config.ts`): `cbor.ts` al 100 % en líneas, ramas
| `@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`, `@sveltejs/kit`, `@sveltejs/vite-plugin-svelte`, `svelte-check` | las del prototipo | paso 5 |
-| `@sveltejs/adapter-static` | por fijar | paso 5, pendiente de aprobación: la página es estática y no necesita servidor |
+| `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 |
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.
diff --git a/docs/PLAN_codec_cbor_y_pagina_svelte.md b/docs/PLAN_codec_cbor_y_pagina_svelte.md
index 996ca88..3b61c3a 100644
--- a/docs/PLAN_codec_cbor_y_pagina_svelte.md
+++ b/docs/PLAN_codec_cbor_y_pagina_svelte.md
@@ -309,7 +309,7 @@ Todos en un único cambio normativo, con la justificación §76 de la sección 3
| 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 |
+| 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 |
---
@@ -341,7 +341,7 @@ Todos en un único cambio normativo, con la justificación §76 de la sección 3
| `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` | las del prototipo | paso 5 |
-| `@sveltejs/adapter-static` | por fijar | pendiente de aprobación; paso 5, sitio estático |
+| `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 |
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')`).
diff --git a/package-lock.json b/package-lock.json
index b7810e0..794aa06 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,8 +8,13 @@
"name": "datekeys-app",
"version": "0.0.0",
"devDependencies": {
+ "@sveltejs/adapter-static": "3.0.10",
+ "@sveltejs/kit": "2.70.3",
+ "@sveltejs/vite-plugin-svelte": "7.3.0",
"@types/node": "24.13.6",
"@vitest/coverage-v8": "5.0.1",
+ "svelte": "5.57.1",
+ "svelte-check": "4.7.6",
"typescript": "5.9.3",
"vite": "8.3.0",
"vitest": "5.0.1"
@@ -78,6 +83,28 @@
"node": ">=18"
}
},
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -116,6 +143,13 @@
"url": "https://github.com/sponsors/oxc-project"
}
},
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz",
@@ -378,6 +412,115 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sveltejs/acorn-typescript": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz",
+ "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8.9.0"
+ }
+ },
+ "node_modules/@sveltejs/adapter-static": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz",
+ "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@sveltejs/kit": "^2.0.0"
+ }
+ },
+ "node_modules/@sveltejs/kit": {
+ "version": "2.70.3",
+ "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.3.tgz",
+ "integrity": "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@sveltejs/acorn-typescript": "^1.0.9",
+ "@types/cookie": "^0.6.0",
+ "acorn": "^8.16.0",
+ "cookie": "^0.6.0",
+ "devalue": "^5.8.1",
+ "esm-env": "^1.2.2",
+ "kleur": "^4.1.5",
+ "magic-string": "^0.30.5",
+ "mrmime": "^2.0.0",
+ "set-cookie-parser": "^3.0.0",
+ "sirv": "^3.0.0"
+ },
+ "bin": {
+ "svelte-kit": "svelte-kit.js"
+ },
+ "engines": {
+ "node": ">=18.13"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0",
+ "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
+ "svelte": "^4.0.0 || ^5.0.0-next.0",
+ "typescript": "^5.3.3 || ^6.0.0",
+ "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@sveltejs/kit/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/@sveltejs/load-config": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.3.tgz",
+ "integrity": "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.0.0"
+ }
+ },
+ "node_modules/@sveltejs/vite-plugin-svelte": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.0.tgz",
+ "integrity": "sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.3.1",
+ "magic-string": "^1.0.0",
+ "obug": "^2.1.0",
+ "vitefu": "^1.1.2"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24"
+ },
+ "peerDependencies": {
+ "svelte": "^5.46.4",
+ "vite": "^8.0.0-beta.7 || ^8.0.0"
+ }
+ },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -389,6 +532,13 @@
"assertion-error": "^2.0.1"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -503,6 +653,29 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/acorn": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
+ "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -525,6 +698,16 @@
"js-tokens": "^10.0.0"
}
},
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -535,6 +718,52 @@
"node": ">=18"
}
},
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -545,6 +774,13 @@
"node": ">=8"
}
},
+ "node_modules/devalue": {
+ "version": "5.9.4",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz",
+ "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-module-lexer": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
@@ -552,6 +788,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/esm-env": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+ "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esrap": {
+ "version": "2.3.13",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.13.tgz",
+ "integrity": "sha512-Dc8aMY0tqJNIN3ahtccEm/UWma05Mt/N7NBGdTMtJ1bR5SyctNEFVaOUfmkzVXe6Psk8MPfiiqUafXIoMbWCsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/types": "^8.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@typescript-eslint/types": {
+ "optional": true
+ }
+ }
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -605,6 +866,16 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/is-reference": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.6"
+ }
+ },
"node_modules/js-tokens": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
@@ -612,6 +883,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
@@ -873,6 +1154,13 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/locate-character": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
+ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/magic-string": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
@@ -895,6 +1183,26 @@
"source-map-js": "^1.2.1"
}
},
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.19",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
@@ -977,6 +1285,20 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/rolldown": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz",
@@ -1011,6 +1333,26 @@
"@rolldown/binding-win32-x64-msvc": "1.2.11"
}
},
+ "node_modules/sade": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+ "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mri": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
+ "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -1018,6 +1360,21 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1042,6 +1399,68 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/svelte": {
+ "version": "5.57.1",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.1.tgz",
+ "integrity": "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "@jridgewell/sourcemap-codec": "^1.6.0",
+ "@sveltejs/acorn-typescript": "^1.0.13",
+ "@types/estree": "^1.0.9",
+ "acorn": "^8.18.0",
+ "aria-query": "5.3.1",
+ "axobject-query": "^4.1.0",
+ "clsx": "^2.1.1",
+ "devalue": "^5.9.2",
+ "esm-env": "^1.2.1",
+ "esrap": "^2.3.6",
+ "is-reference": "^3.0.3",
+ "locate-character": "^3.0.0",
+ "magic-string": "^0.30.11",
+ "zimmerframe": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/svelte-check": {
+ "version": "4.7.6",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.6.tgz",
+ "integrity": "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "@sveltejs/load-config": "^0.2.3",
+ "chokidar": "^4.0.1",
+ "fdir": "^6.2.0",
+ "picocolors": "^1.0.0",
+ "sade": "^1.7.4"
+ },
+ "bin": {
+ "svelte-check": "bin/svelte-check"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "peerDependencies": {
+ "svelte": "^4.0.0 || ^5.0.0-next.0",
+ "typescript": "^5.0.0 || ^6.0.0"
+ }
+ },
+ "node_modules/svelte/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/tinybench": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz",
@@ -1089,6 +1508,16 @@
"node": ">=14.0.0"
}
},
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1188,6 +1617,26 @@
}
}
},
+ "node_modules/vitefu": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
+ "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*",
+ "tests/projects/workspace/packages/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vitest": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz",
@@ -1287,6 +1736,13 @@
"engines": {
"node": ">=8"
}
+ },
+ "node_modules/zimmerframe": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz",
+ "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==",
+ "dev": true,
+ "license": "MIT"
}
}
}
diff --git a/package.json b/package.json
index 7242671..739320d 100644
--- a/package.json
+++ b/package.json
@@ -2,21 +2,34 @@
"name": "datekeys-app",
"version": "0.0.0",
"private": true,
- "description": "DateKeys in TypeScript: canonical CBOR codec, DKC1/DKK1 parsers, capsule inspector and, later, browser encryption and decryption.",
+ "description": "DateKeys in TypeScript: canonical CBOR codec, DKC1/DKK1 parsers, capsule inspector library and its static inspector page; later, browser encryption and decryption.",
"type": "module",
"engines": {
"node": ">=20"
},
"scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "postbuild": "node scripts/check-build.mjs",
+ "preview": "vite preview",
+ "prepare": "svelte-kit sync || echo \"\"",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings",
"test": "vitest run",
"coverage": "vitest run --coverage",
- "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.lib.json",
+ "typecheck": "svelte-kit sync && tsc --noEmit && tsc --noEmit -p tsconfig.lib.json",
+ "build:check": "node scripts/check-build.mjs",
+ "verify": "npm run check && npm run typecheck && npm run coverage && npm run build",
"testdata:sync": "node scripts/sync-testdata.mjs sync",
"testdata:check": "node scripts/sync-testdata.mjs check --against ../datekeys-go"
},
"devDependencies": {
+ "@sveltejs/adapter-static": "3.0.10",
+ "@sveltejs/kit": "2.70.3",
+ "@sveltejs/vite-plugin-svelte": "7.3.0",
"@types/node": "24.13.6",
"@vitest/coverage-v8": "5.0.1",
+ "svelte": "5.57.1",
+ "svelte-check": "4.7.6",
"typescript": "5.9.3",
"vite": "8.3.0",
"vitest": "5.0.1"
diff --git a/scripts/check-build.mjs b/scripts/check-build.mjs
new file mode 100644
index 0000000..75ddb83
--- /dev/null
+++ b/scripts/check-build.mjs
@@ -0,0 +1,236 @@
+#!/usr/bin/env node
+// Checks the static site in build/ after `npm run build` (plan §8, phase 1).
+// Node only, no dependencies. It fails with a list of problems when:
+//
+// - a route of src/routes has no prerendered HTML page;
+// - a page lacks the Content-Security-Policy , or the policy is not
+// the one promised (default-src 'self', connect-src 'self', object-src
+// 'none', base-uri 'none', form-action 'none', scripts and styles from the
+// origin only), allows another origin, a scheme or 'unsafe-*', or comes
+// after anything the browser could fetch;
+// - an inline script is missing from script-src, or script-src holds a hash
+// of no inline script;
+// - style-src-attr does not list exactly the hashes of the inline style
+// attributes that the client bundle writes (SvelteKit's route announcer),
+// with 'unsafe-hashes' and nothing else;
+// - a page has an inline style, an event handler attribute or a URL to
+// another origin, or a stylesheet imports or references one;
+// - the official .dkc fixtures are not shipped byte for byte, or a secret of
+// the fixtures (.dkk files, plaintexts, identities, payload identities,
+// access material, CONTROL_CBOR) is anywhere in the build.
+
+import { createHash } from 'node:crypto';
+import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
+import { basename, join, relative, resolve, sep } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = fileURLToPath(new URL('..', import.meta.url));
+// The site directory: build/, or the first argument.
+const BUILD = process.argv[2] === undefined ? join(ROOT, 'build') : resolve(process.argv[2]);
+const ROUTES = join(ROOT, 'src', 'routes');
+const FIXTURES = join(ROOT, 'testdata', 'fixtures');
+
+const problems = [];
+const fail = (msg) => problems.push(msg);
+const rel = (p) => relative(ROOT, p).split(sep).join('/');
+const inBuild = (p) => relative(BUILD, p).split(sep).join('/');
+const sha256 = (b) => createHash('sha256').update(b).digest('hex');
+
+function walk(dir) {
+ const out = [];
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
+ const p = join(dir, e.name);
+ if (e.isDirectory()) out.push(...walk(p));
+ else out.push(p);
+ }
+ return out.sort();
+}
+
+if (!existsSync(BUILD)) {
+ console.error(`${BUILD} does not exist: run "npm run build" first.`);
+ process.exit(1);
+}
+const files = walk(BUILD);
+
+// ---------------------------------------------------------------------------
+// Every route is prerendered.
+
+const pages = walk(ROUTES)
+ .filter((p) => basename(p) === '+page.svelte')
+ .map((p) => relative(ROUTES, p).split(sep).slice(0, -1).join('/'));
+const htmlFiles = pages.map((route) => join(BUILD, route === '' ? 'index.html' : `${route}.html`));
+for (const [i, f] of htmlFiles.entries()) {
+ if (!existsSync(f)) fail(`route /${pages[i]} has no prerendered page ${rel(f)}`);
+}
+
+// ---------------------------------------------------------------------------
+// The Content-Security-Policy of each page.
+
+const unescapeHtml = (s) =>
+ s.replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
+
+const REQUIRED = {
+ 'default-src': ["'self'"],
+ 'connect-src': ["'self'"],
+ 'style-src': ["'self'"],
+ 'img-src': ["'self'"],
+ 'font-src': ["'self'"],
+ 'manifest-src': ["'self'"],
+ 'frame-src': ["'none'"],
+ 'worker-src': ["'none'"],
+ 'object-src': ["'none'"],
+ 'base-uri': ["'none'"],
+ 'form-action': ["'none'"],
+};
+const HASH = /^'sha256-[A-Za-z0-9+/]{43}='$/;
+const b64sha256 = (text) => `'sha256-${createHash('sha256').update(text, 'utf8').digest('base64')}'`;
+
+// The inline style attributes that the client bundle writes into the DOM
+// (Svelte templates are HTML strings in the JavaScript): the only ones the
+// policy may allow, by hash, in style-src-attr.
+const bundleStyles = new Set();
+for (const f of files.filter((p) => p.endsWith('.js'))) {
+ for (const [, value] of readFileSync(f, 'utf8').matchAll(/\sstyle="([^"]*)"/g)) bundleStyles.add(b64sha256(value));
+}
+if (bundleStyles.size !== 1) fail(`the client bundle writes ${bundleStyles.size} distinct inline style attributes, want 1 (the route announcer)`);
+
+function checkPage(file) {
+ const name = rel(file);
+ const html = readFileSync(file, 'utf8');
+ const metas = [...html.matchAll(//gi)];
+ if (metas.length !== 1) {
+ fail(`${name}: ${metas.length} Content-Security-Policy elements, want 1`);
+ return;
+ }
+ const meta = metas[0];
+ // Nothing that loads a resource may come before the policy.
+ const head = html.slice(0, meta.index);
+ if (/<(script|link|style|img|iframe|object|embed|base)\b/i.test(head)) fail(`${name}: an element that loads resources precedes the CSP `);
+
+ const policy = new Map();
+ for (const part of unescapeHtml(meta[1]).split(';')) {
+ const [directive, ...sources] = part.trim().split(/\s+/);
+ if (!directive) continue;
+ if (policy.has(directive)) fail(`${name}: CSP directive ${directive} appears twice`);
+ policy.set(directive, sources);
+ }
+ for (const [directive, want] of Object.entries(REQUIRED)) {
+ const got = policy.get(directive);
+ if (got === undefined) fail(`${name}: CSP lacks ${directive}`);
+ else if (got.join(' ') !== want.join(' ')) fail(`${name}: CSP ${directive} is "${got.join(' ')}", want "${want.join(' ')}"`);
+ }
+ const scriptSrc = policy.get('script-src') ?? [];
+ if (scriptSrc[0] !== "'self'") fail(`${name}: CSP script-src must start with 'self'`);
+ const hashes = scriptSrc.slice(1);
+ for (const h of hashes) if (!HASH.test(h)) fail(`${name}: CSP script-src allows ${h}; only 'self' and SHA-256 hashes are allowed`);
+ const styleAttr = policy.get('style-src-attr') ?? [];
+ if (styleAttr[0] !== "'unsafe-hashes'") fail(`${name}: CSP style-src-attr must start with 'unsafe-hashes'`);
+ const attrHashes = styleAttr.slice(1);
+ for (const h of attrHashes) {
+ if (!HASH.test(h)) fail(`${name}: CSP style-src-attr allows ${h}; only SHA-256 hashes are allowed`);
+ else if (!bundleStyles.has(h)) fail(`${name}: style-src-attr hash ${h} matches no inline style of the bundle`);
+ }
+ for (const h of bundleStyles) if (!attrHashes.includes(h)) fail(`${name}: the bundle's inline style ${h} is not in style-src-attr`);
+ const known = new Set([...Object.keys(REQUIRED), 'script-src', 'style-src-attr']);
+ for (const d of policy.keys()) if (!known.has(d)) fail(`${name}: unexpected CSP directive ${d}`);
+
+ // Every inline script is allowed by its hash, and every hash is used.
+ const inline = [...html.matchAll(/
+
+ (toggled = e.currentTarget.open)}>
+ {@render summary()}
+ {#if open}
+
+ Cualquiera que tenga el fichero puede leer estas extensiones, y nada las autentica antes de abrir la cápsula: el
+ paso 15 (header binding) compara la cabecera con la que se selló solo después de la fecha de apertura. No te fíes
+ de su contenido hasta entonces.
+
+ {formatByteCount(report.size)}{#if report.readLength < report.size}. Se leyeron los primeros {formatByteCount(
+ report.readLength,
+ )}: el resto {report.prelude ? 'de PAYLOAD_AGE' : 'del fichero'} no interviene en los pasos 1 a 8{/if}.
+
+
+
+
+
{copyStatus}
+
+
+
+
+
+ {#if report.valid}
+
Estructura válida
+
+ Supera los pasos 1 a 8 de §63 sin red y sin secretos. La cabecera pública solo queda autenticada al abrir la
+ cápsula (paso 15).
+
+ En tu hora local{timeZone ? ` (${timeZone})` : ''}: {formatDateTime(unlockMs)}
+
+
+ Ronda {report.capsule?.round} de {report.profile?.network ?? report.capsule?.network}; {unlockMs <= nowMs
+ ? `la fecha ya pasó, ${formatRelative(unlockMs, nowMs)}`
+ : `la fecha todavía no ha llegado: ${formatRelative(unlockMs, nowMs)}`}.
+
+ {:else if report.unlock}
+
+ {:else}
+
+ Sin calcular: sale de la DateKey en el paso 7, {report.steps[6]?.state === 'failed' ? 'que falló' : 'que no llegó a ejecutarse'}.
+
+ {/if}
+
+
+
+
+
Pasos 1 a 8
+
+
+
+
+
+
Cabecera pública
+ {#if report.capsule}
+ {@const c = report.capsule}
+
+
+
capsule_id
+
{c.capsuleId}
+
+
+
access_policy
+
{c.accessPolicy}{policyGloss(c.accessPolicy)}
+
+
+
DateKey
+
{c.dateKey}
+
+
+
DateKey decodificada
+
{c.dateKeyJSON}
+
+
+
Red (network)
+
{c.network}
+
+
+
Ronda
+
{c.round}
+
+
+
unlock_at
+
{report.unlock?.rfc3339 ?? 'sin calcular'}
+
+
+ {:else}
+
PUBLIC_HEADER no llegó a decodificarse.
+ {/if}
+
+
+
+
Perfil fijado
+ {#if report.profile}
+ {@const p = report.profile}
+
+
+
profile_id
+
{p.id}
+
+
+
Proveedor y red
+
{p.provider}, {p.network}
+
+
+
Cadena (chain hash)
+
{p.chainHash}
+
+
+
Periodo
+
{p.period} s por ronda
+
+
+
Génesis
+
{p.genesis}
+
+
+
Esquema
+
{p.scheme}
+
+
+ {:else if report.capsule}
+
+ La DateKey nombra el perfil {report.capsule.network}, que este lector no tiene fijado.
+
+ {:else}
+
Se elige con la DateKey de PUBLIC_HEADER, que no llegó a decodificarse.
+ {/if}
+
+
+
+
Prelude
+ {#if report.prelude}
+ {@const p = report.prelude}
+
+
+
Bytes
+
{p.hex}
+
+
+
Marca y versión
+
{p.magic}, versión {p.version}
+
+
+
FLAGS y RESERVED
+
{p.flags}, {p.reserved}
+
+
+
PUBLIC_HEADER_LEN
+
{formatByteCount(p.publicHeaderLen)}
+
+
+
SEALED_CONTROL_LEN
+
{formatByteCount(p.sealedControlLen)}
+
+
+
PAYLOAD_AGE
+
+ desde el byte {formatInteger(p.payloadOffset)}{#if p.payloadLength !== undefined}, {formatByteCount(
+ p.payloadLength,
+ )}{:else}, fuera del fichero{/if}
+
Se compara cuando se leen la cabecera, el perfil y OUTER_TIME_AGE.
+ {/if}
+
+
+
+
+
Extensiones de PUBLIC_HEADER
+
+ Este inspector implementa el protocolo base V1 y no conoce ninguna extensión: una crítica hace fallar el paso 4 y
+ las no críticas se ignoran. Sus datos son bytes opacos para el protocolo.
+
+
+
+
+ {@render children()}
+
+
+
+
+
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts
new file mode 100644
index 0000000..664fa18
--- /dev/null
+++ b/src/routes/+layout.ts
@@ -0,0 +1,2 @@
+// Every page is prerendered to static HTML; there is no server code.
+export const prerender = true;
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
new file mode 100644
index 0000000..bfbb446
--- /dev/null
+++ b/src/routes/+page.svelte
@@ -0,0 +1,147 @@
+
+
+
+ DateKeys: comprueba una cápsula sin abrirla
+
+
+
+
+
+
Mira una cápsula DateKeys sin abrirla
+
+ Una cápsula .dkc es un fichero cifrado que nadie puede abrir antes de una fecha: la clave depende de
+ una firma que todavía no existe. La publicará en esa fecha drand, una red pública que emite una firma nueva cada
+ pocos segundos. El inspector lee la parte pública de la cápsula y comprueba que está bien formada, con los mismos
+ pasos que la herramienta datekeys inspect.
+
+
+
+ Los ocho pasos que se comprueban antes de la fecha de apertura (§63)
+
+ {#each INSPECT_STEPS as step (step)}
+
+ {step}
+
+
{stepName(step)}
+
{stepGloss(step)}
+
+
+ {/each}
+
+
+
+
+
+
Qué garantiza la página
+
+
+
El fichero no sale del navegador
+
+ Se lee en local. La política de seguridad de la página (CSP) solo permite conexiones a su propio origen, y solo
+ se usan para descargar las cápsulas de prueba que vienen con el sitio.
+
+
+
+
Sin secretos
+
No pide ni acepta claves de acceso .dkk, identidades ni contraseñas. Solo lee lo que es público.
+
+
+
El veredicto de la referencia
+
+ Pasos, códigos de error y detalles son los de la implementación de referencia en Go. Copiar JSON
+ da exactamente la salida de datekeys inspect -json.
+
+
+
+
Lo que queda para la apertura
+
+ Superar los pasos 1 a 8 no prueba que la cápsula se pueda abrir. La cabecera pública solo queda autenticada en el
+ paso 15, al abrirla después de la fecha.
+
+ Comprueba la parte pública de un fichero .dkc antes de su fecha de apertura: los pasos 1 a 8 de la
+ especificación (§63), con el mismo resultado que datekeys inspect. El fichero no sale de este navegador
+ y no se pide ninguna clave.
+
+
+
+
+
Elegir una cápsula
+
+
+
+
Cápsulas de prueba oficiales
+
+ Cápsulas de ejemplo de la implementación de referencia en Go, incluidas en el sitio (descripciones en inglés).
+
+{/if}
+
+
diff --git a/static/favicon.svg b/static/favicon.svg
new file mode 100644
index 0000000..2c02eec
--- /dev/null
+++ b/static/favicon.svg
@@ -0,0 +1,5 @@
+
diff --git a/svelte.config.js b/svelte.config.js
new file mode 100644
index 0000000..d588bed
--- /dev/null
+++ b/svelte.config.js
@@ -0,0 +1,64 @@
+// SvelteKit configuration of the inspector page (plan §8, phase 1): a static
+// site, every page prerendered, no server code, and a Content-Security-Policy
+// that keeps the page on its own origin.
+import adapter from '@sveltejs/adapter-static';
+
+// The only inline style of the site: the style attribute of SvelteKit's route
+// announcer (
, written by `svelte-kit sync` into
+// .svelte-kit/generated/root.svelte), allowed by its SHA-256 and nothing else.
+// It is the hash of the attribute value for @sveltejs/kit 2.70.3; the build
+// check (scripts/check-build.mjs) fails if the bundle holds any other inline
+// style, and src/app.css hides the announcer anyway in browsers without
+// style-src-attr.
+const ANNOUNCER_STYLE_HASH = 'sha256-S8qMpvofolR8Mpjy4kQvEm7m1q8clzU4dfDH0AmvZjo=';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ compilerOptions: {
+ runes: true,
+ },
+ kit: {
+ // strict: the build fails if any route is not prerendered.
+ adapter: adapter({ strict: true }),
+ // Prerendered pages get the policy as , the first element
+ // of . In 'hash' mode SvelteKit adds the SHA-256 of each inline
+ // script it writes (the hydration bootstrap) to script-src; the styles are
+ // external files (inlineStyleThreshold stays 0), so style-src needs no
+ // hash, and style-src-attr allows the announcer's style attribute alone.
+ // Nonces cannot work in prerendered HTML. The fixtures are fetched
+ // from the same origin, so connect-src 'self' is enough: no other origin
+ // can be reached from the page.
+ csp: {
+ mode: 'hash',
+ directives: {
+ 'default-src': ['self'],
+ 'script-src': ['self'],
+ 'style-src': ['self'],
+ 'style-src-attr': ['unsafe-hashes', ANNOUNCER_STYLE_HASH],
+ 'img-src': ['self'],
+ 'font-src': ['self'],
+ 'connect-src': ['self'],
+ 'manifest-src': ['self'],
+ 'frame-src': ['none'],
+ 'worker-src': ['none'],
+ 'object-src': ['none'],
+ 'base-uri': ['none'],
+ 'form-action': ['none'],
+ },
+ },
+ prerender: {
+ handleHttpError: 'fail',
+ handleMissingId: 'fail',
+ handleUnseenRoutes: 'fail',
+ },
+ typescript: {
+ // The generated tsconfig covers src/ and vite.config.ts; the vitest
+ // configuration is type-checked as well.
+ config(tsconfig) {
+ tsconfig.include.push('../vitest.config.ts');
+ },
+ },
+ },
+};
+
+export default config;
diff --git a/tsconfig.json b/tsconfig.json
index 55a9863..a3af387 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,11 @@
{
+ // Extends the configuration that `svelte-kit sync` generates (the $lib and
+ // $app paths, and the include list: src/, vite.config.ts and, through
+ // svelte.config.js, vitest.config.ts). The options below make it strict.
+ "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"target": "ES2022",
- "lib": ["ES2024", "DOM"],
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
@@ -16,6 +20,5 @@
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"types": ["node"]
- },
- "include": ["src/**/*.ts", "tests/**/*.ts", "vitest.config.ts"]
+ }
}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..f29c536
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,21 @@
+// Vite configuration of the SvelteKit site. The library tests run with
+// vitest.config.ts, which vitest prefers when both files exist.
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [sveltekit()],
+ build: {
+ // Never inline an asset as a data: URL. The CSP allows only the page's
+ // own origin, so the official fixtures (src/lib/inspector/fixtures.ts)
+ // must be separate same-origin files, whatever their size.
+ assetsInlineLimit: 0,
+ },
+ server: {
+ fs: {
+ // The dev server serves the official fixtures straight from testdata/,
+ // the single source of truth; the build copies them as hashed assets.
+ allow: ['testdata/fixtures'],
+ },
+ },
+});
diff --git a/vitest.config.ts b/vitest.config.ts
index 5ae7c20..f6caf2b 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,3 +1,7 @@
+// The tests of src/lib. Vitest prefers this file to vite.config.ts, the
+// SvelteKit configuration of the page, so the library tests run without the
+// SvelteKit plugin; the page helpers in src/lib/inspector import the library
+// by relative paths and need no $lib alias.
import { defineConfig } from 'vitest/config';
export default defineConfig({
@@ -7,13 +11,15 @@ export default defineConfig({
testTimeout: 30_000,
coverage: {
provider: 'v8',
- include: ['src/lib/dkc/**/*.ts'],
- exclude: ['src/lib/dkc/**/*.test.ts', 'src/lib/dkc/testing/**', 'src/lib/dkc/index.ts'],
+ include: ['src/lib/dkc/**/*.ts', 'src/lib/inspector/**/*.ts'],
+ exclude: ['src/lib/**/*.test.ts', 'src/lib/dkc/testing/**', 'src/lib/dkc/index.ts'],
reporter: ['text', 'html', 'json-summary'],
thresholds: {
// The codec is covered completely (plan §7).
'src/lib/dkc/cbor.ts': { 100: true },
'src/lib/dkc/**/*.ts': { statements: 95, branches: 90, functions: 95, lines: 95 },
+ // The page model and helpers of the inspector (plan §8, phase 1).
+ 'src/lib/inspector/**/*.ts': { statements: 95, branches: 90, functions: 95, lines: 95 },
},
},
},