forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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}