@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.
@@ -48,7 +48,7 @@ services:
48
48
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
49
49
  townhouse-dev-connector:
50
50
  # Image tag must match DEFAULT_CONNECTOR_IMAGE in packages/townhouse/src/constants.ts
51
- image: ghcr.io/toon-protocol/connector:3.4.1
51
+ image: ghcr.io/toon-protocol/connector:3.8.0
52
52
  container_name: townhouse-dev-connector
53
53
  networks:
54
54
  - townhouse-dev-net
@@ -22,11 +22,11 @@
22
22
  # Story 45.4 boots only connector + townhouse-api at apex install
23
23
  #
24
24
  # Digest placeholders (substituted at build time from dist/image-manifest.json):
25
- # @sha256:48cb3df422019e252b7b1b5506b2fc666ea206ec57024eb68acbf781ebeb1f11 → @sha256:<hex>
26
- # @sha256:ba83fb6df536dec240f27fb7449db59e803207221c54e61ea019375ddc0461a0 → @sha256:<hex>
27
- # @sha256:38e8c6460a3fc562dab28284b4f51958d8417ce10a7152bbb4224fc00cc42df5 → @sha256:<hex>
28
- # @sha256:26a2aff4cb490d11f8eab9cff3272475c951e7cc87378a1af6c702b529d74e10 → @sha256:<hex>
29
- # @sha256:4a24ccb0997d7b025997e670546032f6a84cd18a77c490509016b85e181a344e → @sha256:<hex>
25
+ # @sha256:f2cf4725895a3fe49593831481d4641465341533786150edf78d337868352994 → @sha256:<hex>
26
+ # @sha256:cada4f94a28e6ddfa4c01b37b5aadc1a8fa374b5fd8e85478d4aa5be33a2d961 → @sha256:<hex>
27
+ # @sha256:5269013677914ebc68d52c844add621c9a34e3219393280353227d074fa8787f → @sha256:<hex>
28
+ # @sha256:4d8e2cf5412c8bbfd9321c416356ba7b3c52ab8405db0df9f1b8f9d266c17cff → @sha256:<hex>
29
+ # @sha256:3343c19649290043e521c81b467b7c6410b8eaedd76d48804ea9b6fc810cddb0 → @sha256:<hex>
30
30
  #
31
31
  # Scope guard (Story 45.2 does NOT include):
32
32
  # - ator-sidecar / ator-sidecar-relay (connector v3.5.x does HS publishing in-process)
@@ -50,15 +50,29 @@
50
50
 
51
51
  networks:
52
52
  townhouse-hs-net:
53
+ name: townhouse-hs-net
53
54
  driver: bridge
54
55
 
55
56
  volumes:
56
57
  # Named volume for the connector's .anyone keypair + HS state.
57
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).
58
68
  townhouse-hs-anon:
69
+ name: townhouse-hs-anon
59
70
  townhouse-hs-town-data:
71
+ name: townhouse-hs-town-data
60
72
  townhouse-hs-mill-data:
73
+ name: townhouse-hs-mill-data
61
74
  townhouse-hs-dvm-data:
75
+ name: townhouse-hs-dvm-data
62
76
 
63
77
  services:
64
78
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -70,10 +84,32 @@ services:
70
84
  #
71
85
  # NFR7: connector MUST NOT mount /var/run/docker.sock.
72
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: ['sh', '-c', 'mkdir -p /data && chown -R 1000:1000 /data && chmod 700 /data']
97
+ volumes:
98
+ - townhouse-hs-anon:/data
99
+ restart: 'no'
100
+ networks:
101
+ - townhouse-hs-net
102
+
73
103
  connector:
74
- image: ghcr.io/toon-protocol/connector@sha256:4a24ccb0997d7b025997e670546032f6a84cd18a77c490509016b85e181a344e
104
+ image: ghcr.io/toon-protocol/connector@sha256:3343c19649290043e521c81b467b7c6410b8eaedd76d48804ea9b6fc810cddb0
105
+ # v3.5.1 has the multi-arch manifest but the default resolves to arm64 on
106
+ # some Docker versions. Pin to amd64 explicitly until the manifest is fixed.
107
+ platform: linux/amd64
75
108
  container_name: townhouse-hs-connector
