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.
DateKeys-App/src/lib/inspector/report.ts

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;
}

Powered by TurnKey Linux.