From e15aa2da84cc9707c2b87186b7be0d731e43bf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Fri, 25 Sep 2026 15:28:48 +0000 Subject: [PATCH] Resolve the active verification ID before verification actions The API has no 'active' alias for verification IDs. Action routes compare the path ID to the current verification and return 409 when they differ, so sas, sas-confirm, approve, cancel, and qr-confirm failed without --id. Look up the real ID from the verification list instead. Fixes #28 --- packages/cli/src/commands/verify/approve.ts | 3 ++- packages/cli/src/commands/verify/cancel.ts | 3 ++- .../cli/src/commands/verify/qr-confirm.ts | 3 ++- .../cli/src/commands/verify/sas-confirm.ts | 3 ++- packages/cli/src/commands/verify/sas.ts | 3 ++- packages/cli/src/lib/app-state.ts | 10 ++++++++ packages/cli/test/verify-id.test.ts | 25 +++++++++++++++++++ 7 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 packages/cli/test/verify-id.test.ts diff --git a/packages/cli/src/commands/verify/approve.ts b/packages/cli/src/commands/verify/approve.ts index 489f8bc4..267775a5 100644 --- a/packages/cli/src/commands/verify/approve.ts +++ b/packages/cli/src/commands/verify/approve.ts @@ -1,6 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' +import { resolveVerificationID } from '../../lib/app-state.js' import { printData } from '../../lib/output.js' export default class AuthVerifyApprove extends BeeperCommand { static override summary = 'Approve a pending device verification request' @@ -11,6 +12,6 @@ export default class AuthVerifyApprove extends BeeperCommand { const { flags } = await this.parse(AuthVerifyApprove) ensureWritable(flags) const client = await createClient(flags) - await printData(await client.app.verifications.accept(flags.id ?? 'active'), flags.json ? 'json' : 'human') + await printData(await client.app.verifications.accept(await resolveVerificationID(client, flags.id)), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/cancel.ts b/packages/cli/src/commands/verify/cancel.ts index f30ddd33..2e09fc65 100644 --- a/packages/cli/src/commands/verify/cancel.ts +++ b/packages/cli/src/commands/verify/cancel.ts @@ -1,6 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' +import { resolveVerificationID } from '../../lib/app-state.js' import { printData } from '../../lib/output.js' export default class AuthVerifyCancel extends BeeperCommand { static override summary = 'Cancel an in-progress device verification' @@ -11,6 +12,6 @@ export default class AuthVerifyCancel extends BeeperCommand { const { flags } = await this.parse(AuthVerifyCancel) ensureWritable(flags) const client = await createClient(flags) - await printData(await client.app.verifications.cancel(flags.id ?? 'active', {}), flags.json ? 'json' : 'human') + await printData(await client.app.verifications.cancel(await resolveVerificationID(client, flags.id), {}), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/qr-confirm.ts b/packages/cli/src/commands/verify/qr-confirm.ts index 0cb190e0..1dc210c9 100644 --- a/packages/cli/src/commands/verify/qr-confirm.ts +++ b/packages/cli/src/commands/verify/qr-confirm.ts @@ -1,6 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' +import { resolveVerificationID } from '../../lib/app-state.js' import { printData } from '../../lib/output.js' export default class AuthVerifyQrConfirm extends BeeperCommand { static override summary = 'Confirm that the other device scanned your QR code' @@ -11,6 +12,6 @@ export default class AuthVerifyQrConfirm extends BeeperCommand { const { flags } = await this.parse(AuthVerifyQrConfirm) ensureWritable(flags) const client = await createClient(flags) - await printData(await client.app.verifications.qr.confirmScanned(flags.id ?? 'active'), flags.json ? 'json' : 'human') + await printData(await client.app.verifications.qr.confirmScanned(await resolveVerificationID(client, flags.id)), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/sas-confirm.ts b/packages/cli/src/commands/verify/sas-confirm.ts index dbd618b0..cbab22ce 100644 --- a/packages/cli/src/commands/verify/sas-confirm.ts +++ b/packages/cli/src/commands/verify/sas-confirm.ts @@ -1,6 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' +import { resolveVerificationID } from '../../lib/app-state.js' import { printData } from '../../lib/output.js' export default class AuthVerifySasConfirm extends BeeperCommand { static override summary = 'Confirm matching emoji verification' @@ -11,6 +12,6 @@ export default class AuthVerifySasConfirm extends BeeperCommand { const { flags } = await this.parse(AuthVerifySasConfirm) ensureWritable(flags) const client = await createClient(flags) - await printData(await client.app.verifications.sas.confirm(flags.id ?? 'active'), flags.json ? 'json' : 'human') + await printData(await client.app.verifications.sas.confirm(await resolveVerificationID(client, flags.id)), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/sas.ts b/packages/cli/src/commands/verify/sas.ts index d184102e..eed25c84 100644 --- a/packages/cli/src/commands/verify/sas.ts +++ b/packages/cli/src/commands/verify/sas.ts @@ -1,6 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' +import { resolveVerificationID } from '../../lib/app-state.js' import { printData } from '../../lib/output.js' export default class AuthVerifySas extends BeeperCommand { static override summary = 'Start emoji verification' @@ -11,6 +12,6 @@ export default class AuthVerifySas extends BeeperCommand { const { flags } = await this.parse(AuthVerifySas) ensureWritable(flags) const client = await createClient(flags) - await printData(await client.app.verifications.sas.start(flags.id ?? 'active'), flags.json ? 'json' : 'human') + await printData(await client.app.verifications.sas.start(await resolveVerificationID(client, flags.id)), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/lib/app-state.ts b/packages/cli/src/lib/app-state.ts index e5ac6c68..7f420e22 100644 --- a/packages/cli/src/lib/app-state.ts +++ b/packages/cli/src/lib/app-state.ts @@ -59,6 +59,16 @@ export async function getAppState(options: { baseURL?: string; target?: string; return appRequest('GET', '/v1/app/setup', options) } +type VerificationLister = { app: { verifications: { list(): PromiseLike<{ items: Array<{ id: string }> }> } } } + +// The API has no "active" alias: action routes 409 unless the path carries the real verification ID. +export async function resolveVerificationID(client: VerificationLister, id?: string): Promise { + if (id) return id + const active = (await client.app.verifications.list()).items[0] + if (!active) throw new Error('No active verification. Start one with `beeper verify`.') + return active.id +} + export async function driveVerification(options: { baseURL?: string; target?: string; userID?: string; yes?: boolean } = {}): Promise { let state = await getAppState(options) if (state.state === 'ready') return state diff --git a/packages/cli/test/verify-id.test.ts b/packages/cli/test/verify-id.test.ts new file mode 100644 index 00000000..aa7c308e --- /dev/null +++ b/packages/cli/test/verify-id.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, mock } from 'bun:test' +import { resolveVerificationID } from '../src/lib/app-state.js' + +const clientWith = (items: Array<{ id: string }>) => { + const list = mock(async () => ({ items })) + return { list, client: { app: { verifications: { list } } } } +} + +describe('resolveVerificationID', () => { + it('uses the active verification ID from the API when no --id is given', async () => { + const { client } = clientWith([{ id: 'txn-123' }]) + expect(await resolveVerificationID(client)).toBe('txn-123') + }) + + it('uses an explicit --id without listing', async () => { + const { client, list } = clientWith([{ id: 'txn-123' }]) + expect(await resolveVerificationID(client, 'txn-explicit')).toBe('txn-explicit') + expect(list).not.toHaveBeenCalled() + }) + + it('fails with a next step when there is no active verification', async () => { + const { client } = clientWith([]) + await expect(resolveVerificationID(client)).rejects.toThrow('No active verification. Start one with `beeper verify`.') + }) +})