76
109
  hostname: connector
110
+ depends_on:
111
+ connector-init:
112
+ condition: service_completed_successfully
77
113
  networks:
78
114
  - townhouse-hs-net
79
115
  ports:
@@ -81,7 +117,11 @@ services:
81
117
  - '127.0.0.1:9401:9401'
82
118
  volumes:
83
119
  # Rendered connector config (Story 45.4 writes this on first-run).
84
- - ~/.townhouse/connector.yaml:/config/connector.yaml:ro
120
+ # TOWNHOUSE_HOME is exported by `townhouse hs up` as the operator's
121
+ # config dir (default ~/.townhouse; --config-dir overrides). Docker does
122
+ # NOT expand `~` in bind-mount sources, so the path must come through
123
+ # Compose interpolation.
124
+ - ${TOWNHOUSE_HOME}/connector.yaml:/config/connector.yaml:ro
85
125
  # .anyone keypair + HS state (persists across down/up cycles).
86
126
  - townhouse-hs-anon:/var/lib/anon/hs
87
127
  environment:
@@ -92,7 +132,11 @@ services:
92
132
  interval: 10s
93
133
  timeout: 5s
94
134
  retries: 5
95
- start_period: 30s
135
+ # The managed anon daemon needs ~60-120s to bootstrap circuits and publish
136
+ # the HS hostname before the connector starts its admin API. Keep
137
+ # start_period generous so health-check failures during bootstrap don't
138
+ # prematurely mark the container unhealthy.
139
+ start_period: 150s
96
140
  restart: unless-stopped
97
141
 
98
142
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -108,8 +152,20 @@ services:
108
152
  # Port D21-008: Fastify host API on 127.0.0.1:28090.
109
153
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
110
154
  townhouse-api:
111
- image: ghcr.io/toon-protocol/townhouse-api@sha256:48cb3df422019e252b7b1b5506b2fc666ea206ec57024eb68acbf781ebeb1f11
155
+ image: ghcr.io/toon-protocol/townhouse-api@sha256:f2cf4725895a3fe49593831481d4641465341533786150edf78d337868352994
112
156
  container_name: townhouse-hs-api
157
+ # Run as the operator's host UID so bind-mounted ~/.townhouse files
158
+ # (rw------- 600) are readable. TOWNHOUSE_UID is injected by `townhouse hs up`.
159
+ # Defaults to 1000 (the standard first-user UID on Linux).
160
+ user: '${TOWNHOUSE_UID:-1000}'
161
+ # Add the host's docker socket group as a supplementary group so the
162
+ # non-root container user can read/write /var/run/docker.sock (typically
163
+ # owned root:docker mode 660 on Linux). Without this, every dockerode call
164
+ # — including the `pull-image` step of POST /api/nodes — fails with
165
+ # `connect EACCES /var/run/docker.sock`. TOWNHOUSE_DOCKER_GID is injected
166
+ # by `townhouse hs up` via `statSync('/var/run/docker.sock').gid`.
167
+ group_add:
168
+ - '${TOWNHOUSE_DOCKER_GID:-0}'
113
169
  networks:
114
170
  - townhouse-hs-net
115
171
  depends_on:
@@ -122,18 +178,60 @@ services:
122
178
  # Docker socket — townhouse-api is the sole orchestration surface.
123
179
  - /var/run/docker.sock:/var/run/docker.sock
124
180
  # Operator home — wallet, config, compose files, snapshots (RW).
