/* HALTSEAL Proof Playground — Offline verifier engine (WebWorker)
 *
 * Design goals:
 * - No network calls
 * - Minimal dependency surface (no third-party libs)
 * - Deterministic, replayable verification of a Conformance Pack
 *
 * Note: This is a public-safe demo verifier. It verifies integrity + pins + signatures + deterministic replay.
 * It does NOT guarantee semantic correctness of an AI system or universal safety.
 */

'use strict';

// ---------- Utilities ----------
const te = new TextEncoder();
const td = new TextDecoder('utf-8', { fatal: false });

function nowUtcIso() {
  return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
}

function bytesToUtf8(u8) {
  return td.decode(u8);
}

function utf8ToBytes(s) {
  return te.encode(s);
}

function bytesToHex(u8) {
  let out = '';
  for (let i = 0; i < u8.length; i++) out += u8[i].toString(16).padStart(2, '0');
  return out;
}

function bytesEqual(a, b) {
  if (!a || !b) return false;
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
  return true;
}

function b64uToBytes(s) {
  // base64url (no padding)
  if (typeof s !== 'string') throw new Error('b64uToBytes expects string');
  s = s.replace(/-/g, '+').replace(/_/g, '/');
  const pad = '='.repeat((4 - (s.length % 4)) % 4);
  const bin = atob(s + pad);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

function stripNvrsPrefix(s) {
  // nvrs:sha256:<hex>
  const m = /^nvrs:sha256:([0-9a-f]{64})$/i.exec(String(s || ''));
  return m ? m[1].toLowerCase() : null;
}

function safeJsonParse(text, label) {
  try {
    const v = JSON.parse(text);
    return { ok: true, value: v };
  } catch (e) {
    return { ok: false, error: `${label}: invalid JSON (${String(e?.message || e)})` };
  }
}

async function sha256Hex(bytes) {
  const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
  const dig = await crypto.subtle.digest('SHA-256', ab);
  return bytesToHex(new Uint8Array(dig));
}

// JCS-ish canonicalization: recursively sort object keys (sufficient for our demo objects).
function canonicalizeValue(v) {
  if (v === null) return null;
  if (Array.isArray(v)) return v.map(canonicalizeValue);
  if (typeof v === 'object') {
    const out = {};
    const keys = Object.keys(v).sort();
    for (const k of keys) out[k] = canonicalizeValue(v[k]);
    return out;
  }
  return v; // string/number/bool
}

function jcsBytes(obj) {
  const canon = canonicalizeValue(obj);
  // JSON.stringify emits no extra whitespace by default.
  return utf8ToBytes(JSON.stringify(canon));
}

function cloneWithout(obj, key) {
  const out = {};
  for (const k of Object.keys(obj || {})) {
    if (k === key) continue;
    out[k] = obj[k];
  }
  return out;
}

// ---------- Zip reader (no deps) ----------
// Supports: stored (0) + deflate (8). Uses DecompressionStream when available.

function u16(buf, off) { return buf[off] | (buf[off + 1] << 8); }
function u32(buf, off) { return (buf[off] | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24)) >>> 0; }

async function inflate(bytes) {
  if (!('DecompressionStream' in self)) {
    throw new Error('DecompressionStream unsupported: use a modern Chromium-based browser.');
  }

  // ZIP uses raw DEFLATE streams.
  const formats = ['deflate-raw', 'deflate'];
  let lastErr = null;

  for (const fmt of formats) {
    try {
      const ds = new DecompressionStream(fmt);
      const stream = new Blob([bytes]).stream().pipeThrough(ds);
      const ab = await new Response(stream).arrayBuffer();
      return new Uint8Array(ab);
    } catch (e) {
      lastErr = e;
    }
  }
  throw new Error(`inflate failed: ${String(lastErr?.message || lastErr)}`);
}

async function unzip(zipBytes, limits) {
  const buf = zipBytes instanceof Uint8Array ? zipBytes : new Uint8Array(zipBytes);
  if (buf.byteLength > limits.maxZipBytes) throw new Error(`ZIP exceeds safety cap: ${buf.byteLength} bytes`);

  // EOCD search (within last 65557 bytes)
  const SIG_EOCD = 0x06054b50;
  const maxBack = Math.min(buf.length, 22 + 0xFFFF);
  let eocdOff = -1;
  for (let i = buf.length - 22; i >= buf.length - maxBack; i--) {
    if (i < 0) break;
    if (u32(buf, i) === SIG_EOCD) { eocdOff = i; break; }
  }
  if (eocdOff < 0) throw new Error('EOCD not found (invalid zip)');

  const cdCount = u16(buf, eocdOff + 10);
  const cdSize = u32(buf, eocdOff + 12);
  const cdOff = u32(buf, eocdOff + 16);

  if (cdCount > limits.maxFileCount) throw new Error(`ZIP file count exceeds cap: ${cdCount}`);
  if (cdOff + cdSize > buf.length) throw new Error('Central directory out of range');

  const SIG_CDH = 0x02014b50;
  const SIG_LFH = 0x04034b50;

  let p = cdOff;
  const entries = [];
  for (let i = 0; i < cdCount; i++) {
    if (u32(buf, p) !== SIG_CDH) throw new Error('Invalid central directory header');
    const method = u16(buf, p + 10);
    const compSize = u32(buf, p + 20);
    const uncompSize = u32(buf, p + 24);
    const nameLen = u16(buf, p + 28);
    const extraLen = u16(buf, p + 30);
    const cmtLen = u16(buf, p + 32);
    const lfhOff = u32(buf, p + 42);
    const name = bytesToUtf8(buf.subarray(p + 46, p + 46 + nameLen));
    entries.push({ name, method, compSize, uncompSize, lfhOff });
    p = p + 46 + nameLen + extraLen + cmtLen;
  }

  const files = new Map();
  for (const e of entries) {
    if (e.name.endsWith('/')) continue; // directory
    const o = e.lfhOff;
    if (u32(buf, o) !== SIG_LFH) throw new Error('Invalid local file header');
    const nameLen = u16(buf, o + 26);
    const extraLen = u16(buf, o + 28);
    const dataOff = o + 30 + nameLen + extraLen;
    const comp = buf.subarray(dataOff, dataOff + e.compSize);

    let data;
    if (e.method === 0) {
      data = comp; // stored
    } else if (e.method === 8) {
      data = await inflate(comp);
    } else {
      throw new Error(`Unsupported zip compression method: ${e.method} (${e.name})`);
    }

    if (e.uncompSize !== 0 && data.byteLength !== e.uncompSize) {
      // not fatal for all zips, but here it's a strong signal
      throw new Error(`ZIP size mismatch for ${e.name} (expected ${e.uncompSize}, got ${data.byteLength})`);
    }
    files.set(e.name, new Uint8Array(data)); // ensure copy
  }

  return files;
}

