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:
@@ -55,6 +55,11 @@ publicVaultRouter.all('/*', async (ctx) => {
|
|||||||
if (!base) return ctx.text('Vault sidecar not available', 503);
|
if (!base) return ctx.text('Vault sidecar not available', 503);
|
||||||
|
|
||||||
const url = new URL(ctx.req.url);
|
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 subpath = (url.pathname.startsWith(PREFIX) ? url.pathname.slice(PREFIX.length) : url.pathname) || '/';
|
||||||
const target = `${base}${subpath}${url.search}`;
|
const target = `${base}${subpath}${url.search}`;
|
||||||
const method = ctx.req.method;
|
const method = ctx.req.method;
|
||||||
|
|||||||
@@ -58,6 +58,20 @@ export const router = createRouter();
|
|||||||
*/
|
*/
|
||||||
router.route('/bw', publicVaultRouter);
|
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
|
* 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
|
* imported, which the platform does once when the plugin's routes are mounted — so the subscription's
|
||||||
|
|||||||
+21
@@ -49,4 +49,25 @@ export const manifest: PluginManifest = {
|
|||||||
re-checks containment at boot, so it can only ever mean `/api/vault/bw/*`.
|
re-checks containment at boot, so it can only ever mean `/api/vault/bw/*`.
|
||||||
*/
|
*/
|
||||||
unauthenticatedPaths: ['/bw/*'],
|
unauthenticatedPaths: ['/bw/*'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
The four paths a Bitwarden client derives from a bare server URL, claimed at the ROOT so that URL can
|
||||||
|
be the bare Officer one. This is what makes the setup "paste https://officer.example.com and log in"
|
||||||
|
rather than five fields copied into an advanced pane — which is the difference between a feature and
|
||||||
|
a thing nobody uses.
|
||||||
|
|
||||||
|
None of them is a name Officer serves: they belong to Vaultwarden's protocol and to nothing else here,
|
||||||
|
so the path alone decides and no sniffing is involved.
|
||||||
|
*/
|
||||||
|
unauthenticatedRootPaths: ['/identity', '/notifications', '/icons', '/events'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
`/api/*` is the one namespace BOTH servers claim — Vaultwarden has /api/sync, /api/ciphers; Officer
|
||||||
|
has /api/chat, /api/settings. The path cannot separate them, so the client says who it is: every
|
||||||
|
Bitwarden client stamps one of these, and older ones `device-type`.
|
||||||
|
|
||||||
|
Routing, not authentication. A forged header reaches Vaultwarden, which demands its own credential
|
||||||
|
exactly as it would have; nothing of Officer's is authorised by it.
|
||||||
|
*/
|
||||||
|
protocolHeaders: ['bitwarden-client-name', 'bitwarden-client-version', 'device-type'],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user