[READ-ONLY] a fast, modern browser for the npm registry
at main 113 lines 4.2 kB view raw
1import type { OAuthClientMetadataInput, OAuthSession } from '@atproto/oauth-client-node' 2import type { EventHandlerRequest, H3Event, SessionManager } from 'h3' 3import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client-node' 4import { parse } from 'valibot' 5import { getOAuthLock } from '#server/utils/atproto/lock' 6import { useOAuthStorage } from '#server/utils/atproto/storage' 7import { LIKES_SCOPE } from '#shared/utils/constants' 8import { OAuthMetadataSchema } from '#shared/schemas/oauth' 9// @ts-expect-error virtual file from oauth module 10import { clientUri } from '#oauth/config' 11// TODO: If you add writing a new record you will need to add a scope for it 12export const scope = `atproto ${LIKES_SCOPE}` 13 14/** 15 * Resolves a did to a handle via DoH or via the http website calls 16 */ 17export const handleResolver = new AtprotoDohHandleResolver({ 18 dohEndpoint: 'https://cloudflare-dns.com/dns-query', 19}) 20 21export function getOauthClientMetadata() { 22 const dev = import.meta.dev 23 24 const client_uri = clientUri 25 const redirect_uri = `${client_uri}/api/auth/atproto` 26 27 const client_id = dev 28 ? `http://localhost?redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}` 29 : `${client_uri}/oauth-client-metadata.json` 30 31 // If anything changes here, please make sure to also update /shared/schemas/oauth.ts to match 32 return parse(OAuthMetadataSchema, { 33 client_name: 'npmx.dev', 34 client_id, 35 client_uri, 36 scope, 37 redirect_uris: [redirect_uri] as [string, ...string[]], 38 grant_types: ['authorization_code', 'refresh_token'], 39 application_type: 'web', 40 token_endpoint_auth_method: 'none', 41 dpop_bound_access_tokens: true, 42 response_types: ['code'], 43 }) as OAuthClientMetadataInput 44} 45 46type EventHandlerWithOAuthSession<T extends EventHandlerRequest, D> = ( 47 event: H3Event<T>, 48 session: OAuthSession | undefined, 49 serverSession: SessionManager, 50) => Promise<D> 51 52async function getOAuthSession( 53 event: H3Event, 54): Promise<{ oauthSession: OAuthSession | undefined; serverSession: SessionManager }> { 55 const serverSession = await useServerSession(event) 56 57 try { 58 const clientMetadata = getOauthClientMetadata() 59 const { stateStore, sessionStore } = useOAuthStorage(serverSession) 60 61 const client = new NodeOAuthClient({ 62 stateStore, 63 sessionStore, 64 clientMetadata, 65 requestLock: getOAuthLock(), 66 handleResolver, 67 }) 68 69 const currentSession = serverSession.data 70 // TODO (jg): why can a session be `{}`? 71 if (!currentSession || !currentSession.public?.did) { 72 return { oauthSession: undefined, serverSession } 73 } 74 75 const oauthSession = await client.restore(currentSession.public.did) 76 return { oauthSession, serverSession } 77 } catch (error) { 78 // Log error safely without using util.inspect on potentially problematic objects 79 // The @atproto library creates error objects with getters that crash Node's util.inspect 80 // eslint-disable-next-line no-console 81 console.error( 82 '[oauth] Failed to get session:', 83 error instanceof Error ? error.message : 'Unknown error', 84 ) 85 return { oauthSession: undefined, serverSession } 86 } 87} 88 89/** 90 * Throws if the logged in OAuth Session does not have the required scopes. 91 * As we add new scopes we need to check if the client has the ability to use it. 92 * If not need to let the client know to redirect the user to the PDS to upgrade their scopes. 93 * @param oAuthSession - The current OAuth session from the event 94 * @param requiredScopes - The required scope you are checking if you can use 95 */ 96export async function throwOnMissingOAuthScope(oAuthSession: OAuthSession, requiredScopes: string) { 97 const tokenInfo = await oAuthSession.getTokenInfo() 98 if (!tokenInfo.scope.includes(requiredScopes)) { 99 throw createError({ 100 status: 403, 101 message: ERROR_NEED_REAUTH, 102 }) 103 } 104} 105 106export function eventHandlerWithOAuthSession<T extends EventHandlerRequest, D>( 107 handler: EventHandlerWithOAuthSession<T, D>, 108) { 109 return defineEventHandler(async event => { 110 const { oauthSession, serverSession } = await getOAuthSession(event) 111 return await handler(event, oauthSession, serverSession) 112 }) 113}