home-hosted 0.6.6 → 0.6.8

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 CHANGED
@@ -3,13 +3,15 @@
3
3
  `home-hosted` is a Node 24 / TypeScript harness for self-hosted servers: `up` starts a Hono/srvx
4
4
  panel (default `127.0.0.1:3999`) that supervises the entries in `$HHOSTED_HOME/servers.config.json`
5
5
  and serves a UI. User docs: `README.md`, `docs/SERVERS.md` (entries and port conflicts),
6
- `docs/NOTIFICATIONS.md`; UI authors: `docs/UI_CREATION.md`.
6
+ `docs/NOTIFICATIONS.md`, `docs/DDNS.md`; UI authors: `docs/UI_CREATION.md`.
7
7
 
8
8
  State lives only in `$HHOSTED_HOME` (default `~/.home-hosted`): `servers.config.json`,
9
- `.control-secrets.json` (0600: password hash, API token hash, Telegram bot token), `.logs/`, `.tls/`,
10
- `.backups/`, `.ui/`, `.state/` (a persistent entry's nanny state, plus its 0600 spawn spec until the
11
- nanny reads it), and `run.json` — the live daemon's pid/url/token, 0600. The package ships **no
12
- servers**: never commit a config, a seed entry, or a path that names one.
9
+ `.control-secrets.json` (0600: password hash, API token hash, Telegram bot token, and DDNS
10
+ credentials sealed with AES-256-GCM under `HHOSTED_DDNS_SECRET`), `.logs/`, `.tls/`, `.backups/`,
11
+ `.ui/`, `.state/` (a persistent entry's nanny state, plus its 0600 spawn spec until the nanny reads
12
+ it, plus `ddns.json` — the last address each target confirmed), and `run.json` — the live daemon's
13
+ pid/url/token, 0600. The package ships **no servers**: never commit a config, a seed entry, or a path
14
+ that names one.
13
15
 
14
16
  ## Commands
15
17
 
@@ -91,14 +93,15 @@ a minor without one.
91
93
  `processCarriesServerId` for the environment marker and `processTreePids` for tree ownership),
