vault, extracted into plugins/vault — the platform half
Fourth extraction, after offscale, music and email. Everything vault is now at
gitea.officer.dev/plugins/vault and installs from the marketplace; this commit
is what the platform stopped knowing.
Deleted, not commented: both routers, the sidecar, the db directory, the
hono.ts mounts/imports, server.tsx's ws import + handler entry + four
route-table entries, '/vault' from UNPROTECTED_API_PREFIXES, both entries in
totality.ts's exemption lists, both database barrels, and the app-store
catalogue entry.
The catalogue deletion is required rather than tidying — the trap the runbook
records as having bitten twice. permissionAvailability() derives from
sidecar_installs, which a plugin never gets a row in, so a leftover entry makes
the permission permanently 'unavailable' and withholds the route and dock tile
on a server where the plugin is installed and working.
── The one new piece of platform: src/servers/account-lifecycle.ts ──
Three auth handlers (signout, distress, panic) imported clearVaultTokens and
clearVaultUnlockKey directly. That is the security property behind the distress
and panic buttons — "someone got in, make the vault need a master password
again" — so it could not just be dropped, and a plugin cannot be imported by
core.
Rejected: having core DELETE from the plugin's tables by name. It removes the
import but moves the coupling somewhere worse — core would depend on a plugin's
SCHEMA, so renaming a column would silently stop the wipe. A security property
that fails silently on an unrelated change is the failure mode to design
against. (docs/email-plugin-extraction.md took that shape for READING a
plugin's data, where a stale answer is a missing feature rather than a lock
left open. The stakes decide it, not the pattern.)
Taken instead: core announces 'signout' | 'distress' | 'panic' with a user id,
and anything holding per-account state subscribes. ~30 lines, fire-and-forget,
one listener's failure cannot stop another. Core names no plugin, no table and
no column; a listener that is not there is a plugin that is not installed. The
severity mapping moved into the plugin — signout keeps the protector key,
distress and panic drop it — so the behaviour is unchanged.
── Two things deleted rather than carried, both dark since 2026-08-13 ──
The unauthenticated Bitwarden passthrough (/vaultwarden plus four root
prefixes and an /api/* header diverter). A plugin's routes mount into
protectedRouter at /api/<name>, behind the account gate; top-level
unauthenticated prefixes are not something a manifest can declare, and the Bun
route table they need entries in is built once at serve(). Keeping it meant
core naming /vaultwarden and /identity in its own route table — the vocabulary
leak the runbook forbids, for a path its own comments called TEMPORARY pending
a first-party browser extension. Owner's call: nice to have, not a requirement.
The notifications websocket, for the same reason plus a second: no plugin owns
a socket on this platform yet (server.reload({routes}) proven, never called;
handlers in server.tsx is a static map). Music parked cliamp/ for exactly this.
SignalR degrades to HTTP long-polling by itself, so clients still sync.
── Tests ──
Vault was the ONLY members: 'invite' catalogue entry, so deleting it would have
silently dropped coverage of that branch in plannedOutcome. It is now tested
against a synthetic entry instead: deleting an assertion along with the last
real user of a case is how a switch arm stops being covered without anyone
deciding it should.
bunx tsgo clean (verified it really does cover plugins/ by injecting a
deliberate error and watching it fail). Suite back at its 7 pre-existing
baseline failures — none of them this feature's, all confirmed unrelated
(the totality ones are about 'desktop'). 85 test files, unchanged, per the
runbook's trap about a moved test vanishing silently.
Needs `bun db:push` and a restart before installing, then point it at a
Vaultwarden from /vault — VAULTWARDEN_URL in .env is no longer read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+1
-21
@@ -18,7 +18,6 @@ import { chatWebsocket } from './servers/api/chat/websocket';
|
||||
import { taskRunnerWebsocket } from './servers/api/tasks/task-executor';
|
||||
import { pipelineWebsocket } from './servers/api/tasks/pipeline-executor';
|
||||
// import { desktopWebsocket } from './servers/api/desktop/websocket';
|
||||
import { vaultWebsocket, upgradeVaultWs } from './servers/api/vault/websocket';
|
||||
import officerWeb from './apps/officer-web/index.gen.html';
|
||||
// import { startBrowserRelay } from './servers/api/browser/relay'; // switched off — see below
|
||||
import { registerSidecar, unregisterSidecar, handleSidecarMessage } from './servers/sidecar-registry';
|
||||
@@ -52,7 +51,7 @@ type WSData = {
|
||||
userId: number;
|
||||
email: string;
|
||||
username: string;
|
||||
provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'desktop' | 'vault' | 'sidecar';
|
||||
provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'desktop' | 'sidecar';
|
||||
sessionId?: string;
|
||||
cwd?: string;
|
||||
command?: string;
|
||||
@@ -144,7 +143,6 @@ const handlers: Record<string, any> = {
|
||||
'task-runner': taskRunnerWebsocket,
|
||||
pipeline: pipelineWebsocket,
|
||||
// desktop: desktopWebsocket,
|
||||
vault: vaultWebsocket,
|
||||
sidecar: sidecarWebsocket,
|
||||
};
|
||||
|
||||
@@ -405,12 +403,6 @@ const server = serve({
|
||||
if (!(await file.exists())) return new Response('Not found', { status: 404 });
|
||||
return new Response(file);
|
||||
},
|
||||
// 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);
|
||||
},
|
||||
// A dev server's HMR socket, framed by the Preview panel. Must be in the route table rather than
|
||||
// behind the `/api/*` catch-all: Bun matches this table first, and an upgrade that reaches Hono is
|
||||
// an ordinary GET by then.
|
||||
@@ -437,18 +429,6 @@ const server = serve({
|
||||
// '/.well-known/carddav': honoServer.fetch,
|
||||
// '/dav': honoServer.fetch,
|
||||
// '/dav/*': honoServer.fetch,
|
||||
// Vaultwarden for third-party Bitwarden clients, unauthenticated at Officer's layer. Needs its own
|
||||
// entry for the same reason /dav does: only the paths listed here reach hono, and anything else
|
||||
// falls through to the SPA — which answers 200 with the React shell, so a missing line here looks
|
||||
// like a working endpoint returning nonsense rather than a 404.
|
||||
'/vaultwarden/*': honoServer.fetch,
|
||||
// The same proxy at the root, so the extension needs only the bare Officer URL. These four prefixes
|
||||
// are Vaultwarden's alone — nothing in Officer answers on them — so routing them here costs nothing.
|
||||
// `/api/*` already reaches hono below, where a Bitwarden client header diverts it.
|
||||
'/identity/*': honoServer.fetch,
|
||||
'/notifications/*': honoServer.fetch,
|
||||
'/icons/*': honoServer.fetch,
|
||||
'/events/*': honoServer.fetch,
|
||||
...spaRoutes,
|
||||
'/api': honoServer.fetch,
|
||||
// A CLOSURE, deliberately, and not the bound `honoServer.fetch`.
|
||||
|
||||
Reference in New Issue
Block a user