// The page model of one inspection: every value the inspector shows, built // from the library's Inspection with no DOM and no clock, so that it can be // tested without a browser. import { canonicalJSON, chainHashHex, compactDateKey, DKC_PRELUDE_SIZE, type ErrorCode, type Extension, type ExtensionRegistry, type Format, formatRFC3339, type Inspection, inspectView, type InspectView, policyName, STANZA_TLOCK, type StanzaInfo, toHex, type Unusable, } from '../dkc/index.ts'; import { cborView, type Diagnostic } from './diagnostic.ts'; import { cliJSON, displayText, instantToEpochMs, INSPECT_STEPS, printableText, stepGloss, stepName } from './format.ts'; export type StepState = 'ok' | 'failed' | 'not-run'; /** One of the eight steps, as the CLI reports it, or not run. */ export interface StepRow { readonly step: number; /** The CLI name, for example "header validation". */ readonly name: string; /** What the step checks, in Spanish. */ readonly gloss: string; readonly state: StepState; /** The pass detail or the error message; undefined when not run. */ readonly detail?: string; /** The normative code of a failed step. */ readonly code?: ErrorCode; } /** A value read from the capsule next to the one the pinned profile expects. */ export interface Comparison { readonly label: string; readonly found: string | undefined; readonly expected: string; readonly match: boolean; } /** One PUBLIC_HEADER extension, with every view of its data. */ export interface ExtensionRow { readonly critical: boolean; /** extension_id as shown: as it is, or quoted and escaped. */ readonly id: string; /** The identifier holds non-printable characters and is shown escaped. */ readonly idEscaped: boolean; readonly version: number; /** Whether this reader implements the extension. */ readonly known: boolean; /** The data error of a known noncritical extension the reader cannot use. */ readonly unusable?: string; /** Data length in bytes; 0 when the extension carries no data. */ readonly length: number; /** Data in lowercase hexadecimal. */ readonly hex: string; /** The data as text when it is valid printable UTF-8. */ readonly text?: string; /** Informative diagnostic notation when the data is CBOR of the §58 profile. */ readonly cbor?: Diagnostic; } export interface CapsuleFacts { readonly capsuleId: string; readonly accessPolicy: string; /** The compact dk1_ DateKey. */ readonly dateKey: string; /** Its decoded payload, the canonical JSON of spec §18. */ readonly dateKeyJSON: string; /** The network field of the DateKey: the profile_id. */ readonly network: string; readonly round: number; } export interface ProfileFacts { readonly id: string; readonly provider: string; readonly network: string; readonly chainHash: string; readonly period: number; readonly genesis: string; readonly scheme: string; } export interface PreludeFacts { /** The 16 prelude bytes in hexadecimal. */ readonly hex: string; readonly magic: string; /** * VERSION, the capsule format (spec §22): format 1 does not hide the * number of credentials nor the exact length of the content (§55.2, §70). */ readonly format: Format; readonly flags: string; readonly reserved: string; readonly publicHeaderLen: number; readonly sealedControlLen: number; /** Where PAYLOAD_AGE starts. */ readonly payloadOffset: number; /** Bytes of PAYLOAD_AGE, when the file reaches its start. */ readonly payloadLength?: number; } export interface Report { readonly fileName: string; /** Size of the whole file. */ readonly size: number; /** Bytes read for the inspection (see inspectedLength). */ readonly readLength: number; readonly valid: boolean; readonly failure?: { readonly step: number; readonly code: ErrorCode; readonly message: string }; /** Always the eight steps, in order. */ readonly steps: readonly StepRow[]; /** The view of `datekeys inspect -json`. */ readonly view: InspectView; /** Its exact JSON text. */ readonly json: string; readonly capsule?: CapsuleFacts; readonly profile?: ProfileFacts; /** The effective round time, once step 7 passed. */ readonly unlock?: { readonly rfc3339: string; readonly epochMs: number | undefined }; readonly prelude?: PreludeFacts; readonly outerStanzas?: readonly StanzaInfo[]; readonly payloadStanzas?: readonly StanzaInfo[]; /** The tlock stanza arguments against the pinned profile. */ readonly tlock?: readonly Comparison[]; /** Critical extensions first, then noncritical; undefined until PUBLIC_HEADER decoded. */ readonly extensions?: readonly ExtensionRow[]; /** * The public note of the header (spec §24.1): text of the creator that * nobody has checked, or `unusable` when the header holds a note that * breaks the rules of text, which is not shown. Undefined without a note. */ readonly note?: { readonly text: string } | { readonly unusable: true }; } export interface ReportInput { readonly fileName: string; /** The bytes that were inspected: the file or its inspected prefix. */ readonly bytes: Uint8Array; /** Size of the whole file. */ readonly size: number; readonly inspection: Inspection; /** The extension registry given to the inspection, if any. */ readonly extensions?: ExtensionRegistry; } const hex8 = (v: number): string => `0x${v.toString(16).padStart(2, '0')}`; export function buildReport(input: ReportInput): Report { const { inspection: r, bytes, size } = input; const view = inspectView(r, input.fileName); const out: { -readonly [K in keyof Report]: Report[K]; } = { fileName: input.fileName, size, readLength: bytes.length, valid: r.error === undefined, steps: steps(r), view, json: cliJSON(view), }; if (r.error !== undefined) { const last = r.checks.at(-1)!; out.failure = { step: last.step, code: r.error.code, message: r.error.message }; } if (r.prelude !== undefined && r.payloadOffset !== undefined) { const p: { -readonly [K in keyof PreludeFacts]: PreludeFacts[K] } = { hex: toHex(bytes.subarray(0, DKC_PRELUDE_SIZE)), magic: 'DKC1', format: r.prelude.format, flags: hex8(bytes[5]!), reserved: `0x${toHex(bytes.subarray(6, 8))}`, publicHeaderLen: r.prelude.publicHeaderLen, sealedControlLen: r.prelude.sealedControlLen, payloadOffset: r.payloadOffset, }; if (size >= r.payloadOffset) p.payloadLength = size - r.payloadOffset; out.prelude = p; } const h = r.header; if (h !== undefined) { out.capsule = { capsuleId: toHex(h.capsuleId), accessPolicy: policyName(h.policy), dateKey: compactDateKey(h.dateKey), dateKeyJSON: canonicalJSON(h.dateKey) ?? '', network: h.dateKey.profileId, round: h.dateKey.round, }; out.extensions = [ ...h.critical.map((e) => extensionRow(e, true, r.unusableExtensions, input.extensions)), ...h.noncritical.map((e) => extensionRow(e, false, r.unusableExtensions, input.extensions)), ]; } const p = r.profile; if (p !== undefined) { out.profile = { id: p.id, provider: p.provider, network: p.network, chainHash: chainHashHex(p), period: p.period, genesis: formatRFC3339({ seconds: p.genesisTime, nanos: 0 }), scheme: p.scheme, }; } if (r.unlockAt !== undefined) { out.unlock = { rfc3339: formatRFC3339(r.unlockAt), epochMs: instantToEpochMs(r.unlockAt) }; } if (r.outerStanzas !== undefined) out.outerStanzas = r.outerStanzas; if (r.payloadStanzas !== undefined) out.payloadStanzas = r.payloadStanzas; if (r.outerStanzas !== undefined && h !== undefined && p !== undefined) { const t = r.outerStanzas.find((s) => s.type === STANZA_TLOCK); if (t !== undefined) { out.tlock = [ compare('Argumentos', String(t.args.length), '2'), compare('Ronda', t.args[0], String(h.dateKey.round)), compare('Cadena (chain hash)', t.args[1], chainHashHex(p)), ]; } } if (r.publicNote !== undefined) out.note = { text: r.publicNote }; else if (r.unusableNote === true) out.note = { unusable: true }; return out; } function compare(label: string, found: string | undefined, expected: string): Comparison { return { label, found, expected, match: found === expected }; } function steps(r: Inspection): StepRow[] { return INSPECT_STEPS.map((step): StepRow => { const c = r.checks.find((x) => x.step === step); const base = { step, name: stepName(step), gloss: stepGloss(step) }; if (c === undefined) return { ...base, state: 'not-run' }; if (c.ok) return c.detail === undefined ? { ...base, state: 'ok' } : { ...base, state: 'ok', detail: c.detail }; const failed: { -readonly [K in keyof StepRow]: StepRow[K] } = { ...base, state: 'failed' }; if (c.detail !== undefined) failed.detail = c.detail; if (r.error !== undefined) failed.code = r.error.code; return failed; }); } /** * The row of one extension of PUBLIC_HEADER, CONTROL_CBOR or a .dkk, given * the known noncritical extensions of that object that the registry rejects. */ export function extensionRow(e: Extension, critical: boolean, unusable: readonly Unusable[], reg: ExtensionRegistry | undefined): ExtensionRow { const id = displayText(e.id); const row: { -readonly [K in keyof ExtensionRow]: ExtensionRow[K] } = { critical, id: id.text, idEscaped: id.escaped, version: e.version, known: reg?.known(e.id, e.version) ?? false, length: e.data?.length ?? 0, hex: e.data === undefined ? '' : toHex(e.data), }; const u = critical ? undefined : unusable.find((x) => x.id === e.id && x.version === e.version); if (u !== undefined) row.unusable = u.error.message; if (e.data !== undefined) { const text = printableText(e.data); if (text !== undefined) row.text = text; const cbor = cborView(e.data); if (cbor !== undefined) row.cbor = cbor; } return row; }