pastilhasandClaude Fable 5 5914e96cf3 proxy-manager: nginx proxy manager in officer
reads hosts and certificates against a live NPM 2.15.1. one grantable
permission, with /_config owner-only — reading which hosts are healthy
is worth sharing, an NPM admin login is not, and NPM has no read-only
role.

the quirk that shapes the whole plugin: NPM answers an unauthenticated
read with 404, not 401, because its router only registers a resource
once a token is present. so 'not signed in' and 'no such endpoint' are
the same response, and a naive client tells you a feature is missing
when your password is wrong. the client mints a fresh token and retries
once, then reports a persistent 404 as likelyAuth.

certificates sort by expiry rather than name, which is the only ordering
that answers a question.

no create/edit/delete and no cert issuance: NPM's own UI does those and
a wrong proxy host takes a domain off the internet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 08:38:15 +00:00

Proxy Manager

Nginx Proxy Manager inside Officer — which hosts are running, whether they are healthy, and which certificates are about to expire.

Status: first round. Reads work against a live NPM 2.15.1; the settings form is not built yet — see "What is not built" below.


Why it is called proxy-manager and not npm

The container is called npm and everybody says NPM. But in a repository where every other sentence is about bun, lockfiles and npm install, a plugin directory called npm collides with the package manager in every grep and every conversation. The label says the real name; the id says what it does.

The same reasoning renamed officer-claude to officer-anthropic-proxy. Trivial to change while nothing depends on it.


What it does

Hosts — every proxy host, sorted by domain, showing where it forwards, whether TLS is on and forced, whether an access list guards it, and whether nginx currently reports it offline. Selecting one lets you take it out of service or put it back.

Certificates — sorted by how soon they expire, not by name. That ordering is the whole design: a list sorted by name is a reference, a list sorted by expiry answers the only question anyone has. Under 30 days is amber, under 14 is red, because Let's Encrypt renews at 30 and anything inside 14 means renewal has actually failed.

Redirection hosts, streams, 404 hosts and access lists are exposed by the API and not yet drawn.


What it deliberately does not do

No creating, editing or deleting hosts. No certificate issuance. NPM's own UI does those well and is one click away, and each is a form with real consequences — a wrong proxy host takes a domain off the internet.

This surface answers "what is running, and is it healthy", and the one write it allows is reversible in a click. Building the editor is a decision to make deliberately, not to arrive at by accretion.


Permissions

One permission, proxy-manager, and it is grantable. Reading which hosts exist and whether their certificates are healthy is genuinely useful to somebody who is not the owner.

The credential is not. It is an NPM admin login, NPM has no read-only role, and anyone holding it can redirect any hostname this machine serves. So /_config is ownerOnly — the split that field exists for, and the same shape Jellyfin uses for its admin token.

The secret is never returned across the API boundary, at any level, to anybody, including the owner. A settings form asking "is one set" gets a boolean.


The connection

One row in proxy_manager_connection — URL, identity, and a secret encrypted under this plugin's own purpose key (encryptSecret('proxy-manager', …)), so a leak of that key opens this column and nothing else.

One row, not one per user. NPM's API is admin or nothing, so a per-user credential would be a per-user copy of total authority over every hostname on the machine. The owner sets it; members granted the permission read through it without ever seeing it.

It is a plugin-owned table rather than core service_connections — which fits the shape exactly, with url, username and an encrypted secret — only because ServiceName there is a closed TypeScript union, and this plugin should install into a platform it did not have to edit. Worth revisiting if that union opens; db/schema.ts says so.


The API quirk that shapes this plugin

NPM answers an unauthenticated read with 404, not 401.

GET /api/nginx/proxy-hosts  ->  {"error":{"code":404,"message":"Not Found - /nginx/proxy-hosts"}}

Its router only registers a resource once the request carries a token, so "not signed in" and "no such endpoint" are the same response. Left alone that produces the worst kind of bug report: a feature that reads as missing when the credential is simply wrong.

So npm-client.ts treats a 404 on a path it knows exists as an authentication failure, carries that as NpmError.likelyAuth, and the panel turns it into a sentence pointing at settings. It also mints a fresh token and retries once before concluding anything — otherwise a restarted NPM makes every panel look broken until a reload.

Verified against the live instance: a wrong credential comes back likelyAuth = true, and POST /api/tokens answers 400 "Invalid email or password" rather than 401.


What is not built

  • The settings form. /_config works — GET, PUT, DELETE, and POST /_config/test which checks a credential without storing it, so a wrong password never replaces a working one. Nothing renders it yet, so the connection has to be inserted by hand for now.
  • db:push has not been run, so proxy_manager_connection does not exist in Postgres yet.
  • Redirections, streams, 404 hosts and access lists have routes and no panels.
  • Nothing is polled. Both panels load once and have a refresh button.
  • No tests. The client's behaviour was verified by hand against the live NPM; that should be a test with a stubbed fetch.

Files

manifest.ts          permission, ownerOnly, readOnlyWrites
api/npm-client.ts    tokens, the 404 quirk, retry
api/connection.ts    the one row; encryption; the secret never leaves
api/router.ts        mounted at /api/proxy-manager
db/schema.ts         proxy_manager_connection
web/panels.ts        two panels
web/layout.ts        how they are arranged
S
Description
Nginx Proxy Manager in Officer — hosts, certificates and access lists
Readme
273 KiB
Languages
TypeScript 100%