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:
@@ -5,7 +5,7 @@
|
||||
/// vault tab's pane area.
|
||||
|
||||
import type { Request, Response } from '../shared/messages';
|
||||
import { registerHost } from '../shared/state';
|
||||
import { registerHost, sendMessage } from '../shared/state';
|
||||
import { type ErrorCta } from '../shared/error-copy';
|
||||
import {
|
||||
type VaultController, type VaultState, type VaultView,
|
||||
@@ -27,32 +27,12 @@ import { parseHash, setHash, renderPane, loadManifest, selectItem } from './vaul
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sendMessage(request: Request): Promise<Response> {
|
||||
// Plain transport to the service worker, registered as the host's sendMessage.
|
||||
// The shared sendMessage() wrapper (shared/state.ts) layers the session-lost
|
||||
// → lock-screen intercept on top of this for every UI RPC.
|
||||
function postToServiceWorker(request: Request): Promise<Response> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage(request, (response: Response) => {
|
||||
// MV3 service workers are evicted after ~30s idle, which wipes the
|
||||
// in-memory session/manifest/gitHost. The fullscreen tab stays open
|
||||
// and has no signal that the SW restarted — the next RPC just comes
|
||||
// back `vault_locked`. Treat that as "session lost" and force the
|
||||
// lock screen so the user can re-enter their passphrase. Skip for
|
||||
// is_unlocked / unlock themselves to avoid loops on cold start.
|
||||
if (
|
||||
response &&
|
||||
!response.ok &&
|
||||
response.error === 'vault_locked' &&
|
||||
request.type !== 'is_unlocked' &&
|
||||
request.type !== 'unlock' &&
|
||||
state.unlocked
|
||||
) {
|
||||
state.unlocked = false;
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
state.entries = [];
|
||||
state.error = 'Session expired — please unlock again.';
|
||||
render(ctx);
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
chrome.runtime.sendMessage(request, (response: Response) => resolve(response));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,6 +95,17 @@ registerHost({
|
||||
renderPane(ctx);
|
||||
},
|
||||
navigate: (view, extras) => {
|
||||
if (view === 'locked') {
|
||||
// Session lost (SW evicted mid-session). The vault shows its lock
|
||||
// screen off state.unlocked, so flip it and drop the in-memory data.
|
||||
state.unlocked = false;
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
state.entries = [];
|
||||
state.error = (extras?.error as string | undefined) ?? null;
|
||||
render(ctx);
|
||||
return;
|
||||
}
|
||||
Object.assign(state, { view, error: null, loading: false, ...extras });
|
||||
setHash(view as VaultView);
|
||||
applyShellViewClass(ctx);
|
||||
@@ -122,7 +113,7 @@ registerHost({
|
||||
if (state.view === 'list') renderListPane(ctx);
|
||||
renderPane(ctx);
|
||||
},
|
||||
sendMessage,
|
||||
sendMessage: postToServiceWorker,
|
||||
escapeHtml,
|
||||
popOutToTab: () => {},
|
||||
isInTab: () => true,
|
||||
|
||||
Reference in New Issue
Block a user