OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / lib / public-hive-gateway-preflight.mjs

1124 lines · javascript ref:
import http from 'http'
import https from 'https'
import { createHash } from 'crypto'
import { isIP } from 'net'
import {
  assertHiveAppGatewayIsolation
} from '../../packages/core/core/relay-node/gateway-server.js'
import {
  encodeHiveAppKey,
  normalizeHiveAppHostSuffix,
  resolveHiveAppHost
} from '../../packages/core/gateway/hive-host.js'
import {
  PUBLIC_HIVE_GATEWAY_ADMISSION_CAPABILITY,
  normalizeHiveAppPublicKeys,
  normalizeHiveAppPublicVersions
} from '../../packages/core/gateway/public-app-admission.js'
import {
  PUBLIC_HIVE_GATEWAY_PROBE_CHECKS,
  PUBLIC_HIVE_GATEWAY_PROBE_SCHEMA
} from './public-hive-gateway-evidence.mjs'
import {
  PUBLIC_HIVE_GATEWAY_FINITE_CONFIG_FIELDS,
  PUBLIC_HIVE_GATEWAY_FINITE_POLICY
} from './public-hive-gateway-policy.mjs'

export {
  inspectPublicHiveGatewayQuarantineNginx
} from './public-hive-gateway-quarantine-authority.mjs'

const MAX_PROXY_TEMPLATE_BYTES = 256 * 1024
const MAX_NGINX_INSPECTION_BYTES = 8 * 1024 * 1024
const MAX_PROBE_BODY_BYTES = 64 * 1024 * 1024
const NGINX_UPSTREAM_DIRECTIVES = new Set([
  'proxy_pass',
  'fastcgi_pass',
  'uwsgi_pass',
  'scgi_pass',
  'grpc_pass'
])
const NGINX_DEFAULT_DIRECTIVES = new Set([
  'listen',
  'server_name',
  'ssl_certificate',
  'ssl_certificate_key',
  'ssl_protocols',
  'ssl_session_tickets',
  'gzip',
  'gunzip',
  'access_log',
  'error_log',
  'return'
])
const NGINX_APP_SERVER_DIRECTIVES = new Set([
  'listen',
  'server_name',
  'ssl_certificate',
  'ssl_certificate_key',
  'ssl_protocols',
  'ssl_session_tickets',
  'access_log',
  'error_log',
  'server_tokens',
  'gzip',
  'gunzip',
  'proxy_cache',
  'proxy_hide_header',
  'client_max_body_size',
  'limit_req',
  'limit_conn',
  'limit_req_status',
  'limit_conn_status',
  'add_header'
])
const NGINX_APP_LOCATION_DIRECTIVES = new Set([
  'proxy_pass',
  'proxy_http_version',
  'proxy_set_header',
  'proxy_pass_request_body',
  'proxy_connect_timeout',
  'proxy_send_timeout',
  'proxy_read_timeout',
  'proxy_next_upstream',
  'proxy_buffering',
  'proxy_request_buffering'
])
const NGINX_REVIEWED_MAIN_DIRECTIVES = new Set([
  'daemon',
  'env',
  'error_log',
  'lock_file',
  'master_process',
  'pcre_jit',
  'pid',
  'thread_pool',
  'timer_resolution',
  'user',
  'worker_cpu_affinity',
  'worker_priority',
  'worker_processes',
  'worker_rlimit_core',
  'worker_rlimit_nofile',
  'working_directory'
])
const NGINX_REVIEWED_HTTP_PARENT_DIRECTIVES = new Set([
  'access_log',
  'client_body_timeout',
  'client_header_timeout',
  'default_type',
  'error_log',
  'include',
  'keepalive_requests',
  'keepalive_timeout',
  'large_client_header_buffers',
  'limit_req_zone',
  'limit_conn_zone',
  'lingering_close',
  'lingering_time',
  'lingering_timeout',
  'log_format',
  'open_file_cache',
  'open_file_cache_errors',
  'open_file_cache_min_uses',
  'open_file_cache_valid',
  'read_ahead',
  'reset_timedout_connection',
  'send_timeout',
  'sendfile',
  'server_names_hash_bucket_size',
  'server_names_hash_max_size',
  'ssl_prefer_server_ciphers',
  'ssl_protocols',
  'tcp_nodelay',
  'tcp_nopush',
  'types_hash_bucket_size',
  'types_hash_max_size'
])
const NGINX_REVIEWED_EVENTS_DIRECTIVES = new Set([
  'accept_mutex',
  'accept_mutex_delay',
  'debug_connection',
  'multi_accept',
  'use',
  'worker_connections'
])

export function normalizePublicHiveGatewayConnectAddress (value) {
  if (typeof value !== 'string' || value.length < 2 || value.length > 64 || value !== value.trim() || isIP(value) === 0) return null
  return value.toLowerCase()
}

