OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / auth / ssh-setup.js

52 lines · javascript ref:
/**
 * Userland (same-account) SSH push setup — classic-git-auth Phase 2.
 *
 * In the same-account model the daemon runs as the operator's user and manages
 * a marked block in that user's OWN ~/.ssh/authorized_keys. The forced command
 * points at a tiny launcher in ~/.local/bin that the daemon self-installs here,
 * so there is no separate `git` account, no root, and no package-bin plumbing —
 * the wrapper runs as the daemon's user and reaches the runtime, socket, and
 * repos natively. See spec/flows/classic-git-auth.md.
 */

import fs from '#node:fs/promises'
import { dirname } from '#node:path'

/**
 * Idempotently install the pgv3-git-shell launcher: a `sh` shim that execs the
 * bare/node runtime on the wrapper script. Returns the launcher path to bake
 * into the authorized_keys forced command.
 *
 * @param {string} launcherPath - e.g. ~/.local/bin/pgv3-git-shell
 * @param {object} opts - { runtime, script }  (absolute paths)
 * @returns {Promise<{ launcherPath, runtime, script }>}
 */
export async function ensureGitShellLauncher (launcherPath, opts = {}) {
  const { runtime, script } = opts
  if (!launcherPath || !runtime || !script) {
    throw new Error('ensureGitShellLauncher requires launcherPath, runtime, script')
  }
  for (const v of [runtime, script]) {
    if (typeof v !== 'string' || /[\r\n"]/.test(v)) throw new Error('invalid runtime/script path')
  }

  const body =
    '#!/bin/sh\n' +
    '# managed-by=pear-git-v3 — pgv3-git-shell launcher (do not edit)\n' +
    'exec "' + runtime + '" "' + script + '" "$@"\n'

  await fs.mkdir(dirname(launcherPath), { recursive: true }).catch(() => {})

  // Only rewrite when changed (a redeploy moves the runtime/script paths).
  let current = null
  try { current = await fs.readFile(launcherPath, 'utf8') } catch {}
  if (current !== body) {
    const tmp = launcherPath + '.pgv3-tmp'
    await fs.writeFile(tmp, body, { mode: 0o755 })
    await fs.rename(tmp, launcherPath)
  }
  try { await fs.chmod(launcherPath, 0o755) } catch {}

  return { launcherPath, runtime, script }
}