home-hosted 0.7.2 → 0.7.3
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/AGENTS.md +13 -9
- package/dist/cli.js +1217 -141
- package/dist/cli.js.map +1 -1
- package/docs/DDNS.md +3 -2
- package/docs/REVERSE_PROXY.md +91 -10
- package/docs/UI_CREATION.md +16 -1
- package/package.json +2 -2
- package/uis/stock/dist/assets/{index-UtOMDd0A.js → index-DoDrajgK.js} +7 -7
- package/uis/stock/dist/index.html +1 -1
- package/uis/stock/dist/ui.json +3 -3
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)
|
|
73
|
-
DNS API)
|
|
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
|
|
package/docs/REVERSE_PROXY.md
CHANGED
|
@@ -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,
|
|
16
|
-
`$HHOSTED_HOME/.hh/.proxy/bin/`. The version
|
|
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
|
-
**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
133
|
-
|
|
|
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
|
|
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. |
|
package/docs/UI_CREATION.md
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.7.3",
|
|
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"
|