export function inspectPublicHiveGatewayConfig (config, opts = {}) {
  const mode = opts.mode || 'canary'
  const errors = []
  const warnings = []
  if (mode !== 'canary' && mode !== 'fleet') errors.push('mode must be canary or fleet')
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
    return { ok: false, errors: ['config must be a JSON object'], warnings, normalized: null }
  }

  try {
    assertHiveAppGatewayIsolation({
      hiveAppHostSuffix: config.hiveAppHostSuffix,
      hiveAppPublicKeys: config.hiveAppPublicKeys,
      hiveAppPublicVersions: config.hiveAppPublicVersions,
      gatewayPort: config.gatewayPort,
      gatewayHost: config.gatewayHost,
      gatewayCompatibilityHosts: config.gatewayCompatibilityHosts,
      gatewayTrustedProxyAddresses: config.gatewayTrustedProxyAddresses,
      gatewayTrustProxy: config.gatewayTrustProxy,
      gatewayRequireForwardedSNI: config.gatewayRequireForwardedSNI,
      gatewayMaxResponseBytes: config.gatewayMaxResponseBytes,
      gatewayMaxTransformBytes: config.gatewayMaxTransformBytes,
      gatewayEgressBytesPerWindow: config.gatewayEgressBytesPerWindow,
      gatewayEgressWindowMs: config.gatewayEgressWindowMs,
      gatewayMaxResponseLifetimeMs: config.gatewayMaxResponseLifetimeMs,
      apiPort: config.apiPort,
      apiHost: config.apiHost,
      enableAPI: config.enableAPI,
      enableSeeding: config.enableSeeding,
      enableRelay: config.enableRelay,
      enableServices: config.enableServices,
      plugins: config.plugins,
      signedDirectory: config.signedDirectory,
      federation: config.federation,
      lease: config.lease,
      payment: config.payment,
      subsidy: config.subsidy,
      shardStore: config.shardStore,
      custody: config.custody,
      mode: config.mode,
      productProfile: config.productProfile,
      requirePhysicalEnforcement: config.requirePhysicalEnforcement,
      // Static inspection reports compiled substrate readiness separately
      // below. Only RelayNode construction/startup may open the runtime gate.
      enforceCompiledAdmission: false
    })
  } catch (err) {
    errors.push(err.message)
  }

  const suffix = normalizeHiveAppHostSuffix(config.hiveAppHostSuffix)
  const keys = normalizeHiveAppPublicKeys(config.hiveAppPublicKeys)
  const versions = normalizeHiveAppPublicVersions(config.hiveAppPublicVersions ?? {})
  if (!keys || keys.size === 0) {
    errors.push('hiveAppPublicKeys must approve exactly one manifest-bound Phase-1 public app')
  } else if (keys.size !== 1) {
    // The release manifest currently binds one public app expectation per
    // relay. Do not let a PSL assertion turn unmanifested sibling apps into an
    // implicit admission set: Phase 1 remains exactly one app in both canary
    // and fleet postures until the manifest/evidence schemas bind every app.
    errors.push('Phase 1 must expose exactly one manifest-bound trusted app; multi-app admission is deferred')
  }
  if (!versions) {
    errors.push('hiveAppPublicVersions must map public app keys to non-negative safe integer versions')
  } else {
    const onlyKey = keys?.size === 1 ? [...keys][0] : null
    const hasExactPin = onlyKey !== null && versions.size === 1 && versions.has(onlyKey)
    if (!hasExactPin) {
      const message = 'Phase 1 requires exactly one immutable hiveAppPublicVersions pin matching its one manifest-bound app key'
      if (mode === 'fleet') errors.push(message)
      else warnings.push(message)
    }
  }
  if (!isLoopbackHost(config.apiHost)) errors.push('apiHost must bind loopback for a public gateway deployment')
  if (!isLoopbackHost(config.gatewayHost)) errors.push('gatewayHost must bind loopback behind the TLS edge')
  if (config.gatewayTrustProxy !== true) errors.push('gatewayTrustProxy must be true behind the strict TLS edge')
  if (config.gatewayRequireForwardedSNI !== true) errors.push('gatewayRequireForwardedSNI must be true for SNI/Host binding')
  if (config.custody?.enabled !== false) {
    errors.push('custody.enabled must be false on the dedicated public availability gateway until the frozen substrate role predicate proves safe mixed-role segregation')
  }
  if (config.requirePhysicalEnforcement !== true ||
      (opts.explicitConfig && (!Object.hasOwn(opts.explicitConfig, 'requirePhysicalEnforcement') ||
        opts.explicitConfig.requirePhysicalEnforcement !== true))) {
    errors.push('requirePhysicalEnforcement must be explicitly true in the raw public-t1 gateway config')
  }

  const trusted = config.gatewayTrustedProxyAddresses
  if (!Array.isArray(trusted) || trusted.length === 0 || trusted.some(address => !isLoopbackIp(address))) {
    errors.push('gatewayTrustedProxyAddresses must contain only loopback proxy IPs')
  }
  const compatibility = config.gatewayCompatibilityHosts
  if (!Array.isArray(compatibility) || compatibility.length === 0 || compatibility.some(host => !isLoopbackHost(host))) {
    errors.push('gatewayCompatibilityHosts must contain only loopback health/probe Hosts')
  }
  if (!Number.isSafeInteger(config.gatewayMaxInFlight) || config.gatewayMaxInFlight < 1 || config.gatewayMaxInFlight > 4096) {
    errors.push('gatewayMaxInFlight must be an integer from 1 to 4096')
  }
  if (!Number.isSafeInteger(config.gatewayMaxInFlightPerApp) || config.gatewayMaxInFlightPerApp < 1 ||
      config.gatewayMaxInFlightPerApp > config.gatewayMaxInFlight) {
    errors.push('gatewayMaxInFlightPerApp must be positive and no greater than gatewayMaxInFlight')
  }
  for (const [field, expected] of Object.entries(PUBLIC_HIVE_GATEWAY_FINITE_CONFIG_FIELDS)) {
    if (config[field] !== expected || (opts.explicitConfig && !Object.hasOwn(opts.explicitConfig, field))) {
      errors.push(`${field} must be explicitly pinned to the finite production value ${expected}`)
    }
  }
  if (opts.apiKeyPresent !== true) errors.push('a non-empty HIVERELAY_API_KEY is required even with a loopback management API')

  const readiness = inspectPublicHiveGatewayAdmissionReadiness(PUBLIC_HIVE_GATEWAY_ADMISSION_CAPABILITY, mode)
  errors.push(...readiness.errors)
  warnings.push(...readiness.warnings)
  if (opts.publicSuffixReady !== true) {
    warnings.push('app suffix is not evidenced as a Public Suffix; Phase 1 remains restricted to its one manifest-bound app')
  }
  warnings.push('Phase 1 disables shared proxy caching; versioned immutable URLs and purge semantics are deferred')

  return {
    ok: errors.length === 0,
    errors,
    warnings,
    normalized: suffix && keys && versions
      ? {
          suffix,
          appKeys: [...keys],
          appVersions: Object.fromEntries(versions),
          apiPort: config.apiPort,
          gatewayPort: config.gatewayPort,
          physicalEnforcementRequired: config.requirePhysicalEnforcement === true,
          finiteProductionPolicy: { ...PUBLIC_HIVE_GATEWAY_FINITE_POLICY },
          admissionProfile: PUBLIC_HIVE_GATEWAY_ADMISSION_CAPABILITY.profile
        }
      : null
  }
}

export function inspectPublicHiveGatewayAdmissionReadiness (capability, mode = 'canary') {
  const errors = []
  const warnings = []
  const profile = typeof capability?.profile === 'string' && capability.profile.length > 0
    ? capability.profile
    : 'unknown-profile'
  const authority = typeof capability?.authority === 'string' && capability.authority.length > 0
    ? capability.authority
    : 'unknown-authority'
  const valid = Object.isFrozen(capability) &&
    capability?.kind === 'public-hive-gateway-admission-capability' &&
    capability?.version === 1 &&
    typeof capability?.profile === 'string' && /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(capability.profile) &&
    typeof capability?.authority === 'string' && /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(capability.authority) &&
    typeof capability?.fleetReady === 'boolean'

  // Profile names are descriptive release metadata, never readiness signals.
  // Only the compiled admission capability can open the production posture.
  if (!valid || capability.fleetReady !== true) {
    const message = `blind-substrate admission is not fleet-ready (${profile}; authority=${authority})`
    if (mode === 'fleet') errors.push(message)
    else warnings.push(message)
  }
  return { ok: errors.length === 0, errors, warnings }
}