125
- - ~/.townhouse:/.townhouse:rw
181
+ # TOWNHOUSE_HOME is exported by `townhouse hs up` (see connector volume
182
+ # above). The container-side path stays `/.townhouse` so config.yaml
183
+ # `TOWNHOUSE_CONFIG: /.townhouse/config.yaml` resolves identically
184
+ # regardless of the operator's host-side config dir.
185
+ - ${TOWNHOUSE_HOME}:/.townhouse:rw
186
+ # Wallet dir mirrored at the host-absolute path so config.yaml's
187
+ # `wallet.encrypted_path` (an absolute host path set by `townhouse init`)
188
+ # resolves correctly inside the container. TOWNHOUSE_WALLET_DIR is
189
+ # injected by `townhouse hs up` as path.dirname(config.wallet.encrypted_path).
190
+ - ${TOWNHOUSE_WALLET_DIR:-~/.townhouse}:${TOWNHOUSE_WALLET_DIR:-~/.townhouse}:ro
126
191
  environment:
127
192
  # Override entrypoint default '/config/config.yaml' so the API reads from
128
193
  # the mounted operator-home dir (where Story 45.4 writes config.yaml).
129
194
  TOWNHOUSE_CONFIG: /.townhouse/config.yaml
195
+ # Bind on all interfaces inside the container so Docker's port mapping
196
+ # (127.0.0.1:28090:28090) can reach it from the host. ALLOW_REMOTE=1
197
+ # is required when HOST is non-loopback; Docker's host-only binding on
198
+ # the outer port (127.0.0.1) is the actual access control gate.
199
+ TOWNHOUSE_API_HOST: 0.0.0.0
200
+ TOWNHOUSE_API_ALLOW_REMOTE: '1'
130
201
  # Wallet decryption password — operator must export TOWNHOUSE_WALLET_PASSWORD
131
- # in the shell that runs `docker compose up`. Compose interpolates the value
132
- # at up time; the container exits immediately if it is unset (intended).
133
- TOWNHOUSE_WALLET_PASSWORD: '${TOWNHOUSE_WALLET_PASSWORD:?TOWNHOUSE_WALLET_PASSWORD must be exported before docker compose up}'
202
+ # in the shell that runs `docker compose up`. The container-side
203
+ # entrypoint (entrypoint-townhouse-api.ts) enforces the password
204
+ # requirement at startup and exits 1 if it is missing — so the YAML
205
+ # uses `:-` (lenient default) here instead of `:?` (Compose-time
206
+ # mandatory error). The `:?` variant would also force `docker compose
207
+ # down` to error out unless the operator pre-exports the password,
208
+ # which broke the teardown flow. Discovered by Story 46.4 live gate
209
+ # run (Finding J, 2026-05-11).
210
+ TOWNHOUSE_WALLET_PASSWORD: '${TOWNHOUSE_WALLET_PASSWORD:-}'
211
+ # Pass the host-side compose-interpolation values through to the
212
+ # townhouse-api container, because the API itself shells out to
213
+ # `docker compose -f /.townhouse/compose/townhouse-hs.yml up -d <type>`
214
+ # for each lazy peer-node provisioning request (Story 46.2). That
215
+ # nested compose re-parses the SAME YAML and needs the same env vars
216
+ # set — otherwise the townhouse-api volume specs interpolate to
217
+ # `:/.townhouse:rw` (empty source) and the inner `up` aborts with
218
+ # `invalid spec: :/.townhouse:rw: empty section between colons`.
219
+ # All four values are HOST paths/IDs (because Docker bind-mount
220
+ # sources are resolved by the daemon, which sees host paths).
221
+ # Discovered by Story 46.4 live gate run (Finding L, 2026-05-12).
222
+ TOWNHOUSE_HOME: '${TOWNHOUSE_HOME}'
223
+ TOWNHOUSE_WALLET_DIR: '${TOWNHOUSE_WALLET_DIR}'
224
+ TOWNHOUSE_UID: '${TOWNHOUSE_UID:-1000}'
225
+ TOWNHOUSE_DOCKER_GID: '${TOWNHOUSE_DOCKER_GID:-0}'
226
+ # MILL_RELAYS is read by POST /api/nodes {type:'mill'} preflight check
227
+ # inside the townhouse-api process. It must be in the API container's env
228
+ # (not only the mill container's env) so the check works correctly.
229
+ MILL_RELAYS: '${MILL_RELAYS:-}'
134
230
  healthcheck:
135
231
  # nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal probe
