first dated version. plugins were all sitting on 1.0.0, which said nothing about
when a build was made or which of two copies is newer — the platform already
uses YYYY.MM.DD.NN and this follows it.
the version lives in manifest.ts because a plugin has no package.json; that is
the field the marketplace publishes and the installer compares.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tags are how the marketplace is browsed rather than scrolled: a list of them on
the left, the plugins that carry one on the right. a closed vocabulary
(PLUGIN_TAGS in the platform) because free text becomes homelab, home-lab, Home
Lab and selfhosted inside a month, and a filter list of synonyms is worse than
no filter at all.
declared here rather than curated in the catalogue, so a plugin can say what it
is and nobody edits a table when one ships. the marketplace caches it on sync,
exactly as it already does for label and summary.
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>
A stock Bitwarden client speaks Vaultwarden's protocol and nothing else: it
gets its own bearer token from /identity/connect/token and has nowhere to put
a platform JWT, so the authenticated /api/vault router cannot serve it. This
was dropped during the extraction because a plugin's routes mount behind the
account gate.
It comes back through the platform's own mechanism rather than an exception:
`unauthenticatedPaths: ['/bw/*']` in the manifest, resolved against this
plugin's mount and re-checked for containment at boot.
ONE prefix, not the four root mounts the platform version needed. A Bitwarden
client takes `https://<officer>/api/vault/bw` as its server URL and derives
/api, /identity, /icons and /notifications beneath it, so the whole protocol
fits under a single declared path.
The router is mounted BEFORE this file's middleware, deliberately: Hono
collects handlers in registration order, so a route registered ahead of a
`.use()` answers without it. Verified rather than assumed, because it is an
auth boundary — /api/vault/bw/* reaches the passthrough with no session while
/api/vault/config still goes through userMiddleware and the owner gate.
Not new exposure: everything here was already reachable at the Vaultwarden URL
it replaces, guarded by the same master password and the same Vaultwarden auth.
Officer adds no security to a Bitwarden client because it has no credential to
check. What it buys is that Vaultwarden need not be exposed at all — on this
install it listens on 127.0.0.1 only, and Officer, already behind the tailnet
and already terminating TLS, becomes the single address any client needs. Net
reduction in surface.
Still temporary, and still deleted rather than adjusted once a first-party
extension authenticates to Officer against the /session/login broker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fourth plugin out, after offscale, music and email. Two routers, a sidecar,
three tables and — new — a settings screen, since the vault never had a UI in
the platform.
What it is: a reverse-proxy to a Vaultwarden you already run, plus a credential
broker that keeps the vault session server-side so devices never hold one. It
stores no passwords and cannot read any; every byte through it is ciphertext.
Two things changed rather than moved:
• VAULTWARDEN_URL left .env for a one-row `vault_config` table with a panel
writing it. A plugin cannot ask the owner to edit a file and restart a
process, so the env var could not survive the move. Server-wide, decided
with the owner: one Vaultwarden per install. The sidecar reads it on a 10s
cache rather than per request, since it sits in front of every proxied byte.
• Core stopped importing clearVaultTokens/clearVaultUnlockKey from three auth
handlers. That is the security property behind the distress and panic
buttons, so it could not simply be dropped — the platform gained a small
account-lifecycle hook instead, and the severity mapping moved here where it
belongs. Core now names no table and no column of this plugin.
Two things were deleted rather than carried, both already dark since
2026-08-13 and both recorded in PLUGIN.md:
• The unauthenticated Bitwarden passthrough. A plugin's routes mount behind
Officer's account gate; top-level unauthenticated prefixes are not something
a manifest can declare. Its own comments already called it TEMPORARY,
pending a first-party browser extension that authenticates to Officer.
• The notifications websocket. No plugin owns a socket on this platform yet.
SignalR falls back to HTTP long-polling on its own, so clients still sync.