mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

1import { App } from '@octokit/app' 2 3let cachedApp: App | undefined 4 5function useApp(): App { 6 if (cachedApp) return cachedApp 7 const appId = process.env.NUXT_GITHUB_APP_ID 8 const privateKey = process.env.NUXT_GITHUB_APP_PRIVATE_KEY 9 if (!appId || !privateKey) { 10 throw new Error('NUXT_GITHUB_APP_ID and NUXT_GITHUB_APP_PRIVATE_KEY must be set') 11 } 12 // The OAuth client id/secret are optional at construction time so the 13 // webhook + sync paths (server-to-server only) keep working without them; 14 // requireOAuthApp() throws explicitly when the /connect flow needs them. 15 const clientId = process.env.NUXT_GITHUB_APP_CLIENT_ID 16 const clientSecret = process.env.NUXT_GITHUB_APP_CLIENT_SECRET 17 cachedApp = new App({ 18 appId, 19 ...(clientId && clientSecret ? { oauth: { clientId, clientSecret } } : {}), 20 // Vercel env vars escape newlines; restore them so PEM parsing works. 21 privateKey: privateKey.replaceAll('\\n', '\n'), 22 }) 23 return cachedApp 24} 25 26export type InstallationOctokit = Awaited<ReturnType<App['getInstallationOctokit']>> 27export type UserOctokit = Awaited<ReturnType<App['oauth']['getUserOctokit']>> 28 29/** Get an Octokit pre-authed for a specific GitHub App installation. */ 30export async function installationOctokit(installationId: number): Promise<InstallationOctokit> { 31 const app = useApp() 32 return app.getInstallationOctokit(installationId) 33} 34 35function requireOAuthApp(): App { 36 if (!process.env.NUXT_GITHUB_APP_CLIENT_ID || !process.env.NUXT_GITHUB_APP_CLIENT_SECRET) { 37 throw createError({ 38 statusCode: 500, 39 statusMessage: 'GitHub user-OAuth is not configured (set NUXT_GITHUB_APP_CLIENT_ID and NUXT_GITHUB_APP_CLIENT_SECRET)', 40 }) 41 } 42 return useApp() 43} 44 45/** 46 * Build the GitHub user-to-server OAuth authorize URL. `state` is round-tripped 47 * back to the callback for CSRF protection; the callback verifies it against a 48 * sealed cookie. 49 */ 50export function githubOAuthUrl(opts: { state: string, redirectUri: string }): string { 51 const app = requireOAuthApp() 52 const { url } = app.oauth.getWebFlowAuthorizationUrl({ 53 state: opts.state, 54 redirectUrl: opts.redirectUri, 55 allowSignup: false, 56 }) 57 return url 58} 59 60/** 61 * Exchange an OAuth `code` for a user access token and return an Octokit 62 * authenticated as that user. 63 */ 64export async function userOctokitFromCode(code: string): Promise<UserOctokit> { 65 const app = requireOAuthApp() 66 return app.oauth.getUserOctokit({ code }) 67} 68 69/** 70 * True if the authenticated user administers `installationId`. The 71 * user-to-server `GET /user/installations` endpoint only returns installations 72 * the user can administer, so membership in the list is the ownership proof. 73 */ 74export async function userAdministersInstallation( 75 userOctokit: UserOctokit, 76 installationId: number, 77): Promise<boolean> { 78 // A user with >100 app installations is implausible here, so one page is 79 // enough; `/user/installations` only lists installs the user administers. 80 const { data } = await userOctokit.request('GET /user/installations', { per_page: 100 }) 81 return data.installations.some(install => install.id === installationId) 82} 83 84/** Resolve an installation's account login (for display on the connect page). */ 85export async function installationAccountLogin(installationId: number): Promise<string | null> { 86 const app = useApp() 87 try { 88 const { data } = await app.octokit.request('GET /app/installations/{installation_id}', { 89 installation_id: installationId, 90 }) 91 const account = data.account 92 if (account && 'login' in account) return account.login 93 return null 94 } 95 catch { 96 return null 97 } 98} 99 100/** Test hook. */ 101export function clearGitHubAppCache() { 102 cachedApp = undefined 103}