136
- test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:28090/api/health']
232
+ # Use 127.0.0.1 (not localhost) to avoid IPv6 resolution surprises. The
233
+ # townhouse-api exposes /api/transport (not /api/health).
234
+ test: ['CMD', 'wget', '-q', '--spider', 'http://127.0.0.1:28090/api/transport']
137
235
  interval: 10s
138
236
  timeout: 5s
139
237
  retries: 5
@@ -154,7 +252,7 @@ services:
154
252
  # start at first run).
155
253
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
156
254
  town:
157
- image: ghcr.io/toon-protocol/town@sha256:ba83fb6df536dec240f27fb7449db59e803207221c54e61ea019375ddc0461a0
255
+ image: ghcr.io/toon-protocol/town@sha256:cada4f94a28e6ddfa4c01b37b5aadc1a8fa374b5fd8e85478d4aa5be33a2d961
158
256
  container_name: townhouse-hs-town
159
257
  profiles: [town]
160
258
  networks:
@@ -179,7 +277,15 @@ services:
179
277
  NODE_EVM_ADDRESS: ''
180
278
  # Derived from HD wallet at runtime (Story 45.4 / Epic 46).
181
279
  NODE_NOSTR_SECRET_KEY: '${TOWN_SECRET_KEY:-}'
182
- SETTLEMENT_PRIVATE_KEY: '${TOWN_SETTLEMENT_PRIVATE_KEY:-}'
280
+ # Town reads TOON_SETTLEMENT_PRIVATE_KEY (packages/town/src/cli.ts:305),
281
+ # 0x-prefixed 32-byte hex. The API passes the already-0x-prefixed value
282
+ # via TOWN_SETTLEMENT_PRIVATE_KEY. Previously this was wired to the wrong
283
+ # env var name (`SETTLEMENT_PRIVATE_KEY`), so town crash-looped at boot
284
+ # with `TOON_SETTLEMENT_PRIVATE_KEY must be a 0x-prefixed 32-byte hex
285
+ # string` and never reached its /health endpoint — failing the gate
286
+ # at step 5 (healthcheck). Story 46.4 live gate run (Finding N+O,
287
+ # 2026-05-12).
288
+ TOON_SETTLEMENT_PRIVATE_KEY: '${TOWN_SETTLEMENT_PRIVATE_KEY:-}'
183
289
  PARENT_EVM_ADDRESS: '${APEX_EVM_ADDRESS:-}'
184
290
  TOON_CONNECTOR_LOG_LEVEL: '${TOON_CONNECTOR_LOG_LEVEL:-warn}'
185
291
  volumes:
@@ -197,7 +303,7 @@ services:
197
303
  # Lazy-provisioned via Epic 46: `townhouse node add mill`
198
304
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
199
305
  mill:
200
- image: ghcr.io/toon-protocol/mill@sha256:38e8c6460a3fc562dab28284b4f51958d8417ce10a7152bbb4224fc00cc42df5
306
+ image: ghcr.io/toon-protocol/mill@sha256:5269013677914ebc68d52c844add621c9a34e3219393280353227d074fa8787f
201
307
  container_name: townhouse-hs-mill
202
308
  profiles: [mill]
203
309
  networks:
@@ -229,7 +335,8 @@ services:
229
335
  TOON_CONNECTOR_LOG_LEVEL: '${TOON_CONNECTOR_LOG_LEVEL:-warn}'
230
336
  volumes:
231
337
  # Operator-managed mill config (Epic 46 provisions this on `townhouse node add mill`).
232
- - ~/.townhouse/mill.config.json:/config/mill.config.json:ro
338
+ # TOWNHOUSE_HOME is exported by `townhouse hs up` (see connector volume above).
339
+ - ${TOWNHOUSE_HOME}/mill.config.json:/config/mill.config.json:ro
233
340
  - townhouse-hs-mill-data:/data
234
341
  healthcheck:
235
342
  test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3200/health']
@@ -244,7 +351,7 @@ services:
244
351
  # Lazy-provisioned via Epic 46: `townhouse node add dvm`
245
352
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
246
353
  dvm:
