import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, relative, resolve } from "node:path"; import { type UniDeskConfig, repoRoot, rootPath } from "./config"; import { runSshCommandCapture } from "./ssh"; export interface GithubSshIdentityDistribution { ok: boolean; detail: string; fingerprint: string | null; seededFromLocal: boolean; identityId?: string; targetId?: string; configTruth?: Record; raw?: unknown; } interface GithubSshIdentity { privateKey: string; publicKey: string; knownHosts: string; updatedAt: string | null; } interface DeploySshSourceRef { sourceRef: string; sourceKey: "file"; } interface DeploySshCreateFromLocal { enabled: boolean; privateKeyPath: string; publicKeyPath: string; knownHostsPath: string; } interface DeploySshIdentitySpec { id: string; host: string; privateKey: DeploySshSourceRef; publicKey: DeploySshSourceRef; knownHosts: DeploySshSourceRef; createFromLocal: DeploySshCreateFromLocal | null; } interface DeploySshTargetSpec { targetId: string; providerId: string; route: string; homeDir: string; egress: DeploySshEgressSpec; identities: string[]; } type DeploySshEgressSpec = | { mode: "direct" } | { mode: "http-connect-proxy"; proxyUrl: string }; interface DeploySshIdentityConfig { configPath: string; sourceRoot: string; defaults: { targetId: string; identityId: string; }; identities: Record; targets: Record; } interface ResolvedDeploySshIdentitySourcePaths { privateKeyPath: string; publicKeyPath: string; knownHostsPath: string; } interface SourceGithubIdentityResult { identity: GithubSshIdentity; seededFromLocal: boolean; } const deploySshIdentitiesConfigPath = "config/deploy-ssh-identities.yaml"; function asRecord(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`); return value as Record; } function optionalRecord(value: unknown, path: string): Record | null { if (value === undefined || value === null) return null; return asRecord(value, path); } function stringField(record: Record, key: string, path: string): string { const value = record[key]; if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return value.trim(); } function absolutePathField(record: Record, key: string, path: string): string { const value = stringField(record, key, path); if (!isAbsolute(value)) throw new Error(`${path}.${key} must be an absolute path`); return value; } function booleanField(record: Record, key: string, path: string): boolean { const value = record[key]; if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`); return value; } function stringArrayField(record: Record, key: string, path: string): string[] { const value = record[key]; if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) { throw new Error(`${path}.${key} must be a non-empty string array`); } return value.map((item) => item.trim()); } function sourceRefField(record: Record, key: string, path: string): DeploySshSourceRef { const value = asRecord(record[key], `${path}.${key}`); const sourceRef = stringField(value, "sourceRef", `${path}.${key}`); const sourceKey = stringField(value, "sourceKey", `${path}.${key}`); if (sourceKey !== "file") throw new Error(`${path}.${key}.sourceKey must be file`); if (isAbsolute(sourceRef) || sourceRef.split(/[\\/]+/u).includes("..")) { throw new Error(`${path}.${key}.sourceRef must be a relative path under sources.root`); } return { sourceRef, sourceKey }; } function parseDeploySshCreateFromLocal(value: unknown, path: string): DeploySshCreateFromLocal | null { const record = optionalRecord(value, path); if (record === null) return null; return { enabled: booleanField(record, "enabled", path), privateKeyPath: absolutePathField(record, "privateKeyPath", path), publicKeyPath: absolutePathField(record, "publicKeyPath", path), knownHostsPath: absolutePathField(record, "knownHostsPath", path), }; } function parseDeploySshIdentity(id: string, value: unknown): DeploySshIdentitySpec { const path = `${deploySshIdentitiesConfigPath}#identities.${id}`; const record = asRecord(value, path); return { id, host: stringField(record, "host", path), privateKey: sourceRefField(record, "privateKey", path), publicKey: sourceRefField(record, "publicKey", path), knownHosts: sourceRefField(record, "knownHosts", path), createFromLocal: parseDeploySshCreateFromLocal(record.createFromLocal, `${path}.createFromLocal`), }; } function parseDeploySshTarget(targetId: string, value: unknown): DeploySshTargetSpec { const path = `${deploySshIdentitiesConfigPath}#targets.${targetId}`; const record = asRecord(value, path); const egress = parseDeploySshEgress(record.egress, `${path}.egress`); return { targetId, providerId: stringField(record, "providerId", path), route: stringField(record, "route", path), homeDir: absolutePathField(record, "homeDir", path), egress, identities: stringArrayField(record, "identities", path), }; } function parseDeploySshEgress(value: unknown, path: string): DeploySshEgressSpec { const record = asRecord(value, path); const mode = stringField(record, "mode", path); if (mode === "direct") return { mode }; if (mode === "http-connect-proxy") return { mode, proxyUrl: stringField(record, "proxyUrl", path) }; throw new Error(`${path}.mode must be direct or http-connect-proxy`); } function loadDeploySshIdentityConfig(): DeploySshIdentityConfig { const parsed = Bun.YAML.parse(readFileSync(rootPath(deploySshIdentitiesConfigPath), "utf8")) as unknown; const root = asRecord(parsed, deploySshIdentitiesConfigPath); if (root.version !== 1) throw new Error(`${deploySshIdentitiesConfigPath}.version must be 1`); if (root.kind !== "UnideskDeploySshIdentities") throw new Error(`${deploySshIdentitiesConfigPath}.kind must be UnideskDeploySshIdentities`); const defaults = asRecord(root.defaults, `${deploySshIdentitiesConfigPath}.defaults`); const sources = asRecord(root.sources, `${deploySshIdentitiesConfigPath}.sources`); const identities = asRecord(root.identities, `${deploySshIdentitiesConfigPath}.identities`); const targets = asRecord(root.targets, `${deploySshIdentitiesConfigPath}.targets`); const config: DeploySshIdentityConfig = { configPath: deploySshIdentitiesConfigPath, sourceRoot: absolutePathField(sources, "root", `${deploySshIdentitiesConfigPath}.sources`), defaults: { targetId: stringField(defaults, "targetId", `${deploySshIdentitiesConfigPath}.defaults`), identityId: stringField(defaults, "identityId", `${deploySshIdentitiesConfigPath}.defaults`), }, identities: Object.fromEntries(Object.entries(identities).map(([id, value]) => [id, parseDeploySshIdentity(id, value)])), targets: Object.fromEntries(Object.entries(targets).map(([id, value]) => [id, parseDeploySshTarget(id, value)])), }; if (config.targets[config.defaults.targetId] === undefined) throw new Error(`${deploySshIdentitiesConfigPath}.defaults.targetId references missing target ${config.defaults.targetId}`); if (config.identities[config.defaults.identityId] === undefined) throw new Error(`${deploySshIdentitiesConfigPath}.defaults.identityId references missing identity ${config.defaults.identityId}`); for (const target of Object.values(config.targets)) { for (const identityId of target.identities) { if (config.identities[identityId] === undefined) throw new Error(`${deploySshIdentitiesConfigPath}#targets.${target.targetId}.identities references missing identity ${identityId}`); } } return config; } function selectDeploySshTarget(config: DeploySshIdentityConfig, selection: string | null | undefined): { target: DeploySshTargetSpec; defaulted: boolean } { const raw = selection === undefined || selection === null || selection.length === 0 ? config.defaults.targetId : selection; const target = Object.values(config.targets).find((candidate) => candidate.targetId.toLowerCase() === raw.toLowerCase() || candidate.providerId.toLowerCase() === raw.toLowerCase()); if (target === undefined) throw new Error(`${config.configPath} has no target ${raw}; known targets: ${Object.keys(config.targets).join(", ")}`); return { target, defaulted: raw === config.defaults.targetId && (selection === undefined || selection === null || selection.length === 0) }; } function selectDeploySshIdentity(config: DeploySshIdentityConfig, target: DeploySshTargetSpec, selection: string | null | undefined): { identity: DeploySshIdentitySpec; defaulted: boolean } { const raw = selection === undefined || selection === null || selection.length === 0 ? config.defaults.identityId : selection; const identity = config.identities[raw]; if (identity === undefined) throw new Error(`${config.configPath} has no identity ${raw}; known identities: ${Object.keys(config.identities).join(", ")}`); if (!target.identities.includes(identity.id)) throw new Error(`${config.configPath}#targets.${target.targetId}.identities does not include ${identity.id}`); return { identity, defaulted: raw === config.defaults.identityId && (selection === undefined || selection === null || selection.length === 0) }; } function sourceFilePath(config: DeploySshIdentityConfig, sourceRef: string): string { const sourceRoot = resolve(config.sourceRoot); const resolved = resolve(sourceRoot, sourceRef); const rel = relative(sourceRoot, resolved); if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`${deploySshIdentitiesConfigPath} sourceRef escapes sources.root: ${sourceRef}`); return resolved; } function sourcePaths(config: DeploySshIdentityConfig, identity: DeploySshIdentitySpec): ResolvedDeploySshIdentitySourcePaths { return { privateKeyPath: sourceFilePath(config, identity.privateKey.sourceRef), publicKeyPath: sourceFilePath(config, identity.publicKey.sourceRef), knownHostsPath: sourceFilePath(config, identity.knownHosts.sourceRef), }; } function configTruth(config: DeploySshIdentityConfig, target: DeploySshTargetSpec, identity: DeploySshIdentitySpec, targetDefaulted: boolean, identityDefaulted: boolean): Record { return { configPath: config.configPath, sourceRoot: config.sourceRoot, targetPath: `${config.configPath}#targets.${target.targetId}`, identityPath: `${config.configPath}#identities.${identity.id}`, defaults: { targetPath: `${config.configPath}#defaults.targetId`, identityPath: `${config.configPath}#defaults.identityId`, targetDefaulted, identityDefaulted, }, }; } function pgLiteral(value: string): string { return `'${value.replace(/'/gu, "''")}'`; } function commandOutput(command: string[], input?: string, timeoutMs = 30_000): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } { const result = spawnSync(command[0], command.slice(1), { cwd: repoRoot, input, encoding: "utf8", maxBuffer: 1024 * 1024 * 8, timeout: timeoutMs, }); return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? result.error?.message ?? "", exitCode: result.status, }; } function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } { return commandOutput([ "docker", "exec", "-i", "unidesk-database", "psql", "-v", "ON_ERROR_STOP=1", "-U", config.database.user, "-d", config.database.name, "-X", "-q", "-t", "-A", ], sql); } function ensureIdentityTableSql(): string { return ` CREATE TABLE IF NOT EXISTS unidesk_deploy_ssh_identities ( id TEXT PRIMARY KEY, host TEXT NOT NULL, private_key TEXT NOT NULL, public_key TEXT NOT NULL, public_key_fingerprint TEXT NOT NULL DEFAULT '', known_hosts TEXT NOT NULL DEFAULT '', source TEXT NOT NULL DEFAULT 'operator-local', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); `; } function publicKeyFingerprint(publicKey: string): string { const parts = publicKey.trim().split(/\s+/u); const encoded = parts[1] ?? ""; if (encoded.length === 0) return ""; const digest = createHash("sha256").update(Buffer.from(encoded, "base64")).digest("base64").replace(/=+$/u, ""); return `SHA256:${digest}`; } function githubKnownHostLinesFromText(text: string, hostName = "github.com"): string[] { const rows: string[] = []; const seen = new Set(); for (const rawLine of text.split(/\r?\n/u)) { const line = rawLine.trim(); if (line.length === 0 || line.startsWith("#")) continue; const parts = line.split(/\s+/u); if (parts.length < 3) continue; const hostList = parts[0]?.split(",") ?? []; if (!hostList.some((host) => host === hostName || host === `[${hostName}]:22`)) continue; if (seen.has(line)) continue; seen.add(line); rows.push(line); } return rows; } function readGithubKnownHosts(knownHostsPath: string, host: string): string { const localLines = existsSync(knownHostsPath) ? githubKnownHostLinesFromText(readFileSync(knownHostsPath, "utf8"), host) : []; if (localLines.length > 0) return `${localLines.join("\n")}\n`; const scan = commandOutput(["ssh-keyscan", "-t", "rsa,ecdsa,ed25519", host], undefined, 20_000); const scannedLines = githubKnownHostLinesFromText(scan.stdout, host); if (scannedLines.length === 0) throw new Error(scan.stderr || `failed to collect ${host} SSH host keys`); return `${scannedLines.join("\n")}\n`; } function readLocalGithubIdentity(identity: DeploySshIdentitySpec): GithubSshIdentity | null { const local = identity.createFromLocal; if (local === null || !local.enabled) return null; const privateKeyPath = local.privateKeyPath; if (!existsSync(privateKeyPath)) return null; const privateKey = readFileSync(privateKeyPath, "utf8"); if (!/-----BEGIN [A-Z ]*PRIVATE KEY-----/u.test(privateKey)) throw new Error(`invalid private key format: ${privateKeyPath}`); const publicKeyPath = local.publicKeyPath; const publicKey = existsSync(publicKeyPath) ? readFileSync(publicKeyPath, "utf8").trim() : commandOutput(["ssh-keygen", "-y", "-f", privateKeyPath]).stdout.trim(); if (!/^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256)\s+\S+/u.test(publicKey)) throw new Error(`invalid public key for ${privateKeyPath}`); return { privateKey: privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`, publicKey, knownHosts: readGithubKnownHosts(local.knownHostsPath, identity.host), updatedAt: null, }; } function writeSourceFileIfMissing(path: string, value: string, mode: number): boolean { if (existsSync(path)) return false; mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); writeFileSync(path, value, { mode }); chmodSync(path, mode); return true; } function ensureSourceGithubIdentity(config: DeploySshIdentityConfig, identitySpec: DeploySshIdentitySpec): SourceGithubIdentityResult { const paths = sourcePaths(config, identitySpec); let seededFromLocal = false; if (!existsSync(paths.privateKeyPath) || !existsSync(paths.publicKeyPath) || !existsSync(paths.knownHostsPath)) { const localIdentity = readLocalGithubIdentity(identitySpec); if (localIdentity !== null) { const seeded = [ writeSourceFileIfMissing(paths.privateKeyPath, localIdentity.privateKey, 0o600), writeSourceFileIfMissing(paths.publicKeyPath, `${localIdentity.publicKey.trim()}\n`, 0o644), writeSourceFileIfMissing(paths.knownHostsPath, localIdentity.knownHosts, 0o600), ]; seededFromLocal = seeded.some(Boolean); } } if (!existsSync(paths.privateKeyPath)) throw new Error(`${config.configPath}#identities.${identitySpec.id}.privateKey sourceRef is missing`); if (!existsSync(paths.publicKeyPath)) throw new Error(`${config.configPath}#identities.${identitySpec.id}.publicKey sourceRef is missing`); if (!existsSync(paths.knownHostsPath)) throw new Error(`${config.configPath}#identities.${identitySpec.id}.knownHosts sourceRef is missing`); const privateKey = readFileSync(paths.privateKeyPath, "utf8"); const publicKey = readFileSync(paths.publicKeyPath, "utf8").trim(); const knownHosts = readFileSync(paths.knownHostsPath, "utf8"); if (!/-----BEGIN [A-Z ]*PRIVATE KEY-----/u.test(privateKey)) throw new Error(`${config.configPath}#identities.${identitySpec.id}.privateKey sourceRef has invalid private key material`); if (!/^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256)\s+\S+/u.test(publicKey)) throw new Error(`${config.configPath}#identities.${identitySpec.id}.publicKey sourceRef has invalid public key material`); if (githubKnownHostLinesFromText(knownHosts, identitySpec.host).length === 0) throw new Error(`${config.configPath}#identities.${identitySpec.id}.knownHosts sourceRef has no ${identitySpec.host} rows`); const derived = commandOutput(["ssh-keygen", "-y", "-f", paths.privateKeyPath]); if (!derived.ok) throw new Error(`${config.configPath}#identities.${identitySpec.id}.privateKey failed ssh-keygen verification`); if (derived.stdout.trim() !== publicKey) throw new Error(`${config.configPath}#identities.${identitySpec.id}.privateKey does not match publicKey`); return { identity: { privateKey: privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`, publicKey, knownHosts: knownHosts.endsWith("\n") ? knownHosts : `${knownHosts}\n`, updatedAt: null, }, seededFromLocal, }; } function upsertGithubIdentity(config: UniDeskConfig, identitySpec: DeploySshIdentitySpec, identity: GithubSshIdentity): void { const fingerprint = publicKeyFingerprint(identity.publicKey); const sql = ` ${ensureIdentityTableSql()} INSERT INTO unidesk_deploy_ssh_identities (id, host, private_key, public_key, public_key_fingerprint, known_hosts, source, updated_at) VALUES ( ${pgLiteral(identitySpec.id)}, ${pgLiteral(identitySpec.host)}, ${pgLiteral(identity.privateKey)}, ${pgLiteral(identity.publicKey)}, ${pgLiteral(fingerprint)}, ${pgLiteral(identity.knownHosts)}, ${pgLiteral(`${deploySshIdentitiesConfigPath}#identities.${identitySpec.id}`)}, now() ) ON CONFLICT (id) DO UPDATE SET host = EXCLUDED.host, private_key = EXCLUDED.private_key, public_key = EXCLUDED.public_key, public_key_fingerprint = EXCLUDED.public_key_fingerprint, known_hosts = EXCLUDED.known_hosts, source = EXCLUDED.source, updated_at = now(); `; const result = runPsql(config, sql); if (!result.ok) throw new Error(`failed to upsert GitHub SSH identity in PostgreSQL: ${result.stderr || `exit=${result.exitCode}`}`); } function readGithubIdentityFromDatabase(config: UniDeskConfig, identitySpec: DeploySshIdentitySpec): GithubSshIdentity { const sql = ` ${ensureIdentityTableSql()} SELECT json_build_object( 'privateKey', private_key, 'publicKey', public_key, 'knownHosts', known_hosts, 'updatedAt', updated_at )::text FROM unidesk_deploy_ssh_identities WHERE id = ${pgLiteral(identitySpec.id)} LIMIT 1; `; const result = runPsql(config, sql); if (!result.ok) throw new Error(`failed to read GitHub SSH identity from PostgreSQL: ${result.stderr || `exit=${result.exitCode}`}`); const line = result.stdout.trim().split(/\r?\n/u).find((item) => item.trim().startsWith("{")); if (line === undefined) throw new Error("GitHub SSH identity is missing in PostgreSQL"); const parsed = JSON.parse(line) as Partial; const privateKey = typeof parsed.privateKey === "string" ? parsed.privateKey : ""; const publicKey = typeof parsed.publicKey === "string" ? parsed.publicKey : ""; const knownHosts = typeof parsed.knownHosts === "string" ? parsed.knownHosts : ""; if (!/-----BEGIN [A-Z ]*PRIVATE KEY-----/u.test(privateKey)) throw new Error("PostgreSQL GitHub SSH identity has invalid private key material"); if (!/^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256)\s+\S+/u.test(publicKey)) throw new Error("PostgreSQL GitHub SSH identity has invalid public key material"); if (githubKnownHostLinesFromText(knownHosts, identitySpec.host).length === 0) throw new Error(`PostgreSQL GitHub SSH identity has no ${identitySpec.host} known_hosts rows`); return { privateKey: privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`, publicKey, knownHosts: knownHosts.endsWith("\n") ? knownHosts : `${knownHosts}\n`, updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : null, }; } function remoteInstallPythonSource(): string { return String.raw` import json import os import pathlib import stat import subprocess import sys data = json.load(sys.stdin) home_dir = pathlib.Path(str(data.get("homeDir") or os.environ.get("HOME") or "/home/ubuntu")) host = str(data.get("host") or "github.com") ssh_dir = home_dir / ".ssh" private_key = str(data.get("privateKey") or "") public_key = str(data.get("publicKey") or "").strip() known_hosts = str(data.get("knownHosts") or "") home_stat = home_dir.stat() owner_uid = home_stat.st_uid owner_gid = home_stat.st_gid def chown_to_home_owner(path): try: os.chown(path, owner_uid, owner_gid) except PermissionError: if os.geteuid() == 0: raise if "PRIVATE KEY-----" not in private_key: raise SystemExit("invalid private key payload") if not public_key.startswith(("ssh-ed25519 ", "ssh-rsa ", "ecdsa-sha2-nistp256 ")): raise SystemExit("invalid public key payload") ssh_dir.mkdir(mode=0o700, parents=True, exist_ok=True) os.chmod(ssh_dir, 0o700) chown_to_home_owner(ssh_dir) private_path = ssh_dir / "id_ed25519" public_path = ssh_dir / "id_ed25519.pub" known_hosts_path = ssh_dir / "known_hosts" private_path.write_text(private_key if private_key.endswith("\n") else private_key + "\n") public_path.write_text(public_key + "\n") os.chmod(private_path, stat.S_IRUSR | stat.S_IWUSR) os.chmod(public_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) chown_to_home_owner(private_path) chown_to_home_owner(public_path) derived = subprocess.run(["ssh-keygen", "-y", "-f", str(private_path)], text=True, capture_output=True) if derived.returncode != 0: raise SystemExit("installed private key failed ssh-keygen verification") if derived.stdout.strip() != public_key: raise SystemExit("installed private key does not match public key") known_hosts_path.touch(mode=0o600, exist_ok=True) chown_to_home_owner(known_hosts_path) subprocess.run(["ssh-keygen", "-R", host, "-f", str(known_hosts_path)], text=True, capture_output=True) subprocess.run(["ssh-keygen", "-R", f"[{host}]:22", "-f", str(known_hosts_path)], text=True, capture_output=True) existing = known_hosts_path.read_text().splitlines() rows = [] seen = set() for line in existing + known_hosts.splitlines(): line = line.strip() if not line or line.startswith("#"): continue parts = line.split() if len(parts) < 3: continue if line in seen: continue seen.add(line) rows.append(line) known_hosts_path.write_text("\n".join(rows) + "\n") os.chmod(known_hosts_path, stat.S_IRUSR | stat.S_IWUSR) chown_to_home_owner(known_hosts_path) print(f"github_ssh_identity_written path={private_path} known_hosts_rows={len(rows)}") `; } function shellQuote(value: string): string { return `'${value.replace(/'/gu, `'\\''`)}'`; } export function gitSshHttpConnectProxySource(): string { return String.raw`#!/usr/bin/env python3 import os import select import socket import sys from urllib.parse import urlparse if len(sys.argv) != 3: raise SystemExit("usage: unidesk-git-ssh-http-connect.py host port") target_host = sys.argv[1] target_port = int(sys.argv[2]) proxy = urlparse(os.environ.get("UNIDESK_GIT_SSH_HTTP_PROXY", "http://127.0.0.1:18789")) proxy_host = proxy.hostname or "127.0.0.1" proxy_port = proxy.port or 80 sock = socket.create_connection((proxy_host, proxy_port), timeout=20) sock.sendall(f"CONNECT {target_host}:{target_port} HTTP/1.1\r\nHost: {target_host}:{target_port}\r\n\r\n".encode("ascii")) header = b"" while b"\r\n\r\n" not in header: chunk = sock.recv(4096) if not chunk: raise SystemExit("proxy closed before CONNECT response") header += chunk head, rest = header.split(b"\r\n\r\n", 1) if not (head.startswith(b"HTTP/1.1 200") or head.startswith(b"HTTP/1.0 200")): sys.stderr.write(head.decode("latin1", "replace") + "\n") raise SystemExit(1) if rest: os.write(1, rest) stdin_open = True sock.setblocking(False) while True: readers = [sock] if stdin_open: readers.append(sys.stdin.buffer) ready, _, _ = select.select(readers, [], []) if sock in ready: try: data = sock.recv(65536) except BlockingIOError: data = b"" if not data: break os.write(1, data) if stdin_open and sys.stdin.buffer in ready: data = os.read(0, 65536) if data: sock.sendall(data) else: stdin_open = False try: sock.shutdown(socket.SHUT_WR) except OSError: pass `; } async function distributeGithubIdentity(config: UniDeskConfig, target: DeploySshTargetSpec, identitySpec: DeploySshIdentitySpec, identity: GithubSshIdentity): Promise<{ ok: boolean; detail: string; raw: unknown }> { const payload = JSON.stringify({ privateKey: identity.privateKey, publicKey: identity.publicKey, knownHosts: identity.knownHosts, homeDir: target.homeDir, host: identitySpec.host, }); const remotePython = remoteInstallPythonSource(); const proxyPython = gitSshHttpConnectProxySource(); const remoteCommand = [ "set -euo pipefail", `python3 -c ${shellQuote(remotePython)}`, `home_dir=${shellQuote(target.homeDir)}`, "export HOME=\"$home_dir\"", `host=${shellQuote(identitySpec.host)}`, `egress_mode=${shellQuote(target.egress.mode)}`, `proxy_url=${target.egress.mode === "http-connect-proxy" ? shellQuote(target.egress.proxyUrl) : "''"}`, "ssh_dir=\"$HOME/.ssh\"", "git_ssh_proxy=\"$ssh_dir/unidesk-git-ssh-http-connect.py\"", "ssh_config=\"$ssh_dir/config\"", "if [ \"$egress_mode\" = \"http-connect-proxy\" ]; then", " curl -fsSL --max-time 20 -x \"$proxy_url\" \"https://$host\" -o /dev/null", "else", " curl -fsSL --max-time 20 \"https://$host\" -o /dev/null", "fi", "tmp_config=$(mktemp)", "touch \"$ssh_config\"", "chmod 600 \"$ssh_config\"", "awk '/^# BEGIN unidesk managed github-ssh-identity$/{skip=1; next} /^# END unidesk managed github-ssh-identity$/{skip=0; next} !skip{print}' \"$ssh_config\" > \"$tmp_config\"", "if [ \"$egress_mode\" = \"http-connect-proxy\" ]; then", " cat > \"$git_ssh_proxy\" <<'UNIDESK_GIT_SSH_PROXY'", proxyPython, "UNIDESK_GIT_SSH_PROXY", " chmod 700 \"$git_ssh_proxy\"", " {", " printf '%s\\n' '# BEGIN unidesk managed github-ssh-identity'", " printf 'Host %s\\n' \"$host\"", " printf ' HostName %s\\n' \"$host\"", " printf '%s\\n' ' User git'", " printf ' IdentityFile %s/.ssh/id_ed25519\\n' \"$HOME\"", " printf '%s\\n' ' IdentitiesOnly yes'", " printf '%s\\n' ' BatchMode yes'", " printf '%s\\n' ' StrictHostKeyChecking yes'", " printf ' UserKnownHostsFile %s/.ssh/known_hosts\\n' \"$HOME\"", " printf ' ProxyCommand env UNIDESK_GIT_SSH_HTTP_PROXY=%s %s %%h %%p\\n' \"$proxy_url\" \"$git_ssh_proxy\"", " printf '%s\\n' '# END unidesk managed github-ssh-identity'", " cat \"$tmp_config\"", " } > \"$ssh_config\"", "else", " {", " printf '%s\\n' '# BEGIN unidesk managed github-ssh-identity'", " printf 'Host %s\\n' \"$host\"", " printf ' HostName %s\\n' \"$host\"", " printf '%s\\n' ' User git'", " printf ' IdentityFile %s/.ssh/id_ed25519\\n' \"$HOME\"", " printf '%s\\n' ' IdentitiesOnly yes'", " printf '%s\\n' ' BatchMode yes'", " printf '%s\\n' ' StrictHostKeyChecking yes'", " printf ' UserKnownHostsFile %s/.ssh/known_hosts\\n' \"$HOME\"", " printf '%s\\n' '# END unidesk managed github-ssh-identity'", " cat \"$tmp_config\"", " } > \"$ssh_config\"", "fi", "rm -f \"$tmp_config\"", "chmod 600 \"$ssh_config\"", "auth_output=$(ssh -F \"$ssh_config\" -T \"git@$host\" 2>&1 || true)", "printf '%s\\n' \"$auth_output\"", "printf '%s\\n' \"$auth_output\" | grep -q 'successfully authenticated'", ].join("\n"); const result = await runSshCommandCapture(config, target.route, ["argv", "bash", "-lc", remoteCommand], payload); const stdout = result.stdout ?? ""; const stderr = result.stderr ?? ""; return { ok: result.exitCode === 0, detail: [stdout, stderr].filter(Boolean).join("\n").slice(-2000), raw: { targetId: target.targetId, providerId: target.providerId, route: target.route, homeDir: target.homeDir, egress: target.egress, identityId: identitySpec.id, host: identitySpec.host, exitCode: result.exitCode, stdoutTail: stdout.slice(-1200), stderrTail: stderr.slice(-1200), valuesRedacted: true, }, }; } function deploySshIdentitySourceStatus(config: DeploySshIdentityConfig, identity: DeploySshIdentitySpec): Record { const paths = sourcePaths(config, identity); const sourceStatus = (kind: string, sourceRef: string, path: string): Record => { const present = existsSync(path); return { kind, sourceRef, sourceKey: "file", present, bytes: present ? statSync(path).size : 0, }; }; const publicKey = existsSync(paths.publicKeyPath) ? readFileSync(paths.publicKeyPath, "utf8").trim() : ""; return { privateKey: sourceStatus("privateKey", identity.privateKey.sourceRef, paths.privateKeyPath), publicKey: { ...sourceStatus("publicKey", identity.publicKey.sourceRef, paths.publicKeyPath), fingerprint: publicKey.length > 0 ? publicKeyFingerprint(publicKey) : null, }, knownHosts: { ...sourceStatus("knownHosts", identity.knownHosts.sourceRef, paths.knownHostsPath), hostRows: existsSync(paths.knownHostsPath) ? githubKnownHostLinesFromText(readFileSync(paths.knownHostsPath, "utf8"), identity.host).length : 0, }, }; } export function deploySshIdentityPlan(targetSelection: string | null | undefined, identitySelection: string | null | undefined = null): Record { const deployConfig = loadDeploySshIdentityConfig(); const { target, defaulted: targetDefaulted } = selectDeploySshTarget(deployConfig, targetSelection); const { identity, defaulted: identityDefaulted } = selectDeploySshIdentity(deployConfig, target, identitySelection); const sourceStatus = deploySshIdentitySourceStatus(deployConfig, identity); const allSourcesPresent = [sourceStatus.privateKey, sourceStatus.publicKey, sourceStatus.knownHosts].every((item) => asRecord(item, "sourceStatus").present === true); return { ok: true, action: "deploy-ssh-identity-plan", mutation: false, identity: { id: identity.id, host: identity.host, sourceStatus, createFromLocal: identity.createFromLocal === null ? null : { enabled: identity.createFromLocal.enabled, privateKeyPath: identity.createFromLocal.privateKeyPath, publicKeyPath: identity.createFromLocal.publicKeyPath, knownHostsPath: identity.createFromLocal.knownHostsPath, }, }, target: { targetId: target.targetId, providerId: target.providerId, route: target.route, homeDir: target.homeDir, egress: target.egress, }, ready: allSourcesPresent || identity.createFromLocal?.enabled === true, configTruth: configTruth(deployConfig, target, identity, targetDefaulted, identityDefaulted), valuesRedacted: true, next: { ensure: `bun scripts/cli.ts ci github-ssh-identity ensure --target ${target.targetId} --identity ${identity.id} --confirm`, }, }; } export async function ensureGithubSshIdentityForProvider(config: UniDeskConfig, providerId: string, identitySelection: string | null | undefined = null): Promise { const deployConfig = loadDeploySshIdentityConfig(); const { target, defaulted: targetDefaulted } = selectDeploySshTarget(deployConfig, providerId); const { identity: identitySpec, defaulted: identityDefaulted } = selectDeploySshIdentity(deployConfig, target, identitySelection); const source = ensureSourceGithubIdentity(deployConfig, identitySpec); upsertGithubIdentity(config, identitySpec, source.identity); const identity = readGithubIdentityFromDatabase(config, identitySpec); const fingerprint = publicKeyFingerprint(identity.publicKey); const distribution = await distributeGithubIdentity(config, target, identitySpec, identity); if (!distribution.ok) { return { ok: false, detail: `failed to distribute GitHub SSH identity from YAML sourceRef via PostgreSQL to ${target.targetId}: ${distribution.detail || "remote ssh failed"}`, fingerprint, seededFromLocal: source.seededFromLocal, identityId: identitySpec.id, targetId: target.targetId, configTruth: configTruth(deployConfig, target, identitySpec, targetDefaulted, identityDefaulted), raw: distribution.raw, }; } return { ok: true, detail: `GitHub SSH identity distributed from YAML sourceRef via PostgreSQL to ${target.targetId}; fingerprint=${fingerprint}; seededFromLocal=${source.seededFromLocal}`, fingerprint, seededFromLocal: source.seededFromLocal, identityId: identitySpec.id, targetId: target.targetId, configTruth: configTruth(deployConfig, target, identitySpec, targetDefaulted, identityDefaulted), raw: distribution.raw, }; }