export function renderPublicHiveGatewayNginx (template, opts) {
  if (typeof template !== 'string' || Buffer.byteLength(template) > MAX_PROXY_TEMPLATE_BYTES) {
    throw new Error('nginx template must be a bounded string')
  }
  const suffix = normalizeHiveAppHostSuffix(opts?.suffix)
  if (!suffix) throw new Error('invalid Hive app host suffix')
  if (!Number.isSafeInteger(opts.gatewayPort) || opts.gatewayPort < 1 || opts.gatewayPort > 65535) {
    throw new Error('invalid gateway port')
  }
  const certificate = safeAbsolutePath(opts.certificate, 'certificate')
  const certificateKey = safeAbsolutePath(opts.certificateKey, 'certificate key')
  const suffixRegex = suffix.replace(/\./g, '\\.')
  const rendered = template
    .replaceAll('__HIVE_APP_HOST_SUFFIX__', suffix)
    .replaceAll('__HIVE_APP_HOST_SUFFIX_REGEX__', suffixRegex)
    .replaceAll('__HIVE_GATEWAY_PORT__', String(opts.gatewayPort))
    .replaceAll('__HIVE_TLS_CERTIFICATE__', certificate)
    .replaceAll('__HIVE_TLS_CERTIFICATE_KEY__', certificateKey)
  if (/__[A-Z0-9_]+__/.test(rendered)) throw new Error('nginx template contains unresolved placeholders')
  return rendered
}

export function inspectPublicHiveGatewayNginx (text, opts) {
  const errors = []
  if (typeof text !== 'string' || Buffer.byteLength(text) > MAX_NGINX_INSPECTION_BYTES) {
    return { ok: false, errors: ['nginx config must be a bounded string'] }
  }
  // Evidence must describe directives nginx can parse, not reassuring words in
  // comments. Preserve quoted # characters (for example in a safe path/value)
  // while removing comment text before every structural check below.
  const effective = stripNginxComments(text)
  const suffix = normalizeHiveAppHostSuffix(opts?.suffix)
  if (!suffix) errors.push('invalid expected suffix')
  if (!Number.isSafeInteger(opts?.gatewayPort)) errors.push('invalid expected gateway port')
  const parsed = parseNginxStatements(effective)
  if (!parsed.ok) errors.push('nginx configuration structure could not be inspected safely')
  if (findNginxBlocks(parsed.nodes, 'stream').length > 0) {
    errors.push('Phase 1 public gateway forbids nginx stream context and stream includes')
  }

  const serverRecords = findNginxBlocksWithContext(parsed.nodes, 'server')
  const serverBlocks = serverRecords.map(record => record.block)
  const httpServerBlocks = serverRecords.filter(isHttpServerContext).map(record => record.block)
  const foreignTlsServers = serverRecords.filter(record =>
    !isHttpServerContext(record) && directDirectives(record.block, 'listen').some(isPort443Listen))
  if (foreignTlsServers.length > 0) {
    errors.push('TLS port 443 server blocks must be in HTTP context; stream or nested listeners are forbidden')
  }
  if (serverBlocks.some(block => directDirectives(block, 'listen').some(directive => directive.args.includes('quic')))) {
    errors.push('Phase 1 public gateway must not configure an HTTP/3 QUIC listener')
  }
  if (allDirectives({ children: parsed.nodes }, 'add_header').some(directive =>
    String(directive.args[0] || '').toLowerCase() === 'alt-svc' &&
    directive.args.slice(1).join(' ').toLowerCase().includes('h3'))) {
    errors.push('Phase 1 public gateway must not advertise HTTP/3 through Alt-Svc')
  }
  inspectReviewedNginxDirectivePosture(parsed.nodes, errors)
  const defaultBlocks = httpServerBlocks.filter(block => directDirectives(block, 'listen').some(isDefaultTls443Listen))
  const safeDefaultBlocks = defaultBlocks.filter(isSafeDefaultServer)
  if (defaultBlocks.length !== 1 || safeDefaultBlocks.length !== 1) {
    errors.push('public TLS listener must have exactly one default 421 reject vhost and no competing default')
  }

  let appBlock = null
  if (suffix) {
    const escaped = suffix.replace(/\./g, '\\.')
    const expectedServerName = `"~^(?<hive_app_key>[ybndrfg8ejkmcpqxot1uwisza345h769]{52})\\.${escaped}$"`
    const appBlocks = httpServerBlocks.filter(block => {
      const names = directDirectives(block, 'server_name')
      return names.length === 1 && sameArgs(names[0].args, [expectedServerName])
    })
    if (appBlocks.length !== 1) {
      errors.push('nginx must contain exactly one active quoted app-key server block scoped to the configured suffix')
    } else {
      appBlock = appBlocks[0]
    }

    for (const block of httpServerBlocks) {
      if (block === appBlock) continue
      if (!directDirectives(block, 'listen').some(isPort443Listen)) continue
      const names = directDirectives(block, 'server_name').flatMap(directive => directive.args)
      if (names.some(name => serverNameCanMatchHiveApp(name, suffix))) {
        errors.push('a sibling TLS vhost can shadow the reviewed app-host server_name')
      }
    }
  }

  for (const block of httpServerBlocks) {
    if (block === appBlock || safeDefaultBlocks.includes(block)) continue
    const listens = directDirectives(block, 'listen')
    if (!listens.some(isPort443Listen)) continue
    if (!hasExactPublicTlsListeners(listens, false)) {
      errors.push('sibling TLS vhosts must use only the shared wildcard 443 ssl listen tuples')
    }
    const names = directDirectives(block, 'server_name').flatMap(directive => directive.args)
    if (names.length === 0 || names.some(name => !isExplicitDisjointServerName(name, suffix))) {
      errors.push('sibling TLS vhosts require explicit server_name values disjoint from the app suffix')
    }
  }

  // Every app-edge property must be a parsed directive in the one selected
  // app server. Directive-shaped words in comments, quoted values, or sibling
  // vhosts are deliberately not evidence.
  const appListens = appBlock ? directDirectives(appBlock, 'listen') : []
  if (!hasExactPublicTlsListeners(appListens, false)) errors.push('app proxy must actively listen on TLS port 443')
  if (!hasSingleDirective(appBlock, 'ssl_certificate') || !hasSingleDirective(appBlock, 'ssl_certificate_key')) {
    errors.push('app proxy must terminate TLS with one explicit certificate and key')
  }
  requireDirective(appBlock, 'ssl_protocols', ['TLSv1.2', 'TLSv1.3'], 'app proxy must permit only TLS 1.2 and TLS 1.3', errors)
  requireDirective(appBlock, 'ssl_session_tickets', ['off'], 'app proxy must disable TLS session tickets', errors)
  requireDirective(appBlock, 'access_log', ['off'], 'app proxy must disable durable request access logging', errors)
  requireDirective(appBlock, 'error_log', ['stderr', 'crit'], 'app proxy must constrain error logging to critical stderr events', errors)
  requireDirective(appBlock, 'server_tokens', ['off'], 'app proxy must suppress nginx version tokens', errors)
  requireDirective(appBlock, 'proxy_set_header', ['Accept-Encoding', '""'], 'proxy must require identity transfer encoding upstream', errors, false)
  requireDirective(appBlock, 'proxy_set_header', ['Host', '$host'], 'proxy must preserve canonical Host', errors, false)
  requireDirective(appBlock, 'proxy_set_header', ['X-Forwarded-For', '$remote_addr'], 'proxy must overwrite X-Forwarded-For', errors, false)
  requireDirective(appBlock, 'proxy_set_header', ['X-Forwarded-Host', '""'], 'proxy must strip X-Forwarded-Host', errors, false)
  requireDirective(appBlock, 'proxy_set_header', ['Forwarded', '""'], 'proxy must strip Forwarded', errors, false)
  requireDirective(appBlock, 'proxy_set_header', ['X-Hive-Forwarded-SNI', '$ssl_server_name'], 'proxy must attest the TLS SNI', errors, false)
  if (!hasExactProxyHeaderPolicy(appBlock)) errors.push('proxy header policy must not contain duplicate or unreviewed overrides')
  const forwardingScope = appBlock || { children: parsed.nodes }
  if (allDirectives(forwardingScope, 'proxy_set_header').some(directive => directive.args.includes('$proxy_add_x_forwarded_for'))) {
    errors.push('proxy must not append attacker-supplied X-Forwarded-For')
  }
  if (!hasSniHostReject(appBlock)) errors.push('proxy must reject SNI/Host mismatch')
  requireDirective(appBlock, 'proxy_cache', ['off'], 'Phase 1 proxy cache must be disabled', errors)
  requireDirective(appBlock, 'gzip', ['off'], 'proxy must not transform exact response bytes with gzip', errors)
  requireDirective(appBlock, 'gunzip', ['off'], 'proxy must not transform upstream response bytes with gunzip', errors)
  requireDirective(appBlock, 'proxy_hide_header', ['Set-Cookie'], 'proxy must suppress gateway Set-Cookie', errors)
  requireDirective(appBlock, 'client_max_body_size', ['1k'], 'app proxy must bound rejected request bodies', errors)
  requireDirective(appBlock, 'limit_req', ['zone=hive_app_rate', 'burst=40', 'nodelay'], 'app proxy must apply the reviewed request-rate zone', errors)
  requireDirective(appBlock, 'limit_conn', ['hive_app_conn', '16'], 'app proxy must apply the reviewed connection-rate zone', errors)
  requireDirective(appBlock, 'limit_req_status', ['429'], 'request-rate throttling must return 429', errors)
  requireDirective(appBlock, 'limit_conn_status', ['429'], 'connection throttling must return 429', errors)
  if (!hasReadOnlyRootLocation(appBlock)) errors.push('app proxy must allow only GET and implicit HEAD')
  requireDirective(appBlock, 'proxy_pass_request_body', ['off'], 'read-only app proxy must discard request bodies', errors)
  requireDirective(appBlock, 'proxy_set_header', ['Content-Length', '""'], 'read-only app proxy must clear request Content-Length', errors, false)
  requireDirective(appBlock, 'proxy_http_version', ['1.1'], 'app proxy must use the reviewed upstream HTTP version', errors)
  requireDirective(appBlock, 'proxy_next_upstream', ['off'], 'app proxy must disable upstream failover', errors)
  requireDirective(appBlock, 'proxy_buffering', ['off'], 'app proxy must disable response buffering', errors)
  requireDirective(appBlock, 'proxy_request_buffering', ['off'], 'app proxy must disable request buffering', errors)
  requireDirective(appBlock, 'add_header', ['Strict-Transport-Security', '"max-age=31536000"', 'always'], 'app proxy must emit HSTS', errors)
  if (Number.isSafeInteger(opts?.gatewayPort)) {
    requireDirective(appBlock, 'proxy_pass', [`http://127.0.0.1:${opts.gatewayPort}`], 'proxy_pass must target the loopback gateway port', errors)
  }
  const upstreams = appBlock
    ? allDirectives(appBlock).filter(directive => NGINX_UPSTREAM_DIRECTIVES.has(directive.name))
    : []
  if (upstreams.length !== 1 || upstreams[0].name !== 'proxy_pass') {
    errors.push('app proxy must contain exactly one upstream')
  }
  if (!hasReviewedAppShape(appBlock)) errors.push('app proxy must not contain directives or nested routes outside the reviewed Phase 1 policy')
  requireGlobalZone(parsed.nodes, 'limit_req_zone', '$binary_remote_addr', 'zone=hive_app_rate:', 'request-rate zone must key the direct client address', errors)
  requireGlobalZone(parsed.nodes, 'limit_conn_zone', '$binary_remote_addr', 'zone=hive_app_conn:', 'connection zone must key the direct client address', errors)
  return { ok: errors.length === 0, errors }
}

