- New docs/concepts-{hosts,dns,access,api-tokens}.md -- plain-language
guides aimed at less technical readers, each linking onward to the
existing system-design-level doc for anyone who wants that detail.
Card help links (Proxy List, Add/Edit host, DNS Provider cards,
Users/Permissions/Groups cards) now point here instead of straight at
Installation/Architecture.
- The "New API Token" card had no help link at all -- added, pointing to
the new API Tokens doc.
- Fixed the in-app docs viewer rendering every docs/*.md page with a
garbled heading + stray <hr> at the top: Jekyll front matter (meant
only for the GitHub Pages build) was never stripped before being
handed to the markdown renderer.
- Fixed cross-doc links never resolving in-app, since this viewer serves
docs at /docs/<slug> with no .html suffix: rewritten to the correct
in-app URL, first by registered slug, falling back to the doc's real
filename (the correct, working link form on the Jekyll/GitHub Pages
build) -- same idea as the existing image-path fix, and lets one link
written in a doc work on both targets.
Bumps to v1.1.13.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
- The edit form's "Parent Wildcard" option stayed greyed out even when a
valid wildcard existed, since hostEditOpen() never ran the eligibility
check (only the host field's keyup handler did, which setting .val()
programmatically doesn't fire) -- and the check itself, GET
/host/lookup/:item, had the same self-match bug as the recently-fixed
Host.prototype.update() case: it resolves an already-existing host to
its own record instead of a sibling wildcard. Added a dedicated
/host/wildcard-parent/:item route combining lookUp() (handles a
brand-new subdomain) with lookUpWildcardParent() (handles an
already-existing host), and hostEditOpen() now actually runs it.
- Migrated ops/nginx_conf/autossl.conf's deprecated "listen ... http2"
directive to the standalone "http2 on;" directive (nginx 1.25.1+).
Bumps to v1.1.12.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
The single header-wide help icon (added last release) pointed at a
per-page doc guess, but a page can have several cards covering different
topics. Removed it and added a small help icon directly to each card
that has real corresponding doc content, linking straight to that doc:
Proxy List + Add/Edit host modal (hosts.ejs), Add DNS Provider + Dynamic
A Records (dns.ejs), Add New User + User List (users.ejs), Add
Permission + Permissions (permissions.ejs), Add Group (groups.ejs).
Bumps to v1.1.11.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
- Plain hosts can now be renamed after creation (wildcard/child/cache hosts
stay locked, since other records reference them by name). Migrates the
cert cache key on rename.
- Along the way, found and fixed a real bug in the vendored model-redis
library: its rename path leaves a stray, incomplete hash behind under
the old key when an `always`-type field (updated_on) is defined earlier
in the schema than the primary key -- silently blocking that hostname
from ever being reused. Worked around at the Host model level (can't
patch node_modules).
- Host list now shows who created each host, and when.
- Host modal's tabs now scroll horizontally on narrow screens instead of
overflowing awkwardly.
- Added missing inline help text (Target SSL, wildcard matching behavior).
Bumps to v1.1.9.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
The three auth_mode radios (Off / Basic / SSO) had no shared [name]
attribute, so per the HTML spec each was its own independent group --
clicking one didn't uncheck the others, letting multiple options
appear selected at once despite the page's own text saying "basic
auth and SSO can't both be enabled."
Added name="auth_mode" to restore native browser radio-group
behavior. The original comment claimed the radios were deliberately
kept nameless to avoid polluting the submitted form data (formAJAX
serializes every [name] field in the form), but that reasoning
doesn't hold: model-redis's processKeys() rebuilds the saved object
strictly from the Host model's own _keyMap, so an unrecognized
auth_mode field is silently stripped before anything is ever
persisted -- confirmed directly with model-redis's own
object_validate.js. Updated the stale comment accordingly.
- Auth tab is now a single choice (Off / Basic auth / SSO) instead of two
independent toggles that could both be on at once, which made it
ambiguous which gate actually protected a request. Enforced both in the
UI and server-side (POST/PUT), accounting for partial PUT updates against
the existing record.
- Add per-user basic-auth management (change password, delete) so an admin
no longer has to blow away and retype the whole user list to remove or
rotate one account.
- Fix: `Model.errors.ObjectValidateError(...)` is a constructor and was
being called without `new` everywhere in this codebase. Without `new`,
`this` inside it was the module's shared `errors` object (mutated in
place) and the call evaluated to `undefined` — so every
`throw Model.errors.ObjectValidateError(...)` actually threw `undefined`,
which Express's `next(undefined)` treats as "no error" and silently
falls through to the catch-all 404 handler. Every host/user/group/
permission/dns-provider validation error (bad hostname, bad IP, etc.) was
showing a confusing "Page not found" instead of the real message.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The target ("ip") field validator required at least two dot-separated
labels, rejecting legitimate single-label hostnames like Docker Compose
service names ("sso-manager"), /etc/hosts entries, or anything resolved
via a search domain. This was enforced identically client-side
(public/lib/js/val.js) and server-side (utils/hostname_validate.js,
routes/host.js), so there was no way to set one through the UI or API
— only by writing to the Host model directly, bypassing validation
entirely (which is how theta-env's setup.sh registers sso-manager as a
target today, working only because it calls Host.create() directly).
Relax HOSTNAME in both places to accept either a bare single label or
the existing dotted-FQDN pattern. Flips the one existing test that
codified the old behavior (isValidHostname('localhost') was asserted
false) and adds coverage for the reported case.
- Default the "Parent Wildcard" challenge type when a wildcard parent exists.
- Split Authentication (basic auth + SSO) into its own tab; Access keeps IP
allow/deny.
- Add GET /api/host/auth-suggestions (authenticated host editors, not just
admins) and datalist-backed "type to search + Add" pickers for the SSO
allowed-users/groups lists.
Verified in a browser (tab present, datalist populated, picker appends deduped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the dense always-open add panel + inline edit card with a single
Bootstrap modal (shared by Add and Edit) organized into tabs: General, TLS &
Wildcard, Traffic, Headers, Access (IP + basic auth + SSO). Adds per-field
explanations, a full-width proxy list, and an "Add host" button. Preserves all
field names/ids, the challenge-type detection JS, and formAJAX wiring; drops the
form-clone edit mechanism in favor of populating the one modal. Verified in a
browser (add + edit, tab navigation, field population).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds opt-in basic auth per Host, following the existing per-host controls
pattern:
- Host fields basicauth_enabled / basicauth_realm / basicauth_users
({user: base64(sha1(pw))}). Credentials are parsed to plaintext by the pure
host_features normalizer and hashed at the route layer (utils/basicauth.js),
so plaintext never reaches Redis.
- ops/nginx_conf/hostfeatures.lua enforces it in access phase: verifies the
Authorization header against base64(sha1(password)), fails closed with a 401
WWW-Authenticate challenge.
- hosts.ejs gains an enable toggle, realm, and a username:password textarea
(passwords never echoed back; blank keeps the current set).
Unit tests cover hashing (matches the htpasswd {SHA} vector), credential
parsing, and normalization. Note: the Lua path needs verification on a live
OpenResty box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend (utils/hostname_validate.js, enforced in routes/host.js on create/update):
- host: IPv4 or a wildcard pattern whose labels may be normal, "*" (one
fragment) or "**" (any depth, incl. a bare "**" catch-all) — matching
Host.lookUp. Lowered Host.host min length to 1 so "**"/"*" pass the model.
- target (ip): IPv4 or a strict hostname, no wildcards.
- Both reject scheme, "/", ":" and whitespace; 422 with per-field keys.
Frontend (val.js) mirrors the rules: host/target validators + hosts.ejs fields
point at them. Unit tests in test/unit/hostname_validate.test.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every proxied request flows through one shared OpenResty location whose
behavior is chosen at request time from the host's Redis hash. Add per-host
controls as new Host fields enforced in Lua rather than static nginx config
(which can't key off a per-request variable):
- Rate limiting: per-client-IP token bucket via resty.limit.req
(ratelimit_enabled/rate/burst), backed by a new `ratelimit` shared dict.
- Response caching: opt-in per host via a global proxy_cache zone gated by
$skip_cache (respcache_enabled). Off by default; upstream Cache-Control
still honored.
- Custom/security headers: req_headers (upstream) + resp_headers (client) and
hsts_enabled, applied in access/header_filter phases.
- IP allow/deny CIDR lists via resty.ipmatcher (deny wins; non-empty allow is
default-deny).
New ops/nginx_conf/hostfeatures.lua holds the enforcement; proxy.conf's
access_by_lua string becomes a block that calls it, plus a header_filter block.
nodejs/utils/host_features.js is the pure, unit-tested normalize/validate layer
(header/CIDR parsing, range clamping, injection-safe values) applied in
routes/host.js and mirrored by the hosts.ejs edit form. install.sh gains the
ipmatcher rock, the cache dir, and the hostfeatures.lua symlink.
Per-host cache TTL is intentionally deferred (global default only) — see the
plan's limitations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A *.example.com wildcard host now chooses between two routing modes:
- wildcard_matchAny=false (default): only subdomains explicitly defined
in redis route; undefined subdomains get no match (406)
- wildcard_matchAny=true: any subdomain catches-all to the wildcard
parent host, preserving the previous behavior
The gate lives in the host_lookup socket service, which is only reached
for domains missing a direct redis entry, so defined children and
**-style hosts are unaffected. Adds the matching-mode selector to the
host add/edit form, shown for wildcard hosts.
Note: existing wildcard hosts have no wildcard_matchAny field and so
default to the stricter "only defined" mode until re-saved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>