Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
#!/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 <meta>, or the policy is not
|
/inspect asks drand for the release when the person asks
The author found copying the release of the round tedious. A button,
"Pedir la firma a drand", fetches it from the three public relays of
the CLI of the reference, as its client does: raced, 6 s, at most
8 KiB an answer, no redirects, and the randomness checked against the
signature; step 10 still verifies the signature with the pinned key,
so a relay cannot make the page accept a false one. It is the only
connection the page makes to another site, and only on that click: the
CSP allows those three origins in connect-src, check-build.mjs
requires exactly them, and the footer says so. Pasting by hand still
works. Checked in Chromium: api2.drand.sh gave the release of round
32668196 and the capsule opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 days ago
|
|
|
// the one promised (default-src 'self', connect-src 'self' and the three
|
|
|
|
|
// relays of drand, object-src
|
Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
// '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,
|
Format 3, step 3: read capsule format 3 of spec v0.10
Syncs testdata with datekeys-go at the tag spec-v0.10 (cc35d2c) and
moves the reader to the DateKeys Protocol Specification v0.10. The
three capsule formats are read.
- framing: FORMAT_3, and isPadded for formats 2 and 3. control: schema
version 3, with the keys 6 and 7 of version 2. SPEC_VERSION is 0.10.
- open: OpenOptions.sink receives the files of a format 3 capsule
(sink.ts: Sink with begin, create, commit and abort, as capsule.Sink,
and MemorySink). Without one, open rejects with a TypeError right
after step 2, before any request, as ErrSinkRequired. Opened gains
head, verdicts, areaLen and unusableHeadExtensions.
- open3.ts: step 17 of format 3 in its substeps 17.2 to 17.8, as
openBody of the reference: a failure of age or a plaintext whose
length is not P prevails, the first failing substep decides, and the
codes other than ERR_INTEGRITY are reported only after reading
PAYLOAD_AGE to its end. Reads grow with the bytes received, never
with the lengths BODY declares. A failure of the sink is ERR_INTEGRITY
with its text, and the sink is aborted once after begin.
- The page: opener.ts opens the fixtures of format 3 into a
MemorySink; the open panel says that it does not deliver their files
yet, and the glosses of the steps name format 3. check-build.mjs
refuses to ship the heads, salts, comments and paths of the format 3
fixtures.
Tests: the 21 fixtures, format 3 laid out byte by byte from its record
and opened into a sink with its files and verdicts; the 209 cases of
the corpus from memory and from a Blob, with the code, the step and,
new, the exact text of capsule.Open, frozen by
scripts/mutation-go-texts.go in testing/mutation-texts.json, which
replays the corpus as internal/testkit does (its extension validator
texts included); the 5110 differential cases over 14 bases; paths,
path_fold, head_schema and security vectors; the control of schema
version 3 in cbor.json; and step 17 on crafted plaintexts sealed again
to I_PAYLOAD, whose texts capsule.Open gives on the same plaintexts.
ibe-vectors.json gains the nine format 3 fixtures from
scripts/ibe-go-vectors.go; the twelve before are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// access material, CONTROL_CBOR, and the heads, salts, comments and paths
|
|
|
|
|
// of format 3) is anywhere in the build;
|
Format 3, step 6: /inspect opens format 3
The page opens capsules of format 3. The files go to the temporary
file through a ZipSink, a lone file of one segment as it is and a ZIP
otherwise, or to memory; the page shows the verdicts first, then the
declared author and the comment as unchecked text of the creator, and
then each path as text in a bdi, with its size, its mtime and the
warnings of the CLI of the reference, compared by their key of R7.
- files.ts: pathWarnings, fileFacts, and the names and order of the
downloads: the file itself when it is the only one, with the ZIP of
its folder second (decision 8), or the ZIP and each file.
- opener.ts: OpenedFiles, and noRoom when the ZIP does not fit.
- zipsink.ts: NoRoom, thrown by begin before writing anything.
- check-build.mjs: the tables of pathrule-tables.ts never come with
the first load of a page, and do come with the code on demand of
/inspect and /create.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// - a page loads the Unicode tables of the paths of format 3 with its first
|
|
|
|
|
// load, not on demand;
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// - the client bundle holds tlock-js, drand-client or Babel's helpers, or a
|
|
|
|
|
// nested copy of a package other than the noble copy under
|
|
|
|
|
// @noble/post-quantum (plan of phase 2, section 3 and decision 5), as
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// .svelte-kit/output/client-modules.json records it (vite.config.ts);
|
The writers as Go: test vectors only through testing/, and the note
Fixes T9, T11 and T12 of the review of the session of 1 and 2 October:
- T9: EncryptOptions no longer has testVectors nor areaLen, with which any
caller could write format 2, or an area of 512 bytes, which rule 13
forbids and which tells that the capsule has no signature (§55.2). What
only a generator of test vectors asks, as Go's TestVectors, is the
TestVectors argument of the core of writer.ts, which only the helpers of
testing/encrypt.ts pass: encryptVectors and encryptWith write format 2,
and encryptFilesWith another area, with which the tests still reproduce
byte for byte the fixtures of 512 bytes. encrypt keeps the shape of
capsule.Encrypt: without a generator it fails with the text of Go,
whatever the caller adds. dependencies.test.ts refuses an import of
testing/ from anything but the tests and testing/ itself, check-build.mjs
refuses a test or a module of testing/ in the bundle of the pages, and
note.ts joins the modules that index.ts must not re-export.
- T12: encrypt as a generator refuses a public note and an area with the
text of Go, "capsule: format 2 has no security area or public note: ...",
after the head and the length, as capsule.Encrypt. The errors of the note
carry "capsule: ", and newSealer checks the note, then the profile and the
clock, in the order of Go. The tests compare the texts byte for byte, also
for two faults at once.
- T11: capsuleLength takes the public note and predicts exactly the size of
the .dkc with it: eight notes of 1 to 1024 bytes, across the boundaries of
the heads of CBOR, with both policies and with other extensions.
The 40 texts that capsule.EncryptFiles and extension.CheckNote give at
spec-v0.11 on the same notes and options, taken with an oracle, are those
of this library; HEAD gave another one in 23 of them. npm run verify
passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
// - the client bundle holds a test, or a module of src/lib/dkc/testing/,
|
|
|
|
|
// whose helpers write what only a generator of test vectors may write
|
|
|
|
|
// (format 2, another security area than 32 KiB);
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// - a page loads noble, @scure/base or age-encryption with the page instead
|
Phase 3, steps 6 and 7: the create page
/create seals a person's file into a .dkc of format 2 and, when asked, a
portable .dkk, in the browser and without network, with the decisions the
author confirmed in step 6: time_only by default, the zone of the device
with a selector, the warnings of §53 and §50 beyond 365 days, a notice of
preliminary protocol, and files named capsula-<opening time, UTC>.
- lengths.ts: sealedControlLength moves out of writer.ts, and
capsuleLength gives the size of the .dkc before writing it; the
property loop checks it on every capsule (500 seeds pass).
- datekey.ts: LONG_HORIZON_SECONDS and isLongHorizon.
- src/lib/inspector: localtime.ts (local times of a zone as UTC instants,
a skipped time refused, a repeated one taken at its later instant),
create-input.ts (the form, checked in its order) and creator.ts (the
writing, loaded on demand), all at 100 %.
- The .dkc goes to an OPFS temporary file, committed only when complete
and checked, or to memory up to 64 MiB; the writing can be cancelled.
The .dkk stays in memory only; losing it when it is the only
credential asks for confirmation.
- /inspect also cleans the temporary files of /create, and check-build
checks the code loaded on demand of both pages.
Checked in the browser on the production build: a capsule made for four
minutes later opened afterwards in /inspect with the pasted release and
with datekeys decrypt of Go over the network, to the same content. An
adversarial review found one major and eight minor issues, all fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// of on demand, or a page of ON_DEMAND cannot load its code on demand: the
|
|
|
|
|
// opening on /inspect (plan of phase 2, section 9) and the writer on
|
|
|
|
|
// /create (plan of phase 3, section 3);
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// - licenses.txt lacks the notice of a package in the client bundle, the
|
|
|
|
|
// copyright lines kept in the header of a module of src/ derived from
|
|
|
|
|
// another project, or the license of the site.
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
//
|
|
|
|
|
// It reports the JavaScript that each page loads, raw and gzip.
|
Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
|
|
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
import { fileURLToPath } from 'node:url';
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
import { gzipSync } from 'node:zlib';
|
Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
|
|
|
|
|
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'"],
|
/inspect asks drand for the release when the person asks
The author found copying the release of the round tedious. A button,
"Pedir la firma a drand", fetches it from the three public relays of
the CLI of the reference, as its client does: raced, 6 s, at most
8 KiB an answer, no redirects, and the randomness checked against the
signature; step 10 still verifies the signature with the pinned key,
so a relay cannot make the page accept a false one. It is the only
connection the page makes to another site, and only on that click: the
CSP allows those three origins in connect-src, check-build.mjs
requires exactly them, and the footer says so. Pasting by hand still
works. Checked in Chromium: api2.drand.sh gave the release of round
32668196 and the capsule opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 days ago
|
|
|
'connect-src': ["'self'", 'https://api.drand.sh', 'https://api2.drand.sh', 'https://api3.drand.sh'],
|
Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
'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(/<meta http-equiv="content-security-policy" content="([^"]*)">/gi)];
|
|
|
|
|
if (metas.length !== 1) {
|
|
|
|
|
fail(`${name}: ${metas.length} Content-Security-Policy <meta> 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 <meta>`);
|
|
|
|
|
|
|
|
|
|
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(/<script(\s[^>]*)?>([\s\S]*?)<\/script>/gi)];
|
|
|
|
|
const used = new Set();
|
|
|
|
|
for (const [, attrs = '', body] of inline) {
|
|
|
|
|
if (/\ssrc=/i.test(attrs)) {
|
|
|
|
|
if (body.trim() !== '') fail(`${name}: a <script src> has a body`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const h = `'sha256-${createHash('sha256').update(body, 'utf8').digest('base64')}'`;
|
|
|
|
|
used.add(h);
|
|
|
|
|
if (!hashes.includes(h)) fail(`${name}: an inline script is not allowed by script-src (${h})`);
|
|
|
|
|
}
|
|
|
|
|
for (const h of hashes) if (!used.has(h)) fail(`${name}: script-src hash ${h} matches no inline script`);
|
|
|
|
|
|
|
|
|
|
// Styles only from files, no event handler attributes, no other origins.
|
|
|
|
|
if (/<style[\s>]/i.test(html)) fail(`${name}: inline <style> element`);
|
|
|
|
|
if (/\sstyle=/i.test(html)) fail(`${name}: inline style attribute`);
|
|
|
|
|
if (/\son[a-z]+=/i.test(html)) fail(`${name}: inline event handler attribute`);
|
|
|
|
|
for (const [, attr, url] of html.matchAll(/\s(src|href|action|srcset|poster|data)="([^"]*)"/gi)) {
|
|
|
|
|
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(url.trim())) fail(`${name}: ${attr}="${url}" points outside the site`);
|
|
|
|
|
}
|
|
|
|
|
return policy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const policies = htmlFiles.filter(existsSync).map(checkPage);
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Stylesheets never reach another origin (no web fonts, no remote images).
|
|
|
|
|
|
|
|
|
|
for (const f of files.filter((p) => p.endsWith('.css'))) {
|
|
|
|
|
const css = readFileSync(f, 'utf8');
|
|
|
|
|
if (/@import/i.test(css)) fail(`${rel(f)}: @import`);
|
|
|
|
|
for (const [, url] of css.matchAll(/url\(\s*['"]?([^'")]*)/gi)) {
|
|
|
|
|
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) fail(`${rel(f)}: url(${url}) points outside the site`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// The fixtures: every .dkc byte for byte, no secret anywhere.
|
|
|
|
|
|
|
|
|
|
const fixtureFiles = readdirSync(FIXTURES).sort();
|
|
|
|
|
const byHash = new Map();
|
|
|
|
|
for (const f of files) {
|
|
|
|
|
const h = sha256(readFileSync(f));
|
|
|
|
|
byHash.set(h, [...(byHash.get(h) ?? []), f]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const name of fixtureFiles.filter((n) => n.endsWith('.dkc'))) {
|
|
|
|
|
const stem = name.slice(0, -'.dkc'.length);
|
|
|
|
|
const copies = byHash.get(sha256(readFileSync(join(FIXTURES, name)))) ?? [];
|
|
|
|
|
const shipped = copies.filter((p) => inBuild(p).startsWith('_app/immutable/assets/') && basename(p).startsWith(`${stem}.`));
|
|
|
|
|
if (shipped.length !== 1) fail(`fixture ${name}: ${shipped.length} byte-exact copies under build/_app/immutable/assets, want 1`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Secret values that must never be shipped.
|
|
|
|
|
const secrets = [];
|
|
|
|
|
const addSecret = (label, value) => {
|
|
|
|
|
if (typeof value === 'string' && value.length >= 16) secrets.push([label, value]);
|
|
|
|
|
};
|
|
|
|
|
for (const name of fixtureFiles) {
|
|
|
|
|
const p = join(FIXTURES, name);
|
|
|
|
|
const bytes = readFileSync(p);
|
|
|
|
|
if (name.endsWith('.dkk') || name.endsWith('.plaintext')) {
|
|
|
|
|
const copies = byHash.get(sha256(bytes)) ?? [];
|
|
|
|
|
if (bytes.length > 0 && copies.length > 0) fail(`${name} is shipped as ${copies.map(rel).join(', ')}`);
|
|
|
|
|
addSecret(`${name} (hex)`, bytes.toString('hex'));
|
|
|
|
|
addSecret(`${name} (base64)`, bytes.toString('base64').replace(/=+$/, ''));
|
|
|
|
|
if (name.endsWith('.plaintext')) addSecret(`${name} (text)`, bytes.toString('utf8').slice(0, 64));
|
|
|
|
|
}
|
|
|
|
|
if (name.endsWith('.json')) {
|
|
|
|
|
const record = JSON.parse(bytes.toString('utf8'));
|
|
|
|
|
for (const id of record.identities ?? []) addSecret(`${name} identities`, id);
|
|
|
|
|
addSecret(`${name} payload_identity`, record.payload_identity);
|
|
|
|
|
addSecret(`${name} access_material`, record.access_material);
|
|
|
|
|
addSecret(`${name} control_cbor`, record.control_cbor);
|
Format 3, step 3: read capsule format 3 of spec v0.10
Syncs testdata with datekeys-go at the tag spec-v0.10 (cc35d2c) and
moves the reader to the DateKeys Protocol Specification v0.10. The
three capsule formats are read.
- framing: FORMAT_3, and isPadded for formats 2 and 3. control: schema
version 3, with the keys 6 and 7 of version 2. SPEC_VERSION is 0.10.
- open: OpenOptions.sink receives the files of a format 3 capsule
(sink.ts: Sink with begin, create, commit and abort, as capsule.Sink,
and MemorySink). Without one, open rejects with a TypeError right
after step 2, before any request, as ErrSinkRequired. Opened gains
head, verdicts, areaLen and unusableHeadExtensions.
- open3.ts: step 17 of format 3 in its substeps 17.2 to 17.8, as
openBody of the reference: a failure of age or a plaintext whose
length is not P prevails, the first failing substep decides, and the
codes other than ERR_INTEGRITY are reported only after reading
PAYLOAD_AGE to its end. Reads grow with the bytes received, never
with the lengths BODY declares. A failure of the sink is ERR_INTEGRITY
with its text, and the sink is aborted once after begin.
- The page: opener.ts opens the fixtures of format 3 into a
MemorySink; the open panel says that it does not deliver their files
yet, and the glosses of the steps name format 3. check-build.mjs
refuses to ship the heads, salts, comments and paths of the format 3
fixtures.
Tests: the 21 fixtures, format 3 laid out byte by byte from its record
and opened into a sink with its files and verdicts; the 209 cases of
the corpus from memory and from a Blob, with the code, the step and,
new, the exact text of capsule.Open, frozen by
scripts/mutation-go-texts.go in testing/mutation-texts.json, which
replays the corpus as internal/testkit does (its extension validator
texts included); the 5110 differential cases over 14 bases; paths,
path_fold, head_schema and security vectors; the control of schema
version 3 in cbor.json; and step 17 on crafted plaintexts sealed again
to I_PAYLOAD, whose texts capsule.Open gives on the same plaintexts.
ibe-vectors.json gains the nine format 3 fixtures from
scripts/ibe-go-vectors.go; the twelve before are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// Format 3: the head, its salt, the comment and the paths are the
|
|
|
|
|
// content of BODY, as secret as the files.
|
|
|
|
|
addSecret(`${name} head_cbor`, record.head_cbor);
|
|
|
|
|
addSecret(`${name} salt`, record.salt);
|
|
|
|
|
addSecret(`${name} comment`, record.comment);
|
|
|
|
|
for (const f of record.files ?? []) addSecret(`${name} path`, f.path);
|
Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (secrets.length < 10) fail(`only ${secrets.length} fixture secrets collected; the fixture records changed shape`);
|
|
|
|
|
const TEXT = /\.(html|js|css|json|svg|txt|map|webmanifest)$/;
|
|
|
|
|
for (const f of files.filter((p) => TEXT.test(p))) {
|
|
|
|
|
const text = readFileSync(f, 'utf8');
|
|
|
|
|
for (const [label, value] of secrets) if (text.includes(value)) fail(`${rel(f)} contains the fixture secret ${label}`);
|
|
|
|
|
}
|
|
|
|
|
for (const f of files) {
|
|
|
|
|
if (/\.(dkk|plaintext)$/.test(f) || /fixtures?\/.*\.json$/.test(inBuild(f))) fail(`${rel(f)}: fixture file that must not be shipped`);
|
|
|
|
|
}
|
|
|
|
|
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// What the client bundle is made of.
|
|
|
|
|
|
|
|
|
|
const MODULES = join(ROOT, '.svelte-kit', 'output', 'client-modules.json');
|
|
|
|
|
const FORBIDDEN_PACKAGES = [/^tlock-js$/, /^drand-client$/, /^@babel\//];
|
The writers as Go: test vectors only through testing/, and the note
Fixes T9, T11 and T12 of the review of the session of 1 and 2 October:
- T9: EncryptOptions no longer has testVectors nor areaLen, with which any
caller could write format 2, or an area of 512 bytes, which rule 13
forbids and which tells that the capsule has no signature (§55.2). What
only a generator of test vectors asks, as Go's TestVectors, is the
TestVectors argument of the core of writer.ts, which only the helpers of
testing/encrypt.ts pass: encryptVectors and encryptWith write format 2,
and encryptFilesWith another area, with which the tests still reproduce
byte for byte the fixtures of 512 bytes. encrypt keeps the shape of
capsule.Encrypt: without a generator it fails with the text of Go,
whatever the caller adds. dependencies.test.ts refuses an import of
testing/ from anything but the tests and testing/ itself, check-build.mjs
refuses a test or a module of testing/ in the bundle of the pages, and
note.ts joins the modules that index.ts must not re-export.
- T12: encrypt as a generator refuses a public note and an area with the
text of Go, "capsule: format 2 has no security area or public note: ...",
after the head and the length, as capsule.Encrypt. The errors of the note
carry "capsule: ", and newSealer checks the note, then the profile and the
clock, in the order of Go. The tests compare the texts byte for byte, also
for two faults at once.
- T11: capsuleLength takes the public note and predicts exactly the size of
the .dkc with it: eight notes of 1 to 1024 bytes, across the boundaries of
the heads of CBOR, with both policies and with other extensions.
The 40 texts that capsule.EncryptFiles and extension.CheckNote give at
spec-v0.11 on the same notes and options, taken with an oracle, are those
of this library; HEAD gave another one in 23 of them. npm run verify
passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
// The modules that only the tests load: the tests and the helpers of testing/.
|
|
|
|
|
const TEST_ONLY = /\/src\/(?:lib\/dkc\/testing\/|.*\.test\.ts$)/;
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const chunks = existsSync(MODULES) ? JSON.parse(readFileSync(MODULES, 'utf8')).chunks : {};
|
|
|
|
|
if (!existsSync(MODULES)) fail(`${rel(MODULES)} is missing: vite.config.ts writes it during "npm run build"`);
|
|
|
|
|
|
|
|
|
|
// The npm package of a module id, and the package it is nested under, if any.
|
|
|
|
|
function packageOf(id) {
|
|
|
|
|
const i = id.lastIndexOf('/node_modules/');
|
|
|
|
|
if (i < 0) return null;
|
|
|
|
|
const parts = id.slice(i + '/node_modules/'.length).split('/');
|
|
|
|
|
const name = parts[0].startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
|
|
|
const outer = id.slice(0, i);
|
|
|
|
|
const j = outer.lastIndexOf('/node_modules/');
|
|
|
|
|
return { name, parent: j < 0 ? null : outer.slice(j + '/node_modules/'.length) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const packages = new Set();
|
|
|
|
|
for (const [file, chunk] of Object.entries(chunks)) {
|
|
|
|
|
if (!existsSync(join(BUILD, file))) fail(`client chunk ${file} is not in the site`);
|
|
|
|
|
for (const id of Object.keys(chunk.modules)) {
|
The writers as Go: test vectors only through testing/, and the note
Fixes T9, T11 and T12 of the review of the session of 1 and 2 October:
- T9: EncryptOptions no longer has testVectors nor areaLen, with which any
caller could write format 2, or an area of 512 bytes, which rule 13
forbids and which tells that the capsule has no signature (§55.2). What
only a generator of test vectors asks, as Go's TestVectors, is the
TestVectors argument of the core of writer.ts, which only the helpers of
testing/encrypt.ts pass: encryptVectors and encryptWith write format 2,
and encryptFilesWith another area, with which the tests still reproduce
byte for byte the fixtures of 512 bytes. encrypt keeps the shape of
capsule.Encrypt: without a generator it fails with the text of Go,
whatever the caller adds. dependencies.test.ts refuses an import of
testing/ from anything but the tests and testing/ itself, check-build.mjs
refuses a test or a module of testing/ in the bundle of the pages, and
note.ts joins the modules that index.ts must not re-export.
- T12: encrypt as a generator refuses a public note and an area with the
text of Go, "capsule: format 2 has no security area or public note: ...",
after the head and the length, as capsule.Encrypt. The errors of the note
carry "capsule: ", and newSealer checks the note, then the profile and the
clock, in the order of Go. The tests compare the texts byte for byte, also
for two faults at once.
- T11: capsuleLength takes the public note and predicts exactly the size of
the .dkc with it: eight notes of 1 to 1024 bytes, across the boundaries of
the heads of CBOR, with both policies and with other extensions.
The 40 texts that capsule.EncryptFiles and extension.CheckNote give at
spec-v0.11 on the same notes and options, taken with an oracle, are those
of this library; HEAD gave another one in 23 of them. npm run verify
passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
if (TEST_ONLY.test(id)) fail(`${file} bundles ${id}, which only the tests may load`);
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const pkg = packageOf(id);
|
|
|
|
|
if (pkg === null) continue;
|
|
|
|
|
if (FORBIDDEN_PACKAGES.some((re) => re.test(pkg.name))) fail(`${file} bundles ${pkg.name}: ${id}`);
|
|
|
|
|
if (pkg.parent !== null && pkg.parent !== '@noble/post-quantum') fail(`${file} bundles a copy of ${pkg.name} nested under ${pkg.parent}`);
|
|
|
|
|
packages.add(pkg.parent === null ? pkg.name : `${pkg.name} (under ${pkg.parent})`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The JavaScript of a page: the chunks it preloads and the entries its
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// inline script imports, as SvelteKit writes them (relative to the page, with
|
|
|
|
|
// ../ below the root), with their static imports; and apart, what those load
|
|
|
|
|
// on demand, other than the nodes of other routes. A page whose scripts are
|
|
|
|
|
// not all chunks of the bundle fails, so that no guard below passes without
|
|
|
|
|
// looking at them.
|
|
|
|
|
function pageScripts(file) {
|
|
|
|
|
const html = readFileSync(file, 'utf8');
|
|
|
|
|
const inSite = (url) => inBuild(resolve(dirname(file), url));
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const eager = new Set();
|
|
|
|
|
for (const [link] of html.matchAll(/<link\b[^>]*>/gi)) {
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const href = /\brel="modulepreload"/i.test(link) ? link.match(/\bhref="([^"]+)"/i) : null;
|
|
|
|
|
if (href) eager.add(inSite(href[1]));
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
}
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
for (const [, src] of html.matchAll(/\bimport\("([^"]+)"\)/g)) eager.add(inSite(src));
|
|
|
|
|
if (eager.size === 0) fail(`${rel(file)}: no script of the page found`);
|
|
|
|
|
for (const f of eager) if (chunks[f] === undefined) fail(`${rel(file)}: script ${f} is not a chunk of the client bundle`);
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const close = (set, seeds) => {
|
|
|
|
|
const queue = [...seeds];
|
|
|
|
|
while (queue.length > 0) {
|
|
|
|
|
for (const f of chunks[queue.pop()]?.imports ?? []) {
|
|
|
|
|
if (!set.has(f) && !eager.has(f)) {
|
|
|
|
|
set.add(f);
|
|
|
|
|
queue.push(f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
close(eager, eager);
|
|
|
|
|
const lazy = new Set();
|
|
|
|
|
for (const f of eager) {
|
|
|
|
|
for (const d of chunks[f]?.dynamicImports ?? []) {
|
|
|
|
|
if (!eager.has(d) && !/^_app\/immutable\/(nodes|entry)\//.test(d)) lazy.add(d);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
close(lazy, lazy);
|
|
|
|
|
return { eager, lazy };
|
|
|
|
|
}
|
|
|
|
|
const weight = (set) => {
|
|
|
|
|
let raw = 0;
|
|
|
|
|
let gzip = 0;
|
|
|
|
|
for (const f of set) {
|
|
|
|
|
const bytes = readFileSync(join(BUILD, f));
|
|
|
|
|
raw += bytes.length;
|
|
|
|
|
gzip += gzipSync(bytes, { level: 9 }).length;
|
|
|
|
|
}
|
|
|
|
|
return `${set.size} files, ${raw} bytes, ${gzip} gzip`;
|
|
|
|
|
};
|
|
|
|
|
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// ---------------------------------------------------------------------------
|
Phase 3, steps 6 and 7: the create page
/create seals a person's file into a .dkc of format 2 and, when asked, a
portable .dkk, in the browser and without network, with the decisions the
author confirmed in step 6: time_only by default, the zone of the device
with a selector, the warnings of §53 and §50 beyond 365 days, a notice of
preliminary protocol, and files named capsula-<opening time, UTC>.
- lengths.ts: sealedControlLength moves out of writer.ts, and
capsuleLength gives the size of the .dkc before writing it; the
property loop checks it on every capsule (500 seeds pass).
- datekey.ts: LONG_HORIZON_SECONDS and isLongHorizon.
- src/lib/inspector: localtime.ts (local times of a zone as UTC instants,
a skipped time refused, a repeated one taken at its later instant),
create-input.ts (the form, checked in its order) and creator.ts (the
writing, loaded on demand), all at 100 %.
- The .dkc goes to an OPFS temporary file, committed only when complete
and checked, or to memory up to 64 MiB; the writing can be cancelled.
The .dkk stays in memory only; losing it when it is the only
credential asks for confirmation.
- /inspect also cleans the temporary files of /create, and check-build
checks the code loaded on demand of both pages.
Checked in the browser on the production build: a capsule made for four
minutes later opened afterwards in /inspect with the pasted release and
with datekeys decrypt of Go over the network, to the same content. An
adversarial review found one major and eight minor issues, all fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// The opening and the writer are loaded on demand: no page loads noble or
|
|
|
|
|
// age-encryption first, and each page of ON_DEMAND can load the packages of
|
|
|
|
|
// its code when the person opens or creates a capsule.
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
|
|
|
|
|
const OPENING_PACKAGES = /^(?:@noble\/|@scure\/|age-encryption$)/;
|
Phase 3, steps 6 and 7: the create page
/create seals a person's file into a .dkc of format 2 and, when asked, a
portable .dkk, in the browser and without network, with the decisions the
author confirmed in step 6: time_only by default, the zone of the device
with a selector, the warnings of §53 and §50 beyond 365 days, a notice of
preliminary protocol, and files named capsula-<opening time, UTC>.
- lengths.ts: sealedControlLength moves out of writer.ts, and
capsuleLength gives the size of the .dkc before writing it; the
property loop checks it on every capsule (500 seeds pass).
- datekey.ts: LONG_HORIZON_SECONDS and isLongHorizon.
- src/lib/inspector: localtime.ts (local times of a zone as UTC instants,
a skipped time refused, a repeated one taken at its later instant),
create-input.ts (the form, checked in its order) and creator.ts (the
writing, loaded on demand), all at 100 %.
- The .dkc goes to an OPFS temporary file, committed only when complete
and checked, or to memory up to 64 MiB; the writing can be cancelled.
The .dkk stays in memory only; losing it when it is the only
credential asks for confirmation.
- /inspect also cleans the temporary files of /create, and check-build
checks the code loaded on demand of both pages.
Checked in the browser on the production build: a capsule made for four
minutes later opened afterwards in /inspect with the pasted release and
with datekeys decrypt of Go over the network, to the same content. An
adversarial review found one major and eight minor issues, all fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const ON_DEMAND = {
|
|
|
|
|
'inspect.html': ['age-encryption', '@noble/curves', '@noble/ciphers'],
|
|
|
|
|
'create.html': ['age-encryption', '@noble/curves', '@noble/ciphers', '@noble/hashes'],
|
|
|
|
|
};
|
|
|
|
|
for (const name of Object.keys(ON_DEMAND)) {
|
|
|
|
|
if (!htmlFiles.some((f) => basename(f) === name && existsSync(f))) fail(`${name}, a page that loads code on demand, is not in the site`);
|
|
|
|
|
}
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const bundled = (set) => {
|
|
|
|
|
const names = new Set();
|
|
|
|
|
for (const f of set) {
|
|
|
|
|
for (const id of Object.keys(chunks[f]?.modules ?? {})) {
|
|
|
|
|
const pkg = packageOf(id);
|
|
|
|
|
if (pkg !== null) names.add(pkg.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return names;
|
|
|
|
|
};
|
|
|
|
|
for (const f of htmlFiles.filter(existsSync)) {
|
|
|
|
|
const { eager, lazy } = pageScripts(f);
|
|
|
|
|
const first = [...bundled(eager)].filter((n) => OPENING_PACKAGES.test(n));
|
|
|
|
|
if (first.length > 0) fail(`${rel(f)} loads ${first.join(', ')} with the page, not on demand`);
|
Format 3, step 6: /inspect opens format 3
The page opens capsules of format 3. The files go to the temporary
file through a ZipSink, a lone file of one segment as it is and a ZIP
otherwise, or to memory; the page shows the verdicts first, then the
declared author and the comment as unchecked text of the creator, and
then each path as text in a bdi, with its size, its mtime and the
warnings of the CLI of the reference, compared by their key of R7.
- files.ts: pathWarnings, fileFacts, and the names and order of the
downloads: the file itself when it is the only one, with the ZIP of
its folder second (decision 8), or the ZIP and each file.
- opener.ts: OpenedFiles, and noRoom when the ZIP does not fit.
- zipsink.ts: NoRoom, thrown by begin before writing anything.
- check-build.mjs: the tables of pathrule-tables.ts never come with
the first load of a page, and do come with the code on demand of
/inspect and /create.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// The Unicode tables of the paths of format 3 (pathrule-tables.ts, some
|
|
|
|
|
// 120 KB) come with the opening and the writer, never with the page.
|
|
|
|
|
const holdsTables = (c) => Object.keys(chunks[c]?.modules ?? {}).some((id) => id.replaceAll('\\', '/').endsWith('src/lib/dkc/pathrule-tables.ts'));
|
|
|
|
|
if ([...eager].some(holdsTables)) fail(`${rel(f)} loads the tables of pathrule-tables.ts with the page, not on demand`);
|
|
|
|
|
if (ON_DEMAND[basename(f)] !== undefined && ![...lazy].some(holdsTables)) fail(`${rel(f)}: the code loaded on demand lacks pathrule-tables.ts`);
|
Phase 3, steps 6 and 7: the create page
/create seals a person's file into a .dkc of format 2 and, when asked, a
portable .dkk, in the browser and without network, with the decisions the
author confirmed in step 6: time_only by default, the zone of the device
with a selector, the warnings of §53 and §50 beyond 365 days, a notice of
preliminary protocol, and files named capsula-<opening time, UTC>.
- lengths.ts: sealedControlLength moves out of writer.ts, and
capsuleLength gives the size of the .dkc before writing it; the
property loop checks it on every capsule (500 seeds pass).
- datekey.ts: LONG_HORIZON_SECONDS and isLongHorizon.
- src/lib/inspector: localtime.ts (local times of a zone as UTC instants,
a skipped time refused, a repeated one taken at its later instant),
create-input.ts (the form, checked in its order) and creator.ts (the
writing, loaded on demand), all at 100 %.
- The .dkc goes to an OPFS temporary file, committed only when complete
and checked, or to memory up to 64 MiB; the writing can be cancelled.
The .dkk stays in memory only; losing it when it is the only
credential asks for confirmation.
- /inspect also cleans the temporary files of /create, and check-build
checks the code loaded on demand of both pages.
Checked in the browser on the production build: a capsule made for four
minutes later opened afterwards in /inspect with the pasted release and
with datekeys decrypt of Go over the network, to the same content. An
adversarial review found one major and eight minor issues, all fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const later = bundled(lazy);
|
|
|
|
|
for (const n of ON_DEMAND[basename(f)] ?? []) {
|
|
|
|
|
if (!later.has(n)) fail(`${rel(f)}: the code loaded on demand lacks ${n}`);
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// licenses.txt holds the notice of every npm package in the client bundle,
|
|
|
|
|
// of every module of src/ that keeps the notice of another project, and the
|
|
|
|
|
// license of the site (vite.config.ts writes it).
|
|
|
|
|
|
|
|
|
|
const NOTICES = join(BUILD, 'licenses.txt');
|
|
|
|
|
if (!existsSync(NOTICES)) {
|
|
|
|
|
fail('licenses.txt is missing from the site');
|
|
|
|
|
} else {
|
|
|
|
|
const notices = readFileSync(NOTICES, 'utf8');
|
|
|
|
|
for (const [file, chunk] of Object.entries(chunks)) {
|
|
|
|
|
for (const [id, size] of Object.entries(chunk.modules)) {
|
|
|
|
|
const i = id.lastIndexOf('/node_modules/');
|
|
|
|
|
const virtual = /^\0(vite|rolldown)\//.exec(id);
|
|
|
|
|
if (virtual !== null) {
|
|
|
|
|
// The bundler's own code: \0vite/preload-helper.js, \0rolldown/runtime.js.
|
|
|
|
|
const { version } = JSON.parse(readFileSync(join(ROOT, 'node_modules', virtual[1], 'package.json'), 'utf8'));
|
|
|
|
|
if (!notices.includes(`\n${virtual[1]} ${version} (`)) fail(`licenses.txt lacks ${virtual[1]} ${version}, bundled in ${file}`);
|
|
|
|
|
} else if (id.startsWith('\0')) {
|
|
|
|
|
if (size > 0) fail(`${file} bundles the virtual module ${JSON.stringify(id)}, of no known license`);
|
|
|
|
|
} else if (i >= 0) {
|
|
|
|
|
const pkg = packageOf(id);
|
|
|
|
|
const dir = `${id.slice(0, i)}/node_modules/${pkg.name}`;
|
|
|
|
|
const { version } = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
|
|
|
if (!notices.includes(`\n${pkg.name} ${version} (`)) fail(`licenses.txt lacks ${pkg.name} ${version}, bundled in ${file}`);
|
|
|
|
|
} else if (/\/src\/.*\.(ts|js|svelte)$/.test(id) && existsSync(id)) {
|
|
|
|
|
// The copyright lines of a notice kept in the module's header.
|
|
|
|
|
for (const [, line] of readFileSync(id, 'utf8').matchAll(/^\/\/ {3}(.*copyright.*)$/gim)) {
|
|
|
|
|
if (!notices.includes(line)) fail(`licenses.txt lacks "${line}" of ${relative(ROOT, id).split(sep).join('/')}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const own = readFileSync(join(ROOT, 'LICENSE'), 'utf8').trim();
|
|
|
|
|
if (!notices.includes(own)) fail('licenses.txt lacks the license of the site');
|
|
|
|
|
}
|
|
|
|
|
|
Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
if (problems.length > 0) {
|
|
|
|
|
console.error(`build check failed, ${problems.length} problems:`);
|
|
|
|
|
for (const p of problems) console.error(` - ${p}`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
const size = files.reduce((n, f) => n + statSync(f).size, 0);
|
|
|
|
|
console.log(`build check passed: ${htmlFiles.length} prerendered pages, ${files.length} files, ${size} bytes`);
|
|
|
|
|
for (const [i, f] of htmlFiles.entries()) {
|
|
|
|
|
const p = policies[i];
|
|
|
|
|
console.log(` ${rel(f)}: CSP ${[...p].map(([d, s]) => `${d} ${s.join(' ')}`).join('; ')}`);
|
Phase 2, step 8: the open action of /inspect
After steps 1 to 8, a valid capsule whose date has passed on the device
clock can be opened in the page: steps 9 to 18 of spec section 63 with
open, loaded on demand with a dynamic import (opener.ts), so noble and
age-encryption stay out of the first load of every page.
- The release is supplied directly by the person (spec 63, step 10):
drand's JSON answer or the bare signature, pasted after opening the
drand URL the page links to, or the release in the record of an
official fixture. The page never fetches it and reads only its round
and signature (spec 11, 13). The CSP is unchanged.
- time_and_key credentials: a .dkk (readAccessKey reads at most
12 bytes + 16 MiB + 1) or age identities, one per line.
- The plaintext of the person's own file goes to a temporary OPFS file
(tempfile.ts), committed only after step 18 (spec 56), offered for
download and deleted on request, with another capsule, on pagehide
and, if left over, on the next visit. One directory and one Web Lock
per tab keep other tabs' clean-up away from files in use. Without
OPFS, or when the browser refuses it, capsules up to 64 MiB open in
memory. An opening in progress stops when another capsule is loaded.
- opening.ts builds the page model of steps 9 to 18 as the reference
records them; fixtures show their plaintext and compare its SHA-256
with their record.
- licenses.txt: the notices of tlock-js (ibe.ts) and age (bech32.ts),
the license of every package in the client bundle, Vite's and
rolldown's runtime code, and the site's own license. check-build now
fails if a notice is missing, or if a page loads noble, @scure/base
or age-encryption with its first load.
- The home page no longer says that the page never asks for keys.
Checked in the browser on the production build: the time_only,
time_and_key_portable (with its .dkk) and time_and_key_recipients (with
a pasted identity) fixtures open with the SHA-256 of their records; a
tampered signature fails at step 10 and a tampered STREAM chunk at step
17, with no download and no file left; an own file opens to OPFS,
downloads without a CSP violation and is deleted with its lock; a left
over directory goes on the next visit; no request leaves the origin.
An adversarial review (four dimensions, each finding checked by a
refuter) confirmed 15 findings, all fixed here.
2611 tests; coverage 100 % of the new modules, now a threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
const { eager, lazy } = pageScripts(f);
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
console.log(` ${rel(f)}: JavaScript ${weight(eager)}; on demand ${weight(lazy)}`);
|
Inspector page: static SvelteKit site with /inspect (plan step 5)
A prerendered static site (adapter-static) with a landing page and
/inspect, which runs spec §63 steps 1 to 8 on a .dkc chosen with the file
picker, dropped anywhere on the page, or taken from the official fixtures
bundled at build time. It shows every step, the decoded header, the unlock
date in UTC and local time, and each extension's id, version, criticality,
length and hex, with a text view and an informative CBOR diagnostic view,
all escaped and labelled as unauthenticated before step 15. Copiar JSON
copies the exact "datekeys inspect -json" view.
No network: a hash-mode Content-Security-Policy with connect-src 'self'
is the first element of every page, and scripts/check-build.mjs verifies
it, the fixtures and the absence of external URLs after every build.
Large files are read only up to what steps 1 to 8 need.
Reviewed for design and accessibility (WCAG AA contrast, keyboard,
focus, live status, 360 px), security and correctness; 262 tests pass,
svelte-check has no warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
}
|
Phase 2, step 2: runtime dependencies and their guards
- age-encryption 0.3.1, @noble/curves 2.4.0 (moved from dev) and
@noble/hashes 2.4.0 become exact runtime dependencies (plan section 3,
decision 5). The lockfile gains six packages: age-encryption,
@noble/ciphers 2.4.0, @scure/base 2.4.0, @noble/post-quantum 0.5.4 and
its own @noble/curves and @noble/hashes 2.0.1. No file of src/ imports
them yet, so the site does not change.
- src/lib/dependencies.test.ts guards them. package.json declares exactly
these three, pinned. The lockfile has no tlock-js, drand-client or noble
1.x, and no noble 2.x copy other than 2.4.0 at the root and 2.0.1 under
@noble/post-quantum. No file of src/ imports tlock-js or drand-client.
Only ibe.ts, release.ts and the tests name @noble/, always subpaths of
@noble/curves or @noble/hashes that resolve to the root 2.4.0 copy.
Every check also runs on bad inputs. It replaces the "only tests import
@noble/curves" test of bls12381.contrast.test.ts.
- vite.config.ts records the modules of each client chunk in
.svelte-kit/output/client-modules.json. check-build.mjs fails if the
bundle holds tlock-js, drand-client or @babel/*, or a nested copy
other than noble under @noble/post-quantum. It also reports the
JavaScript each page loads: /inspect today loads 157 KB, 58.7 KB gzip.
- Measured with a probe build (Vite 8, minified, gzip 9): the Decrypter
is 48 KB gzip, with the Encrypter 56 KB, noble BLS12-381 plus SHA-256
28 KB, and all of them 73 KB. age-encryption imports its hybrid ML-KEM
recipients statically, so post-quantum and its nested noble copy are
about 99 KB of the Decrypter's 212 KB of rendered code.
- npm audit --omit=dev: no vulnerabilities. The full audit finds two low
ones in the tooling: cookie < 0.7.0 through @sveltejs/kit 2.70.3,
which is the latest version and affects only SvelteKit's server.
npm run verify is green: 2,384 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
console.log(` npm packages in the client bundle: ${[...packages].sort().join(', ') || 'none'}`);
|