export function inspectActivePublicHiveGatewayNginx (text, opts) {
  const result = inspectPublicHiveGatewayNginx(text, opts)
  const errors = [...result.errors]
  const installed = opts?.installedConfig
  const installedPath = opts?.installedPath
  if (typeof installed !== 'string' || Buffer.byteLength(installed) > MAX_PROXY_TEMPLATE_BYTES ||
      typeof installedPath !== 'string' || !installedPath.startsWith('/') || /[\r\n\0]/.test(installedPath)) {
    errors.push('active nginx attestation requires a bounded installed config and absolute path')
    return { ok: false, errors }
  }
  if (/^# configuration file [^\r\n]+:\r?$/m.test(installed)) {
    errors.push('installed gateway configuration contains an ambiguous nginx dump marker')
    return { ok: false, errors }
  }

  const marker = `# configuration file ${installedPath}:\n`
  const markerOffsets = findLineOffsets(text, marker)
  if (markerOffsets.length !== 1) {
    errors.push('active nginx -T output must identify the installed gateway configuration exactly once')
    return { ok: false, errors }
  }
  const contentStart = markerOffsets[0] + marker.length
  if (!text.startsWith(installed, contentStart)) {
    errors.push('active nginx -T output does not contain the exact installed gateway configuration at its parser marker')
    return { ok: false, errors }
  }
  const remainder = text.slice(contentStart + installed.length)
  const nextMarker = '# configuration file '
  // nginx inserts one dump-separator newline after each file, including the
  // final file. It is framing, not an installed config byte.
  const exactBoundary = remainder.length === 0 || remainder === '\n' ||
    remainder.startsWith(`\n${nextMarker}`) ||
    (installed.endsWith('\n') && remainder.startsWith(nextMarker))
  if (!exactBoundary) {
    errors.push('active nginx -T output extends the installed gateway configuration before the next parser marker')
  }
  return { ok: errors.length === 0, errors }
}

