You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
286 lines
9.8 KiB
286 lines
9.8 KiB
// 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;
|
|
}
|