what this is actually for

the brief that produced this plugin was "build me an NPM proxy", in full. no
vision was given, so the agent inferred one — a careful read-only window onto
NPM, with creating and editing deliberately excluded — and inferred it wrong.

the point is the handover. two paths, and setup should ask out loud which one
you are on: give Officer your NPM login and your DNS provider key and then ask
for what you want in a sentence, or keep them and manage NPM yourself on a
public address. strip the credentials out and what remains is a list NPM
already shows plus a warning you could set up yourself — a notifier, not a
plugin. it exists for the handover.

which makes the clearest case worth stating on its own: TLS on a tailnet-only
name is impossible without the DNS credential. let's encrypt cannot visit a
private address, so the only check left is a DNS record, which needs the
provider's API. that is not a limitation of the design, it is the shape of the
problem.

half the old principle survives. creating is additive and cannot break a host
that already works, so it is the write path the agent should get and it is the
product rather than a later phase. editing and deleting are not, and stay in
NPM's UI until there is a reason they should not.

nothing here is built. said so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 15:40:41 +00:00
co-authored by Claude Opus 5
parent 3380a482ed
commit 50e60c77ab
+56 -11
View File
@@ -1,10 +1,11 @@
# Proxy Manager
Nginx Proxy Manager inside Officer — which hosts are running, whether they are healthy, and which
certificates are about to expire.
Hand your proxying over to Officer, and tell it what you want in a sentence.
**Status: first round. Reads work against a live NPM 2.15.1; the settings form is not built and the
plugin is not installed** — see "What is not built" below.
**Status: first round, and built to the wrong thesis.** Reads work against a live NPM 2.15.1. The brief
that produced this code was, in full, "build me an NPM proxy" — no vision was given, so the agent that
wrote it inferred a sensible one and inferred it wrong. What is below under "What this is for" is the
actual intent, written down after the fact. Read that before extending anything.
---
@@ -34,16 +35,53 @@ Redirection hosts, streams, 404 hosts and access lists are exposed by the API an
---
## What it deliberately does not do
## What this is for
**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.
Not a second window onto Nginx Proxy Manager. NPM already has a perfectly good one.
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.
The point is to let someone hand over the parts of this they would rather not think about, and to make
handing over safe enough that it is a reasonable thing to do. There are two paths and the setup should
ask, out loud, which one you are on.
---
**You hand over the credentials.** Your NPM login, and your DNS provider's API key — GoDaddy,
Cloudflare, whoever you registered with. Both go into the secret store, each encrypted under its own
purpose key, and neither is ever returned across the API to anybody, including you. From then on you ask
for what you want in a sentence — *"expose sub.mydomain.com on the tailnet, over HTTPS"* — and an agent
does the three things that sentence actually means: the DNS record, the certificate, the proxy host.
**You keep them.** Then you run NPM yourself on a public address and handle DNS at your provider. That
is a completely reasonable way to run a server, and if it is you, this plugin has little to offer: a
list you already have, plus a certificate warning you could set up yourself.
Which is the honest test of whether this should exist at all. **It exists for the handover.** Everything
here is worth building in proportion to how much it helps somebody who does not want to learn what a
proxy host is — and it should still be legible to somebody who has been doing this by hand for years,
because that person is who decides whether the handover looks safe.
## Why a private name needs your DNS key
The clearest case, and the one that shows the credential is a requirement rather than a nice-to-have.
HTTPS needs a certificate. Let's Encrypt issues them free, but only after checking you control the name,
and normally it checks by visiting it over the internet. A name that resolves to a tailnet address
cannot be visited. There is exactly one other check it accepts: prove control by placing a specific
record in that domain's DNS — which needs the provider's API.
So **TLS on a VPN-only hostname is impossible without the credential.** That is not a limitation of this
design, it is the shape of the problem. The alternative is not "the same thing without a key", it is a
publicly exposed address with DNS you manage yourself.
## Read-only is a state, not the design
The first round creates nothing, edits nothing and deletes nothing, and the original README argued that
as a principle: NPM's own UI does those well, and each is a form with real consequences.
Half of that survives. **Creating is additive** — a new proxy host cannot break one that already works —
so it is the write path the agent should get, and it is the product rather than a later phase. **Editing
and deleting are not**, and those can stay in NPM's UI until there is a reason they should not.
Read-only is what the plugin shows before anyone has handed anything over. It is the fallback, not the
thesis.
## Permissions
@@ -114,6 +152,13 @@ Verified against the live instance: a wrong credential comes back `likelyAuth =
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.
- **All of the above.** No domain registry, no DNS provider credentials, no agent, no write path. The
vision in "What this is for" is entirely unbuilt — the sentence a user should be able to say is the
thing to design next, and where domains and provider keys live is an open boundary question rather
than a settled one.
- **Certificate expiry notifies nobody.** `daysUntilExpiry` is computed in `api/router.ts` and thrown
away when the panel closes. NPM renews at 30 days; inside 14 means renewal has actually failed and the
domain goes dark at zero. Nothing on the machine will say so.
- **Nothing is polled.** Both panels load once and have a refresh button. `GET /status` is built to be
polled when something does: the probe is cached for 30 seconds and the row is written only when the
verdict or the version changes, so a poll is not an UPDATE.