Skip to content

Commit f7dcd3f

Browse files
ex3ndrclaude
andcommitted
refactor: simplify login flow with --qr flag
- Default login uses agent-style flow (no prompts) - Add --qr flag to show QR code inline - Remove --agent flag and browser/qr picker - Check existing auth before login, require logout first - Remove unused imports (select, openBrowser, emojiHash) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c3a828c commit f7dcd3f

3 files changed

Lines changed: 45 additions & 123 deletions

File tree

‎.context/notes.md‎

Whitespace-only changes.

‎.context/todos.md‎

Whitespace-only changes.

‎sources/commands/login/index.ts‎

Lines changed: 45 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { select } from "@inquirer/prompts";
21
import type { Command, CommandContext } from "@/commands/types";
32
import type { Environment } from "@/environment";
43
import {
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";
1816
import { renderQrCode } from "@/utils/qrCode";
1917
import { fetchClientMe } from "@/client/clientMe";
2018

2119
type LoginOptions = {
2220
token?: string;
2321
tokenStdin: boolean;
24-
agent: boolean;
22+
qr: boolean;
2523
};
2624

27-
type DeviceAuthMethod = "browser" | "qr";
28-
2925
const 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: {
101110
function 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

176185
async 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-
349263
async 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

Comments
 (0)