OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-client / generate-browser-artifact-v3.mjs

370 lines · javascript ref:
#!/usr/bin/env node
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'
import b4a from 'b4a'
import { build } from 'esbuild'
import { hashAbi, hashClientCompositionFormat } from '@hiverelay/blind-protocol/hashes'
import {
  hashBlindClientBrowserArtifactManifestV2,
  hashBlindClientBrowserArtifactV2
} from './browser-artifact-v2.js'
import {
  encodeBlindClientBrowserArtifactManifestV3,
  hashBlindClientBrowserArtifactManifestV3,
  hashBlindClientBrowserArtifactV3,
  hashBlindClientBrowserSourceClosureV3
} from './browser-artifact-v3.js'
import { BLIND_CLIENT_CONTROL_V3_AUTHORITY } from './browser-forward-state-v3.js'

const check = process.argv.includes('--check')
const packageRoot = path.dirname(new URL(import.meta.url).pathname)
const repoRoot = path.resolve(packageRoot, '../..')
const protocolRoot = path.resolve(packageRoot, '../blind-protocol')
const artifactRoot = path.join(packageRoot, 'browser-artifacts')
const require = createRequire(import.meta.url)
const esbuildVersion = require('esbuild/package.json').version
const virtualEntryPath = 'packages/blind-client/browser-artifacts/blind-client-control-v3.entry.mjs'
const externalV2 = './blind-client-control-v2.mjs'
const entrySource = `export * from '${externalV2}'\nexport * from '../browser-forward-state-v3.js'\n`
const buildProfile = Object.freeze({
  bundle: true,
  platform: 'browser',
  format: 'esm',
  target: 'es2020',
  minify: true,
  sourcemap: false,
  charset: 'utf8',
  legalComments: 'none'
})
const buildProfileText = 'bundle=true;platform=browser;format=esm;target=es2020;minify=true;sourcemap=false;charset=utf8;legalComments=none'
const hex = value => b4a.toString(value, 'hex')
const json = value => b4a.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8')
const sha256 = value => createHash('sha256').update(value).digest('hex')

if (esbuildVersion !== '0.28.1') throw new Error(`browser v3 artifact requires esbuild@0.28.1, found ${esbuildVersion}`)

const compatibilityFloor = Object.freeze({
  'browser-artifact.js': '92f4daa973af957f295ce96c11e9684ad9a2313247832934b952819d2710204e',
  'browser-artifact-v2.js': 'f5d91cd97c2623906c9db9aef3151a47ba83b9382854bea69210dab7b3decb28',
  'generate-browser-artifact-v2.mjs': '74d1d15ba41e17add53d666fd1e50f1ab84b665d29b690817bbc9bb1a4ce3a81',
  'browser-artifacts/blind-client-control-v1.mjs': '10425bb00fb8045e63ce2869b5e6bf88af39dc0723963203a6b021e0fd28090a',
  'browser-artifacts/blind-client-control-v1.manifest.cenc': '76a7ea97db644971203c2f94c476614a95b0320980a58504b927f04b152aadf1',
  'browser-artifacts/blind-client-control-v1.chromium-evidence.json': '1382b24b21cae661392a199b0470a22768b85f62215db6fb943ed17b66859c2e',
  'browser-artifacts/blind-client-control-v1.cross-host-evidence.json': '3cfd3c12a7664899a0901eff7b613f85da6fd039b930f74ade5bc820d215dcd2',
  'browser-artifacts/blind-client-control-v2.mjs': 'dfb4276ae74a42d487ad1ef77783309a93ba8985591799ea298c01f8f47442aa',
  'browser-artifacts/blind-client-control-v2.manifest.cenc': 'eb37a674bb92e218088f9f6e9a820866e1eca9f540feeeee825923e311b81731',
  'browser-artifacts/blind-client-control-v2.authority.json': 'eef9468bc97b04fb53692ca7096997ce71e5e56f85a451d1aacdcde12215b43c',
  'browser-artifacts/blind-client-control-v2.source-closure.json': 'b6dac623480fd1e36ffe39bed8fab6e16df4139488e2039abe6bd71e2f03cf6f',
  'package.json': '39ca1e16d47b42c529683a4ae13e437292c67f55ce67e404c9894a2444dbb3a8'
})

