@toon-protocol/townhouse 0.1.1 → 0.1.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/README.md CHANGED
@@ -1,503 +1,160 @@
1
1
  # @toon-protocol/townhouse
2
2
 
3
- Host-native orchestrator and dashboard for Docker-containerized TOON nodes (Town, Mill, DVM) behind a shared standalone connector.
3
+ **Run a TOON node on your own machine with two commands.**
4
4
 
5
- ## Local Dev Loop (Townhouse Dev Stack)
6
-
7
- Stories 21.9–21.13 (dashboard views) and 21.8.5 (design system) **must** be developed against the Townhouse dev stack, not against mocks or the SDK E2E topology (D21-009).
8
-
9
- ### One-command boot
5
+ TOON is a pay-to-write, free-to-read [Nostr](https://nostr.com) relay network: writers pay a tiny per-byte fee to publish, anyone reads for free. `townhouse` is the command-line installer and dashboard for running your own node. It sets up your keys, starts the node in Docker, and publishes it as a private hidden-service address so peers can reach you without exposing anything to the public internet.
10
6
 
11
7
  ```bash
12
- ./scripts/townhouse-dev-infra.sh up
13
- ```
14
-
15
- This command:
16
- 1. Builds `toon:town`, `toon:mill`, `toon:dvm` Docker images (Docker layer cache applies on subsequent runs)
17
- 2. Starts Anvil (EVM), Solana test-validator, and Mina lightnet chain devnets
18
- 3. Deploys Mock USDC to Anvil and the Solana payment-channel program
19
- 4. Starts the standalone connector with all 5 child peers registered
20
- 5. Starts 5 child nodes with deterministic Nostr keys
21
- 6. Polls each child's `/health` endpoint until ready (60 s timeout per node)
22
- 7. Prints a success banner listing every endpoint URL
23
- 8. Writes `.env.townhouse-dev` at the workspace root
24
-
25
- **First run** (no cached images): ~5 minutes (dominated by image pulls).
26
- **Subsequent runs**: ~90 seconds (cached images, warm Docker daemon).
27
-
28
- ### Endpoint banner
29
-
30
- On success, the script prints every endpoint grouped by category. Copy these URLs into your browser or `curl` to verify the stack manually:
31
-
32
- ```
33
- Connector http://127.0.0.1:28080
34
- town-01 relay ws://127.0.0.1:28700
35
- town-01 health http://127.0.0.1:28100
36
- town-02 relay ws://127.0.0.1:28710
37
- town-02 health http://127.0.0.1:28110
38
- mill-01 health http://127.0.0.1:28200 (EVM↔Solana)
39
- mill-02 health http://127.0.0.1:28210 (EVM↔Mina)
40
- dvm-01 health http://127.0.0.1:28400
41
- Anvil RPC http://127.0.0.1:28545
42
- Solana RPC http://127.0.0.1:28899
43
- Mina GraphQL http://127.0.0.1:28085
44
- Mina Accounts http://127.0.0.1:28181
45
- SOCKS5 socks5://127.0.0.1:28050
8
+ npx @toon-protocol/townhouse init # 1. create your config + wallet (one time)
9
+ npx @toon-protocol/townhouse hs up # 2. start your node
46
10
  ```
47
11
 
48
- ### Host-Fastify integration via `.env.townhouse-dev`
12
+ That's it — `hs up` prints `Apex live at <your-address>` when your node is running.
49
13
 
50
- The script writes `.env.townhouse-dev` at the workspace root. Story 21.8.5 wires a `pnpm dev:docker` script in `packages/townhouse-web` that sources this file at startup so the host-side Fastify API knows the connector admin URL and child node addresses without any manual configuration.
14
+ ---
51
15
 
52
- The contract (env var names the Fastify API reads):
53
- - `TOWNHOUSE_CONNECTOR_ADMIN_URL` — connector admin base URL
54
- - `TOWNHOUSE_DEV_TOWN_01_RELAY`, `TOWNHOUSE_DEV_TOWN_02_RELAY` — relay WebSocket URLs
55
- - `TOWNHOUSE_DEV_TOWN_0{1,2}_HEALTH`, `TOWNHOUSE_DEV_MILL_0{1,2}_HEALTH`, `TOWNHOUSE_DEV_DVM_01_HEALTH` — BLS health URLs
56
- - `TOWNHOUSE_DEV_ANVIL_RPC`, `TOWNHOUSE_DEV_SOLANA_RPC`, `TOWNHOUSE_DEV_MINA_GRAPHQL` — chain RPC URLs
57
- - `SOLANA_PROGRAM_ID`, `MINA_ZKAPP_ADDRESS`, `TOON_USDC_ADDRESS` — deployed contract addresses
58
- - `TOWNHOUSE_DEV_WALLET_MNEMONIC` — **DEV ONLY** BIP-39 test-vector-zero mnemonic (`abandon … about`); read by `api-server.mjs` to auto-initialize the `WalletManager` without running `townhouse init`. This is the publicly known test vector — NEVER use in production. The dev API loop **rejects any other value** at startup so a developer who pastes a real mnemonic by accident gets a loud error rather than silent address derivation.
16
+ ## Before you start
59
17
 
60
- When the dev mnemonic is loaded, the API loop also writes `~/.townhouse/wallet.enc` (if absent) encrypted with the documented dev password `townhouse-dev`. This makes `POST /wallet/reveal` exercisable against the live dev stack — open the wallet view, click "Reveal seed phrase", enter `townhouse-dev`, see the 12-word mnemonic. The on-disk file is never overwritten if it already exists, so an operator who later runs the production `townhouse init` flow keeps their real wallet.
18
+ You'll need:
61
19
 
62
- `.env.townhouse-dev` is git-ignored. Never commit it.
20
+ - [ ] **Docker** running — verify with `docker version` (it pulls and runs your node's containers)
21
+ - [ ] **Node.js 20+** — verify with `node --version`
22
+ - [ ] **Network access to `ghcr.io`** — your node's container images are pulled from there
23
+ - [ ] **A few free ports on `127.0.0.1`** — `9401`, `28090`, `7100`, `3100`, `3200`, `3400` (all loopback-only)
24
+ - [ ] A little disk space for container images
63
25
 
64
- ### TURBO_TOKEN
26
+ > Supported on Linux and macOS (including WSL2). Everything binds to `127.0.0.1` only — nothing is exposed to the public internet except your node's hidden-service address.
65
27
 
66
- `TURBO_TOKEN` is used by the DVM container for Arweave uploads via Turbo. It is passed through from the host environment.
28
+ ---
67
29
 
68
- - **Working on Town/Mill views (21.9–21.11):** No `TURBO_TOKEN` needed. The DVM starts in disabled-upload mode and its health endpoint still responds 200.
69
- - **Working on DVM views (21.12) or upload flows:** Set `TURBO_TOKEN` in your shell before running `up`.
30
+ ## Quickstart
70
31
 
71
- The script logs a warning (not an error) when `TURBO_TOKEN` is unset, then continues.
72
-
73
- ### Teardown
32
+ ### 1. Initialize `init`
74
33
 
75
34
  ```bash
76
- ./scripts/townhouse-dev-infra.sh down # Stop containers + remove .env.townhouse-dev
77
- ./scripts/townhouse-dev-infra.sh down-v # Same + delete named volumes (fresh state next run)
78
- ./scripts/townhouse-dev-infra.sh status # Show container state + health summary
35
+ npx @toon-protocol/townhouse init
79
36
  ```
80
37
 
81
- Use `down-v` when you want a completely fresh channel/data state on the next `up`.
82
-
83
- ### Port allocation
84
-
85
- All ports are `127.0.0.1` only (never `0.0.0.0`). Full table also in `CLAUDE.md` "Townhouse Dev Stack (28xxx)".
38
+ This creates `~/.townhouse/config.yaml` and an encrypted wallet, then **shows your seed phrase once**:
86
39
 
87
- | Host Port | Service |
88
- |-----------|---------|
89
- | 28080 | Connector admin |
90
- | 28050 | SOCKS5 proxy |
91
- | 28100 | town-01 BLS health |
92
- | 28110 | town-02 BLS health |
93
- | 28200 | mill-01 BLS health (EVM↔Solana) |
94
- | 28210 | mill-02 BLS health (EVM↔Mina) |
95
- | 28400 | dvm-01 BLS health |
96
- | 28700 | town-01 relay WebSocket |
97
- | 28710 | town-02 relay WebSocket |
98
- | 28545 | Anvil JSON-RPC |
99
- | 28899 | Solana RPC |
100
- | 28900 | Solana WebSocket |
101
- | 28085 | Mina GraphQL |
102
- | 28181 | Mina accounts manager |
40
+ ```text
41
+ Config created at ~/.townhouse/config.yaml
103
42
 
104
- ### What this stack is NOT
43
+ === IMPORTANT: Back up your seed phrase ===
105
44
 
106
- - **Not a production deployment.** The Townhouse production compose (`docker-compose-townhouse.yml`) describes one operator's actual node. This file describes a contributor's rig. Do not confuse them.
107
- - **Not the SDK E2E topology.** The SDK E2E stack (`docker-compose-sdk-e2e.yml` / `scripts/sdk-e2e-infra.sh`) uses embedded connectors inside SDK peers. The Townhouse dev stack uses a standalone connector fronting separate child nodes — the production Townhouse shape.
108
- - **Not for performance testing.** Boot-and-smoke only. Performance tuning is out of scope for this stack; it belongs in a dedicated story.
109
- - **Not multi-tenant.** The 5 child nodes use deterministic dev keys that never change across `up`/`down`/`up` cycles. They are NOT for use as real TOON nodes.
45
+ snake juice eternal vendor remove ladder aisle crumble match hockey weasel guide
110
46
 
111
- ## Running E2E Tests (Story 21.16)
47
+ This is the ONLY time your seed phrase will be shown.
48
+ Store it safely. You will need it to recover your node keys.
49
+ ============================================
112
50
 
113
- There are two test harnesses with different purposes:
51
+ Wallet saved to ~/.townhouse/wallet.enc
114
52
 
115
- ### 1. Dev stack integration (contributor dev loop)
53
+ Derived Node Addresses:
54
+ -----------------------
55
+ town Nostr: 5eb3ba2d... EVM: 0x90bd2F2f...
56
+ mill Nostr: 47838cd5... EVM: 0xAAE12f6B...
57
+ dvm Nostr: 1b52a745... EVM: 0x18Ac7427...
116
58
 
117
- Uses `townhouse-dev-infra.sh` (multi-peer fixtures, deterministic keys, 28xxx ports).
118
- See "Local Dev Loop" above.
119
-
120
- ```bash
121
- ./scripts/townhouse-dev-infra.sh up
122
- pnpm --filter @toon-protocol/townhouse test:integration -- dev-stack-smoke
59
+ Next start your node:
60
+ npx @toon-protocol/townhouse hs up
123
61
  ```
124
62
 
125
- ### 2. Real-CLI E2E (operator-facing lifecycle Story 21.16)
63
+ **Write the seed phrase down.** It is shown only once and is the only way to recover your keys.
126
64
 
127
- Uses `townhouse-test-infra.sh` (image pre-warm only; tests run the real CLI).
65
+ `init` needs a password to encrypt the wallet. It will prompt you when run in a terminal, or you can pass one non-interactively:
128
66
 
129
67
  ```bash
130
- # One-time image cache warm-up (pulls connector image + builds toon:{town,mill,dvm})
131
- bash scripts/townhouse-test-infra.sh up
132
-
133
- # Run the integration suite (CLI lifecycle + config propagation)
134
- RUN_DOCKER_INTEGRATION=1 pnpm --filter @toon-protocol/townhouse test:integration
135
-
136
- # Or: combined script (up + test + down)
137
- pnpm --filter @toon-protocol/townhouse test:e2e:docker
68
+ npx @toon-protocol/townhouse init --password "<your-password>"
69
+ # or: export TOWNHOUSE_WALLET_PASSWORD=... then run init
138
70
  ```
139
71
 
140
- **Key differences from the dev stack:**
141
-
142
- | | Dev stack (`townhouse-dev-infra.sh`) | Real-CLI E2E (`townhouse-test-infra.sh`) |
143
- |---|---|---|
144
- | Starts containers? | Yes (multi-peer compose) | No (tests run the real CLI) |
145
- | Keys | Deterministic dev keys | Fresh wallet per test (mkdtempSync) |
146
- | Topology | 2 Town + 2 Mill + 1 DVM + SOCKS5 | 1 Town + 1 Mill + 1 DVM |
147
- | Port range | 28xxx | 9400 (API), 9401 (connector admin) |
148
- | Audience | Dashboard developers | CI / publish gate validation |
149
-
150
- **Diagnostic runbook:** see the header comment in `scripts/townhouse-test-infra.sh`.
151
-
152
- **Playwright SPA tests (mock-driven + real-stack):**
72
+ ### 2. Start your node — `hs up`
153
73
 
154
74
  ```bash
155
- # Mock-driven specs (transport flip, config change) — no real stack needed
156
- pnpm --filter @toon-protocol/townhouse-web e2e
157
-
158
- # Real-stack lifecycle spec — requires townhouse up to be running
159
- TOWNHOUSE_E2E_REAL_STACK=1 pnpm --filter @toon-protocol/townhouse-web e2e:real
75
+ npx @toon-protocol/townhouse hs up
160
76
  ```
161
77
 
162
- ## Compose Templates (npm tarball, Story 45.2)
163
-
164
- The published `@toon-protocol/townhouse` package ships two Docker Compose templates:
165
-
166
- | Profile | File in tarball | Purpose |
167
- |---------|-----------------|---------|
168
- | `hs` | `dist/compose/townhouse-hs.yml` | Operator-facing apex boot — digest-pinned GHCR images |
169
- | `dev` | `dist/compose/townhouse-dev.yml` | Contributor dev stack — local `toon:*` build images |
170
-
171
- > **Port collision warning.** The HS template binds canonical ports
172
- > (`127.0.0.1:9401`, `:28090`, `:7100`, `:3100`, `:3200`, `:3400`); the
173
- > contributor dev stack binds 28xxx-namespaced equivalents (28080:9401,
174
- > 28100:3100, 28110:3100, 28200:3200, 28210:3200, 28400:3400, 28700:7100,
175
- > 28710:7100). HS-mode and the dev stack (`scripts/townhouse-dev-infra.sh`)
176
- > **must not run concurrently on the same machine** — host:9401, host:3100,
177
- > host:3200, host:3400, host:7100 will conflict. The HS template's
178
- > single-tenant defaults are intentional for the apex operator path
179
- > (Story 45.4 `townhouse hs up`); open an enhancement issue if multi-tenant
180
- > bindings become a real need.
181
-
182
- ### API
78
+ The first run pulls images and bootstraps the hidden service, narrating each stage:
183
79
 
184
- ```typescript
185
- import { loadComposeTemplate, materializeComposeTemplate } from '@toon-protocol/townhouse';
80
+ ```text
81
+ Pulling 2 apex images...
82
+ [1/2] ghcr.io/toon-protocol/connector@sha256:...
83
+ [2/2] ghcr.io/toon-protocol/townhouse-api@sha256:...
84
+ Bootstrapping hidden service (this takes 30–90s)…
85
+ Apex live at uagxuabpuvm6mf4l4zptgth2442sbct5lvtur2nffpqnouesgawyv2ad.anon
86
+ ```
186
87
 
187
- // Read the rendered YAML for a profile (read-only, returns a string).
188
- const yaml = loadComposeTemplate('hs');
88
+ The address on the final line is **your node's hidden-service address** — share it with peers, who reach you at `wss://<your-address>/btp`. It's also saved to `~/.townhouse/host.json`. On a cold image cache the first boot can take a few minutes; later boots are faster.
189
89
 
190
- // Write the compose file + image-manifest.json to ~/.townhouse/ (side-effecting).
191
- // Both output files are written with mode 0o600 (NFR8 — operator-secret).
192
- const { composePath, manifestPath } = materializeComposeTemplate('hs');
193
- // composePath → ~/.townhouse/compose/townhouse-hs.yml
194
- // manifestPath → ~/.townhouse/image-manifest.json
195
- ```
90
+ If you run it in an interactive terminal, a live dashboard opens after the node is up. Press `Ctrl-C` to exit the dashboard — your node keeps running.
196
91
 
197
- Both functions accept an optional `options` object:
92
+ ### 3. Stop your node `hs down`
198
93
 
199
- ```typescript
200
- interface ComposeLoaderOptions {
201
- townhouseHome?: string; // Override ~/.townhouse/ write target (useful in tests)
202
- distDir?: string; // Override dist/ read root (useful in tests)
203
- }
94
+ ```bash
95
+ npx @toon-protocol/townhouse hs down
204
96
  ```
205
97
 
206
- ### `image-manifest.json` schema
207
-
208
- The manifest pinning every image to a content-addressed `sha256:` digest:
209
-
210
- ```json
211
- {
212
- "schemaVersion": 1,
213
- "townhouseVersion": "0.1.0",
214
- "builtAt": "<ISO timestamp>",
215
- "images": {
216
- "townhouse-api": { "name": "ghcr.io/toon-protocol/townhouse-api", "tag": "0.1.0", "digest": "sha256:..." },
217
- "town": { "name": "ghcr.io/toon-protocol/town", "tag": "0.1.0", "digest": "sha256:..." },
218
- "mill": { "name": "ghcr.io/toon-protocol/mill", "tag": "0.1.0", "digest": "sha256:..." },
219
- "dvm": { "name": "ghcr.io/toon-protocol/dvm", "tag": "0.1.0", "digest": "sha256:..." },
220
- "connector": { "name": "ghcr.io/toon-protocol/connector", "tag": "3.4.1", "digest": "sha256:..." }
221
- }
222
- }
98
+ ```text
99
+ Apex stopped. Volumes preserved — your .anyone address is stable.
223
100
  ```
224
101
 
225
- Full schema source: `scripts/build-image-manifest.mjs` (lines 44–67).
226
-
227
- ### Dev stack compose (canonical source)
102
+ Your hidden-service address stays the same across stop/start. To deliberately rotate to a brand-new address, use `npx @toon-protocol/townhouse hs down --rotate-keys`.
228
103
 
229
- The package-local `packages/townhouse/compose/townhouse-dev.yml` is the canonical source of the dev template. It is shipped verbatim in the npm tarball (no digest substitution — uses local `toon:*` image tags).
104
+ ---
230
105
 
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.
106
+ ## Everyday commands
232
107
 
233
- ## DockerOrchestrator Profiles
108
+ | Command | What it does |
109
+ | -------------------------------------------------- | ------------------------------------------------------ |
110
+ | `townhouse init` | Create config + wallet (one time) |
111
+ | `townhouse hs up` | Start your node and publish its hidden-service address |
112
+ | `townhouse hs down` | Stop your node (address preserved) |
113
+ | `townhouse status` | Show node status |
114
+ | `townhouse health` | Health summary |
115
+ | `townhouse logs <node-id> [-f]` | Tail a node's logs |
116
+ | `townhouse wallet show` | Show your derived addresses |
117
+ | `townhouse node list` / `node add` / `node remove` | Manage child nodes |
118
+ | `townhouse --help` | Full command list |
234
119
 
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:
120
+ (Prefix each with `npx @toon-protocol/townhouse`, or install once and call `townhouse` directly.)
238
121
 
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')`).
122
+ > Running with a config in a non-default location? Add `-c <path-to-config.yaml>` to any command. `init`'s next-step hint includes the right `-c` flag automatically when you use `--config-dir`.
247
123
 
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
- ```
124
+ ---
261
125
 
262
- ### Connector Anon Requirement (HS Profile)
126
+ ## Troubleshooting
263
127
 
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.
128
+ | Symptom | Fix |
129
+ | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
130
+ | `cannot start — host ports already in use` | Another stack is using the canonical ports. Stop it (the message tells you how), or run `hs up --skip-preflight` if you know the conflict is harmless. |
131
+ | Boot fails pulling images | Check your network and that you can reach `ghcr.io`, then retry. |
132
+ | `Docker daemon unreachable` | Start Docker and re-run. |
133
+ | `Wallet password required, but no interactive terminal…` | In CI/SSH there's no prompt — pass `--password` or set `TOWNHOUSE_WALLET_PASSWORD`. |
134
+ | `Wallet not found Run \`townhouse init\` first.` | Run `init` before `hs up`. |
135
+ | Forgot your password | The wallet is encrypted and can't be recovered without it. Re-run `init --force` to regenerate (this **replaces** your keys — only do this if you've backed up the seed elsewhere or are starting fresh). |
271
136
 
