diff --git a/extension/src/service-worker/router/__tests__/router.test.ts b/extension/src/service-worker/router/__tests__/router.test.ts index af984d5..b00b0e1 100644 --- a/extension/src/service-worker/router/__tests__/router.test.ts +++ b/extension/src/service-worker/router/__tests__/router.test.ts @@ -26,11 +26,19 @@ vi.mock('../../session', () => ({ requireCurrent: vi.fn(), })); +vi.mock('../../devices', () => ({ + readDevices: vi.fn(), + writeDevices: vi.fn(), + addDevice: vi.fn().mockResolvedValue(undefined), + revokeDevice: vi.fn(), +})); + import { route, type RouterState } from '../index'; import type { Request } from '../../../shared/messages'; import type { Item } from '../../../shared/types'; import * as vault from '../../vault'; import * as session from '../../session'; +import * as devices from '../../devices'; // --- chrome.* shim --- @@ -368,6 +376,38 @@ describe('setup tab exception scope', () => { }); }); +// --- register_this_device: wasm returns a JS object, not a JSON string --- +// +// The #[wasm_bindgen] binding for `generate_device_keypair` uses +// `serde-wasm-bindgen` and returns a plain JsValue (object), not a JSON +// string. Calling JSON.parse on it throws `SyntaxError: "[object Object]" +// is not valid JSON`. This regression test pins the contract. + +describe('register_this_device', () => { + it('treats generate_device_keypair() as an object, not a JSON string', async () => { + const state = makeState(); + state.gitHost = {} as never; + state.wasm.generate_device_keypair = () => ({ + public_key_hex: 'aa'.repeat(32), + private_key_base64: 'AAAA', + }); + + vi.mocked(devices.addDevice).mockClear(); + + const res = await route( + { type: 'register_this_device', name: 'Test Browser' }, + state, + makePopupSender(), + ); + + expect(res).toEqual({ ok: true }); + expect(devices.addDevice).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ name: 'Test Browser', public_key: 'aa'.repeat(32) }), + ); + }); +}); + // --- isContent rejects unknown sender.id --- describe('isContent sender.id guard', () => { diff --git a/extension/src/service-worker/router/popup-only.ts b/extension/src/service-worker/router/popup-only.ts index 15d3582..905e949 100644 --- a/extension/src/service-worker/router/popup-only.ts +++ b/extension/src/service-worker/router/popup-only.ts @@ -312,7 +312,7 @@ export async function handle( case 'register_this_device': { if (!state.gitHost) return { ok: false, error: 'vault_locked' }; - const keypair = JSON.parse(state.wasm.generate_device_keypair()) as { + const keypair = state.wasm.generate_device_keypair() as { public_key_hex: string; private_key_base64: string; }; diff --git a/extension/src/setup/setup.ts b/extension/src/setup/setup.ts index 90eec22..a6aa5eb 100644 --- a/extension/src/setup/setup.ts +++ b/extension/src/setup/setup.ts @@ -1049,9 +1049,7 @@ function attachStep5(): void { try { const w = await loadWasm(); - const keypair = JSON.parse(w.generate_device_keypair()) as { - public_key_hex: string; private_key_base64: string; - }; + const keypair = w.generate_device_keypair(); // 1) Save private key + name locally. await chrome.storage.local.set({ diff --git a/extension/src/wasm.d.ts b/extension/src/wasm.d.ts index 2281bc7..3ba8038 100644 --- a/extension/src/wasm.d.ts +++ b/extension/src/wasm.d.ts @@ -61,7 +61,7 @@ declare module 'relicario-wasm' { export function totp_compute(config_json: string, now_unix_seconds: bigint): TotpCode; - export function generate_device_keypair(): string; + export function generate_device_keypair(): { public_key_hex: string; private_key_base64: string }; export function get_field_history(item_json: string): unknown; export default function init(module_or_path?: unknown): Promise;