const fixedAuthorityPaths = Object.freeze([
  'package.json',
  'package-lock.json',
  'docs/protocol/HIVERELAY-BLIND-WIRE-V3.md',
  'docs/protocol/HIVERELAY-BLIND-CLIENT-COMPOSITION-V3.md',
  'packages/blind-client/package.json',
  'packages/blind-client/browser-artifact-v3.js',
  'packages/blind-client/browser-forward-state-v3.js',
  'packages/blind-client/generate-browser-artifact-v3.mjs',
  'packages/blind-client/browser-artifacts/blind-client-control-v2.mjs',
  'packages/blind-client/browser-artifacts/blind-client-control-v2.manifest.cenc',
  'packages/blind-client/browser-artifacts/blind-client-control-v2.authority.json',
  'packages/blind-client/browser-artifacts/blind-client-control-v2.source-closure.json',
  'packages/blind-protocol/package.json',
  'packages/blind-protocol/generate-wire-v3.mjs',
  'packages/blind-protocol/wire-v3.js',
  'packages/blind-protocol/hiverelay-blind-abi-v3.cenc',
  'packages/blind-protocol/hiverelay-blind-wire-authority-v3.json',
  'packages/blind-protocol/generate-client-composition-v3.mjs',
  'packages/blind-protocol/client-composition-v3.js',
  'packages/blind-protocol/hiverelay-blind-client-composition-format-v3.cenc',
  'packages/blind-protocol/hiverelay-blind-client-composition-authority-v3.json',
  'node_modules/esbuild/package.json'
])

async function assertCompatibilityFloor () {
  for (const [relative, expected] of Object.entries(compatibilityFloor)) {
    const actual = sha256(await fs.readFile(path.join(packageRoot, relative)))
    if (actual !== expected) throw new Error(`frozen browser v1/v2 artifact changed: ${relative} ${actual}`)
  }
}

function normalizeGraphPath (rawPath) {
  const value = rawPath.replaceAll('\\', '/')
  if (!value || value.includes('\0') || value.startsWith('/') || /^[A-Za-z]:\//.test(value) ||
      value === '..' || value.startsWith('../') || value.includes('/../')) {
    throw new Error(`browser v3 graph path is not normalized root-relative authority: ${rawPath}`)
  }
  return value
}

function loaderFor (absolute) {
  const extension = path.extname(absolute)
  if (extension === '.js' || extension === '.mjs' || extension === '.cjs') return 'js'
  if (extension === '.json') return 'json'
  throw new Error(`browser v3 graph input has no frozen loader: ${absolute}`)
}

