import { App } from '@octokit/app' let cachedApp: App | undefined function useApp(): App { if (cachedApp) return cachedApp const appId = process.env.NUXT_GITHUB_APP_ID const privateKey = process.env.NUXT_GITHUB_APP_PRIVATE_KEY if (!appId || !privateKey) { throw new Error('NUXT_GITHUB_APP_ID and NUXT_GITHUB_APP_PRIVATE_KEY must be set') } // The OAuth client id/secret are optional at construction time so the // webhook + sync paths (server-to-server only) keep working without them; // requireOAuthApp() throws explicitly when the /connect flow needs them. const clientId = process.env.NUXT_GITHUB_APP_CLIENT_ID const clientSecret = process.env.NUXT_GITHUB_APP_CLIENT_SECRET cachedApp = new App({ appId, ...(clientId && clientSecret ? { oauth: { clientId, clientSecret } } : {}), // Vercel env vars escape newlines; restore them so PEM parsing works. privateKey: privateKey.replaceAll('\\n', '\n'), }) return cachedApp } export type InstallationOctokit = Awaited> export type UserOctokit = Awaited> /** Get an Octokit pre-authed for a specific GitHub App installation. */ export async function installationOctokit(installationId: number): Promise { const app = useApp() return app.getInstallationOctokit(installationId) } /** Mint a short-lived installation access token for authenticating git fetches. */ export async function installationToken(octokit: InstallationOctokit): Promise { const { token } = (await octokit.auth({ type: 'installation' })) as { token: string } return token } function requireOAuthApp(): App { if (!process.env.NUXT_GITHUB_APP_CLIENT_ID || !process.env.NUXT_GITHUB_APP_CLIENT_SECRET) { throw createError({ statusCode: 500, statusMessage: 'GitHub user-OAuth is not configured (set NUXT_GITHUB_APP_CLIENT_ID and NUXT_GITHUB_APP_CLIENT_SECRET)', }) } return useApp() } /** * Build the GitHub user-to-server OAuth authorize URL. `state` is round-tripped * back to the callback for CSRF protection; the callback verifies it against a * sealed cookie. */ export function githubOAuthUrl(opts: { state: string, redirectUri: string }): string { const app = requireOAuthApp() const { url } = app.oauth.getWebFlowAuthorizationUrl({ state: opts.state, redirectUrl: opts.redirectUri, allowSignup: false, }) return url } /** * Exchange an OAuth `code` for a user access token and return an Octokit * authenticated as that user. */ export async function userOctokitFromCode(code: string): Promise { const app = requireOAuthApp() return app.oauth.getUserOctokit({ code }) } /** * True if the authenticated user administers `installationId`. The * user-to-server `GET /user/installations` endpoint only returns installations * the user can administer, so membership in the list is the ownership proof. */ export async function userAdministersInstallation( userOctokit: UserOctokit, installationId: number, ): Promise { // A user with >100 app installations is implausible here, so one page is // enough; `/user/installations` only lists installs the user administers. const { data } = await userOctokit.request('GET /user/installations', { per_page: 100 }) return data.installations.some(install => install.id === installationId) } /** Resolve an installation's account login (for display on the connect page). */ export async function installationAccountLogin(installationId: number): Promise { const app = useApp() try { const { data } = await app.octokit.request('GET /app/installations/{installation_id}', { installation_id: installationId, }) const account = data.account if (account && 'login' in account) return account.login return null } catch { return null } } /** Test hook. */ export function clearGitHubAppCache() { cachedApp = undefined }