OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / scripts / verify-release-contents.js

134 lines · javascript ref:
/**
 * Verify release metadata from a fresh peer, including paths that must be absent.
 *
 * Usage:
 *   node scripts/verify-release-contents.js --expect 18640 \
 *     --missing /.landing-seed.mjs --missing /docs --missing /scripts
 */

import Hyperswarm from 'hyperswarm'
import Corestore from 'corestore'
import Hyperdrive from 'hyperdrive'
import b4a from 'b4a'
import { mkdtempSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'

const DEFAULT_KEY = '8b21b577993ce0fc45036ca9011861e25f0a49fd4d68bcc655fb2690a03cb062'
const PEER_TIMEOUT_MS = 30_000
const LENGTH_TIMEOUT_MS = 20_000
const SCAN_TIMEOUT_MS = 90_000

function normalizePath (path) {
  if (!path) return null
  const prefixed = path.startsWith('/') ? path : '/' + path
  return prefixed.length > 1 ? prefixed.replace(/\/+$/, '') : prefixed
}

function parseArgs (argv) {
  const args = { key: DEFAULT_KEY, expect: null, missing: [] }
  for (let i = 0; i < argv.length; i++) {
    if (argv[i] === '--key') args.key = argv[++i]
    else if (argv[i] === '--expect') args.expect = parseInt(argv[++i], 10)
    else if (argv[i] === '--missing') args.missing.push(normalizePath(argv[++i]))
  }
  args.missing = args.missing.filter(Boolean)
  return args
}

function isUnderPath (entryKey, path) {
  return entryKey === path || entryKey.startsWith(path + '/')
}

async function main () {
  const args = parseArgs(process.argv.slice(2))
  if (!/^[0-9a-f]{64}$/i.test(args.key)) {
    console.error('error: key must be 64-char hex')
    process.exit(2)
  }

  const storage = mkdtempSync(join(tmpdir(), 'verify-release-contents-'))
  const store = new Corestore(storage)
  const swarm = new Hyperswarm()
  const drive = new Hyperdrive(store, b4a.from(args.key, 'hex'))

  const cleanup = async (code) => {
    try { await swarm.destroy() } catch {}
    try { await store.close() } catch {}
    try { rmSync(storage, { recursive: true, force: true }) } catch {}
    process.exit(code)
  }

  swarm.on('connection', (conn) => store.replicate(conn))

  try {
    await drive.ready()
    console.log('🍐 Verifying release contents from the network')
    console.log('   key:', args.key)
    if (args.expect != null) console.log('   expect length ≥', args.expect)
    if (args.missing.length) console.log('   missing:', args.missing.join(', '))
    console.log()

    const discovery = swarm.join(drive.discoveryKey, { server: false, client: true })
    process.stdout.write('   → waiting for peers...')
    await Promise.race([
      new Promise((resolve) => swarm.once('connection', () => {
        process.stdout.write(' ✓\n')
        resolve()
      })),
      new Promise((_, reject) => setTimeout(() =>
        reject(new Error(`no peers within ${PEER_TIMEOUT_MS / 1000}s`)),
      PEER_TIMEOUT_MS))
    ])
    await discovery.flushed().catch(() => {})

    process.stdout.write('   → reading drive length...')
    await Promise.race([
      drive.core.update({ wait: true }),
      new Promise((_, reject) => setTimeout(() =>
        reject(new Error(`couldn't read drive length within ${LENGTH_TIMEOUT_MS / 1000}s`)),
      LENGTH_TIMEOUT_MS))
    ])
    const length = drive.core.length
    process.stdout.write(' length=' + length + '\n')

    if (args.expect != null && length < args.expect) {
      throw new Error(`drive length ${length} < expected ${args.expect}`)
    }

    const foundMissing = new Set()
    let count = 0
    process.stdout.write('   → scanning metadata...')
    await Promise.race([
      (async () => {
        for await (const entry of drive.list('/', { recursive: true })) {
          count += 1
          for (const path of args.missing) {
            if (isUnderPath(entry.key, path)) foundMissing.add(path)
          }
        }
      })(),
      new Promise((_, reject) => setTimeout(() =>
        reject(new Error(`metadata scan timed out after ${SCAN_TIMEOUT_MS / 1000}s`)),
      SCAN_TIMEOUT_MS))
    ])
    process.stdout.write(' ' + count + ' entries\n')

    if (foundMissing.size) {
      throw new Error('release still contains forbidden path(s): ' + Array.from(foundMissing).join(', '))
    }

    console.log()
    console.log('✅ Release contents verified')
    console.log('   length:  ' + length)
    console.log('   entries: ' + count)
    await cleanup(0)
  } catch (err) {
    console.error()
    console.error('✗ FAIL —', err.message)
    await cleanup(1)
  }
}

main()