Commit Graph
7 Commits
Author SHA1 Message Date
Claude 11a63e52fd fix: register the mount as a proxied prefix, so bodyParser stops eating POST bodies
Every POST through /api/vault died with 502 sidecar unreachable while bodyless
GETs worked. Nothing was unreachable: the platform bodyParser reads JSON and
form bodies on the protected router, disturbing the request stream, and the
catch-all proxy then forwards ctx.req.raw.body — an already-consumed stream that
Bun fetch throws on. The throw is caught and reported as the sidecar being down.

Core sidecars never hit this because createSidecarProxy registers the prefix for
them. This router proxies by hand, and the extraction dropped that registration.

Safe across the whole prefix: no handler here reads ctx.get(body), they all call
ctx.req.json() themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:12:20 +00:00
pastilhasandClaude Opus 5 4006be0e3b 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>
2026-08-18 14:47:20 +00:00
pastilhasandClaude Opus 5 224a525751 restore the Bitwarden door, as one declared unauthenticated path
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>
2026-08-18 14:26:50 +00:00
pastilhasandClaude Opus 5 aa948d3ee0 panel: a missing identified is unknown, not a failed identification
The health card reported a healthy Vaultwarden as "Answered, but not a
Vaultwarden". The vault was fine; the two processes were on different versions.

`identified` was added to the sidecar's /_health in 680b8fb. An older sidecar
answers without the field at all, and the panel read missing as false — so a
version skew between officer and officer-vault surfaced as a claim about the
user's server. They are PM2 peers, and `pm2 restart officer` does not restart
`officer-vault`, so that skew is the normal state after updating a plugin
rather than an edge case.

Now only an explicit `false` warns. Missing renders as plain "Answering":
reachable, and honest that the sidecar did not say what it reached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:15:38 +00:00
pastilhasandClaude Opus 5 1b8bf299da panel: stop wiping the URL input on every keystroke
Typing was impossible — each character vanished, and pasting flashed and
reverted to the saved value.

`useClient()` calls `createClient()` on every render with no memoisation, so
`get`/`put` are new function identities each time. The panel wrapped its
fetches in `useCallback(..., [get])`, which therefore also changed identity
every render, and the effects depended on THOSE:

  keystroke -> setUrl -> re-render -> new `get` -> new callback -> effect fires
            -> GET /config -> setUrl(saved value) -> character gone

Fixed by making them plain functions and loading once on mount, with the health
probe fired from the load rather than from a second effect watching
`configured` — an effect is one more thing that can re-fire.

The rule, written into the file since nothing about the symptom pointed at a
dependency array: never put a value derived from `useClient()` in a dependency
array. The rest of the codebase already follows it (CommandHqDetail keys its
fetch effect on `sessionId` alone).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:11:46 +00:00
pastilhasandClaude Opus 5 680b8fb2ba health: probe /api/config, and report when the target is not a Vaultwarden
Was /alive, which answers on a Vaultwarden and proves only that SOMETHING is
listening on that address. /api/config self-identifies:

  {"version":"2026.6.0","server":{"name":"Vaultwarden",...}}

That distinction is the point of the check. The failure worth catching is not
"the box is down" — that surfaces on its own the moment anything else fails —
it is pointing the vault at the wrong port on a host running several things,
where a 200 from an unrelated service reads as success and the real problem
appears later as a login that cannot work.

So the panel now has three states rather than two: reachable and identified,
reachable but NOT a Vaultwarden (amber, with what to check), or no answer. The
server version is shown when it identifies itself.

Only /api/config is parsed, and only for `server.name` and `version`. It is
unauthenticated and carries no vault data — the hard rule about never reading
bodies governs the proxy path, and this is not on it.

Also recorded in-code: Vaultwarden does not implement /api/status, which does
exist in Bitwarden's official cloud API, so a probe aimed there 404s against a
perfectly healthy server. That near-miss looks exactly like a wrong address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:56:35 +00:00
pastilhas 2f2529d8d5 vault, extracted from the platform into its own repository
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.
2026-08-18 13:42:44 +00:00