272
- ## HS Mode (Apex Install)
137
+ For verbose logs on any failure, re-run with `DEBUG=townhouse:*`.
273
138
 
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.
139
+ ---
277
140
 
278
- ### First-run flow
141
+ ## Using it as a library
279
142
 
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
- ```
143
+ The package also exports its building blocks for programmatic use — `DockerOrchestrator`, `ConnectorAdminClient`, wallet helpers, and the Compose-template loaders:
284
144
 
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
- }
145
+ ```typescript
146
+ import {
147
+ materializeComposeTemplate,
148
+ DockerOrchestrator,
149
+ } from '@toon-protocol/townhouse';
309
150
  ```
310
151
 
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.
152
+ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full API surface and internals.
316
153
 
317
- ### `hs down` vs. `hs down --rotate-keys`
154
+ ## Contributing / local development
318
155
 
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 |
156
+ The local multi-node dev stack, the E2E test harnesses, Compose-template internals, and advanced docker-compose operator paths are documented in [`CONTRIBUTING.md`](./CONTRIBUTING.md).
323
157
 
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
-
346
- ## Running the townhouse as a hidden service (laptop)
347
-
348
- `docker-compose-townhouse-hs.yml` brings up the full operator stack —
349
- apex connector + town + mill + dvm + (optional) Anvil + Solana + EVM
350
- faucet — with the connector publishing a `.anyone` hidden service via
351
- the Anyone Protocol overlay. External peers reach the townhouse only
352
- through that `.anyone` address; the laptop never exposes anything to
353
- the public clearnet.
354
-
355
- ```bash
356
- # 1. Build the local node images (one-time, until they're on ghcr)
357
- docker compose -f docker-compose-townhouse.yml --profile town --profile mill --profile dvm build
358
-
359
- # 2. Pick a chain profile (localnet is the default — copy when ready to switch)
360
- cp .env.townhouse-hs.example .env
361
-
362
- # 3. Boot the HS stack. Profiles select what runs:
363
- # --profile localnet bundles anvil + solana (skip for real testnets)
364
- # --profile town/mill/dvm child nodes
365
- # --profile faucet EVM ETH + Mock USDC faucet UI on :3500
366
- docker compose -f docker-compose-townhouse-hs.yml \
367
- --profile localnet --profile town --profile mill --profile dvm --profile faucet up -d
368
-
369
- # 4. Wait ~30-90s for anon to bootstrap and publish the descriptor.
370
- # Then read your published .anyone address:
371
- docker compose -f docker-compose-townhouse-hs.yml exec connector \
372
- cat /var/lib/anon/hs/hostname
373
- # → eag2qnhil4vpvfo2eu3qtqj3rzzkrzbmboivwwbbgzr4svfvjigoxpad.anyone
374
-
375
- # 5. Share that address with peers. They reach you over Tor at:
376
- # wss://<address>.anyone/btp
377
- ```
378
-
379
- ### Chain configuration
380
-
381
- Mill and the faucet read chain endpoints from environment variables, with
382
- localnet defaults. Override via `.env` to switch profiles — see
383
- `.env.townhouse-hs.example` for the four supported shapes:
384
-
385
- | Profile | EVM | Solana | Mock USDC |
386
- |---|---|---|---|
387
- | **localnet** (default) | bundled Anvil at `anvil:8545` | bundled validator at `solana:8899` | pre-deployed in both bundled images |
388
- | **Akash devnet** | `anvil` lease URL from `deploy/akash/leases.json` | `solana` lease URL from same | baked into `akash-anvil` + `akash-solana` images (same addresses as localnet) |
389
- | **Public testnet** | Sepolia (`infura.io/v3/<KEY>`) | `api.devnet.solana.com` | real Circle testnet USDC contracts |
390
- | **Mainnet** | mainnet RPC | `api.mainnet-beta.solana.com` | real Circle mainnet USDC — **disable the faucet profile** |
391
-
392
- Variables consumed: `EVM_RPC_URL`, `EVM_CHAIN_ID`, `EVM_USDC_ADDRESS`,
393
- `SOLANA_RPC_URL`, `SOLANA_USDC_MINT`. Setting these in `.env` configures
394
- the laptop compose AND the Akash deploy (`scripts/akash-deploy.sh
395
- townhouse`) identically.
396
-
397
- ### Faucet workflow
398
-
399
- **EVM ETH + Mock USDC** — bundled `faucet` service runs at
400
- `http://127.0.0.1:3500`. Operator pastes their address, gets ETH for gas
401
- and Mock USDC for transfers. Rate-limited 1 request per address per hour
402
- by default (override via `FAUCET_RATE_LIMIT_HOURS`). The faucet uses
403
- well-known Anvil dev keys — only meaningful against localnet or the
404
- Akash devnet; harmless against testnets/mainnet (transactions just fail).
405
-
406
- **Solana SOL + Mock USDC** — the standalone EVM faucet container does
407
- NOT yet handle Solana. Two paths until that gap closes:
408
-
409
- 1. **Dashboard panel**: the townhouse host API (`pnpm --filter
410
- @toon-protocol/townhouse-web dev` + the host-side townhouse API)
411
- exposes a Faucet panel that does both EVM and Solana drips through
412
- `POST /api/faucet`. Best for live operator use.
413
- 2. **Script**: `scripts/faucet-sol-usdc.mjs <recipient>` from the host —
414
- talks to whatever `SOLANA_RPC_URL` resolves to in `leases.json`.
415
-
416
- Both options use the bootstrap-baked Mock USDC mint
417
- (`6GbdrVghwNKTz9raga7y3Y4qqX5Zgg3AC4d48Kt7C59Q`) and faucet authority
418
- keypair at `infra/solana/keys/faucet-authority.json`.
419
-
420
- ### Persistence
421
-
422
- The `townhouse-hs-anon` named docker volume preserves
423
- `hs_ed25519_secret_key` across `docker compose down` cycles — the
424
- `.anyone` address is stable for as long as the volume exists. Delete the
425
- volume to rotate the address.
426
-
427
- ### What's exposed on the host (loopback only)
428
-
429
- - Connector admin: `127.0.0.1:9401` — no auth, **never expose publicly**
430
- - Anvil RPC: `127.0.0.1:8545` (localnet profile)
431
- - Solana RPC: `127.0.0.1:8899`, WS `127.0.0.1:8900` (localnet profile)
432
- - Town Nostr relay clearnet: `127.0.0.1:7100` — direct local clients
433
- bypass the HS, handy for debugging
434
- - EVM faucet UI: `127.0.0.1:3500` (faucet profile)
435
-
436
- ### Akash deployment of the same stack
437
-
438
- `deploy/akash/townhouse.sdl.yaml` deploys apex + town + mill + dvm +
439
- faucet to Akash with the same architecture. Chain devnets stay as
440
- separate Akash leases (clearnet) — see `scripts/akash-deploy.sh`'s
441
- `cmd_townhouse` (TODO) for the leases.json wiring. The faucet's HTTP
442
- port is the SDL's "one global service" validator scaffolding;
443
- operationally, external peers reach the townhouse only via the `.anyone`
444
- hidden service.
445
-
446
- ## Package overview
447
-
448
- The `@toon-protocol/townhouse` package provides:
449
-
450
- - **DockerOrchestrator** — manages container lifecycle for Town/Mill/DVM nodes
451
- - **ConnectorConfigGenerator** — generates connector peer config from node identities
452
- - **ConnectorAdminClient** — typed HTTP client for the connector admin API (`/health`, `/admin/peers`, `/admin/metrics.json`)
453
- - **HD wallet management** — BIP-44 key derivation per node type (story 21.4)
454
- - **Fastify REST/WebSocket metrics API** — host-side API for the dashboard (story 21.8)
455
-
456
- See `packages/townhouse/src/index.ts` for the full public API surface.
457
-
458
- ## Transport configuration
459
-
460
- The `transport` block selects how the connector reaches peers (outbound) and
461
- how peers reach the connector (inbound).
462
-
463
- ```yaml
464
- transport:
465
- mode: direct # 'direct' | 'ator'
466
- socksProxy: socks5h://proxy.ator.io:9050 # required when mode='ator'
467
- externalUrl: wss://my-connector.example/btp # see below
468
- hiddenService: # optional — connector publishes its own .anyone HS
469
- dir: /var/lib/anon/hs
470
- port: 3000
471
- startupTimeoutMs: 60000 # optional
472
- stopTimeoutMs: 10000 # optional
473
- externalUrl: wss://forced.anyone/btp # optional override of "auto"
474
- ```
158
+ ## License
475
159
 
