OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / publish-fleet-channel.mjs

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

import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import { constants as fsConstants } from 'node:fs'
import { lstat, mkdir, mkdtemp, open, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { promisify } from 'node:util'
import { fileURLToPath } from 'node:url'
import {
  verifyPublicHiveGatewayOpsEvidence
} from './lib/public-hive-gateway-ops.mjs'

const execFileAsync = promisify(execFile)
const scriptPath = fileURLToPath(import.meta.url)
const scriptDir = path.dirname(scriptPath)
const defaultRepoRoot = path.resolve(scriptDir, '..')
const PROMOTER_RELATIVE_PATH = 'scripts/promote-fleet-channel.mjs'
const PROMOTER_LIBRARY_RELATIVE_PATH = 'scripts/lib/public-hive-gateway-release-manifest.mjs'
const PROMOTER_POLICY_RELATIVE_PATH = 'scripts/lib/public-hive-gateway-policy.mjs'
const CHANNELS_RELATIVE_PATH = 'fleet/channels.json'
const MAX_CHANNELS_BYTES = 256 * 1024
const MAX_ALLOWED_SIGNERS_BYTES = 256 * 1024
const MAX_TRUSTED_PROMOTER_BYTES = 1024 * 1024
const MAX_PROCESS_OUTPUT_BYTES = 4 * 1024 * 1024
const MAX_BOOTSTRAP_AUTHORITY_BYTES = 2 * 1024 * 1024
const GIT_PROGRAM = '/usr/bin/git'
const SSH_KEYGEN_PROGRAM = '/usr/bin/ssh-keygen'
const DISABLED_HOOKS_PATH = '/dev/null'
const RELEASE_TAG_PATTERN = /^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/
const REF_COMPONENT_PATTERN = /^(?!\.)(?!.*(?:\.\.|\/\.|\.lock(?:\/|$)))[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/
const REMOTE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/
const GOOD_SIGNATURE_PATTERN = /GOODSIG|TRUST_(?:FULLY|ULTIMATE)|Good[^\r\n]*signature/i
const PUBLIC_GATEWAY_PREDECESSOR_AUTHORITY = Object.freeze([
  Object.freeze({ path: 'fleet/updater.sh', mode: '100755' }),
  Object.freeze({ path: 'fleet/quarantine-public-gateway.sh', mode: '100755' }),
  Object.freeze({ path: 'scripts/verify-public-hive-gateway-quarantine.mjs', mode: '100644' }),
  Object.freeze({ path: 'scripts/lib/public-hive-gateway-quarantine-authority.mjs', mode: '100644' }),
  Object.freeze({ path: 'scripts/lib/public-hive-gateway-release-manifest.mjs', mode: '100644' }),
  Object.freeze({ path: 'scripts/lib/public-hive-gateway-policy.mjs', mode: '100644' })
])
const UNSAFE_GIT_ENV = new Set([
  'GIT_ALTERNATE_OBJECT_DIRECTORIES',
  'GIT_COMMON_DIR',
  'GIT_CONFIG',
  'GIT_CONFIG_COUNT',
  'GIT_CONFIG_GLOBAL',
  'GIT_CONFIG_PARAMETERS',
  'GIT_CONFIG_SYSTEM',
  'GIT_DIR',
  'GIT_EXEC_PATH',
  'GIT_GRAFT_FILE',
  'GIT_INDEX_FILE',
  'GIT_NAMESPACE',
  'GIT_OBJECT_DIRECTORY',
  'GIT_PREFIX',
  'GIT_PROXY_COMMAND',
  'GIT_QUARANTINE_PATH',
  'GIT_REPLACE_REF_BASE',
  'GIT_SHALLOW_FILE',
  'GIT_SSH',
  'GIT_SSH_COMMAND',
  'GIT_SSL_CAINFO',
  'GIT_SSL_CAPATH',
  'GIT_SSL_NO_VERIFY',
  'GIT_WORK_TREE'
])

const usage = `
Usage:
  node scripts/publish-fleet-channel.mjs --channel <canary|stable> --target <vX.Y.Z> [options]

Publisher options:
  --publish                  Create, verify, and atomically publish the signed channel commit
  --remote <name>            Git remote (default: origin)
  --branch <name>            Published control branch (default: main)
  --repo <absolute-path>     Clean control checkout (default: repository root)

Promotion evidence options:
  --canary-evidence <path>   Verified canary rollout evidence (stable)
  --gateway-window-state <p> Completed signed observation state (gateway stable)
  --relays <path>            Current full fleet inventory
  --allowed-signers <path>   Trusted OpenSSH allowed_signers file
  --gateway-manifest <path>  Legacy gateway manifest override only
  --gateway-ops-evidence-dir <absolute-dir>
                              Fresh per-relay <relay>.json ops artifacts (public-t1)
  --require-public-gateway   Require gateway evidence for a legacy release
  --help                     Show this help

Without --publish this command performs validation without changing the control
worktree, refs, or remotes. A private temporary trust snapshot is removed on
exit. Enabled
canonical gateway manifests force their own canary/stable evidence gates. This
command uses exact compare-and-swap leases, never an unguarded force push, and
never advances stable automatically.`

if (path.resolve(process.argv[1] || '') === scriptPath) {
  try {
    await main()
  } catch (err) {
    console.error(`fleet channel publisher: ${safeError(err)}`)
    process.exitCode = 1
  }
}

async function main () {
  const options = parseArgs(process.argv.slice(2))
  if (options.help) {
    console.log(usage.trim())
    return
  }
  const result = await publishFleetChannel(options)
  console.log(JSON.stringify(result, null, 2))
}

export async function publishFleetChannel (input, injected = {}) {
  const options = normalizeOptions(input)
  const repoRoot = options.repo
  const sourceAllowedSignersPath = options.allowedSigners || path.join(repoRoot, 'fleet', 'allowed-signers')
  const pinTrust = injected.pinAllowedSigners || pinAllowedSigners
  const pinnedTrust = await pinTrust(sourceAllowedSignersPath)
  if (!pinnedTrust || !path.isAbsolute(pinnedTrust.path || '') || typeof pinnedTrust.cleanup !== 'function') {
    throw new Error('could not create an isolated allowed_signers snapshot')
  }
  try {
    return await publishWithPinnedTrust(options, injected, pinnedTrust.path)
  } finally {
    await pinnedTrust.cleanup()
  }
}

async function publishWithPinnedTrust (options, injected, allowedSignersPath) {
  const repoRoot = options.repo
  const channelsPath = path.join(repoRoot, CHANNELS_RELATIVE_PATH)
  const git = injected.git || ((args, failure) => runGit(repoRoot, args, failure))
  const injectedPromote = injected.promote
  const deriveChannels = injected.deriveChannels || deriveExpectedChannels

  const topLevel = (await git(['rev-parse', '--show-toplevel'], 'not a Git worktree')).stdout.trim()
  if (path.resolve(topLevel) !== repoRoot && !(await pathsReferToSameLocation(topLevel, repoRoot))) {
    throw new Error(`--repo must be the exact Git worktree root (${topLevel || 'unknown'})`)
  }
  const branch = (await git(['symbolic-ref', '--quiet', '--short', 'HEAD'],
    'publisher requires an attached branch')).stdout.trim()
  if (branch !== options.branch) {
    throw new Error(`publisher requires branch ${options.branch}; current branch is ${branch || 'detached'}`)
  }
  await assertCleanControlWorktree(git)

  const startingHead = normalizeObjectId((await git(['rev-parse', '--verify', 'HEAD'],
    'cannot resolve local HEAD')).stdout, 'local HEAD')
  const promote = injectedPromote ||
    (args => runPromoterFromCommit(args, startingHead, git))
  await assertNoGitUrlRewrites(git)
  const remoteEndpoint = await resolveRemoteEndpoint(git, options.remote)
  const remoteHead = await readRemoteHead(git, remoteEndpoint, options.remote, options.branch)
  if (remoteHead !== startingHead) {
    throw new Error(`local ${options.branch} is not the current ${options.remote}/${options.branch}; refusing stale publication`)
  }

  const promoterArgs = buildPromoterArgs(options, repoRoot, channelsPath, allowedSignersPath)
  const dryRun = validatePromotionResult(await promote([...promoterArgs, '--dry-run']), options, channelsPath, true)
  await assertRecoveryExpectedCurrent(options, dryRun, git)
  await assertRemoteReleaseTag(git, remoteEndpoint, options.remote, options.target, dryRun)
  const predecessorAuthority = await validateEnabledGatewayPredecessor(
    dryRun, git, allowedSignersPath, remoteEndpoint, options.remote)
  const operatorContracts = await validateOperatorReadiness(options, dryRun, git)
  const baseResult = {
    schema: 'hiverelay-fleet-channel-publication-v1',
    status: options.publish ? 'validated' : 'dry-run',
    channel: options.channel,
    target: options.target,
    branch: options.branch,
    remote: options.remote,
    startingHead,
    publicGatewayRequired: dryRun.publicGatewayRequired,
    predecessorAuthority,
    operatorContracts,
    wouldChange: dryRun.wouldChange
  }
  if (!options.publish) return baseResult

  const expectedChannels = dryRun.wouldChange
    ? await deriveChannels(channelsPath, options.channel, options.target, dryRun.previousTarget, git)
    : null

  // Recheck immediately before the only local mutation. The promoter then
  // atomically updates the one control-plane file it already validated.
  await assertCleanControlWorktree(git)
  const preMutationHead = normalizeObjectId((await git(['rev-parse', '--verify', 'HEAD'],
    'cannot re-resolve local HEAD')).stdout, 'local HEAD')
  if (preMutationHead !== startingHead ||
      await readRemoteHead(git, remoteEndpoint, options.remote, options.branch) !== startingHead) {
    throw new Error('local or remote control branch changed after validation')
  }
  await assertRemoteReleaseTag(git, remoteEndpoint, options.remote, options.target, dryRun)
  const revalidatedPredecessorAuthority = await validateEnabledGatewayPredecessor(
    dryRun, git, allowedSignersPath, remoteEndpoint, options.remote)
  if (JSON.stringify(revalidatedPredecessorAuthority) !== JSON.stringify(predecessorAuthority)) {
    throw new Error('public gateway predecessor authority changed after publication validation')
  }
  const revalidatedOperatorContracts = await validateOperatorReadiness(options, dryRun, git)
  if (JSON.stringify(revalidatedOperatorContracts) !== JSON.stringify(operatorContracts)) {
    throw new Error('operator readiness evidence changed after publication validation')
  }
  await assertRecoveryExpectedCurrent(options, dryRun, git)

  const applied = validatePromotionResult(await promote(promoterArgs), options, channelsPath, false)
  if (applied.wouldChange !== dryRun.wouldChange ||
      applied.previousTarget !== dryRun.previousTarget ||
      applied.targetSha !== dryRun.targetSha || applied.tagObjectSha !== dryRun.tagObjectSha ||
      JSON.stringify(applied.operatorContracts) !== JSON.stringify(dryRun.operatorContracts)) {
    throw new Error('promotion result changed between validation and application')
  }
  await assertRecoveryExpectedCurrent(options, applied, git)
  if (!applied.wouldChange) {
    await assertCleanControlWorktree(git)
    return { ...baseResult, status: 'unchanged', publishedHead: startingHead }
  }

  await assertOnlyChannelsWorktreeChange(git)
  await git(['diff', '--check', '--', CHANNELS_RELATIVE_PATH],
    'channel update failed git diff safety checks')
  if (await readRemoteHead(git, remoteEndpoint, options.remote, options.branch) !== startingHead) {
    throw new Error('remote control branch changed after channel validation; leaving the local channel edit for inspection')
  }

  await git(['add', '--', CHANNELS_RELATIVE_PATH], 'could not stage the channel update')
  await assertOnlyChannelsStaged(git)
  await assertIndexEntry(git, expectedChannels.blobOid)
  await git(
    [
      '-c', `core.hooksPath=${DISABLED_HOOKS_PATH}`,
      '-c', 'gpg.format=ssh',
      'commit', '-S', '-m', `fleet: promote ${options.channel} to ${options.target}`
    ],
    'could not create the required signed channel commit'
  )

  const publishedHead = normalizeObjectId((await git(['rev-parse', '--verify', 'HEAD'],
    'cannot resolve publication commit')).stdout, 'publication commit')
  const parent = normalizeObjectId((await git(['rev-parse', '--verify', 'HEAD^'],
    'publication commit has no parent')).stdout, 'publication parent')
  if (publishedHead === startingHead || parent !== startingHead) {
    throw new Error('publication commit is not a single child of the validated control head')
  }
  await assertCommitEntry(git, expectedChannels.blobOid)
  await assertPublicationCommit(git)
  await assertCleanControlWorktree(git)

  const signature = await git([
    '-c', 'gpg.format=ssh',
    '-c', `gpg.ssh.allowedSignersFile=${allowedSignersPath}`,
    '-c', `gpg.ssh.program=${SSH_KEYGEN_PROGRAM}`,
    'verify-commit', '--raw', publishedHead
  ], 'publication commit is not signed by a trusted fleet signer')
  if (!GOOD_SIGNATURE_PATTERN.test(`${signature.stdout}\n${signature.stderr}`)) {
    throw new Error('publication commit verification did not report a trusted signature')
  }

  if (await readRemoteHead(git, remoteEndpoint, options.remote, options.branch) !== startingHead) {
    throw new Error('remote control branch raced the signed commit; refusing to push')
  }
  await assertNoGitUrlRewrites(git)
  await assertRemoteReleaseTag(git, remoteEndpoint, options.remote, options.target, dryRun)
  const releaseTagRef = `refs/tags/${options.target}`
  await git(
    [
      '-c', `core.hooksPath=${DISABLED_HOOKS_PATH}`,
      'push', '--atomic', '--porcelain', '--no-follow-tags',
      `--force-with-lease=refs/heads/${options.branch}:${startingHead}`,
      `--force-with-lease=${releaseTagRef}:${dryRun.tagObjectSha}`,
      '--', remoteEndpoint,
      `${dryRun.tagObjectSha}:${releaseTagRef}`,
      `${publishedHead}:refs/heads/${options.branch}`
    ],
    'atomic compare-and-swap publication was rejected; the signed local commit was preserved'
  )
  const confirmedRemoteHead = await readRemoteHead(git, remoteEndpoint, options.remote, options.branch)
  if (confirmedRemoteHead !== publishedHead) {
    throw new Error('remote did not resolve to the signed publication commit after push')
  }
  await assertRemoteReleaseTag(git, remoteEndpoint, options.remote, options.target, dryRun)

  return {
    ...baseResult,
    status: 'published',
    publishedHead,
    remoteHead: confirmedRemoteHead
  }
}

async function assertRecoveryExpectedCurrent (options, promotion, git) {
  if (!options.expectedCurrentTarget) return
  if (promotion.previousTarget !== options.expectedCurrentTarget) {
    throw new Error('starting fleet channel does not match the recovery expected-current target')
  }
  const previousSha = normalizeObjectId((await git([
    'rev-parse', '--verify', `refs/tags/${promotion.previousTarget}^{commit}`
  ], 'cannot resolve recovery expected-current tag')).stdout, 'recovery expected-current SHA')
  if (previousSha !== options.expectedCurrentTargetSha) {
    throw new Error('starting fleet channel target SHA does not match the recovery receipt')
  }
}

function normalizeOptions (input) {
  const options = input || {}
  if (options.channel !== 'canary' && options.channel !== 'stable') {
    throw new Error('--channel must be exactly canary or stable')
  }
  if (!RELEASE_TAG_PATTERN.test(options.target || '')) {
    throw new Error('--target must be an immutable release tag like v1.2.3')
  }
  const repo = path.resolve(options.repo || defaultRepoRoot)
  if (!path.isAbsolute(repo) || hasControlChars(repo)) throw new Error('--repo must be a safe absolute path')
  const remote = options.remote || 'origin'
  if (!REMOTE_NAME_PATTERN.test(remote)) throw new Error('--remote must be a bounded Git remote name')
  const branch = options.branch || 'main'
  if (!isSafeBranch(branch)) throw new Error('--branch must be a bounded safe Git branch name')
  if (options.expectedCurrentTarget != null || options.expectedCurrentTargetSha != null) {
    if (!RELEASE_TAG_PATTERN.test(options.expectedCurrentTarget || '') ||
        !isObjectId(options.expectedCurrentTargetSha || '')) {
      throw new Error('recovery expected-current target and SHA must be an exact release binding')
    }
  }
  if (options.channel === 'stable' && !options.canaryEvidence) {
    throw new Error('--canary-evidence is required before stable publication')
  }
  for (const name of ['canaryEvidence', 'gatewayWindowState', 'relays', 'allowedSigners', 'gatewayOpsEvidenceDir']) {
    if (options[name] != null && (!path.isAbsolute(options[name]) || hasControlChars(options[name]))) {
      throw new Error(`--${kebab(name)} must be an absolute safe path`)
    }
  }
  return { ...options, repo, remote, branch, publish: options.publish === true }
}

function buildPromoterArgs (options, repoRoot, channelsPath, allowedSignersPath) {
  const args = [
    '--channel', options.channel,
    '--target', options.target,
    '--repo', repoRoot,
    '--channels', channelsPath,
    '--allowed-signers', allowedSignersPath
  ]
  for (const [name, value] of [
    ['canary-evidence', options.canaryEvidence],
    ['gateway-window-state', options.gatewayWindowState],
    ['relays', options.relays],
    ['gateway-manifest', options.gatewayManifest]
  ]) {
    if (value) args.push(`--${name}`, value)
  }
  if (options.requirePublicGateway) args.push('--require-public-gateway')
  return args
}

function validatePromotionResult (result, options, channelsPath, dryRun) {
  if (!result || typeof result !== 'object' || Array.isArray(result) ||
      result.schema !== 'hiverelay-fleet-channel-promotion-v1' ||
      result.channel !== options.channel || result.target !== options.target ||
      path.resolve(result.channelsPath || '') !== channelsPath ||
      typeof result.wouldChange !== 'boolean' || typeof result.publicGatewayRequired !== 'boolean' ||
      !Array.isArray(result.operatorContracts) ||
      !RELEASE_TAG_PATTERN.test(result.previousTarget || '') ||
      !isObjectId(result.targetSha) || !isObjectId(result.tagObjectSha)) {
    throw new Error('promoter returned an invalid or drifted publication contract')
  }
  const wantedStatus = dryRun ? 'dry-run' : (result.wouldChange ? 'updated' : 'unchanged')
  if (result.status !== wantedStatus) throw new Error(`promoter returned unexpected status ${result.status || '(missing)'}`)
  for (const contract of result.operatorContracts) {
    if (!contract || typeof contract !== 'object' || Array.isArray(contract) ||
        !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(contract.relay || '') ||
        contract.path !== `fleet/public-hive-gateway-operators/${contract.relay}.json` ||
        !/^[a-f0-9]{64}$/.test(contract.sha256 || '')) {
      throw new Error('promoter returned an invalid public-t1 operator contract binding')
    }
  }
  return result
}

async function validateOperatorReadiness (options, promotion, git) {
  const expected = promotion.operatorContracts
  if (expected.length === 0) {
    if (options.gatewayOpsEvidenceDir) throw new Error('--gateway-ops-evidence-dir is valid only for a public-t1-gateway release')
    return []
  }
  if (!options.gatewayOpsEvidenceDir) {
    throw new Error('--gateway-ops-evidence-dir is required for a public-t1-gateway channel publication')
  }
  let evidenceDirectoryStat
  try {
    evidenceDirectoryStat = await lstat(options.gatewayOpsEvidenceDir)
  } catch {
    throw new Error('--gateway-ops-evidence-dir must be an existing non-symlink directory')
  }
  if (!evidenceDirectoryStat.isDirectory() || evidenceDirectoryStat.isSymbolicLink()) {
    throw new Error('--gateway-ops-evidence-dir must be an existing non-symlink directory')
  }
  const manifestBytes = await readTrustedHeadFile(promotion.targetSha, 'fleet/public-hive-gateway-release.json', git)
  const manifest = parseJsonObject(manifestBytes, 'trusted public gateway release manifest')
  const manifestSha256 = createHash('sha256').update(manifestBytes).digest('hex')
  const verified = []
  for (const binding of expected) {
    const contractBytes = await readTrustedHeadFile(promotion.targetSha, binding.path, git)
    const contract = parseJsonObject(contractBytes, `trusted operator contract ${binding.relay}`)
    const evidencePath = path.join(options.gatewayOpsEvidenceDir, `${binding.relay}.json`)
    const evidenceBytes = await readBoundedSingleLinkFile(evidencePath,
      `operator readiness evidence for ${binding.relay}`, 2 * 1024 * 1024)
    const evidence = parseJsonObject(evidenceBytes, `operator readiness evidence for ${binding.relay}`)
    const result = verifyPublicHiveGatewayOpsEvidence(evidence, {
      contract,
      manifest,
      releaseSha: promotion.targetSha,
      relay: binding.relay,
      expectedContractSha256: binding.sha256,
      contractFileSha256: createHash('sha256').update(contractBytes).digest('hex'),
      releaseManifestSha256: manifestSha256
    })
    if (result.operatorContractSha256 !== binding.sha256) {
      throw new Error(`passing ops evidence did not derive the signed operator contract digest for ${binding.relay}`)
    }
    verified.push({
      relay: binding.relay,
      operatorId: result.operatorId,
      registrableDomain: result.registrableDomain,
      suffix: result.suffix,
      operatorContractSha256: result.operatorContractSha256,
      certificateSpkiSha256: result.certificateSpkiSha256,
      expectedAddresses: result.expectedAddresses,
      checkedAt: result.checkedAt,
      evidenceSha256: createHash('sha256').update(evidenceBytes).digest('hex')
    })
  }
  return verified
}

async function validateEnabledGatewayPredecessor (promotion, git, allowedSignersPath, remoteEndpoint, remote) {
  if (promotion.operatorContracts.length === 0) return null
  const predecessor = promotion.previousTarget
  if (!RELEASE_TAG_PATTERN.test(predecessor || '')) {
    throw new Error('enabled public gateway publication has an invalid predecessor release tag')
  }
  const tagRef = `refs/tags/${predecessor}`
  const tagType = (await git(['cat-file', '-t', tagRef],
    `public gateway predecessor ${predecessor} is missing`)).stdout.trim()
  if (tagType !== 'tag') {
    throw new Error(`public gateway predecessor ${predecessor} must be an annotated signed tag`)
  }
  const signature = await git([
    '-c', 'gpg.format=ssh',
    '-c', `gpg.ssh.allowedSignersFile=${allowedSignersPath}`,
    '-c', `gpg.ssh.program=${SSH_KEYGEN_PROGRAM}`,
    'verify-tag', '--raw', predecessor
  ], `public gateway predecessor ${predecessor} is not signed by a trusted fleet signer`)
  if (!GOOD_SIGNATURE_PATTERN.test(`${signature.stdout}\n${signature.stderr}`)) {
    throw new Error(`public gateway predecessor ${predecessor} did not report a trusted signature`)
  }
  const tagObjectSha = normalizeObjectId((await git([
    'rev-parse', '--verify', `${tagRef}^{tag}`
  ], 'cannot resolve public gateway predecessor tag object')).stdout, 'public gateway predecessor tag object')
  const commitSha = normalizeObjectId((await git([
    'rev-parse', '--verify', `${tagRef}^{commit}`
  ], 'cannot resolve public gateway predecessor commit')).stdout, 'public gateway predecessor commit')
  await assertRemoteTagIdentity(git, remoteEndpoint, remote, predecessor, tagObjectSha, commitSha)

  const files = []
  for (const requirement of PUBLIC_GATEWAY_PREDECESSOR_AUTHORITY) {
    const target = await readTrustedAuthorityEntry(promotion.targetSha, requirement, git)
    const previous = await readTrustedAuthorityEntry(commitSha, requirement, git)
    if (target.oid !== previous.oid) {
      throw new Error(`enabled public gateway target requires predecessor ${predecessor} to contain blob-identical ${requirement.path}`)
    }
    files.push({ path: requirement.path, mode: requirement.mode, oid: target.oid })
  }
  return { target: predecessor, tagObjectSha, commitSha, files }
}

async function readTrustedAuthorityEntry (commit, requirement, git) {
  const entry = await git(['ls-tree', '-z', commit, '--', requirement.path],
    `cannot inspect public gateway bootstrap authority ${requirement.path}`)
  const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t([^\0]+)\0$/.exec(entry.stdout)
  if (!match || match[1] !== requirement.mode || match[3] !== requirement.path) {
    throw new Error(`public gateway bootstrap authority ${requirement.path} must be one tracked ${requirement.mode} regular blob`)
  }
  const blob = await git(['cat-file', 'blob', match[2]],
    `cannot read public gateway bootstrap authority ${requirement.path}`)
  const bytes = Buffer.from(blob.stdout)
  if (bytes.byteLength < 1 || bytes.byteLength > MAX_BOOTSTRAP_AUTHORITY_BYTES || bytes.includes(0)) {
    throw new Error(`public gateway bootstrap authority ${requirement.path} has invalid bounded source bytes`)
  }
  return { oid: match[2], bytes: bytes.byteLength }
}

async function assertRemoteTagIdentity (git, endpoint, remote, target, tagObjectSha, commitSha) {
  const tagRef = `refs/tags/${target}`
  const peeledRef = `${tagRef}^{}`
  const result = await git(['ls-remote', '--exit-code', '--', endpoint, tagRef, peeledRef],
    `cannot resolve public gateway predecessor ${target} on remote ${remote}`)
  const refs = new Map()
  for (const line of result.stdout.trim().split(/\r?\n/).filter(Boolean)) {
    const match = /^([a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/.exec(line)
    if (!match || refs.has(match[2])) throw new Error(`remote public gateway predecessor ${target} returned invalid refs`)
    refs.set(match[2], match[1])
  }
  if (refs.size !== 2 || refs.get(tagRef) !== tagObjectSha || refs.get(peeledRef) !== commitSha) {
    throw new Error(`remote public gateway predecessor ${target} does not match its trusted local tag`)
  }
}

async function assertCleanControlWorktree (git) {
  const status = await git(['status', '--porcelain=v1', '-z', '--untracked-files=all'],
    'cannot inspect control worktree status')
  if (status.stdout.length !== 0) {
    throw new Error('publisher requires a completely clean control worktree, including untracked files')
  }
  const tracked = splitNul((await git(['ls-files', '-v', '-z'],
    'cannot inspect tracked index flags')).stdout)
  if (tracked.some(entry => !entry.startsWith('H '))) {
    throw new Error('publisher forbids assume-unchanged, skip-worktree, and nonstandard tracked index flags')
  }
}

async function assertOnlyChannelsWorktreeChange (git) {
  const unstaged = splitNul((await git(['diff', '--name-only', '-z'],
    'cannot inspect channel worktree diff')).stdout)
  const staged = splitNul((await git(['diff', '--cached', '--name-only', '-z'],
    'cannot inspect staged files')).stdout)
  const untracked = splitNul((await git(['ls-files', '--others', '--exclude-standard', '-z'],
    'cannot inspect untracked files')).stdout)
  if (unstaged.length !== 1 || unstaged[0] !== CHANNELS_RELATIVE_PATH || staged.length !== 0 || untracked.length !== 0) {
    throw new Error(`promoter changed files outside ${CHANNELS_RELATIVE_PATH}; refusing publication`)
  }
}

async function assertOnlyChannelsStaged (git) {
  const staged = splitNul((await git(['diff', '--cached', '--name-only', '-z'],
    'cannot inspect staged channel update')).stdout)
  const unstaged = splitNul((await git(['diff', '--name-only', '-z'],
    'cannot inspect residual worktree changes')).stdout)
  const untracked = splitNul((await git(['ls-files', '--others', '--exclude-standard', '-z'],
    'cannot inspect residual untracked files')).stdout)
  if (staged.length !== 1 || staged[0] !== CHANNELS_RELATIVE_PATH || unstaged.length !== 0 || untracked.length !== 0) {
    throw new Error(`signed publication must stage only ${CHANNELS_RELATIVE_PATH}`)
  }
}

async function assertPublicationCommit (git) {
  const names = splitNul((await git([
    'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', 'HEAD'
  ], 'cannot inspect publication commit')).stdout)
  if (names.length !== 1 || names[0] !== CHANNELS_RELATIVE_PATH) {
    throw new Error(`publication commit contains files outside ${CHANNELS_RELATIVE_PATH}`)
  }
}

async function assertIndexEntry (git, expectedBlobOid) {
  const result = await git(['ls-files', '--stage', '-z', '--', CHANNELS_RELATIVE_PATH],
    'cannot inspect the staged channel entry')
  const expected = `100644 ${expectedBlobOid} 0\t${CHANNELS_RELATIVE_PATH}\0`
  if (result.stdout !== expected) {
    throw new Error('staged channel mode or bytes changed after validation')
  }
}

async function assertCommitEntry (git, expectedBlobOid) {
  const result = await git(['ls-tree', '-z', 'HEAD', '--', CHANNELS_RELATIVE_PATH],
    'cannot inspect the signed channel entry')
  const expected = `100644 blob ${expectedBlobOid}\t${CHANNELS_RELATIVE_PATH}\0`
  if (result.stdout !== expected) {
    throw new Error('signed publication commit contains an unvalidated channel mode or blob')
  }
}

async function resolveRemoteEndpoint (git, remote) {
  const fetchUrls = nonemptyLines((await git(['remote', 'get-url', '--all', remote],
    `Git remote ${remote} has no fetch URL`)).stdout)
  const pushUrls = nonemptyLines((await git(['remote', 'get-url', '--push', '--all', remote],
    `Git remote ${remote} has no push URL`)).stdout)
  if (fetchUrls.length !== 1 || pushUrls.length !== 1 || fetchUrls[0] !== pushUrls[0]) {
    throw new Error(`Git remote ${remote} must have exactly one identical fetch and push endpoint`)
  }
  if (!isSafeGitEndpoint(fetchUrls[0])) {
    throw new Error(`Git remote ${remote} endpoint must be one safe HTTPS, SSH, or absolute local URL`)
  }
  return fetchUrls[0]
}

async function assertNoGitUrlRewrites (git) {
  const result = await git(['config', '--null', '--list'], 'cannot inspect effective Git URL configuration')
  for (const field of splitNul(result.stdout)) {
    const separator = field.indexOf('\n')
    const key = (separator < 0 ? field : field.slice(0, separator)).toLowerCase()
    if (/^url\..*\.(?:insteadof|pushinsteadof)$/.test(key)) {
      throw new Error('Git URL rewrite configuration is forbidden for fleet channel publication')
    }
  }
}

async function readRemoteHead (git, endpoint, remote, branch) {
  const result = await git(['ls-remote', '--exit-code', '--', endpoint, `refs/heads/${branch}`],
    `cannot resolve ${remote}/${branch}`)
  const lines = result.stdout.trim().split(/\r?\n/).filter(Boolean)
  if (lines.length !== 1) throw new Error(`remote ${remote}/${branch} did not resolve exactly once`)
  const match = /^([a-f0-9]{40}|[a-f0-9]{64})\trefs\/heads\/(.+)$/.exec(lines[0])
  if (!match || match[2] !== branch) throw new Error(`remote ${remote}/${branch} returned an invalid ref`)
  return match[1]
}

async function assertRemoteReleaseTag (git, endpoint, remote, target, expected) {
  const tagRef = `refs/tags/${target}`
  const peeledRef = `${tagRef}^{}`
  const result = await git(['ls-remote', '--exit-code', '--', endpoint, tagRef, peeledRef],
    `cannot resolve immutable release tag ${target} on remote ${remote}`)
  const refs = new Map()
  for (const line of result.stdout.trim().split(/\r?\n/).filter(Boolean)) {
    const match = /^([a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/.exec(line)
    if (!match || refs.has(match[2])) throw new Error(`remote release tag ${target} returned invalid refs`)
    refs.set(match[2], match[1])
  }
  if (refs.size !== 2 || refs.get(tagRef) !== expected.tagObjectSha ||
      refs.get(peeledRef) !== expected.targetSha) {
    throw new Error(`remote release tag ${target} does not match the locally verified signed tag object and commit`)
  }
}

async function deriveExpectedChannels (file, channel, target, previousTarget, git) {
  const buffer = await readBoundedSingleLinkFile(file, 'fleet channels', MAX_CHANNELS_BYTES)
  let channels
  try {
    channels = JSON.parse(buffer.toString('utf8'))
  } catch {
    throw new Error('promoted fleet channels are not valid JSON')
  }
  if (!channels || typeof channels !== 'object' || Array.isArray(channels) ||
      channels[channel] !== previousTarget) {
    throw new Error(`starting fleet channels do not bind ${channel} to the promoter's previous target`)
  }
  const objectFormat = (await git(['rev-parse', '--show-object-format'],
    'cannot resolve Git object format')).stdout.trim()
  if (objectFormat !== 'sha1' && objectFormat !== 'sha256') {
    throw new Error('repository uses an unsupported Git object format')
  }
  const startingBlobOid = gitBlobOid(buffer, objectFormat)
  const startingEntry = await git(['ls-tree', '-z', 'HEAD', '--', CHANNELS_RELATIVE_PATH],
    'cannot inspect the starting channel entry')
  if (startingEntry.stdout !== `100644 blob ${startingBlobOid}\t${CHANNELS_RELATIVE_PATH}\0`) {
    throw new Error('starting channel file mode or bytes do not match the validated HEAD')
  }
  channels[channel] = target
  const expected = Buffer.from(JSON.stringify(channels, null, 2) + '\n')
  const blobOid = gitBlobOid(expected, objectFormat)
  return { blobOid }
}

function gitBlobOid (buffer, objectFormat) {
  const header = Buffer.from(`blob ${buffer.byteLength}\0`)
  return createHash(objectFormat).update(header).update(buffer).digest('hex')
}

function parseJsonObject (buffer, label) {
  let value
  try { value = JSON.parse(buffer.toString('utf8')) } catch { throw new Error(`${label} is not valid JSON`) }
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must contain a JSON object`)
  return value
}

async function pinAllowedSigners (source) {
  const buffer = await readBoundedSingleLinkFile(source, 'allowed signers', MAX_ALLOWED_SIGNERS_BYTES)
  const directory = await mkdtemp(path.join(tmpdir(), 'hiverelay-publisher-trust-'))
  const file = path.join(directory, 'allowed_signers')
  try {
    await writeFile(file, buffer, { flag: 'wx', mode: 0o600 })
  } catch (err) {
    await rm(directory, { recursive: true, force: true }).catch(() => {})
    throw err
  }
  return {
    path: file,
    cleanup: () => rm(directory, { recursive: true, force: true })
  }
}

async function readBoundedSingleLinkFile (file, label, maxBytes) {
  let handle
  try {
    if (typeof fsConstants.O_NOFOLLOW !== 'number') throw new Error('platform lacks O_NOFOLLOW')
    handle = await open(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW)
    const before = await handle.stat({ bigint: true })
    if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > BigInt(maxBytes)) {
      throw new Error(`${label} must be a bounded single-link regular file`)
    }
    const buffer = Buffer.allocUnsafe(maxBytes + 1)
    let length = 0
    while (length < buffer.length) {
      const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length)
      if (bytesRead === 0) break
      length += bytesRead
    }
    if (length > maxBytes) throw new Error(`${label} exceeds its read bound`)
    const after = await handle.stat({ bigint: true })
    if (!sameFileSnapshot(before, after) || BigInt(length) !== after.size) {
      throw new Error(`${label} changed while being read`)
    }
    return buffer.subarray(0, length)
  } catch (err) {
    if (err?.message?.startsWith(label) || err?.message === 'platform lacks O_NOFOLLOW') throw err
    throw new Error(`${label} must be a readable non-symlink file`)
  } finally {
    await handle?.close().catch(() => {})
  }
}

function sameFileSnapshot (left, right) {
  return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
    left.nlink === right.nlink && left.size === right.size && left.mtimeNs === right.mtimeNs &&
    left.ctimeNs === right.ctimeNs
}

async function runGit (repoRoot, args, failure) {
  try {
    return await execFileAsync(GIT_PROGRAM, [
      '--no-replace-objects',
      '-C', repoRoot,
      '-c', `core.hooksPath=${DISABLED_HOOKS_PATH}`,
      '-c', 'core.fsmonitor=false',
      '-c', 'http.sslVerify=true',
      '-c', 'protocol.ext.allow=never',
      ...args
    ], {
      encoding: 'utf8',
      env: hardenedGitEnv({
        GIT_GRAFT_FILE: '/dev/null/hiverelay-disabled',
        GIT_NO_REPLACE_OBJECTS: '1',
        GIT_OPTIONAL_LOCKS: '0'
      }),
      maxBuffer: MAX_PROCESS_OUTPUT_BYTES,
      timeout: 30_000
    })
  } catch {
    throw new Error(failure)
  }
}

async function runPromoterFromCommit (args, commit, git) {
  const snapshot = await pinPromoterCode(commit, git)
  try {
    return await runPromoter(args, snapshot.path)
  } finally {
    await snapshot.cleanup()
  }
}

async function pinPromoterCode (commit, git) {
  const files = await Promise.all([
    readTrustedHeadFile(commit, PROMOTER_RELATIVE_PATH, git),
    readTrustedHeadFile(commit, PROMOTER_LIBRARY_RELATIVE_PATH, git),
    readTrustedHeadFile(commit, PROMOTER_POLICY_RELATIVE_PATH, git)
  ])
  const directory = await mkdtemp(path.join(tmpdir(), 'hiverelay-publisher-code-'))
  const scriptsDirectory = path.join(directory, 'scripts')
  const libraryDirectory = path.join(scriptsDirectory, 'lib')
  const promoter = path.join(directory, PROMOTER_RELATIVE_PATH)
  try {
    await mkdir(libraryDirectory, { recursive: true, mode: 0o700 })
    await writeFile(promoter, files[0], { flag: 'wx', mode: 0o400 })
    await writeFile(path.join(directory, PROMOTER_LIBRARY_RELATIVE_PATH), files[1], { flag: 'wx', mode: 0o400 })
    await writeFile(path.join(directory, PROMOTER_POLICY_RELATIVE_PATH), files[2], { flag: 'wx', mode: 0o400 })
  } catch (err) {
    await rm(directory, { recursive: true, force: true }).catch(() => {})
    throw err
  }
  return {
    path: promoter,
    cleanup: () => rm(directory, { recursive: true, force: true })
  }
}

async function readTrustedHeadFile (commit, repoRelativePath, git) {
  const entry = await git(['ls-tree', '-z', commit, '--', repoRelativePath],
    `cannot inspect trusted ${repoRelativePath}`)
  const match = /^(100644) blob ([a-f0-9]{40}|[a-f0-9]{64})\t([^\0]+)\0$/.exec(entry.stdout)
  if (!match || match[3] !== repoRelativePath) {
    throw new Error(`trusted ${repoRelativePath} must be one non-executable regular blob`)
  }
  const blob = await git(['cat-file', 'blob', match[2]], `cannot read trusted ${repoRelativePath}`)
  const buffer = Buffer.from(blob.stdout)
  if (buffer.byteLength < 1 || buffer.byteLength > MAX_TRUSTED_PROMOTER_BYTES || buffer.includes(0)) {
    throw new Error(`trusted ${repoRelativePath} has invalid bounded source bytes`)
  }
  return buffer
}

async function runPromoter (args, trustedPromoterPath) {
  try {
    const result = await execFileAsync(process.execPath, [trustedPromoterPath, ...args], {
      encoding: 'utf8',
      env: hardenedGitEnv({
        GIT_GRAFT_FILE: '/dev/null/hiverelay-disabled',
        GIT_NO_REPLACE_OBJECTS: '1',
        GIT_OPTIONAL_LOCKS: '0'
      }),
      maxBuffer: MAX_PROCESS_OUTPUT_BYTES,
      timeout: 60_000
    })
    return JSON.parse(result.stdout)
  } catch (err) {
    const diagnostic = safeError(err?.stderr || err)
    throw new Error(`channel promoter rejected publication: ${diagnostic}`)
  }
}

function parseArgs (argv) {
  const out = {}
  const valueArgs = new Set([
    'channel',
    'target',
    'repo',
    'remote',
    'branch',
    'canary-evidence',
    'gateway-window-state',
    'relays',
    'allowed-signers',
    'gateway-manifest',
    'gateway-ops-evidence-dir'
  ])
  const booleanArgs = new Set(['publish', 'require-public-gateway', 'help'])
  const seen = new Set()
  for (let i = 0; i < argv.length; i++) {
    const raw = argv[i]
    if (raw === '-h') {
      if (seen.has('help')) throw new Error('duplicate --help')
      seen.add('help')
      out.help = true
      continue
    }
    if (!raw.startsWith('--')) throw new Error(`unexpected positional 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)
    const key = camel(name)
    if (booleanArgs.has(name)) {
      out[key] = true
      continue
    }
    const value = argv[++i]
    if (!value || value.startsWith('--')) throw new Error(`missing value for --${name}`)
    out[key] = value
  }
  return out
}

function splitNul (value) {
  if (!value) return []
  const fields = value.split('\0')
  if (fields.at(-1) === '') fields.pop()
  return fields
}

function nonemptyLines (value) {
  const lines = String(value).split(/\r?\n/)
  if (lines.at(-1) === '') lines.pop()
  return lines.filter(line => line.length > 0)
}

function normalizeObjectId (value, label) {
  const normalized = String(value || '').trim().toLowerCase()
  if (!isObjectId(normalized)) throw new Error(`invalid ${label}`)
  return normalized
}

function isObjectId (value) {
  return typeof value === 'string' && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value)
}

function isSafeBranch (value) {
  return typeof value === 'string' && REF_COMPONENT_PATTERN.test(value) &&
    !value.endsWith('.') && !value.endsWith('/') && !value.includes('@{') && !value.includes('//')
}

function isSafeGitEndpoint (value) {
  if (typeof value !== 'string' || value.length < 1 || value.length > 4096 ||
      value.startsWith('-') || hasControlChars(value) || /\s/.test(value)) return false
  if (path.isAbsolute(value)) return true
  if (/^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9.-]+:[A-Za-z0-9._~/-]+$/.test(value)) return true
  try {
    const parsed = new URL(value)
    if (!['https:', 'ssh:', 'file:'].includes(parsed.protocol) || parsed.password || parsed.search || parsed.hash) return false
    if (parsed.protocol === 'https:' && parsed.username) return false
    if (parsed.protocol === 'file:' && parsed.hostname && parsed.hostname !== 'localhost') return false
    return parsed.protocol !== 'file:' || path.isAbsolute(decodeURIComponent(parsed.pathname))
  } catch {
    return false
  }
}

function hasControlChars (value) {
  for (const character of String(value)) {
    const code = character.charCodeAt(0)
    if (code <= 31 || code === 127) return true
  }
  return false
}

function hardenedGitEnv (overrides) {
  const env = { ...process.env }
  for (const name of Object.keys(env)) {
    if (UNSAFE_GIT_ENV.has(name) || /^GIT_CONFIG_(?:KEY|VALUE)_\d+$/.test(name)) delete env[name]
  }
  return { ...env, ...overrides }
}

async function pathsReferToSameLocation (left, right) {
  try {
    return await realpath(left) === await realpath(right)
  } catch {
    return false
  }
}

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

function kebab (value) {
  return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`)
}

function safeError (err) {
  return String(err?.message || err || 'unknown error').replace(/[\r\n\0]/g, ' ').slice(0, 1000)
}