home-hosted 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/DDNS.md CHANGED
@@ -68,9 +68,10 @@ and the guess is wrong (`home.example.co.uk` is handled; an unusual suffix may n
68
68
  | **Dynu** | A, AAAA | Username + password | `password` may be the SHA-256 hash. |
69
69
  | **Hurricane Electric** | A, AAAA | Per-host DDNS key | `dyn.dns.he.net`. |
70
70
  | **FreeDNS (afraid.org)** | A, AAAA | The per-record "Direct URL" | Paste the whole URL or just its hash. |
71
+ | **Namecheap (API)** | — | API user + key + whitelisted IP | For **DNS-01 only**: it answers a challenge through the XML API, which rewrites the whole zone per write. It updates no addresses — use the Dynamic DNS provider above for that. The client IP must be whitelisted under Profile → Tools → Namecheap API Access, and error `1017105` means it is not. |
71
72
 
72
- Not supported: **Route 53** (AWS SigV4 signing, a much larger dependency), **DreamHost** (no public
73
- DNS API), and the Namecheap XML API (a full zone rewrite per update).
73
+ Not supported: **Route 53** (AWS SigV4 signing, a much larger dependency) and **DreamHost** (no
74
+ public DNS API).
74
75
 
75
76
  ## Notifications
76
77
 
@@ -12,8 +12,8 @@ Setup is **Others → Reverse Proxy** in the sidebar.
12
12
 
13
13
  ## Quick start
14
14
 
15
- 1. **Install the engine** — one button, a pinned Caddy release (~46 MB) into
16
- `$HHOSTED_HOME/.hh/.proxy/bin/`. The version and its SHA-256 are shown afterwards.
15
+ 1. **Install the engine** — one button, the current Caddy release (~46 MB) into
16
+ `$HHOSTED_HOME/.hh/.proxy/bin/`. The version the binary reports is shown afterwards.
17
17
  2. **Set the ports** — `80` and `443` by default, plus the ACME account e-mail.
18
18
  3. **Add a route** — a hostname and a target.
19
19
  4. Point the hostname's DNS at this machine (Dynamic DNS can do that) and switch the
@@ -68,6 +68,9 @@ panel did not start is ever killed.
68
68
  | `path` | Optional prefix, so one hostname can serve several apps. |
69
69
  | `tls` | `auto` (default), `off`, `manual`. |
70
70
 
71
+ The list's hostname is a link to where the route answers — `https://host:<https port>`, or
72
+ the plain port for `tls: "off"`, path included. A route that is switched off is plain text.
73
+
71
74
  A route pointing at the panel is exposure, and answers to the same rule as binding the
72
75
  listener beyond loopback: authentication on and a non-default password, or the save is
73
76
  refused.
@@ -93,6 +96,35 @@ The challenge is the part that catches people out: with the proxy on 4480/4443,
93
96
  to reach 4480 and 443 has to reach 4443**, or the CA never sees the challenge and the name
94
97
  never gets its certificate.
95
98
 
99
+ A name the CA will not issue for falls back to the engine's own CA. The route reads
100
+ **Fell back to Local CA**: the name answers, but with an untrusted certificate, and the
101
+ engine keeps trying on its own. certmagic renews once a third of a certificate's lifetime
102
+ is left, and the engine's own CA issues 12 hours by default, so the next attempt is about
103
+ **8 hours** away — no restart, no downtime.
104
+
105
+ **Force retry certificate** appears on that row only, and asks the CA again immediately.
106
+ The engine is **not** restarted: a certificate the engine holds cannot be cleared by
107
+ deleting the file or by reloading the configuration, but it *is* released when the name
108
+ leaves the configuration — so the panel reloads without that one route, drops the stored
109
+ pair while it is out, and puts the route back. Every other route keeps serving; the name
110
+ itself has no certificate until the CA answers. It is not offered while a name is still
111
+ *pending*, because there the engine is already mid-ACME.
112
+
113
+ A **removed** route has its stored certificate dropped on the next apply, so adding it
114
+ back asks the CA instead of serving the old pair. A **disabled** route keeps its
115
+ certificate: it is still configured, and switching it back on should not cost a new
116
+ issuance.
117
+
118
+ The wait can be shortened by giving the engine's own issuer a shorter `lifetime` in the
119
+ generated policy; the row reads the interval from the certificate, so the confirmation
120
+ follows whatever is configured.
121
+
122
+ A hostname **no route claims** gets a page saying so, on both ports — not a redirect into
123
+ a handshake that cannot finish. A visitor with no hostname at all (a bare IP on the HTTPS
124
+ port) is answered by the engine's own CA under `hh-fallback.invalid`, so the page appears
125
+ after the browser's warning. A hostname that is neither routed nor covered by a
126
+ certificate still fails its handshake: there is nothing to present a certificate for.
127
+
96
128
  **A name with no certificate yet is still reachable.** The engine's own CA is the last
97
129
  issuer in the policy, so the handshake completes with an untrusted certificate — the
98
130
  browser offers to continue instead of failing with `ERR_SSL_PROTOCOL_ERROR` — and over
@@ -120,17 +152,63 @@ Certificates are managed by the engine, inside `.hh/.proxy/engine/`, and nothing
120
152
  the machine is touched: the generated configuration pins its storage root, and the
121
153
  panel passes the engine no `$HOME` of its own to write into.
122
154
 
123
- **Not yet: DNS-01.** Wildcard certificates, and issuing with no inbound port at all,
124
- need a Caddy build with the registrar's plugin compiled in — every registrar is a
125
- separate Go module, and the stock binary ships none. The engine download path already
126
- takes a plugin list, so this is the next step, not a redesign.
155
+ **DNS-01** is the way round a router that will not forward 80 and 443. A route is one
156
+ hostname, so there is no wildcard to issue; what DNS-01 buys is a certificate with
157
+ **no inbound port at all** — behind CGNAT, or on a host only reachable over VPN.
158
+ Turn it on in the page and point each route at a DNS account:
159
+
160
+ ```json
161
+ {
162
+ "proxy": {
163
+ "dns01": { "enabled": true, "resolvers": [] },
164
+ "routes": [
165
+ { "id": "gitea", "host": "git.example.com", "target": "server", "workspace": "default", "server": "gitea", "dnsAccount": "default/cf" }
166
+ ]
167
+ }
168
+ }
169
+ ```
170
+
171
+ The account is the one from **Workspace Settings → Dynamic DNS** — same credentials,
172
+ same workspace, same notification routing. It does not need the hostname in its
173
+ hostname list: the account names the zone, the route names the subject. A route may
174
+ name a different account from its neighbour, so names in two zones at two registrars
175
+ each get the right one; the panel gives each account a policy of its own.
176
+
177
+ Leave a route on **Automatic** and it uses the only account that can answer a
178
+ challenge. With more than one account, Automatic cannot choose: pick one per route, or
179
+ those names keep using the port challenges. A local-only name (`.lan`, an IP) is never
180
+ challenged — it is signed by the engine's own CA.
181
+
182
+ The account is checked when you save, not when the certificate is due: unknown, an
183
+ account whose provider cannot write a `TXT` record (the router-style Dynamic DNS
184
+ password providers), or one with no credentials stored yet is refused there.
185
+
186
+ Which accounts can answer: any whose provider writes a `TXT` record. Today that is
187
+ Cloudflare and **Namecheap (API)** — the latter is a separate provider from Namecheap's
188
+ Dynamic DNS entry, because its credentials differ (API user, key and a whitelisted
189
+ client IP) and it rewrites the whole zone per write. Namecheap's Dynamic DNS password
190
+ cannot write a TXT record at all, so that account is refused with the reason.
191
+
192
+ The engine holds no DNS credentials. It asks the panel over the ACMEProxy protocol on
193
+ loopback, with credentials the panel generated for it, and the panel writes the
194
+ `_acme-challenge` record through that account. `resolvers` overrides the nameservers
195
+ the engine checks the record against, which is what split-horizon DNS needs.
196
+
197
+ Setting it up needs no inbound port, but the record has to be **published** before the
198
+ CA looks: the panel writes it, the CA reads it, and the panel removes it again. A
199
+ provider that can only write a whole record set — Namecheap's XML API is the one —
200
+ is read-modify-write, so the panel rewrites records it did not create, and refuses
201
+ rather than adding a 151st record to a zone already at the limit.
127
202
 
128
203
  ## The engine
129
204
 
130
205
  | | |
131
206
  | --- | --- |
132
- | Comes from | Caddy's own build service (`caddyserver.com/api/download`), at a version the panel pins — never "latest" by accident. |
133
- | Installed at | `.hh/.proxy/bin/hh-caddy` + `engine.json` (version, URL, SHA-256, size). |
207
+ | Comes from | Caddy's own build service (`caddyserver.com/api/download`), at **its current release** — the panel pins no version, so **Update engine** is how a security fix reaches you. |
208
+ | Verified against | **Caddy 2.11.6.** That is the release this build was tested with; a newer one is installed anyway, and the panel reports the version that actually arrived. A release that changes the JSON schema may need a panel update first. |
209
+ | Carries | The `acmeproxy` DNS provider, compiled in for every install so the binary does not depend on a setting. `DNS-01` is offered because of it; an engine installed by an older release is refused with *update the engine* rather than failing inside Caddy. |
210
+ | Installed at | `.hh/.proxy/bin/hh-caddy` + `engine.json` (version, URL, size, when). |
211
+ | Replaceable | Drop any `hh-caddy` build at that path and the panel runs it: it reports *a path configured by hand* and probes no version. Installing or updating is refused while the engine is up — **Stop** it first, because the binary is replaced on disk. |
134
212
  | Runs as | A child of the panel's own nanny, exactly like a `persistent: true` entry. |
135
213
  | Supervised by | The panel. The admin API is opened on the panel side only: a unix socket in a `0700` directory (loopback TCP plus Caddy's origin check on Windows). |
136
214
  | Reloaded by | `POST /load` — atomic, and the revision that applied is kept on disk so a change can be reverted in one click (the first one has nothing to go back to). A reload onto a port something else holds is refused with that pid before the engine is touched, and the panel keeps reporting the ports the engine is really on. |
@@ -145,9 +223,11 @@ internet: the engine keeps serving, and the panel reattaches to it on the next b
145
223
  | --- | --- |
146
224
  | `GET /api/proxy` | Policy, engine, live state and every route with its resolved upstream. |
147
225
  | `PATCH /api/proxy` | Change the settings or replace the route list, then apply. |
148
- | `POST /api/proxy/engine` | Install or update the engine (`{ "version": "" }` = the pinned one). |
226
+ | `POST /api/proxy/engine` | Install or update the engine (`{ "version": "" }` = the current release). |
149
227
  | `PUT` \| `DELETE /api/proxy/certificates/:id` | Store or remove one uploaded pair (`{ label, certificate, privateKey }`). |
150
228
  | `POST /api/proxy/start` \| `stop` \| `apply` \| `revert` | Engine lifecycle and configuration. |
229
+ | `POST /api/proxy/routes/:id/retry-certificate` | Ask the CA again for one route, without restarting the engine. |
230
+ | `POST /_acme/present` \| `cleanup` | The engine's DNS-01 channel: loopback only, and behind credentials the panel generated. Not part of `/api`, so it needs no session. |
151
231
 
152
232
 
153
233
  ## Files
@@ -155,7 +235,8 @@ internet: the engine keeps serving, and the panel reattaches to it on the next b
155
235
  | Path | What |
156
236
  | --- | --- |
157
237
  | `.hh/.proxy/bin/` | The engine binary and the record of what was installed. |
158
- | `.hh/.proxy/engine/current.json` | The generated configuration; `previous.json` is the revision before it. |
238
+ | `.hh/.proxy/engine/current.json` | The generated configuration; `previous.json` is the revision before it. Both are `0600`: with DNS-01 on they carry the challenge credentials the engine authenticates with. |
159
239
  | `.hh/.proxy/state/` | The nanny's spec and state, and `admin.json` (how to reach a running engine). |
240
+ | `.hh/.proxy/state/challenge.json` | The credentials the engine presents to `/_acme`, `0600`; written only when DNS-01 is on. |
160
241
  | `.hh/.proxy/tls/<id>.crt.pem` \| `<id>.key.pem` | One uploaded pair per entry (the key is `0600`). |
161
242
  | `.hh/.logs/proxy.log` | The engine's own JSON output, as the nanny captures it. |
@@ -90,6 +90,21 @@ must never lock you out of your own machine.
90
90
  drive letters.
91
91
  7. **Offline and plain-http friendly.** Assume a LAN over `http://`: no `Secure`-only cookies, no
92
92
  hard-coded port, no https-only APIs.
93
+ 8. **Give each page its own path.** An SPA with canonical URLs is the best default: a path is what
94
+ makes a page bookmarkable, shareable, and reachable with back/forward — and matching the stock
95
+ paths keeps two UIs of the same panel feeling alike. Those are:
96
+
97
+ | page | path |
98
+ | --- | --- |
99
+ | workspace: overview · servers · logs · settings | `/w/<workspace>` · `/w/<workspace>/servers` · `/w/<workspace>/logs` · `/w/<workspace>/settings` |
100
+ | one server | `/w/<workspace>/servers/<id>` |
101
+ | panel-wide: overview · settings | `/global/overview` · `/global/settings` |
102
+ | reverse proxy | `/proxy` |
103
+
104
+ Keep the workspace id in the URL (a link then opens the same workspace in a browser that never
105
+ selected it), and rewrite bare or unknown paths onto the canonical one (`/` → Global Overview) so an
106
+ old bookmark still lands somewhere sensible. **Not a hard rule** — a different direction may route
107
+ however it fits; just keep deep links working.
93
108
 
94
109
  ## The API
95
110
 
@@ -247,7 +262,7 @@ with an installed panel on the default 3999; override with `--port`/`--ui-port`
247
262
  - [ ] only `/api/...` calls, no absolute origins, no hard-coded port
248
263
  - [ ] `401 { code: 'AUTH_REQUIRED' }` handled with a login screen
249
264
  - [ ] live data from SSE — a panel that only polls feels broken
250
- - [ ] deep links render (the server falls back to `index.html`)
265
+ - [ ] deep links render (the server falls back to `index.html`) and each page has its own path
251
266
  - [ ] assets self-hosted, no CDN
252
267
  - [ ] works over plain http on a LAN
253
268
  - [ ] `ui.json` with a name and version, so Global settings can tell you what is installed — plus `repo`/`tag`/`asset` if you want `ui-update` to follow your releases
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "home-hosted",
3
3
  "type": "module",
4
- "version": "0.7.2",
4
+ "version": "0.7.4",
5
5
  "packageManager": "pnpm@12.5.1",
6
6
  "description": "Self-hosted control panel that keeps your home-server processes alive: supervision, restarts, port conflicts, logs — and a UI you can replace.",
7
7
  "author": "NamesMT <dangquoctrung123@gmail.com>",
@@ -133,7 +133,7 @@
133
133
  "vue-tsc": "^3.3.11"
134
134
  },
135
135
  "simple-git-hooks": {
136
- "pre-commit": "pnpm lint-staged"
136
+ "pre-commit": "pnpm lint-staged && node scripts/stamp-uis.mjs"
137
137
  },
138
138
  "lint-staged": {
139
139
  "*": "eslint --fix"