vault: session-gated notifications WS + lifecycle cleanup
- token-store: shared "give me a valid Vaultwarden access token" (proactive refresh) used by both the HTTP proxy and the WS; router refactored onto it. - notifications WS: validates the platform session in `open` (deferred, owner only), injects the stored Vaultwarden token into the upstream, and buffers client frames during the async setup so the SignalR handshake isn't dropped. The device connects with its platform JWT (?access_token=), never a vault one. - lifecycle: logout drops the vault token set (keeps the protector); distress (/auth/revoke) and panic wipe both token set and protector, forcing a one-time master-password re-setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
import type { Handler } from 'hono';
|
||||
import { clearVaultTokens, clearVaultUnlockKey } from 'officerdb';
|
||||
import { triggerLockdown } from './panic';
|
||||
import { clientIp } from './client-ip';
|
||||
|
||||
// Trigger the panic lockdown. Authenticated (Bearer token) — no password in the body. Once tripped,
|
||||
// all logins and existing sessions are refused until the server is manually restarted.
|
||||
export const panicHandler: Handler = async (ctx) => {
|
||||
const user = ctx.get('user') as { email?: string } | undefined;
|
||||
const user = ctx.get('user') as { id?: number; email?: string } | undefined;
|
||||
const origin = ctx.get('origin') as string | undefined;
|
||||
triggerLockdown(`/panic (user=${user?.email || '?'}, origin=${origin || '-'}, ip=${clientIp(ctx)})`);
|
||||
|
||||
// Panic wipes the vault session AND the protector key (like distress).
|
||||
if (user?.id) {
|
||||
clearVaultTokens(user.id).catch(() => {});
|
||||
clearVaultUnlockKey(user.id).catch(() => {});
|
||||
}
|
||||
|
||||
return ctx.json({ ok: true });
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Handler } from 'hono';
|
||||
import { blacklistToken, cleanupExpiredTokens } from 'officerdb';
|
||||
import { blacklistToken, cleanupExpiredTokens, clearVaultTokens, clearVaultUnlockKey } from 'officerdb';
|
||||
import { clientIp } from './client-ip';
|
||||
|
||||
// Distress: blacklist the current token (same as signout) but log it as a security event.
|
||||
export const revokeHandler: Handler = async (ctx) => {
|
||||
const user = ctx.get('user') as { jti: string; exp: number; email?: string };
|
||||
const user = ctx.get('user') as { id: number; jti: string; exp: number; email?: string };
|
||||
const origin = ctx.get('origin') as string | undefined;
|
||||
|
||||
console.warn(
|
||||
@@ -12,6 +12,12 @@ export const revokeHandler: Handler = async (ctx) => {
|
||||
);
|
||||
|
||||
await blacklistToken(user.jti, user.exp);
|
||||
|
||||
// Distress wipes the vault session AND the stored protector key — forces a one-time master-password
|
||||
// re-setup, the correct "someone got in" behavior.
|
||||
clearVaultTokens(user.id).catch(() => {});
|
||||
clearVaultUnlockKey(user.id).catch(() => {});
|
||||
|
||||
cleanupExpiredTokens().catch(() => {});
|
||||
|
||||
return ctx.json({ ok: true });
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { Handler } from 'hono';
|
||||
import { blacklistToken, cleanupExpiredTokens } from 'officerdb';
|
||||
import { blacklistToken, cleanupExpiredTokens, clearVaultTokens } from 'officerdb';
|
||||
|
||||
export const signoutHandler: Handler = async (ctx) => {
|
||||
const user = ctx.get('user') as { jti: string; exp: number };
|
||||
const user = ctx.get('user') as { id: number; jti: string; exp: number };
|
||||
|
||||
await blacklistToken(user.jti, user.exp);
|
||||
|
||||
// Drop the brokered vault session on logout (the protector key stays, so re-login is frictionless).
|
||||
clearVaultTokens(user.id).catch(() => {});
|
||||
|
||||
// Opportunistic cleanup of expired tokens (non-blocking)
|
||||
cleanupExpiredTokens().catch(() => {});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user