export function stripNginxComments (text) {
  let output = ''
  let quote = null
  let escaped = false
  for (let i = 0; i < text.length; i++) {
    const character = text[i]
    if (escaped) {
      output += character
      escaped = false
      continue
    }
    if (character === '\\') {
      output += character
      escaped = true
      continue
    }
    if (quote) {
      output += character
      if (character === quote) quote = null
      continue
    }
    if (character === '"' || character === "'") {
      quote = character
      output += character
      continue
    }
    if (character === '#') {
      while (i + 1 < text.length && text[i + 1] !== '\n') i++
      continue
    }
    output += character
  }
  return output
}

function parseNginxStatements (text) {
  const nodes = []
  const stack = [nodes]
  let tokens = []
  let token = ''
  let quote = null
  let ok = true

  const pushToken = () => {
    if (token.length > 0) tokens.push(token)
    token = ''
  }
  for (let i = 0; i < text.length; i++) {
    const character = text[i]
    if (quote) {
      token += character
      if (character === '\\' && i + 1 < text.length) token += text[++i]
      else if (character === quote) quote = null
      continue
    }
    if (character === '"' || character === "'") {
      quote = character
      token += character
      continue
    }
    if (character === '\\') {
      token += character
      if (i + 1 < text.length) token += text[++i]
      else ok = false
      continue
    }
    if (/\s/.test(character)) {
      pushToken()
      continue
    }
    if (character !== ';' && character !== '{' && character !== '}') {
      token += character
      continue
    }

    pushToken()
    if (character === ';') {
      if (tokens.length === 0) ok = false
      else stack[stack.length - 1].push({ kind: 'directive', name: tokens[0], args: tokens.slice(1) })
      tokens = []
      continue
    }
    if (character === '{') {
      if (tokens.length === 0) {
        ok = false
      } else {
        const block = { kind: 'block', name: tokens[0], args: tokens.slice(1), children: [] }
        stack[stack.length - 1].push(block)
        stack.push(block.children)
      }
      tokens = []
      continue
    }
    if (tokens.length > 0 || stack.length === 1) ok = false
    else stack.pop()
    tokens = []
  }
  pushToken()
  if (quote || tokens.length > 0 || stack.length !== 1) ok = false
  return { ok, nodes }
}

function findNginxBlocks (nodes, name) {
  const matches = []
  for (const node of nodes) {
    if (node.kind !== 'block') continue
    if (node.name === name) matches.push(node)
    matches.push(...findNginxBlocks(node.children, name))
  }
  return matches
}

function findNginxBlocksWithContext (nodes, name, ancestors = []) {
  const matches = []
  for (const node of nodes) {
    if (node.kind !== 'block') continue
    if (node.name === name) matches.push({ block: node, ancestors })
    matches.push(...findNginxBlocksWithContext(node.children, name, [...ancestors, node]))
  }
  return matches
}

function isHttpServerContext (record) {
  // Included conf.d files are dumped by nginx -T without their lexical `http`
  // wrapper, so top-level server blocks are treated as HTTP include content.
  // An explicit parent must, however, be the HTTP context. This keeps a
  // `stream { server { listen 443; } }` block from satisfying or shadowing the
  // reviewed HTTPS topology.
  if (record.ancestors.length === 0) return true
  return record.ancestors.at(-1).name === 'http'
}

function directDirectives (block, name = null) {
  if (!block) return []
  return block.children.filter(node => node.kind === 'directive' && (name === null || node.name === name))
}

function allDirectives (block, name = null) {
  if (!block) return []
  const matches = []
  for (const node of block.children) {
    if (node.kind === 'directive' && (name === null || node.name === name)) matches.push(node)
    if (node.kind === 'block') matches.push(...allDirectives(node, name))
  }
  return matches
}

function inspectReviewedNginxDirectivePosture (nodes, errors) {
  const rejected = []
  const isReviewedTypesBlock = node => node.kind === 'block' &&
    node.name === 'types' &&
    node.children.every(child => child.kind === 'directive')
  const inspectHttpChildren = children => {
    for (const node of children) {
      if (node.kind === 'directive') {
        const reviewedParentCompression =
          (node.name === 'gzip' && (sameArgs(node.args, ['on']) || sameArgs(node.args, ['off']))) ||
          (node.name === 'gunzip' && sameArgs(node.args, ['off']))
        if (!NGINX_REVIEWED_HTTP_PARENT_DIRECTIVES.has(node.name) && !reviewedParentCompression) {
          rejected.push(node.name)
        }
        continue
      }
      if (node.kind === 'block' && node.name !== 'server' && !isReviewedTypesBlock(node)) {
        rejected.push(`${node.name} {}`)
      }
    }
  }

  for (const node of nodes) {
    if (node.kind === 'directive') {
      const reviewedTopLevel = NGINX_REVIEWED_MAIN_DIRECTIVES.has(node.name) ||
        NGINX_REVIEWED_HTTP_PARENT_DIRECTIVES.has(node.name) ||
        (node.name === 'gzip' && (sameArgs(node.args, ['on']) || sameArgs(node.args, ['off']))) ||
        (node.name === 'gunzip' && sameArgs(node.args, ['off']))
      if (!reviewedTopLevel) rejected.push(node.name)
      continue
    }
    if (node.name === 'events') {
      for (const child of node.children) {
        if (child.kind !== 'directive' || !NGINX_REVIEWED_EVENTS_DIRECTIVES.has(child.name)) {
          rejected.push(child.kind === 'block' ? `${child.name} {}` : child.name)
        }
      }
    } else if (node.name === 'http') {
      inspectHttpChildren(node.children)
    } else if (node.name !== 'server' && !isReviewedTypesBlock(node)) {
      rejected.push(`${node.name} {}`)
    }
  }

  if (rejected.length > 0) {
    const names = [...new Set(rejected)].sort().slice(0, 8).join(', ')
    errors.push(`Phase 1 public gateway rejects unreviewed inherited HTTP/module directives or blocks: ${names}`)
  }
}

function sameArgs (actual, expected) {
  return actual.length === expected.length && actual.every((value, index) => value === expected[index])
}

function isPort443Listen (directive) {
  const endpoint = directive.args[0] || ''
  return endpoint === '443' || /:443$/.test(endpoint)
}

function isDefaultTls443Listen (directive) {
  // `ssl` is a socket option. A separate server can claim default_server on a
  // socket whose sibling listen enables TLS without repeating the ssl token.
  return isPort443Listen(directive) && directive.args.includes('default_server')
}

