Summary#
The /cabinet route guard checks session.status but not identity.status. A user who logs in but does not complete seed phrase setup can navigate directly to /cabinet/files. Crypto operations then run with missing keys, producing errors or potentially writing unrecoverable ciphertext.
Reproduction#
- Log in via OAuth (session becomes
active) - Do NOT complete seed phrase setup (identity stays
uncheckedorfresh) - Navigate directly to
/cabinet/filesvia URL bar or deep link - The route loads —
loadCabinetfires, callsstorage.loadIdentity(did)which throws - Cabinet shows a blank/error state instead of redirecting to
/devices
Impact#
- Silent encryption failures if identity happens to be partially available
- Confusing UX — user sees a broken cabinet instead of being guided to set up their keys
- Deep links to cabinet files bypass the normal devices → identity setup → cabinet flow
Location#
web/src/routes/cabinet/route.tsx:65-78 — the beforeLoad guard:
beforeLoad: async () => {
const state = useAuthStore.getState();
if (state.session.status === "initializing") {
await state.boot();
}
if (useAuthStore.getState().session.status !== "active") {
throw redirect({ to: "/devices/login" });
}
// No identity check here — the bug
},
Proposed fix#
Check for local identity in IndexedDB before allowing cabinet access:
const { did } = useAuthStore.getState().session;
const hasIdentity = await storage.loadIdentity(did).then(() => true, () => false);
if (!hasIdentity) {
throw redirect({ to: "/devices" });
}
Important: do NOT call checkIdentity() in the route guard — it does a PDS call (fetchUpstreamPublicKey) that blocks rendering while WASM initializes. Check IndexedDB directly instead.
Regression test#
e2e-tests/tests/web/identity-guard.test.ts — logs in, skips seed phrase, navigates to /cabinet, verifies the page is blocked.