OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / check-vnext-protocol-remediation.mjs

175 lines · javascript ref:
#!/usr/bin/env node

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const scriptPath = fileURLToPath(import.meta.url)
const repoRoot = path.resolve(path.dirname(scriptPath), '..')
const defaultRemediation = path.join(repoRoot, 'docs', 'vnext', 'protocol-remediation.json')
const defaultProgram = path.join(repoRoot, 'docs', 'vnext', 'program-state.json')
const CR_IDS = Array.from({ length: 8 }, (_, index) => `CR-${index + 1}`)
const FINAL_AUTHORITY_PATHS = [
  'packages/blind-protocol/hiverelay-blind-wire-authority-v1.json',
  'packages/blind-protocol/hiverelay-blind-abi-v1.cenc',
  'packages/blind-protocol/vector-manifest-v1.cenc',
  'packages/blind-protocol/wire-runtime-authority.js'
]
const SAFE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/
const SHA1 = /^[0-9a-f]{40}$/

if (path.resolve(process.argv[1] || '') === scriptPath) {
  try {
    const remediationPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultRemediation
    const programPath = process.argv[3] ? path.resolve(process.argv[3]) : defaultProgram
    const remediation = JSON.parse(fs.readFileSync(remediationPath, 'utf8'))
    const program = JSON.parse(fs.readFileSync(programPath, 'utf8'))
    const result = validateVnextProtocolRemediation(remediation, program, { repoRoot })
    console.log(JSON.stringify({ status: 'pass', remediationPath, programPath, ...result }, null, 2))
  } catch (err) {
    console.error(`vNext protocol-remediation check: ${safeError(err)}`)
    process.exitCode = 1
  }
}

export function validateVnextProtocolRemediation (remediation, program, options = {}) {
  const root = path.resolve(options.repoRoot || repoRoot)
  object(remediation, 'protocol remediation')
  exactKeys(remediation, [
    'schema', 'sourceAudit', 'candidateProtocolCommit', 'authorityPolicy',
    'requiredDecisions', 'controls', 'requiredBeforeFreeze'
  ], 'protocol remediation')
  equal(remediation.schema, 'hiverelay-vnext-protocol-remediation-v1', 'protocol remediation schema')
  safeReference(remediation.sourceAudit, 'source audit')
  if (!SHA1.test(remediation.candidateProtocolCommit || '')) throw new Error('candidate protocol commit must be a full lowercase SHA-1')

  validateAuthorityPolicy(remediation.authorityPolicy, root)
  exactStringSet(remediation.requiredDecisions, ['D-6', 'D-7'], 'required decisions')
  validateControls(remediation.controls)
  uniqueStrings(remediation.requiredBeforeFreeze, 'requiredBeforeFreeze')
  if (remediation.requiredBeforeFreeze.length < 5) throw new Error('requiredBeforeFreeze is incomplete')
  for (const row of remediation.requiredBeforeFreeze) boundedString(row, 'freeze requirement', 16, 512)

  object(program, 'program state')
  if (!Array.isArray(program.decisions) || !Array.isArray(program.gates)) throw new Error('program state lacks decisions or gates')
  const decisions = new Map(program.decisions.map(row => [row.id, row]))
  const d6 = decisions.get('D-6')
  const d7 = decisions.get('D-7')
  if (!d6 || !d7) throw new Error('program state lacks D-6 or D-7')
  const pg2 = program.gates.find(row => row.id === 'PG-2')
  if (!pg2) throw new Error('program state lacks PG-2')
  const controlsPassed = remediation.controls.every(row => row.gateStatus === 'passed')
  const d6Resolved = !String(d6.status || '').startsWith('pending-')
  const d7Ratified = d7.status === 'approved'
  const freezeEligible = controlsPassed && d6Resolved && d7Ratified && pg2.status === 'passed'
  if (remediation.authorityPolicy.freezeAllowed !== freezeEligible) {
    throw new Error(`authority freezeAllowed must equal computed eligibility ${freezeEligible}`)
  }
  if (freezeEligible) {
    if (remediation.authorityPolicy.status !== 'frozen' || remediation.authorityPolicy.finalAuthorityForbidden) {
      throw new Error('eligible authority must explicitly move from draft-only to frozen')
    }
  } else if (remediation.authorityPolicy.status !== 'draft-only' || !remediation.authorityPolicy.finalAuthorityForbidden) {
    throw new Error('ineligible authority must remain draft-only and forbid final paths')
  }

  return {
    schema: remediation.schema,
    authorityStatus: remediation.authorityPolicy.status,
    freezeEligible,
    pendingControls: remediation.controls.filter(row => row.gateStatus !== 'passed').map(row => row.id),
    blockingDecisions: [
      ...(d6Resolved ? [] : ['D-6']),
      ...(d7Ratified ? [] : ['D-7'])
    ]
  }
}