function normalizedInputGraph (metafile) {
  return Object.entries(metafile.inputs).map(([rawPath, metadata]) => ({
    path: normalizeGraphPath(rawPath),
    bytes: metadata.bytes,
    format: metadata.format || null,
    imports: (metadata.imports || []).map(value => ({
      path: normalizeGraphPath(value.path),
      kind: value.kind,
      original: value.original || null,
      external: value.external === true
    })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
  })).sort((left, right) => left.path.localeCompare(right.path))
}

function assertClosedGraph (metafile) {
  const inputs = Object.keys(metafile.inputs).map(normalizeGraphPath)
  const forbidden = inputs.filter(value =>
    /(^|\/)node_modules\/(?:sodium-native|bare-crypto)(?:\/|$)/.test(value) ||
    value.endsWith('.node') || value.includes('packages/blind-protocol/index.js') ||
    value.includes('packages/blind-protocol/client-internal-schemas.js') ||
    value.includes('packages/blind-protocol/durability-schemas.js'))
  if (forbidden.length > 0) throw new Error(`browser v3 graph contains native/server authority: ${forbidden.join(', ')}`)
  const disabled = inputs.filter(value => value.startsWith('(disabled):'))
  if (disabled.length !== 1 || disabled[0] !== '(disabled):crypto') {
    throw new Error(`browser v3 disabled graph changed: ${disabled.join(', ')}`)
  }
  const externalEdges = []
  for (const [rawInput, metadata] of Object.entries(metafile.inputs)) {
    const input = normalizeGraphPath(rawInput)
    for (const imported of metadata.imports || []) {
      if (imported.external) externalEdges.push({ input, ...imported })
    }
  }
  const runtimeInputs = externalEdges.filter(value => value.path === '<runtime>').map(value => value.input).sort()
  const expectedRuntimeInputs = [
    'node_modules/sodium-javascript/randombytes.js',
    'packages/blind-protocol/wire-v2.js',
    'packages/blind-protocol/wire-v3.js'
  ]
  const externalModuleEdges = externalEdges.filter(value => value.path !== '<runtime>')
  const expectedV2 = externalModuleEdges.find(value =>
    value.input === virtualEntryPath && value.path === externalV2 && value.kind === 'import-statement')
  if (JSON.stringify(runtimeInputs) !== JSON.stringify(expectedRuntimeInputs) ||
      externalModuleEdges.length !== 1 || !expectedV2) {
    throw new Error(`browser v3 input graph has unaudited external edges: ${JSON.stringify(externalEdges)}`)
  }
  const outputImports = Object.values(metafile.outputs).flatMap(value => value.imports || [])
  if (outputImports.length !== 1 || outputImports[0].path !== externalV2 ||
      outputImports[0].kind !== 'import-statement' || outputImports[0].external !== true) {
    throw new Error(`browser v3 output external is not the exact frozen v2 artifact: ${JSON.stringify(outputImports)}`)
  }
}

async function buildOnce (snapshot = null) {
  const plugins = []
  if (snapshot) {
    plugins.push({
      name: 'hiverelay-browser-v3-frozen-source',
      setup (builder) {
        builder.onLoad({ filter: /.*/, namespace: 'file' }, args => {
          const relative = normalizeGraphPath(path.relative(repoRoot, args.path))
          if (!snapshot.graphInputs.has(relative)) throw new Error(`browser v3 build requested unexpected input: ${relative}`)
          const entry = snapshot.entries.get(relative)
          if (!entry) throw new Error(`browser v3 frozen input disappeared: ${relative}`)
          return { contents: entry.content, loader: loaderFor(args.path), resolveDir: path.dirname(args.path) }
        })
      }
    })
  }
  const result = await build({
    absWorkingDir: repoRoot,
    stdin: {
      contents: entrySource,
      resolveDir: artifactRoot,
      sourcefile: 'blind-client-control-v3.entry.mjs',
      loader: 'js'
    },
    ...buildProfile,
    metafile: true,
    write: false,
    outfile: 'packages/blind-client/browser-artifacts/blind-client-control-v3.mjs',
    external: [externalV2],
    logLevel: 'silent',
    plugins
  })
  assertClosedGraph(result.metafile)
  if (snapshot) {
    const actual = normalizedInputGraph(result.metafile)
    if (JSON.stringify(actual) !== JSON.stringify(snapshot.inputGraph)) {
      throw new Error('browser v3 frozen metafile input graph changed')
    }
  }
  if (result.outputFiles.length !== 1) throw new Error('browser v3 build did not produce one exact ESM artifact')
  return Object.freeze({ artifactBytes: b4a.from(result.outputFiles[0].contents), metafile: result.metafile })
}

async function addFile (entries, relative, role, required = true) {
  relative = normalizeGraphPath(relative)
  let content
  try {
    content = b4a.from(await fs.readFile(path.join(repoRoot, ...relative.split('/'))))
  } catch (error) {
    if (!required && error && error.code === 'ENOENT') return false
    throw new Error(`browser v3 source authority is missing ${relative}: ${error.message}`)
  }
  const current = entries.get(relative)
  if (current) {
    if (!b4a.equals(current.content, content)) throw new Error(`browser v3 authority changed while snapshotting ${relative}`)
    current.roles.add(role)
  } else {
    entries.set(relative, { content, roles: new Set([role]) })
  }
  return true
}

async function sourceSnapshot (metafile) {
  const inputGraph = normalizedInputGraph(metafile)
  const graphInputs = new Set(inputGraph.map(value => value.path))
  const entries = new Map()
  for (const input of inputGraph) {
    if (input.path === virtualEntryPath) {
      entries.set(input.path, { content: b4a.from(entrySource), roles: new Set(['METAFILE_INPUT', 'VIRTUAL_ENTRY']) })
      continue
    }
    if (input.path === '(disabled):crypto') {
      entries.set(input.path, {
        content: b4a.from('esbuild@0.28.1-disabled-browser-module:(disabled):crypto'),
        roles: new Set(['METAFILE_INPUT', 'DISABLED_BROWSER_MODULE'])
      })
      continue
    }
    await addFile(entries, input.path, 'METAFILE_INPUT')
    let directory = path.posix.dirname(input.path)
    while (directory !== '.') {
      await addFile(entries, path.posix.join(directory, 'package.json'), 'RESOLUTION_METADATA', false)
      const parent = path.posix.dirname(directory)
      if (parent === directory) break
      directory = parent
    }
  }
  for (const relative of fixedAuthorityPaths) await addFile(entries, relative, 'FIXED_AUTHORITY')
  return Object.freeze({ entries, graphInputs, inputGraph })
}

function sourceClosureBytes (snapshot) {
  const entries = [...snapshot.entries].map(([entryPath, value]) => ({
    path: entryPath,
    bytes: value.content.byteLength,
    sha256: sha256(value.content),
    roles: [...value.roles].sort()
  })).sort((left, right) => left.path.localeCompare(right.path))
  return json({
    schema: 'HiveRelayBlindClientBrowserSourceClosureV3',
    toolchain: `esbuild@${esbuildVersion}`,
    buildProfile,
    buildProfileText,
    externalArtifacts: [externalV2],
    metafileInputs: snapshot.inputGraph,
    entries
  })
}

async function writeOrCheck (file, content) {
  content = b4a.from(content)
  if (check) {
    let current
    try { current = await fs.readFile(file) } catch { throw new Error(`missing generated browser v3 artifact: ${path.relative(repoRoot, file)}`) }
    if (!b4a.equals(current, content)) throw new Error(`stale generated browser v3 artifact: ${path.relative(repoRoot, file)}`)
    return
  }
  await fs.mkdir(path.dirname(file), { recursive: true })
  await fs.writeFile(file, content)
}

await assertCompatibilityFloor()

const [v2ArtifactBytes, v2ManifestBytes, wireV3AbiBytes, compositionV3FormatBytes] = await Promise.all([
  fs.readFile(path.join(artifactRoot, 'blind-client-control-v2.mjs')),
  fs.readFile(path.join(artifactRoot, 'blind-client-control-v2.manifest.cenc')),
  fs.readFile(path.join(protocolRoot, 'hiverelay-blind-abi-v3.cenc')),
  fs.readFile(path.join(protocolRoot, 'hiverelay-blind-client-composition-format-v3.cenc'))
])
if (hex(hashBlindClientBrowserArtifactV2(v2ArtifactBytes)) !== '217ac636ded5bc6358a7a33b31aab7361b65feb117cf38dce68f6041bf773122') {
  throw new Error('frozen browser v2 artifact hash mismatch')
}
const wireV3AbiHash = hashAbi(wireV3AbiBytes)
const compositionV3FormatHash = hashClientCompositionFormat(compositionV3FormatBytes)
if (hex(wireV3AbiHash) !== BLIND_CLIENT_CONTROL_V3_AUTHORITY.wireV3AbiHash ||
    hex(compositionV3FormatHash) !== BLIND_CLIENT_CONTROL_V3_AUTHORITY.clientCompositionV3FormatHash) {
  throw new Error('browser v3 source authority does not match the frozen WIRE/composition tuple')
}

const discovery = await buildOnce()
const snapshot = await sourceSnapshot(discovery.metafile)
const first = await buildOnce(snapshot)
const second = await buildOnce(snapshot)
if (!b4a.equals(discovery.artifactBytes, first.artifactBytes) || !b4a.equals(first.artifactBytes, second.artifactBytes)) {
  throw new Error('browser v3 esbuild output is not deterministic over its frozen source closure')
}

const artifactBytes = first.artifactBytes
const sourceClosure = sourceClosureBytes(snapshot)
const manifestValue = {
  version: 3,
  baseBrowserV2ArtifactHash: hashBlindClientBrowserArtifactV2(v2ArtifactBytes),
  baseBrowserV2ManifestHash: hashBlindClientBrowserArtifactManifestV2(v2ManifestBytes),
  wireV3AbiHash,
  clientCompositionV3FormatHash: compositionV3FormatHash,
  artifactHash: hashBlindClientBrowserArtifactV3(artifactBytes),
  sourceClosureHash: hashBlindClientBrowserSourceClosureV3(sourceClosure),
  exactRequestBytes: 65_536,
  exactResultBytes: 65_536,
  forwardReadinessOperationBits: 0
}
const manifestBytes = encodeBlindClientBrowserArtifactManifestV3(manifestValue)
const authority = {
  profile: 'blind-client-browser-artifact-v3',
  authorityVersion: 3,
  artifactPath: 'browser-artifacts/blind-client-control-v3.mjs',
  manifestPath: 'browser-artifacts/blind-client-control-v3.manifest.cenc',
  baseBrowserV2ArtifactHash: hex(manifestValue.baseBrowserV2ArtifactHash),
  baseBrowserV2ManifestHash: hex(manifestValue.baseBrowserV2ManifestHash),
  wireV3AbiHash: hex(wireV3AbiHash),
  clientCompositionV3FormatHash: hex(compositionV3FormatHash),
  artifactHash: hex(manifestValue.artifactHash),
  manifestHash: hex(hashBlindClientBrowserArtifactManifestV3(manifestBytes)),
  sourceClosureHash: hex(manifestValue.sourceClosureHash),
  sourceClosureSchema: 'HiveRelayBlindClientBrowserSourceClosureV3',
  sourceClosureEntries: snapshot.entries.size,
  metafileInputCount: snapshot.inputGraph.length,
  toolchain: `esbuild@${esbuildVersion}`,
  buildProfile: buildProfileText,
  externalArtifacts: [externalV2],
  exactRequestBytes: 65_536,
  exactResultBytes: 65_536,
  credentialMode: 'omit',
  cacheMode: 'no-store',
  redirectMode: 'error',
  referrerPolicy: 'no-referrer',
  continuityBackend: 'INDEXEDDB_PERSISTENT',
  forwardDescriptorOperationBits: 0,
  forwardAdvertisedOperationBits: 0,
  forwardReadinessOperationBits: 0,
  runtimeReady: false,
  realBrowserEvidenceAccepted: false,
  authorizesRelease: false,
  compatibilityFloor
}

const outputs = [
  [path.join(artifactRoot, 'blind-client-control-v3.mjs'), artifactBytes],
  [path.join(artifactRoot, 'blind-client-control-v3.manifest.cenc'), manifestBytes],
  [path.join(artifactRoot, 'blind-client-control-v3.authority.json'), json(authority)],
  [path.join(artifactRoot, 'blind-client-control-v3.source-closure.json'), sourceClosure]
]
for (const [file, content] of outputs) await writeOrCheck(file, content)

console.log(check ? 'browser v3 artifact verified' : 'browser v3 artifact generated')