Commit Graph
4 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 dcb48be419 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
2026-08-25 15:55:30 +00:00
pastilhasandClaude Opus 5 50e60c77ab 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
2026-08-25 15:40:41 +00:00
pastilhasandClaude Opus 5 3380a482ed the clever type cost twelve errors; the untested rule was the load-bearing one
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
2026-08-25 15:26:18 +00:00
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