refactor(ext/state): lift vault_locked intercept into shared/state.ts (Plan C Phase 4)

The session-lost intercept lived in vault.ts's local sendMessage; both surfaces
now consume it through the shared sendMessage() wrapper. On a vault_locked
response to any non-bypassed request, the wrapper calls host.navigate('locked').
The vault host's navigate gains a 'locked' branch (it shows its lock screen off
state.unlocked); the popup's navigate already handles 'locked'. vault.ts routes
ctx.sendMessage through the shared wrapper and registers a plain transport as
host.sendMessage, so internal RPCs keep the intercept without recursion.
grep -c vault_locked vault.ts == 0. New state-vault-locked.test.ts (TDD, 6 cases).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-31 20:20:52 -04:00
parent 31913b8648
commit 0c722b3a9d
3 changed files with 107 additions and 30 deletions

View File

@@ -50,13 +50,33 @@ export function navigate(view: View, extras?: Partial<PopupState>): void {
host.navigate(view, extras);
}
// Requests that must NOT trigger the lock screen on a vault_locked response:
// they run during cold start / unlock, before a session exists, so a
// vault_locked here is expected rather than a lost session.
const VAULT_LOCKED_BYPASS: ReadonlySet<Request['type']> = new Set([
'unlock', 'is_unlocked',
]);
/**
* Phase 4 will add a vault_locked intercept here. For now, this is a pure
* pass-through so the signature is stable for Phase 4 to fill.
* Dispatches a request to the service worker and intercepts the `vault_locked`
* response. MV3 evicts the service worker after ~30s idle, wiping the in-memory
* session/manifest; the next RPC comes back `vault_locked`. Any surface (popup
* or vault tab) that gets that on a non-bypassed request treats it as "session
* lost" and navigates to the lock screen so the user can re-enter their
* passphrase. Lifted here from vault.ts's local sendMessage in Plan C Phase 4
* so both surfaces share one channel.
*/
export async function sendMessage(request: Request): Promise<Response> {
if (!host) throw new Error('No state host registered');
return host.sendMessage(request);
const response = await host.sendMessage(request);
if (
!response.ok &&
response.error === 'vault_locked' &&
!VAULT_LOCKED_BYPASS.has(request.type)
) {
host.navigate('locked', { error: 'Session expired — please unlock again.' });
}
return response;
}
export function escapeHtml(s: string): string {