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:
@@ -22,8 +22,6 @@ import { dashboardsRouter } from './api/dashboards';
|
||||
import { router as fileBrowserRouter } from './api/file-browser/router';
|
||||
import { devProxyRouter } from './api/dev-proxy/router';
|
||||
import { pluginsRouter } from './api/plugins/router';
|
||||
import { vaultRouter } from './api/vault/router';
|
||||
import { publicVaultRouter, VAULT_ONLY_PREFIXES, isBitwardenClient } from './api/vault/public-router';
|
||||
import { agentHandoffRouter } from './api/agent-handoff/router';
|
||||
// import { slskdRouter } from './api/slskd/router';
|
||||
// import { transmissionRouter } from './api/transmission/router';
|
||||
@@ -42,9 +40,6 @@ import { claimIosProfile } from './api/dav/ios-profile';
|
||||
// import { notifyRouter } from './api/notify/router';
|
||||
import { systemMonitorRouter } from './api/system-monitor/system-monitor';
|
||||
import { activityRouter } from './api/activity/router';
|
||||
// Vault still hand-rolls its port capture, so it keeps a side-effect import; every other HTTP sidecar
|
||||
// registers its listener when createSidecarProxy runs inside the router this file already imports.
|
||||
import './api/vault/sidecar-server'; // side-effect: capture the officer-vault reverse-proxy port
|
||||
import { dockRouter } from './api/dock/dock';
|
||||
import { integrationsRouter, googleCallbackHandler } from './api/integrations/integrations';
|
||||
import { queueRouter } from './api/queue/queue';
|
||||
@@ -175,7 +170,6 @@ export const UNPROTECTED_API_PREFIXES: string[] = [
|
||||
'/landing-page-data',
|
||||
'/themes',
|
||||
'/waitlist',
|
||||
'/vault',
|
||||
'/sidecar',
|
||||
'/agent-handoff',
|
||||
'/dev-proxy',
|
||||
@@ -202,31 +196,6 @@ export function buildHonoApp(plugins: MountedPlugin[] = []): Hono<{ Variables: H
|
||||
app.route('/api/landing-page-data', landingPageDataRouter);
|
||||
app.route('/api/themes', themesRouter);
|
||||
app.route('/api/waitlist', waitlistRouter);
|
||||
// Vaultwarden reverse-proxy — mounted TOP-LEVEL (not under protectedRouter): the Bitwarden client
|
||||
// carries its own bearer token, not a platform session JWT, so userMiddleware would 401 it. The
|
||||
// notifications WebSocket is upgraded at the serve level (server.tsx).
|
||||
app.route('/api/vault', vaultRouter);
|
||||
|
||||
// The same Vaultwarden, with NO Officer authentication, so the Bitwarden browser extension can point at
|
||||
// this host instead of at a second public hostname for Vaultwarden. Deliberately its own mount rather
|
||||
// than a mode of the router above: that one requires an Officer session and swaps the caller's
|
||||
// Authorization header for a server-held token, and blending the two would put an unauthenticated branch
|
||||
// inside the authenticated path. Temporary — see public-router.ts for what replaces it and why leaving it
|
||||
// open is not a new exposure.
|
||||
app.route('/vaultwarden', publicVaultRouter);
|
||||
|
||||
// …and at the ROOT, so the extension can be pointed at the bare Officer URL with no path at all.
|
||||
//
|
||||
// Registered BEFORE `/api` is mounted, because hono matches in registration order and this has to win
|
||||
// for a Bitwarden client. It is deliberately narrow: the four prefixes below belong to Vaultwarden and
|
||||
// to nothing else here, and `/api/*` is diverted ONLY when the request carries a Bitwarden client
|
||||
// header. An ordinary Officer request never matches, so nothing that worked before changes.
|
||||
for (const prefix of VAULT_ONLY_PREFIXES) app.route(prefix, publicVaultRouter);
|
||||
|
||||
app.use('/api/*', async (ctx, next) => {
|
||||
if (!isBitwardenClient(ctx.req.raw.headers)) return next();
|
||||
return publicVaultRouter.fetch(ctx.req.raw, ctx.env);
|
||||
});
|
||||
app.get('/api/integrations/google/callback', googleCallbackHandler);
|
||||
|
||||
// Agent-to-agent handoff — mounted TOP-LEVEL for the same reason the vault is: the caller is a Claude
|
||||
|
||||
Reference in New Issue
Block a user