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 { describe, expect, it } from 'vitest';
|
|
|
|
|
import { defaultRegistry, type ExtensionRegistry, ExtensionSet, inspectView, inspectWith } from '../dkc/index.ts';
|
|
|
|
|
import { frame, replaceText, split } from '../dkc/testing/capsule.ts';
|
|
|
|
|
import { arr, b, dkRound, ext, map, t, u, bn } from '../dkc/testing/cborhex.ts';
|
|
|
|
|
import { h, listTestdata, readBytes, readJSON } from '../dkc/testing/testdata.ts';
|
|
|
|
|
import { cliJSON } from './format.ts';
|
|
|
|
|
import { buildReport, type Report } from './report.ts';
|
|
|
|
|
|
|
|
|
|
const registry = await defaultRegistry();
|
|
|
|
|
const timeOnly = readBytes('fixtures/time_only_extensions.dkc');
|
|
|
|
|
const parts = split(timeOnly);
|
|
|
|
|
|
|
|
|
|
function report(dkc: Uint8Array, extensions?: ExtensionRegistry, fileName = 'x.dkc'): Report {
|
|
|
|
|
const inspection = inspectWith(dkc, registry, extensions);
|
|
|
|
|
return buildReport(extensions === undefined ? { fileName, bytes: dkc, size: dkc.length, inspection } : { fileName, bytes: dkc, size: dkc.length, inspection, extensions });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A PUBLIC_HEADER like the one of time_only_extensions, with other extensions.
|
|
|
|
|
function header(o: { crit?: string; non?: string; dk?: string }): Uint8Array {
|
|
|
|
|
const entries: [number, string][] = [
|
|
|
|
|
[0, t('datekeycap')],
|
|
|
|
|
[1, u(1)],
|
|
|
|
|
[2, bn(16, 7)],
|
|
|
|
|
[3, o.dk ?? t(dkRound(2000))],
|
|
|
|
|
[4, u(0)],
|
|
|
|
|
];
|
|
|
|
|
if (o.crit) entries.push([5, o.crit]);
|
|
|
|
|
if (o.non) entries.push([6, o.non]);
|
|
|
|
|
return h(map(...entries));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FixtureRecord {
|
Read capsule format 2 of spec v0.9
Syncs testdata with datekeys-go at spec-v0.9 (7e2d83c) and moves the
reader to the DateKeys Protocol Specification v0.9. Both capsule formats
are read; a format 1 capsule keeps the verdict v0.8.2 gave it.
- framing: the VERSION of the prelude is the capsule format, 1 or 2
(Prelude.format, FORMAT_1, FORMAT_2, isFormat).
- control: decodeControl and encodeControl take the format; schema
version 2 adds payload_length (8 bytes, at most L_MAX) and padding
(1 or 2).
- padding.ts: the rules bloque256 and reforzado of spec §29.1, exact up
to L_MAX with BigInt bit lengths and ceil roundings, and the length of
PAYLOAD_AGE.
- open: exactly 16 stanzas in INNER_ACCESS_AGE of format 2 (step 12), P
at step 16, and at step 17 a plaintext of exactly P bytes whose
padding is zero; only the first L bytes are delivered, never the
padding. Step 17 is recorded when it passes, and step 18 gives the
bytes of content, as the reference does. Opened reports the format, L
and, in format 2, the rule and P.
- inspect: the JSON view carries format, as datekeys inspect -json.
- The page shows the format, warns about format 1, and gives the
padding rule and P once a format 2 capsule opens.
Tests: the twelve fixtures, the 125 mutation cases through open from
memory and from a Blob, the 4380 differential cases, padding.json, the
format 2 CBOR vectors, and padding.test.ts against a BigInt statement of
§29.1. The error texts of the 125 corpus cases were compared with
capsule.Open at spec-v0.9. ibe-vectors.json gains the seven format 2
fixtures from scripts/ibe-go-vectors.go; its frozen values are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
format: number;
|
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
|
|
|
capsule_id: string;
|
|
|
|
|
datekey: string;
|
|
|
|
|
access_policy: string;
|
|
|
|
|
unlock_at: string;
|
|
|
|
|
prelude: string;
|
|
|
|
|
outer_stanzas: { type: string; args: string[] }[];
|
|
|
|
|
payload_stanzas: { type: string; args: string[] }[];
|
|
|
|
|
header_extensions?: { critical: boolean; id: string; version: number; data?: string }[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('buildReport', () => {
|
|
|
|
|
it('shows every official fixture as its JSON record', () => {
|
|
|
|
|
for (const f of listTestdata('fixtures', '.dkc')) {
|
|
|
|
|
const dkc = readBytes(f);
|
|
|
|
|
const want = readJSON<FixtureRecord>(f.replace(/\.dkc$/, '.json'));
|
|
|
|
|
const name = f.slice('fixtures/'.length);
|
|
|
|
|
const r = report(dkc, undefined, name);
|
|
|
|
|
expect(r.valid, f).toBe(true);
|
|
|
|
|
expect(r.failure).toBeUndefined();
|
|
|
|
|
expect(r.steps.map((s) => [s.step, s.state])).toEqual([1, 2, 3, 4, 5, 6, 7, 8].map((s) => [s, 'ok']));
|
|
|
|
|
expect(r.capsule).toMatchObject({ capsuleId: want.capsule_id, dateKey: want.datekey, accessPolicy: want.access_policy, network: 'datekeys:quicknet:v1' });
|
|
|
|
|
expect(JSON.parse(r.capsule!.dateKeyJSON)).toEqual({ version: 1, network: 'datekeys:quicknet:v1', round: r.capsule!.round });
|
|
|
|
|
expect(r.unlock?.rfc3339).toBe(want.unlock_at);
|
|
|
|
|
expect(r.unlock?.epochMs).toBe(Date.parse(want.unlock_at));
|
|
|
|
|
expect(r.prelude?.hex).toBe(want.prelude);
|
Read capsule format 2 of spec v0.9
Syncs testdata with datekeys-go at spec-v0.9 (7e2d83c) and moves the
reader to the DateKeys Protocol Specification v0.9. Both capsule formats
are read; a format 1 capsule keeps the verdict v0.8.2 gave it.
- framing: the VERSION of the prelude is the capsule format, 1 or 2
(Prelude.format, FORMAT_1, FORMAT_2, isFormat).
- control: decodeControl and encodeControl take the format; schema
version 2 adds payload_length (8 bytes, at most L_MAX) and padding
(1 or 2).
- padding.ts: the rules bloque256 and reforzado of spec §29.1, exact up
to L_MAX with BigInt bit lengths and ceil roundings, and the length of
PAYLOAD_AGE.
- open: exactly 16 stanzas in INNER_ACCESS_AGE of format 2 (step 12), P
at step 16, and at step 17 a plaintext of exactly P bytes whose
padding is zero; only the first L bytes are delivered, never the
padding. Step 17 is recorded when it passes, and step 18 gives the
bytes of content, as the reference does. Opened reports the format, L
and, in format 2, the rule and P.
- inspect: the JSON view carries format, as datekeys inspect -json.
- The page shows the format, warns about format 1, and gives the
padding rule and P once a format 2 capsule opens.
Tests: the twelve fixtures, the 125 mutation cases through open from
memory and from a Blob, the 4380 differential cases, padding.json, the
format 2 CBOR vectors, and padding.test.ts against a BigInt statement of
§29.1. The error texts of the 125 corpus cases were compared with
capsule.Open at spec-v0.9. ibe-vectors.json gains the seven format 2
fixtures from scripts/ibe-go-vectors.go; its frozen values are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
expect(r.prelude).toMatchObject({ magic: 'DKC1', format: want.format, flags: '0x00', reserved: '0x0000' });
|
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
|
|
|
expect(r.prelude!.payloadOffset + r.prelude!.payloadLength!).toBe(dkc.length);
|
|
|
|
|
expect(r.outerStanzas).toEqual(want.outer_stanzas);
|
|
|
|
|
expect(r.payloadStanzas).toEqual(want.payload_stanzas);
|
|
|
|
|
expect(r.tlock?.every((c) => c.match)).toBe(true);
|
|
|
|
|
expect(r.profile).toMatchObject({ id: 'datekeys:quicknet:v1', network: 'quicknet', provider: 'drand', period: 3, genesis: '2023-08-23T15:09:27Z' });
|
|
|
|
|
expect(r.extensions?.map((e) => ({ critical: e.critical, id: e.id, version: e.version, data: e.hex }))).toEqual(
|
|
|
|
|
(want.header_extensions ?? []).map((e) => ({ critical: e.critical, id: e.id, version: e.version, data: e.data ?? '' })),
|
|
|
|
|
);
|
|
|
|
|
expect(r.view).toEqual(inspectView(inspectWith(dkc, registry), name));
|
|
|
|
|
expect(r.json).toBe(cliJSON(r.view));
|
|
|
|
|
expect(r.view.file).toBe(name);
|
|
|
|
|
expect(r.readLength).toBe(dkc.length);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows header extension data as text, never as CBOR when it is not', () => {
|
|
|
|
|
const r = report(timeOnly);
|
|
|
|
|
expect(r.extensions).toEqual([
|
|
|
|
|
{
|
|
|
|
|
critical: false,
|
|
|
|
|
id: 'org.example.label',
|
|
|
|
|
idEscaped: false,
|
|
|
|
|
version: 1,
|
|
|
|
|
known: false,
|
|
|
|
|
length: 12,
|
|
|
|
|
hex: '7075626c6963206c6162656c',
|
|
|
|
|
text: 'public label',
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('decodes CBOR data informatively and escapes identifiers', () => {
|
|
|
|
|
const hb = header({ non: arr(ext('a\u202eb', 3, b('a2000701667365616c6564')), ext('z', 1)) });
|
|
|
|
|
const r = report(frame({ ...parts, header: hb }));
|
|
|
|
|
expect(r.valid).toBe(true);
|
|
|
|
|
expect(r.extensions).toEqual([
|
|
|
|
|
{
|
|
|
|
|
critical: false,
|
|
|
|
|
id: '"a\\u202eb"',
|
|
|
|
|
idEscaped: true,
|
|
|
|
|
version: 3,
|
|
|
|
|
known: false,
|
|
|
|
|
length: 11,
|
|
|
|
|
hex: 'a2000701667365616c6564',
|
|
|
|
|
cbor: { text: '{\n 0: 7,\n 1: "sealed"\n}', truncated: false },
|
|
|
|
|
},
|
|
|
|
|
{ critical: false, id: 'z', idEscaped: false, version: 1, known: false, length: 0, hex: '' },
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('reports an unknown critical extension at step 4 and still lists it', () => {
|
|
|
|
|
const hb = header({ crit: arr(ext('org.example.must-understand', 1, b('01'))) });
|
|
|
|
|
const r = report(frame({ ...parts, header: hb }));
|
|
|
|
|
expect(r.valid).toBe(false);
|
|
|
|
|
expect(r.failure).toEqual({
|
|
|
|
|
step: 4,
|
|
|
|
|
code: 'ERR_EXTENSION_CRITICAL_UNKNOWN',
|
|
|
|
|
message: 'capsule: PUBLIC_HEADER: extension org.example.must-understand v1: ERR_EXTENSION_CRITICAL_UNKNOWN',
|
|
|
|
|
});
|
|
|
|
|
expect(r.steps.map((s) => s.state)).toEqual(['ok', 'ok', 'ok', 'failed', 'not-run', 'not-run', 'not-run', 'not-run']);
|
|
|
|
|
expect(r.steps[3]).toMatchObject({ code: 'ERR_EXTENSION_CRITICAL_UNKNOWN', name: 'header validation' });
|
|
|
|
|
expect(r.steps[4]!.detail).toBeUndefined();
|
|
|
|
|
expect(r.extensions).toEqual([
|
|
|
|
|
{ critical: true, id: 'org.example.must-understand', idEscaped: false, version: 1, known: false, length: 1, hex: '01', cbor: { text: '1', truncated: false } },
|
|
|
|
|
]);
|
|
|
|
|
expect(r.capsule?.capsuleId).toBe('07'.repeat(16));
|
|
|
|
|
expect(r.unlock).toBeUndefined();
|
|
|
|
|
expect(r.outerStanzas).toBeUndefined();
|
|
|
|
|
expect(r.tlock).toBeUndefined();
|
|
|
|
|
expect(JSON.parse(r.json)).toMatchObject({ valid: false, error: 'ERR_EXTENSION_CRITICAL_UNKNOWN' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('marks known extensions and the unusable ones', () => {
|
|
|
|
|
const reg: ExtensionRegistry = {
|
|
|
|
|
known: (id) => id === 'a' || id === 'b',
|
|
|
|
|
validateData: (e) => (e.id === 'b' ? new Error('bad') : undefined),
|
|
|
|
|
};
|
|
|
|
|
const r = report(frame({ ...parts, header: header({ crit: arr(ext('a', 1, b('01'))), non: arr(ext('b', 1, b('02')), ext('c', 1)) }) }), reg);
|
|
|
|
|
expect(r.valid).toBe(true);
|
|
|
|
|
expect(r.extensions?.map((e) => [e.id, e.critical, e.known, e.unusable])).toEqual([
|
|
|
|
|
['a', true, true, undefined],
|
|
|
|
|
['b', false, true, 'extension b v1: data: bad: ERR_EXTENSION_DATA_INVALID'],
|
|
|
|
|
['c', false, false, undefined],
|
|
|
|
|
]);
|
|
|
|
|
const known = report(frame({ ...parts, header: header({ crit: arr(ext('a', 1)) }) }), new ExtensionSet([['a', [1]]]));
|
|
|
|
|
expect(known.extensions?.[0]?.known).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('compares the tlock stanza with the pinned profile', () => {
|
|
|
|
|
const round = report(replaceText(timeOnly, '-> tlock 2000 ', '-> tlock 2001 '));
|
|
|
|
|
expect(round.failure?.code).toBe('ERR_ROUND_MISMATCH');
|
|
|
|
|
expect(round.unlock?.rfc3339).toBe('2023-08-23T16:49:24Z');
|
|
|
|
|
expect(round.tlock).toEqual([
|
|
|
|
|
{ label: 'Argumentos', found: '2', expected: '2', match: true },
|
|
|
|
|
{ label: 'Ronda', found: '2001', expected: '2000', match: false },
|
|
|
|
|
{
|
|
|
|
|
label: 'Cadena (chain hash)',
|
|
|
|
|
found: '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971',
|
|
|
|
|
expected: '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971',
|
|
|
|
|
match: true,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
const noChain = report(frame({ ...parts, sealed: replaceText(parts.sealed, ' 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971', '') }));
|
|
|
|
|
expect(noChain.tlock?.map((c) => [c.found, c.match])).toEqual([
|
|
|
|
|
['1', false],
|
|
|
|
|
['2000', true],
|
|
|
|
|
[undefined, false],
|
|
|
|
|
]);
|
|
|
|
|
const noTlock = report(frame({ ...parts, sealed: replaceText(parts.sealed, '-> tlock', '-> TLOCK') }));
|
|
|
|
|
expect(noTlock.failure?.step).toBe(5);
|
|
|
|
|
expect(noTlock.outerStanzas?.map((s) => s.type)).toEqual(['TLOCK']);
|
|
|
|
|
expect(noTlock.tlock).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps what the framing steps found', () => {
|
|
|
|
|
const r = report(timeOnly.subarray(0, 100));
|
|
|
|
|
expect(r.failure).toMatchObject({ step: 3, code: 'ERR_INTEGRITY' });
|
|
|
|
|
expect(r.prelude).toMatchObject({ publicHeaderLen: 159, sealedControlLen: 482, payloadOffset: 657 });
|
|
|
|
|
expect(r.prelude?.payloadLength).toBeUndefined();
|
|
|
|
|
expect(r.capsule).toBeUndefined();
|
|
|
|
|
expect(r.extensions).toBeUndefined();
|
|
|
|
|
const empty = report(new Uint8Array(0));
|
|
|
|
|
expect(empty.failure).toMatchObject({ step: 1, code: 'ERR_INVALID_MAGIC' });
|
|
|
|
|
expect(empty.prelude).toBeUndefined();
|
|
|
|
|
expect(empty.steps.filter((s) => s.state === 'not-run')).toHaveLength(7);
|
|
|
|
|
// An unknown profile: the header decoded, no profile, no comparison.
|
|
|
|
|
const other = report(frame({ ...parts, header: header({ dk: t(dkRound(2000, 'datekeys:evmnet:v1')) }) }));
|
|
|
|
|
expect(other.failure?.code).toBe('ERR_UNKNOWN_PROFILE');
|
|
|
|
|
expect(other.capsule?.network).toBe('datekeys:evmnet:v1');
|
|
|
|
|
expect(other.profile).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('records the prefix that was read', () => {
|
|
|
|
|
const inspection = inspectWith(timeOnly, registry);
|
|
|
|
|
const r = buildReport({ fileName: 'big.dkc', bytes: timeOnly, size: timeOnly.length + 5_000_000, inspection });
|
|
|
|
|
expect(r.readLength).toBe(timeOnly.length);
|
|
|
|
|
expect(r.size).toBe(timeOnly.length + 5_000_000);
|
|
|
|
|
expect(r.prelude!.payloadLength).toBe(timeOnly.length + 5_000_000 - 657);
|
|
|
|
|
});
|
|
|
|
|
});
|