diff --git a/packages/spec/src/__tests__/evidence-level-intrinsics.test.ts b/packages/spec/src/__tests__/evidence-level-intrinsics.test.ts new file mode 100644 index 000000000..d6c3c5eff --- /dev/null +++ b/packages/spec/src/__tests__/evidence-level-intrinsics.test.ts @@ -0,0 +1,314 @@ +/** + * #345's final evidence-level.ts under post-load realm mutation (steward #5186, + * evidence #6440). Nothing that runs after @pcc/spec loads may change a level, a + * verdict, a contradiction or an input refusal it returns. On master 37adc7bf it + * could: with Array.prototype.indexOf replaced after load, + * meetsEvidenceLevel("submitted", "inspected_output") answered true. + * + * The patch harness runs in child processes (harness/evidence-level-realm.ts), + * one per scenario: load, build every item, run them clean, make one change + * (replace an intrinsic, write onto Object.prototype, push onto an exported + * list), run them again and undo it. Every answer must be IDENTICAL to the clean + * one. Children, because vitest runs in the realm of the test it runs (see + * profile-admission-intrinsics.test.ts on #519). + * + * The rest of this file changes nothing in its own realm: source scans (the + * rules of #519's profile-admission-intrinsics.test.ts), frozen exports, no + * reachable RegExp, and the principal-id predicate held equal to the pattern it + * replaced. + */ +import { execFile, execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { availableParallelism } from "node:os"; +import { fileURLToPath } from "node:url"; + +import { beforeAll, describe, expect, it } from "vitest"; + +import * as levelModule from "../evidence/evidence-level.js"; +import { evidenceLevelOfBundles, type AuthenticatedBundle } from "../evidence/evidence-level.js"; + +// -- the child realms -- +const SPEC_DIR = fileURLToPath(new URL("../../", import.meta.url)); +const REALM = fileURLToPath(new URL("./harness/evidence-level-realm.ts", import.meta.url)); +const NODE_ARGS = ["--import", "tsx", REALM]; + +type Row = [label: string, value: unknown]; +interface Outcome { + scenario: string; + clean: Row[]; + patched?: unknown; + error?: string; +} + +function lastLine(stdout: string): string { + const lines = stdout.trim().split("\n"); + return lines[lines.length - 1] ?? ""; +} + +const SCENARIOS: string[] = JSON.parse(lastLine(execFileSync(process.execPath, [...NODE_ARGS, "--list"], { cwd: SPEC_DIR, encoding: "utf8" }))); + +function runScenario(id: string): Promise { + return new Promise((resolve) => { + execFile( + process.execPath, + [...NODE_ARGS, id], + { cwd: SPEC_DIR, encoding: "utf8", timeout: 120_000, maxBuffer: 1 << 26 }, + (err, stdout, stderr) => { + try { + resolve(JSON.parse(lastLine(stdout)) as Outcome); + } catch { + resolve({ scenario: id, clean: [], error: `${err?.message ?? "no result line"} | ${stderr.slice(-800)}` }); + } + }, + ); + }); +} + +const OUTCOMES = new Map(); + +beforeAll(async () => { + const queue = [...SCENARIOS]; + const workers = Math.max(2, Math.min(8, availableParallelism() - 1)); + await Promise.all( + Array.from({ length: workers }, async () => { + for (let id = queue.shift(); id !== undefined; id = queue.shift()) OUTCOMES.set(id, await runScenario(id)); + }), + ); +}, 900_000); + +/** Each answer that changed. */ +function changes(clean: Row[], patched: Row[]): string[] { + const out: string[] = []; + for (let i = 0; i < clean.length; i++) { + const [label, before] = clean[i]!; + const a = JSON.stringify(before); + const b = JSON.stringify(patched[i]?.[1]); + if (a !== b) out.push(`${label}: ${a} -> ${b}`); + } + return out; +} + +describe("evidence-level answers do not change under post-load realm mutation (harness/evidence-level-realm.ts)", () => { + it("lists the scenarios, and every child answers with the same clean rows", () => { + expect(SCENARIOS.length).toBeGreaterThanOrEqual(50); + const first = OUTCOMES.get(SCENARIOS[0]!); + expect(first?.error).toBeUndefined(); + expect(first?.clean.length).toBeGreaterThanOrEqual(100); + for (const id of SCENARIOS) expect(JSON.stringify(OUTCOMES.get(id)?.clean), id).toBe(JSON.stringify(first?.clean)); + }); + + it.each(SCENARIOS)("%s", (id) => { + const outcome = OUTCOMES.get(id)!; + expect(outcome.error, "the child failed").toBeUndefined(); + expect(Array.isArray(outcome.patched), String(outcome.patched)).toBe(true); + expect(changes(outcome.clean, outcome.patched as Row[])).toEqual([]); + }); +}); + +// -- source scans: #519's rules (profile-admission-intrinsics.test.ts), unchanged -- +/** + * The code with comments and strings blanked, and line breaks kept. A template + * literal's `${...}` substitutions are kept as code, so a call inside one is + * scanned too. + */ +function codeOnly(source: string): string { + let i = 0; + const code = (untilBrace: boolean): string => { + let out = ""; + let depth = 0; + while (i < source.length) { + const c = source[i]!; + const next = source[i + 1]; + if (untilBrace && c === "}" && depth === 0) { + i++; + return out; + } + if (c === "/" && next === "*") { + const end = source.indexOf("*/", i + 2); + const stop = end < 0 ? source.length : end + 2; + out += source.slice(i, stop).replace(/[^\n]/g, " "); + i = stop; + } else if (c === "/" && next === "/") { + const end = source.indexOf("\n", i); + i = end < 0 ? source.length : end; + } else if (c === '"' || c === "'") { + let j = i + 1; + while (j < source.length && source[j] !== c) j += source[j] === "\\" ? 2 : 1; + i = j + 1; + out += '""'; + } else if (c === "`") { + i++; + out += '""'; + while (i < source.length && source[i] !== "`") { + if (source[i] === "\\") i += 2; + else if (source[i] === "$" && source[i + 1] === "{") { + i += 2; + out += "+(" + code(true) + ")+"; + } else { + if (source[i] === "\n") out += "\n"; + i++; + } + } + i++; + out += '""'; + } else { + if (c === "{") depth++; + if (c === "}") depth--; + out += c; + i++; + } + } + return out; + }; + return code(false); +} + +const AMBIENT: Array<[RegExp, string]> = [ + [/\b(Object|Array|Number|Reflect|JSON|Symbol|Math|String|Date|Set|Map|Promise|Error|globalThis|crypto|Buffer)\s*\./, "a member of an ambient global"], + [/\bfor\s*\([^)]*\bof\b/, "for...of (the iterator protocol)"], + [/\bfor\s*\([^)]*\bin\b/, "for...in (it enumerates inherited keys)"], + [/\.\.\./, "spread or rest (the iterator protocol, or reads through getters)"], + [/\b(const|let|var)\s*\[/, "array destructuring (the iterator protocol)"], + [/\(\s*\[[^\]]*\]\s*\)\s*(=>|:)/, "an array-destructuring parameter (the iterator protocol)"], + [/\bnew\s+(Set|Map|WeakSet|WeakMap|Array|Promise|Date|RegExp|TextEncoder|Uint8Array|Error)\b/, "an ambient constructor"], + [/\binstanceof\b/, "instanceof"], + [/[\w$)\]"]\s+in\s+[\w$("[{]/, "the in operator (it consults prototypes)"], + [/\.(map|filter|forEach|some|every|includes|indexOf|find|findIndex|join|push|pop|shift|unshift|splice|sort|reverse|concat|slice|split|reduce|entries|values|keys|trim|test|exec|has|add|delete|get|set|call|apply|bind|then|catch|finally|padStart|toString|charCodeAt|startsWith|endsWith|replace|match)\(/, "a method looked up at call time"], + [/[\w$\])]\s*\.\s*[A-Za-z_$][\w$]*\s*\(/, "a method call (looked up at call time)"], + [/\?\.\s*([A-Za-z_$][\w$]*\s*)?\(/, "an optional call (looked up at call time)"], + [/\b(String|Number|Boolean|structuredClone|queueMicrotask|setTimeout|parseInt|parseFloat|isNaN|isFinite|fetch)\s*\(/, "an ambient function"], +]; +const NO_REGEXP: Array<[RegExp, string]> = [ + [/\bRegExp\b/, "a RegExp"], + [/\.(regex|test|exec|match|matchAll|search)\(/, "a regex method"], + [/(^|[=(,:!&|?;{}[<>+\-*%~^]|\breturn|\btypeof)\s*\/(?![/*])/, "a regex literal"], +]; +function scan(label: string, code: string, rules: Array<[RegExp, string]>): string[] { + const found: string[] = []; + code.split("\n").forEach((line, n) => { + for (const [pattern, what] of rules) if (pattern.test(line)) found.push(`${label}:${n + 1} ${what}: ${line.trim()}`); + }); + return found; +} +const read = (rel: string) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8"); + +describe("source scans: evidence-level.ts", () => { + it("the scan sees a call inside a template substitution, and nothing inside a string or comment", () => { + expect(scan("t", codeOnly("const a = `x${JSON.stringify(b)}y`;"), AMBIENT)).not.toEqual([]); + expect(scan("t", codeOnly('const a = "JSON.stringify(b)"; // JSON.parse(c)'), AMBIENT)).toEqual([]); + expect(scan("t", codeOnly("if (!(\"value\" in d)) x();"), AMBIENT)).not.toEqual([]); + expect(scan("t", codeOnly("for (const k of ks) x();"), AMBIENT)).not.toEqual([]); + expect(scan("t", codeOnly("const r = /^a$/;"), NO_REGEXP)).not.toEqual([]); + }); + + it("evidence-level.ts calls only what util/primordials.ts captured, plain loops and operators, and holds no RegExp, at load as at call time", () => { + expect(scan("evidence-level.ts", codeOnly(read("../evidence/evidence-level.ts")), [...AMBIENT, ...NO_REGEXP])).toEqual([]); + }); + + it("util/primordials.ts holds no RegExp and imports nothing (browser-safe: no node: import)", () => { + const source = read("../util/primordials.ts"); + expect(scan("primordials.ts", codeOnly(source), NO_REGEXP)).toEqual([]); + expect(source).not.toMatch(/^\s*import\b/m); + }); +}); + +describe("evidence-level's exported data and RegExps", () => { + it("no RegExp is reachable from evidence-level's exports (astra pack 167's class)", () => { + const found: string[] = []; + const seen = new Set(); + const walk = (v: unknown, path: string) => { + if ((typeof v !== "object" && typeof v !== "function") || v === null || seen.has(v)) return; + seen.add(v); + if (Object.prototype.toString.call(v) === "[object RegExp]") found.push(path); + for (const k of Reflect.ownKeys(v)) { + const d = Reflect.getOwnPropertyDescriptor(v, k)!; + if ("value" in d) walk(d.value, `${path}.${String(k)}`); + } + }; + for (const k of Object.keys(levelModule)) walk((levelModule as Record)[k], `evidence-level.${k}`); + expect(found).toEqual([]); + }); + + it("every exported value of evidence-level that is data is deeply frozen", () => { + const unfrozen: string[] = []; + const walk = (v: unknown, path: string) => { + if (typeof v !== "object" || v === null) return; + if (!Object.isFrozen(v)) unfrozen.push(path); + for (const k of Object.keys(v)) walk((v as Record)[k], `${path}.${k}`); + }; + let objects = 0; + for (const k of Object.keys(levelModule)) { + const v = (levelModule as Record)[k]; + if (typeof v === "object" && v !== null) objects++; + walk(v, `evidence-level.${k}`); + } + expect(objects).toBeGreaterThanOrEqual(7); + expect(unfrozen).toEqual([]); + }); +}); + +// -- the operator principal id: the code-unit predicate equals the pattern it replaced -- +const OLD_PATTERN = /^eip155:([1-9][0-9]*):0x[0-9a-f]{40}$/; +const oldAccepts = (s: string): boolean => { + const match = OLD_PATTERN.exec(s); + return match !== null && Number.isSafeInteger(Number(match[1])); +}; +/** The predicate, through the public API: a declared trustDomain it refuses throws. */ +const accepts = (s: string): boolean => { + try { + evidenceLevelOfBundles([{ events: [], trustDomain: s } as AuthenticatedBundle]); + return true; + } catch (err) { + if ((err as Error).name !== "EvidenceLevelInputError") throw err; + return false; + } +}; + +describe("the operator principal id predicate equals /^eip155:([1-9][0-9]*):0x[0-9a-f]{40}$/ with a safe-integer chain id", () => { + const hex = "0123456789abcdef".repeat(3).slice(0, 40); + const valid = (chain: string, h = hex) => `eip155:${chain}:0x${h}`; + + it("agrees on the edge cases", () => { + const cases = [ + valid("1"), valid("9"), valid("10"), valid("8453"), valid("0"), valid("01"), valid(""), + valid("900719925474099"), valid("9007199254740991"), valid("9007199254740992"), valid("9007199254740990"), + valid("9999999999999999"), valid("1000000000000000"), valid("10000000000000000"), valid("18446744073709551616"), + valid("1", hex.toUpperCase()), valid("1", hex.slice(0, 39)), valid("1", `${hex}a`), valid("1", `${hex.slice(0, 39)}g`), + `${valid("1")}\n`, `${valid("1")} `, ` ${valid("1")}`, `EIP155:1:0x${hex}`, `eip155:1:0X${hex}`, `eip155::0x${hex}`, + `eip155:1:${hex}`, `eip155:1::0x${hex}`, `eip155:1:0x${hex}\u0000`, valid("\u0661"), valid("\uff11"), valid("1\u0661"), + "", "eip155:", "eip155:1", "eip155:1:", "eip155:1:0x", `eip15:1:0x${hex}`, `eip1555:1:0x${hex}`, + ]; + for (const s of cases) expect(accepts(s), JSON.stringify(s)).toBe(oldAccepts(s)); + }); + + it("agrees on 30,000 generated strings (seeded)", () => { + let seed = 0x5eed; + const rand = (n: number) => { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + return seed % n; + }; + const alphabet = "0123456789abcdefABCDEFgx:eip\n \u0661\uff11"; + const digits = (n: number) => { + let out = ""; + for (let i = 0; i < n; i++) out += "0123456789"[rand(10)]; + return out; + }; + let agreed = 0; + let accepted = 0; + for (let k = 0; k < 30_000; k++) { + let s = valid(rand(4) === 0 ? digits(1 + rand(20)) : `${1 + rand(9)}${digits(rand(17))}`); + const edits = rand(3); + for (let e = 0; e < edits; e++) { + const at = rand(s.length + 1); + const op = rand(3); + const c = alphabet[rand(alphabet.length)]!; + s = op === 0 ? s.slice(0, at) + c + s.slice(at + 1) : op === 1 ? s.slice(0, at) + c + s.slice(at) : s.slice(0, at) + s.slice(at + 1); + } + expect(accepts(s), JSON.stringify(s)).toBe(oldAccepts(s)); + agreed++; + if (oldAccepts(s)) accepted++; + } + expect(agreed).toBe(30_000); + expect(accepted).toBeGreaterThan(3_000); // both sides of the predicate are exercised + }); +}); diff --git a/packages/spec/src/__tests__/evidence-level.test.ts b/packages/spec/src/__tests__/evidence-level.test.ts index b5f9dfb20..978d4ecf1 100644 --- a/packages/spec/src/__tests__/evidence-level.test.ts +++ b/packages/spec/src/__tests__/evidence-level.test.ts @@ -790,8 +790,9 @@ describe("single read: one snapshot of the input per call, so no pass can disagr level([executorBundle(), bundle([ev("instrument_result", READER, payload)], OP_B)], ASSIGNED_A), label, ).toBe("inspected_output"); - // get:mock is isFabricated's single read of payload.mock. - expect([...traps].sort(), label).toEqual(["get:mock", "getOwnPropertyDescriptor:pass", "getPrototypeOf", "ownKeys"]); + // getOwnPropertyDescriptor:mock is the own-only read of payload.mock (astra pack 267): an + // absent mock is never read through the prototype, so no get:mock. + expect([...traps].sort(), label).toEqual(["getOwnPropertyDescriptor:mock", "getOwnPropertyDescriptor:pass", "getPrototypeOf", "ownKeys"]); } }); @@ -819,7 +820,6 @@ describe("single read: one snapshot of the input per call, so no pass can disagr ["mock:1", ev("execution_completed", PRINTER, { mock: 1 })], ["payload null", ev("execution_completed", PRINTER, null)], ["payload undefined", { ...ev("execution_completed", PRINTER), payload: undefined as never }], - ["inherited mock:true", ev("execution_completed", PRINTER, Object.create({ mock: true }))], ["both markers", ev("execution_completed", PRINTER, { mock: true }, { simulated: true })], ]; for (const [label, variant] of variants) { @@ -828,6 +828,12 @@ describe("single read: one snapshot of the input per call, so no pass can disagr expect(provesALevel, label).toBe(!isFabricated(variant)); expect(bundleHasFabricatedEvents({ events: [variant] }), label).toBe(isFabricated(variant)); } + // An INHERITED marker (here a custom prototype) is not the payload's own: the levels read + // own fields only (astra pack 267), so it is ignored, as one written on Object.prototype + // would be. Hashed evidence never carries one, because canonicalize reads own fields only. + const inherited = ev("execution_completed", PRINTER, Object.create({ mock: true })); + expect(isFabricated(inherited)).toBe(true); + expect(level([bundle([inherited], OP_A)])).toBe("device_reported"); }); }); diff --git a/packages/spec/src/__tests__/harness/evidence-level-realm.ts b/packages/spec/src/__tests__/harness/evidence-level-realm.ts new file mode 100644 index 000000000..46839aec0 --- /dev/null +++ b/packages/spec/src/__tests__/harness/evidence-level-realm.ts @@ -0,0 +1,411 @@ +/** + * One realm for the evidence-level realm-mutation harness (../evidence-level-intrinsics.test.ts). + * + * Why a child process: vitest runs its own runner in the realm of the test it + * runs, so a test that replaces an intrinsic breaks vitest itself (see + * admission-realm.ts). Each scenario runs here, in a process that holds only + * @pcc/spec, this file and Node. That is also the threat: post-load code in a + * process that loaded @pcc/spec first. + * + * 1. load: the static imports below; + * 2. build every item, and run them clean; + * 3. apply the scenario's change, run the items again, undo the change; + * 4. print one JSON line: { scenario, clean, patched }. + * + * Usage: tsx evidence-level-realm.ts | --list + * The code that runs while a change is in place (`results`, `copy` and the + * items) uses index loops, literals and operators only. + */ +import { pathToFileURL } from "node:url"; + +import { + DEVICE_REPORTED_EVENT_TYPES, + deriveContradictions, + EVIDENCE_LEVELS, + evidenceLevelOfBundles, + evidenceLevelRank, + evidenceLevelsOfEvents, + EXECUTION_EVENT_TYPES, + INSPECTION_EVENT_TYPES, + inspectionFailed, + inspectionVerdict, + meetsEvidenceLevel, + NO_OUTCOME_LEVEL_EVENT_TYPES, + NON_EXECUTOR_EVENT_TYPES, + SUBMITTED_EVENT_TYPES, + type AuthenticatedBundle, + type EvidenceLevel, +} from "../../evidence/evidence-level.js"; +import type { EvidenceEvent } from "../../types/evidence.js"; + +// -- what this file itself calls while a change is in place, captured at load -- +const ReflectDefineProperty = Reflect.defineProperty; +const ReflectGetOwnPropertyDescriptor = Reflect.getOwnPropertyDescriptor; +const ReflectDeleteProperty = Reflect.deleteProperty; +const ObjectCreate = Object.create; +const ObjectKeys = Object.keys; +const ArrayIteratorPrototype = Object.getPrototypeOf([][Symbol.iterator]()) as object; + +/** A property descriptor with a null prototype: once Object.prototype.value or .get is written, a literal would inherit it. */ +function nullDescriptor(fields: Record): PropertyDescriptor { + const d = ObjectCreate(null) as Record; + const keys = ObjectKeys(fields); + for (let i = 0; i < keys.length; i++) d[keys[i]!] = fields[keys[i]!]; + return d as PropertyDescriptor; +} + +// -- builders, run before any change -- +const EXEC = `eip155:1:0x${"11".repeat(20)}`; +const INSP = `eip155:1:0x${"22".repeat(20)}`; +const OTHER = `eip155:8453:0x${"33".repeat(20)}`; +const PRINTER = "dev-printer"; +const CAMERA = "dev-camera"; +const T = "2026-10-03T12:00:00.000Z"; + +function ev(type: string, deviceId: string | undefined, payload: Record = {}, extraSource: Record = {}): EvidenceEvent { + const source: Record = { deviceType: "machine", kernelId: "kernel-realm", ...extraSource }; + if (deviceId !== undefined) source.deviceId = deviceId; + return { type, timestamp: T, source, payload } as unknown as EvidenceEvent; +} +const bundle = (events: EvidenceEvent[], trustDomain?: string): AuthenticatedBundle => + (trustDomain === undefined ? { events } : { events, trustDomain }) as AuthenticatedBundle; + +const printed = () => [ev("execution_started", PRINTER), ev("execution_completed", PRINTER)]; +const camera = (payload: Record) => [ev("cv_inspection_result", CAMERA, payload)]; +const accessorPinned = (): Record => { + const payload: Record = {}; + Object.defineProperty(payload, "passed", { get: () => true, enumerable: true }); + return payload; +}; +const ctx = { executorTrustDomains: [EXEC] }; + +const B = { + independent: [bundle(printed(), EXEC), bundle(camera({ passed: true }), INSP)], + selfInspect: [bundle(printed(), EXEC), bundle(camera({ passed: true }), EXEC)], + unknownExecutor: [bundle(printed()), bundle(camera({ passed: true }), INSP)], + fabricatedPrinter: [bundle([ev("execution_completed", PRINTER, {}, { simulated: true })], EXEC), bundle(camera({ passed: true }), INSP)], + mockCamera: [bundle(printed(), EXEC), bundle(camera({ passed: true, mock: true }), INSP)], + failedInspection: [bundle(printed(), EXEC), bundle(camera({ passed: false }), INSP)], + noVerdict: [bundle(printed(), EXEC), bundle(camera({}), INSP)], + foldedSpelling: [bundle(printed(), EXEC), bundle(camera({ Passed: true }), INSP)], + twoVerdicts: [bundle(printed(), EXEC), bundle(camera({ passed: true, status: "PASS" }), INSP)], + accessor: [bundle(printed(), EXEC), bundle(camera(accessorPinned()), INSP)], + gatewayStamp: [bundle([ev("execution_completed", " Gateway ")], EXEC)], + noDevice: [bundle([ev("execution_completed", undefined)], EXEC)], + completionFailure: [bundle([...printed(), ev("execution_failed", PRINTER)], EXEC)], + failureAlone: [bundle([ev("execution_started", PRINTER), ev("execution_failed", PRINTER)], EXEC)], + batchPass: [bundle(printed(), EXEC), bundle([ev("batch_sample_result", CAMERA, { status: "PASS" })], OTHER)], + instrumentFail: [bundle(printed(), EXEC), bundle([ev("instrument_result", CAMERA, { pass: false })], OTHER)], + submittedOnly: [bundle([ev("gcode_received", PRINTER), ev("execution_progress", PRINTER)], EXEC)], + empty: [] as AuthenticatedBundle[], +}; + +const principal = (chainId: string, hex = "ab".repeat(20)) => `eip155:${chainId}:0x${hex}`; +const DOMAIN_CANDIDATES: Array<[string, string]> = [ + ["valid chain 1", principal("1")], + ["valid chain 9007199254740991", principal("9007199254740991")], + ["unsafe chain 9007199254740992", principal("9007199254740992")], + ["17-digit chain", principal("10000000000000000")], + ["leading zero", principal("01")], + ["chain 0", principal("0")], + ["uppercase hex", principal("1", "AB".repeat(20))], + ["39 hex", principal("1", "ab".repeat(19) + "a")], + ["trailing newline", `${principal("1")}\n`], + ["EIP155 prefix", `EIP155:1:0x${"ab".repeat(20)}`], + ["fullwidth digit", principal("\uff11")], +]; + +const meetsPairs: Array<[EvidenceLevel | null, EvidenceLevel]> = [ + ["submitted", "inspected_output"], ["device_reported", "inspected_output"], ["inspected_output", "inspected_output"], + ["submitted", "device_reported"], ["inspected_output", "submitted"], [null, "submitted"], +]; + +type Item = [label: string, run: () => unknown]; +const ITEMS: Item[] = []; +const keys = ObjectKeys(B) as Array; +for (let i = 0; i < keys.length; i++) { + const name = keys[i]!; + ITEMS[ITEMS.length] = [`levelOfBundles ${name}, assigned`, () => evidenceLevelOfBundles(B[name], ctx)]; + ITEMS[ITEMS.length] = [`levelOfBundles ${name}, no context`, () => evidenceLevelOfBundles(B[name])]; + ITEMS[ITEMS.length] = [`levelsOfEvents ${name}, assigned`, () => evidenceLevelsOfEvents(B[name], ctx)]; + ITEMS[ITEMS.length] = [`contradictions ${name}`, () => deriveContradictions(B[name])]; +} +const inspections: Array<[string, EvidenceEvent]> = [ + ["cv passed", camera({ passed: true })[0]!], + ["cv failed", camera({ passed: false })[0]!], + ["cv none", camera({})[0]!], + ["cv Passed", camera({ Passed: true })[0]!], + ["cv passed + status", camera({ passed: true, status: "PASS" })[0]!], + ["cv accessor", camera(accessorPinned())[0]!], + ["cv passed 'yes'", camera({ passed: "yes" })[0]!], + ["batch PASS", ev("batch_sample_result", CAMERA, { status: "PASS" })], + ["batch pass", ev("batch_sample_result", CAMERA, { status: "pass" })], + ["instrument false", ev("instrument_result", CAMERA, { pass: false })], + ["photo comparison verdict", ev("photo_comparison_result", CAMERA, { result: "ok" })], + ["not an inspection", ev("execution_completed", PRINTER, { passed: false })], +]; +for (let i = 0; i < inspections.length; i++) { + const [label, event] = inspections[i]!; + ITEMS[ITEMS.length] = [`verdict ${label}`, () => inspectionVerdict(event)]; + ITEMS[ITEMS.length] = [`failed ${label}`, () => inspectionFailed(event)]; +} +for (let i = 0; i < meetsPairs.length; i++) { + const [reached, required] = meetsPairs[i]!; + ITEMS[ITEMS.length] = [`meets ${String(reached)} for ${required}`, () => meetsEvidenceLevel(reached, required)]; +} +for (let i = 0; i < EVIDENCE_LEVELS.length; i++) { + const level = EVIDENCE_LEVELS[i]!; + ITEMS[ITEMS.length] = [`rank ${level}`, () => evidenceLevelRank(level)]; +} +ITEMS[ITEMS.length] = ["rank vibes", () => evidenceLevelRank("vibes" as EvidenceLevel)]; +for (let i = 0; i < DOMAIN_CANDIDATES.length; i++) { + const [label, domain] = DOMAIN_CANDIDATES[i]!; + const declared = [bundle(printed(), domain)]; + const assigned = { executorTrustDomains: [domain] }; + ITEMS[ITEMS.length] = [`trustDomain ${label}`, () => evidenceLevelOfBundles(declared, ctx)]; + ITEMS[ITEMS.length] = [`executorTrustDomains ${label}`, () => evidenceLevelOfBundles(B.independent, assigned)]; +} +const NOT_BUNDLES = {} as unknown as AuthenticatedBundle[]; +const NOT_EVENTS = [{ events: {} } as unknown as AuthenticatedBundle]; +ITEMS[ITEMS.length] = ["bundles not an array", () => evidenceLevelOfBundles(NOT_BUNDLES)]; +ITEMS[ITEMS.length] = ["events not an array", () => evidenceLevelOfBundles(NOT_EVENTS)]; +ITEMS[ITEMS.length] = ["context not an object", () => evidenceLevelOfBundles(B.independent, 7 as never)]; + +// Inputs that leave a field to inheritance: each answer must not change when Object.prototype +// (or Array.prototype) supplies it. +const EMPTY_CONTEXT = {}; +const SPARSE_ASSIGNED = { executorTrustDomains: new Array(1) }; +const NO_TYPE = [bundle([{ timestamp: T, source: { deviceId: PRINTER, deviceType: "machine" }, payload: {} } as unknown as EvidenceEvent], EXEC)]; +const NO_SOURCE = [bundle([{ type: "execution_completed", timestamp: T, payload: {} } as unknown as EvidenceEvent], EXEC)]; +const NO_PAYLOAD = [bundle(printed(), EXEC), bundle([{ type: "cv_inspection_result", timestamp: T, source: { deviceId: CAMERA, deviceType: "camera" } } as unknown as EvidenceEvent], INSP)]; +const NO_EVENTS = [{ trustDomain: EXEC } as unknown as AuthenticatedBundle]; +ITEMS[ITEMS.length] = ["an empty context", () => evidenceLevelOfBundles(B.independent, EMPTY_CONTEXT)]; +ITEMS[ITEMS.length] = ["a hole in executorTrustDomains", () => evidenceLevelOfBundles(B.independent, SPARSE_ASSIGNED)]; +ITEMS[ITEMS.length] = ["an event with no type", () => evidenceLevelOfBundles(NO_TYPE, ctx)]; +ITEMS[ITEMS.length] = ["an event with no source", () => evidenceLevelOfBundles(NO_SOURCE, ctx)]; +ITEMS[ITEMS.length] = ["an inspection with no payload", () => evidenceLevelOfBundles(NO_PAYLOAD, ctx)]; +ITEMS[ITEMS.length] = ["an inspection with no payload: verdict", () => inspectionVerdict(NO_PAYLOAD[1]!.events[0]!)]; +ITEMS[ITEMS.length] = ["a bundle with no events", () => evidenceLevelOfBundles(NO_EVENTS, ctx)]; +const SPARSE_BUNDLES = new Array(1); +const SPARSE_EVENTS = [{ events: new Array(1), trustDomain: EXEC } as unknown as AuthenticatedBundle]; +ITEMS[ITEMS.length] = ["a hole in bundles", () => evidenceLevelOfBundles(SPARSE_BUNDLES, ctx)]; +ITEMS[ITEMS.length] = ["a hole in a bundle's events", () => evidenceLevelOfBundles(SPARSE_EVENTS, ctx)]; +ITEMS[ITEMS.length] = ["a hole in bundles: contradictions", () => deriveContradictions(SPARSE_BUNDLES)]; + +// Primitive sources and payloads: a read through them would box the primitive and consult +// Number.prototype, String.prototype and Object.prototype (pack 267 HIGH 2). +const PRIMITIVE_SOURCE = [bundle([ev("execution_completed", PRINTER), { type: "execution_failed", timestamp: T, source: 0, payload: {} } as unknown as EvidenceEvent], EXEC)]; +const PRIMITIVE_PAYLOAD = [bundle([ev("execution_completed", PRINTER), { type: "execution_failed", timestamp: T, source: { deviceId: PRINTER, deviceType: "machine" }, payload: "x" } as unknown as EvidenceEvent], EXEC)]; +ITEMS[ITEMS.length] = ["a primitive source: contradictions", () => deriveContradictions(PRIMITIVE_SOURCE)]; +ITEMS[ITEMS.length] = ["a primitive source: level", () => evidenceLevelOfBundles(PRIMITIVE_SOURCE, ctx)]; +ITEMS[ITEMS.length] = ["a primitive payload: contradictions", () => deriveContradictions(PRIMITIVE_PAYLOAD)]; +ITEMS[ITEMS.length] = ["a primitive payload: level", () => evidenceLevelOfBundles(PRIMITIVE_PAYLOAD, ctx)]; + +/** The unattributed completion's own source object (B.noDevice): a getter elsewhere could write to it. */ +const NO_DEVICE_SOURCE = (B.noDevice[0]!.events[0] as unknown as { source: Record }).source; + +// -- running items while a change is in place: index loops, literals and operators only -- +type Row = [label: string, value: unknown]; + +/** A plain copy of an answer: frozen records become plain records, lists plain lists. */ +function copy(value: unknown): unknown { + if (typeof value !== "object" || value === null) return value; + const v = value as Record & { length?: unknown }; + if (typeof v.length === "number") { + const out: unknown[] = []; + for (let i = 0; i < (v.length as number); i++) out[i] = copy((value as unknown[])[i]); + return out; + } + return { bundleIndex: v.bundleIndex, eventIndex: v.eventIndex, level: v.level }; +} + +function results(): Row[] { + const rows: Row[] = []; + for (let i = 0; i < ITEMS.length; i++) { + let value: unknown; + try { + value = copy(ITEMS[i]![1]()); + } catch (err) { + value = `threw ${(err as { name?: unknown }).name === "EvidenceLevelInputError" ? "EvidenceLevelInputError" : "other"}`; + } + rows[i] = [ITEMS[i]![0], value]; + } + return rows; +} + +// -- the changes -- +type Apply = () => () => void; + +function replace(target: object, key: PropertyKey, make: (original: any) => unknown): Apply { + return () => { + const original = ReflectGetOwnPropertyDescriptor(target, key)!; + const changed = nullDescriptor({ configurable: original.configurable, enumerable: original.enumerable, writable: original.writable, value: make(original.value) }); + ReflectDefineProperty(target, key, changed); + return () => void ReflectDefineProperty(target, key, original); + }; +} + +function replaceGetter(target: object, key: PropertyKey, make: (original: any) => () => unknown): Apply { + return () => { + const original = ReflectGetOwnPropertyDescriptor(target, key)!; + ReflectDefineProperty(target, key, nullDescriptor({ configurable: original.configurable, enumerable: original.enumerable, get: make(original.get), set: original.set })); + return () => void ReflectDefineProperty(target, key, original); + }; +} + +/** Write `values` onto Object.prototype (non-enumerable), as post-load code could. */ +function pollute(values: () => Record): Apply { + return () => { + const v = values(); + const names = ObjectKeys(v); + for (let i = 0; i < names.length; i++) { + ReflectDefineProperty(Object.prototype, names[i]!, nullDescriptor({ value: v[names[i]!], writable: true, configurable: true, enumerable: false })); + } + return () => { + for (let i = 0; i < names.length; i++) ReflectDeleteProperty(Object.prototype, names[i]!); + }; + }; +} + +/** Exported data evidence-level reads, written as post-load code could; a frozen list refuses. */ +function mutate(change: () => () => void): Apply { + return () => { + try { + return change(); + } catch { + return () => undefined; + } + }; +} +const pushOnto = (list: readonly unknown[], value: unknown) => () => { + (list as unknown[]).push(value); + return () => void (list as unknown[]).pop(); +}; + +const SCENARIOS: Array<[id: string, apply: Apply]> = [ + ["Array.prototype.map", replace(Array.prototype, "map", () => () => [])], + ["Array.prototype.filter", replace(Array.prototype, "filter", () => () => [])], + ["Array.prototype.some", replace(Array.prototype, "some", () => () => true)], + ["Array.prototype.every", replace(Array.prototype, "every", () => () => false)], + ["Array.prototype.includes", replace(Array.prototype, "includes", () => () => true)], + ["Array.prototype.indexOf", replace(Array.prototype, "indexOf", () => () => 2)], + ["Array.prototype.push", replace(Array.prototype, "push", () => () => 0)], + ["Array.prototype.slice", replace(Array.prototype, "slice", () => () => [])], + ["Array.prototype.concat", replace(Array.prototype, "concat", () => () => [])], + ["Array.prototype[Symbol.iterator]", replace(Array.prototype, Symbol.iterator, () => function* () {})], + ["%ArrayIteratorPrototype%.next", replace(ArrayIteratorPrototype, "next", () => () => ({ done: true, value: undefined }))], + ["Array.isArray", replace(Array, "isArray", () => () => false)], + ["Array.prototype[0] = a principal", () => { + ReflectDefineProperty(Array.prototype, "0", nullDescriptor({ value: EXEC, writable: true, configurable: true, enumerable: false })); + return () => void ReflectDeleteProperty(Array.prototype, "0"); + }], + ["Array.prototype[0] = a completed bundle and event", () => { + // One object that reads as a bundle (events, trustDomain) and as an event (type, source, payload). + const forged = { events: [ev("execution_completed", PRINTER), ev("execution_failed", PRINTER)], trustDomain: EXEC, type: "execution_completed", timestamp: T, source: { deviceId: PRINTER, deviceType: "machine" }, payload: {} }; + ReflectDefineProperty(Array.prototype, "0", nullDescriptor({ value: forged, writable: true, configurable: true, enumerable: false })); + return () => void ReflectDeleteProperty(Array.prototype, "0"); + }], + ["Object.freeze", replace(Object, "freeze", () => (v: unknown) => v)], + ["Object.keys", replace(Object, "keys", () => () => [])], + ["Object.create", replace(Object, "create", () => () => ({}))], + ["Object.getOwnPropertyNames", replace(Object, "getOwnPropertyNames", () => () => [])], + ["Object.getOwnPropertyDescriptor", replace(Object, "getOwnPropertyDescriptor", () => () => ({ value: true }))], + ["Object.getPrototypeOf", replace(Object, "getPrototypeOf", () => () => Array.prototype)], + ["Object.prototype.hasOwnProperty", replace(Object.prototype, "hasOwnProperty", () => () => true)], + ["Reflect.ownKeys", replace(Reflect, "ownKeys", () => () => [])], + ["Set.prototype.has", replace(Set.prototype, "has", () => () => false)], + ["Set.prototype.add", replace(Set.prototype, "add", () => function (this: unknown) { return this; })], + ["Set.prototype.size", replaceGetter(Set.prototype, "size", () => () => 0)], + ["Map.prototype.get", replace(Map.prototype, "get", () => () => undefined)], + ["RegExp.prototype.exec", replace(RegExp.prototype, "exec", () => () => null)], + ["RegExp.prototype.test", replace(RegExp.prototype, "test", () => () => false)], + ["String.prototype.charCodeAt", replace(String.prototype, "charCodeAt", () => () => 0x41)], + ["String.prototype.charAt", replace(String.prototype, "charAt", () => () => "x")], + ["String.prototype.toLowerCase", replace(String.prototype, "toLowerCase", () => () => "passed")], + ["String.fromCharCode", replace(String, "fromCharCode", () => () => "x")], + ["String", replace(globalThis, "String", () => () => "x")], + ["Number", replace(globalThis, "Number", () => () => 1)], + ["Number.isSafeInteger", replace(Number, "isSafeInteger", () => () => false)], + ["Number.isInteger", replace(Number, "isInteger", () => () => false)], + ["Math.max", replace(Math, "max", () => () => 0)], + ["Function.prototype.call", replace(Function.prototype, "call", () => () => undefined)], + ["Function.prototype.apply", replace(Function.prototype, "apply", () => () => undefined)], + ["Function.prototype.bind", replace(Function.prototype, "bind", () => () => () => undefined)], + ["Object.prototype.value = true", pollute(() => ({ value: true }))], + ["Object.prototype.passed = true", pollute(() => ({ passed: true }))], + ["Object.prototype.passed = false", pollute(() => ({ passed: false }))], + ["Object.prototype.pass and status", pollute(() => ({ pass: true, status: "PASS" }))], + ["Object.prototype.deviceId", pollute(() => ({ deviceId: PRINTER }))], + ["Object.prototype.simulated = true", pollute(() => ({ simulated: true }))], + ["Object.prototype.mock = true", pollute(() => ({ mock: true }))], + // Pack 267 HIGH 1: an inherited mock GETTER that attributes the unattributed event, then answers false. + ["Object.prototype.mock getter that writes source.deviceId", () => { + ReflectDefineProperty(Object.prototype, "mock", nullDescriptor({ configurable: true, enumerable: false, get: () => { + NO_DEVICE_SOURCE.deviceId = PRINTER; + return false; + } })); + return () => { + ReflectDeleteProperty(Object.prototype, "mock"); + ReflectDeleteProperty(NO_DEVICE_SOURCE, "deviceId"); + }; + }], + ["Object.prototype.mock getter that throws", () => { + ReflectDefineProperty(Object.prototype, "mock", nullDescriptor({ configurable: true, enumerable: false, get: () => { + throw new Error("inherited mock"); + } })); + return () => void ReflectDeleteProperty(Object.prototype, "mock"); + }], + ["Number.prototype.simulated = true", () => { + ReflectDefineProperty(Number.prototype, "simulated", nullDescriptor({ value: true, writable: true, configurable: true, enumerable: false })); + return () => void ReflectDeleteProperty(Number.prototype, "simulated"); + }], + ["String.prototype.mock = true", () => { + ReflectDefineProperty(String.prototype, "mock", nullDescriptor({ value: true, writable: true, configurable: true, enumerable: false })); + return () => void ReflectDeleteProperty(String.prototype, "mock"); + }], + ["Object.prototype.trustDomain", pollute(() => ({ trustDomain: OTHER }))], + ["Object.prototype.executorTrustDomains", pollute(() => ({ executorTrustDomains: [EXEC] }))], + ["Object.prototype.type = execution_completed", pollute(() => ({ type: "execution_completed" }))], + ["Object.prototype.source", pollute(() => ({ source: { deviceId: PRINTER, deviceType: "machine" } }))], + ["Object.prototype.payload", pollute(() => ({ payload: { passed: true } }))], + ["Object.prototype.events", pollute(() => ({ events: [ev("execution_completed", PRINTER)] }))], + ["Object.prototype.instrument_result (pinned table)", pollute(() => ({ photo_comparison_result: { field: "result", read: () => "pass" } }))], + ["DEVICE_REPORTED_EVENT_TYPES += cv_inspection_result", mutate(pushOnto(DEVICE_REPORTED_EVENT_TYPES, "cv_inspection_result"))], + ["SUBMITTED_EVENT_TYPES += execution_failed", mutate(pushOnto(SUBMITTED_EVENT_TYPES, "execution_failed"))], + ["INSPECTION_EVENT_TYPES += execution_completed", mutate(pushOnto(INSPECTION_EVENT_TYPES, "execution_completed"))], + ["NO_OUTCOME_LEVEL_EVENT_TYPES += cv_inspection_result", mutate(pushOnto(NO_OUTCOME_LEVEL_EVENT_TYPES, "cv_inspection_result"))], + ["EXECUTION_EVENT_TYPES += cv_inspection_result", mutate(pushOnto(EXECUTION_EVENT_TYPES, "cv_inspection_result"))], + ["NON_EXECUTOR_EVENT_TYPES += execution_completed", mutate(pushOnto(NON_EXECUTOR_EVENT_TYPES, "execution_completed"))], + ["EVIDENCE_LEVELS += vibes", mutate(pushOnto(EVIDENCE_LEVELS, "vibes"))], +]; + +// -- main -- +function write(line: unknown): void { + process.stdout.write(`${JSON.stringify(line)}\n`); +} + +function main(): void { + const id = process.argv[2]; + if (id === "--list") { + write(SCENARIOS.map((s) => s[0])); + return; + } + let scenario: [string, Apply] | undefined; + for (let i = 0; i < SCENARIOS.length; i++) if (SCENARIOS[i]![0] === id) scenario = SCENARIOS[i]; + if (scenario === undefined) throw new Error(`no scenario ${JSON.stringify(id)}`); + const clean = results(); + let undo: () => void = () => undefined; + let patched: unknown; + try { + undo = scenario[1](); + patched = results(); + } catch (err) { + patched = `the scenario threw: ${String(err)}`; + } finally { + undo(); + } + write({ scenario: id, clean, patched }); +} + +// Run only when invoked as a script (tsx evidence-level-realm.ts ...), never when imported. +if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) main(); diff --git a/packages/spec/src/evidence/evidence-level.ts b/packages/spec/src/evidence/evidence-level.ts index 231dd9743..31bb83680 100644 --- a/packages/spec/src/evidence/evidence-level.ts +++ b/packages/spec/src/evidence/evidence-level.ts @@ -113,31 +113,63 @@ * Every member of EVIDENCE_EVENT_TYPES is ruled on below, including the ones * that prove no outcome level, so a new event type cannot join the vocabulary * without someone deciding its level (a test enforces the partition). + * + * 8. Nothing that runs after this module loads can change a level, a verdict or + * a contradiction it returns (steward #5186, evidence #6440; the realm- + * mutation class of astra packs 162-171). It calls only intrinsics captured + * at load (util/primordials.ts), plain index loops and operators: never a + * method looked up on a prototype or a global at the time of the call, never + * the iterator protocol, spread, `in` or a RegExp. Its exported lists are + * frozen where they are defined, and its sets and the pinned-verdict table are + * frozen null-prototype records built from them at load. Rule 7's reads stay + * one read each, but of OWN properties only: an event's type, source and + * payload, source.deviceId and source.simulated, a bundle's events and + * trustDomain, payload.mock, the context's executorTrustDomains and every + * list element. A value or getter written on Object.prototype or + * Array.prototype is never taken for the input's (an own accessor still runs + * once), and a primitive source or payload is read as having no fields, never + * boxed. The boundary: a realm whose intrinsics were replaced before + * @pcc/spec loaded is out of scope, since no in-process check can tell. */ import type { EvidenceEvent, EvidenceEventType } from "../types/evidence.js"; import { isFabricated } from "./is-fabricated.js"; - -export const EVIDENCE_LEVELS = ["submitted", "device_reported", "inspected_output"] as const; +import { + append, + ArrayIsArray, + charCodeAt, + hasOwn, + inSet, + NumberIsInteger, + ObjectCreate, + ObjectFreeze, + ObjectGetOwnPropertyDescriptor, + ObjectGetPrototypeOf, + ObjectPrototype, + ReflectOwnKeys, + stringSet, +} from "../util/primordials.js"; + +export const EVIDENCE_LEVELS = ObjectFreeze(["submitted", "device_reported", "inspected_output"] as const); export type EvidenceLevel = (typeof EVIDENCE_LEVELS)[number]; /** The device took the work; its outcome is not observed. */ -export const SUBMITTED_EVENT_TYPES = [ +export const SUBMITTED_EVENT_TYPES = ObjectFreeze([ "gcode_received", "gcode_loaded", "method_loaded", "execution_progress", "courier_pickup_confirmed", -] as const satisfies readonly EvidenceEventType[]; +] as const satisfies readonly EvidenceEventType[]); /** The device that did the work reports it finished. */ -export const DEVICE_REPORTED_EVENT_TYPES = [ +export const DEVICE_REPORTED_EVENT_TYPES = ObjectFreeze([ "execution_completed", "digital_task_completed", "batch_session_completed", "courier_delivery_confirmed", -] as const satisfies readonly EvidenceEventType[]; +] as const satisfies readonly EvidenceEventType[]); /** * An observation or measurement of the output. It proves a level only when its @@ -147,12 +179,12 @@ export const DEVICE_REPORTED_EVENT_TYPES = [ * reporting, and so is an observation whose independence cannot be shown (no * assigned executor, an unknown executor, no trust domain): all device_reported. */ -export const INSPECTION_EVENT_TYPES = [ +export const INSPECTION_EVENT_TYPES = ObjectFreeze([ "cv_inspection_result", "photo_comparison_result", "instrument_result", "batch_sample_result", -] as const satisfies readonly EvidenceEventType[]; +] as const satisfies readonly EvidenceEventType[]); /** * Event types that prove no outcome level on their own: input commitments, @@ -165,7 +197,7 @@ export const INSPECTION_EVENT_TYPES = [ * The custody, capture-protocol and touchstone events are here until a CSD * needs one of them as outcome evidence and composition rules on its level. */ -export const NO_OUTCOME_LEVEL_EVENT_TYPES = [ +export const NO_OUTCOME_LEVEL_EVENT_TYPES = ObjectFreeze([ "gcode_hash_verified", "execution_started", "execution_failed", @@ -209,7 +241,7 @@ export const NO_OUTCOME_LEVEL_EVENT_TYPES = [ "capture_liveness_result", "capture_multi_sensor_fusion", "capture_anchor_committed", -] as const satisfies readonly EvidenceEventType[]; +] as const satisfies readonly EvidenceEventType[]); /** * Event types that show a party is executing the job. A bundle that holds any @@ -231,7 +263,7 @@ export const NO_OUTCOME_LEVEL_EVENT_TYPES = [ * (`NON_EXECUTOR_EVENT_TYPES`). Every vocabulary member is in exactly one of * the two lists, and a test fails if a new type is not ruled on. */ -export const EXECUTION_EVENT_TYPES = [ +export const EXECUTION_EVENT_TYPES = ObjectFreeze([ "gcode_received", "gcode_hash_verified", "gcode_loaded", @@ -256,7 +288,7 @@ export const EXECUTION_EVENT_TYPES = [ "custody_handoff_initiated", "custody_handoff_confirmed", "photo_captured", -] as const satisfies readonly EvidenceEventType[]; +] as const satisfies readonly EvidenceEventType[]); /** * Event types that do not identify an executing party: inspections, and the @@ -278,7 +310,7 @@ export const EXECUTION_EVENT_TYPES = [ * photo_anti_spoof_check, touchstone_*, capture_*). A new producer of any of * them must re-check this ruling. */ -export const NON_EXECUTOR_EVENT_TYPES = [ +export const NON_EXECUTOR_EVENT_TYPES = ObjectFreeze([ "cv_inspection_result", "photo_comparison_result", "instrument_result", @@ -311,18 +343,28 @@ export const NON_EXECUTOR_EVENT_TYPES = [ "capture_liveness_result", "capture_multi_sensor_fusion", "capture_anchor_committed", -] as const satisfies readonly EvidenceEventType[]; +] as const satisfies readonly EvidenceEventType[]); + +/** The members of three lists, in order, as one new list. */ +function unionOf(a: readonly string[], b: readonly string[], c: readonly string[]): string[] { + const out: string[] = []; + for (let i = 0; i < a.length; i += 1) append(out, a[i]!); + for (let i = 0; i < b.length; i += 1) append(out, b[i]!); + for (let i = 0; i < c.length; i += 1) append(out, c[i]!); + return out; +} -const SUBMITTED = new Set(SUBMITTED_EVENT_TYPES); -const DEVICE_REPORTED = new Set(DEVICE_REPORTED_EVENT_TYPES); -const INSPECTION = new Set(INSPECTION_EVENT_TYPES); +const SUBMITTED = stringSet(SUBMITTED_EVENT_TYPES); +const DEVICE_REPORTED = stringSet(DEVICE_REPORTED_EVENT_TYPES); +const INSPECTION = stringSet(INSPECTION_EVENT_TYPES); // The union makes "took or finished the work identifies an executor" hold even if // a list above is edited carelessly; a test also keeps EXECUTION_EVENT_TYPES complete. -const EXECUTION = new Set([...EXECUTION_EVENT_TYPES, ...SUBMITTED_EVENT_TYPES, ...DEVICE_REPORTED_EVENT_TYPES]); +const EXECUTION = stringSet(unionOf(EXECUTION_EVENT_TYPES, SUBMITTED_EVENT_TYPES, DEVICE_REPORTED_EVENT_TYPES)); -/** Position in EVIDENCE_LEVELS; higher is stronger. */ +/** Position in EVIDENCE_LEVELS; higher is stronger. -1 for anything else, as indexOf answered. */ export function evidenceLevelRank(level: EvidenceLevel): number { - return EVIDENCE_LEVELS.indexOf(level); + for (let i = 0; i < EVIDENCE_LEVELS.length; i += 1) if (EVIDENCE_LEVELS[i] === level) return i; + return -1; } /** True when `reached` is at least as strong as `required`. Null meets nothing. */ @@ -398,17 +440,21 @@ function isAsciiSpace(code: number): boolean { function asciiFold(text: string, maxLength: number): string | null { let start = 0; let end = text.length; - while (start < end && isAsciiSpace(text.charCodeAt(start))) start += 1; - while (end > start && isAsciiSpace(text.charCodeAt(end - 1))) end -= 1; + while (start < end && isAsciiSpace(charCodeAt(text, start))) start += 1; + while (end > start && isAsciiSpace(charCodeAt(text, end - 1))) end -= 1; if (end - start > maxLength) return null; let folded = ""; for (let i = start; i < end; i += 1) { - const code = text.charCodeAt(i); - folded += code >= 0x41 && code <= 0x5a ? String.fromCharCode(code + 0x20) : text.charAt(i); + const code = charCodeAt(text, i); + // A string's own index reads its code unit; no String method is looked up. + folded += code >= 0x41 && code <= 0x5a ? ASCII_LOWERCASE[code - 0x41]! : text[i]!; } return folded; } +/** a-z, indexed by an uppercase letter's offset from "A". */ +const ASCII_LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; + function isGatewayStamp(deviceId: string): boolean { return asciiFold(deviceId, GATEWAY_STAMPED_DEVICE_ID.length) === GATEWAY_STAMPED_DEVICE_ID; } @@ -461,11 +507,21 @@ function readPassFailVerdict(value: unknown): "pass" | "fail" | "malformed" { * produces or reads it, so there is no verdict field to pin. Its verdict is * `none` (or `malformed` when the payload carries a verdict-looking key). */ -const PINNED_VERDICTS: ReadonlyMap = new Map([ - ["instrument_result", { field: "pass", read: readBooleanVerdict }], - ["cv_inspection_result", { field: "passed", read: readBooleanVerdict }], - ["batch_sample_result", { field: "status", read: readPassFailVerdict }], -]); +const PINNED_VERDICTS: Readonly> = pinnedVerdicts(); + +/** The table above, as a frozen null-prototype record of frozen entries. */ +function pinnedVerdicts(): Readonly> { + const table = ObjectCreate(null) as Record; + table.instrument_result = ObjectFreeze({ field: "pass", read: readBooleanVerdict }); + table.cv_inspection_result = ObjectFreeze({ field: "passed", read: readBooleanVerdict }); + table.batch_sample_result = ObjectFreeze({ field: "status", read: readPassFailVerdict }); + return ObjectFreeze(table); +} + +/** The pinned verdict of an inspection type, if it has one. */ +function pinnedVerdictOf(type: string): PinnedVerdict | undefined { + return hasOwn(PINNED_VERDICTS, type) ? PINNED_VERDICTS[type] : undefined; +} /** * Names that look like a verdict. A payload key is matched against them (and @@ -475,19 +531,29 @@ const PINNED_VERDICTS: ReadonlyMap = new Map(VERDICT_LOOKING_KEYS); +const VERDICT_LOOKING_KEYS = ObjectFreeze(["pass", "passed", "status", "result", "verdict", "ok", "success"] as const); +const VERDICT_LOOKING = stringSet(VERDICT_LOOKING_KEYS); /** No folded key longer than this can be a verdict-looking name or a pinned field. */ -const LONGEST_VERDICT_NAME = Math.max( - ...VERDICT_LOOKING_KEYS.map((name) => name.length), - ...[...PINNED_VERDICTS.values()].map((pinned) => pinned.field.length), -); +const LONGEST_VERDICT_NAME = longestVerdictName(); + +function longestVerdictName(): number { + let longest = 0; + for (let i = 0; i < VERDICT_LOOKING_KEYS.length; i += 1) { + if (VERDICT_LOOKING_KEYS[i]!.length > longest) longest = VERDICT_LOOKING_KEYS[i]!.length; + } + const types = ReflectOwnKeys(PINNED_VERDICTS); + for (let i = 0; i < types.length; i += 1) { + const field = PINNED_VERDICTS[types[i] as string]!.field; + if (field.length > longest) longest = field.length; + } + return longest; +} function isPlainObject(value: unknown): value is Record { - if (typeof value !== "object" || value === null || Array.isArray(value)) return false; - const prototype: unknown = Object.getPrototypeOf(value); - return prototype === Object.prototype || prototype === null; + if (typeof value !== "object" || value === null || ArrayIsArray(value)) return false; + const prototype: unknown = ObjectGetPrototypeOf(value); + return prototype === ObjectPrototype || prototype === null; } /** @@ -509,15 +575,19 @@ function isPlainObject(value: unknown): value is Record { */ function verdictOfPayload(type: string, payload: unknown): InspectionVerdict { if (!isPlainObject(payload)) return "malformed"; - const pinned = PINNED_VERDICTS.get(type); + const pinned = pinnedVerdictOf(type); let exactPinned = false; let pinnedSpelling = false; let verdictLooking = false; let otherVerdict = false; - for (const key of Object.getOwnPropertyNames(payload)) { + // Its own string keys, enumerable or not (what Object.getOwnPropertyNames listed). + const keys = ReflectOwnKeys(payload); + for (let k = 0; k < keys.length; k += 1) { + const key = keys[k]; + if (typeof key !== "string") continue; const folded = asciiFold(key, LONGEST_VERDICT_NAME); if (folded === null) continue; - if (VERDICT_LOOKING.has(folded)) { + if (inSet(VERDICT_LOOKING, folded)) { verdictLooking = true; if (pinned === undefined || folded !== pinned.field) otherVerdict = true; } @@ -531,9 +601,11 @@ function verdictOfPayload(type: string, payload: unknown): InspectionVerdict { // A second verdict-looking key is a conflicting claim: a reader of that key // (types/dpp.ts reads `pass` for cv) would answer differently. Fail closed. if (otherVerdict) return "malformed"; - const descriptor = Object.getOwnPropertyDescriptor(payload, pinned.field); - if (descriptor === undefined || !("value" in descriptor)) return "malformed"; - return pinned.read(descriptor.value); + const descriptor = ObjectGetOwnPropertyDescriptor(payload, pinned.field); + // An own "value" only: a value written on Object.prototype never makes an accessor read as data. + if (descriptor === undefined || !hasOwn(descriptor, "value")) return "malformed"; + const read = pinned.read; + return read(descriptor.value); } return verdictLooking ? "malformed" : "none"; } @@ -546,9 +618,9 @@ function verdictOfPayload(type: string, payload: unknown): InspectionVerdict { * It reads `event.type` and `event.payload` once each. */ export function inspectionVerdict(event: EvidenceEvent): InspectionVerdict { - const type: unknown = event.type; - if (typeof type !== "string" || !INSPECTION.has(type)) return "none"; - return verdictOfPayload(type, event.payload); + const type: unknown = own(event, "type"); + if (typeof type !== "string" || !inSet(INSPECTION, type)) return "none"; + return verdictOfPayload(type, own(event, "payload")); } /** @@ -566,25 +638,59 @@ export function inspectionFailed(event: EvidenceEvent): boolean { // --------------------------------------------------------------------------- // Mirrors #399's OPERATOR_PRINCIPAL_ID_PATTERN (the pcc.evidence.principal-id.v1 -// operator form). Kept local so this branch does not depend on #399. The chain -// id has no leading zero and is at least 1; the safe-integer bound is checked -// separately. Lowercase hex only, so equal principals are equal strings. -const OPERATOR_PRINCIPAL_ID_PATTERN = /^eip155:([1-9][0-9]*):0x[0-9a-f]{40}$/; +// operator form), /^eip155:([1-9][0-9]*):0x[0-9a-f]{40}$/, as a code-unit +// predicate (no RegExp, see rule 8; a test holds it equal to the pattern). Kept +// local so this branch does not depend on #399. The chain id has no leading zero, +// is at least 1 and is a safe integer. Lowercase hex only, so equal principals +// are equal strings. +const OPERATOR_PRINCIPAL_PREFIX = "eip155:"; +/** Number.MAX_SAFE_INTEGER as decimal digits: a longer chain id, or a 16-digit one above it, is not safe. */ +const MAX_SAFE_INTEGER_DIGITS = "9007199254740991"; function isOperatorPrincipalId(value: unknown): value is string { if (typeof value !== "string") return false; - const match = OPERATOR_PRINCIPAL_ID_PATTERN.exec(value); - return match !== null && Number.isSafeInteger(Number(match[1])); + const length = value.length; + let i = 0; + for (; i < OPERATOR_PRINCIPAL_PREFIX.length; i += 1) { + if (i >= length || value[i] !== OPERATOR_PRINCIPAL_PREFIX[i]) return false; + } + // The chain id: [1-9][0-9]*, collected as it is read. + if (i >= length || !(value[i]! >= "1" && value[i]! <= "9")) return false; + let chainId = ""; + while (i < length && value[i]! >= "0" && value[i]! <= "9") { + chainId += value[i]!; + i += 1; + } + if (i + 3 + 40 !== length || value[i] !== ":" || value[i + 1] !== "0" || value[i + 2] !== "x") return false; + for (i += 3; i < length; i += 1) { + const c = value[i]!; + if (!((c >= "0" && c <= "9") || (c >= "a" && c <= "f"))) return false; + } + // Number.isSafeInteger(Number(chainId)), without either: equal-length digit strings compare as numbers. + return ( + chainId.length < MAX_SAFE_INTEGER_DIGITS.length || + (chainId.length === MAX_SAFE_INTEGER_DIGITS.length && chainId <= MAX_SAFE_INTEGER_DIGITS) + ); } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); + return typeof value === "object" && value !== null && !ArrayIsArray(value); +} + +/** + * `object[key]` when `key` is the object's OWN property, read once (an own accessor + * runs once, rule 7); undefined when it is inherited or absent, so nothing written + * on Object.prototype or Array.prototype is taken for the input's (rule 8). + */ +function own(object: object, key: PropertyKey): unknown { + return hasOwn(object, key) ? (object as Record)[key] : undefined; } /** `list.length`, read once, which must be a non-negative safe integer. */ function readLength(list: readonly unknown[], what: string): number { const length: unknown = list.length; - if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0) { + // Number.isSafeInteger(length) && length >= 0, from the captured NumberIsInteger. + if (typeof length !== "number" || !NumberIsInteger(length) || length < 0 || length > 9007199254740991) { throw new EvidenceLevelInputError(`${what} has no valid length`); } return length; @@ -614,26 +720,43 @@ interface PreparedBundle { readonly events: readonly EventFacts[]; } +/** + * A null-prototype snapshot holding only `key`, read once as an OWN property of `value`, and + * undefined when `value` is not an object or `key` is inherited or absent. isFabricated reads + * these, so no inherited value or getter and no boxed primitive can answer (astra pack 267). + */ +function ownFieldView(value: unknown, key: string): Record { + const view = ObjectCreate(null) as Record; + view[key] = typeof value === "object" && value !== null ? own(value, key) : undefined; + return view; +} + /** * Read one event, once, into its frozen facts: `type`, `source` and `payload` * each once, then `source.deviceId`, `source.simulated`, `payload.mock` and the * payload's verdict material each once, all from those locals. */ function readEventFacts(event: Record): EventFacts { - const rawType: unknown = event.type; - const source: unknown = event.source; - const payload: unknown = event.payload; + const rawType: unknown = own(event, "type"); + const source: unknown = own(event, "source"); + const payload: unknown = own(event, "payload"); const type = typeof rawType === "string" ? rawType : null; - // The canonical predicate, over a snapshot of the values read: it reads - // source.simulated and payload.mock once each, so this cannot drift from it. - const fabricated = isFabricated({ type, source, payload } as unknown as EvidenceEvent); + // The canonical predicate, over null-prototype snapshots of the two values it reads, each + // read once as an OWN property: source.simulated and payload.mock. An inherited mock getter + // could otherwise write source.deviceId before it is read, and a primitive source or + // payload would box and consult its prototypes (astra pack 267). + const fabricated = isFabricated({ + type, + source: ownFieldView(source, "simulated"), + payload: ownFieldView(payload, "mock"), + } as unknown as EvidenceEvent); let deviceAttributed = false; if (typeof source === "object" && source !== null) { - const deviceId: unknown = (source as { deviceId?: unknown }).deviceId; + const deviceId: unknown = own(source, "deviceId"); deviceAttributed = typeof deviceId === "string" && deviceId.length > 0 && !isGatewayStamp(deviceId); } - const verdict: InspectionVerdict = type !== null && INSPECTION.has(type) ? verdictOfPayload(type, payload) : "none"; - return Object.freeze({ type, deviceAttributed, fabricated, verdict }); + const verdict: InspectionVerdict = type !== null && inSet(INSPECTION, type) ? verdictOfPayload(type, payload) : "none"; + return ObjectFreeze({ type, deviceAttributed, fabricated, verdict }); } /** @@ -645,34 +768,34 @@ function readEventFacts(event: Record): EventFacts { * disagree: the one read decides. */ function prepareBundles(bundles: unknown): readonly PreparedBundle[] { - if (!Array.isArray(bundles)) { + if (!ArrayIsArray(bundles)) { throw new EvidenceLevelInputError("bundles must be an array of AuthenticatedBundle"); } const bundleCount = readLength(bundles, "bundles"); const prepared: PreparedBundle[] = []; for (let i = 0; i < bundleCount; i += 1) { - const bundle: unknown = bundles[i]; + const bundle: unknown = own(bundles, i); if (!isRecord(bundle)) { throw new EvidenceLevelInputError(`bundles[${i}] must be an object`); } - const events: unknown = bundle.events; - if (!Array.isArray(events)) { + const events: unknown = own(bundle, "events"); + if (!ArrayIsArray(events)) { throw new EvidenceLevelInputError(`bundles[${i}].events must be an array`); } - const declared: unknown = bundle.trustDomain; + const declared: unknown = own(bundle, "trustDomain"); const eventCount = readLength(events, `bundles[${i}].events`); const facts: EventFacts[] = []; let fabricated = false; let holdsExecutionEvent = false; for (let j = 0; j < eventCount; j += 1) { - const event: unknown = events[j]; + const event: unknown = own(events, j); if (!isRecord(event)) { throw new EvidenceLevelInputError(`bundles[${i}].events[${j}] must be an object`); } const eventFacts = readEventFacts(event); - facts.push(eventFacts); + append(facts, eventFacts); if (eventFacts.fabricated) fabricated = true; - if (eventFacts.type !== null && EXECUTION.has(eventFacts.type)) holdsExecutionEvent = true; + if (eventFacts.type !== null && inSet(EXECUTION, eventFacts.type)) holdsExecutionEvent = true; } let trustDomain: string | null = null; if (declared !== undefined) { @@ -683,9 +806,9 @@ function prepareBundles(bundles: unknown): readonly PreparedBundle[] { } trustDomain = declared; } - prepared.push(Object.freeze({ trustDomain, fabricated, holdsExecutionEvent, events: Object.freeze(facts) })); + append(prepared, ObjectFreeze({ trustDomain, fabricated, holdsExecutionEvent, events: ObjectFreeze(facts) })); } - return Object.freeze(prepared); + return ObjectFreeze(prepared); } function assignedExecutorDomains(context: unknown): readonly string[] { @@ -693,21 +816,21 @@ function assignedExecutorDomains(context: unknown): readonly string[] { if (!isRecord(context)) { throw new EvidenceLevelInputError("context must be an object"); } - const assigned: unknown = context.executorTrustDomains; + const assigned: unknown = own(context, "executorTrustDomains"); if (assigned === undefined) return []; - if (!Array.isArray(assigned)) { + if (!ArrayIsArray(assigned)) { throw new EvidenceLevelInputError("context.executorTrustDomains must be an array of operator principal ids"); } const count = readLength(assigned, "context.executorTrustDomains"); const domains: string[] = []; for (let i = 0; i < count; i += 1) { - const domain: unknown = assigned[i]; + const domain: unknown = own(assigned, i); if (!isOperatorPrincipalId(domain)) { throw new EvidenceLevelInputError( `context.executorTrustDomains[${i}] is not an operator principal id (eip155::0x<40 lowercase hex>)`, ); } - domains.push(domain); + append(domains, domain); } return domains; } @@ -720,9 +843,9 @@ function assignedExecutorDomains(context: unknown): readonly string[] { function eventLevel(facts: EventFacts, inspectorIndependent: boolean): EvidenceLevel | null { if (!facts.deviceAttributed || facts.type === null) return null; const type = facts.type; - if (SUBMITTED.has(type)) return "submitted"; - if (DEVICE_REPORTED.has(type)) return "device_reported"; - if (INSPECTION.has(type)) { + if (inSet(SUBMITTED, type)) return "submitted"; + if (inSet(DEVICE_REPORTED, type)) return "device_reported"; + if (inSet(INSPECTION, type)) { if (facts.verdict !== "pass" && facts.verdict !== "fail") return null; return inspectorIndependent ? "inspected_output" : "device_reported"; } @@ -764,12 +887,16 @@ export function evidenceLevelsOfEvents( const assigned = assignedExecutorDomains(context); const prepared = prepareBundles(bundles); - const executors = new Set(assigned); + // The executor domains, as a null-prototype record: the assigned ones, then every + // bundle's that holds an execution event. + const executors = ObjectCreate(null) as Record; + for (let i = 0; i < assigned.length; i += 1) executors[assigned[i]!] = true; let executorsUnknown = false; - for (const bundle of prepared) { + for (let b = 0; b < prepared.length; b += 1) { + const bundle = prepared[b]!; if (!bundle.holdsExecutionEvent) continue; if (bundle.trustDomain === null) executorsUnknown = true; - else executors.add(bundle.trustDomain); + else executors[bundle.trustDomain] = true; } const independenceProvable = assigned.length > 0 && !executorsUnknown; @@ -777,13 +904,13 @@ export function evidenceLevelsOfEvents( for (let bundleIndex = 0; bundleIndex < prepared.length; bundleIndex += 1) { const bundle = prepared[bundleIndex]!; const inspectorIndependent = - independenceProvable && bundle.trustDomain !== null && !executors.has(bundle.trustDomain); + independenceProvable && bundle.trustDomain !== null && !hasOwn(executors, bundle.trustDomain); for (let eventIndex = 0; eventIndex < bundle.events.length; eventIndex += 1) { const level = bundle.fabricated ? null : eventLevel(bundle.events[eventIndex]!, inspectorIndependent); - levels.push(Object.freeze({ bundleIndex, eventIndex, level })); + append(levels, ObjectFreeze({ bundleIndex, eventIndex, level })); } } - return Object.freeze(levels); + return ObjectFreeze(levels); } /** @@ -797,7 +924,9 @@ export function evidenceLevelOfBundles( context?: EvidenceLevelContext, ): EvidenceLevel | null { let best: EvidenceLevel | null = null; - for (const { level } of evidenceLevelsOfEvents(bundles, context)) { + const levels = evidenceLevelsOfEvents(bundles, context); + for (let i = 0; i < levels.length; i += 1) { + const level = levels[i]!.level; if (level !== null && (best === null || evidenceLevelRank(level) > evidenceLevelRank(best))) { best = level; } @@ -841,17 +970,20 @@ export function deriveContradictions(bundles: readonly AuthenticatedBundle[]): C let completed = false; let failed = false; let failedInspection = false; - for (const bundle of prepareBundles(bundles)) { + const prepared = prepareBundles(bundles); + for (let b = 0; b < prepared.length; b += 1) { + const bundle = prepared[b]!; if (bundle.fabricated) continue; - for (const facts of bundle.events) { - if (facts.type !== null && DEVICE_REPORTED.has(facts.type)) completed = true; + for (let e = 0; e < bundle.events.length; e += 1) { + const facts = bundle.events[e]!; + if (facts.type !== null && inSet(DEVICE_REPORTED, facts.type)) completed = true; if (facts.type === "execution_failed") failed = true; if (facts.verdict === "fail" || facts.verdict === "malformed") failedInspection = true; } } if (!completed) return []; const kinds: ContradictionKind[] = []; - if (failed) kinds.push("completion-and-failure"); - if (failedInspection) kinds.push("completion-and-failed-inspection"); + if (failed) append(kinds, "completion-and-failure"); + if (failedInspection) append(kinds, "completion-and-failed-inspection"); return kinds; } diff --git a/packages/spec/src/util/primordials.ts b/packages/spec/src/util/primordials.ts index a637f994e..7abfe9e71 100644 --- a/packages/spec/src/util/primordials.ts +++ b/packages/spec/src/util/primordials.ts @@ -18,6 +18,14 @@ * * Browser-safe: no `node:` import, because the dashboard bundles canonical.ts. * Internal to @pcc/spec; not exported from the package index. + * + * #363 round 9 adds what profile admission and the evidence levels need: the + * tagged-digest check, own-data reads, Set methods for the evidence levels' + * exported Set API, the structured clone, and promises whose delivery reads + * nothing that code running after load can replace: `ownPromise` for a promise + * handed to a caller (its species is pinned too, so every promise derived from + * it is: astra pack 187), `awaitedHere` for one this package only awaits, and + * `fulfillsWithTrue`. */ const FunctionPrototype = Function.prototype; @@ -147,3 +155,190 @@ export function isHex256Digest(value: unknown): value is `0x${string}` { } return true; } + +// -- #363 round 9: profile admission and the evidence levels (steward #5186) -- + +export const ReflectOwnKeys = Reflect.ownKeys; +export const DateParse = Date.parse; +export const ErrorCtor = Error; +export const PromiseCtor = Promise; +export const SetCtor = Set; +export const SetPrototypeAdd = uncurryThis(Set.prototype.add) as (set: Set, value: unknown) => Set; +export const SetPrototypeHas = uncurryThis(Set.prototype.has) as (set: ReadonlySet, value: unknown) => boolean; +export const SetPrototypeSize = uncurryThis(ObjectGetOwnPropertyDescriptor(Set.prototype, "size")!.get!) as (set: ReadonlySet) => number; +/** The runtime's structured clone (HTML, and Node since 17), captured at load. */ +export const StructuredClone = (globalThis as { structuredClone?: (value: T) => T }).structuredClone; +const PromisePrototypeThenOriginal = Promise.prototype.then; +const PromisePrototypeCatchOriginal = Promise.prototype.catch; +const PromisePrototypeFinallyOriginal = Promise.prototype.finally; +const PromisePrototypeThen = uncurryThis(Promise.prototype.then) as ( + promise: Promise, + onFulfilled: (value: unknown) => unknown, + onRejected: (reason: unknown) => unknown, +) => Promise; + +/** `s.charCodeAt(i)`, through String.prototype.charCodeAt as it was at load. */ +export function charCodeAt(s: string, i: number): number { + return StringPrototypeCharCodeAt(s, i); +} + +const TAGGED_SHA256_PREFIX = "sha256:"; + +/** + * `sha256:` followed by exactly 64 lowercase hex digits: the evidence family's + * tagged digest (signing-preimage.ts TAGGED_DIGEST_PATTERN), checked code unit + * by code unit, with no RegExp (astra pack 167). + */ +export function isTaggedSha256(value: unknown): value is `sha256:${string}` { + if (typeof value !== "string" || value.length !== 71) return false; + for (let i = 0; i < 7; i++) { + if (StringPrototypeCharCodeAt(value, i) !== StringPrototypeCharCodeAt(TAGGED_SHA256_PREFIX, i)) return false; + } + for (let i = 7; i < 71; i++) { + const unit = StringPrototypeCharCodeAt(value, i); + if (!((unit >= 0x30 && unit <= 0x39) || (unit >= 0x61 && unit <= 0x66))) return false; + } + return true; +} + +/** + * An own data property's value: undefined when `o` is not an object, or the + * property is absent, inherited, or an accessor (which never runs). Nothing + * written on a prototype after load can supply it. + */ +export function ownDataValue(o: unknown, key: PropertyKey): unknown { + if (o === null || (typeof o !== "object" && typeof o !== "function")) return undefined; + const descriptor = ObjectGetOwnPropertyDescriptor(o, key); + return descriptor !== undefined && hasOwn(descriptor, "value") ? descriptor.value : undefined; +} + +/** A data descriptor that is not writable, enumerable or configurable, with a null prototype. */ +function fixedDescriptor(value: unknown): PropertyDescriptor { + const descriptor = ObjectCreate(null) as PropertyDescriptor; + descriptor.value = value; + descriptor.writable = false; + descriptor.enumerable = false; + descriptor.configurable = false; + return descriptor; +} + +const SymbolSpecies = Symbol.species; + +/** + * The species of every promise `ownPromise` returns (astra pack 187). Native + * `then`, `catch` and `finally` build the promise they return by constructing + * this with their executor: it makes a native promise with the Promise + * captured at load and passes it through `ownPromise`, so the derived promise + * is pinned like its parent, and so on down any chain. A function declaration, + * because a species must be a constructor. Frozen. + */ +function PinnedSpecies(executor: (resolve: (value: unknown) => void, reject: (reason: unknown) => void) => void): Promise { + return ownPromise(new PromiseCtor(executor)); +} +ObjectFreeze(PinnedSpecies); + +/** + * What `ownPromise` installs as a promise's own `constructor`: a record with no + * prototype whose one property, `[Symbol.species]`, is PinnedSpecies. + * SpeciesConstructor reads that property of `constructor` and nothing else. + * Every returned promise shares this record, and any caller can reach it + * (`promise.constructor`), so it is frozen: otherwise one caller could re-point + * the species every other caller's `.then` uses. + */ +const PINNED_CONSTRUCTOR = ObjectCreate(null) as Record; +PINNED_CONSTRUCTOR[SymbolSpecies] = PinnedSpecies; +ObjectFreeze(PINNED_CONSTRUCTOR); + +/** + * `promise`, ready to hand to a caller: whatever code running after load + * replaces on Promise, Promise.prototype or Promise[Symbol.species], the + * caller receives the value the promise settles with, through + * `await promise`, through `promise.then`, `.catch` and `.finally`, and + * through every promise those return, at any depth. + * + * It gets four own properties, which are read before anything on + * Promise.prototype, each fixed (not writable, enumerable or configurable): + * - `then`, `catch` and `finally`: Promise.prototype's, as they were at + * load. A caller's `promise.catch(...)` would otherwise look the method + * up on Promise.prototype, where code running after load can replace it + * with one that answers anything (astra pack 187, checked with the + * species). The native `catch` and `finally` call the own `then`, and + * `finally` builds its promises with the pinned species; + * - `constructor`: PINNED_CONSTRUCTOR, whose `[Symbol.species]` is + * PinnedSpecies. Native `then`, `catch` and `finally` build the promise + * they return with SpeciesConstructor(promise), that is + * `promise.constructor[Symbol.species]`. A `constructor` pinned to the + * global Promise (#363 round 9) was not enough: its species is a + * configurable accessor, which code running after load can point at a + * constructor whose "promise" is a forged thenable (astra pack 187). + * + * `await promise` takes the engine's thenable path, since `constructor` is not + * %Promise%: it calls the own `then`, which reads the own `constructor` and + * the frozen record's species, and builds its promise with PinnedSpecies and + * the Promise captured at load. Nothing it reads is on Promise.prototype or + * the global Promise. The value is then resolved as any resolution is, + * looking `then` up on it if it is an object. So hand out only promises that + * settle with a primitive or an object with no prototype (admission's result, + * the set digest, a boolean). A promise this package awaits itself, and never + * hands out, takes `awaitedHere`, on whose value `await` looks nothing up. + * + * The boundary: the promise returned, its own methods, and the promises they + * return. A static that a caller calls after the change, such as + * `Promise.all`, `Promise.race` or `Promise.resolve`, reads the global Promise + * and its statics as they are then (`Promise.all` looks up `resolve` on its + * receiver, and `then` on each promise it makes) and is outside it. So is what + * a caller's own callback returns: a derived promise follows it, as `then` + * does. `promise.constructor` is not the global Promise; `instanceof Promise` + * still holds. + */ +export function ownPromise(promise: Promise): Promise { + ObjectDefineProperty(promise, "constructor", fixedDescriptor(PINNED_CONSTRUCTOR)); + ObjectDefineProperty(promise, "then", fixedDescriptor(PromisePrototypeThenOriginal)); + ObjectDefineProperty(promise, "catch", fixedDescriptor(PromisePrototypeCatchOriginal)); + ObjectDefineProperty(promise, "finally", fixedDescriptor(PromisePrototypeFinallyOriginal)); + return promise; +} + +/** + * `promise`, to be awaited by this package and never handed out. Its own, + * fixed `constructor` is the Promise captured at load, which is %Promise% + * itself, so `await` takes the engine's internal path: it reads that one own + * property and attaches its reactions directly, with no `then` call and no + * species, and the value arrives without `then` being looked up on it. (An + * `ownPromise` resolves `await` through its `then`, and a resolution looks + * `then` up on an object value; binding's answer is an ordinary object.) Its + * own `then` is Promise.prototype.then as it was at load. Never return it: a + * `.then` call on it builds its result with the global Promise's species, + * which code running after load can replace. + */ +export function awaitedHere(promise: Promise): Promise { + ObjectDefineProperty(promise, "constructor", fixedDescriptor(PromiseCtor)); + ObjectDefineProperty(promise, "then", fixedDescriptor(PromisePrototypeThenOriginal)); + return promise; +} + +/** + * Whether a trusted callback answered exactly true, or with a native promise + * that fulfills with exactly true. A promise is followed through + * Promise.prototype.then as it was at load: its handlers receive the value + * that promise settled with, whatever species that call constructs for the + * promise it returns (that promise is discarded; a species that throws, or + * never hands over its resolving functions, makes the answer false). The + * answer is a boolean, or an `ownPromise` of one, so nothing replaced after + * load changes it, whether it is awaited or followed with `.then`, `.catch` + * or `.finally` (the boundary is `ownPromise`'s). Anything else answers + * false: a value that is not exactly true, a rejection, and an object that is + * not a native promise (a thenable is never followed). + */ +export function fulfillsWithTrue(answer: unknown): boolean | Promise { + if (typeof answer !== "object" || answer === null) return answer === true; + return ownPromise( + new PromiseCtor((resolve) => { + try { + PromisePrototypeThen(answer as Promise, (value) => resolve(value === true), () => resolve(false)); + } catch { + resolve(false); + } + }), + ); +}