vault: Vaultwarden reverse-proxy as the officer-vault sidecar

Transparent pass-through fronting a self-hosted Vaultwarden so the OffVault
(Bitwarden-SDK) app reaches it through the platform's per-app origin gate. True
out-of-process sidecar (officer-vault): it owns all Vaultwarden knowledge (URL,
paths, notifications WebSocket) on a random loopback port and registers via the
sidecar connector; the platform is a thin origin-gated forwarder that knows only
the sidecar's port. Never decrypts/parses/rewrites/logs bodies.

- sidecar/vault: HTTP + notifications-WS proxy to VAULTWARDEN_URL, /_health
- api/vault: sidecar-port discovery + thin forwarder + WS pipe + origin gate
- origin: OFFICER_VAULT_ORIGIN allow-listed, scoped to /api/vault
- mounted top-level (not protected) so the Bitwarden bearer token isn't 401'd
- protocol: vault:server event; ecosystem: officer-vault app

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 01:23:23 +00:00
co-authored by Claude Opus 4.8
parent b2fb6f148c
commit fcf6715844
11 changed files with 549 additions and 1 deletions
+9
View File
@@ -11,6 +11,7 @@ import { pipelineWebsocket } from './servers/api/tasks/pipeline-executor';
import { cliampWebsocket } from './servers/api/cliamp/websocket';
import { cliampAudioWebsocket } from './servers/api/cliamp/audio-ws';
import { desktopWebsocket } from './servers/api/desktop/websocket';
import { vaultWebsocket, upgradeVaultWs } from './servers/api/vault/websocket';
import { findEntryByProxyId, touchEntry } from './servers/api/dev-server/router';
import officerWeb from './apps/officer-web/index.gen.html';
import { startBrowserRelay } from './servers/api/browser/relay';
@@ -42,6 +43,7 @@ type WSData = {
| 'cliamp'
| 'cliamp-audio'
| 'desktop'
| 'vault'
| 'sidecar';
sessionId?: string;
cwd?: string;
@@ -145,6 +147,7 @@ const handlers: Record<string, any> = {
cliamp: cliampWebsocket,
'cliamp-audio': cliampAudioWebsocket,
desktop: desktopWebsocket,
vault: vaultWebsocket,
sidecar: sidecarWebsocket,
};
@@ -315,6 +318,12 @@ const server = serve({
if (req.headers.get('upgrade') === 'websocket') return upgradeDevServerWs(req, server);
return honoServer.fetch(req, server);
},
// Vaultwarden notifications hub: upgrade the WebSocket here (proxied to upstream by vaultWebsocket);
// everything else on this path (SignalR long-poll negotiate/poll) falls through to the HTTP proxy.
'/api/vault/notifications/*': (req, server) => {
if (req.headers.get('upgrade') === 'websocket') return upgradeVaultWs(req, server);
return honoServer.fetch(req, server);
},
'/api/sidecar/register': (req: Request, server: any) => {
const ok = server.upgrade(req, {
data: { provider: 'sidecar', userId: 0, email: '', username: '' },