mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

1import { Agent } from '@atproto/api' 2import type { OAuthSession } from '@atproto/oauth-client-node' 3import { sql } from 'drizzle-orm' 4import { sshKey } from '../db/schema' 5import { useDb } from './db' 6import { encrypt } from './encryption' 7import { generateKeypair } from './ssh-keypair' 8 9const PUBKEY_LEXICON = 'sh.tangled.publicKey' 10 11/** 12 * Generate a per-install SSH keypair, write the public half to the user's PDS 13 * as a `sh.tangled.publicKey` record, and persist the encrypted private half 14 * + the resulting record key in the `ssh_key` table. 15 * 16 * If a row already exists for `(installation_id, did)` we no-op. Rotation is a 17 * separate, explicit dashboard action (commit 16-ish) that re-runs this with 18 * the existing record then deletes the old one. 19 */ 20export async function generateAndPublishKey(opts: { 21 oauthSession: OAuthSession 22 installationId: number 23 keyName?: string 24}): Promise<{ created: boolean }> { 25 const db = useDb() 26 const did = opts.oauthSession.did 27 28 const existing = await db.select({ id: sshKey.id }) 29 .from(sshKey) 30 .where(sql`${sshKey.installationId} = ${opts.installationId} AND ${sshKey.did} = ${did}`) 31 if (existing.length > 0) { 32 return { created: false } 33 } 34 35 const keyName = opts.keyName ?? `synchub.to/${opts.installationId}` 36 const keypair = generateKeypair(keyName) 37 38 // Publish to PDS first. If this fails, we surface the error and leave no 39 // half-state in the DB \u2014 the caller can retry. 40 const agent = new Agent(opts.oauthSession) 41 const result = await agent.com.atproto.repo.createRecord({ 42 repo: did, 43 collection: PUBKEY_LEXICON, 44 record: { 45 $type: PUBKEY_LEXICON, 46 key: keypair.publicKeyOpenSsh, 47 name: keyName, 48 createdAt: new Date().toISOString(), 49 }, 50 }) 51 52 // Extract the rkey from the returned at-uri (`at://<did>/<collection>/<rkey>`). 53 const rkey = result.data.uri.split('/').pop() 54 if (!rkey) { 55 throw new Error(`could not parse rkey from publicKey record uri: ${result.data.uri}`) 56 } 57 58 const { ciphertext, nonce } = encrypt(keypair.privateKeyPem) 59 await db.insert(sshKey).values({ 60 installationId: opts.installationId, 61 did, 62 publicKey: keypair.publicKeyOpenSsh, 63 privateKeyCiphertext: ciphertext, 64 privateKeyNonce: nonce, 65 tangledKeyRkey: rkey, 66 }) 67 68 return { created: true } 69} 70 71/** 72 * Rotate the SSH key for `(installationId, did)`. 73 * 74 * Delete the existing `sh.tangled.publicKey` PDS record (best-effort: if the 75 * record is already gone on the PDS we proceed), drop the local row, then 76 * fall through to `generateAndPublishKey` to mint a fresh key. Pushes 77 * already in flight with the old key will fail and get retried with the new 78 * one via the queue's normal backoff. 79 */ 80export async function rotateKey(opts: { 81 oauthSession: OAuthSession 82 installationId: number 83 keyName?: string 84}): Promise<{ created: boolean }> { 85 const db = useDb() 86 const did = opts.oauthSession.did 87 88 const existing = await db.select({ id: sshKey.id, rkey: sshKey.tangledKeyRkey }) 89 .from(sshKey) 90 .where(sql`${sshKey.installationId} = ${opts.installationId} AND ${sshKey.did} = ${did}`) 91 92 if (existing.length > 0) { 93 const row = existing[0]! 94 if (row.rkey) { 95 const agent = new Agent(opts.oauthSession) 96 try { 97 await agent.com.atproto.repo.deleteRecord({ 98 repo: did, 99 collection: PUBKEY_LEXICON, 100 rkey: row.rkey, 101 }) 102 } 103 catch (err) { 104 // If the record is already gone (404) we can safely continue; any 105 // other error means the PDS rejected the delete and we should bail 106 // rather than leave the user with two records. 107 const status = err && typeof err === 'object' && 'status' in err && typeof err.status === 'number' 108 ? err.status 109 : undefined 110 if (status !== 404) throw err 111 } 112 } 113 await db.delete(sshKey).where(sql`${sshKey.id} = ${row.id}`) 114 } 115 116 return generateAndPublishKey(opts) 117}