function serverNameCanMatchHiveApp (rawName, suffix) {
  let name = String(rawName || '').trim()
  if ((name.startsWith('"') && name.endsWith('"')) || (name.startsWith("'") && name.endsWith("'"))) {
    name = name.slice(1, -1)
  }
  name = name.toLowerCase().replace(/\.$/, '')
  if (!name || name === '_') return false
  // Regex precedence and PCRE semantics are too broad to prove disjoint from
  // the 52-character key space with a string heuristic. Any sibling regex on
  // the shared TLS socket is therefore rejected fail-closed.
  if (name.startsWith('~')) return true
  const suffixName = suffix.toLowerCase()
  if (name.startsWith('*.')) {
    const base = name.slice(2)
    return suffixName === base || suffixName.endsWith(`.${base}`)
  }
  if (name.startsWith('.')) {
    const base = name.slice(1)
    return suffixName === base || suffixName.endsWith(`.${base}`)
  }
  if (name.includes('*')) return true
  if (!name.endsWith(`.${suffixName}`)) return false
  const label = name.slice(0, -(suffixName.length + 1))
  return /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/.test(label)
}

function isExplicitDisjointServerName (rawName, suffix) {
  let name = String(rawName || '').trim()
  if ((name.startsWith('"') && name.endsWith('"')) || (name.startsWith("'") && name.endsWith("'"))) {
    name = name.slice(1, -1)
  }
  name = name.toLowerCase().replace(/\.$/, '')
  if (!name || name === '_' || name.startsWith('~') || name.includes('*')) return false
  if (!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/.test(name)) return false
  return !serverNameCanMatchHiveApp(name, suffix)
}

function hasExactPublicTlsListeners (listens, defaultServer) {
  const option = defaultServer ? ['ssl', 'default_server'] : ['ssl']
  const expected = [
    ['443', ...option],
    ['[::]:443', ...option]
  ]
  return listens.length === expected.length && expected.every(args =>
    listens.some(directive => sameArgs(directive.args, args)))
}

function isSafeDefaultServer (block) {
  const directives = directDirectives(block)
  const names = directDirectives(block, 'server_name')
  const returns = directDirectives(block, 'return')
  const hasNestedBlock = block.children.some(node => node.kind === 'block')
  const upstreams = allDirectives(block).filter(directive => NGINX_UPSTREAM_DIRECTIVES.has(directive.name))
  return hasExactPublicTlsListeners(directDirectives(block, 'listen'), true) &&
    names.length === 1 && sameArgs(names[0].args, ['_']) &&
    returns.length === 1 && sameArgs(returns[0].args, ['421']) &&
    hasSingleDirective(block, 'ssl_certificate') &&
    hasSingleDirective(block, 'ssl_certificate_key') &&
    hasSingleDirective(block, 'ssl_protocols', ['TLSv1.2', 'TLSv1.3']) &&
    hasSingleDirective(block, 'ssl_session_tickets', ['off']) &&
    hasSingleDirective(block, 'gzip', ['off']) &&
    hasSingleDirective(block, 'gunzip', ['off']) &&
    hasSingleDirective(block, 'access_log', ['off']) &&
    hasSingleDirective(block, 'error_log', ['stderr', 'crit']) &&
    directives.every(directive => NGINX_DEFAULT_DIRECTIVES.has(directive.name)) &&
    !hasNestedBlock && upstreams.length === 0
}

function hasSingleDirective (block, name, args = null) {
  const directives = directDirectives(block, name)
  return directives.length === 1 && directives[0].args.length === (args?.length || 1) &&
    (args === null || sameArgs(directives[0].args, args))
}

function requireDirective (block, name, args, message, errors, exclusive = true) {
  const directives = allDirectives(block, name)
  const matches = directives.filter(directive => sameArgs(directive.args, args))
  if (matches.length !== 1 || (exclusive && directives.length !== 1)) errors.push(message)
}

function hasExactProxyHeaderPolicy (block) {
  const expected = [
    ['Connection', '""'],
    ['Accept-Encoding', '""'],
    ['Host', '$host'],
    ['Forwarded', '""'],
    ['X-Forwarded-Host', '""'],
    ['X-Forwarded-For', '$remote_addr'],
    ['X-Real-IP', '$remote_addr'],
    ['X-Forwarded-Proto', 'https'],
    ['X-Hive-Forwarded-SNI', '$ssl_server_name'],
    ['Content-Length', '""']
  ]
  const headers = allDirectives(block, 'proxy_set_header')
  return headers.length === expected.length && expected.every(args =>
    headers.filter(directive => sameArgs(directive.args, args)).length === 1)
}

function hasSniHostReject (block) {
  if (!block) return false
  const matches = block.children.filter(node => node.kind === 'block' && node.name === 'if' &&
    node.args.join(' ') === '($ssl_server_name != $host)')
  const returns = matches.length === 1 ? directDirectives(matches[0], 'return') : []
  return returns.length === 1 && sameArgs(returns[0].args, ['421'])
}

function hasReadOnlyRootLocation (block) {
  if (!block) return false
  const locations = findNginxBlocks(block.children, 'location')
  if (locations.length !== 1 || !sameArgs(locations[0].args, ['/'])) return false
  const limits = findNginxBlocks(locations[0].children, 'limit_except')
  if (limits.length !== 1 || !sameArgs(limits[0].args, ['GET'])) return false
  const denies = directDirectives(limits[0], 'deny')
  return denies.length === 1 && sameArgs(denies[0].args, ['all'])
}

function hasReviewedAppShape (block) {
  if (!block) return false
  const directBlocks = block.children.filter(node => node.kind === 'block')
  if (!directDirectives(block).every(directive => NGINX_APP_SERVER_DIRECTIVES.has(directive.name))) return false
  if (!directBlocks.every(node => node.name === 'if' || node.name === 'location')) return false

  const conditions = directBlocks.filter(node => node.name === 'if')
  const locations = directBlocks.filter(node => node.name === 'location')
  if (conditions.length !== 1 || locations.length !== 1) return false
  if (!directDirectives(conditions[0]).every(directive => directive.name === 'return') ||
      conditions[0].children.some(node => node.kind === 'block')) return false

  const location = locations[0]
  if (!directDirectives(location).every(directive => NGINX_APP_LOCATION_DIRECTIVES.has(directive.name))) return false
  const limitBlocks = location.children.filter(node => node.kind === 'block')
  if (limitBlocks.length !== 1 || limitBlocks[0].name !== 'limit_except') return false
  return directDirectives(limitBlocks[0]).every(directive => directive.name === 'deny') &&
    !limitBlocks[0].children.some(node => node.kind === 'block')
}

function requireGlobalZone (nodes, name, key, zonePrefix, message, errors) {
  const root = { children: nodes }
  const found = allDirectives(root, name).some(directive =>
    directive.args[0] === key && directive.args.some(value => value.startsWith(zonePrefix)))
  if (!found) errors.push(message)
}

