OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / scripts / verify-live-catalog.js

191 lines · javascript ref:
/**
 * Fresh-peer verification for a live Hyperbee app catalogue.
 *
 * This complements `npm run validate` in the publisher directory: validation
 * proves the source JSON is good; this proves the published Hyperbee key is
 * reachable and contains the expected rows.
 *
 * Usage:
 *   node scripts/verify-live-catalog.js \
 *     --key f5fb7500bccd60a976d2b1d24246108f4444a210b9ca591533114dffc089934d \
 *     --expect-app peercord --expect-app peerit --expect-app hiveworm
 */

import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import Hyperbee from 'hyperbee'
import b4a from 'b4a'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { readCatalogBee } from './lib/catalog-bee.js'

const DEFAULT_KEY = 'f5fb7500bccd60a976d2b1d24246108f4444a210b9ca591533114dffc089934d'
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
const catalogSource = JSON.parse(readFileSync(new URL('../catalog-source/pearbrowser-network.catalog.json', import.meta.url), 'utf8'))
const browserSource = catalogSource.apps.find((app) => app.id === 'pearbrowser-desktop')
if (!browserSource) throw new Error('catalogue source is missing pearbrowser-desktop')
const PEER_TIMEOUT_MS = 30_000
const UPDATE_TIMEOUT_MS = 20_000
const READ_TIMEOUT_MS = 20_000

function parseArgs (argv) {
  const args = {
    key: DEFAULT_KEY,
    expectApps: [],
    expectCount: 14,
    expectName: 'PearBrowser Network',
    expectBrowserVersion: pkg.version,
    expectBrowserLink: pkg.upgrade,
    expectBrowserHomepage: browserSource.homepage
  }
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i]
    if (arg === '--key') args.key = argv[++i]
    else if (arg === '--expect-app') args.expectApps.push(argv[++i])
    else if (arg === '--expect-count') args.expectCount = Number(argv[++i])
    else if (arg === '--expect-name') args.expectName = argv[++i]
    else if (arg === '--expect-browser-version') args.expectBrowserVersion = argv[++i]
    else if (arg === '--expect-browser-link') args.expectBrowserLink = argv[++i]
    else if (arg === '--expect-browser-homepage') args.expectBrowserHomepage = argv[++i]
  }
  return args
}

function withTimeout (promise, ms, label) {
  return Promise.race([
    promise,
    new Promise((_resolve, reject) => setTimeout(() => reject(new Error(label + ' timed out')), ms))
  ])
}

function fail (message) {
  throw new Error(message)
}

const args = parseArgs(process.argv.slice(2))
if (!/^[0-9a-f]{64}$/i.test(args.key || '')) fail('--key must be a 64-char hex key')
if (!Number.isInteger(args.expectCount) || args.expectCount < 0) fail('--expect-count must be a non-negative integer')

const storage = mkdtempSync(join(tmpdir(), 'verify-live-catalog-'))
const store = new Corestore(storage)
const swarm = new Hyperswarm()
let peerCount = 0

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