// Read helpers (respect root prefix)
function zipReadBytes(files, rootPrefix, relPath) {
  const p = rootPrefix + relPath;
  const b = files.get(p);
  if (!b) throw new Error(`Missing file: ${relPath}`);
  return b;
}

function zipReadText(files, rootPrefix, relPath) {
  return bytesToUtf8(zipReadBytes(files, rootPrefix, relPath));
}

// ---------- Ed25519 via WebCrypto ----------
const _edKeyCache = new Map(); // kid -> CryptoKey
async function importEd25519PublicKey(pubKey32) {
  if (!(pubKey32 instanceof Uint8Array) || pubKey32.length !== 32) throw new Error('Ed25519 public key must be 32 bytes');
  // Cache by hex to avoid keeping multiple keys.
  const k = bytesToHex(pubKey32);
  if (_edKeyCache.has(k)) return _edKeyCache.get(k);

  const key = await crypto.subtle.importKey(
    'raw',
    pubKey32.buffer.slice(pubKey32.byteOffset, pubKey32.byteOffset + pubKey32.byteLength),
    { name: 'Ed25519' },
    false,
    ['verify']
  );
  _edKeyCache.set(k, key);
  return key;
}

async function ed25519Verify(pubKey32, messageBytes, sigBytes) {
  const key = await importEd25519PublicKey(pubKey32);
  return crypto.subtle.verify(
    { name: 'Ed25519' },
    key,
    sigBytes.buffer.slice(sigBytes.byteOffset, sigBytes.byteOffset + sigBytes.byteLength),
    messageBytes.buffer.slice(messageBytes.byteOffset, messageBytes.byteOffset + messageBytes.byteLength)
  );
}

// ---------- DSSE ----------
function dssePAE(payloadType, payloadBytes) {
  const pt = utf8ToBytes(payloadType);
  const pieces = [
    utf8ToBytes('DSSEv1 '),
    utf8ToBytes(String(pt.length)),
    utf8ToBytes(' '),
    pt,
    utf8ToBytes(' '),
    utf8ToBytes(String(payloadBytes.length)),
    utf8ToBytes(' '),
    payloadBytes
  ];
  let total = 0;
  for (const p of pieces) total += p.length;
  const out = new Uint8Array(total);
  let o = 0;
  for (const p of pieces) { out.set(p, o); o += p.length; }
  return out;
}

function dssePayloadBytes(env) {
  return b64uToBytes(env.payload);
}

async function verifyDsseSignature(env, keyid, pubKey32) {
  const sigEntry = (env.signatures || []).find(s => s.keyid === keyid);
  if (!sigEntry) return false;
  const payload = dssePayloadBytes(env);
  const pae = dssePAE(env.payloadType, payload);
  const sigBytes = b64uToBytes(sigEntry.sig);
  return ed25519Verify(pubKey32, pae, sigBytes);
}

async function verifyAnyDsseSignature(env, candidates) {
  const payload = dssePayloadBytes(env);
  const pae = dssePAE(env.payloadType, payload);

  for (const sig of env.signatures || []) {
    const cand = candidates.find(c => c.keyid === sig.keyid);
    if (!cand) continue;
    const sigBytes = b64uToBytes(sig.sig);
    const ok = await ed25519Verify(cand.pubKey32, pae, sigBytes);
    if (ok) return { ok: true, keyid: sig.keyid };
  }
  return { ok: false, error: 'No valid DSSE signature found for candidate keys' };
}

// ---------- COSE_Sign1 (minimal CBOR) ----------
function cborReadUInt(ai, buf, off) {
  if (ai < 24) return { val: ai, off };
  if (ai === 24) return { val: buf[off], off: off + 1 };
  if (ai === 25) return { val: (buf[off] << 8) | buf[off + 1], off: off + 2 };
  if (ai === 26) return { val: u32(buf, off), off: off + 4 };
  throw new Error('CBOR uint too large');
}

function cborDecode(buf, off = 0) {
  const b0 = buf[off++];
  const major = b0 >> 5;
  const ai = b0 & 31;

  // Indefinite length not supported in this demo.
  if (ai === 31) throw new Error('CBOR indefinite length not supported');

  if (major === 0) {
    const r = cborReadUInt(ai, buf, off);
    return { value: r.val, off: r.off };
  }
  if (major === 1) {
    const r = cborReadUInt(ai, buf, off);
    return { value: -1 - r.val, off: r.off };
  }
  if (major === 2) {
    const r = cborReadUInt(ai, buf, off);
    const v = buf.subarray(r.off, r.off + r.val);
    return { value: v, off: r.off + r.val };
  }
  if (major === 3) {
    const r = cborReadUInt(ai, buf, off);
    const v = bytesToUtf8(buf.subarray(r.off, r.off + r.val));
    return { value: v, off: r.off + r.val };
  }
  if (major === 4) {
    const r = cborReadUInt(ai, buf, off);
    const arr = [];
    let o = r.off;
    for (let i = 0; i < r.val; i++) {
      const d = cborDecode(buf, o);
      arr.push(d.value);
      o = d.off;
    }
    return { value: arr, off: o };
  }
  if (major === 5) {
    const r = cborReadUInt(ai, buf, off);
    const m = new Map();
    let o = r.off;
    for (let i = 0; i < r.val; i++) {
      const k = cborDecode(buf, o); o = k.off;
      const v = cborDecode(buf, o); o = v.off;
      m.set(k.value, v.value);
    }
    return { value: m, off: o };
  }
  if (major === 7) {
    if (ai === 20) return { value: false, off };
    if (ai === 21) return { value: true, off };
    if (ai === 22) return { value: null, off };
    throw new Error('CBOR simple/float not supported');
  }
  throw new Error(`CBOR major type unsupported: ${major}`);
}

