@toon-protocol/townhouse 0.1.0-rc5

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,276 @@
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
+ # @sha256:48cb3df422019e252b7b1b5506b2fc666ea206ec57024eb68acbf781ebeb1f11 → @sha256:<hex>
26
+ # @sha256:ba83fb6df536dec240f27fb7449db59e803207221c54e61ea019375ddc0461a0 → @sha256:<hex>
27
+ # @sha256:38e8c6460a3fc562dab28284b4f51958d8417ce10a7152bbb4224fc00cc42df5 → @sha256:<hex>
28
+ # @sha256:26a2aff4cb490d11f8eab9cff3272475c951e7cc87378a1af6c702b529d74e10 → @sha256:<hex>
29
+ # @sha256:4a24ccb0997d7b025997e670546032f6a84cd18a77c490509016b85e181a344e → @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
+ driver: bridge
54
+
55
+ volumes:
56
+ # Named volume for the connector's .anyone keypair + HS state.
57
+ # Survives `docker compose down`; delete to rotate the .anyone address.
58
+ townhouse-hs-anon:
59
+ townhouse-hs-town-data:
60
+ townhouse-hs-mill-data:
61
+ townhouse-hs-dvm-data:
62
+
63
+ services:
64
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65
+ # Apex connector — terminates inbound BTP, publishes .anyone HS
66
+ #
67
+ # The connector image embeds @anyone-protocol/anyone-client v1.1.x+
68
+ # which handles HS publishing in-process (no sidecar required for v3.5.x+).
69
+ # Config file written by `townhouse hs up` (Story 45.4) on first-run.
70
+ #
71
+ # NFR7: connector MUST NOT mount /var/run/docker.sock.
72
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
+ connector:
74
+ image: ghcr.io/toon-protocol/connector@sha256:4a24ccb0997d7b025997e670546032f6a84cd18a77c490509016b85e181a344e
75
+ container_name: townhouse-hs-connector
76
+ hostname: connector
77
+ networks:
78
+ - townhouse-hs-net
79
+ ports:
80
+ # Admin API on host loopback only (NFR9). Operator uses for status.
81
+ - '127.0.0.1:9401:9401'
82
+ volumes:
83
+ # Rendered connector config (Story 45.4 writes this on first-run).
84
+ - ~/.townhouse/connector.yaml:/config/connector.yaml:ro
85
+ # .anyone keypair + HS state (persists across down/up cycles).
86
+ - townhouse-hs-anon:/var/lib/anon/hs
87
+ environment:
88
+ CONFIG_FILE: /config/connector.yaml
89
+ healthcheck:
90
+ # nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal probe
91
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:9401/health']
92
+ interval: 10s
93
+ timeout: 5s
94
+ retries: 5
95
+ start_period: 30s
96
+ restart: unless-stopped
97
+
98
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99
+ # Townhouse API — containerized host API (NEW in HS-mode v1)
100
+ #
101
+ # Owns /var/run/docker.sock (the only service that may). Provides:
102
+ # - Fastify REST API for operator dashboard / CLI
103
+ # - Calls connector admin API (/admin/hs-hostname, /admin/peers, etc.)
104
+ # - Manages lifecycle for lazy-provisioned peer containers (Epic 46)
105
+ #
106
+ # Planning doc §4 anchor: "host-side townhouse-api owns dockerode and
107
+ # runs on the townhouse-hs-net so it can reach connector via Docker DNS".
108
+ # Port D21-008: Fastify host API on 127.0.0.1:28090.
109
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
110
+ townhouse-api:
111
+ image: ghcr.io/toon-protocol/townhouse-api@sha256:48cb3df422019e252b7b1b5506b2fc666ea206ec57024eb68acbf781ebeb1f11
112
+ container_name: townhouse-hs-api
113
+ networks:
114
+ - townhouse-hs-net
115
+ depends_on:
116
+ connector:
117
+ condition: service_healthy
118
+ ports:
119
+ # Fastify host API — loopback only (NFR9).
120
+ - '127.0.0.1:28090:28090'
121
+ volumes:
122
+ # Docker socket — townhouse-api is the sole orchestration surface.
123
+ - /var/run/docker.sock:/var/run/docker.sock
124
+ # Operator home — wallet, config, compose files, snapshots (RW).
125
+ - ~/.townhouse:/.townhouse:rw
126
+ environment:
127
+ # Override entrypoint default '/config/config.yaml' so the API reads from
128
+ # the mounted operator-home dir (where Story 45.4 writes config.yaml).
129
+ TOWNHOUSE_CONFIG: /.townhouse/config.yaml
130
+ # 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}'
134
+ healthcheck:
135
+ # nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url -- container-internal probe
136
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:28090/api/health']
137
+ interval: 10s
138
+ timeout: 5s
139
+ retries: 5
140
+ start_period: 15s
141
+ restart: unless-stopped
142
+
143
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
144
+ # Town — Nostr relay node (profile: town)
145
+ # Lazy-provisioned via Epic 46: `townhouse node add town`
146
+ #
147
+ # SECURITY TODO (Epic 46): the per-node secrets below use `${VAR:-}` empty
148
+ # defaults, which silently start the container with a zero/empty key when
149
+ # the operator forgets to export the env var. Story 45.2 review (R2-MINOR)
150
+ # recommends switching to `${VAR:?msg}` (fail-fast) once Epic 46 boots
151
+ # these profiles — keeping the empty default for now lets `docker compose
152
+ # config` validation pass without injecting per-node secrets, matching
153
+ # Story 45.4's apex-only boot semantic (only connector + townhouse-api
154
+ # start at first run).
155
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
156
+ town:
157
+ image: ghcr.io/toon-protocol/town@sha256:ba83fb6df536dec240f27fb7449db59e803207221c54e61ea019375ddc0461a0
158
+ container_name: townhouse-hs-town
159
+ profiles: [town]
160
+ networks:
161
+ - townhouse-hs-net
162
+ depends_on:
163
+ connector:
164
+ condition: service_healthy
165
+ expose: ['3000']
166
+ ports:
167
+ - '127.0.0.1:7100:7100'
168
+ - '127.0.0.1:3100:3100'
169
+ environment:
170
+ # nosemgrep: detect-insecure-websocket -- Docker-internal
171
+ CONNECTOR_URL: ws://connector:3000
172
+ ILP_ADDRESS: g.townhouse.town
173
+ NODE_ID: town
174
+ PARENT_PEER_ID: apex
175
+ FEE_PER_EVENT: '0'
176
+ # Chain RPC — operator sets EVM_RPC_URL in ~/.townhouse/env or via CLI.
177
+ TOON_RPC_URL: ${EVM_RPC_URL:-}
178
+ NODE_NOSTR_PUBKEY: ''
179
+ NODE_EVM_ADDRESS: ''
180
+ # Derived from HD wallet at runtime (Story 45.4 / Epic 46).
181
+ NODE_NOSTR_SECRET_KEY: '${TOWN_SECRET_KEY:-}'
182
+ SETTLEMENT_PRIVATE_KEY: '${TOWN_SETTLEMENT_PRIVATE_KEY:-}'
183
+ PARENT_EVM_ADDRESS: '${APEX_EVM_ADDRESS:-}'
184
+ TOON_CONNECTOR_LOG_LEVEL: '${TOON_CONNECTOR_LOG_LEVEL:-warn}'
185
+ volumes:
186
+ - townhouse-hs-town-data:/data
187
+ healthcheck:
188
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3100/health']
189
+ interval: 30s
190
+ timeout: 10s
191
+ retries: 3
192
+ start_period: 10s
193
+ restart: unless-stopped
194
+
195
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
196
+ # Mill — multi-chain swap node (profile: mill)
197
+ # Lazy-provisioned via Epic 46: `townhouse node add mill`
198
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
199
+ mill:
200
+ image: ghcr.io/toon-protocol/mill@sha256:38e8c6460a3fc562dab28284b4f51958d8417ce10a7152bbb4224fc00cc42df5
201
+ container_name: townhouse-hs-mill
202
+ profiles: [mill]
203
+ networks:
204
+ - townhouse-hs-net
205
+ depends_on:
206
+ connector:
207
+ condition: service_healthy
208
+ expose: ['3000']
209
+ ports:
210
+ - '127.0.0.1:3200:3200'
211
+ environment:
212
+ # nosemgrep: detect-insecure-websocket -- Docker-internal
213
+ CONNECTOR_URL: ws://connector:3000
214
+ FEE_BASIS_POINTS: '0'
215
+ SETTLEMENT_RPC_URL: ${EVM_RPC_URL:-}
216
+ SETTLEMENT_CHAIN_ID: ${EVM_CHAIN_ID:-}
217
+ SETTLEMENT_TOKEN_ADDRESS: ${EVM_USDC_ADDRESS:-}
218
+ SOLANA_RPC_URL: ${SOLANA_RPC_URL:-}
219
+ SOLANA_USDC_MINT: ${SOLANA_USDC_MINT:-}
220
+ NODE_NOSTR_PUBKEY: ''
221
+ NODE_EVM_ADDRESS: ''
222
+ # Derived from HD wallet at runtime (Story 45.4 / Epic 46).
223
+ MILL_MNEMONIC: '${MILL_MNEMONIC:-}'
224
+ NODE_NOSTR_SECRET_KEY: '${MILL_SECRET_KEY:-}'
225
+ MILL_CONFIG_PATH: /config/mill.config.json
226
+ MILL_RELAYS: ${MILL_RELAYS:-}
227
+ SETTLEMENT_PRIVATE_KEY: '${MILL_SETTLEMENT_PRIVATE_KEY:-}'
228
+ PARENT_EVM_ADDRESS: '${APEX_EVM_ADDRESS:-}'
229
+ TOON_CONNECTOR_LOG_LEVEL: '${TOON_CONNECTOR_LOG_LEVEL:-warn}'
230
+ volumes:
231
+ # Operator-managed mill config (Epic 46 provisions this on `townhouse node add mill`).
232
+ - ~/.townhouse/mill.config.json:/config/mill.config.json:ro
233
+ - townhouse-hs-mill-data:/data
234
+ healthcheck:
235
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3200/health']
236
+ interval: 30s
237
+ timeout: 10s
238
+ retries: 3
239
+ start_period: 15s
240
+ restart: unless-stopped
241
+
242
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243
+ # DVM — Arweave upload DVM (profile: dvm)
244
+ # Lazy-provisioned via Epic 46: `townhouse node add dvm`
245
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
246
+ dvm:
247
+ image: ghcr.io/toon-protocol/dvm@sha256:26a2aff4cb490d11f8eab9cff3272475c951e7cc87378a1af6c702b529d74e10
248
+ container_name: townhouse-hs-dvm
249
+ profiles: [dvm]
250
+ networks:
251
+ - townhouse-hs-net
252
+ depends_on:
253
+ connector:
254
+ condition: service_healthy
255
+ expose: ['3300']
256
+ ports:
257
+ - '127.0.0.1:3400:3400'
258
+ environment:
259
+ # nosemgrep: detect-insecure-websocket -- Docker-internal
260
+ CONNECTOR_URL: ws://connector:3000
261
+ FEE_PER_JOB: '0'
262
+ DVM_KIND: '5094'
263
+ NODE_NOSTR_PUBKEY: ''
264
+ NODE_EVM_ADDRESS: ''
265
+ # Derived from HD wallet at runtime (Story 45.4 / Epic 46).
266
+ NODE_NOSTR_SECRET_KEY: '${DVM_SECRET_KEY:-}'
267
+ TURBO_TOKEN: ${TURBO_TOKEN:-}
268
+ volumes:
269
+ - townhouse-hs-dvm-data:/data
270
+ healthcheck:
271
+ test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3400/health']
272
+ interval: 30s
273
+ timeout: 10s
274
+ retries: 3
275
+ start_period: 10s
276
+ restart: unless-stopped
@@ -0,0 +1,117 @@
1
+ import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
+ import {
3
+ DEFAULT_CONNECTOR_IMAGE
4
+ } from "./chunk-UTFWPLTB.js";
5
+
6
+ // src/presets/demo.ts
7
+ import { join } from "path";
8
+ import { homedir } from "os";
9
+ import { readFileSync, existsSync } from "fs";
10
+ import { resolve } from "path";
11
+ var LOCAL_DEVNET_FALLBACK = {
12
+ anvilUrl: "http://localhost:28545",
13
+ solanaUrl: "http://localhost:28899"
14
+ };
15
+ var DEMO_DETERMINISTIC_PASSWORD = "townhouse-demo-INSECURE-do-not-use-in-prod";
16
+ function resolveChainEndpoints(leasesPath) {
17
+ const localEvm = { rpcUrl: LOCAL_DEVNET_FALLBACK.anvilUrl };
18
+ const localSol = { rpcUrl: LOCAL_DEVNET_FALLBACK.solanaUrl };
19
+ if (!leasesPath || !existsSync(leasesPath)) {
20
+ return { source: "local-fallback", evm: localEvm, solana: localSol };
21
+ }
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(readFileSync(leasesPath, "utf-8"));
25
+ } catch {
26
+ return { source: "local-fallback", evm: localEvm, solana: localSol };
27
+ }
28
+ const evmUrl = typeof parsed.anvil?.url === "string" ? parsed.anvil.url : void 0;
29
+ const evmWs = typeof parsed.anvil?.ws_url === "string" ? parsed.anvil.ws_url : void 0;
30
+ const solUrl = typeof parsed.solana?.url === "string" ? parsed.solana.url : void 0;
31
+ const solWs = typeof parsed.solana?.ws_url === "string" ? parsed.solana.ws_url : void 0;
32
+ return {
33
+ source: leasesPath,
34
+ evm: evmUrl ? { rpcUrl: evmUrl, ...evmWs ? { wsUrl: evmWs } : {} } : localEvm,
35
+ solana: solUrl ? { rpcUrl: solUrl, ...solWs ? { wsUrl: solWs } : {} } : localSol
36
+ };
37
+ }
38
+ function defaultLeasesPath() {
39
+ return resolve(process.cwd(), "deploy", "akash", "leases.json");
40
+ }
41
+ function buildDemoConfig(options) {
42
+ const leasesPath = options.leasesPath === null ? void 0 : options.leasesPath ?? defaultLeasesPath();
43
+ const endpoints = resolveChainEndpoints(leasesPath);
44
+ return {
45
+ nodes: {
46
+ town: {
47
+ enabled: true,
48
+ feePerEvent: 0
49
+ },
50
+ mill: {
51
+ enabled: true,
52
+ feeBasisPoints: 0,
53
+ // Demo pair: EVM (Anvil) <-> Solana. The orchestrator does not
54
+ // currently consume mill.chains directly — it round-trips through
55
+ // YAML so the dashboard / future stories can read it.
56
+ chains: {
57
+ evm: {
58
+ rpcUrl: endpoints.evm.rpcUrl,
59
+ ...endpoints.evm.wsUrl ? { wsUrl: endpoints.evm.wsUrl } : {}
60
+ },
61
+ solana: {
62
+ rpcUrl: endpoints.solana.rpcUrl,
63
+ ...endpoints.solana.wsUrl ? { wsUrl: endpoints.solana.wsUrl } : {}
64
+ }
65
+ },
66
+ pairs: ["EVM<->SOL"]
67
+ },
68
+ dvm: {
69
+ enabled: true,
70
+ feePerJob: 0,
71
+ // Arweave DVM (kind:5094) — frictionless demo pricing. Operators
72
+ // running for real should raise this; entrypoint-dvm.ts treats the
73
+ // value as msats per byte uploaded to Arweave.
74
+ kindPricing: { "5094": 0 }
75
+ }
76
+ },
77
+ wallet: {
78
+ encrypted_path: options.walletPath
79
+ },
80
+ connector: {
81
+ image: DEFAULT_CONNECTOR_IMAGE,
82
+ adminPort: 9401
83
+ },
84
+ transport: {
85
+ // 'direct' for the demo because `townhouse up` doesn't bring up a
86
+ // SOCKS5 sidecar — ATOR mode would require one at `socks5://127.0.0.1:28050`
87
+ // (provided by the dev-infra stack but not the operator CLI). Switch
88
+ // to 'ator' once the sidecar story lands in townhouse `up`.
89
+ mode: "direct"
90
+ },
91
+ api: {
92
+ port: 9400,
93
+ host: "127.0.0.1"
94
+ },
95
+ logging: {
96
+ level: "info"
97
+ },
98
+ preset: {
99
+ name: "demo",
100
+ // Source recorded so operators can see at-a-glance whether their demo
101
+ // is hitting Akash or local devnets.
102
+ chainEndpointSource: endpoints.source
103
+ }
104
+ };
105
+ }
106
+ function defaultDemoConfigDir() {
107
+ return join(homedir(), ".townhouse");
108
+ }
109
+ export {
110
+ DEMO_DETERMINISTIC_PASSWORD,
111
+ LOCAL_DEVNET_FALLBACK,
112
+ buildDemoConfig,
113
+ defaultDemoConfigDir,
114
+ defaultLeasesPath,
115
+ resolveChainEndpoints
116
+ };
117
+ //# sourceMappingURL=demo-MJR47QHZ.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 = '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":[]}
@@ -0,0 +1,32 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "townhouseVersion": "0.1.0-rc5",
4
+ "builtAt": "2026-05-10T00:04:39.745Z",
5
+ "images": {
6
+ "townhouse-api": {
7
+ "name": "ghcr.io/toon-protocol/townhouse-api",
8
+ "tag": "0.1.0-rc5",
9
+ "digest": "sha256:48cb3df422019e252b7b1b5506b2fc666ea206ec57024eb68acbf781ebeb1f11"
10
+ },
11
+ "town": {
12
+ "name": "ghcr.io/toon-protocol/town",
13
+ "tag": "0.1.0-rc5",
14
+ "digest": "sha256:ba83fb6df536dec240f27fb7449db59e803207221c54e61ea019375ddc0461a0"
15
+ },
16
+ "mill": {
17
+ "name": "ghcr.io/toon-protocol/mill",
18
+ "tag": "0.1.0-rc5",
19
+ "digest": "sha256:38e8c6460a3fc562dab28284b4f51958d8417ce10a7152bbb4224fc00cc42df5"
20
+ },
21
+ "dvm": {
22
+ "name": "ghcr.io/toon-protocol/dvm",
23
+ "tag": "0.1.0-rc5",
24
+ "digest": "sha256:26a2aff4cb490d11f8eab9cff3272475c951e7cc87378a1af6c702b529d74e10"
25
+ },
26
+ "connector": {
27
+ "name": "ghcr.io/toon-protocol/connector",
28
+ "tag": "3.4.1",
29
+ "digest": "sha256:4a24ccb0997d7b025997e670546032f6a84cd18a77c490509016b85e181a344e"
30
+ }
31
+ }
32
+ }