the agent calls the plugin, and private is a network you name

two conclusions from working through it.

the agent never touches NPM and never holds a credential. it calls this
plugin, which owns keeping up with NPM's API — that is what having a plugin
is for. the reads stay a thin proxy; the expose route deliberately is not,
because that is where "reachable only from the private network" gets turned
into a DNS record, a certificate and a proxy host. writing that down once
here is the safety argument: the alternative puts it in the agent's memory,
and you find out it forgot when a hostname is reachable that should not be.

and "tailnet" was too specific. officer can set up tailscale and knows its
range, so it can prefill — but a company with a VPN already in place has
their own, and must be able to say so. what gets stored is a named network
and its address ranges; NPM access lists take ranges, so it costs nothing.
the certificate half is unchanged, because what forces the DNS proof is that
the name is not publicly reachable, whoever runs the network.

also recorded: NPM's DNS provider list is fixed and unverified here, and so
is whether a godaddy account still gets DNS API access.

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:55:30 +00:00
co-authored by Claude Opus 5
parent 50e60c77ab
commit dcb48be419
+37 -4
View File
@@ -71,6 +71,36 @@ So **TLS on a VPN-only hostname is impossible without the credential.** That is
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.
## How the agent asks, and what it is never told
The agent calls **this plugin**, and never Nginx Proxy Manager. Keeping up with NPM's API — its version,
its shape, the day it changes — is this plugin's job, and the whole point of having one.
Most of what it exposes is a thin proxy over NPM's reads, and should stay thin. **The expose route is
the one place it deliberately is not.** The agent sends what was asked for — a hostname, what it should
point at, and which network may reach it — and the plugin turns that into the three separate things it
actually means: a DNS record, a certificate, a proxy host.
That split is the whole safety argument. "Reachable only from the private network actually means these
three things" gets written down once, here, where it can be read and tested. The alternative is teaching
the agent NPM's own request format, which puts that knowledge in the agent's memory instead — and you
would discover it had forgotten only when a hostname is reachable that should not be.
The agent is never given a credential. Not NPM's, not the DNS provider's. It says the sentence; the
plugin holds the keys and does the work.
## "Private" is a network the user names, not a tailnet
Officer can set up a Tailscale network for you, and it knows that network's address range — so it can
fill this in without asking. **That is a convenience, not the model.** Somewhere with a VPN already in
place has their own ranges and nothing to do with Tailscale, and they must be able to say so.
So what gets stored is a **named network and the addresses it covers**, and the request is *"expose this
to <name>"*. NPM's access lists take address ranges, so this costs nothing to support properly.
The certificate half is unchanged by any of it. What forces the DNS proof is that the name cannot be
reached from the public internet — true of anybody's private network, whoever runs it.
## Read-only is a state, not the design
The first round creates nothing, edits nothing and deletes nothing, and the original README argued that
@@ -152,10 +182,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.
- **All of the above.** No domain registry, no DNS provider credentials, no named networks, no agent,
no expose route. Everything from "What this is for" through "Private is a network the user names" is
agreed and entirely unbuilt.
- **Two things to check before building the DNS half.** NPM can only run the DNS proof for providers it
ships support for — a fixed list, not "any provider" — and if yours is missing, the proof has to be
done elsewhere and a finished certificate handed over, which is a much larger job. Neither the list
nor whether a given GoDaddy account still has DNS API access has been verified here.
- **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.