1- import { select } from "@inquirer/prompts" ;
21import type { Command , CommandContext } from "@/commands/types" ;
32import type { Environment } from "@/environment" ;
43import {
4+ loadToken ,
55 saveToken ,
66 loadPairingState ,
77 savePairingState ,
@@ -13,22 +13,18 @@ import {
1313 decryptAppPairingToken ,
1414 generateAppPairingKeyPair ,
1515} from "@/utils/appPairingCrypto" ;
16- import { emojiHash } from "@/utils/emojiHash" ;
17- import { openBrowser } from "@/utils/browser" ;
1816import { renderQrCode } from "@/utils/qrCode" ;
1917import { fetchClientMe } from "@/client/clientMe" ;
2018
2119type LoginOptions = {
2220 token ?: string ;
2321 tokenStdin : boolean ;
24- agent : boolean ;
22+ qr : boolean ;
2523} ;
2624
27- type DeviceAuthMethod = "browser" | "qr" ;
28-
2925const USAGE = [
3026 "bee login" ,
31- "bee login --agent " ,
27+ "bee login --qr " ,
3228 "bee login --token <token>" ,
3329 "bee login --token-stdin" ,
3430] . join ( "\n" ) ;
@@ -62,7 +58,27 @@ async function handleLogin(
6258 }
6359
6460 if ( ! token ) {
65- token = await loginWithAppPairing ( context , options . agent ) ;
61+ // Check if already authenticated
62+ const existingToken = await loadToken ( context . env ) ;
63+ if ( existingToken ) {
64+ try {
65+ const user = await fetchClientMe ( context , existingToken ) ;
66+ const name = [ user . first_name , user . last_name ] . filter ( Boolean ) . join ( " " ) ;
67+ console . log ( "" ) ;
68+ console . log ( `You're already connected to Bee as ${ name } .` ) ;
69+ console . log ( "" ) ;
70+ console . log ( "If you need to switch to a different account, please run 'bee logout' first." ) ;
71+ console . log ( "" ) ;
72+ console . log ( "Important: Only log out if you intentionally want to disconnect this device." ) ;
73+ console . log ( "Re-authenticating will require access to the Bee app on your phone." ) ;
74+ console . log ( "" ) ;
75+ return ;
76+ } catch {
77+ // Token is invalid, proceed with login
78+ }
79+ }
80+
81+ token = await loginWithAppPairing ( context , options . qr ) ;
6682 }
6783
6884 if ( ! token ) {
@@ -75,17 +91,10 @@ async function handleLogin(
7591
7692 await saveToken ( context . env , token ) ;
7793
78- if ( options . agent ) {
79- printAgentSuccessMessage ( user ) ;
80- } else if ( user ) {
81- const name = [ user . first_name , user . last_name ] . filter ( Boolean ) . join ( " " ) ;
82- console . log ( `Authenticated as ${ name } (id ${ user . id } ).` ) ;
83- } else {
84- console . log ( "Token stored." ) ;
85- }
94+ printSuccessMessage ( user ) ;
8695}
8796
88- function printAgentSuccessMessage ( user : {
97+ function printSuccessMessage ( user : {
8998 id : number ;
9099 first_name : string ;
91100 last_name : string | null ;
@@ -101,7 +110,7 @@ function printAgentSuccessMessage(user: {
101110function parseLoginArgs ( args : readonly string [ ] ) : LoginOptions {
102111 let token : string | undefined ;
103112 let tokenStdin = false ;
104- let agent = false ;
113+ let qr = false ;
105114 const positionals : string [ ] = [ ] ;
106115
107116 for ( let i = 0 ; i < args . length ; i += 1 ) {
@@ -125,8 +134,8 @@ function parseLoginArgs(args: readonly string[]): LoginOptions {
125134 continue ;
126135 }
127136
128- if ( arg === "--agent " ) {
129- agent = true ;
137+ if ( arg === "--qr " ) {
138+ qr = true ;
130139 continue ;
131140 }
132141
@@ -141,7 +150,7 @@ function parseLoginArgs(args: readonly string[]): LoginOptions {
141150 throw new Error ( `Unexpected arguments: ${ positionals . join ( " " ) } ` ) ;
142151 }
143152
144- const options : LoginOptions = { tokenStdin, agent } ;
153+ const options : LoginOptions = { tokenStdin, qr } ;
145154 if ( token !== undefined ) {
146155 options . token = token ;
147156 }
@@ -175,52 +184,7 @@ async function readTokenFromStdin(): Promise<string> {
175184
176185async function loginWithAppPairing (
177186 context : CommandContext ,
178- agentMode : boolean
179- ) : Promise < string > {
180- if ( ! agentMode && ! process . stdin . isTTY ) {
181- throw new Error (
182- "Interactive login requires a TTY. Use --token or --token-stdin."
183- ) ;
184- }
185-
186- if ( agentMode ) {
187- return await loginWithAppPairingAgentMode ( context ) ;
188- }
189-
190- const appId = getDefaultAppId ( context . env ) ;
191- const keyPair = generateAppPairingKeyPair ( ) ;
192- const publicKey = keyPair . publicKeyBase64 ;
193- const secretKey = keyPair . secretKey ;
194- const emoji = shouldShowEmojiHash ( )
195- ? formatEmojiHash ( keyPair . publicKeyBytes )
196- : null ;
197-
198- const initial = await requestAppPairing ( context . env , appId , publicKey ) ;
199-
200- if ( initial . status === "completed" ) {
201- return decryptAppPairingToken ( initial . encryptedToken , secretKey ) ;
202- }
203-
204- if ( initial . status === "expired" ) {
205- throw new Error ( "Pairing request expired. Please try again." ) ;
206- }
207-
208- const pairingUrl = buildPairingUrl ( initial . requestId ) ;
209- const method = await selectAuthMethod ( ) ;
210- await presentAppPairing ( method , pairingUrl , initial . requestId , emoji ) ;
211- console . log ( "Waiting for authorization..." ) ;
212-
213- return await pollForAppToken ( {
214- env : context . env ,
215- appId,
216- publicKey,
217- secretKey,
218- expiresAt : initial . expiresAt ,
219- } ) ;
220- }
221-
222- async function loginWithAppPairingAgentMode (
223- context : CommandContext
187+ useQr : boolean
224188) : Promise < string > {
225189 const existingState = await loadPairingState ( context . env ) ;
226190
@@ -229,7 +193,7 @@ async function loginWithAppPairingAgentMode(
229193 const isExpired = ! Number . isNaN ( expiresAtMs ) && Date . now ( ) >= expiresAtMs ;
230194
231195 if ( ! isExpired ) {
232- printAgentWelcomeMessage ( existingState . pairingUrl , existingState . expiresAt , "resumed" ) ;
196+ printWelcomeMessage ( existingState . pairingUrl , existingState . expiresAt , "resumed" , useQr ) ;
233197
234198 const secretKey = Buffer . from ( existingState . secretKey , "base64" ) ;
235199 try {
@@ -279,7 +243,7 @@ async function loginWithAppPairingAgentMode(
279243 await savePairingState ( context . env , state ) ;
280244
281245 const authStatus = existingState ? "reset" : "new" ;
282- printAgentWelcomeMessage ( pairingUrl , initial . expiresAt , authStatus ) ;
246+ printWelcomeMessage ( pairingUrl , initial . expiresAt , authStatus , useQr ) ;
283247
284248 try {
285249 const token = await pollForAppToken ( {
@@ -296,56 +260,6 @@ async function loginWithAppPairingAgentMode(
296260 }
297261}
298262
299- async function selectAuthMethod ( ) : Promise < DeviceAuthMethod > {
300- return await select ( {
301- message : "How would you like to authenticate?" ,
302- choices : [
303- { name : "Open a browser window" , value : "browser" } ,
304- { name : "Show a QR code" , value : "qr" } ,
305- ] ,
306- } ) ;
307- }
308-
309- async function presentAppPairing (
310- method : DeviceAuthMethod ,
311- pairingUrl : string ,
312- requestId : string ,
313- emojiHashValue : string | null
314- ) : Promise < void > {
315- console . log ( `Pairing request: ${ requestId } ` ) ;
316- console . log ( `Open this URL to approve the app: ${ pairingUrl } ` ) ;
317- if ( emojiHashValue ) {
318- console . log ( `Emoji hash: ${ emojiHashValue } ` ) ;
319- }
320-
321- if ( method === "browser" ) {
322- const opened = await openBrowser ( pairingUrl ) ;
323- if ( ! opened ) {
324- console . log ( "Unable to open the browser automatically." ) ;
325- }
326- return ;
327- }
328-
329- const qrCode = await renderQrCode ( pairingUrl ) ;
330- console . log ( qrCode ) ;
331- }
332-
333- function formatEmojiHash ( publicKeyBytes : Uint8Array ) : string | null {
334- const emojis = emojiHash ( publicKeyBytes , 4 ) ;
335- if ( emojis . length === 0 ) {
336- return null ;
337- }
338- return emojis . join ( " " ) ;
339- }
340-
341- function shouldShowEmojiHash ( ) : boolean {
342- const value = process . env [ "BEE_EMOJI_HASH" ] ?. trim ( ) . toLowerCase ( ) ;
343- if ( ! value ) {
344- return false ;
345- }
346- return [ "1" , "true" , "on" , "yes" ] . includes ( value ) ;
347- }
348-
349263async function pollForAppToken ( opts : {
350264 env : Environment ;
351265 appId : string ;
@@ -398,13 +312,14 @@ async function sleep(durationMs: number): Promise<void> {
398312 } ) ;
399313}
400314
401- type AgentAuthStatus = "new" | "resumed" | "reset" ;
315+ type AuthStatus = "new" | "resumed" | "reset" ;
402316
403- function printAgentWelcomeMessage (
317+ async function printWelcomeMessage (
404318 pairingUrl : string ,
405319 expiresAt : string ,
406- status : AgentAuthStatus
407- ) : void {
320+ status : AuthStatus ,
321+ useQr : boolean
322+ ) : Promise < void > {
408323 const expiresAtMs = Date . parse ( expiresAt ) ;
409324 const remainingMs = expiresAtMs - Date . now ( ) ;
410325 const remainingMinutes = Math . max ( 1 , Math . ceil ( remainingMs / 60000 ) ) ;
@@ -438,6 +353,13 @@ function printAgentWelcomeMessage(
438353 ) ;
439354 console . log ( "" ) ;
440355 console . log ( `Authentication link: ${ pairingUrl } ` ) ;
356+
357+ if ( useQr ) {
358+ console . log ( "" ) ;
359+ const qrCode = await renderQrCode ( pairingUrl ) ;
360+ console . log ( qrCode ) ;
361+ }
362+
441363 console . log ( "" ) ;
442364 console . log (
443365 "Once the link is opened, follow the instructions to approve the connection."
0 commit comments