OverviewFilesCommitsIssuesDocsReleasesPeers

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

174 lines · javascript ref:
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { createHash } from 'node:crypto'
import b4a from 'b4a'
import { hashAbi, hashClientCompositionFormat } from '@hiverelay/blind-protocol/hashes'
import {
  hashBlindClientBrowserArtifact,
  hashBlindClientBrowserArtifactManifest
} from './browser-artifact.js'
import {
  encodeBlindClientBrowserArtifactManifestV2,
  hashBlindClientBrowserArtifactManifestV2,
  hashBlindClientBrowserArtifactV2,
  hashBlindClientBrowserSourceClosureV2
} from './browser-artifact-v2.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 hex = value => b4a.toString(value, 'hex')
const json = value => b4a.from(`${JSON.stringify(value, null, 2)}\n`)

const compatibilityFloor = Object.freeze({
  'blind-client-control-v1.mjs': '10425bb00fb8045e63ce2869b5e6bf88af39dc0723963203a6b021e0fd28090a',
  'blind-client-control-v1.manifest.cenc': '76a7ea97db644971203c2f94c476614a95b0320980a58504b927f04b152aadf1',
  'blind-client-control-v1.chromium-evidence.json': '1382b24b21cae661392a199b0470a22768b85f62215db6fb943ed17b66859c2e',
  'blind-client-control-v1.cross-host-evidence.json': '3cfd3c12a7664899a0901eff7b613f85da6fd039b930f74ade5bc820d215dcd2'
})

function sha256 (bytes) {
  return createHash('sha256').update(bytes).digest('hex')
}

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

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

function artifactSource (wireV2AbiHash, compositionV2FormatHash) {
  const authority = {
    profile: 'blind-client-control-v2',
    wireV2AbiHash,
    clientCompositionV2FormatHash: compositionV2FormatHash,
    releaseProfileId: 2,
    routeKind: 7,
    exactRequestBytes: 65_536,
    exactResultBytes: 65_536,
    forwardDescriptorOperationBits: 0,
    forwardAdvertisedOperationBits: 0,
    forwardReadinessOperationBits: 0,
    runtimeReady: false
  }
  return '/* eslint-disable */\nexport * from \'./blind-client-control-v1.mjs\'\n' +
    `export const BLIND_CLIENT_CONTROL_V2_AUTHORITY = Object.freeze(${JSON.stringify(authority, null, 2)})\n` +
    'function bytes (value, length, field) {\n' +
    '  if (!(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value)) throw new TypeError(field + \' must be ArrayBuffer-backed bytes\')\n' +
    '  const output = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength)\n' +
    '  if (output.byteLength !== length) throw new RangeError(field + \' must be exactly \' + length + \' bytes\')\n' +
    '  return new Uint8Array(output)\n' +
    '}\n' +
    'export function assertForwardHttpsBrowserCellV2 (value) {\n' +
    '  if (!value || typeof value !== \'object\') throw new TypeError(\'browser cell must be an object\')\n' +
    '  for (const field of [\'url\', \'host\', \'hostname\', \'ip\', \'ipAddress\', \'dialAddress\', \'credentials\']) {\n' +
    '    if (field in value) throw new TypeError(\'browser cell \' + field + \' is forbidden\')\n' +
    '  }\n' +
    '  if (value.releaseProfileId !== 2 || value.routeKind !== 7 || value.credentialsMode !== \'omit\' ||\n' +
    '      value.cacheMode !== \'no-store\' || value.redirectMode !== \'error\' || value.referrerPolicy !== \'no-referrer\') {\n' +
    '    throw new TypeError(\'browser cell privacy policy is invalid\')\n' +
    '  }\n' +
    '  return Object.freeze({ body: bytes(value.body, 65536, \'body\'), releaseProfileId: 2, routeKind: 7 })\n' +
    '}\n'
}

function closureBytes (entries) {
  return json(entries.map(([entryPath, content]) => ({
    path: entryPath,
    bytes: content.byteLength,
    sha256: sha256(content)
  })).sort((left, right) => left.path.localeCompare(right.path)))
}

