From f1ae5841bc4a5c098457da8f36d7b0965e5babf8 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 30 Apr 2026 20:21:47 -0400 Subject: [PATCH] fix(ext): generate_device_keypair returns object not JSON string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wasm-bindgen binding for generate_device_keypair uses serde-wasm-bindgen and returns a plain JsValue (object), not a JSON string. Two consumers were calling JSON.parse on it, causing the runtime error 'SyntaxError: "[object Object]" is not valid JSON' which broke device registration end-to-end. Fixes: - wasm.d.ts: return type now { public_key_hex; private_key_base64 } matching the rate_passphrase pattern (also a JsValue-returning binding). - popup-only.ts (register_this_device handler) and setup.ts (initial device wire-up): drop JSON.parse, use the object directly. - router.test.ts: pin the contract — mock generate_device_keypair as a function returning an object (matching real binding behavior) and assert register_this_device returns ok and forwards the public key. Co-Authored-By: Claude Opus 4.7 --- .../router/__tests__/router.test.ts | 40 +++++++++++++++++++ .../src/service-worker/router/popup-only.ts | 2 +- extension/src/setup/setup.ts | 4 +- extension/src/wasm.d.ts | 2 +- 4 files changed, 43 insertions(+), 5 deletions(-) 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;