function cborEncodeText(s) {
  const b = utf8ToBytes(s);
  return cborEncodeBytesWithMajor(3, b);
}

function cborEncodeBytes(u8) {
  return cborEncodeBytesWithMajor(2, u8);
}

function cborEncodeBytesWithMajor(major, u8) {
  const len = u8.length;
  const head = cborEncodeLen(major, len);
  const out = new Uint8Array(head.length + len);
  out.set(head, 0);
  out.set(u8, head.length);
  return out;
}

function cborEncodeArray(arrParts) {
  const head = cborEncodeLen(4, arrParts.length);
  let total = head.length;
  for (const p of arrParts) total += p.length;
  const out = new Uint8Array(total);
  out.set(head, 0);
  let o = head.length;
  for (const p of arrParts) { out.set(p, o); o += p.length; }
  return out;
}

function cborEncodeLen(major, len) {
  if (len < 24) return new Uint8Array([ (major << 5) | len ]);
  if (len < 256) return new Uint8Array([ (major << 5) | 24, len ]);
  if (len < 65536) return new Uint8Array([ (major << 5) | 25, (len >> 8) & 0xff, len & 0xff ]);
  // 32-bit
  return new Uint8Array([ (major << 5) | 26, (len >> 24) & 0xff, (len >> 16) & 0xff, (len >> 8) & 0xff, len & 0xff ]);
}

async function verifyCoseSign1Ed25519(coseBytes, resolveKey) {
  try {
    const d = cborDecode(coseBytes, 0);
    const v = d.value;
    if (!Array.isArray(v) || v.length !== 4) return { ok: false, error: 'COSE_Sign1 must be an array[4]' };

    const protectedBstr = v[0]; // Uint8Array
    const unprotected = v[1]; // Map
    const payload = v[2]; // Uint8Array
    const signature = v[3]; // Uint8Array

    if (!(protectedBstr instanceof Uint8Array) || !(payload instanceof Uint8Array) || !(signature instanceof Uint8Array)) {
      return { ok: false, error: 'COSE_Sign1 parts must be bstr for protected/payload/signature' };
    }

    // protected header is CBOR map
    let kid = null;
    try {
      const ph = cborDecode(protectedBstr, 0).value;
      if (ph instanceof Map) {
        // kid label in COSE is 4
        if (ph.has(4)) {
          const kidVal = ph.get(4);
          if (typeof kidVal === 'string') kid = kidVal;
          if (kidVal instanceof Uint8Array) kid = bytesToUtf8(kidVal);
        }
      }
    } catch (_) {}

    // kid may also exist in unprotected
    if (!kid && unprotected instanceof Map) {
      if (unprotected.has(4)) {
        const kidVal = unprotected.get(4);
        if (typeof kidVal === 'string') kid = kidVal;
        if (kidVal instanceof Uint8Array) kid = bytesToUtf8(kidVal);
      }
    }

    if (!kid) return { ok: false, error: 'COSE kid missing' };

    const pub = resolveKey(kid);
    if (!pub) return { ok: false, error: `COSE kid not found in trust anchors: ${kid}` };

    // Sig_structure = ["Signature1", protected, external_aad, payload]
    const sigStruct = cborEncodeArray([
      cborEncodeText('Signature1'),
      cborEncodeBytes(protectedBstr),
      cborEncodeBytes(new Uint8Array(0)), // external_aad
      cborEncodeBytes(payload),
    ]);

    const ok = await ed25519Verify(pub, sigStruct, signature);
    if (!ok) return { ok: false, error: `COSE signature invalid (kid=${kid})` };
    return { ok: true, kid, payload };
  } catch (e) {
    return { ok: false, error: `COSE verify exception: ${String(e?.message || e)}` };
  }
}

// ---------- JWS (compact, EdDSA) ----------
async function verifyJwsCompactEd25519(jwsCompact, resolveKey) {
  try {
    const parts = String(jwsCompact || '').split('.');
    if (parts.length !== 3) return { ok: false, error: 'JWS must have 3 parts' };
    const [h64, p64, s64] = parts;
    const header = safeJsonParse(bytesToUtf8(b64uToBytes(h64)), 'JWS header');
    if (!header.ok) return { ok: false, error: header.error };
    const kid = header.value?.kid;
    if (typeof kid !== 'string') return { ok: false, error: 'JWS kid missing' };
    const pub = resolveKey(kid);
    if (!pub) return { ok: false, error: `JWS kid not found in trust anchors: ${kid}` };

    const message = utf8ToBytes(h64 + '.' + p64);
    const sigBytes = b64uToBytes(s64);
    const ok = await ed25519Verify(pub, message, sigBytes);
    if (!ok) return { ok: false, error: `JWS signature invalid (kid=${kid})` };

    const payload = b64uToBytes(p64);
    return { ok: true, kid, payload };
  } catch (e) {
    return { ok: false, error: `JWS verify exception: ${String(e?.message || e)}` };
  }
}

// ---------- Transparency receipts ----------
async function verifyTransparencyReceipt(receiptObj, envelopeType, envelopePath, envelopeBytes) {
  const expectSchema = 'mvg.transparency_receipt@1';
  if (receiptObj?.schema_id !== expectSchema) return { ok: false, error: `Receipt schema_id mismatch (expected ${expectSchema})` };

  if (receiptObj?.envelope_type !== envelopeType) return { ok: false, error: `Receipt envelope_type mismatch (expected ${envelopeType})` };
  if (receiptObj?.envelope_path !== envelopePath) return { ok: false, error: `Receipt envelope_path mismatch (expected ${envelopePath})` };

  const h = await sha256Hex(envelopeBytes);
  const want = String(receiptObj?.log_entry_hash_sha256 || '').toLowerCase();
  if (want !== h) return { ok: false, error: `Receipt hash mismatch (computed ${h}, receipt ${want})` };
  return { ok: true };
}

// ---------- Trust anchor map ----------
function jwkToPubKey32(jwk) {
  // JWK OKP Ed25519: x is base64url-encoded 32-byte public key
  const x = jwk?.x;
  if (typeof x !== 'string') return null;
  const b = b64uToBytes(x);
  if (b.length !== 32) return null;
  return b;
}

