1- import { createHmac } from "crypto" ;
21import { PrivyClient } from "@privy-io/node" ;
32import { Elysia } from "elysia" ;
4- import supabase from "@/lib/supabase/serverClient" ;
3+ import getAccountDetailsByEmails from "@/lib/supabase/account_emails/getAccountDetailsByEmails" ;
4+ import { getAccountIdFromApiKey } from "@/lib/supabase/account_api_keys/getAccountIdFromApiKey" ;
55
66const PRIVY_APP_ID = process . env . PRIVY_APP_ID || process . env . NEXT_PUBLIC_PRIVY_APP_ID ;
77const PRIVY_PROJECT_SECRET = process . env . PRIVY_PROJECT_SECRET ;
@@ -16,127 +16,49 @@ const privyClient =
1616 } )
1717 : null ;
1818
19- function hashApiKey ( rawKey : string ) : string {
20- if ( ! PRIVY_PROJECT_SECRET ) {
21- throw new Error ( "Missing PRIVY_PROJECT_SECRET" ) ;
22- }
23-
24- return createHmac ( "sha256" , PRIVY_PROJECT_SECRET ) . update ( rawKey ) . digest ( "hex" ) ;
25- }
26-
27- function parseCookies ( cookieHeader : string | null ) : Record < string , string > {
28- if ( ! cookieHeader ) return { } ;
29-
30- return cookieHeader
31- . split ( ";" )
32- . map ( ( part ) => part . trim ( ) )
33- . filter ( Boolean )
34- . reduce < Record < string , string > > ( ( acc , part ) => {
35- const separatorIndex = part . indexOf ( "=" ) ;
36- if ( separatorIndex <= 0 ) return acc ;
37-
38- const key = part . slice ( 0 , separatorIndex ) . trim ( ) ;
39- const value = part . slice ( separatorIndex + 1 ) . trim ( ) ;
19+ export const authPlugin = new Elysia ( { name : "auth-plugin" } )
20+ . derive ( async ( { cookie, request } ) => {
21+ const apiKey = request . headers . get ( "x-api-key" ) ;
22+ const privyToken = cookie [ "privy-token" ] . value as string ;
23+
24+ let user : { userId : string ; identifier : string } | null = null ;
4025
26+ if ( apiKey ) {
4127 try {
42- acc [ key ] = decodeURIComponent ( value ) ;
43- } catch {
44- acc [ key ] = value ;
28+ user = {
29+ userId : await getAccountIdFromApiKey ( apiKey ) ,
30+ identifier : apiKey ,
31+ } ;
32+ } catch ( error ) {
33+ console . error ( "Elysia auth apiKey validation failed:" , error ) ;
4534 }
46-
47- return acc ;
48- } , { } ) ;
49- }
50-
51- function getPrivyAuthTokenFromRequest ( request : Request ) : string | null {
52- const cookies = parseCookies ( request . headers . get ( "cookie" ) ) ;
53- return cookies [ "privy-token" ] || null ;
54- }
55-
56- async function getAccountIdFromApiKey ( apiKey : string ) : Promise < string | null > {
57- const keyHash = hashApiKey ( apiKey ) ;
58-
59- const { data, error } = await supabase
60- . from ( "account_api_keys" )
61- . select ( "account" )
62- . eq ( "key_hash" , keyHash )
63- . limit ( 1 )
64- . maybeSingle ( ) ;
65-
66- if ( error ) {
67- throw new Error ( `Failed API key lookup: ${ error . message } ` ) ;
68- }
69-
70- return data ?. account ?? null ;
71- }
72-
73- function getEmailFromPrivyUser ( user : { linked_accounts ?: Array < { type ?: string ; address ?: string } > } ) {
74- const emailAccount = user . linked_accounts ?. find ( ( account ) => account . type === "email" ) ;
75- return emailAccount ?. address ?. toLowerCase ( ) ?? null ;
76- }
77-
78- async function getAccountIdFromPrivyAuthToken ( authToken : string ) : Promise < string | null > {
79- if ( ! privyClient ) {
80- throw new Error (
81- "Missing Privy configuration: PRIVY_APP_ID/NEXT_PUBLIC_PRIVY_APP_ID, PRIVY_PROJECT_SECRET, and PRIVY_JWT_VERIFICATION_KEY are required" ,
82- ) ;
83- }
84-
85- const verified = await privyClient . utils ( ) . auth ( ) . verifyAuthToken ( authToken ) ;
86- const user = await privyClient . users ( ) . _get ( verified . user_id ) ;
87- const email = getEmailFromPrivyUser ( user ) ;
88-
89- if ( ! email ) {
90- throw new Error ( "No email found in authenticated Privy user" ) ;
91- }
92-
93- const { data, error } = await supabase
94- . from ( "account_emails" )
95- . select ( "account_id" )
96- . eq ( "email" , email )
97- . limit ( 1 )
98- . maybeSingle ( ) ;
99-
100- if ( error ) {
101- throw new Error ( `Failed account email lookup: ${ error . message } ` ) ;
102- }
103-
104- return data ?. account_id ?? null ;
105- }
106-
107- export const authMacro = new Elysia ( { name : "auth-macro" } ) . macro ( {
108- auth ( enabled : boolean ) {
109- if ( ! enabled ) return ;
110-
111- return {
112- async beforeHandle ( { request, status } ) {
113- const apiKey = request . headers . get ( "x-api-key" ) ;
114-
115- try {
116- if ( apiKey ) {
117- const accountId = await getAccountIdFromApiKey ( apiKey ) ;
118- if ( ! accountId ) {
119- return status ( 401 , { error : "Unauthorized" } ) ;
120- }
121- return ;
122- }
123-
124- const privyAuthToken = getPrivyAuthTokenFromRequest ( request ) ;
125- if ( ! privyAuthToken ) {
126- return status ( 401 , { error : "Authentication required" } ) ;
127- }
128-
129- const accountId = await getAccountIdFromPrivyAuthToken ( privyAuthToken ) ;
130- if ( ! accountId ) {
131- return status ( 401 , { error : "Unauthorized" } ) ;
132- }
133- } catch ( error ) {
134- return status ( 401 , {
135- error : "Authentication failed" ,
136- details : error instanceof Error ? error . message : "Unknown authentication error" ,
137- } ) ;
35+ } else if ( privyToken && privyClient ) {
36+ try {
37+ const verified = await privyClient . utils ( ) . auth ( ) . verifyAccessToken ( privyToken ) ;
38+ const privyUser = await privyClient . users ( ) . _get ( verified . user_id ) ;
39+ const email = privyUser . linked_accounts ?. find ( ( account ) => account . type === "email" ) ?. address ;
40+ const accountDetails = email ? await getAccountDetailsByEmails ( [ email ] ) : [ ] ;
41+ const accountId = accountDetails [ 0 ] ?. account_id ?? null ;
42+
43+ if ( accountId ) {
44+ user = {
45+ userId : accountId ,
46+ identifier : privyToken ,
47+ } ;
13848 }
139- } ,
140- } ;
141- } ,
142- } ) ;
49+ } catch ( error ) {
50+ console . error ( "Elysia auth privyToken validation failed:" , error ) ;
51+ }
52+ }
53+
54+ return { user } ;
55+ } )
56+ . macro ( {
57+ auth : {
58+ beforeHandle ( { user, status } ) {
59+ if ( ! user ) {
60+ return status ( 401 , "Authentication required" ) ;
61+ }
62+ } ,
63+ } ,
64+ } ) ;
0 commit comments