@toon-protocol/townhouse 0.1.0-rc5 → 0.1.0

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/README.md CHANGED
@@ -230,6 +230,119 @@ The package-local `packages/townhouse/compose/townhouse-dev.yml` is the canonica
230
230
 
231
231
  For backward compatibility, `docker-compose-townhouse-dev.yml` at the repo root is preserved and continues to be used by `scripts/townhouse-dev-infra.sh`. A follow-up story will route the script through the package-local copy.
232
232
 
233
+ ## DockerOrchestrator Profiles
234
+
235
+ The `DockerOrchestrator` class drives both the contributor dev stack and
236
+ the operator HS-mode apex stack via a single `profile: 'dev' | 'hs'`
237
+ parameter:
238
+
239
+ - **`profile: 'dev'`** (default) — uses `dockerode` for fine-grained
240
+ programmatic control. Matches the lifecycle the existing `townhouse up`
241
+ CLI has shipped since Epic 21. No `composePath` required.
242
+ - **`profile: 'hs'`** — shells out to `docker compose -f <composePath> up -d`
243
+ with `--profile <type>` flags for each enabled peer. Waits on the
244
+ connector's `GET /admin/hs-hostname` endpoint (connector v3.5.0+) until
245
+ the `.anyone` hostname is published. Requires `composePath` (typically
246
+ the path returned by `materializeComposeTemplate('hs')`).
247
+
248
+ Example (HS-mode caller, as Story 45.4's `townhouse hs up` will use):
249
+ ```typescript
250
+ import { materializeComposeTemplate, DockerOrchestrator } from '@toon-protocol/townhouse';
251
+ import Docker from 'dockerode';
252
+
253
+ const { composePath } = materializeComposeTemplate('hs');
254
+ const docker = new Docker();
255
+ const orch = new DockerOrchestrator(docker, config, walletManager, {
256
+ profile: 'hs',
257
+ composePath,
258
+ });
259
+ await orch.up([]); // apex-only (connector + townhouse-api)
260
+ ```
261
+
262
+ ### Connector Anon Requirement (HS Profile)
263
+
264
+ The HS profile's readiness gate calls `GET /admin/hs-hostname`. The
265
+ connector container MUST be configured with `anon.enabled: true` —
266
+ if anon is disabled, the endpoint returns 503 and the orchestrator
267
+ throws `OrchestratorError("connector is anon-disabled — set
268
+ anon.enabled: true in the connector config")`. Story 45.4's
269
+ `townhouse hs up` generates the connector config with `anon.enabled: true`
270
+ by default; manual configurations should mirror that setting.
271
+
272
+ ## HS Mode (Apex Install)
273
+
274
+ `townhouse hs up` is the one-command install for homelab operators. It boots the
275
+ apex stack (connector + townhouse-api) and publishes a `.anyone` hidden-service
276
+ address, writing the address to `~/.townhouse/host.json` as the final step.
277
+
278
+ ### First-run flow
279
+
280
+ ```bash
281
+ npx @toon-protocol/townhouse init # initialise config + wallet (one-time)
282
+ npx @toon-protocol/townhouse hs up # boot apex — prints "Apex live at <hostname>.anyone"
283
+ ```
284
+
285
+ On a cold image cache the command takes up to 5 minutes (image pull + anon
286
+ bootstrap). On subsequent runs with a warm cache, the HS bootstrap phase
287
+ (30–90 s) dominates.
288
+
289
+ ### Files written by `hs up`
290
+
291
+ | File | Mode | Purpose |
292
+ |------|------|---------|
293
+ | `~/.townhouse/config.yaml` | 0o600 | Townhouse config (written by `init`) |
294
+ | `~/.townhouse/wallet.enc` | 0o600 | Encrypted BIP-39 wallet (written by `init`) |
295
+ | `~/.townhouse/compose/townhouse-hs.yml` | 0o600 | Materialised HS compose template |
296
+ | `~/.townhouse/image-manifest.json` | 0o600 | Digest-pinned image manifest |
297
+ | `~/.townhouse/connector.yaml` | 0o600 | Connector config with `anon.enabled: true` |
298
+ | `~/.townhouse/host.json` | 0o600 | Published hostname + metadata |
299
+
300
+ `host.json` schema:
301
+ ```json
302
+ {
303
+ "hostname": "<onion>.anyone",
304
+ "publishedAt": "<ISO-8601>",
305
+ "connectorAdminUrl": "http://127.0.0.1:9401",
306
+ "townhouseApiUrl": "http://127.0.0.1:28090",
307
+ "writtenAt": "<ISO-8601>"
308
+ }
309
+ ```
310
+
311
+ ### Idempotent re-run
312
+
313
+ Re-running `townhouse hs up` against an already-running apex detects the
314
+ running connector, re-prints the hostname, and exits 0 without pulling images
315
+ or restarting containers. `~/.townhouse/host.json` is refreshed.
316
+
317
+ ### `hs down` vs. `hs down --rotate-keys`
318
+
319
+ | Command | Volumes | `host.json` | Next `hs up` |
320
+ |---------|---------|-------------|--------------|
321
+ | `townhouse hs down` | **Preserved** (`townhouse-hs-anon`) | Kept | **Same** `.anyone` address |
322
+ | `townhouse hs down --rotate-keys` | **Deleted** | Deleted | **New** `.anyone` address |
323
+
324
+ `--rotate-keys` prompts for confirmation when stdin is a TTY. When stdin is not
325
+ a TTY (CI, scripted), it proceeds without prompting.
326
+
327
+ ### Password sourcing
328
+
329
+ Resolution order:
330
+ 1. `--password <pw>` flag
331
+ 2. `TOWNHOUSE_WALLET_PASSWORD` environment variable
332
+ 3. Interactive prompt (only when `process.stdin.isTTY === true`)
333
+ 4. Exit 1 with an error message (non-interactive, no password provided)
334
+
335
+ ### Failure-state copy (UX-DR5)
336
+
337
+ | Class | Detection | Next step shown |
338
+ |-------|-----------|-----------------|
339
+ | anon-timeout | `HS hostname publication timeout` in error | `Re-run with DEBUG=townhouse:*` |
340
+ | anon-disabled | `anon-disabled (HTTP 503)` from probe | Edit `connector.yaml`, set `anon.enabled: true` |
341
+ | image-pull-failure | `failed to pull` / `pull access denied` in stderr | Check your network |
342
+ | port-collision | `address already in use` / `port is already allocated` in stderr | Stop the conflicting service |
343
+ | missing-docker-sock | `Cannot connect to the Docker daemon` / `docker CLI not found` | Start Docker |
344
+ | generic | Any other error | `Run with DEBUG=townhouse:*` |
345
+
233
346
  ## Running the townhouse as a hidden service (laptop)
234
347
 
235
348
  `docker-compose-townhouse-hs.yml` brings up the full operator stack —
@@ -384,3 +497,7 @@ but the connector at 3.3.x reads a discriminated union keyed on `type`
384
497
  defaulting to direct — operators toggling ATOR got direct traffic anyway.
385
498
  The current generator emits the correct `type: 'socks5'` shape with
386
499
  `externalUrl`, `managed`, and `managedOptions` per the connector contract.
500
+
501
+ ## Notes
502
+
503
+ `townhouse status --units=sats` exists as an undocumented power-user flag for Bitcoin-native operators. It converts the earnings block to integer sats using a CLI-supplied rate (`--rate <sats-per-usdc>`) or the `TOWNHOUSE_SATS_PER_USDC` environment variable; if neither is set, the command exits 1. There is no built-in price oracle — this is intentionally a manual conversion. USDC remains the canonical denomination across every other Townhouse surface (TUI hero band, drill subcommands like `townhouse peer` and `townhouse channels`); this flag is absent from `townhouse --help` per design decision D44-002.