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`.') + }) +})