await assertCompatibilityFloor()

const v1ArtifactBytes = await fs.readFile(path.join(artifactRoot, 'blind-client-control-v1.mjs'))
const v1ManifestBytes = await fs.readFile(path.join(artifactRoot, 'blind-client-control-v1.manifest.cenc'))
const wireV2AbiBytes = await fs.readFile(path.join(protocolRoot, 'hiverelay-blind-abi-v2.cenc'))
const compositionV2FormatBytes = await fs.readFile(path.join(protocolRoot, 'hiverelay-blind-client-composition-format-v2.cenc'))
const wireV2AbiHash = hashAbi(wireV2AbiBytes)
const compositionV2FormatHash = hashClientCompositionFormat(compositionV2FormatBytes)
const artifactBytes = b4a.from(artifactSource(hex(wireV2AbiHash), hex(compositionV2FormatHash)))

const sourcePaths = [
  'packages/blind-client/browser-artifact-v2.js',
  'packages/blind-client/generate-browser-artifact-v2.mjs',
  'packages/blind-client/browser-artifacts/blind-client-control-v1.mjs',
  'packages/blind-client/browser-artifacts/blind-client-control-v1.manifest.cenc',
  'packages/blind-protocol/hiverelay-blind-abi-v2.cenc',
  'packages/blind-protocol/hiverelay-blind-wire-authority-v2.json',
  'packages/blind-protocol/hiverelay-blind-client-composition-format-v2.cenc',
  'packages/blind-protocol/hiverelay-blind-client-composition-authority-v2.json'
]
const sourceEntries = []
for (const relative of sourcePaths) sourceEntries.push([relative, await fs.readFile(path.join(repoRoot, relative))])
const sourceClosure = closureBytes(sourceEntries)
const manifestValue = {
  version: 2,
  baseBrowserV1ArtifactHash: hashBlindClientBrowserArtifact(v1ArtifactBytes),
  baseBrowserV1ManifestHash: hashBlindClientBrowserArtifactManifest(v1ManifestBytes),
  wireV2AbiHash,
  clientCompositionV2FormatHash: compositionV2FormatHash,
  artifactHash: hashBlindClientBrowserArtifactV2(artifactBytes),
  sourceClosureHash: hashBlindClientBrowserSourceClosureV2(sourceClosure),
  exactRequestBytes: 65_536,
  exactResultBytes: 65_536,
  forwardReadinessOperationBits: 0
}
const manifestBytes = encodeBlindClientBrowserArtifactManifestV2(manifestValue)
const authority = {
  profile: 'blind-client-browser-artifact-v2',
  authorityVersion: 2,
  artifactPath: 'browser-artifacts/blind-client-control-v2.mjs',
  manifestPath: 'browser-artifacts/blind-client-control-v2.manifest.cenc',
  baseBrowserV1ArtifactHash: hex(manifestValue.baseBrowserV1ArtifactHash),
  baseBrowserV1ManifestHash: hex(manifestValue.baseBrowserV1ManifestHash),
  wireV2AbiHash: hex(wireV2AbiHash),
  clientCompositionV2FormatHash: hex(compositionV2FormatHash),
  artifactHash: hex(manifestValue.artifactHash),
  manifestHash: hex(hashBlindClientBrowserArtifactManifestV2(manifestBytes)),
  sourceClosureHash: hex(manifestValue.sourceClosureHash),
  exactRequestBytes: 65_536,
  exactResultBytes: 65_536,
  credentialMode: 'omit',
  cacheMode: 'no-store',
  redirectMode: 'error',
  referrerPolicy: 'no-referrer',
  forwardDescriptorOperationBits: 0,
  forwardAdvertisedOperationBits: 0,
  forwardReadinessOperationBits: 0,
  runtimeReady: false,
  realBrowserEvidenceAccepted: false,
  compatibilityFloor
}

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

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