92
94
  `identity` (which port holder is this entry's own successor: marker first, then resolved image +
93
95
  argv), `nanny` (a persistent entry's spec/state files and the liveness rules for one),
94
- `log-tail` (an offset reader that survives rotation and truncation), health-check, host, telegram,
96
+ `log-tail` (an offset reader that survives rotation and truncation), `ddns/` (one `DdnsProvider` per
97
+ registrar over one HTTP call each, plus `ip` and `zone` helpers), health-check, host, telegram,
95
98
  archive.
96
99
  - `src/services/` — stateful orchestration: supervisor, control-server, config-watch, state,
97
- auth + exposure, dependencies, history, log-buffer/log-files, notifications, host-monitor, backups,
98
- tls, ui, plus `init` (the scaffold behind `home-hosted init`: a manifest, a `.gitignore`, and the
99
- prompts stay in the CLI), `nanny` (the process a persistent entry runs under) and `log-relay` (the
100
- tailer that feeds its lines to the supervisor). It names no server — the scaffold must stay as
101
- neutral as the supervisor.
100
+ auth + exposure, dependencies, history, log-buffer/log-files, notifications, host-monitor, ddns,
101
+ backups, tls, ui, plus `init` (the scaffold behind `home-hosted init`: a manifest, a `.gitignore`,
102
+ and the prompts stay in the CLI), `nanny` (the process a persistent entry runs under) and
103
+ `log-relay` (the tailer that feeds its lines to the supervisor). It names no server — the scaffold
104
+ must stay as neutral as the supervisor.
102
105
  - `src/middleware/auth.ts` — the `/api/*` guard, and `requestIdentity()`, the one place a request's
103
106
  credentials are read: the `hh_session` cookie or `Authorization: Bearer <api token>`. A token is
104
107
  a first-class credential (same authority as a signed-in browser) and is verified from the secrets
@@ -127,10 +130,10 @@ a minor without one.
127
130
  - `scripts/` — `build-uis.mjs` (build one UI, optionally zip it), `typecheck-uis.mjs`,
128
131
  `capture-media.mjs` (the README's media), `check-release-version.mjs` and `release-notes.mjs`
129
132
  (used by the release workflow), `dev.mjs` (`pnpm dev`).
130
- - `docs/` — the topic docs (`SERVERS.md`, `NOTIFICATIONS.md`, `UI_CREATION.md`), `mockups/*.html`
131
- (hand-written UI examples in four directions) and `media/*`: their screenshots plus the README's
132
- `tour.gif`, all regenerated by `pnpm run media`. Only the three `.md` files are published; the
133
- media is a build artifact.
133
+ - `docs/` — the topic docs (`SERVERS.md`, `NOTIFICATIONS.md`, `DDNS.md`, `UI_CREATION.md`),
134
+ `mockups/*.html` (hand-written UI examples in four directions) and `media/*`: their screenshots plus
135
+ the README's `tour.gif`, all regenerated by `pnpm run media`. Only the four `.md` files are
136
+ published; the media is a build artifact.
134
137
 
135
138
  ## Conventions
136
139
 
@@ -147,7 +150,9 @@ a minor without one.
147
150
  envelope `{ message, code, detail }`. Never hand-roll `c.json({ error })`.
148
151
  - Document routes with `describeRoute` + `jsonBody(schema)`; `jsonBody` needs a real schema.
149
152
  - Patch schemas carry no defaults; nested groups (`restart`/`health`/`stop`/`auth`/`tls`/`telegram`/
150
- `http`) merge key-by-key, and an explicit `null` clears a key.
153
+ `http`) merge key-by-key, and an explicit `null` clears a key. `ddns` is the exception: its
154
+ `accounts`/`domains` lists are **replaced** (a merge cannot express removing a hostname), and only
155
+ `ipv4`/`ipv6` merge.
151
156
  - Two-sided bounds read inclusively (`'1 <= number.integer <= 512'`). `test/shared/contracts.test.ts`
152
157
  pins every boundary and the patch/schema parity — update it with any schema change.
153
158
  - A new field in a **response** DTO is optional (`'x?'`) and its clients read it defensively. An
@@ -163,6 +168,12 @@ a minor without one.
163
168
  - A destructive action that is one click away confirms in a **popover** (`KillPortButton.vue`),
164
169
  never by arming the same button for a second press: an impatient double click on an arming
165
170
  button fires it. Keep the safe choice first in the popover's tab order.
171
+ - **Docs ship with the change.** A user-visible change updates the docs that describe it
172
+ (`README.md`, `docs/*.md`) in the same commit — do not forget them; a stale doc is a bug like
173
+ stale code.
174
+ - **UIs move together.** `uis/stock` is not the only client: a change to it — or to a shared
175
+ contract it reads — lands in every other UI under `uis/`, and each altered UI bumps its
176
+ `ui.json` per the rule above.
166
177
 
167
178
  ## Compatibility
168
179
 
@@ -192,6 +203,9 @@ either is a last resort, and never an accidental one.
192
203
  - **Unknown keys are dropped from the resolved config, kept on disk, and listed in a startup
193
204
  warning.** Dropping one is the normal way a newer config looks here, so it must never fail the
194
205
  group it sits in (that used to reset `control` — port, bind, auth policy — to schema defaults).
206
+ A *write* to a group validates the same tolerant way (`parseTolerant` via the store), so a key this
207
+ release does not know never blocks a save — removing a key from a schema must not brick the page
208
+ that writes it.
195
209
  - **Migrations ship inside the package** (`src/config/migrations.ts`), are ordered, idempotent and
196
210
  described in one line each. `home-hosted migrate` prints the plan, keeps `servers.config.json.bak`,
197
211
  refuses to write a config it cannot read, and needs consent: `--yes`, `HHOSTED_MIGRATE=allow`, or a
@@ -346,11 +360,14 @@ either is a last resort, and never an accidental one.
346
360
  - **Server field**: `serverSchema` + its patch in contracts, merge keys in `config/store.ts` when
347
361
  nested, the form in `uis/stock/src/components/settings/`, and the contract tests.
348
362
  - **UI**: a new `uis/<name>/` with a `vite.config.ts` from the shared factory;
349
- `node scripts/build-uis.mjs <name> --zip`. The contract is `docs/UI_CREATION.md`.
350
- - **Capability**: a stateless `src/providers/*` returning plain data.
363
+ `node scripts/build-uis.mjs <name> --zip`. The contract is `docs/UI_CREATION.md`. A change to one
364
+ UI (`stock`, `noc-console`) is carried to the others in the same commit.
365
+ - **Capability**: a stateless `src/providers/*` returning plain data; a DDNS provider is one file plus
366
+ a registry line (`src/providers/ddns/index.ts`), and the settings form, credentials and validation
367
+ follow from its metadata — see `docs/DDNS.md`.
351
368
 
352
369
  ## Publishing
353
370
 
354
- `pnpm pack` runs `prepack` (a full build) and ships `bin/`, `dist/`, `uis/stock/dist`, `README.md`, `docs/SERVERS.md`,
355
- `docs/NOTIFICATIONS.md`, `docs/UI_CREATION.md`, `AGENTS.md` and `LICENSE`. The bin falls back to tsx so `pnpm link` works before a
371
+ `pnpm pack` runs `prepack` (a full build) and ships `bin/`, `dist/`, `uis/stock/dist`, `README.md`, `docs/DDNS.md`,
372
+ `docs/SERVERS.md`, `docs/NOTIFICATIONS.md`, `docs/UI_CREATION.md`, `AGENTS.md` and `LICENSE`. The bin falls back to tsx so `pnpm link` works before a
356
373
  build; `vue`/`vue-router` are devDependencies because the UIs are prebuilt.
package/README.md CHANGED
@@ -255,7 +255,8 @@ can be told what to be: *"Help me build a UI for home-hosted: nostalgic game the
255
255
  | 📈 **Resources** | CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart. |
256
256
  | 🌡️ **Host vitals** | Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery. |
257
257
  | 🤖 **Token API** | Scripts and agents drive it with `Authorization: Bearer` — no browser, no session. [↑](#-agents-scripts-and-tools) |
258
- | 🔔 **Notifications** | Telegram on crash, unhealthy, forced restart, recovery and host thresholds — [setup here](./docs/NOTIFICATIONS.md). |
258
+ | 🔔 **Notifications** | Telegram on crash, unhealthy, forced restart, recovery, host thresholds and DNS changes — [setup here](./docs/NOTIFICATIONS.md). |
259
+ | 🌐 **Dynamic DNS** | Keep hostnames pointed at your public IP — Cloudflare, Namecheap, Spaceship, Porkbun, GoDaddy, Gandi and more. [DDNS.md](./docs/DDNS.md) |
259
260
  | 💾 **Backups** | One click for config, secrets, TLS and your declared data directories — plain `.zip`, or AES-256 with a password, restored per path. |
260
261
  | 🎨 **BYOU — Bring Your Own UI** | Upload a static build, `ui-update` to follow its releases, `ui-revert` to go back. [UI_CREATION.md](./docs/UI_CREATION.md) |
261
262
  | 🔐 **Security** | Cookie sessions, API tokens, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
@@ -293,8 +294,8 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.
293
294
  | `home-hosted down` | stop it cleanly — supervised processes included, persistent entries left running |
294
295
  | `home-hosted restart` | `down`, then `up` |
295
296
  | `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
296
- | `home-hosted start <id>` | start one server, leaving the panel and its other servers alone |
297
- | `home-hosted stop <id>` | stop one server — the panel stays up |
297
+ | `home-hosted start <id>` | start one server — and anything it `dependsOn` |
298
+ | `home-hosted stop <id>` | stop one server, nothing else |
298
299
  | `home-hosted set-password` | set the panel password without opening a browser |
299
300
  | `home-hosted set-token` | set the API token scripts and agents use (`--generate`, `--clear`) |
300
301
  | `home-hosted migrate` | bring `servers.config.json` up to this release's schema (`--dry-run`, `--yes`) |
@@ -374,8 +375,10 @@ Everything binds `127.0.0.1` until you say otherwise.
374
375
  never taken from the message, and anything the panel supervises is refused, not killed. A server
375
376
  that [restarts itself](./docs/SERVERS.md#when-a-program-restarts-itself) can be followed, or replaced
376
377
  with a supervised copy.
377
- - **Secrets never enter the config**: the password hash, the API token hash and the Telegram bot
378
- token live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`; the TLS pair in `.tls/`.
378
+ - **Secrets never enter the config**: the password hash, the API token hash, the Telegram bot
379
+ token and the DDNS credentials live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`; the
380
+ TLS pair in `.tls/`. DDNS credentials are sealed with AES-256-GCM under `HHOSTED_DDNS_SECRET`
381
+ (default `hh` — set your own).
379
382
  - **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https, or
380
383
  upload a PEM pair and let home-hosted terminate TLS itself.
381
384
 
@@ -384,18 +387,29 @@ Everything binds `127.0.0.1` until you say otherwise.
384
387
  ## 🔔 Notifications
385
388
 
386
389
  Telegram, when something happens while you are not looking: a server that gave up restarting, a
387
- failing health check, a forced restart, a recovery, or a host threshold (disk, memory, swap, load,
388
- temperature). Opt-in, rate-limited per server *and* reason, and the bot token stays in the secrets
389
- file. **Two minutes of setup: [NOTIFICATIONS.md](./docs/NOTIFICATIONS.md).**
390
+ failing health check, a forced restart, a recovery, a host threshold (disk, memory, swap, load,
391
+ temperature), or a dynamic DNS change. Opt-in, rate-limited per server *and* reason, and the bot token
392
+ stays in the secrets file. **Two minutes of setup: [NOTIFICATIONS.md](./docs/NOTIFICATIONS.md).**
393
+
394
+ ---
395
+
396
+ ## 🌐 Dynamic DNS
397
+
398
+ **Settings → Dynamic DNS** keeps a list of hostnames pointed at this machine's public IP — add an
399
+ account, paste its credentials, add hostnames. The panel checks the address on an interval and calls a
400
+ provider only when it actually changed, and the last confirmed address survives a restart. Provider
401
+ tokens stay in the secrets file. **Providers and the config shape: [DDNS.md](./docs/DDNS.md).**
390
402
 
391
403
  ---
392
404
 
393
405
  ## 💾 Backups
394
406
 
395
407
  **Settings → Backups** archives the config, secrets, TLS pair and every data directory your entries
396
- declare — an ordinary `.zip`, or WinZip AES-256 with a password, restored per path. Known build output
397
- and dependency directories (`node_modules`, `dist`, `.next`, framework caches) are skipped per entry;
398
- `backupIgnoreGenerated: false` captures them anyway.
408
+ declare — an ordinary `.zip`, or WinZip AES-256 with a password, restored per path. *Create backup…*
409
+ lists what the archive will hold and lets you drop any item; *Restore…* shows the same list from an
410
+ archive before anything is written. Known build output and dependency directories (`node_modules`,
411
+ `dist`, `.next`, framework caches) are skipped per entry; `backupIgnoreGenerated: false` captures them
412
+ anyway.
399
413
 
400
414
  <details>
401
415
  <summary><b>🚚 One archive is a whole setup</b></summary>
@@ -406,7 +420,9 @@ archive and restore. Definitions come back, data lands where *this* machine's co
406
420
 
407
421
  It works because an archive carries its own `servers.config.json` and paths are matched by the
408
422
  **declaration** (`omniroute:DATA_DIR`), not by an absolute path from the source machine. A restore never
409
- writes where no config declares.
423
+ writes where no config declares. A declaration using `{projectDir}`, `{dataRoot}` or `{home}` follows
424
+ the restoring panel (`{projectDir}` is `HHOSTED_PROJECT` or the panel's cwd, not `HHOSTED_HOME`); a
425
+ literal absolute path is restored to that same path.
410
426
 
411
427
  </details>
412
428