feat(ext/sw): multi-context session (personal + orgs), clearAll zeroes all
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
This commit is contained in:
17
extension/src/service-worker/__tests__/org-session.test.ts
Normal file
17
extension/src/service-worker/__tests__/org-session.test.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import type { SessionHandle } from '../../../wasm/relicario_wasm';
|
||||||
|
import * as session from '../session';
|
||||||
|
|
||||||
|
describe('multi-context session', () => {
|
||||||
|
it('clearAll frees personal and every org handle', () => {
|
||||||
|
const free = vi.fn();
|
||||||
|
const mk = (id: number) => ({ value: id, free } as unknown as SessionHandle);
|
||||||
|
session.setPersonal(mk(1));
|
||||||
|
session.setOrg('org-a', mk(2));
|
||||||
|
session.setOrg('org-b', mk(3));
|
||||||
|
session.clearAll();
|
||||||
|
expect(free).toHaveBeenCalledTimes(3);
|
||||||
|
expect(session.getPersonal()).toBeNull();
|
||||||
|
expect(session.getOrg('org-a')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,34 +1,55 @@
|
|||||||
/// Single module-scope "current" SessionHandle.
|
/// Multi-context SW session: one personal handle + a Map of org handles.
|
||||||
///
|
///
|
||||||
/// α assumes one vault per extension install. The master key lives only
|
/// The master key for every context lives only inside WASM linear memory
|
||||||
/// inside WASM linear memory (wrapped in Zeroizing<[u8;32]>); this module
|
/// (wrapped in Zeroizing<[u8;32]>); this module holds the opaque SessionHandle
|
||||||
/// just holds the opaque handle that names it.
|
/// references that name them. Calling `.free()` on a SessionHandle removes
|
||||||
|
/// the entry from the WASM SESSIONS registry and zeroizes master_key and
|
||||||
|
/// image_secret.
|
||||||
///
|
///
|
||||||
/// Future multi-vault (β+) would replace `current` with
|
/// Security invariant: a vault lock OR an inactivity-timer expiry must zero
|
||||||
/// `Map<vaultId, SessionHandle>` and thread `vaultId` through every
|
/// EVERY handle — personal AND every org handle. `clearAll()` is the
|
||||||
/// handler. Deliberate α simplicity — not an oversight.
|
/// canonical zero-all path. `clearCurrent()` is aliased to `clearAll()` so
|
||||||
|
/// the existing lock handler (popup-only.ts) and inactivity timer (index.ts)
|
||||||
|
/// automatically zero org handles without code changes.
|
||||||
///
|
///
|
||||||
/// As of Phase 1 of the security-polish series, `impl Drop for SessionHandle`
|
/// The org master key MUST NEVER be written to localStorage, IndexedDB, or
|
||||||
/// on the Rust side makes `.free()` the meaningful cleanup call: it removes
|
/// any persistent store. Only in-memory SessionHandle references live here.
|
||||||
/// the entry from the SESSIONS registry and zeroizes `master_key` and
|
|
||||||
/// `image_secret`. Calling `wasm.lock(handle.value)` before `.free()` would
|
|
||||||
/// be redundant belt-and-suspenders; this module intentionally does not.
|
|
||||||
|
|
||||||
import type { SessionHandle } from '../../wasm/relicario_wasm';
|
import type { SessionHandle } from '../../wasm/relicario_wasm';
|
||||||
|
|
||||||
let current: SessionHandle | null = null;
|
let personal: SessionHandle | null = null;
|
||||||
|
const orgs = new Map<string, SessionHandle>();
|
||||||
|
let context: 'personal' | string = 'personal';
|
||||||
|
|
||||||
export function setCurrent(h: SessionHandle): void { current = h; }
|
export function setPersonal(h: SessionHandle): void { personal = h; }
|
||||||
|
export function getPersonal(): SessionHandle | null { return personal; }
|
||||||
|
|
||||||
export function getCurrent(): SessionHandle | null { return current; }
|
export function setOrg(orgId: string, h: SessionHandle): void { orgs.set(orgId, h); }
|
||||||
|
export function getOrg(orgId: string): SessionHandle | null { return orgs.get(orgId) ?? null; }
|
||||||
|
|
||||||
|
export function setContext(c: 'personal' | string): void { context = c; }
|
||||||
|
export function currentContext(): 'personal' | string { return context; }
|
||||||
|
|
||||||
|
export function requireCurrentHandle(): SessionHandle {
|
||||||
|
const h = context === 'personal' ? personal : orgs.get(context) ?? null;
|
||||||
|
if (!h) throw new Error('vault_locked');
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAll(): void {
|
||||||
|
if (personal) { personal.free(); personal = null; }
|
||||||
|
for (const [, h] of orgs) h.free();
|
||||||
|
orgs.clear();
|
||||||
|
context = 'personal';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back-compat wrappers so existing personal-vault callers compile unchanged.
|
||||||
|
// clearCurrent() aliases clearAll() so the lock handler and inactivity timer
|
||||||
|
// in popup-only.ts and index.ts zero EVERY handle (personal + all orgs).
|
||||||
|
export function setCurrent(h: SessionHandle): void { setPersonal(h); }
|
||||||
|
export function getCurrent(): SessionHandle | null { return getPersonal(); }
|
||||||
export function requireCurrent(): SessionHandle {
|
export function requireCurrent(): SessionHandle {
|
||||||
if (!current) throw new Error('vault_locked');
|
if (!personal) throw new Error('vault_locked');
|
||||||
return current;
|
return personal;
|
||||||
}
|
|
||||||
|
|
||||||
export function clearCurrent(): void {
|
|
||||||
if (!current) return;
|
|
||||||
current.free();
|
|
||||||
current = null;
|
|
||||||
}
|
}
|
||||||
|
export function clearCurrent(): void { clearAll(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user