mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

1import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' 2import os from 'node:os' 3import path from 'node:path' 4import { sql } from 'drizzle-orm' 5import { sshKey } from '../db/schema' 6import { useDb } from './db' 7import { decrypt } from './encryption' 8import { pkcs8ToOpenSshPrivate } from './ssh-keypair' 9 10/** 11 * Materialise the install's SSH private key as an OpenSSH-format file on disk 12 * and return: 13 * - the `GIT_SSH_COMMAND` string to point `git` at it 14 * - a `cleanup()` callback that synchronously removes the temp dir 15 * 16 * The key file lives in `os.tmpdir()` with 0600 perms, has a random filename 17 * (collision-resistant for concurrent worker invocations on the same instance), 18 * and is removed in `cleanup()`. Callers must invoke `cleanup()` in a `finally` 19 * — leaking the key on disk is the worst failure mode here. 20 * 21 * Host key checking: tangled knots are addressed by hostname; v1 uses 22 * `StrictHostKeyChecking=accept-new` (TOFU) with a per-call empty known_hosts, 23 * which is effectively "trust the DNS for the configured knot". A future 24 * commit can ship pinned host keys for the canonical knots once we know what 25 * those are. 26 */ 27export async function loadSshCommandForInstall(installationId: number): Promise<{ 28 gitSshCommand: string 29 cleanup: () => void 30}> { 31 const db = useDb() 32 const rows = await db.select({ 33 privateKeyCiphertext: sshKey.privateKeyCiphertext, 34 privateKeyNonce: sshKey.privateKeyNonce, 35 }) 36 .from(sshKey) 37 .where(sql`${sshKey.installationId} = ${installationId}`) 38 .limit(1) 39 40 if (rows.length === 0) { 41 throw new Error(`no ssh key for installation ${installationId}`) 42 } 43 const row = rows[0]! 44 45 const pem = decrypt(row.privateKeyCiphertext, row.privateKeyNonce) 46 const openSsh = pkcs8ToOpenSshPrivate(pem, `synchub.to/${installationId}`) 47 48 // Distinct dir per call so concurrent pushes within one process don't race. 49 const dir = mkdtempSync(path.join(os.tmpdir(), 'synchub-ssh-')) 50 const keyPath = path.join(dir, 'id_ed25519') 51 const knownHostsPath = path.join(dir, 'known_hosts') 52 53 writeFileSync(keyPath, openSsh, { mode: 0o600 }) 54 chmodSync(keyPath, 0o600) 55 writeFileSync(knownHostsPath, '', { mode: 0o600 }) 56 57 const gitSshCommand = [ 58 'ssh', 59 '-i', shellQuote(keyPath), 60 '-o', `UserKnownHostsFile=${shellQuote(knownHostsPath)}`, 61 '-o', 'StrictHostKeyChecking=accept-new', 62 '-o', 'IdentitiesOnly=yes', 63 '-o', 'BatchMode=yes', 64 '-o', 'ConnectTimeout=15', 65 ].join(' ') 66 67 return { 68 gitSshCommand, 69 cleanup: () => { 70 try { 71 rmSync(dir, { recursive: true, force: true }) 72 } 73 catch { 74 // best-effort; the temp dir will be cleaned up on process restart. 75 } 76 }, 77 } 78} 79 80/** Minimal shell-quoting for paths inside GIT_SSH_COMMAND. */ 81function shellQuote(s: string): string { 82 // GIT_SSH_COMMAND is split on whitespace by git, so escape spaces. We don't 83 // bother with full shell-quoting here because the paths we generate (in 84 // os.tmpdir()) won't contain quotes/backslashes; this is defense in depth. 85 if (!/[\s"'\\]/.test(s)) return s 86 return `"${s.replace(/(["\\])/g, '\\$1')}"` 87}