GET /nginx/certificates/dns-providers, authenticated, 86 entries on the 2.15.1
image here. verified against the live instance and against the file it reads
from, /app/certbot/dns-plugins.json in the container.
which settles how the provider picker gets built. the list is per install —
whatever certbot plugins that image carries — so curating one centrally and
shipping it as the truth would offer somebody a provider their NPM has never
heard of, take their API key, and fail at issue time with an error that does
not explain itself. ask the instance.
and NPM hands back each provider's credential TEMPLATE, carrying its own field
names. so the form is generated rather than written per provider, and it stays
right for providers we have never heard of. a curated list of ours is left with
the human half only: which we recommend, and how you actually get a key.
also noted: this endpoint answers 403 unauthenticated, not 404. the 404-means-
auth quirk is per route, not a property of the API. the client's heuristic
stays correct; the documentation of it was too broad.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
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
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
four fixes from a review, and the first one was leaking out of the plugin.
TYPING. `respond` derived its context from `Parameters<typeof router.get>[1]`.
hono's .get is overloaded, `Parameters<>` resolves the FIRST overload — `[path:
string]` — so index 1 was `undefined`, ctx was `never`, and all nine handlers
errored. twelve errors from one line, in a tree where `bunx tsgo` is shared with
the platform and expected clean. imports hono's `Context` instead. tsgo now
exits 0 tree-wide, which it has not done since this plugin landed.
READONLYWRITES. `/_config/test` was declared as a read-only write so a member
would not meet a 403 on the test button. they always would: authorize.ts asks
isOwnerOnlyRoute before isRequestAllowedAtLevel, and ownerOnly is prefix
matched, so `/_config` already covered it and the entry was inert. removed, and
the reason it SHOULD be inert written down — that request carries the admin
secret in its body, so a member who could call it would hold an oracle pointed
at somebody else's password.
STATUS WROTE ON EVERY POLL. recordProbe ran unconditionally on a GET whose own
comment invites polling, so a read endpoint issued an UPDATE per poll per open
panel. the probe is cached 30s and the row is written only when the verdict or
the version changes — a flapping NPM still records every transition, which is
the case the column exists for. the cache is dropped when the credential does.
DEAD TERNARY. `likelyAuth ? 502 : 502`.
and the tests. 22 over a stubbed fetch, 99% of npm-client.ts. the two that
matter are the 404-means-credential rule and the mint-once-and-retry: both are
silent when broken, and a refactor of either turns a working panel into one
that says "not found" forever. every test uses its own credential because the
token cache is module state — sharing one would let an earlier test's token
satisfy the retry test, which would then pass for the wrong reason.
still not installed, still no settings form, still no db:push. README says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
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>