11import { redirect } from 'next/navigation'
22import { Suspense } from 'react'
33import { AUTH_URLS , PROTECTED_URLS } from '@/configs/urls'
4+ import type { FeatureFlagContext } from '@/core/modules/feature-flags/context'
5+ import { featureFlags } from '@/core/modules/feature-flags/feature-flags.server'
46import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server'
57import { getAuthContext } from '@/core/server/auth'
68import { l , serializeErrorForLog } from '@/core/shared/clients/logger/logger'
79import { isLoopbackUrl } from '@/core/shared/schemas/url'
810import { encodedRedirect } from '@/lib/utils/auth'
911import { generateE2BUserAccessToken } from '@/lib/utils/server'
12+ import { isVersionGreaterOrEqual } from '@/lib/utils/version'
1013import { Alert , AlertDescription , AlertTitle } from '@/ui/primitives/alert'
1114import { CloudIcon , LaptopIcon , LinkIcon } from '@/ui/primitives/icons'
1215
13- // Types
16+ // Minimum CLI version that supports the OAuth JWT auth flow.
17+ // CLI versions >= this use the new public-client OAuth flow;
18+ // older versions fall back to the legacy e2b access token flow.
19+ // Old binaries (pre-2.13.0) don't send cliVersion at all (it's a new param),
20+ // so they correctly fall to 'legacy'.
21+ const MIN_CLI_VERSION_FOR_HYDRA_FLOW = '2.13.0'
22+
1423type CLISearchParams = Promise < {
1524 next ?: string
25+ cliVersion ?: string
1626 state ?: string
1727 error ?: string
1828} >
1929
20- // Server Actions
30+ type CLIFlow = 'hydra' | 'legacy'
31+
32+ function resolveCLIFlow ( cliVersion : string | undefined ) : CLIFlow {
33+ if ( ! cliVersion ) return 'legacy'
34+ return isVersionGreaterOrEqual ( cliVersion , MIN_CLI_VERSION_FOR_HYDRA_FLOW )
35+ ? 'hydra'
36+ : 'legacy'
37+ }
2138
22- async function handleCLIAuth (
39+ function handleHydraCLIAuth ( next : string ) {
40+ if ( ! isLoopbackUrl ( next ) ) {
41+ throw new Error ( 'Invalid redirect URL' )
42+ }
43+ return redirect ( `/api/auth/oauth/cli-start?next=${ encodeURIComponent ( next ) } ` )
44+ }
45+
46+ async function handleLegacyCLIAuth (
2347 next : string ,
2448 userEmail : string ,
2549 authProviderAccessToken : string
@@ -57,10 +81,9 @@ async function handleCLIAuth(
5781 return redirect ( `${ next } ?${ searchParams . toString ( ) } ` )
5882}
5983
60- // UI Components
6184function CLIIcons ( ) {
6285 return (
63- < p className = "flex items-center justify-center gap-4 text-3xl tracking-tight sm:text-4xl" >
86+ < p className = "flex items-center justify-center gap-4 text-3xl tracking-tight sm:text-4xl" >
6487 < span className = "text-fg-tertiary" >
6588 < LaptopIcon className = "size-8" />
6689 </ span >
@@ -86,98 +109,123 @@ function ErrorAlert({ message }: { message: string }) {
86109function SuccessState ( ) {
87110 return (
88111 < >
89- < h2 className = "text-brand-400 " > Successfully linked</ h2 >
112+ < h2 className = "text-brand-400" > Successfully linked</ h2 >
90113 < div > You can close this page and start using CLI.</ div >
91114 </ >
92115 )
93116}
94117
95- // Main Component
96118export default async function CLIAuthPage ( {
97119 searchParams,
98120} : {
99121 searchParams : CLISearchParams
100122} ) {
101- const { next, state, error } = await searchParams
123+ const { next, cliVersion , state, error } = await searchParams
102124 const authContext = await getAuthContext ( )
103125
104126 if ( state === 'success' ) {
105127 return < SuccessState />
106128 }
107129
108- // Validate redirect URL
109130 if ( ! next || ! isLoopbackUrl ( next ) ) {
110131 l . error (
111132 {
112133 key : 'cli_auth:invalid_redirect_url' ,
113134 user_id : authContext ?. user . id ,
114- context : {
115- next,
116- } ,
135+ context : { next } ,
117136 } ,
118137 `Invalid redirect URL: ${ next } `
119138 )
120139 redirect ( PROTECTED_URLS . DASHBOARD )
121140 }
122141
123- // If user is not authenticated, redirect to sign in with return URL
124142 if ( ! authContext ) {
125- const searchParams = new URLSearchParams ( {
126- returnTo : `${ AUTH_URLS . CLI } ?${ new URLSearchParams ( { next } ) . toString ( ) } ` ,
127- } )
128- redirect ( `${ AUTH_URLS . SIGN_IN } ?${ searchParams . toString ( ) } ` )
143+ const returnToParams = new URLSearchParams ( { next } )
144+ if ( cliVersion ) returnToParams . set ( 'cliVersion' , cliVersion )
145+ return redirect (
146+ `${ AUTH_URLS . SIGN_IN } ?returnTo=${ encodeURIComponent (
147+ `${ AUTH_URLS . CLI } ?${ returnToParams . toString ( ) } `
148+ ) } `
149+ )
129150 }
130151
131- // Handle CLI callback if authenticated
132- if ( ! error && next && authContext ) {
133- try {
134- if ( ! authContext . user . email ) {
135- throw new Error ( 'No user email found' )
136- }
137-
138- return await handleCLIAuth (
139- next ,
140- authContext . user . email ,
141- authContext . accessToken
142- )
143- } catch ( err ) {
144- if ( err instanceof Error && err . message . includes ( 'NEXT_REDIRECT' ) ) {
145- throw err
146- }
147-
148- l . error (
149- {
150- key : 'cli_auth:unexpected_error' ,
151- error : serializeErrorForLog ( err ) ,
152- user_id : authContext . user . id ,
153- context : {
154- next,
155- } ,
156- } ,
157- `Unexpected error during CLI authentication: ${ err instanceof Error ? err . message : String ( err ) } `
158- )
152+ const flow = resolveCLIFlow ( cliVersion )
159153
160- return encodedRedirect ( 'error' , '/auth/cli' , ( err as Error ) . message , {
161- next,
162- } )
154+ if ( flow === 'legacy' ) {
155+ const flagContext : FeatureFlagContext = {
156+ user : {
157+ id : authContext . user . id ,
158+ email : authContext . user . email ?? undefined ,
159+ } ,
160+ }
161+ const tokenProvisioningDisabled = await featureFlags . isEnabled (
162+ 'disableE2BAccessTokenProvisioning' ,
163+ flagContext
164+ )
165+
166+ if ( tokenProvisioningDisabled ) {
167+ const errorUrl = new URL ( next )
168+ errorUrl . searchParams . set (
169+ 'error' ,
170+ 'CLI update required. Run: npm install -g @e2b/cli@latest'
171+ )
172+ return redirect ( errorUrl . toString ( ) )
163173 }
164174 }
165175
166- return (
167- < div className = "p-6 text-center" >
168- < CLIIcons / >
169- < h2 className = "mt-6 text-base leading-7" >
170- Linking CLI with your account
171- </ h2 >
172- < div className = "text-fg-tertiary mt-12 leading-8" >
173- < Suspense fallback = { < div > Loading... </ div > } >
174- { error ? (
176+ if ( error ) {
177+ return (
178+ < div className = "p-6 text-center" >
179+ < CLIIcons / >
180+ < h2 className = "mt-6 text-base leading-7" >
181+ Linking CLI with your account
182+ </ h2 >
183+ < div className = "text-fg-tertiary mt-12 leading-8" >
184+ < Suspense fallback = { < div > Loading... </ div > } >
175185 < ErrorAlert message = { error } />
176- ) : (
177- < div > Authorizing CLI...</ div >
178- ) }
179- </ Suspense >
186+ </ Suspense >
187+ </ div >
180188 </ div >
181- </ div >
182- )
189+ )
190+ }
191+
192+ try {
193+ if ( flow === 'hydra' ) {
194+ return handleHydraCLIAuth ( next )
195+ }
196+
197+ if ( ! authContext . user . email ) {
198+ throw new Error ( 'No user email found' )
199+ }
200+
201+ return await handleLegacyCLIAuth (
202+ next ,
203+ authContext . user . email ,
204+ authContext . accessToken
205+ )
206+ } catch ( err ) {
207+ if ( err instanceof Error && err . message . includes ( 'NEXT_REDIRECT' ) ) {
208+ throw err
209+ }
210+
211+ l . error (
212+ {
213+ key : 'cli_auth:unexpected_error' ,
214+ error : serializeErrorForLog ( err ) ,
215+ user_id : authContext . user . id ,
216+ context : { next, flow } ,
217+ } ,
218+ `Unexpected error during CLI authentication: ${ err instanceof Error ? err . message : String ( err ) } `
219+ )
220+
221+ const redirectParams : Record < string , string > = { next }
222+ if ( cliVersion ) redirectParams . cliVersion = cliVersion
223+
224+ return encodedRedirect (
225+ 'error' ,
226+ '/auth/cli' ,
227+ ( err as Error ) . message ,
228+ redirectParams
229+ )
230+ }
183231}
0 commit comments