claim the root paths back, so the setup is the bare Officer URL again

Before the extraction this worked by pasting https://officer.example.com into a
Bitwarden client and logging in normally. The extraction moved the door to
/api/vault/bw, which needs five URLs configured by hand — technically the same
thing, practically nobody's setup.

Declares what it actually needs, now that the platform can express it:

  unauthenticatedRootPaths  /identity, /notifications, /icons, /events — the
                            four a Bitwarden client derives from a bare server
                            URL. None is a name Officer serves, so the path
                            alone decides and no sniffing is involved.

  protocolHeaders           bitwarden-client-name, bitwarden-client-version,
                            device-type — for /api/*, the one namespace both
                            servers claim, where the path cannot separate
                            Vaultwarden's /api/sync from Officer's /api/chat.

`publicRouter` is exported for the platform to mount above the gate. One router
serves all three doors: it strips /api/vault/bw when reached that way, and
otherwise the path is already Vaultwarden's own.

/bw stays. It costs nothing and remains the explicit address for a client that
would rather be told than infer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:47:20 +00:00
co-authored by Claude Opus 5
parent 224a525751
commit 4006be0e3b
3 changed files with 40 additions and 0 deletions
+5
View File
@@ -55,6 +55,11 @@ publicVaultRouter.all('/*', async (ctx) => {
if (!base) return ctx.text('Vault sidecar not available', 503);
const url = new URL(ctx.req.url);
// Reached three ways, and the path is normalised for all of them:
// /api/vault/bw/identity/... the explicit address — strip the prefix
// /identity/... a claimed top-level path — already Vaultwarden's own path
// /api/sync diverted by a Bitwarden client header — also already correct
// Stripping only when the prefix is actually present is what makes one router serve all three.
const subpath = (url.pathname.startsWith(PREFIX) ? url.pathname.slice(PREFIX.length) : url.pathname) || '/';
const target = `${base}${subpath}${url.search}`;
const method = ctx.req.method;
+14
View File
@@ -58,6 +58,20 @@ export const router = createRouter();
*/
router.route('/bw', publicVaultRouter);
/**
* The same router, exported for the platform to mount ABOVE the account gate.
*
* Mounted by the platform at the top-level paths this plugin's manifest claims (`/identity`,
* `/notifications`, `/icons`, `/events`) and for `/api/*` requests carrying a Bitwarden client header.
* That combination is what lets a Bitwarden client be given the bare Officer URL and nothing else —
* which is the entire point, and what the `/bw` prefix above could not deliver: deriving five URLs by
* hand into an advanced settings pane is not a setup anyone completes.
*
* `/bw` stays as well. It costs nothing (the router strips whichever prefix it was reached through) and
* it is the explicit address for a client that would rather be told than infer.
*/
export const publicRouter = publicVaultRouter;
/**
* Wipe this account's vault state when its session ends. Registered as a side effect of the router being
* imported, which the platform does once when the plugin's routes are mounted — so the subscription's