247
- image: ghcr.io/toon-protocol/dvm@sha256:26a2aff4cb490d11f8eab9cff3272475c951e7cc87378a1af6c702b529d74e10
354
+ image: ghcr.io/toon-protocol/dvm@sha256:4d8e2cf5412c8bbfd9321c416356ba7b3c52ab8405db0df9f1b8f9d266c17cff
248
355
  container_name: townhouse-hs-dvm
249
356
  profiles: [dvm]
250
357
  networks:
@@ -1,7 +1,8 @@
1
1
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
2
  import {
3
3
  DEFAULT_CONNECTOR_IMAGE
4
- } from "./chunk-UTFWPLTB.js";
4
+ } from "./chunk-GQNBZJ6F.js";
5
+ import "./chunk-I2R4CRUX.js";
5
6
 
6
7
  // src/presets/demo.ts
7
8
  import { join } from "path";
@@ -114,4 +115,4 @@ export {
114
115
  defaultLeasesPath,
115
116
  resolveChainEndpoints
116
117
  };
117
- //# sourceMappingURL=demo-MJR47QHZ.js.map
118
+ //# sourceMappingURL=demo-3DWRDMYY.js.map
@@ -1 +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 = 'ator'\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 — ATOR 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 'ator' 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":[]}
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 = 'ator'\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 — ATOR 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 'ator' 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":[]}
@@ -1,32 +1,32 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "townhouseVersion": "0.1.0-rc5",
4
- "builtAt": "2026-05-10T00:04:39.745Z",
3
+ "townhouseVersion": "0.1.0",
4
+ "builtAt": "2026-06-01T20:34:54.854Z",
5
5
  "images": {
6
6
  "townhouse-api": {
7
7
  "name": "ghcr.io/toon-protocol/townhouse-api",
8
- "tag": "0.1.0-rc5",
9
- "digest": "sha256:48cb3df422019e252b7b1b5506b2fc666ea206ec57024eb68acbf781ebeb1f11"
8
+ "tag": "0.1.0",
9
+ "digest": "sha256:f2cf4725895a3fe49593831481d4641465341533786150edf78d337868352994"
10
10
  },
11
11
  "town": {
12
12
  "name": "ghcr.io/toon-protocol/town",
13
- "tag": "0.1.0-rc5",
14
- "digest": "sha256:ba83fb6df536dec240f27fb7449db59e803207221c54e61ea019375ddc0461a0"
13
+ "tag": "0.1.0",
14
+ "digest": "sha256:cada4f94a28e6ddfa4c01b37b5aadc1a8fa374b5fd8e85478d4aa5be33a2d961"
15
15
  },
16
16
  "mill": {
17
17
  "name": "ghcr.io/toon-protocol/mill",
18
- "tag": "0.1.0-rc5",
19
- "digest": "sha256:38e8c6460a3fc562dab28284b4f51958d8417ce10a7152bbb4224fc00cc42df5"
18
+ "tag": "0.1.0",
19
+ "digest": "sha256:5269013677914ebc68d52c844add621c9a34e3219393280353227d074fa8787f"
20
20
  },
21
21
  "dvm": {
22
22
  "name": "ghcr.io/toon-protocol/dvm",
23
- "tag": "0.1.0-rc5",
24
- "digest": "sha256:26a2aff4cb490d11f8eab9cff3272475c951e7cc87378a1af6c702b529d74e10"
23
+ "tag": "0.1.0",
24
+ "digest": "sha256:4d8e2cf5412c8bbfd9321c416356ba7b3c52ab8405db0df9f1b8f9d266c17cff"
25
25
  },
26
26
  "connector": {
27
27
  "name": "ghcr.io/toon-protocol/connector",
28
- "tag": "3.4.1",
29
- "digest": "sha256:4a24ccb0997d7b025997e670546032f6a84cd18a77c490509016b85e181a344e"
28
+ "tag": "3.8.0",
29
+ "digest": "sha256:3343c19649290043e521c81b467b7c6410b8eaedd76d48804ea9b6fc810cddb0"
30
30
  }
31
31
  }
32
32
  }