mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

chore: lint

+109 -45
+1 -1
nuxt.config.ts
··· 23 23 atprotoPrivateJwk: '', 24 24 public: { 25 25 url: '', 26 - } 26 + }, 27 27 }, 28 28 typescript: { 29 29 nodeTsConfig: {
+4 -2
server/utils/atproto-oauth.ts
··· 114 114 const rows = await db.select().from(atprotoState).where(sql`${atprotoState.key} = ${key}`) 115 115 if (rows.length === 0) return undefined 116 116 const row = rows[0]! 117 - return JSON.parse(decrypt(row.valueCiphertext, row.valueNonce)) as NodeSavedState 117 + const parsed: NodeSavedState = JSON.parse(decrypt(row.valueCiphertext, row.valueNonce)) 118 + return parsed 118 119 }, 119 120 async del(key: string) { 120 121 const db = useDb() ··· 142 143 const rows = await db.select().from(atprotoSession).where(sql`${atprotoSession.sub} = ${sub}`) 143 144 if (rows.length === 0) return undefined 144 145 const row = rows[0]! 145 - return JSON.parse(decrypt(row.valueCiphertext, row.valueNonce)) as NodeSavedSession 146 + const parsed: NodeSavedSession = JSON.parse(decrypt(row.valueCiphertext, row.valueNonce)) 147 + return parsed 146 148 }, 147 149 async del(sub: string) { 148 150 const db = useDb()
+53 -4
server/utils/job-handlers.ts
··· 56 56 page: number 57 57 } 58 58 59 + function asObject(value: unknown): Record<string, unknown> { 60 + if (value === null || typeof value !== 'object') { 61 + throw new TypeError(`expected object payload, got ${typeof value}`) 62 + } 63 + return { ...value } 64 + } 65 + 66 + function publishPubkeyPayload(value: unknown): PublishPubkeyPayload { 67 + const o = asObject(value) 68 + if (typeof o.did !== 'string' || typeof o.installationId !== 'number') { 69 + throw new TypeError('invalid atproto.publish-pubkey payload') 70 + } 71 + return { did: o.did, installationId: o.installationId } 72 + } 73 + 74 + function createRepoPayload(value: unknown): CreateRepoPayload { 75 + const o = asObject(value) 76 + if (typeof o.installationId !== 'number' || typeof o.githubRepoId !== 'number') { 77 + throw new TypeError('invalid tangled.create-repo payload') 78 + } 79 + return { installationId: o.installationId, githubRepoId: o.githubRepoId } 80 + } 81 + 82 + function backfillInstallationPayload(value: unknown): BackfillInstallationPayload { 83 + const o = asObject(value) 84 + if (typeof o.installationId !== 'number' || typeof o.page !== 'number') { 85 + throw new TypeError('invalid tangled.backfill-installation payload') 86 + } 87 + return { installationId: o.installationId, page: o.page } 88 + } 89 + 90 + function installationRepositoriesPayload(value: unknown): InstallationRepositoriesPayload { 91 + const o = asObject(value) 92 + if ( 93 + typeof o.installationId !== 'number' 94 + || (o.action !== 'added' && o.action !== 'removed') 95 + || !Array.isArray(o.addedRepoIds) 96 + || !Array.isArray(o.removedRepoIds) 97 + ) { 98 + throw new TypeError('invalid github.installation_repositories payload') 99 + } 100 + return { 101 + installationId: o.installationId, 102 + action: o.action, 103 + addedRepoIds: o.addedRepoIds.filter((id): id is number => typeof id === 'number'), 104 + removedRepoIds: o.removedRepoIds.filter((id): id is number => typeof id === 'number'), 105 + } 106 + } 107 + 59 108 export async function dispatch(envelope: JobEnvelope): Promise<void> { 60 109 if (!KNOWN_KINDS.has(envelope.kind)) { 61 110 throw new Error(`unknown job kind: ${envelope.kind}`) 62 111 } 63 112 64 113 if (envelope.kind === 'atproto.publish-pubkey') { 65 - const { did, installationId } = envelope.payload as PublishPubkeyPayload 114 + const { did, installationId } = publishPubkeyPayload(envelope.payload) 66 115 const client = await useOAuthClient() 67 116 const session = await client.restore(did) 68 117 await generateAndPublishKey({ oauthSession: session, installationId }) ··· 70 119 } 71 120 72 121 if (envelope.kind === 'tangled.create-repo') { 73 - const { installationId, githubRepoId } = envelope.payload as CreateRepoPayload 122 + const { installationId, githubRepoId } = createRepoPayload(envelope.payload) 74 123 75 124 // Find the user identity bound to this install. If OAuth hasn't completed 76 125 // yet, drop this job silently \u2014 OAuth callback re-enqueues for all ··· 88 137 } 89 138 90 139 if (envelope.kind === 'tangled.backfill-installation') { 91 - const { installationId, page } = envelope.payload as BackfillInstallationPayload 140 + const { installationId, page } = backfillInstallationPayload(envelope.payload) 92 141 const octokit = await installationOctokit(installationId) 93 142 const { data } = await octokit.request('GET /installation/repositories', { 94 143 per_page: BACKFILL_PAGE_SIZE, ··· 112 161 } 113 162 114 163 if (envelope.kind === 'github.installation_repositories') { 115 - const { installationId, action, addedRepoIds } = envelope.payload as InstallationRepositoriesPayload 164 + const { installationId, action, addedRepoIds } = installationRepositoriesPayload(envelope.payload) 116 165 if (action !== 'added') return 117 166 118 167 // Fan out one tangled.create-repo job per added repo. The fan-out keeps
+7 -5
server/utils/queue.ts
··· 2 2 import { job } from '../db/schema' 3 3 import { useDb } from './db' 4 4 5 - export interface JobEnvelope { 5 + export interface JobEnvelope extends Record<string, unknown> { 6 6 id: number 7 7 kind: string 8 8 payload: unknown ··· 42 42 // Two conditions for a job to be claimable: 43 43 // 1. status='queued' AND run_after <= now() 44 44 // 2. status='running' AND locked_until < now() (lease expired) 45 - const result = await db.execute(sql` 45 + const result = await db.execute<JobEnvelope>(sql` 46 46 UPDATE ${job} 47 47 SET 48 48 status = 'running', ··· 65 65 66 66 // drizzle's neon-http execute returns rows on `.rows`; pglite's returns directly. 67 67 // Normalise. 68 - const rows = (Array.isArray(result) ? result : (result as { rows?: unknown[] }).rows) ?? [] 69 - if (rows.length === 0) return null 70 - return rows[0] as JobEnvelope 68 + const rows: JobEnvelope[] = Array.isArray(result) 69 + ? result 70 + : ((result as { rows?: JobEnvelope[] }).rows ?? []) 71 + const envelope = rows[0] 72 + return envelope ?? null 71 73 } 72 74 73 75 /** Mark a job as completed. */
+1 -1
server/utils/ssh-keypair.ts
··· 26 26 27 27 return { 28 28 publicKeyOpenSsh: encodeOpenSshEd25519(rawPublic, comment), 29 - privateKeyPem: privateKey as string, 29 + privateKeyPem: privateKey, 30 30 } 31 31 } 32 32
+2 -1
server/utils/tangled-repo.ts
··· 98 98 const body = await knotResponse.text() 99 99 throw new Error(`knot ${knot} returned ${knotResponse.status}: ${body}`) 100 100 } 101 - const { repoDid } = await knotResponse.json() as { repoDid?: string } 101 + const knotJson: { repoDid?: string } = await knotResponse.json() 102 + const { repoDid } = knotJson 102 103 if (!repoDid) { 103 104 throw new Error(`knot ${knot} returned no repoDid`) 104 105 }
+1 -1
test/unit/ssh-keypair.spec.ts
··· 20 20 const { publicKeyOpenSsh, privateKeyPem } = generateKeypair('test') 21 21 22 22 // Decode the OpenSSH public key back to raw bytes and reconstruct an SPKI key. 23 - const b64 = publicKeyOpenSsh.split(' ')[1]! 23 + const b64 = publicKeyOpenSsh.split(' ')[1] 24 24 const blob = Buffer.from(b64, 'base64') 25 25 // ssh-ed25519 framing: <4 bytes len><"ssh-ed25519"><4 bytes len><32 bytes raw key> 26 26 const algoLen = blob.readUInt32BE(0)
+9 -8
test/unit/tangled-pubkey.spec.ts
··· 23 23 }, 24 24 })) 25 25 26 + function fakeOauthSession(did: string) { 27 + // The Agent mock above ignores its constructor argument, so we only need 28 + // a `.did` field for the helper itself. 29 + // eslint-disable-next-line ts/no-unsafe-type-assertion 30 + return { did } as unknown as Parameters<typeof generateAndPublishKey>[0]['oauthSession'] 31 + } 32 + 26 33 describe('generateAndPublishKey', () => { 27 34 beforeEach(async () => { 28 35 process.env.NUXT_ENCRYPTION_KEY = crypto.randomBytes(32).toString('base64') ··· 50 57 clearDb() 51 58 }) 52 59 53 - function fakeOauthSession(did: string) { 54 - // The Agent mock above ignores its constructor argument, so we only need 55 - // a `.did` field for the helper itself. 56 - return { did } as never 57 - } 58 - 59 60 it('generates a key, publishes to PDS, and stores the encrypted private half', async () => { 60 61 const result = await generateAndPublishKey({ 61 62 oauthSession: fakeOauthSession('did:plc:abc'), ··· 64 65 65 66 expect(result.created).toBe(true) 66 67 expect(createRecordMock).toHaveBeenCalledTimes(1) 67 - const call = createRecordMock.mock.calls[0]![0] 68 + const call = createRecordMock.mock.calls[0][0] 68 69 expect(call.repo).toBe('did:plc:abc') 69 70 expect(call.collection).toBe('sh.tangled.publicKey') 70 71 expect(call.record.$type).toBe('sh.tangled.publicKey') ··· 75 76 const rows = await db.select().from(sshKey) 76 77 .where(sql`${sshKey.installationId} = 1 AND ${sshKey.did} = 'did:plc:abc'`) 77 78 expect(rows).toHaveLength(1) 78 - const row = rows[0]! 79 + const row = rows[0] 79 80 expect(row.publicKey).toMatch(/^ssh-ed25519 /) 80 81 expect(row.tangledKeyRkey).toBe('3kh2y4xq2lk2v') 81 82
+30 -22
test/unit/tangled-repo.spec.ts
··· 39 39 clearGitHubAppCache: () => {}, 40 40 })) 41 41 42 - const fakeFetch = vi.fn<(url: string, init: RequestInit) => Promise<Response>>() 42 + interface CapturedInit { 43 + method?: string 44 + headers?: Record<string, string> 45 + body?: string 46 + } 47 + const fakeFetch = vi.fn<(url: string, init: CapturedInit) => Promise<Response>>() 43 48 const ORIGINAL_FETCH = globalThis.fetch 44 49 50 + function fakeOauthSession(did: string) { 51 + // eslint-disable-next-line ts/no-unsafe-type-assertion 52 + return { did } as unknown as Parameters<typeof enrollRepo>[0]['oauthSession'] 53 + } 54 + 55 + function ghRepo(over: Partial<GithubRepoLike> = {}): GithubRepoLike { 56 + return { 57 + id: 9001, 58 + full_name: 'alice/my-project', 59 + private: false, 60 + fork: false, 61 + default_branch: 'main', 62 + ...over, 63 + } 64 + } 65 + 45 66 describe('enrollRepo', () => { 46 67 beforeEach(async () => { 47 68 process.env.NUXT_ENCRYPTION_KEY = crypto.randomBytes(32).toString('base64') ··· 56 77 getServiceAuthMock.mockReset() 57 78 putRecordMock.mockReset() 58 79 fakeFetch.mockReset() 59 - globalThis.fetch = fakeFetch as unknown as typeof fetch 80 + // eslint-disable-next-line ts/no-unsafe-type-assertion 81 + globalThis.fetch = fakeFetch as unknown as typeof globalThis.fetch 60 82 61 83 getServiceAuthMock.mockResolvedValue({ data: { token: 'service-auth-jwt' } }) 62 84 putRecordMock.mockResolvedValue({ data: { uri: 'at://did:plc:abc/sh.tangled.repo/whatever', cid: 'bafy' } }) ··· 70 92 clearDb() 71 93 }) 72 94 73 - function fakeOauthSession(did: string) { 74 - return { did } as never 75 - } 76 - 77 - function ghRepo(over: Partial<GithubRepoLike> = {}): GithubRepoLike { 78 - return { 79 - id: 9001, 80 - full_name: 'alice/my-project', 81 - private: false, 82 - fork: false, 83 - default_branch: 'main', 84 - ...over, 85 - } 86 - } 87 - 88 95 it('enrolls a public, non-fork repo end to end', async () => { 89 96 githubGet.mockResolvedValue({ data: ghRepo() }) 90 97 fakeFetch.mockResolvedValue(new Response( ··· 111 118 const url = fetchCall?.[0] 112 119 const init = fetchCall?.[1] 113 120 expect(url).toBe('https://knot1.tangled.sh/xrpc/sh.tangled.repo.create') 114 - expect((init!.headers as Record<string, string>).authorization).toBe('Bearer service-auth-jwt') 115 - const body = JSON.parse(init!.body as string) as Record<string, unknown> 121 + expect(init?.headers?.authorization).toBe('Bearer service-auth-jwt') 122 + if (typeof init?.body !== 'string') throw new TypeError('expected string body') 123 + const body: Record<string, unknown> = JSON.parse(init.body) 116 124 expect(body.name).toBe('my-project') 117 125 expect(body.source).toBe('https://github.com/alice/my-project') 118 126 expect(body.defaultBranch).toBe('main') ··· 129 137 const rows = await useDb().select().from(repoMapping) 130 138 .where(sql`${repoMapping.installationId} = 1`) 131 139 expect(rows).toHaveLength(1) 132 - expect(rows[0]!.tangledRepoDid).toBe('did:plc:repo-xyz') 133 - expect(rows[0]!.knot).toBe('knot1.tangled.sh') 134 - expect(rows[0]!.status).toBe('active') 140 + expect(rows[0].tangledRepoDid).toBe('did:plc:repo-xyz') 141 + expect(rows[0].knot).toBe('knot1.tangled.sh') 142 + expect(rows[0].status).toBe('active') 135 143 }) 136 144 137 145 it('skips private repos', async () => {
+1
test/utils/db.ts
··· 26 26 if (trimmed) await pg.exec(trimmed) 27 27 } 28 28 29 + // eslint-disable-next-line ts/no-unsafe-type-assertion 29 30 return drizzle(pg, { schema }) as unknown as Db 30 31 }