From bffb78e34cc186f09dd2794b4d933c9048361a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Fri, 25 Sep 2026 17:23:48 +0000 Subject: [PATCH] Warn that resetting the recovery key disconnects cloud accounts The reset confirms through the app, which deletes every Beeper Cloud bridge on the account before creating new keys. The CLI gave no warning, and suggested the command for accounts in needs-cross-signing-setup. Say so in the prompt, help, and docs, and refuse without --yes before starting a reset in non-interactive mode. --- packages/cli/README.md | 2 ++ packages/cli/docs/auth.md | 1 + .../src/commands/verify/reset-recovery-key.ts | 16 +++++---- .../test/verify-reset-recovery-key.test.ts | 35 +++++++++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 packages/cli/test/verify-reset-recovery-key.test.ts diff --git a/packages/cli/README.md b/packages/cli/README.md index 9545291e..cf0dc58e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1049,6 +1049,8 @@ Create a new encrypted-messages recovery key beeper verify reset-recovery-key ``` +Resetting the recovery key signs out every chat account connected through Beeper Cloud (WhatsApp, Telegram, Signal, …) on all your devices. You will need to reconnect them. Use only when you have lost your recovery key and have no other verified device. + Examples: ```sh diff --git a/packages/cli/docs/auth.md b/packages/cli/docs/auth.md index 7ca3a99b..5bf712ee 100644 --- a/packages/cli/docs/auth.md +++ b/packages/cli/docs/auth.md @@ -37,6 +37,7 @@ beeper verify cancel - For agents, drive the explicit subcommands (`start` → `sas` → `sas-confirm`) and use `--json` to inspect state. - `verify status` returns the encryption-readiness state (`ready`, `needs-verification`, `verification-in-progress`). - `recovery-key` and `reset-recovery-key` apply to the encrypted-messages key, not to Beeper account login. +- `reset-recovery-key` signs out every chat account connected through Beeper Cloud on all your devices. Use it only when the recovery key is lost and no other device is verified; otherwise run `verify recovery-key` or approve from another device. ## Examples diff --git a/packages/cli/src/commands/verify/reset-recovery-key.ts b/packages/cli/src/commands/verify/reset-recovery-key.ts index f2676d98..95aa0a85 100644 --- a/packages/cli/src/commands/verify/reset-recovery-key.ts +++ b/packages/cli/src/commands/verify/reset-recovery-key.ts @@ -1,24 +1,28 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData } from '../../lib/output.js' -import { promptYesNoDefaultYes } from '../../lib/app-api.js' +import { promptYesNo } from '../../lib/app-api.js' + +const resetWarning = 'Resetting the recovery key signs out every chat account connected through Beeper Cloud (WhatsApp, Telegram, Signal, …) on all your devices. You will need to reconnect them.' export default class AuthVerifyResetRecoveryKey extends BeeperCommand { static override summary = 'Create a new encrypted-messages recovery key' + static override description = `${resetWarning} Use only when you have lost your recovery key and have no other verified device.` async run(): Promise { const { flags } = await this.parse(AuthVerifyResetRecoveryKey) ensureWritable(flags) - const client = await createClient(flags) - const reset = await client.app.login.verification.recoveryKey.reset.create({}) - if ((flags.json || !process.stdin.isTTY) && !flags.yes) { - throw new Error('Resetting the recovery key requires --yes in non-interactive mode so the new key can be confirmed.') + throw new Error(`${resetWarning} Pass --yes to confirm in non-interactive mode.`) } + const client = await createClient(flags) + const reset = await client.app.login.verification.recoveryKey.reset.create({}) + + process.stderr.write(`Warning: ${resetWarning}\n`) if (!flags.yes) { process.stderr.write(`New recovery key:\n${reset.recoveryKey}\n`) - if (!await promptYesNoDefaultYes('I saved this recovery key. Use it for this account?')) throw new Error('Recovery key reset cancelled.') + if (!await promptYesNo('I saved this recovery key. Reset now and disconnect my chat accounts?')) throw new Error('Recovery key reset cancelled.') } const confirmed = await client.app.login.verification.recoveryKey.reset.confirm({ recoveryKey: reset.recoveryKey }) diff --git a/packages/cli/test/verify-reset-recovery-key.test.ts b/packages/cli/test/verify-reset-recovery-key.test.ts new file mode 100644 index 00000000..9c162c1e --- /dev/null +++ b/packages/cli/test/verify-reset-recovery-key.test.ts @@ -0,0 +1,35 @@ +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'bun:test' + +const cliRoot = fileURLToPath(new URL('..', import.meta.url)) + +describe('verify reset-recovery-key', () => { + it('refuses without --yes before touching the account, and says accounts will be disconnected', async () => { + const requests: string[] = [] + const server = Bun.serve({ + port: 0, + hostname: '127.0.0.1', + fetch(request) { + requests.push(`${request.method} ${new URL(request.url).pathname}`) + return Response.json({}) + }, + }) + + try { + const child = Bun.spawn([process.execPath, './bin/dev.js', 'verify', 'reset-recovery-key', '--base-url', server.url.origin, '--json'], { + cwd: cliRoot, + env: { ...process.env, BEEPER_ACCESS_TOKEN: 'test-token', BEEPER_CLI_CONFIG_DIR: '/tmp/beeper-cli-bun-test', BEEPER_NO_LOGO: '1' }, + stdin: 'ignore', + stdout: 'pipe', + stderr: 'pipe', + }) + const output = await new Response(child.stdout).text() + await new Response(child.stderr).text() + + expect(await child.exited).not.toBe(0) + expect(output).toContain('signs out every chat account') + expect(requests).toEqual([]) + } finally { + server.stop(true) + } + }, 20_000) +})