one section, not two. a provider serves many domains so the lists differ, but a person thinks in DOMAINS — the provider is something the check discovers, and a key only matters as the answer to "can officer do anything for THIS domain". two tabs would make somebody hold that join in their head, and the providers tab would be two rows most of the time. so the key is entered from the row that needs one and stored against its provider: add it for one cloudflare domain and every other cloudflare domain turns green at once. THE CHECK runs before you commit, because its answer is usually the reason you would or would not add it, and a list of unchecked rows is a list somebody has to remember to revisit. it reports five verdicts, not two, and the extra three are the interesting ones. `private` — atelier.officer.dev resolves to 100.64.x, which is CORRECT and reads as broken against any public-IP comparison. `unknown` — we could not establish our own address, so "is that us" has no answer and saying so beats a confident wrong one. `nowhere` — a name bought ten minutes ago. plus maybeProxied, because a cloudflare domain resolving to cloudflare is a working setup rather than a misconfiguration. NS gives the DNS HOST, not the registrar. officer.dev is godaddy on both counts here, but a domain bought at godaddy and delegated to cloudflare answers cloudflare — and that is the one that matters, since its API is what places an ACME challenge. the UI says so where it would otherwise be read as wrong. only an apex has NS records, so the lookup walks up to find them. verified live: platform.officer.dev now answers godaddy instead of nothing. CREDENTIALS are keyed on the provider, encrypted under this plugin's purpose key, never returned by any route. scope is narrow|broad and NOT read|write — DNS-01 works by WRITING a TXT record, so no read-only key can ever issue a certificate, and offering one would produce a setup that looks configured and fails at the first renewal sixty days later. the dialog says that out loud. the form is generated from NPM's own dns-providers template, so the field names are the provider's and it stays right for providers we have never heard of. the routes live under /_config, which the manifest already marks ownerOnly: a DNS key can rewrite every record for a domain, which is strictly more than the NPM login kept there, and declaring it once beats checking it twice. tables verified by query rather than by db:push's exit code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
129 lines
4.3 KiB
TypeScript
129 lines
4.3 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
import { Globe, Cable, Radio, FileX2, Users, ShieldCheck, Plug, Network } from 'lucide-react';
|
|
|
|
/**
|
|
* The sections.
|
|
*
|
|
* ── NPM's words, not NPM's order ──
|
|
*
|
|
* The VOCABULARY is deliberately NPM's. Somebody who has used it should find the same words, and somebody
|
|
* who has not will end up in NPM's UI eventually anyway — inventing better names only means learning both.
|
|
* `404 Hosts` reads oddly and is theirs; the hint says what it means, because the label cannot.
|
|
*
|
|
* The ORDER is ours, and this file said the opposite until 2026-08-25. NPM's nav order is an accident of
|
|
* how NPM grew; ours is a claim about what people come here for. Proxy hosts, then certificates, then
|
|
* streams — that is most of every visit. The rest are real and rarely the reason you opened this.
|
|
*
|
|
* **Being able to make that claim is most of why this is a plugin** rather than a link to NPM. A wrapper
|
|
* that reproduces the original's ordering has given up the one thing it was for.
|
|
*
|
|
* ── Two groups ──
|
|
*
|
|
* Everything above `domains` is a thing NPM owns and this reads. From `domains` down is OFFICER's own —
|
|
* what it knows about your domains, the DNS credentials it holds, and where NPM is. Those are not NPM
|
|
* concepts and should not pretend to be.
|
|
*
|
|
* ── Domains and DNS Providers are ONE section ──
|
|
*
|
|
* They were briefly two. A provider serves many domains, so the lists are genuinely different — but a
|
|
* person thinks in domains, the provider is something the check discovers, and a key only ever matters as
|
|
* the answer to "can Officer do anything for THIS domain". Two tabs would make somebody hold that join in
|
|
* their head, and the providers tab would be two rows most of the time. So the key is entered from the
|
|
* domain row that needs one and stored against its provider.
|
|
*/
|
|
|
|
export type SectionKey =
|
|
| 'proxy-hosts'
|
|
| 'certificates'
|
|
| 'streams'
|
|
| 'redirections'
|
|
| 'dead-hosts'
|
|
| 'access-lists'
|
|
| 'domains'
|
|
| 'connection';
|
|
|
|
export type Section = {
|
|
key: SectionKey;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
/** One line under the label in the nav. What this is, for somebody who does not already know. */
|
|
hint: string;
|
|
/** The plugin route its count comes from, or null when there is nothing to count. */
|
|
path: string | null;
|
|
/**
|
|
* Agreed and not built.
|
|
*
|
|
* Listed rather than hidden on purpose: these two are the shape the plugin is going towards, and a nav
|
|
* that shows them says what this is FOR to somebody who opens it — including the owner, six months from
|
|
* now, wondering what was decided. The pane says plainly that it does not exist, which is better than a
|
|
* feature that looks half-present.
|
|
*/
|
|
planned?: boolean;
|
|
};
|
|
|
|
export const SECTIONS: Section[] = [
|
|
{
|
|
key: 'proxy-hosts',
|
|
label: 'Proxy Hosts',
|
|
icon: Globe,
|
|
hint: 'A domain, forwarded to something running here',
|
|
path: '/proxy-manager/hosts',
|
|
},
|
|
{
|
|
key: 'certificates',
|
|
label: 'Certificates',
|
|
icon: ShieldCheck,
|
|
hint: 'TLS, and how long each one has left',
|
|
path: '/proxy-manager/certificates',
|
|
},
|
|
{
|
|
key: 'streams',
|
|
label: 'Streams',
|
|
icon: Radio,
|
|
hint: 'A raw TCP or UDP port, forwarded',
|
|
path: '/proxy-manager/streams',
|
|
},
|
|
{
|
|
key: 'redirections',
|
|
label: 'Redirection Hosts',
|
|
icon: Cable,
|
|
hint: 'A domain that sends visitors somewhere else',
|
|
path: '/proxy-manager/redirections',
|
|
},
|
|
{
|
|
key: 'dead-hosts',
|
|
label: '404 Hosts',
|
|
icon: FileX2,
|
|
hint: 'A domain served deliberately as nothing',
|
|
path: '/proxy-manager/dead-hosts',
|
|
},
|
|
{
|
|
key: 'access-lists',
|
|
label: 'Access Lists',
|
|
icon: Users,
|
|
hint: 'Who is allowed to reach a host',
|
|
path: '/proxy-manager/access-lists',
|
|
},
|
|
|
|
// ── Officer's own, below here ──
|
|
{
|
|
key: 'domains',
|
|
label: 'Domains',
|
|
icon: Network,
|
|
hint: 'What you own, who runs its DNS, and the keys',
|
|
path: '/proxy-manager/domains',
|
|
},
|
|
{
|
|
key: 'connection',
|
|
label: 'Connection',
|
|
icon: Plug,
|
|
hint: 'Where NPM is, and the credential — owner only',
|
|
path: null,
|
|
},
|
|
];
|
|
|
|
export const DEFAULT_SECTION: SectionKey = 'proxy-hosts';
|
|
|
|
export const isSectionKey = (value: string | null): value is SectionKey =>
|
|
!!value && SECTIONS.some((s) => s.key === value);
|