Something went wrong. Try again.
mirror your GitHub repos to tangled.org automatically synchub.to
mirror sync tangled github git
Something went wrong. Try again.
6.9 kB · 190 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191import { Agent } from '@atproto/api'import type { OAuthSession } from '@atproto/oauth-client-node'import { sql } from 'drizzle-orm'import { sshKey } from '../db/schema'import { useOAuthClient } from './atproto-oauth'import { useDb } from './db'import { encrypt } from './encryption'import { generateKeypair } from './ssh-keypair'
const PUBKEY_LEXICON = 'sh.tangled.publicKey'
/** HTTP status off an unknown thrown value, or undefined if it has none. */function errorStatus(err: unknown): number | undefined { return err && typeof err === 'object' && 'status' in err && typeof err.status === 'number' ? err.status : undefined}
/** * Delete one `sh.tangled.publicKey` record from `did`'s PDS, treating an * already-gone record (404) as success. Any other PDS error re-throws. */async function deletePubkeyRecord(session: OAuthSession, did: string, rkey: string): Promise<void> { const agent = new Agent(session) try { await agent.com.atproto.repo.deleteRecord({ repo: did, collection: PUBKEY_LEXICON, rkey }) } catch (err) { if (errorStatus(err) !== 404) throw err }}
/** * Restore `did`'s OAuth session and delete one `sh.tangled.publicKey` record, * never throwing: a 404 is already-gone, and any other failure (including a * session that can no longer be restored) is logged so the caller's cleanup * proceeds. */async function bestEffortRevoke(did: string, rkey: string, installationId: number): Promise<void> { const client = await useOAuthClient() try { const session = await client.restore(did) await deletePubkeyRecord(session, did, rkey) } catch (err) { console.error(`failed to revoke publicKey record for did ${did} (installation ${installationId})`, err) }}
/** * Generate a per-install SSH keypair, write the public half to the user's PDS * as a `sh.tangled.publicKey` record, and persist the encrypted private half * + the resulting record key in the `ssh_key` table. * * If a row already exists for `(installation_id, did)` we no-op. Rotation is a * separate, explicit dashboard action (`rotateKey`) that deletes the existing * record then re-runs this. */export async function generateAndPublishKey(opts: { oauthSession: OAuthSession installationId: number keyName?: string}): Promise<{ created: boolean }> { const db = useDb() const did = opts.oauthSession.did
const existing = await db.select({ id: sshKey.id }) .from(sshKey) .where(sql`${sshKey.installationId} = ${opts.installationId} AND ${sshKey.did} = ${did}`) if (existing.length > 0) { return { created: false } }
const keyName = opts.keyName ?? `synchub.to/${opts.installationId}` const keypair = generateKeypair(keyName)
// Publish to PDS first. If this fails, we surface the error and leave no // half-state in the DB; the caller can retry. const agent = new Agent(opts.oauthSession) const result = await agent.com.atproto.repo.createRecord({ repo: did, collection: PUBKEY_LEXICON, record: { $type: PUBKEY_LEXICON, key: keypair.publicKeyOpenSsh, name: keyName, createdAt: new Date().toISOString(), }, })
// Extract the rkey from the returned at-uri (`at://<did>/<collection>/<rkey>`). const rkey = result.data.uri.split('/').pop() if (!rkey) { throw new Error(`could not parse rkey from publicKey record uri: ${result.data.uri}`) }
const { ciphertext, nonce } = encrypt(keypair.privateKeyPem) await db.insert(sshKey).values({ installationId: opts.installationId, did, publicKey: keypair.publicKeyOpenSsh, privateKeyCiphertext: ciphertext, privateKeyNonce: nonce, tangledKeyRkey: rkey, })
return { created: true }}
/** * Rotate the SSH key for `(installationId, did)`. * * Delete the existing `sh.tangled.publicKey` PDS record (best-effort: if the * record is already gone on the PDS we proceed), drop the local row, then * fall through to `generateAndPublishKey` to mint a fresh key. Pushes * already in flight with the old key will fail and get retried with the new * one via the queue's normal backoff. */export async function rotateKey(opts: { oauthSession: OAuthSession installationId: number keyName?: string}): Promise<{ created: boolean }> { const db = useDb() const did = opts.oauthSession.did
const existing = await db.select({ id: sshKey.id, rkey: sshKey.tangledKeyRkey }) .from(sshKey) .where(sql`${sshKey.installationId} = ${opts.installationId} AND ${sshKey.did} = ${did}`)
if (existing.length > 0) { const row = existing[0]! if (row.rkey) { await deletePubkeyRecord(opts.oauthSession, did, row.rkey) } await db.delete(sshKey).where(sql`${sshKey.id} = ${row.id}`) }
return generateAndPublishKey(opts)}
/** * Delete every `sh.tangled.publicKey` record we published for an installation * from the owning user's PDS. * * Called on `installation.deleted` *before* the local cascade delete, so the * user isn't left with a dead key to clean up by hand. Restoring the OAuth * session can fail if the user revoked the app on their PDS before uninstalling * the GitHub App; in that case there's nothing for us to delete, so we swallow * the error and let the caller proceed with the local cascade. A 404 on the * delete itself is likewise treated as already-gone. */export async function revokeKeysForInstallation(installationId: number): Promise<void> { const db = useDb() const rows = await db.select({ did: sshKey.did, rkey: sshKey.tangledKeyRkey }) .from(sshKey) .where(sql`${sshKey.installationId} = ${installationId}`)
for (const row of rows) { if (!row.rkey) continue // eslint-disable-next-line no-await-in-loop -- one PDS session per row await bestEffortRevoke(row.did, row.rkey, installationId) }}
/** * Revoke the `sh.tangled.publicKey` PDS record for one `(installationId, did)` * pair and drop its local `ssh_key` row. * * Used when a re-bind displaces a DID from an installation: the displaced * DID's key is now dead for that account, so we revoke it from their PDS and * delete the local row. Best-effort on the PDS side (a 404 or a failed * session restore is logged, not fatal) so the re-bind always completes; the * local row is dropped regardless. */export async function revokeKeyForInstallationDid(installationId: number, did: string): Promise<void> { const db = useDb() const rows = await db.select({ id: sshKey.id, rkey: sshKey.tangledKeyRkey }) .from(sshKey) .where(sql`${sshKey.installationId} = ${installationId} AND ${sshKey.did} = ${did}`)
for (const row of rows) { if (row.rkey) { // eslint-disable-next-line no-await-in-loop -- one PDS session per row await bestEffortRevoke(did, row.rkey, installationId) } // eslint-disable-next-line no-await-in-loop -- sequential row deletes await db.delete(sshKey).where(sql`${sshKey.id} = ${row.id}`) }}