function findLineOffsets (text, line) {
  const offsets = []
  let offset = 0
  while (offset < text.length) {
    const found = text.indexOf(line, offset)
    if (found < 0) break
    if (found === 0 || text[found - 1] === '\n') offsets.push(found)
    offset = found + line.length
  }
  return offsets
}

export async function probePublicHiveGateway (opts) {
  const appKey = String(opts?.appKey || '').toLowerCase()
  if (!/^[0-9a-f]{64}$/.test(appKey)) throw new Error('probe appKey must be 64 hex characters')
  const origin = new URL(opts.origin)
  if (origin.protocol !== 'https:' || origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash) {
    throw new Error('probe origin must be a credential-free HTTPS origin')
  }
  const resolved = resolveHiveAppHost(origin.hostname, opts.suffix)
  if (resolved.kind !== 'app' || resolved.appKey !== appKey) throw new Error('probe origin does not encode the expected app key')
  const connectAddress = opts.connectAddress == null
    ? null
    : normalizePublicHiveGatewayConnectAddress(opts.connectAddress)
  if (opts.connectAddress != null && !connectAddress) throw new Error('probe connectAddress must be an explicit IP address')
  const expectedDriveVersion = opts.expectedDriveVersion == null
    ? null
    : opts.expectedDriveVersion
  if (expectedDriveVersion != null && (!Number.isSafeInteger(expectedDriveVersion) || expectedDriveVersion < 0)) {
    throw new Error('probe expectedDriveVersion must be a non-negative safe integer')
  }
  const requestOpts = {
    ca: opts.ca,
    lookup: connectAddress ? createPinnedLookup(connectAddress) : opts.lookup,
    timeoutMs: opts.timeoutMs || 5000,
    maxBytes: opts.maxBytes || MAX_PROBE_BODY_BYTES
  }
  const path = opts.path || '/index.html'

  const browserEncoding = { 'Accept-Encoding': 'gzip, br, zstd' }
  const metadata = await requestUrl(new URL('/.well-known/hiverelay-app.json', origin), 'GET', browserEncoding, requestOpts)
  const metadataBody = parseJson(metadata.raw, 'gateway metadata')
  assertResponse(metadata.statusCode === 200, 'gateway metadata must return 200')
  assertResponse(metadataBody.appKey === appKey, 'gateway metadata app key mismatch')
  assertResponse(metadataBody.gatewayHost === origin.hostname, 'gateway metadata Host mismatch')
  assertAppHeaders(metadata.headers, appKey, 'generated')

  const exact = await requestUrl(new URL(path, origin), 'GET', browserEncoding, requestOpts)
  assertResponse(exact.statusCode === 200, 'exact app probe must return 200')
  assertAppHeaders(exact.headers, appKey, 'exact', path)
  assertResponse(exact.raw.byteLength > 0, 'exact app probe must return a non-empty fixture')
  assertResponse(exact.tlsProtocol === 'TLSv1.2' || exact.tlsProtocol === 'TLSv1.3', 'gateway must negotiate TLS 1.2 or newer')
  assertResponse(/^(?:[0-9a-f]{2}:){31}[0-9a-f]{2}$/i.test(exact.peerFingerprint256 || ''), 'gateway peer certificate fingerprint unavailable')
  const driveVersion = exact.headers['x-hive-drive-version'] || null
  if (expectedDriveVersion != null) {
    assertResponse(driveVersion === String(expectedDriveVersion), 'exact app response drive version does not match the configured immutable pin')
  }
  const sha256 = createHash('sha256').update(exact.raw).digest('hex')
  if (opts.expectedSha256 && sha256 !== String(opts.expectedSha256).toLowerCase()) {
    throw new Error('exact app response SHA-256 mismatch')
  }

  const range = await requestUrl(new URL(path, origin), 'GET', {
    ...browserEncoding,
    Range: 'bytes=0-0'
  }, requestOpts)
  assertResponse(range.statusCode === 206 && range.raw.byteLength === 1, 'single-byte Range probe failed')
  assertResponse(range.raw[0] === exact.raw[0], 'Range probe returned different source bytes')
  assertResponse(range.headers['content-range'] === `bytes 0-0/${exact.raw.byteLength}`, 'Range Content-Range mismatch')
  assertAppHeaders(range.headers, appKey, 'exact', path)
  assertResponse(range.headers['x-hive-drive-version'] === driveVersion, 'Range probe drive version changed')

  const head = await requestUrl(new URL(path, origin), 'HEAD', browserEncoding, requestOpts)
  assertResponse(head.statusCode === 200 && head.raw.byteLength === 0, 'HEAD probe failed')
  assertResponse(head.headers['content-length'] === String(exact.raw.byteLength), 'HEAD content length mismatch')
  assertAppHeaders(head.headers, appKey, 'exact', path)
  assertResponse(head.headers['x-hive-drive-version'] === driveVersion, 'HEAD probe drive version changed')

  const management = await requestUrl(new URL('/api/manage/config', origin), 'GET', browserEncoding, requestOpts)
  assertResponse(management.statusCode === 403 || management.statusCode === 404, 'app Host reached a management-looking route')
  assertResponse(!management.raw.toString('utf8').includes('apiKey'), 'management response leaked through app Host')
  assertResponse(management.headers['alt-svc'] === undefined, 'app edge must not advertise an unprobed HTTP/3 route')
  assertResponse(management.headers['content-encoding'] === undefined, 'non-probed app route must not be content encoded')

  const forwarded = await requestUrl(new URL(path, origin), 'GET', {
    ...browserEncoding,
    'X-Forwarded-Host': `${'y'.repeat(52)}.${resolved.host.slice(resolved.label.length + 1)}`,
    'X-Forwarded-For': '198.51.100.99'
  }, requestOpts)
  assertResponse(forwarded.statusCode === 200, 'forwarded headers changed app availability')
  assertResponse(forwarded.raw.equals(exact.raw), 'forwarded headers changed exact app bytes')
  assertAppHeaders(forwarded.headers, appKey, 'exact', path)
  assertResponse(forwarded.headers['x-hive-drive-version'] === driveVersion,
    'forwarded headers changed the immutable drive version')

  const unavailableAppKey = appKey === '0'.repeat(64) ? 'f'.repeat(64) : '0'.repeat(64)
  const unavailableOrigin = new URL(origin)
  unavailableOrigin.hostname = `${encodeHiveAppKey(Buffer.from(unavailableAppKey, 'hex'))}.${resolved.host.slice(resolved.label.length + 1)}`
  const unavailable = await requestUrl(new URL('/', unavailableOrigin), 'GET', browserEncoding, requestOpts)
  const unavailableBody = parseJson(unavailable.raw, 'unavailable app response')
  assertResponse(unavailable.statusCode === 403, 'unapproved canonical app Host must fail closed')
  assertResponse(unavailableBody.error === 'App unavailable through public Hive gateway', 'unavailable app response is not generic')
  assertAppOriginHeaders(unavailable.headers, unavailableAppKey)

  // Keep the destination URL/lookup pinned to the approved origin while
  // varying only TLS SNI and HTTP Host. The unrelated-SNI request disables
  // certificate verification solely because a correct default vhost cannot
  // present a certificate for an intentionally unrelated name; no positive
  // content or certificate evidence is taken from that request.
  const unrelatedHost = 'public-hive-default-reject.invalid'
  const defaultSni = await requestUrl(new URL('/', origin), 'GET', {
    Host: unrelatedHost
  }, {
    ...requestOpts,
    servername: unrelatedHost,
    rejectUnauthorized: false
  })
  assertResponse(defaultSni.statusCode === 421, 'public TLS default vhost must reject unrelated SNI with 421')

  const sniHostMismatch = await requestUrl(new URL('/', origin), 'GET', {
    Host: unavailableOrigin.host
  }, {
    ...requestOpts,
    servername: origin.hostname
  })
  assertResponse(sniHostMismatch.statusCode === 421, 'public TLS edge must reject SNI/Host mismatch with 421')

  return {
    schema: PUBLIC_HIVE_GATEWAY_PROBE_SCHEMA,
    observedAt: new Date().toISOString(),
    origin: origin.origin,
    connectAddress,
    appKey,
    path,
    sha256,
    bytes: exact.raw.byteLength,
    driveVersion,
    tlsProtocol: exact.tlsProtocol,
    peerFingerprint256: exact.peerFingerprint256,
    peerValidTo: exact.peerValidTo,
    metadataSigned: metadataBody.signed === true,
    checks: Object.fromEntries(PUBLIC_HIVE_GATEWAY_PROBE_CHECKS.map(name => [name, true]))
  }
}