function buildTrustAnchorMap(trustAnchorSnapshotObj) {
  const map = new Map();
  const entries = trustAnchorSnapshotObj?.entries;
  if (!Array.isArray(entries)) return map;

  for (const e of entries) {
    const kid = e?.trust_anchor_id;
    const status = e?.status || 'active';
    const purposes = Array.isArray(e?.purposes) ? e.purposes : [];
    const pubKey32 = jwkToPubKey32(e?.key_material);

    if (typeof kid === 'string' && pubKey32) {
      map.set(kid, { kid, status, purposes, pubKey32 });
    }
  }
  return map;
}

function trustAnchorsByPurpose(trustAnchors, purpose) {
  const out = [];
  for (const [kid, ta] of trustAnchors.entries()) {
    if (ta?.status === 'active' && Array.isArray(ta?.purposes) && ta.purposes.includes(purpose)) out.push(ta);
  }
  return out;
}

async function verifyContextedJsonSignature(objWithSig, expectedContextLabel, resolveKey) {
  const sig = objWithSig?.signature;
  if (!sig) return { ok: false, error: 'Missing signature object' };
  const kid = sig?.kid;
  const context = sig?.context_label;
  const sigB64u = sig?.sig;
  if (typeof kid !== 'string') return { ok: false, error: 'Signature.kid missing' };
  if (typeof context !== 'string') return { ok: false, error: 'Signature.context_label missing' };
  if (typeof sigB64u !== 'string') return { ok: false, error: 'Signature.sig missing' };
  if (context !== expectedContextLabel) return { ok: false, error: `Context label mismatch (expected ${expectedContextLabel}, got ${context})` };

  const pub = resolveKey(kid);
  if (!pub) return { ok: false, error: `Unknown signing kid: ${kid}` };

  const objNoSig = cloneWithout(objWithSig, 'signature');
  const can = jcsBytes(objNoSig);
  const prefix = utf8ToBytes(expectedContextLabel + '\u0000');
  const preimage = new Uint8Array(prefix.length + can.length);
  preimage.set(prefix, 0);
  preimage.set(can, prefix.length);

  const sigBytes = b64uToBytes(sigB64u);
  const ok = await ed25519Verify(pub, preimage, sigBytes);
  return ok ? { ok: true } : { ok: false, error: 'Signature invalid' };
}

async function verifyRegistrySnapshotId(snapshotObj) {
  const claimed = snapshotObj?.registry_snapshot_id;
  if (typeof claimed !== 'string') return { ok: false, error: 'registry_snapshot_id missing' };
  const claimedHex = stripNvrsPrefix(claimed);
  if (!claimedHex) return { ok: false, error: 'registry_snapshot_id must match nvrs:sha256:<hex>' };

  const noId = cloneWithout(snapshotObj, 'registry_snapshot_id');
  const can = jcsBytes(noId);
  const h = await sha256Hex(can);

  if (h !== claimedHex) return { ok: false, error: 'registry_snapshot_id mismatch', computedHex: h };
  return { ok: true, computedHex: h };
}

function worsenVerdict(current, next) {
  // FAIL > HOLD > PASS
  const rank = { PASS: 0, HOLD: 1, FAIL: 2 };
  return rank[next] > rank[current] ? next : current;
}

function dispositionFromReasonCodes(codes, reasonMap) {
  // DENY > HOLD > ALLOW
  let d = 'ALLOW';
  for (const c of codes || []) {
    const disp = reasonMap.get(c);
    if (disp === 'DENY') return 'DENY';
    if (disp === 'HOLD') d = 'HOLD';
  }
  return d;
}

function postProgress(stage, detail, current = null, total = null) {
  self.postMessage({ type: 'progress', stage, detail, current, total });
}