async function main () {
  console.log('🍐 Verifying live Hyperbee catalogue')
  console.log('   key:', args.key)
  console.log()

  await store.ready()
  const core = store.get({ key: b4a.from(args.key, 'hex') })
  core.on('error', (err) => console.warn('[catalog-verify] core error:', err && err.message))
  await core.ready()

  const discovery = swarm.join(core.discoveryKey, { server: false, client: true })
  swarm.on('connection', (conn) => {
    peerCount += 1
    store.replicate(conn)
  })

  process.stdout.write('   → waiting for peers...')
  await withTimeout(new Promise((resolve) => swarm.once('connection', resolve)), PEER_TIMEOUT_MS, 'peer discovery')
  process.stdout.write(' ✓\n')
  await discovery.flushed().catch(() => {})

  process.stdout.write('   → updating catalogue core...')
  await withTimeout(core.update({ wait: true }), UPDATE_TIMEOUT_MS, 'catalogue update')
  process.stdout.write(' length=' + core.length + '\n')

  const bee = new Hyperbee(core, { keyEncoding: 'utf-8', valueEncoding: 'json' })
  await bee.ready()

  const signedMeta = await withTimeout(bee.get('\x00meta'), READ_TIMEOUT_MS, 'signed meta read')
  if (!signedMeta || !signedMeta.value || typeof signedMeta.value.signature !== 'string') {
    fail('missing signed \\x00meta signature')
  }

  const data = await withTimeout(readCatalogBee(bee, args.key), READ_TIMEOUT_MS, 'catalogue app scan')
  if (args.expectName && data.name !== args.expectName) fail(`expected name "${args.expectName}", got "${data.name}"`)
  if (data.apps.length !== args.expectCount) fail(`expected ${args.expectCount} apps, got ${data.apps.length}`)

  const byId = new Map(data.apps.map((app) => [app.id, app]))
  for (const id of args.expectApps) {
    if (!byId.has(id)) fail(`missing expected app: ${id}`)
  }

  const pearbrowser = byId.get('pearbrowser-desktop')
  if (!pearbrowser) fail('missing expected app: pearbrowser-desktop')
  if (pearbrowser.version !== args.expectBrowserVersion) {
    fail(`PearBrowser version mismatch: expected ${args.expectBrowserVersion}, got ${pearbrowser.version || '(missing)'}`)
  }
  if (browserSource.nativeDelivery?.status === 'available') {
    if (pearbrowser.nativeDelivery?.installLink !== args.expectBrowserLink) {
      fail(`PearBrowser native install link mismatch: expected ${args.expectBrowserLink}, got ${pearbrowser.nativeDelivery?.installLink || '(missing)'}`)
    }
    if (pearbrowser.nativeDelivery?.status !== 'available' || pearbrowser.nativeDelivery?.kind !== 'pear-v3') {
      fail('PearBrowser native delivery must be an available Pear v3 release')
    }
    if (pearbrowser.nativeDelivery?.productName !== pkg.productName) {
      fail(`PearBrowser product name mismatch: expected ${pkg.productName}, got ${pearbrowser.nativeDelivery?.productName || '(missing)'}`)
    }
    if (!Array.isArray(pearbrowser.nativeDelivery?.targets) || pearbrowser.nativeDelivery.targets.length === 0) {
      fail('PearBrowser native delivery targets are missing')
    }
  } else {
    const expectedMigrationId = args.expectBrowserLink.replace(/^pear:\/\//, '')
    if (pearbrowser.legacyMigrationId !== expectedMigrationId || pearbrowser.nativeDelivery?.status !== 'migration-required') {
      fail('PearBrowser migration identity mismatch')
    }
    if (pearbrowser.nativeDelivery?.installLink) fail('PearBrowser migration record must not expose an install link')
  }
  if (pearbrowser.homepage !== args.expectBrowserHomepage) {
    fail(`PearBrowser homepage mismatch: expected ${args.expectBrowserHomepage}, got ${pearbrowser.homepage || '(missing)'}`)
  }

  const peercord = byId.get('peercord')
  if (peercord) {
    if (peercord.legacyMigrationId !== 'wmir47w7mai3b1skj66mx7fzso6k6o91kipaney7gtt69npimouy') fail('Peercord migration id mismatch')
    if (peercord.link || peercord.driveKey) fail('Peercord must not expose a remote executable or browsable content target')
    if (peercord.nativeDelivery?.status !== 'migration-required') fail('Peercord must require a verified native v3 package')
    if (peercord.sourceUrl !== 'https://git.churchofmalware.org/mastercodeon/Peercord') fail('Peercord sourceUrl mismatch')
    if (peercord.license !== 'GPL-3.0') fail('Peercord license mismatch')
  }

  const hiveworm = byId.get('hiveworm')
  if (hiveworm) {
    if (hiveworm.driveKey !== 'e3f910d11e70044afe361b1cecfb5cfb3c4f61f600cc81c2365ba0e6f58c8d4d') fail('HiveWorm driveKey mismatch')
    if (hiveworm.link !== 'hyper://e3f910d11e70044afe361b1cecfb5cfb3c4f61f600cc81c2365ba0e6f58c8d4d/') fail('HiveWorm link mismatch')
  }

  const peerit = byId.get('peerit')
  if (peerit) {
    if (peerit.driveKey !== 'ec6e2d6d9d22b9d6b40e11a9ca3042be3197e4bdca9e9a7f079be6ee830761b4') fail('peerit driveKey mismatch')
    if (peerit.link !== 'hyper://ec6e2d6d9d22b9d6b40e11a9ca3042be3197e4bdca9e9a7f079be6ee830761b4/') fail('peerit link mismatch')
    if (peerit.type !== 'hypersite') fail(`peerit type mismatch: expected hypersite, got ${peerit.type || '(missing)'}`)
  }

  console.log('   → signed meta signature:', signedMeta.value.signature.slice(0, 16) + '…')
  console.log('   → catalogue:', data.name, '· apps:', data.apps.length)
  console.log('   → PearBrowser:', pearbrowser.version, '·', pearbrowser.nativeDelivery?.installLink || `migration:${pearbrowser.legacyMigrationId}`, '·', pearbrowser.homepage)
  if (peercord) console.log('   → Peercord:', peercord.type, '·', peercord.sourceUrl, '·', peercord.license)
  console.log('   → expected apps:', args.expectApps.length ? args.expectApps.join(', ') : '(none)')
  console.log()
  console.log('✅ Live catalogue is reachable and matches expected release rows')
  console.log('   peers:', peerCount)
  await cleanup(0)
}

main().catch(async (err) => {
  console.error()
  console.error('✗ FAIL —', err.message)
  await cleanup(1)
})