OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / resolve-public-hive-gateway-node.mjs

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

import {
  PUBLIC_HIVE_GATEWAY_RELEASE_SCHEMA,
  normalizePublicHiveGatewayReleaseManifest
} from './lib/public-hive-gateway-release-manifest.mjs'

const MAX_MANIFEST_BYTES = 2 * 1024 * 1024

try {
  const args = parseArgs(process.argv.slice(2))
  const bytes = await readStdinBounded(MAX_MANIFEST_BYTES)
  const parsed = parseJson(bytes)

  if (parsed?.enabled === false) {
    requireDisabledControl(parsed)
    process.stdout.write('ordinary\tdisabled\n')
    process.exit(0)
  }

  const manifest = normalizePublicHiveGatewayReleaseManifest(parsed, {
    releaseTarget: args.releaseTarget,
    requirePublicT1: args.requirePublicT1 === true
  })
  const entries = manifest.cohort.filter(entry => entry.relay === args.relay)
  if (entries.length === 0) {
    process.stdout.write('ordinary\tnoncohort\n')
    process.exit(0)
  }
  if (entries.length !== 1) throw new Error(`manifest does not uniquely identify relay ${args.relay}`)

  const entry = entries[0]
  if (entry.channel !== args.channel) {
    throw new Error(`relay ${args.relay} is signed for channel ${entry.channel}, not ${args.channel}`)
  }

  // Every value was normalized by the closed manifest schema and excludes
  // control characters. A fixed tab-delimited record lets the shell consume
  // it as data without eval/source or dynamically generated option names.
  process.stdout.write([
    'cohort',
    manifest.admissionProfile,
    entry.origin,
    entry.connectAddress,
    entry.appKey,
    entry.path,
    entry.contentSha256,
    entry.driveVersion,
    entry.peerFingerprint256,
    entry.nginxConfigSha256,
    entry.deploymentProfile || 'legacy',
    entry.operatorContractSha256 || '-'
  ].join('\t') + '\n')
} catch (err) {
  console.error(`public gateway node contract: ${safeError(err)}`)
  process.exitCode = 1
}

function parseArgs (argv) {
  const out = {}
  const valueArgs = new Set(['release-target', 'relay', 'channel'])
  const booleanArgs = new Set(['require-public-t1'])
  const seen = new Set()
  for (let i = 0; i < argv.length; i++) {
    const raw = argv[i]
    if (!raw.startsWith('--')) throw new Error(`unexpected argument ${JSON.stringify(raw)}`)
    const name = raw.slice(2)
    if (!valueArgs.has(name) && !booleanArgs.has(name)) throw new Error(`unknown option --${name}`)
    if (seen.has(name)) throw new Error(`duplicate option --${name}`)
    seen.add(name)
    if (booleanArgs.has(name)) {
      out[camel(name)] = true
      continue
    }
    const value = argv[++i]
    if (!value || value.startsWith('--')) throw new Error(`missing value for --${name}`)
    out[camel(name)] = value
  }
  if (!/^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(out.releaseTarget || '')) {
    throw new Error('--release-target must be a release tag like v1.2.3')
  }
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(out.relay || '')) {
    throw new Error('--relay must be a canonical fleet relay name')
  }
  if (!/^[A-Za-z0-9._-]{1,32}$/.test(out.channel || '')) {
    throw new Error('--channel must be a canonical fleet channel name')
  }
  return out
}

async function readStdinBounded (maxBytes) {
  const chunks = []
  let length = 0
  for await (const chunk of process.stdin) {
    length += chunk.length
    if (length > maxBytes) throw new Error('manifest exceeds the input limit')
    chunks.push(chunk)
  }
  if (length === 0) throw new Error('manifest input is empty')
  return Buffer.concat(chunks, length)
}

function parseJson (bytes) {
  try {
    const value = JSON.parse(bytes.toString('utf8'))
    if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('not an object')
    return value
  } catch {
    throw new Error('manifest input must be valid JSON object')
  }
}

function requireDisabledControl (value) {
  const keys = Object.keys(value).sort()
  if (keys.length !== 2 || keys[0] !== 'enabled' || keys[1] !== 'schema' ||
      value.schema !== PUBLIC_HIVE_GATEWAY_RELEASE_SCHEMA || value.enabled !== false) {
    throw new Error('disabled manifest must contain exactly the canonical schema and enabled=false')
  }
}

function camel (value) {
  return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
}

function safeError (err) {
  const value = err instanceof Error ? err.message : String(err)
  return value.replace(/[\r\n\0]/g, ' ').slice(0, 1000)
}