// ---------- Main verification ----------
async function verifyEvidencePack(zipBytes, auditor) {
  const MAX_ZIP_BYTES = 50 * 1024 * 1024;
  const MAX_FILE_COUNT = 2000;

  const report = {
    schema_id: 'mvg.conformance_report@1',
    schema_version: '1.0.0',
    report_id: `CR-${crypto.randomUUID()}`,
    generated_utc: nowUtcIso(),
    ok: false,
    verdict: 'FAIL',
    pins: {},
    observed_reason_codes: [],
    checks: {
      sha256sums: { ok: false, errors: [] },
      sha256sums_signature: { ok: true, errors: [] },
      manifest_signature: { ok: false, errors: [] },
      claim_card_signature: { ok: false, errors: [] },
      registry_snapshots: { ok: false, errors: [] },
      witness_policy: { ok: true, errors: [] },
      transparency: { ok: true, errors: [] },
      replay: { ok: false, errors: [] },
    }
  };

  let verdict = 'PASS';

  // --- unzip ---
  postProgress('unzip', 'Reading ZIP …');
  const files = await unzip(zipBytes, { maxZipBytes: MAX_ZIP_BYTES, maxFileCount: MAX_FILE_COUNT });

  // find manifest
  const manifestPath = [...files.keys()].find(p => p.endsWith('/manifest.json') || p === 'manifest.json');
  if (!manifestPath) {
    report.checks.sha256sums.errors.push('manifest.json not found');
    report.verdict = 'FAIL';
    report.ok = false;
    return report;
  }

  const rootPrefix = manifestPath.endsWith('manifest.json') ? manifestPath.slice(0, manifestPath.length - 'manifest.json'.length) : '';
  const readRoot = (rel) => zipReadBytes(files, rootPrefix, rel);

  // parse manifest
  postProgress('parse', 'Parsing manifest …');
  const manifestTxt = bytesToUtf8(files.get(manifestPath));
  const pm = safeJsonParse(manifestTxt, 'manifest.json');
  if (!pm.ok) {
    report.checks.sha256sums.errors.push(pm.error);
    report.verdict = 'FAIL';
    report.ok = false;
    return report;
  }
  const manifest = pm.value;

  // record pins
  report.pins = {
    rfc_version: manifest?.rfc_version || null,
    verifier: manifest?.bindings?.verifier || null,
    canonicalization_id: manifest?.bindings?.canonicalization_id || null,
    policy_bundle: manifest?.bindings?.policy_bundle || null,
    registry_pins: manifest?.bindings?.registry_pins || [],
  };

  // --- Bootstrap trust anchors from manifest ---
  const bootstrap = Array.isArray(manifest?.bindings?.bootstrap_trust_anchors) ? manifest.bindings.bootstrap_trust_anchors : [];
  const bootstrapKeys = [];
  for (const b of bootstrap) {
    const keyid = b?.kid;
    const pubKey32 = jwkToPubKey32(b);
    if (typeof keyid === 'string' && pubKey32) bootstrapKeys.push({ keyid, pubKey32 });
  }

  // --- Verify trust anchor registry first ---
  postProgress('registry', 'Verifying trust anchors …');

  const pins = Array.isArray(manifest?.bindings?.registry_pins) ? manifest.bindings.registry_pins : [];
  const taPin = pins.find(p => p?.registry_id === 'mvg.trust_anchors');
  if (!taPin) {
    report.checks.registry_snapshots.errors.push('[trust_anchors] missing pin');
    verdict = worsenVerdict(verdict, 'FAIL');
  }

  let trustAnchors = new Map();

  if (taPin) {
    try {
      const snapshotObj = JSON.parse(zipReadText(files, rootPrefix, taPin.snapshot_path));
      const sid = await verifyRegistrySnapshotId(snapshotObj);
      if (!sid.ok) {
        report.checks.registry_snapshots.errors.push(`[trust_anchors] ${sid.error}`);
        verdict = worsenVerdict(verdict, 'FAIL');
      }

      // pin match
      if (taPin.registry_snapshot_id && taPin.registry_snapshot_id !== snapshotObj.registry_snapshot_id) {
        report.checks.registry_snapshots.errors.push('[trust_anchors] pin mismatch (registry_snapshot_id)');
        verdict = worsenVerdict(verdict, 'FAIL');
      }

      // DSSE
      const dsseEnv = JSON.parse(zipReadText(files, rootPrefix, taPin.dsse_envelope_path));
      if (dsseEnv?.payloadType !== 'application/vnd.mvg.registry.snapshot+json') {
        report.checks.registry_snapshots.errors.push('[trust_anchors] DSSE payloadType mismatch');
        verdict = worsenVerdict(verdict, 'FAIL');
      }

      const payloadBytes = dssePayloadBytes(dsseEnv);
      const expectedPayload = jcsBytes(snapshotObj);
      if (!bytesEqual(payloadBytes, expectedPayload)) {
        report.checks.registry_snapshots.errors.push('[trust_anchors] DSSE payload != JCS(snapshot)');
        verdict = worsenVerdict(verdict, 'FAIL');
      }

      // verify at least one signature with bootstrap keys
      const anyOk = await verifyAnyDsseSignature(dsseEnv, bootstrapKeys);
      if (!anyOk.ok) {
        report.checks.registry_snapshots.errors.push('[trust_anchors] bootstrap DSSE signature invalid');
        verdict = worsenVerdict(verdict, 'FAIL');
      }

      // build TA map (authenticated by bootstrap)
      trustAnchors = buildTrustAnchorMap(snapshotObj);

      // auditor: witness threshold even for trust_anchors (optional)
      if (auditor?.enabled && auditor?.witness?.required) {
        const verified = await countWitnessSignatures(dsseEnv, trustAnchors);
        if (verified < auditor.witness.threshold) {
          report.checks.witness_policy.ok = false;
          report.checks.witness_policy.errors.push(`[trust_anchors] witness threshold not met (verified=${verified}, threshold=${auditor.witness.threshold})`);
          verdict = worsenVerdict(verdict, 'HOLD');
        }
      }

      // auditor: equivalent envelopes (COSE/JWS) + payload equality
      if (auditor?.enabled && auditor?.equivalentEnvelopes) {
        const payloads = [{ profile: 'dsse', bytes: payloadBytes }];

        if (taPin.cose_sign1_path) {
          const coseBytes = zipReadBytes(files, rootPrefix, taPin.cose_sign1_path);
          const coseRes = await verifyCoseSign1Ed25519(coseBytes, (kid) => trustAnchors.get(kid)?.pubKey32 || null);
          if (!coseRes.ok) {
            report.checks.registry_snapshots.errors.push(`[trust_anchors] ${coseRes.error}`);
            verdict = worsenVerdict(verdict, 'FAIL');
          } else {
            payloads.push({ profile: 'cose', bytes: coseRes.payload });
          }
        }

        if (taPin.jws_compact_path) {
          const jwsAscii = zipReadText(files, rootPrefix, taPin.jws_compact_path);
          const jwsRes = await verifyJwsCompactEd25519(jwsAscii, (kid) => trustAnchors.get(kid)?.pubKey32 || null);
          if (!jwsRes.ok) {
            report.checks.registry_snapshots.errors.push(`[trust_anchors] ${jwsRes.error}`);
            verdict = worsenVerdict(verdict, 'FAIL');
          } else {
            payloads.push({ profile: 'jws', bytes: jwsRes.payload });
          }
        }

        for (const p of payloads) {
          if (!bytesEqual(p.bytes, payloadBytes)) {
            report.checks.registry_snapshots.errors.push(`[trust_anchors] envelope payload mismatch: ${p.profile} != dsse`);
            verdict = worsenVerdict(verdict, 'FAIL');
          }
        }
      }

      // auditor: transparency receipts
      if (auditor?.enabled && auditor?.transparency?.required) {
        const wantTypes = auditor.transparency.envelopeTypes || ['dsse'];
        const receipts = Array.isArray(taPin.transparency_receipts) ? taPin.transparency_receipts : [];
        for (const t of wantTypes) {
          const rec = receipts.find(x => x?.envelope_type === t);
          if (!rec?.path) {
            report.checks.transparency.ok = false;
            report.checks.transparency.errors.push(`[trust_anchors] missing transparency receipt for envelope_type=${t}`);
            verdict = worsenVerdict(verdict, 'HOLD');
            continue;
          }
          const receiptTxt = zipReadText(files, rootPrefix, rec.path);
          const pr = safeJsonParse(receiptTxt, rec.path);
          if (!pr.ok) {
            report.checks.transparency.ok = false;
            report.checks.transparency.errors.push(`[trust_anchors] ${pr.error}`);
            verdict = worsenVerdict(verdict, 'FAIL');
            continue;
          }

          const envPath = t === 'dsse' ? taPin.dsse_envelope_path : t === 'cose' ? taPin.cose_sign1_path : taPin.jws_compact_path;
          const envBytes = zipReadBytes(files, rootPrefix, envPath);
          const vr = await verifyTransparencyReceipt(pr.value, t, envPath, envBytes);
          if (!vr.ok) {
            report.checks.transparency.ok = false;
            report.checks.transparency.errors.push(`[trust_anchors] ${vr.error}`);
            verdict = worsenVerdict(verdict, 'FAIL');
          }
        }
      }

    } catch (e) {
      report.checks.registry_snapshots.errors.push(`[trust_anchors] exception: ${String(e?.message || e)}`);
      verdict = worsenVerdict(verdict, 'FAIL');
    }
  }

  // --- Verify manifest + claim-card signatures (requires trust anchors) ---
  postProgress('signatures', 'Verifying manifest + claim card signatures …');
  const resolveKey = (kid) => trustAnchors.get(kid)?.pubKey32 || null;

  try {
    const vm = await verifyContextedJsonSignature(manifest, 'MVG/evidence_pack_manifest/v1', resolveKey);
    if (!vm.ok) {
      report.checks.manifest_signature.errors.push(vm.error || 'manifest signature invalid');
      verdict = worsenVerdict(verdict, 'FAIL');
    } else {
      report.checks.manifest_signature.ok = true;
    }
  } catch (e) {
    report.checks.manifest_signature.errors.push(String(e?.message || e));
    verdict = worsenVerdict(verdict, 'FAIL');
  }

  try {
    const claimTxt = zipReadText(files, rootPrefix, 'claim-card.json');
    const pc = safeJsonParse(claimTxt, 'claim-card.json');
    if (!pc.ok) throw new Error(pc.error);
    const vc = await verifyContextedJsonSignature(pc.value, 'MVG/claim_card/v1', resolveKey);
    if (!vc.ok) {
      report.checks.claim_card_signature.errors.push(vc.error || 'claim-card signature invalid');
      verdict = worsenVerdict(verdict, 'FAIL');
    } else {
      report.checks.claim_card_signature.ok = true;
    }
  } catch (e) {
    report.checks.claim_card_signature.errors.push(String(e?.message || e));
    verdict = worsenVerdict(verdict, 'FAIL');
  }

  // --- Auditor: verify SHA256SUMS.sig (optional) ---
  if (auditor?.enabled) {
    try {
      const sigTxt = zipReadText(files, rootPrefix, 'SHA256SUMS.sig');
      const ps = safeJsonParse(sigTxt, 'SHA256SUMS.sig');
      if (ps.ok) {
        const shaTxt = zipReadText(files, rootPrefix, 'SHA256SUMS');
        const shaDigest = await sha256Hex(utf8ToBytes(shaTxt));
        if (ps.value?.target?.sha256 !== shaDigest) {
          report.checks.sha256sums_signature.ok = false;
          report.checks.sha256sums_signature.errors.push('SHA256SUMS.sig target.sha256 mismatch');
          verdict = worsenVerdict(verdict, 'FAIL');
        }
        const v = await verifyContextedJsonSignature(ps.value, 'MVG/sha256sums/v1', resolveKey);
        if (!v.ok) {
          report.checks.sha256sums_signature.ok = false;
          report.checks.sha256sums_signature.errors.push(v.error || 'SHA256SUMS.sig signature invalid');
          verdict = worsenVerdict(verdict, 'FAIL');
        }
      } else {
        report.checks.sha256sums_signature.ok = false;
        report.checks.sha256sums_signature.errors.push(ps.error);
        verdict = worsenVerdict(verdict, 'HOLD');
      }
    } catch (e) {
      report.checks.sha256sums_signature.ok = false;
      report.checks.sha256sums_signature.errors.push(`SHA256SUMS.sig missing/invalid: ${String(e?.message || e)}`);
      verdict = worsenVerdict(verdict, 'HOLD');
    }
  }

  // --- Verify SHA256SUMS + manifest.files ---
  postProgress('integrity', 'Recomputing SHA‑256 digests …');
  try {
    const shaTxt = zipReadText(files, rootPrefix, 'SHA256SUMS');
    const lines = shaTxt.split(/\r?\n/).filter(Boolean);
    const shaMap = new Map();
    for (const line of lines) {
      const m = /^([0-9a-f]{64})\s{2}(.+)$/.exec(line.trim());
      if (!m) throw new Error('Invalid SHA256SUMS line: ' + line);
      shaMap.set(m[2], m[1].toLowerCase());
    }

    const mf = Array.isArray(manifest?.files) ? manifest.files : [];
    if (mf.length === 0) throw new Error('manifest.files missing/empty');

    for (const f of mf) {
      const rel = f?.path;
      const want = String(f?.sha256 || '').toLowerCase();
      if (typeof rel !== 'string' || want.length !== 64) throw new Error('manifest.files entry invalid');

      const bytes = zipReadBytes(files, rootPrefix, rel);
      const got = await sha256Hex(bytes);
      if (got !== want) throw new Error(`Digest mismatch for ${rel} (manifest)`);
      const want2 = shaMap.get(rel);
      if (want2 && want2 !== got) throw new Error(`Digest mismatch for ${rel} (SHA256SUMS)`);
    }

    report.checks.sha256sums.ok = true;
  } catch (e) {
    report.checks.sha256sums.errors.push(String(e?.message || e));
    verdict = worsenVerdict(verdict, 'FAIL');
  }

  // --- Verify all registry snapshots (pins) ---
  postProgress('registries', 'Verifying pinned registries …');

  const perRegistry = {};
  const profilesVerified = new Set();
  for (const p of pins) {
    const registryId = String(p?.registry_id || '');
    if (!registryId) continue;

    // already did trust anchors above
    if (registryId === 'mvg.trust_anchors') continue;

    let registryOk = true;

    try {
      const snapshotObj = JSON.parse(zipReadText(files, rootPrefix, p.snapshot_path));
      const sid = await verifyRegistrySnapshotId(snapshotObj);
      if (!sid.ok) {
        report.checks.registry_snapshots.errors.push(`[${registryId}] ${sid.error}`);
        verdict = worsenVerdict(verdict, 'FAIL');
        registryOk = false;
      }

      // pin match
      if (p.registry_snapshot_id && p.registry_snapshot_id !== snapshotObj.registry_snapshot_id) {
        report.checks.registry_snapshots.errors.push(`[${registryId}] pin mismatch (registry_snapshot_id)`);
        verdict = worsenVerdict(verdict, 'FAIL');
        registryOk = false;
      }

      // DSSE
      const dsseEnv = JSON.parse(zipReadText(files, rootPrefix, p.dsse_envelope_path));
      if (dsseEnv?.payloadType !== 'application/vnd.mvg.registry.snapshot+json') {
        report.checks.registry_snapshots.errors.push(`[${registryId}] DSSE payloadType mismatch`);
        verdict = worsenVerdict(verdict, 'FAIL');
        registryOk = false;
      }
      const dssePayload = dssePayloadBytes(dsseEnv);
      const expectedPayload = jcsBytes(snapshotObj);
      if (!bytesEqual(dssePayload, expectedPayload)) {
        report.checks.registry_snapshots.errors.push(`[${registryId}] DSSE payload != JCS(snapshot)`);
        verdict = worsenVerdict(verdict, 'FAIL');
        registryOk = false;
      }

      // signing kid check
      const signingKid = snapshotObj?.signing_kid;
      if (typeof signingKid !== 'string') {
        report.checks.registry_snapshots.errors.push(`[${registryId}] snapshot.signing_kid missing`);
        verdict = worsenVerdict(verdict, 'FAIL');
        registryOk = false;
      } else {
        const ta = trustAnchors.get(signingKid);
        if (!ta || ta.status !== 'active') {
          report.checks.registry_snapshots.errors.push(`[${registryId}] signing_kid not found/active in trust anchors: ${signingKid}`);
          verdict = worsenVerdict(verdict, 'FAIL');
          registryOk = false;
        } else {
          // strict: require DSSE signature from signing_kid
          const ok = await verifyDsseSignature(dsseEnv, signingKid, ta.pubKey32);
          if (!ok) {
            report.checks.registry_snapshots.errors.push(`[${registryId}] DSSE signature invalid for signing_kid=${signingKid}`);
            verdict = worsenVerdict(verdict, 'FAIL');
            registryOk = false;
          }
        }
      }

      const profiles = ['dsse'];
      profilesVerified.add('dsse');

      // Auditor: witness threshold
      let witnessOk = true;
      let witnessVerified = 0;
      if (auditor?.enabled && auditor?.witness?.required) {
        witnessVerified = await countWitnessSignatures(dsseEnv, trustAnchors);
        witnessOk = witnessVerified >= auditor.witness.threshold;
        if (!witnessOk) {
          report.checks.witness_policy.ok = false;
          report.checks.witness_policy.errors.push(`[${registryId}] witness threshold not met (verified=${witnessVerified}, threshold=${auditor.witness.threshold})`);
          verdict = worsenVerdict(verdict, 'HOLD');
          registryOk = false;
        }
      }

      // Auditor: equivalent envelopes
      if (auditor?.enabled && auditor?.equivalentEnvelopes) {
        // COSE
        if (p.cose_sign1_path) {
          const coseBytes = zipReadBytes(files, rootPrefix, p.cose_sign1_path);
          const coseRes = await verifyCoseSign1Ed25519(coseBytes, (kid) => trustAnchors.get(kid)?.pubKey32 || null);
          if (!coseRes.ok) {
            report.checks.registry_snapshots.errors.push(`[${registryId}] ${coseRes.error}`);
            verdict = worsenVerdict(verdict, 'FAIL');
            registryOk = false;
          } else {
            profiles.push('cose');
            profilesVerified.add('cose');
            if (typeof signingKid === 'string' && coseRes.kid !== signingKid) {
              report.checks.registry_snapshots.errors.push(`[${registryId}] COSE kid != snapshot.signing_kid (${coseRes.kid} != ${signingKid})`);
              verdict = worsenVerdict(verdict, 'FAIL');
              registryOk = false;
            }
            if (!bytesEqual(coseRes.payload, dssePayload)) {
              report.checks.registry_snapshots.errors.push(`[${registryId}] COSE payload bytes != DSSE payload bytes (equivalence violation)`);
              verdict = worsenVerdict(verdict, 'FAIL');
              registryOk = false;
            }
          }
        }

        // JWS
        if (p.jws_compact_path) {
          const jwsAscii = zipReadText(files, rootPrefix, p.jws_compact_path);
          const jwsRes = await verifyJwsCompactEd25519(jwsAscii, (kid) => trustAnchors.get(kid)?.pubKey32 || null);
          if (!jwsRes.ok) {
            report.checks.registry_snapshots.errors.push(`[${registryId}] ${jwsRes.error}`);
            verdict = worsenVerdict(verdict, 'FAIL');
            registryOk = false;
          } else {
            profiles.push('jws');
            profilesVerified.add('jws');
            if (typeof signingKid === 'string' && jwsRes.kid !== signingKid) {
              report.checks.registry_snapshots.errors.push(`[${registryId}] JWS kid != snapshot.signing_kid (${jwsRes.kid} != ${signingKid})`);
              verdict = worsenVerdict(verdict, 'FAIL');
              registryOk = false;
            }
            if (!bytesEqual(jwsRes.payload, dssePayload)) {
              report.checks.registry_snapshots.errors.push(`[${registryId}] JWS payload bytes != DSSE payload bytes (equivalence violation)`);
              verdict = worsenVerdict(verdict, 'FAIL');
              registryOk = false;
            }
          }
        }
      }

      // Auditor: transparency receipts
      if (auditor?.enabled && auditor?.transparency?.required) {
        const want = auditor.transparency.envelopeTypes || ['dsse'];
        const receipts = Array.isArray(p.transparency_receipts) ? p.transparency_receipts : [];
        for (const t of want) {
          const rec = receipts.find(x => x?.envelope_type === t);
          if (!rec?.path) {
            report.checks.transparency.ok = false;
            report.checks.transparency.errors.push(`[${registryId}] missing transparency receipt for envelope_type=${t}`);
            verdict = worsenVerdict(verdict, 'HOLD');
            registryOk = false;
            continue;
          }
          const receiptTxt = zipReadText(files, rootPrefix, rec.path);
          const pr = safeJsonParse(receiptTxt, rec.path);
          if (!pr.ok) {
            report.checks.transparency.ok = false;
            report.checks.transparency.errors.push(`[${registryId}] ${pr.error}`);
            verdict = worsenVerdict(verdict, 'FAIL');
            registryOk = false;
            continue;
          }

          const envPath = t === 'dsse' ? p.dsse_envelope_path : t === 'cose' ? p.cose_sign1_path : p.jws_compact_path;
          const envBytes = zipReadBytes(files, rootPrefix, envPath);
          const vr = await verifyTransparencyReceipt(pr.value, t, envPath, envBytes);
          if (!vr.ok) {
            report.checks.transparency.ok = false;
            report.checks.transparency.errors.push(`[${registryId}] ${vr.error}`);
            verdict = worsenVerdict(verdict, 'FAIL');
            registryOk = false;
          }
        }
      }

      perRegistry[registryId] = {
        ok: registryOk,
        profiles_verified: profiles,
        witness: auditor?.enabled ? { ok: witnessOk, threshold: auditor?.witness?.threshold || 0, verified: witnessVerified } : undefined,
      };

    } catch (e) {
      report.checks.registry_snapshots.errors.push(`[${registryId}] exception: ${String(e?.message || e)}`);
      verdict = worsenVerdict(verdict, 'FAIL');
      perRegistry[registryId] = { ok: false, profiles_verified: ['dsse'] };
    }
  }

  report.checks.registry_snapshots.ok = report.checks.registry_snapshots.errors.length === 0;
  if (!report.checks.registry_snapshots.ok) verdict = worsenVerdict(verdict, 'FAIL');

  // --- Reason code registry interpretation + deterministic replay ---
  postProgress('replay', 'Replaying decisions under pinned reason code semantics …');

  const reasonMap = new Map();
  try {
    const rcPin = pins.find(p => p?.registry_id === 'mvg.reason_codes');
    if (!rcPin) throw new Error('Missing reason code registry pin');
    const rcSnap = JSON.parse(zipReadText(files, rootPrefix, rcPin.snapshot_path));
    for (const e of (rcSnap.entries || [])) {
      if (typeof e?.code === 'string' && typeof e?.default_disposition === 'string') {
        reasonMap.set(e.code, e.default_disposition);
      }
    }

    const observed = new Map(); // code -> count
    let mismatches = 0;

    // IAL
    const ialTxt = zipReadText(files, rootPrefix, 'artifacts/ial/ial.jsonl');
    for (const line of ialTxt.split(/\r?\n/).filter(Boolean)) {
      const r = safeJsonParse(line, 'ial.jsonl line');
      if (!r.ok) { mismatches++; continue; }
      const rc = Array.isArray(r.value.reason_codes) ? r.value.reason_codes : [];
      for (const c of rc) observed.set(c, (observed.get(c) || 0) + 1);
      const derived = dispositionFromReasonCodes(rc, reasonMap);
      if (String(r.value.disposition) !== derived) mismatches++;
    }

    // Gate evidence
    const gateTxt = zipReadText(files, rootPrefix, 'artifacts/gate-evidence/gate_validation.jsonl');
    for (const line of gateTxt.split(/\r?\n/).filter(Boolean)) {
      const r = safeJsonParse(line, 'gate_validation.jsonl line');
      if (!r.ok) { mismatches++; continue; }
      const rc = Array.isArray(r.value.reason_codes) ? r.value.reason_codes : [];
      for (const c of rc) observed.set(c, (observed.get(c) || 0) + 1);
      const derived = dispositionFromReasonCodes(rc, reasonMap);
      if (String(r.value.gate_decision) !== derived) mismatches++;
    }

    report.observed_reason_codes = [...observed.entries()].map(([code, count]) => ({ code, count, disposition: reasonMap.get(code) || null }));
    report.observed_reason_codes.sort((a, b) => (b.count - a.count) || a.code.localeCompare(b.code));

    if (mismatches > 0) {
      report.checks.replay.ok = false;
      report.checks.replay.errors.push(`Decision replay mismatches: ${mismatches}`);
      verdict = worsenVerdict(verdict, 'FAIL');
    } else {
      report.checks.replay.ok = true;
    }

  } catch (e) {
    report.checks.replay.ok = false;
    report.checks.replay.errors.push(String(e?.message || e));
    verdict = worsenVerdict(verdict, 'FAIL');
  }

  // final verdict
  report.ok = (verdict === 'PASS');
  report.verdict = verdict;
  report.checks.registry_details = perRegistry;
  report.checks.signature_profiles_verified = [...profilesVerified.values()].sort();

  return report;
}

