import type { OAuthClientMetadataInput, OAuthSession } from '@atproto/oauth-client-node' import type { EventHandlerRequest, H3Event, SessionManager } from 'h3' import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client-node' import { parse } from 'valibot' import { getOAuthLock } from '#server/utils/atproto/lock' import { useOAuthStorage } from '#server/utils/atproto/storage' import { LIKES_SCOPE } from '#shared/utils/constants' import { OAuthMetadataSchema } from '#shared/schemas/oauth' // @ts-expect-error virtual file from oauth module import { clientUri } from '#oauth/config' // TODO: If you add writing a new record you will need to add a scope for it export const scope = `atproto ${LIKES_SCOPE}` /** * Resolves a did to a handle via DoH or via the http website calls */ export const handleResolver = new AtprotoDohHandleResolver({ dohEndpoint: 'https://cloudflare-dns.com/dns-query', }) export function getOauthClientMetadata() { const dev = import.meta.dev const client_uri = clientUri const redirect_uri = `${client_uri}/api/auth/atproto` const client_id = dev ? `http://localhost?redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}` : `${client_uri}/oauth-client-metadata.json` // If anything changes here, please make sure to also update /shared/schemas/oauth.ts to match return parse(OAuthMetadataSchema, { client_name: 'npmx.dev', client_id, client_uri, scope, redirect_uris: [redirect_uri] as [string, ...string[]], grant_types: ['authorization_code', 'refresh_token'], application_type: 'web', token_endpoint_auth_method: 'none', dpop_bound_access_tokens: true, response_types: ['code'], }) as OAuthClientMetadataInput } type EventHandlerWithOAuthSession = ( event: H3Event, session: OAuthSession | undefined, serverSession: SessionManager, ) => Promise async function getOAuthSession( event: H3Event, ): Promise<{ oauthSession: OAuthSession | undefined; serverSession: SessionManager }> { const serverSession = await useServerSession(event) try { const clientMetadata = getOauthClientMetadata() const { stateStore, sessionStore } = useOAuthStorage(serverSession) const client = new NodeOAuthClient({ stateStore, sessionStore, clientMetadata, requestLock: getOAuthLock(), handleResolver, }) const currentSession = serverSession.data // TODO (jg): why can a session be `{}`? if (!currentSession || !currentSession.public?.did) { return { oauthSession: undefined, serverSession } } const oauthSession = await client.restore(currentSession.public.did) return { oauthSession, serverSession } } catch (error) { // Log error safely without using util.inspect on potentially problematic objects // The @atproto library creates error objects with getters that crash Node's util.inspect // eslint-disable-next-line no-console console.error( '[oauth] Failed to get session:', error instanceof Error ? error.message : 'Unknown error', ) return { oauthSession: undefined, serverSession } } } /** * Throws if the logged in OAuth Session does not have the required scopes. * As we add new scopes we need to check if the client has the ability to use it. * If not need to let the client know to redirect the user to the PDS to upgrade their scopes. * @param oAuthSession - The current OAuth session from the event * @param requiredScopes - The required scope you are checking if you can use */ export async function throwOnMissingOAuthScope(oAuthSession: OAuthSession, requiredScopes: string) { const tokenInfo = await oAuthSession.getTokenInfo() if (!tokenInfo.scope.includes(requiredScopes)) { throw createError({ status: 403, message: ERROR_NEED_REAUTH, }) } } export function eventHandlerWithOAuthSession( handler: EventHandlerWithOAuthSession, ) { return defineEventHandler(async event => { const { oauthSession, serverSession } = await getOAuthSession(event) return await handler(event, oauthSession, serverSession) }) }