feat(ext/sw): get_vault_settings + update_vault_settings popup-only messages

This commit is contained in:
adlee-was-taken
2026-04-24 18:56:17 -04:00
parent b52e49a51e
commit e47945d86a
3 changed files with 104 additions and 2 deletions

View File

@@ -654,3 +654,81 @@ describe('get_totp handler covers both Login.totp and Totp.config', () => {
expect(res).toEqual({ ok: false, error: 'no_totp' }); expect(res).toEqual({ ok: false, error: 'no_totp' });
}); });
}); });
// --- get_vault_settings / update_vault_settings (β₂ Slice 3) ---
describe('get_vault_settings / update_vault_settings', () => {
function primeUnlocked(state: RouterState): void {
vi.mocked(session.getCurrent).mockReturnValue({ free: () => {} } as never);
state.gitHost = {} as never;
}
beforeEach(() => {
vi.mocked(session.getCurrent).mockReset();
vi.mocked(vault.fetchAndDecryptSettings).mockReset();
vi.mocked(vault.encryptAndWriteSettings).mockReset();
});
it('get_vault_settings accepted from popup; returns VaultSettings', async () => {
const state = makeState();
primeUnlocked(state);
const mockSettings = {
trash_retention: { kind: 'days', value: 30 },
field_history_retention: { kind: 'forever' },
generator_defaults: {
kind: 'random', length: 20,
classes: { lower: true, upper: true, digits: true, symbols: true },
symbol_charset: { kind: 'safe_only' },
},
attachment_caps: {},
autofill_origin_acks: { 'github.com': 1000 },
};
vi.mocked(vault.fetchAndDecryptSettings).mockResolvedValueOnce(mockSettings as never);
const res = await route({ type: 'get_vault_settings' }, state, makePopupSender());
expect(res).toMatchObject({ ok: true });
if (res.ok) {
const d = res.data as { settings: typeof mockSettings };
expect(d.settings).toEqual(mockSettings);
}
});
it('get_vault_settings rejected from content', async () => {
const state = makeState();
const res = await route({ type: 'get_vault_settings' }, state, makeContentSender());
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
});
it('update_vault_settings accepted from popup; calls encryptAndWriteSettings', async () => {
const state = makeState();
primeUnlocked(state);
vi.mocked(vault.encryptAndWriteSettings).mockResolvedValueOnce(undefined);
const newSettings = {
trash_retention: { kind: 'forever' },
field_history_retention: { kind: 'last_n', value: 5 },
generator_defaults: {
kind: 'bip39', word_count: 6, separator: '-', capitalization: 'lower',
},
attachment_caps: {},
autofill_origin_acks: {},
};
const res = await route(
{ type: 'update_vault_settings', settings: newSettings as never },
state,
makePopupSender(),
);
expect(res).toMatchObject({ ok: true });
expect(vault.encryptAndWriteSettings).toHaveBeenCalledWith(
expect.anything(), expect.anything(), newSettings, expect.any(String),
);
});
it('update_vault_settings rejected from setup tab (not in SETUP_ALLOWED)', async () => {
const state = makeState();
const res = await route(
{ type: 'update_vault_settings', settings: {} as never },
state,
makeSetupSender(),
);
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
});
});

View File

@@ -171,6 +171,23 @@ export async function handle(
return { ok: true }; return { ok: true };
} }
case 'get_vault_settings': {
const handle = session.getCurrent();
if (!handle || !state.gitHost) return { ok: false, error: 'vault_locked' };
const settings = await vault.fetchAndDecryptSettings(state.gitHost, handle);
return { ok: true, data: { settings } };
}
case 'update_vault_settings': {
const handle = session.getCurrent();
if (!handle || !state.gitHost) return { ok: false, error: 'vault_locked' };
await vault.encryptAndWriteSettings(
state.gitHost, handle, msg.settings,
'settings: update vault-level config',
);
return { ok: true };
}
case 'get_blacklist': case 'get_blacklist':
return { ok: true, data: { blacklist: await loadBlacklist() } }; return { ok: true, data: { blacklist: await loadBlacklist() } };

View File

@@ -1,6 +1,6 @@
import type { import type {
Item, ItemId, Manifest, ManifestEntry, VaultConfig, SetupState, Item, ItemId, Manifest, ManifestEntry, VaultConfig, SetupState,
DeviceSettings, GeneratorRequest, DeviceSettings, GeneratorRequest, VaultSettings,
} from './types'; } from './types';
// --- Messages a popup (or setup page) may send --- // --- Messages a popup (or setup page) may send ---
@@ -24,6 +24,8 @@ export type PopupMessage =
| { type: 'ack_autofill_origin'; hostname: string } | { type: 'ack_autofill_origin'; hostname: string }
| { type: 'get_settings' } | { type: 'get_settings' }
| { type: 'update_settings'; settings: Partial<DeviceSettings> } | { type: 'update_settings'; settings: Partial<DeviceSettings> }
| { type: 'get_vault_settings' }
| { type: 'update_vault_settings'; settings: VaultSettings }
| { type: 'get_blacklist' } | { type: 'get_blacklist' }
| { type: 'remove_blacklist'; hostname: string }; | { type: 'remove_blacklist'; hostname: string };
@@ -88,13 +90,18 @@ export interface RatePassphraseResponse extends Extract<Response, { ok: true }>
data: { score: number; guesses_log10: number }; data: { score: number; guesses_log10: number };
} }
export interface VaultSettingsResponse extends Extract<Response, { ok: true }> {
data: { settings: VaultSettings };
}
// --- Capability sets (consumed by the router) --- // --- Capability sets (consumed by the router) ---
export const POPUP_ONLY_TYPES: ReadonlySet<PopupMessage['type']> = new Set([ export const POPUP_ONLY_TYPES: ReadonlySet<PopupMessage['type']> = new Set([
'is_unlocked', 'unlock', 'lock', 'list_items', 'get_item', 'add_item', 'is_unlocked', 'unlock', 'lock', 'list_items', 'get_item', 'add_item',
'update_item', 'delete_item', 'get_totp', 'sync', 'get_setup_state', 'update_item', 'delete_item', 'get_totp', 'sync', 'get_setup_state',
'save_setup', 'rate_passphrase', 'generate_password', 'fill_credentials', 'save_setup', 'rate_passphrase', 'generate_password', 'fill_credentials',
'ack_autofill_origin', 'get_settings', 'update_settings', 'get_blacklist', 'ack_autofill_origin', 'get_settings', 'update_settings',
'get_vault_settings', 'update_vault_settings', 'get_blacklist',
'remove_blacklist', 'remove_blacklist',
] as PopupMessage['type'][]); ] as PopupMessage['type'][]);