async function countWitnessSignatures(dsseEnv, trustAnchors) {
  let ok = 0;
  const witnesses = trustAnchorsByPurpose(trustAnchors, 'registry.witness_signature');
  const cand = witnesses.map(w => ({ keyid: w.kid, pubKey32: w.pubKey32 }));
  const payload = dssePayloadBytes(dsseEnv);
  const pae = dssePAE(dsseEnv.payloadType, payload);

  for (const sig of dsseEnv.signatures || []) {
    const c = cand.find(x => x.keyid === sig.keyid);
    if (!c) continue;
    const sigBytes = b64uToBytes(sig.sig);
    const v = await ed25519Verify(c.pubKey32, pae, sigBytes);
    if (v) ok++;
  }
  return ok;
}

// ---------- Worker message handling ----------
self.onmessage = async (ev) => {
  const msg = ev.data || {};
  if (msg.type !== 'verify') return;

  try {
    const zipBytes = msg.zipBytes;
    const auditor = msg.auditor || { enabled: false };
    const report = await verifyEvidencePack(zipBytes, auditor);
    self.postMessage({ type: 'result', report });
  } catch (e) {
    const message = String(e?.message || e);
    const isUnsupported = /DecompressionStream unsupported|NotSupportedError|Ed25519/i.test(message);

    const report = {
      schema_id: 'mvg.conformance_report@1',
      schema_version: '1.0.0',
      report_id: `CR-${crypto.randomUUID()}`,
      generated_utc: nowUtcIso(),
      ok: false,
      verdict: isUnsupported ? 'HOLD' : 'FAIL',
      error: message,
      note: isUnsupported
        ? 'This browser lacks required primitives for offline verification. Try a modern Chromium-based browser (latest Chrome/Edge).'
        : undefined
    };

    self.postMessage({ type: 'result', report });
  }
};