476
- **`mode: 'direct'`** — clearnet TCP, no overlay. Default for development.
477
-
478
- **`mode: 'ator'`** — outbound BTP through the Anyone Protocol (ATOR) overlay
479
- via SOCKS5. Requires either `externalUrl` (operator-managed anon binary
480
- external to the connector) OR `hiddenService` (connector manages its own
481
- anon binary in-process and publishes a `.anyone` hidden service). Without
482
- one of these the connector rejects the manifest at boot — the validator
483
- catches this case before deploy.
484
-
485
- **`hiddenService` (Story 35.5 of the connector repo)** — when set, the
486
- connector boots `@anyone-protocol/anyone-client` in-process, spawns the
487
- `anon` binary, and publishes a v3 hidden service. The keypair lives at
488
- `dir`; persist that path on a mounted volume to keep the `.anyone` address
489
- stable across redeploys, or delete it to rotate. The connector reads
490
- `${dir}/hostname` after publish and advertises `wss://<hostname>.anyone/btp`
491
- to peers (you can override with an explicit `externalUrl` if needed).
492
-
493
- **Wire-format note (silent-bug fix in this story):** the previous shape
494
- emitted `transport: { mode: 'ator', socksProxy }` to the connector image,
495
- but the connector at 3.3.x reads a discriminated union keyed on `type`
496
- (`'direct' | 'socks5'`). The unknown `mode` field was silently discarded,
497
- defaulting to direct — operators toggling ATOR got direct traffic anyway.
498
- The current generator emits the correct `type: 'socks5'` shape with
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.
160
+ MIT
@@ -18337,18 +18337,24 @@ function buildCorsOptions() {
18337
18337
  // src/api/build-app.ts
18338
18338
  var STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
18339
18339
  var _localRequire = nodeCreateRequire(import.meta.url);
18340
- function _loadPackageJson() {
18341
- for (const rel of ["../package.json", "../../package.json"]) {
18340
+ function _resolvePackageVersion(req = _localRequire, env = process.env) {
18341
+ for (const rel of [
18342
+ "../package.json",
18343
+ "../../package.json",
18344
+ "./package.json"
18345
+ ]) {
18342
18346
  try {
18343
- return _localRequire(rel);
18347
+ const pkg = req(rel);
18348
+ if (pkg && typeof pkg.version === "string") {
18349
+ return pkg.version;
18350
+ }
18344
18351
  } catch {
18345
18352
  }
18346
18353
  }
18347
- throw new Error(
18348
- "build-app.ts: could not resolve package.json from '../package.json' or '../../package.json'. Bundle layout may have changed \u2014 update the resolution ladder."
18349
- );
18354
+ const envVersion = env["TOWNHOUSE_VERSION"];
18355
+ return typeof envVersion === "string" && envVersion.length > 0 ? envVersion : "0.0.0-unknown";
18350
18356
  }
18351
- var _pkgVersion = _loadPackageJson()["version"];
18357
+ var _pkgVersion = _resolvePackageVersion();
18352
18358
  var LOOPBACK_HOSTS = ["127.0.0.1", "::1", "localhost"];
18353
18359
  async function buildFastifyApp(opts = {}) {
18354
18360
  const bindHost = opts.bindHost ?? "127.0.0.1";
@@ -22108,4 +22114,4 @@ export {
22108
22114
  @scure/bip32/index.js:
22109
22115
  (*! scure-bip32 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) *)
22110
22116
  */
22111
- //# sourceMappingURL=chunk-W33MEOPM.js.map
22117
+ //# sourceMappingURL=chunk-QHFUIWEN.js.map