function validateAuthorityPolicy (value, root) {
  object(value, 'authorityPolicy')
  exactKeys(value, ['status', 'freezeAllowed', 'finalAuthorityForbidden', 'finalAuthorityPaths'], 'authorityPolicy')
  if (!['draft-only', 'frozen'].includes(value.status)) throw new Error('authorityPolicy status is invalid')
  if (typeof value.freezeAllowed !== 'boolean' || typeof value.finalAuthorityForbidden !== 'boolean') {
    throw new Error('authorityPolicy booleans are invalid')
  }
  exactStringSet(value.finalAuthorityPaths, FINAL_AUTHORITY_PATHS, 'final authority paths')
  if (!value.finalAuthorityForbidden) return
  const present = FINAL_AUTHORITY_PATHS.filter(relative => fs.existsSync(path.join(root, relative)))
  if (present.length > 0) throw new Error(`final authority paths are forbidden before freeze: ${present.join(', ')}`)
}

function validateControls (rows) {
  if (!Array.isArray(rows)) throw new Error('controls must be an array')
  const ids = rows.map(row => row?.id)
  if (ids.length !== CR_IDS.length || new Set(ids).size !== ids.length || CR_IDS.some(id => !ids.includes(id))) {
    throw new Error(`controls must contain exactly ${CR_IDS.join(', ')}`)
  }
  for (const row of rows) {
    object(row, `control ${row.id}`)
    exactKeys(row, ['id', 'implementationStatus', 'gateStatus', 'requiredValidations', 'evidence'], `control ${row.id}`)
    if (!['missing', 'partial', 'implemented'].includes(row.implementationStatus)) throw new Error(`control ${row.id} implementationStatus is invalid`)
    if (!['blocked', 'pending', 'passed'].includes(row.gateStatus)) throw new Error(`control ${row.id} gateStatus is invalid`)
    uniqueStrings(row.requiredValidations, `control ${row.id} requiredValidations`)
    if (row.requiredValidations.length === 0) throw new Error(`control ${row.id} has no required validation`)
    for (const ref of row.requiredValidations) safeReference(ref, `control ${row.id} validation`)
    uniqueStrings(row.evidence, `control ${row.id} evidence`)
    for (const ref of row.evidence) safeReference(ref, `control ${row.id} evidence`)
    if ((row.gateStatus === 'passed' || row.implementationStatus !== 'missing') && row.evidence.length === 0) {
      throw new Error(`control ${row.id} requires evidence for its claimed progress`)
    }
    if (row.gateStatus === 'passed' && row.implementationStatus !== 'implemented') {
      throw new Error(`passed control ${row.id} must be implemented`)
    }
  }
}

function exactStringSet (actual, expected, label) {
  uniqueStrings(actual, label)
  if (actual.length !== expected.length || expected.some(value => !actual.includes(value))) {
    throw new Error(`${label} must contain exactly ${expected.join(', ')}`)
  }
}

function safeReference (value, label) {
  if (!SAFE_REFERENCE.test(value || '') || value.startsWith('/') || value.includes('..')) throw new Error(`${label} is unsafe`)
}

function uniqueStrings (values, label) {
  if (!Array.isArray(values) || values.some(value => typeof value !== 'string')) throw new Error(`${label} must be a string array`)
  if (new Set(values).size !== values.length) throw new Error(`${label} must not contain duplicates`)
}

function boundedString (value, label, minimum, maximum) {
  if (typeof value !== 'string' || value.length < minimum || value.length > maximum || hasControlChars(value)) {
    throw new Error(`${label} must be a bounded printable string`)
  }
}

function object (value, label) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object`)
}

function exactKeys (value, keys, label) {
  const extra = Object.keys(value).filter(key => !keys.includes(key))
  const missing = keys.filter(key => !Object.hasOwn(value, key))
  if (extra.length || missing.length) throw new Error(`${label} has invalid keys (extra=${extra.join(',') || 'none'} missing=${missing.join(',') || 'none'})`)
}

function equal (actual, expected, label) {
  if (actual !== expected) throw new Error(`${label} must equal ${JSON.stringify(expected)}`)
}

function hasControlChars (value) {
  return Array.from(value).some(character => {
    const code = character.charCodeAt(0)
    return code <= 31 || code === 127
  })
}

function safeError (err) {
  return Array.from(String(err?.message || err || 'unknown error'), character => {
    const code = character.charCodeAt(0)
    return code <= 31 || code === 127 ? ' ' : character
  }).join('').slice(0, 1200)
}