fix(ext): generate_device_keypair returns object not JSON string
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 <noreply@anthropic.com>
This commit is contained in:
@@ -26,11 +26,19 @@ vi.mock('../../session', () => ({
|
|||||||
requireCurrent: vi.fn(),
|
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 { route, type RouterState } from '../index';
|
||||||
import type { Request } from '../../../shared/messages';
|
import type { Request } from '../../../shared/messages';
|
||||||
import type { Item } from '../../../shared/types';
|
import type { Item } from '../../../shared/types';
|
||||||
import * as vault from '../../vault';
|
import * as vault from '../../vault';
|
||||||
import * as session from '../../session';
|
import * as session from '../../session';
|
||||||
|
import * as devices from '../../devices';
|
||||||
|
|
||||||
// --- chrome.* shim ---
|
// --- 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 ---
|
// --- isContent rejects unknown sender.id ---
|
||||||
|
|
||||||
describe('isContent sender.id guard', () => {
|
describe('isContent sender.id guard', () => {
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ export async function handle(
|
|||||||
|
|
||||||
case 'register_this_device': {
|
case 'register_this_device': {
|
||||||
if (!state.gitHost) return { ok: false, error: 'vault_locked' };
|
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;
|
public_key_hex: string;
|
||||||
private_key_base64: string;
|
private_key_base64: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1049,9 +1049,7 @@ function attachStep5(): void {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const w = await loadWasm();
|
const w = await loadWasm();
|
||||||
const keypair = JSON.parse(w.generate_device_keypair()) as {
|
const keypair = w.generate_device_keypair();
|
||||||
public_key_hex: string; private_key_base64: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1) Save private key + name locally.
|
// 1) Save private key + name locally.
|
||||||
await chrome.storage.local.set({
|
await chrome.storage.local.set({
|
||||||
|
|||||||
2
extension/src/wasm.d.ts
vendored
2
extension/src/wasm.d.ts
vendored
@@ -61,7 +61,7 @@ declare module 'relicario-wasm' {
|
|||||||
|
|
||||||
export function totp_compute(config_json: string, now_unix_seconds: bigint): TotpCode;
|
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 function get_field_history(item_json: string): unknown;
|
||||||
|
|
||||||
export default function init(module_or_path?: unknown): Promise<void>;
|
export default function init(module_or_path?: unknown): Promise<void>;
|
||||||
|
|||||||
Reference in New Issue
Block a user