function requestUrl (url, method, headers, opts) {
  return new Promise((resolve, reject) => {
    const client = url.protocol === 'https:' ? https : http
    const req = client.request(url, {
      method,
      headers,
      ca: opts.ca,
      lookup: opts.lookup,
      servername: opts.servername,
      rejectUnauthorized: opts.rejectUnauthorized,
      timeout: opts.timeoutMs,
      agent: false
    }, res => {
      const encrypted = res.socket.encrypted === true
      const certificate = encrypted ? res.socket.getPeerCertificate() : null
      const tlsProtocol = encrypted ? res.socket.getProtocol() : null
      const chunks = []
      let bytes = 0
      res.on('data', chunk => {
        bytes += chunk.byteLength
        if (bytes > opts.maxBytes) {
          req.destroy(new Error('gateway probe response exceeded byte limit'))
          return
        }
        chunks.push(chunk)
      })
      res.on('end', () => resolve({
        statusCode: res.statusCode,
        headers: res.headers,
        raw: Buffer.concat(chunks),
        tlsProtocol,
        peerFingerprint256: certificate?.fingerprint256 || null,
        peerValidTo: certificate?.valid_to || null
      }))
    })
    req.on('timeout', () => req.destroy(new Error('gateway probe timed out')))
    req.on('error', reject)
    req.end()
  })
}

function createPinnedLookup (address) {
  const family = isIP(address)
  return (_hostname, options, callback) => {
    if (options?.all === true) callback(null, [{ address, family }])
    else callback(null, address, family)
  }
}

function assertAppHeaders (headers, appKey, mode, filePath = null) {
  assertAppOriginHeaders(headers, appKey)
  assertResponse(headers['x-hive-byte-mode'] === mode, `X-Hive-Byte-Mode must be ${mode}`)
  if (mode === 'exact') assertCanonicalIdentity(headers.link, appKey, filePath)
}

function assertCanonicalIdentity (header, appKey, filePath) {
  const canonical = new URL(`hive://${appKey}/`)
  canonical.pathname = new URL(filePath, 'https://gateway.invalid').pathname
  const relations = parseLinkRelations(header)
  assertResponse(relations.some(link => link.target === canonical.href && link.relations.includes('canonical')),
    'exact app response must advertise its canonical hive:// identity')
  assertResponse(relations.some(link => link.target === '/.well-known/hiverelay-app.json' && link.relations.includes('describedby')),
    'exact app response must advertise the gateway metadata relation')
}

function parseLinkRelations (header) {
  const value = Array.isArray(header) ? header.join(',') : String(header || '')
  if (value.length > 8192 || /[\r\n]/.test(value)) return []
  return value.split(',').map(part => {
    const match = part.trim().match(/^<([^<>]+)>\s*;\s*rel=(?:"([^"]+)"|([^\s;,]+))$/i)
    if (!match) return null
    return {
      target: match[1],
      relations: String(match[2] || match[3]).toLowerCase().split(/\s+/)
    }
  }).filter(Boolean)
}

function assertAppOriginHeaders (headers, appKey) {
  assertResponse(headers['x-hive-app-key'] === appKey, 'X-Hive-App-Key mismatch')
  assertResponse(String(headers.vary || '').split(',').map(v => v.trim().toLowerCase()).includes('host'), 'Vary must include Host')
  assertResponse(headers['cache-control'] === 'no-store, max-age=0', 'Phase 1 app response must be no-store')
  assertResponse(headers['set-cookie'] === undefined, 'gateway response must not set cookies')
  assertResponse(headers['alt-svc'] === undefined, 'Phase 1 app response must not advertise HTTP/3')
  assertResponse(headers['content-encoding'] === undefined, 'Phase 1 app response must not be content encoded')
}

function assertResponse (condition, message) {
  if (!condition) throw new Error(message)
}

function parseJson (raw, label) {
  try { return JSON.parse(raw.toString('utf8')) } catch { throw new Error(`${label} is not valid JSON`) }
}

function isLoopbackHost (value) {
  if (typeof value !== 'string') return false
  const host = value.trim().toLowerCase().replace(/^\[|\]$/g, '')
  if (host === 'localhost' || host === '::1') return true
  return isIP(host) === 4 && host.startsWith('127.')
}

function isLoopbackIp (value) {
  if (typeof value !== 'string') return false
  let address = value.trim().toLowerCase()
  if (address.startsWith('::ffff:')) address = address.slice(7)
  return address === '::1' || (isIP(address) === 4 && address.startsWith('127.'))
}

function safeAbsolutePath (value, label) {
  if (typeof value !== 'string' || value.length > 4096 || !/^\/[A-Za-z0-9._/-]+$/.test(value)) {
    throw new Error(`${label} must be a safe absolute path`)
  }
  return value
}