@toon-protocol/hub 0.34.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.
@@ -0,0 +1,468 @@
1
+ # Townhouse — Hidden Service mode (HS) operator compose template
2
+ #
3
+ # THIS IS A BUILD-TIME TEMPLATE. Do not use it directly.
4
+ # The digest placeholders (${TOON_*_DIGEST}) are substituted by
5
+ # scripts/render-compose-template.mjs during `pnpm build` (when
6
+ # dist/image-manifest.json is present) to produce the fully-resolved
7
+ # dist/compose/townhouse-hs.yml that ships inside the npm tarball.
8
+ #
9
+ # Resolved copy location at runtime: ~/.townhouse/compose/townhouse-hs.yml
10
+ # - Written by materializeComposeTemplate('hs') in packages/townhouse/src/compose-loader.ts
11
+ # - File mode: 0o600 (operator-secret — may embed private keys at deploy time)
12
+ #
13
+ # Story ownership:
14
+ # - Placeholder substitution: Story 45.2 (this file)
15
+ # - Boot sequence (townhouse hs up): Story 45.4
16
+ #
17
+ # Architecture (HS-mode v1, Epic 45):
18
+ # - Apex connector: standalone, anon HS publishing in-process (connector v3.5.x)
19
+ # - townhouse-api: containerized host API — owns /var/run/docker.sock, calls
20
+ # connector admin API, manages lifecycle for lazy-provisioned peers
21
+ # - town/mill/dvm: lazy-provisioned via Docker Compose profiles (Epic 46)
22
+ # Story 45.4 boots only connector + townhouse-api at apex install
23
+ #
24
+ # Digest placeholders (substituted at build time from dist/image-manifest.json):
25
+ # ${TOON_TOWNHOUSE_API_DIGEST} → @sha256:<hex>
26
+ # ${TOON_TOWN_DIGEST} → @sha256:<hex>
27
+ # ${TOON_MILL_DIGEST} → @sha256:<hex>
28
+ # ${TOON_DVM_DIGEST} → @sha256:<hex>
29
+ # ${TOON_CONNECTOR_DIGEST} → @sha256:<hex>
30
+ #
31
+ # Scope guard (Story 45.2 does NOT include):
32
+ # - ator-sidecar / ator-sidecar-relay (connector v3.5.x does HS publishing in-process)
33
+ # - anvil / solana / faucet (dev-stack concerns, not operator-facing)
34
+ # - build: directives (all images are digest-pinned GHCR pulls)
35
+ #
36
+ # Port allocation (HS-mode binds canonical ports — single-tenant operator box):
37
+ # 127.0.0.1:9401 connector admin
38
+ # 127.0.0.1:28090 townhouse-api Fastify
39
+ # 127.0.0.1:7100,3100 town (relay WS, BLS health) — profile gated
40
+ # 127.0.0.1:3200 mill BLS health — profile gated
41
+ # 127.0.0.1:3400 dvm BLS health — profile gated
42
+ #
43
+ # These collide with the contributor dev stack's 28xxx-namespaced bindings
44
+ # (28080:9401, 28100:3100, 28110:3100, 28200:3200, 28210:3200, 28400:3400,
45
+ # 28700:7100, 28710:7100). HS-mode and the contributor dev stack
46
+ # (scripts/townhouse-dev-infra.sh) MUST NOT run concurrently on the same
47
+ # machine. If you need to run multiple townhouse instances on a multi-tenant
48
+ # box, open an enhancement issue — the canonical ports here are intentional
49
+ # for the apex operator path (Story 45.4 `townhouse hs up`).
50
+
51
+ networks:
52
+ townhouse-hs-net:
53
+ name: townhouse-hs-net
54
+ driver: bridge
55
+
56
+ volumes:
57
+ # Named volume for the connector's .anyone keypair + HS state.
58
+ # Survives `docker compose down`; delete to rotate the .anyone address.
59
+ #
60
+ # The explicit `name:` fields bypass Compose's project-prefix mechanism.
61
+ # Without them, Compose derives the project name from the compose file's
62
+ # parent directory (e.g. `compose` when the file lives at
63
+ # ~/.townhouse/compose/townhouse-hs.yml or <tmpDir>/compose/townhouse-hs.yml),
64
+ # producing on-disk volumes named `compose_townhouse-hs-anon` etc. — which
65
+ # would break the test 5 `volumeExists('townhouse-hs-anon')` assertion AND
66
+ # operator-facing rotate-keys docs that reference the bare name. Discovered
67
+ # by Story 46.4 live gate run (Finding H, 2026-05-11).
68
+ townhouse-hs-anon:
69
+ name: townhouse-hs-anon
70
+ townhouse-hs-town-data:
71
+ name: townhouse-hs-town-data
72
+ townhouse-hs-mill-data:
73
+ name: townhouse-hs-mill-data
74
+ townhouse-hs-dvm-data:
75
+ name: townhouse-hs-dvm-data
76
+
77
+ services:
78
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79
+ # Apex connector — terminates inbound BTP, publishes .anyone HS
80
+ #
81
+ # The connector image embeds @anyone-protocol/anyone-client v1.1.x+
82
+ # which handles HS publishing in-process (no sidecar required for v3.5.x+).
83
+ # Config file written by `townhouse hs up` (Story 45.4) on first-run.
84
+ #
85
+ # NFR7: connector MUST NOT mount /var/run/docker.sock.
86
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
87
+ # One-shot init: ensure the .anyone key volume is owned by uid 1000 (node)
88
+ # before the connector starts. Docker creates named volumes as root on first
89
+ # boot; the connector (running as node) cannot write the HS keypair/hostname
90
+ # without this chown step.
91
+ connector-init:
92
+ image: busybox:1.37@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
93
+ user: root
94
+ # anon (Tor-based) requires the HS dir to be mode 0700 — too-permissive
95
+ # directories cause an immediate abort. Docker creates volumes as root 755.
96
+ command:
97
+ [
98
+ 'sh',
99
+ '-c',
100
+ 'mkdir -p /data && chown -R 1000:1000 /data && chmod 700 /data',
101
+ ]
102
+ volumes:
103
+ - townhouse-hs-anon:/data
104
+ restart: 'no'
105
+ networks:
106
+ - townhouse-hs-net
107
+
108
+ connector:
109
+ image: ghcr.io/toon-protocol/connector${TOON_CONNECTOR_DIGEST}
110
+ # v3.5.1 has the multi-arch manifest but the default resolves to arm64 on
111
+ # some Docker versions. Pin to amd64 explicitly until the manifest is fixed.
112
+ platform: linux/amd64
113
+ container_name: townhouse-hs-connector
114
+ hostname: connector
115
+ depends_on:
116
+ connector-init:
117
+ condition: service_completed_successfully
118
+ networks:
119
+ - townhouse-hs-net
120
+ ports:
121
+ # Admin API on host loopback only (NFR9). Operator uses for status.
122
+ - '127.0.0.1:9401:9401'
123
+ volumes:
124
+ # Rendered connector config (Story 45.4 writes this on first-run).
125
+ # TOWNHOUSE_HOME is exported by `townhouse hs up` as the operator's
126
+ # config dir (default ~/.townhouse; --config-dir overrides). Docker does
127
+ # NOT expand `~` in bind-mount sources, so the path must come through
128
+ # Compose interpolation.
129
+ - ${TOWNHOUSE_HOME}/connector.yaml:/config/connector.yaml:ro
130
+ # .anyone keypair + HS state (persists across down/up cycles).
131
+ - townhouse-hs-anon:/var/lib/anon/hs
132
+ environment:
133
+ CONFIG_FILE: /config/connector.yaml
134
+ healthcheck:
135
+ # nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal probe
136
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:9401/health']
137
+ interval: 10s
138
+ timeout: 5s
139
+ retries: 5
140
+ # The managed anon daemon needs ~60-120s to bootstrap circuits and publish
141
+ # the HS hostname before the connector starts its admin API. Keep
142
+ # start_period generous so health-check failures during bootstrap don't
143
+ # prematurely mark the container unhealthy.
144
+ start_period: 150s
145
+ restart: unless-stopped
146
+
147
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
148
+ # Townhouse API — containerized host API (NEW in HS-mode v1)
149
+ #
150
+ # Owns /var/run/docker.sock (the only service that may). Provides:
151
+ # - Fastify REST API for operator dashboard / CLI
152
+ # - Calls connector admin API (/admin/hs-hostname, /admin/peers, etc.)
153
+ # - Manages lifecycle for lazy-provisioned peer containers (Epic 46)
154
+ #
155
+ # Planning doc §4 anchor: "host-side townhouse-api owns dockerode and
156
+ # runs on the townhouse-hs-net so it can reach connector via Docker DNS".
157
+ # Port D21-008: Fastify host API on 127.0.0.1:28090.
158
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
159
+ townhouse-api:
160
+ image: ghcr.io/toon-protocol/townhouse-api${TOON_TOWNHOUSE_API_DIGEST}
161
+ container_name: townhouse-hs-api
162
+ # Run as the operator's host UID so bind-mounted ~/.townhouse files
163
+ # (rw------- 600) are readable. TOWNHOUSE_UID is injected by `townhouse hs up`.
164
+ # Defaults to 1000 (the standard first-user UID on Linux).
165
+ user: '${TOWNHOUSE_UID:-1000}'
166
+ # Add the host's docker socket group as a supplementary group so the
167
+ # non-root container user can read/write /var/run/docker.sock (typically
168
+ # owned root:docker mode 660 on Linux). Without this, every dockerode call
169
+ # — including the `pull-image` step of POST /api/nodes — fails with
170
+ # `connect EACCES /var/run/docker.sock`. TOWNHOUSE_DOCKER_GID is injected
171
+ # by `townhouse hs up` via `statSync('/var/run/docker.sock').gid`.
172
+ group_add:
173
+ - '${TOWNHOUSE_DOCKER_GID:-0}'
174
+ networks:
175
+ - townhouse-hs-net
176
+ depends_on:
177
+ connector:
178
+ condition: service_healthy
179
+ ports:
180
+ # Fastify host API — loopback only (NFR9).
181
+ - '127.0.0.1:28090:28090'
182
+ volumes:
183
+ # Docker socket — townhouse-api is the sole orchestration surface.
184
+ - /var/run/docker.sock:/var/run/docker.sock
185
+ # Operator home — wallet, config, compose files, snapshots (RW).
186
+ # TOWNHOUSE_HOME is exported by `townhouse hs up` (see connector volume
187
+ # above). The container-side path stays `/.townhouse` so config.yaml
188
+ # `TOWNHOUSE_CONFIG: /.townhouse/config.yaml` resolves identically
189
+ # regardless of the operator's host-side config dir.
190
+ - ${TOWNHOUSE_HOME}:/.townhouse:rw
191
+ # Wallet dir mirrored at the host-absolute path so config.yaml's
192
+ # `wallet.encrypted_path` (an absolute host path set by `townhouse init`)
193
+ # resolves correctly inside the container. TOWNHOUSE_WALLET_DIR is
194
+ # injected by `townhouse hs up` as path.dirname(config.wallet.encrypted_path).
195
+ - ${TOWNHOUSE_WALLET_DIR:-~/.townhouse}:${TOWNHOUSE_WALLET_DIR:-~/.townhouse}:ro
196
+ environment:
197
+ # Override entrypoint default '/config/config.yaml' so the API reads from
198
+ # the mounted operator-home dir (where Story 45.4 writes config.yaml).
199
+ TOWNHOUSE_CONFIG: /.townhouse/config.yaml
200
+ # Bind on all interfaces inside the container so Docker's port mapping
201
+ # (127.0.0.1:28090:28090) can reach it from the host. ALLOW_REMOTE=1
202
+ # is required when HOST is non-loopback; Docker's host-only binding on
203
+ # the outer port (127.0.0.1) is the actual access control gate.
204
+ TOWNHOUSE_API_HOST: 0.0.0.0
205
+ TOWNHOUSE_API_ALLOW_REMOTE: '1'
206
+ # Wallet decryption password — operator must export TOWNHOUSE_WALLET_PASSWORD
207
+ # in the shell that runs `docker compose up`. The container-side
208
+ # entrypoint (entrypoint-townhouse-api.ts) enforces the password
209
+ # requirement at startup and exits 1 if it is missing — so the YAML
210
+ # uses `:-` (lenient default) here instead of `:?` (Compose-time
211
+ # mandatory error). The `:?` variant would also force `docker compose
212
+ # down` to error out unless the operator pre-exports the password,
213
+ # which broke the teardown flow. Discovered by Story 46.4 live gate
214
+ # run (Finding J, 2026-05-11).
215
+ TOWNHOUSE_WALLET_PASSWORD: '${TOWNHOUSE_WALLET_PASSWORD:-}'
216
+ # P1b — operator/agent wallet mode. When set, the entrypoint loads the
217
+ # operator wallet DIRECTLY from this mnemonic (no encrypted file, no
218
+ # password). Lenient `:-` default keeps `down` working when unset.
219
+ TOWNHOUSE_MNEMONIC: '${TOWNHOUSE_MNEMONIC:-}'
220
+ # Pass the host-side compose-interpolation values through to the
221
+ # townhouse-api container, because the API itself shells out to
222
+ # `docker compose -f /.townhouse/compose/townhouse-hs.yml up -d <type>`
223
+ # for each lazy peer-node provisioning request (Story 46.2). That
224
+ # nested compose re-parses the SAME YAML and needs the same env vars
225
+ # set — otherwise the townhouse-api volume specs interpolate to
226
+ # `:/.townhouse:rw` (empty source) and the inner `up` aborts with
227
+ # `invalid spec: :/.townhouse:rw: empty section between colons`.
228
+ # All four values are HOST paths/IDs (because Docker bind-mount
229
+ # sources are resolved by the daemon, which sees host paths).
230
+ # Discovered by Story 46.4 live gate run (Finding L, 2026-05-12).
231
+ TOWNHOUSE_HOME: '${TOWNHOUSE_HOME}'
232
+ TOWNHOUSE_WALLET_DIR: '${TOWNHOUSE_WALLET_DIR}'
233
+ TOWNHOUSE_UID: '${TOWNHOUSE_UID:-1000}'
234
+ TOWNHOUSE_DOCKER_GID: '${TOWNHOUSE_DOCKER_GID:-0}'
235
+ # MILL_RELAYS is read by POST /api/nodes {type:'mill'} preflight check
236
+ # inside the townhouse-api process. It must be in the API container's env
237
+ # (not only the mill container's env) so the check works correctly.
238
+ MILL_RELAYS: '${MILL_RELAYS:-}'
239
+ # Chain RPC + token config for the wallet balances/withdraw routes. The
240
+ # network profile + compose .env supply these under the production names;
241
+ # the API must read them (it previously read only TOWNHOUSE_DEV_* dev vars,
242
+ # so EVM balances showed `fetch failed` / `usdc_address_not_configured`
243
+ # and withdraw was blocked on a real testnet/mainnet apex — #232).
244
+ EVM_RPC_URL: '${EVM_RPC_URL:-}'
245
+ EVM_USDC_ADDRESS: '${EVM_USDC_ADDRESS:-}'
246
+ SOLANA_RPC_URL: '${SOLANA_RPC_URL:-}'
247
+ SOLANA_USDC_MINT: '${SOLANA_USDC_MINT:-}'
248
+ healthcheck:
249
+ # nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal probe
250
+ # Use 127.0.0.1 (not localhost) to avoid IPv6 resolution surprises. The
251
+ # townhouse-api exposes /api/transport (not /api/health).
252
+ test:
253
+ [
254
+ 'CMD',
255
+ 'wget',
256
+ '-q',
257
+ '--spider',
258
+ 'http://127.0.0.1:28090/api/transport',
259
+ ]
260
+ interval: 10s
261
+ timeout: 5s
262
+ retries: 5
263
+ start_period: 15s
264
+ restart: unless-stopped
265
+
266
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
267
+ # Town — Nostr relay node (profile: town)
268
+ # Lazy-provisioned via Epic 46: `townhouse node add town`
269
+ #
270
+ # SECURITY TODO (Epic 46): the per-node secrets below use `${VAR:-}` empty
271
+ # defaults, which silently start the container with a zero/empty key when
272
+ # the operator forgets to export the env var. Story 45.2 review (R2-MINOR)
273
+ # recommends switching to `${VAR:?msg}` (fail-fast) once Epic 46 boots
274
+ # these profiles — keeping the empty default for now lets `docker compose
275
+ # config` validation pass without injecting per-node secrets, matching
276
+ # Story 45.4's apex-only boot semantic (only connector + townhouse-api
277
+ # start at first run).
278
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
279
+ town:
280
+ image: ghcr.io/toon-protocol/town${TOON_TOWN_DIGEST}
281
+ container_name: townhouse-hs-town
282
+ profiles: [town]
283
+ networks:
284
+ - townhouse-hs-net
285
+ depends_on:
286
+ connector:
287
+ condition: service_healthy
288
+ expose: ['3000']
289
+ ports:
290
+ - '127.0.0.1:7100:7100'
291
+ - '127.0.0.1:3100:3100'
292
+ environment:
293
+ # nosemgrep: detect-insecure-websocket -- Docker-internal
294
+ CONNECTOR_URL: ws://connector:3000
295
+ ILP_ADDRESS: g.townhouse.town
296
+ NODE_ID: town
297
+ # MUST equal the apex connector's nodeId (g.townhouse). The child connector
298
+ # keys peerRelations by the auth-declared peerId of the inbound session, so
299
+ # a mismatched value (e.g. the old literal 'apex') means the child never
300
+ # applies the 'parent' relation → it F06-rejects every paid PREPARE the
301
+ # apex forwards ("No payment channel claim attached"). entrypoint-town maps
302
+ # PARENT_PEER_ID → TOON_PARENT_PEER_ID.
303
+ PARENT_PEER_ID: g.townhouse
304
+ # Publish price (ILP base units per event). Default 0; the node-env overlay
305
+ # (assembleNodeEnv) sets it from config.nodes.town.feePerEvent. The town
306
+ # enforces it AND advertises it in kind:10032 (feePerByte).
307
+ FEE_PER_EVENT: ${FEE_PER_EVENT:-0}
308
+ # NIP-40 TTL (seconds) for the town's kind:10032 announcement (issue #261).
309
+ # The town re-publishes at half this interval so a live apex stays fresh
310
+ # while an offline one's announcement expires — clients then skip its
311
+ # unreachable BTP endpoint instead of failing against it. Especially
312
+ # relevant for an HS apex (relay can outlive the BTP hidden service).
313
+ # Default 1h; set 0 to disable the expiration tag + heartbeat.
314
+ TOON_ANNOUNCEMENT_TTL_SECONDS: ${TOON_ANNOUNCEMENT_TTL_SECONDS:-3600}
315
+ # Apex public BTP URL the town advertises in kind:10032 so clients learn
316
+ # where to route packets for g.townhouse.town (.anyone URL or direct).
317
+ # Set by the node-env overlay / boot rebinder; entrypoint-town maps it to
318
+ # TOON_BTP_ENDPOINT.
319
+ PUBLIC_BTP_URL: ${PUBLIC_BTP_URL:-}
320
+ # Public Nostr relay read URL advertised in kind:10032/10166 (HS .anyone
321
+ # relay URL, derived by `hs up` from the relay hidden service). entrypoint
322
+ # maps it to TOON_EXTERNAL_RELAY_URL.
323
+ PUBLIC_RELAY_URL: ${PUBLIC_RELAY_URL:-}
324
+ # Settlement asset advertised in kind:10032 (operator-configurable token).
325
+ ASSET_CODE: ${ASSET_CODE:-}
326
+ ASSET_SCALE: ${ASSET_SCALE:-}
327
+ # Chain selection — driven by the `network` flag (resolveNetworkProfile).
328
+ # EVM_CHAIN is the primary EVM preset name (e.g. base-mainnet) → town
329
+ # TOON_CHAIN; EVM_RPC_URL is the matching RPC. Both come from the node-env
330
+ # overlay (nodes-lifecycle buildNetworkNodeEnv) and ~/.townhouse/compose/.env
331
+ # (env-writer). 'none' makes the town node run relay-only (no settlement).
332
+ TOON_CHAIN: ${EVM_CHAIN:-}
333
+ TOON_RPC_URL: ${EVM_RPC_URL:-}
334
+ # x-only pubkey derived from the node secret at provisioning time and
335
+ # injected by the API (nodes-lifecycle buildNodeEnv → TOWN_NOSTR_PUBKEY).
336
+ # Informational — lets operators / SDK clients read it via `docker inspect`
337
+ # or `node list --json` without re-deriving from the secret (issue #81).
338
+ NODE_NOSTR_PUBKEY: '${TOWN_NOSTR_PUBKEY:-}'
339
+ NODE_EVM_ADDRESS: ''
340
+ # Derived from HD wallet at runtime (Story 45.4 / Epic 46).
341
+ NODE_NOSTR_SECRET_KEY: '${TOWN_SECRET_KEY:-}'
342
+ # Town reads TOON_SETTLEMENT_PRIVATE_KEY (packages/town/src/cli.ts:305),
343
+ # 0x-prefixed 32-byte hex. The API passes the already-0x-prefixed value
344
+ # via TOWN_SETTLEMENT_PRIVATE_KEY. Previously this was wired to the wrong
345
+ # env var name (`SETTLEMENT_PRIVATE_KEY`), so town crash-looped at boot
346
+ # with `TOON_SETTLEMENT_PRIVATE_KEY must be a 0x-prefixed 32-byte hex
347
+ # string` and never reached its /health endpoint — failing the gate
348
+ # at step 5 (healthcheck). Story 46.4 live gate run (Finding N+O,
349
+ # 2026-05-12).
350
+ TOON_SETTLEMENT_PRIVATE_KEY: '${TOWN_SETTLEMENT_PRIVATE_KEY:-}'
351
+ PARENT_EVM_ADDRESS: '${APEX_EVM_ADDRESS:-}'
352
+ TOON_CONNECTOR_LOG_LEVEL: '${TOON_CONNECTOR_LOG_LEVEL:-warn}'
353
+ volumes:
354
+ - townhouse-hs-town-data:/data
355
+ healthcheck:
356
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3100/health']
357
+ interval: 30s
358
+ timeout: 10s
359
+ retries: 3
360
+ start_period: 10s
361
+ restart: unless-stopped
362
+
363
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
364
+ # Mill — multi-chain swap node (profile: mill)
365
+ # Lazy-provisioned via Epic 46: `townhouse node add mill`
366
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
367
+ mill:
368
+ image: ghcr.io/toon-protocol/mill${TOON_MILL_DIGEST}
369
+ container_name: townhouse-hs-mill
370
+ profiles: [mill]
371
+ networks:
372
+ - townhouse-hs-net
373
+ depends_on:
374
+ connector:
375
+ condition: service_healthy
376
+ expose: ['3000']
377
+ ports:
378
+ - '127.0.0.1:3200:3200'
379
+ environment:
380
+ # nosemgrep: detect-insecure-websocket -- Docker-internal
381
+ CONNECTOR_URL: ws://connector:3000
382
+ # MUST match the route the apex registers for the mill peer
383
+ # (`g.townhouse.<type>` — see nodes-lifecycle.ts). entrypoint-mill maps
384
+ # ILP_ADDRESS → TOON_ILP_ADDRESS → the embedded connector's self-route.
385
+ # WITHOUT this the mill defaults its self-route to `g.toon.mill.<pubkey>`,
386
+ # the apex-forwarded swap PREPARE (`g.townhouse.mill`) misses it, falls
387
+ # through to the up-to-parent route, and the per-packet-claim-service
388
+ # T00-rejects trying to open an outbound channel back to g.townhouse
389
+ # (issue #157). town sets the equivalent ILP_ADDRESS=g.townhouse.town.
390
+ ILP_ADDRESS: g.townhouse.mill
391
+ NODE_ID: mill
392
+ # MUST equal the apex connector's nodeId (g.townhouse) so the mill's
393
+ # embedded connector applies the 'parent' relation to the apex session and
394
+ # treats forwarded swap PREPAREs as free parent traffic. entrypoint-mill
395
+ # reads TOON_PARENT_PEER_ID directly (it does NOT map PARENT_PEER_ID like
396
+ # town does); without it the mill defaults to 'apex' and rejects swaps with
397
+ # T00 "Per-packet claim service not configured".
398
+ TOON_PARENT_PEER_ID: g.townhouse
399
+ FEE_BASIS_POINTS: '0'
400
+ SETTLEMENT_RPC_URL: ${EVM_RPC_URL:-}
401
+ SETTLEMENT_CHAIN_ID: ${EVM_CHAIN_ID:-}
402
+ SETTLEMENT_TOKEN_ADDRESS: ${EVM_USDC_ADDRESS:-}
403
+ SOLANA_RPC_URL: ${SOLANA_RPC_URL:-}
404
+ SOLANA_USDC_MINT: ${SOLANA_USDC_MINT:-}
405
+ # x-only pubkey injected by the API (buildNodeEnv → MILL_NOSTR_PUBKEY).
406
+ # SDK clients need the mill pubkey for streamSwap seal verification —
407
+ # surfacing it here avoids re-deriving from the secret (issue #81).
408
+ NODE_NOSTR_PUBKEY: '${MILL_NOSTR_PUBKEY:-}'
409
+ NODE_EVM_ADDRESS: ''
410
+ # Derived from HD wallet at runtime (Story 45.4 / Epic 46).
411
+ MILL_MNEMONIC: '${MILL_MNEMONIC:-}'
412
+ NODE_NOSTR_SECRET_KEY: '${MILL_SECRET_KEY:-}'
413
+ MILL_CONFIG_PATH: /config/mill.config.json
414
+ MILL_RELAYS: ${MILL_RELAYS:-}
415
+ SETTLEMENT_PRIVATE_KEY: '${MILL_SETTLEMENT_PRIVATE_KEY:-}'
416
+ PARENT_EVM_ADDRESS: '${APEX_EVM_ADDRESS:-}'
417
+ TOON_CONNECTOR_LOG_LEVEL: '${TOON_CONNECTOR_LOG_LEVEL:-warn}'
418
+ volumes:
419
+ # Operator-managed mill config (Epic 46 provisions this on `townhouse node add mill`).
420
+ # TOWNHOUSE_HOME is exported by `townhouse hs up` (see connector volume above).
421
+ - ${TOWNHOUSE_HOME}/mill.config.json:/config/mill.config.json:ro
422
+ - townhouse-hs-mill-data:/data
423
+ healthcheck:
424
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3200/health']
425
+ interval: 30s
426
+ timeout: 10s
427
+ retries: 3
428
+ start_period: 15s
429
+ restart: unless-stopped
430
+
431
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
432
+ # DVM — Arweave upload DVM (profile: dvm)
433
+ # Lazy-provisioned via Epic 46: `townhouse node add dvm`
434
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
435
+ dvm:
436
+ image: ghcr.io/toon-protocol/dvm${TOON_DVM_DIGEST}
437
+ container_name: townhouse-hs-dvm
438
+ profiles: [dvm]
439
+ networks:
440
+ - townhouse-hs-net
441
+ depends_on:
442
+ connector:
443
+ condition: service_healthy
444
+ expose: ['3300']
445
+ ports:
446
+ - '127.0.0.1:3400:3400'
447
+ environment:
448
+ # nosemgrep: detect-insecure-websocket -- Docker-internal
449
+ CONNECTOR_URL: ws://connector:3000
450
+ FEE_PER_JOB: '0'
451
+ DVM_KIND: '5094'
452
+ # x-only pubkey injected by the API (buildNodeEnv → DVM_NOSTR_PUBKEY).
453
+ # Informational — readable via `docker inspect` / `node list --json`
454
+ # without re-deriving from the secret (issue #81).
455
+ NODE_NOSTR_PUBKEY: '${DVM_NOSTR_PUBKEY:-}'
456
+ NODE_EVM_ADDRESS: ''
457
+ # Derived from HD wallet at runtime (Story 45.4 / Epic 46).
458
+ NODE_NOSTR_SECRET_KEY: '${DVM_SECRET_KEY:-}'
459
+ TURBO_TOKEN: ${TURBO_TOKEN:-}
460
+ volumes:
461
+ - townhouse-hs-dvm-data:/data
462
+ healthcheck:
463
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3400/health']
464
+ interval: 30s
465
+ timeout: 10s
466
+ retries: 3
467
+ start_period: 10s
468
+ restart: unless-stopped
@@ -0,0 +1,118 @@
1
+ import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
+ import {
3
+ DEFAULT_CONNECTOR_IMAGE
4
+ } from "./chunk-MNVIN5XK.js";
5
+ import "./chunk-I2R4CRUX.js";
6
+
7
+ // src/presets/demo.ts
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ import { readFileSync, existsSync } from "fs";
11
+ import { resolve } from "path";
12
+ var LOCAL_DEVNET_FALLBACK = {
13
+ anvilUrl: "http://localhost:28545",
14
+ solanaUrl: "http://localhost:28899"
15
+ };
16
+ var DEMO_DETERMINISTIC_PASSWORD = "townhouse-demo-INSECURE-do-not-use-in-prod";
17
+ function resolveChainEndpoints(leasesPath) {
18
+ const localEvm = { rpcUrl: LOCAL_DEVNET_FALLBACK.anvilUrl };
19
+ const localSol = { rpcUrl: LOCAL_DEVNET_FALLBACK.solanaUrl };
20
+ if (!leasesPath || !existsSync(leasesPath)) {
21
+ return { source: "local-fallback", evm: localEvm, solana: localSol };
22
+ }
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(readFileSync(leasesPath, "utf-8"));
26
+ } catch {
27
+ return { source: "local-fallback", evm: localEvm, solana: localSol };
28
+ }
29
+ const evmUrl = typeof parsed.anvil?.url === "string" ? parsed.anvil.url : void 0;
30
+ const evmWs = typeof parsed.anvil?.ws_url === "string" ? parsed.anvil.ws_url : void 0;
31
+ const solUrl = typeof parsed.solana?.url === "string" ? parsed.solana.url : void 0;
32
+ const solWs = typeof parsed.solana?.ws_url === "string" ? parsed.solana.ws_url : void 0;
33
+ return {
34
+ source: leasesPath,
35
+ evm: evmUrl ? { rpcUrl: evmUrl, ...evmWs ? { wsUrl: evmWs } : {} } : localEvm,
36
+ solana: solUrl ? { rpcUrl: solUrl, ...solWs ? { wsUrl: solWs } : {} } : localSol
37
+ };
38
+ }
39
+ function defaultLeasesPath() {
40
+ return resolve(process.cwd(), "deploy", "akash", "leases.json");
41
+ }
42
+ function buildDemoConfig(options) {
43
+ const leasesPath = options.leasesPath === null ? void 0 : options.leasesPath ?? defaultLeasesPath();
44
+ const endpoints = resolveChainEndpoints(leasesPath);
45
+ return {
46
+ nodes: {
47
+ town: {
48
+ enabled: true,
49
+ feePerEvent: 0
50
+ },
51
+ mill: {
52
+ enabled: true,
53
+ feeBasisPoints: 0,
54
+ // Demo pair: EVM (Anvil) <-> Solana. The orchestrator does not
55
+ // currently consume mill.chains directly — it round-trips through
56
+ // YAML so the dashboard / future stories can read it.
57
+ chains: {
58
+ evm: {
59
+ rpcUrl: endpoints.evm.rpcUrl,
60
+ ...endpoints.evm.wsUrl ? { wsUrl: endpoints.evm.wsUrl } : {}
61
+ },
62
+ solana: {
63
+ rpcUrl: endpoints.solana.rpcUrl,
64
+ ...endpoints.solana.wsUrl ? { wsUrl: endpoints.solana.wsUrl } : {}
65
+ }
66
+ },
67
+ pairs: ["EVM<->SOL"]
68
+ },
69
+ dvm: {
70
+ enabled: true,
71
+ feePerJob: 0,
72
+ // Arweave DVM (kind:5094) — frictionless demo pricing. Operators
73
+ // running for real should raise this; entrypoint-dvm.ts treats the
74
+ // value as msats per byte uploaded to Arweave.
75
+ kindPricing: { "5094": 0 }
76
+ }
77
+ },
78
+ wallet: {
79
+ encrypted_path: options.walletPath
80
+ },
81
+ connector: {
82
+ image: DEFAULT_CONNECTOR_IMAGE,
83
+ adminPort: 9401
84
+ },
85
+ transport: {
86
+ // 'direct' for the demo because `townhouse up` doesn't bring up a
87
+ // SOCKS5 sidecar — hs mode would require one at `socks5://127.0.0.1:28050`
88
+ // (provided by the dev-infra stack but not the operator CLI). Switch
89
+ // to 'hs' once the sidecar story lands in townhouse `up`.
90
+ mode: "direct"
91
+ },
92
+ api: {
93
+ port: 9400,
94
+ host: "127.0.0.1"
95
+ },
96
+ logging: {
97
+ level: "info"
98
+ },
99
+ preset: {
100
+ name: "demo",
101
+ // Source recorded so operators can see at-a-glance whether their demo
102
+ // is hitting Akash or local devnets.
103
+ chainEndpointSource: endpoints.source
104
+ }
105
+ };
106
+ }
107
+ function defaultDemoConfigDir() {
108
+ return join(homedir(), ".townhouse");
109
+ }
110
+ export {
111
+ DEMO_DETERMINISTIC_PASSWORD,
112
+ LOCAL_DEVNET_FALLBACK,
113
+ buildDemoConfig,
114
+ defaultDemoConfigDir,
115
+ defaultLeasesPath,
116
+ resolveChainEndpoints
117
+ };
118
+ //# sourceMappingURL=demo-UJ37MLCG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/presets/demo.ts"],"sourcesContent":["/**\n * `--preset=demo` configuration (Story D2).\n *\n * Non-interactive preset for the TOON demo: 1 town, 1 mill (EVM<->SOL pair),\n * 1 dvm, ATOR transport ON, all fees zeroed (demo = free). Chain RPC URLs are\n * sourced from `deploy/akash/leases.json` if present, otherwise from local\n * devnet defaults documented in CLAUDE.md (Anvil 28545, Solana 28899).\n *\n * Future presets (test, prod) follow the same shape — see {@link PresetBuilder}.\n *\n * NOTE on schema reach: this preset writes mill chain endpoints into a\n * `chains` field on the mill node config. The orchestrator wiring that\n * forwards these into MILL_CONFIG_JSON is out of scope for D2 — for now the\n * field round-trips through the YAML so future stories (and the dashboard)\n * can read it without re-deriving it from leases.json.\n */\n\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { readFileSync, existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { TownhouseConfig } from '../config/schema.js';\nimport { DEFAULT_CONNECTOR_IMAGE } from '../constants.js';\n\n/**\n * Preset identifier — keep the union closed so we surface unknown presets at\n * the CLI boundary instead of silently falling through to defaults.\n */\nexport type PresetName = 'demo';\n\n/**\n * Local-devnet fallback URLs (CLAUDE.md \"Townhouse Dev Stack\" 28xxx range).\n * Used when `deploy/akash/leases.json` is absent or unreadable.\n */\nexport const LOCAL_DEVNET_FALLBACK = {\n anvilUrl: 'http://localhost:28545',\n solanaUrl: 'http://localhost:28899',\n} as const;\n\n/**\n * Deterministic-but-clearly-unsafe password used for the demo wallet when the\n * caller passes `--preset=demo --yes` without `--password`. The string\n * embeds the warning so it shows up in any log scrape; production callers\n * MUST supply their own `--password` (AC-D2-6).\n */\nexport const DEMO_DETERMINISTIC_PASSWORD =\n 'townhouse-demo-INSECURE-do-not-use-in-prod';\n\n/**\n * Shape of `deploy/akash/leases.json` that this preset cares about. The full\n * file emitted by `scripts/akash-deploy.sh` has more fields; we only need\n * RPC + WS URLs here, so the type is intentionally narrow + tolerant.\n */\nexport interface AkashLeases {\n anvil?: {\n url?: string;\n host?: string;\n port?: number | string;\n ws_url?: string;\n };\n solana?: {\n url?: string;\n host?: string;\n port?: number | string;\n ws_host?: string;\n ws_port?: number | string;\n ws_url?: string;\n };\n}\n\nexport interface ResolvedChainEndpoints {\n /** Source of truth for traceability — either an absolute leases.json path or 'local-fallback'. */\n source: string;\n evm: { rpcUrl: string; wsUrl?: string };\n solana: { rpcUrl: string; wsUrl?: string };\n}\n\n/**\n * Read `deploy/akash/leases.json` and extract chain endpoints, falling back\n * to local devnet URLs if the file is missing or any field is unusable.\n *\n * The fallback is per-chain: a leases.json that defines anvil but not solana\n * still gets the local Solana URL (and vice-versa). This keeps half-deployed\n * states usable.\n */\nexport function resolveChainEndpoints(\n leasesPath?: string\n): ResolvedChainEndpoints {\n const localEvm = { rpcUrl: LOCAL_DEVNET_FALLBACK.anvilUrl };\n const localSol = { rpcUrl: LOCAL_DEVNET_FALLBACK.solanaUrl };\n\n if (!leasesPath || !existsSync(leasesPath)) {\n return { source: 'local-fallback', evm: localEvm, solana: localSol };\n }\n\n let parsed: AkashLeases;\n try {\n parsed = JSON.parse(readFileSync(leasesPath, 'utf-8')) as AkashLeases;\n } catch {\n // Malformed JSON — better to demo on local devnets than to error out at\n // wizard-bypass time. Caller never blocks on bad leases data.\n return { source: 'local-fallback', evm: localEvm, solana: localSol };\n }\n\n const evmUrl =\n typeof parsed.anvil?.url === 'string' ? parsed.anvil.url : undefined;\n const evmWs =\n typeof parsed.anvil?.ws_url === 'string' ? parsed.anvil.ws_url : undefined;\n const solUrl =\n typeof parsed.solana?.url === 'string' ? parsed.solana.url : undefined;\n const solWs =\n typeof parsed.solana?.ws_url === 'string'\n ? parsed.solana.ws_url\n : undefined;\n\n return {\n source: leasesPath,\n evm: evmUrl\n ? { rpcUrl: evmUrl, ...(evmWs ? { wsUrl: evmWs } : {}) }\n : localEvm,\n solana: solUrl\n ? { rpcUrl: solUrl, ...(solWs ? { wsUrl: solWs } : {}) }\n : localSol,\n };\n}\n\nexport interface BuildDemoConfigOptions {\n /** Absolute path to the wallet file (typically `<configDir>/wallet.enc`). */\n walletPath: string;\n /**\n * Absolute path to `deploy/akash/leases.json`. Defaults to\n * `<repoRoot>/deploy/akash/leases.json` if not provided. Pass `null` to\n * force local-devnet fallback (used in tests and when the file is known\n * to not exist on the operator's machine).\n */\n leasesPath?: string | null;\n}\n\n/**\n * Default location of the Akash leases file. Resolved relative to CWD —\n * the demo CLI is run from anywhere, but the leases.json that matters lives\n * at `<repo>/deploy/akash/leases.json`.\n */\nexport function defaultLeasesPath(): string {\n return resolve(process.cwd(), 'deploy', 'akash', 'leases.json');\n}\n\n/**\n * Build the full TownhouseConfig for `--preset=demo`.\n *\n * AC-D2-5 invariants enforced here:\n * - 1 town, 1 mill, 1 dvm (all enabled)\n * - feePerEvent = 0, feeBasisPoints = 0, feePerJob = 0\n * - transport.mode = 'direct'\n * - mill.chains contains exactly one EVM<->SOL pair\n */\nexport function buildDemoConfig(\n options: BuildDemoConfigOptions\n): TownhouseConfig {\n const leasesPath =\n options.leasesPath === null\n ? undefined\n : (options.leasesPath ?? defaultLeasesPath());\n\n const endpoints = resolveChainEndpoints(leasesPath);\n\n return {\n nodes: {\n town: {\n enabled: true,\n feePerEvent: 0,\n },\n mill: {\n enabled: true,\n feeBasisPoints: 0,\n // Demo pair: EVM (Anvil) <-> Solana. The orchestrator does not\n // currently consume mill.chains directly — it round-trips through\n // YAML so the dashboard / future stories can read it.\n chains: {\n evm: {\n rpcUrl: endpoints.evm.rpcUrl,\n ...(endpoints.evm.wsUrl ? { wsUrl: endpoints.evm.wsUrl } : {}),\n },\n solana: {\n rpcUrl: endpoints.solana.rpcUrl,\n ...(endpoints.solana.wsUrl\n ? { wsUrl: endpoints.solana.wsUrl }\n : {}),\n },\n },\n pairs: ['EVM<->SOL'],\n },\n dvm: {\n enabled: true,\n feePerJob: 0,\n // Arweave DVM (kind:5094) — frictionless demo pricing. Operators\n // running for real should raise this; entrypoint-dvm.ts treats the\n // value as msats per byte uploaded to Arweave.\n kindPricing: { '5094': 0 },\n },\n },\n wallet: {\n encrypted_path: options.walletPath,\n },\n connector: {\n image: DEFAULT_CONNECTOR_IMAGE,\n adminPort: 9401,\n },\n transport: {\n // 'direct' for the demo because `townhouse up` doesn't bring up a\n // SOCKS5 sidecar — hs mode would require one at `socks5://127.0.0.1:28050`\n // (provided by the dev-infra stack but not the operator CLI). Switch\n // to 'hs' once the sidecar story lands in townhouse `up`.\n mode: 'direct',\n },\n api: {\n port: 9400,\n host: '127.0.0.1',\n },\n logging: {\n level: 'info',\n },\n preset: {\n name: 'demo',\n // Source recorded so operators can see at-a-glance whether their demo\n // is hitting Akash or local devnets.\n chainEndpointSource: endpoints.source,\n },\n };\n}\n\n/**\n * Default config dir used by the demo preset when `--config-dir` is omitted.\n * Mirrors the value in cli.ts; duplicated here so tests can construct the\n * same path without importing from the CLI surface.\n */\nexport function defaultDemoConfigDir(): string {\n return join(homedir(), '.townhouse');\n}\n"],"mappings":"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","names":[]}