@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.
- package/LICENSE +190 -0
- package/README.md +386 -0
- package/dist/chunk-IB6TNCUQ.js +8274 -0
- package/dist/chunk-IB6TNCUQ.js.map +1 -0
- package/dist/chunk-UTFWPLTB.js +59 -0
- package/dist/chunk-UTFWPLTB.js.map +1 -0
- package/dist/cli.d.ts +38 -0
- package/dist/cli.js +684 -0
- package/dist/cli.js.map +1 -0
- package/dist/compose/townhouse-dev.yml +406 -0
- package/dist/compose/townhouse-hs.yml +276 -0
- package/dist/demo-MJR47QHZ.js +117 -0
- package/dist/demo-MJR47QHZ.js.map +1 -0
- package/dist/image-manifest.json +32 -0
- package/dist/index.d.ts +1410 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import Docker from 'dockerode';
|
|
3
|
+
import { FastifyBaseLogger, FastifyInstance } from 'fastify';
|
|
4
|
+
import { MillHealthResponse } from '@toon-protocol/mill';
|
|
5
|
+
export { MillHealthResponse } from '@toon-protocol/mill';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Townhouse configuration schema — TypeScript interfaces only.
|
|
9
|
+
* Runtime validation lives in validator.ts.
|
|
10
|
+
*/
|
|
11
|
+
interface TownNodeConfig {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Nostr relay fee in millisatoshis per event */
|
|
14
|
+
feePerEvent?: number;
|
|
15
|
+
/** Docker image override */
|
|
16
|
+
image?: string;
|
|
17
|
+
}
|
|
18
|
+
interface ChainEndpoint {
|
|
19
|
+
rpcUrl: string;
|
|
20
|
+
wsUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
interface MillChainsConfig {
|
|
23
|
+
evm?: ChainEndpoint;
|
|
24
|
+
solana?: ChainEndpoint;
|
|
25
|
+
mina?: ChainEndpoint;
|
|
26
|
+
}
|
|
27
|
+
interface MillNodeConfig {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
/** Swap fee basis points (1 = 0.01%) */
|
|
30
|
+
feeBasisPoints?: number;
|
|
31
|
+
/** Docker image override */
|
|
32
|
+
image?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Chain RPC endpoints the mill should swap against (D2). The orchestrator
|
|
35
|
+
* does not currently forward this directly into MILL_CONFIG_JSON — it
|
|
36
|
+
* round-trips through YAML so the dashboard and future stories can read it.
|
|
37
|
+
*/
|
|
38
|
+
chains?: MillChainsConfig;
|
|
39
|
+
/** Enabled swap pairs, e.g. ['EVM<->SOL']. Informational; D2-introduced. */
|
|
40
|
+
pairs?: string[];
|
|
41
|
+
}
|
|
42
|
+
interface DvmNodeConfig {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
/** DVM job fee in millisatoshis */
|
|
45
|
+
feePerJob?: number;
|
|
46
|
+
/** Per-kind pricing in millisatoshis (key = stringified kind number) */
|
|
47
|
+
kindPricing?: Record<string, number>;
|
|
48
|
+
/** Docker image override */
|
|
49
|
+
image?: string;
|
|
50
|
+
}
|
|
51
|
+
interface NodesConfig {
|
|
52
|
+
town: TownNodeConfig;
|
|
53
|
+
mill: MillNodeConfig;
|
|
54
|
+
dvm: DvmNodeConfig;
|
|
55
|
+
}
|
|
56
|
+
interface WalletConfig {
|
|
57
|
+
/** Path to encrypted wallet file (no plaintext mnemonic in config) */
|
|
58
|
+
encrypted_path: string;
|
|
59
|
+
}
|
|
60
|
+
interface ConnectorConfig {
|
|
61
|
+
/** Docker image for the shared ILP connector */
|
|
62
|
+
image: string;
|
|
63
|
+
/** Admin API port */
|
|
64
|
+
adminPort: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Hidden-service publication config (Story 35.5 of the connector repo).
|
|
68
|
+
*
|
|
69
|
+
* When set, the connector boots `@anyone-protocol/anyone-client` in-process,
|
|
70
|
+
* spawns the `anon` binary, publishes a v3 hidden service, and advertises
|
|
71
|
+
* its `wss://<addr>.anyone/btp` URL to peers. The keypair lives at `dir`
|
|
72
|
+
* inside the connector container and persists across restarts when that
|
|
73
|
+
* path is on a mounted volume.
|
|
74
|
+
*
|
|
75
|
+
* Operator surface (this type) is intentionally narrow; the connector's
|
|
76
|
+
* own config has more knobs (binaryPath, configFilePath) that we omit
|
|
77
|
+
* here until a real use case demands them.
|
|
78
|
+
*/
|
|
79
|
+
interface HiddenServiceConfig$1 {
|
|
80
|
+
/** Path inside the connector container for hs_ed25519_secret_key etc. */
|
|
81
|
+
dir: string;
|
|
82
|
+
/** Hidden service port — peers dial <addr>.anyone:<port>. */
|
|
83
|
+
port: number;
|
|
84
|
+
/**
|
|
85
|
+
* Optional override of the externalUrl the connector advertises. Default
|
|
86
|
+
* is `"auto"` — the connector reads `${dir}/hostname` at startup and
|
|
87
|
+
* builds `wss://<hostname>.anyone/btp` itself.
|
|
88
|
+
*/
|
|
89
|
+
externalUrl?: string;
|
|
90
|
+
/** Optional override of the SDK's start-up readiness deadline (ms). */
|
|
91
|
+
startupTimeoutMs?: number;
|
|
92
|
+
/** Optional override of the SDK's shutdown deadline (ms). */
|
|
93
|
+
stopTimeoutMs?: number;
|
|
94
|
+
}
|
|
95
|
+
interface TransportConfig {
|
|
96
|
+
/** Transport mode: 'ator' for Tor-based, 'direct' for clearnet */
|
|
97
|
+
mode: 'ator' | 'direct';
|
|
98
|
+
/** SOCKS5 proxy address when using ator transport */
|
|
99
|
+
socksProxy?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Externally reachable BTP URL. Required when mode='ator' AND
|
|
102
|
+
* hiddenService is unset (operator runs their own anon binary external
|
|
103
|
+
* to the connector and is responsible for the URL). Ignored for
|
|
104
|
+
* mode='direct'. When hiddenService is set and externalUrl is unset,
|
|
105
|
+
* the generator emits the literal `"auto"` so the connector resolves
|
|
106
|
+
* the .anyone hostname from disk at startup.
|
|
107
|
+
*/
|
|
108
|
+
externalUrl?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Optional inbound hidden-service publication. When set, the connector
|
|
111
|
+
* manages its own anon binary and publishes a .anyone hidden service.
|
|
112
|
+
*/
|
|
113
|
+
hiddenService?: HiddenServiceConfig$1;
|
|
114
|
+
/**
|
|
115
|
+
* Town relay hidden service. When set, the orchestrator starts a second
|
|
116
|
+
* ator sidecar (parallel to any connector HS sidecar) that forwards
|
|
117
|
+
* inbound HS traffic to the town container's Nostr WebSocket port (7100),
|
|
118
|
+
* and the town container is configured to advertise the .anyone URL.
|
|
119
|
+
* Reuses HiddenServiceConfig — same shape as connector HS config.
|
|
120
|
+
*/
|
|
121
|
+
relayHiddenService?: HiddenServiceConfig$1;
|
|
122
|
+
}
|
|
123
|
+
interface ApiConfig {
|
|
124
|
+
/** Dashboard/API port */
|
|
125
|
+
port: number;
|
|
126
|
+
/** Bind address */
|
|
127
|
+
host: string;
|
|
128
|
+
}
|
|
129
|
+
interface LoggingConfig {
|
|
130
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
131
|
+
}
|
|
132
|
+
interface PresetMetadata {
|
|
133
|
+
/** Preset that produced this config (D2). */
|
|
134
|
+
name: 'demo';
|
|
135
|
+
/** Where the chain endpoints came from — leases.json path or 'local-fallback'. */
|
|
136
|
+
chainEndpointSource: string;
|
|
137
|
+
}
|
|
138
|
+
interface TownhouseConfig {
|
|
139
|
+
nodes: NodesConfig;
|
|
140
|
+
wallet: WalletConfig;
|
|
141
|
+
connector: ConnectorConfig;
|
|
142
|
+
transport: TransportConfig;
|
|
143
|
+
api: ApiConfig;
|
|
144
|
+
logging: LoggingConfig;
|
|
145
|
+
/** Present only when the config was generated by `init --preset=<name>`. */
|
|
146
|
+
preset?: PresetMetadata;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sensible default configuration. All nodes disabled by default —
|
|
151
|
+
* operator must explicitly enable what they want to run.
|
|
152
|
+
*/
|
|
153
|
+
declare function getDefaultConfig(): TownhouseConfig;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Config file loader — reads YAML, validates, writes.
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Load and validate a Townhouse config from a YAML file.
|
|
161
|
+
* Environment variables override YAML values for key settings:
|
|
162
|
+
* - TOWNHOUSE_API_PORT
|
|
163
|
+
* - TOWNHOUSE_TRANSPORT_MODE
|
|
164
|
+
* - TOWNHOUSE_LOG_LEVEL
|
|
165
|
+
*/
|
|
166
|
+
declare function loadConfig(configPath: string): TownhouseConfig;
|
|
167
|
+
/**
|
|
168
|
+
* Save a config to a YAML file atomically.
|
|
169
|
+
* Writes to a temp file first, then renames to the target (atomic on POSIX).
|
|
170
|
+
*/
|
|
171
|
+
declare function saveConfig(configPath: string, config: TownhouseConfig): void;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Runtime validation for Townhouse configuration.
|
|
175
|
+
* Validates shape, narrows types, returns typed config or throws.
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
declare class ConfigValidationError extends Error {
|
|
179
|
+
constructor(message: string);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Validate raw input and return a typed TownhouseConfig.
|
|
183
|
+
* Throws ConfigValidationError with descriptive messages on invalid input.
|
|
184
|
+
*/
|
|
185
|
+
declare function validateConfig(raw: unknown): TownhouseConfig;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Docker orchestration types for Townhouse (Story 21.2).
|
|
189
|
+
*
|
|
190
|
+
* STUB FILE: Created by ATDD workflow (red phase).
|
|
191
|
+
* Implementation will be added during the green phase.
|
|
192
|
+
*/
|
|
193
|
+
/** Node types that can be orchestrated by Townhouse. */
|
|
194
|
+
type NodeType$1 = 'town' | 'mill' | 'dvm';
|
|
195
|
+
/** Specification for a container to be created by the orchestrator. */
|
|
196
|
+
interface ContainerSpec {
|
|
197
|
+
/** Container name (e.g., 'townhouse-town') */
|
|
198
|
+
name: string;
|
|
199
|
+
/** Docker image to use */
|
|
200
|
+
image: string;
|
|
201
|
+
/** Environment variables to pass to the container */
|
|
202
|
+
env: Record<string, string>;
|
|
203
|
+
/** Network to attach the container to */
|
|
204
|
+
network: string;
|
|
205
|
+
/** Port mappings (host:container) */
|
|
206
|
+
ports?: Record<string, string>;
|
|
207
|
+
}
|
|
208
|
+
/** Events emitted by the orchestrator during operations. */
|
|
209
|
+
interface OrchestratorEvents {
|
|
210
|
+
/** Emitted during image pull with progress info */
|
|
211
|
+
pullProgress: {
|
|
212
|
+
image: string;
|
|
213
|
+
status: string;
|
|
214
|
+
id?: string;
|
|
215
|
+
progress?: string;
|
|
216
|
+
};
|
|
217
|
+
/** Emitted when a container changes state */
|
|
218
|
+
containerState: {
|
|
219
|
+
name: string;
|
|
220
|
+
state: 'creating' | 'starting' | 'running' | 'stopping' | 'stopped' | 'error';
|
|
221
|
+
/** Additional context for error states (e.g., error message) */
|
|
222
|
+
detail?: string;
|
|
223
|
+
};
|
|
224
|
+
/** Emitted during health check polling */
|
|
225
|
+
healthCheck: {
|
|
226
|
+
name: string;
|
|
227
|
+
status: string;
|
|
228
|
+
attempt: number;
|
|
229
|
+
};
|
|
230
|
+
/** Emitted before connector restart during peer registration update */
|
|
231
|
+
connectorRestarting: {
|
|
232
|
+
reason: string;
|
|
233
|
+
};
|
|
234
|
+
/** Emitted after connector restart and health check passes */
|
|
235
|
+
connectorRestarted: {
|
|
236
|
+
peers: string[];
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Options for health check polling. */
|
|
240
|
+
interface HealthCheckOptions {
|
|
241
|
+
/** Polling interval in milliseconds (default: 2000) */
|
|
242
|
+
interval?: number;
|
|
243
|
+
/** Timeout in milliseconds (default: 60000) */
|
|
244
|
+
timeout?: number;
|
|
245
|
+
}
|
|
246
|
+
/** Network I/O stats for a container (from dockerode stats stream) */
|
|
247
|
+
interface BandwidthStats {
|
|
248
|
+
bytesIn: number;
|
|
249
|
+
bytesOut: number;
|
|
250
|
+
sampleAt: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Wallet management types for Townhouse (Story 21.4).
|
|
255
|
+
*
|
|
256
|
+
* Defines interfaces for HD wallet key derivation, encryption at rest,
|
|
257
|
+
* and node key management.
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
/** Configuration for the WalletManager */
|
|
261
|
+
interface WalletManagerConfig {
|
|
262
|
+
/** Path to encrypted wallet file */
|
|
263
|
+
encryptedPath: string;
|
|
264
|
+
}
|
|
265
|
+
/** Keys derived for a specific node type */
|
|
266
|
+
interface NodeKeys {
|
|
267
|
+
/** Nostr public key (hex-encoded, 32 bytes) */
|
|
268
|
+
nostrPubkey: string;
|
|
269
|
+
/** Nostr secret key (raw 32-byte scalar) */
|
|
270
|
+
nostrSecretKey: Uint8Array;
|
|
271
|
+
/** EVM address (checksummed, 0x-prefixed) */
|
|
272
|
+
evmAddress: string;
|
|
273
|
+
/** EVM private key (raw 32 bytes) */
|
|
274
|
+
evmPrivateKey: Uint8Array;
|
|
275
|
+
/** BIP-44 derivation path used for Nostr key */
|
|
276
|
+
nostrDerivationPath: string;
|
|
277
|
+
/** BIP-44 derivation path used for EVM key */
|
|
278
|
+
evmDerivationPath: string;
|
|
279
|
+
/** Base58-encoded Solana public key — mill only, omitted for town/dvm */
|
|
280
|
+
solanaAddress?: string;
|
|
281
|
+
/** Mina public key hex — mill only, omitted for town/dvm */
|
|
282
|
+
minaAddress?: string;
|
|
283
|
+
}
|
|
284
|
+
/** Map of node type to its derived keys */
|
|
285
|
+
interface DerivedNodeKeys {
|
|
286
|
+
town: NodeKeys;
|
|
287
|
+
mill: NodeKeys;
|
|
288
|
+
dvm: NodeKeys;
|
|
289
|
+
}
|
|
290
|
+
/** Summary info for display (no secrets) */
|
|
291
|
+
interface NodeKeyInfo {
|
|
292
|
+
/** Node type */
|
|
293
|
+
nodeType: NodeType$1;
|
|
294
|
+
/** Nostr public key (hex) */
|
|
295
|
+
nostrPubkey: string;
|
|
296
|
+
/** EVM address (checksummed) */
|
|
297
|
+
evmAddress: string;
|
|
298
|
+
/** Nostr derivation path */
|
|
299
|
+
nostrDerivationPath: string;
|
|
300
|
+
/** EVM derivation path */
|
|
301
|
+
evmDerivationPath: string;
|
|
302
|
+
/** Base58-encoded Solana public key — mill only, omitted for town/dvm */
|
|
303
|
+
solanaAddress?: string;
|
|
304
|
+
/** Mina public key hex — mill only, omitted for town/dvm */
|
|
305
|
+
minaAddress?: string;
|
|
306
|
+
}
|
|
307
|
+
/** Persisted wallet state (in memory after decryption) */
|
|
308
|
+
interface WalletState {
|
|
309
|
+
/** All derived node keys */
|
|
310
|
+
keys: DerivedNodeKeys;
|
|
311
|
+
}
|
|
312
|
+
/** Encrypted wallet file format (JSON, all fields base64-encoded) */
|
|
313
|
+
interface EncryptedWallet {
|
|
314
|
+
/** scrypt salt (base64) */
|
|
315
|
+
salt: string;
|
|
316
|
+
/** AES-GCM initialization vector (base64) */
|
|
317
|
+
iv: string;
|
|
318
|
+
/** AES-256-GCM ciphertext (base64) */
|
|
319
|
+
ciphertext: string;
|
|
320
|
+
/** AES-GCM authentication tag (base64) */
|
|
321
|
+
tag: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* WalletManager — HD key derivation for Townhouse (Story 21.4, Task 1).
|
|
326
|
+
*
|
|
327
|
+
* Single BIP-39 mnemonic, deterministic HD derivation per node type.
|
|
328
|
+
* Uses BIP-44 paths with distinct account indices per node type:
|
|
329
|
+
* - Town: account 0
|
|
330
|
+
* - Mill: account 1
|
|
331
|
+
* - DVM: account 2
|
|
332
|
+
*
|
|
333
|
+
* Nostr keys use NIP-06 coin type 1237: m/44'/1237'/{account}'/0/0
|
|
334
|
+
* EVM keys use standard coin type 60: m/44'/60'/{account}'/0/0
|
|
335
|
+
*/
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* WalletManager handles mnemonic generation, key derivation, and in-memory
|
|
339
|
+
* key lifecycle for Townhouse node operations.
|
|
340
|
+
*/
|
|
341
|
+
declare class WalletManager {
|
|
342
|
+
private readonly config;
|
|
343
|
+
private state;
|
|
344
|
+
constructor(config: WalletManagerConfig);
|
|
345
|
+
/** Path to the encrypted wallet file */
|
|
346
|
+
get encryptedPath(): string;
|
|
347
|
+
/**
|
|
348
|
+
* Generate a new 12-word BIP-39 mnemonic and derive all node keys.
|
|
349
|
+
* Returns the mnemonic (for one-time display) and the derived state.
|
|
350
|
+
*/
|
|
351
|
+
generate(): Promise<{
|
|
352
|
+
mnemonic: string;
|
|
353
|
+
state: WalletState;
|
|
354
|
+
}>;
|
|
355
|
+
/**
|
|
356
|
+
* Import an existing mnemonic (12 or 24 words) and derive all node keys.
|
|
357
|
+
* Throws if mnemonic is invalid (wrong checksum, wrong word count, etc).
|
|
358
|
+
*/
|
|
359
|
+
fromMnemonic(mnemonic: string): Promise<WalletState>;
|
|
360
|
+
/**
|
|
361
|
+
* Get derived keys for a specific node type.
|
|
362
|
+
* Throws if wallet has not been initialized (call generate() or fromMnemonic() first).
|
|
363
|
+
*/
|
|
364
|
+
getNodeKeys(nodeType: NodeType$1): NodeKeys;
|
|
365
|
+
/**
|
|
366
|
+
* Get display-safe info for all node types (no secrets).
|
|
367
|
+
*/
|
|
368
|
+
getAllKeys(): NodeKeyInfo[];
|
|
369
|
+
/**
|
|
370
|
+
* List keys for all node types (alias for getAllKeys for API compatibility).
|
|
371
|
+
*/
|
|
372
|
+
listKeys(): NodeKeyInfo[];
|
|
373
|
+
/**
|
|
374
|
+
* Zero all in-memory key material. After calling lock(),
|
|
375
|
+
* getNodeKeys() and getAllKeys() will throw.
|
|
376
|
+
*/
|
|
377
|
+
lock(): void;
|
|
378
|
+
/**
|
|
379
|
+
* Derive keys for all node types from a mnemonic.
|
|
380
|
+
*/
|
|
381
|
+
private deriveAllKeys;
|
|
382
|
+
/**
|
|
383
|
+
* Derive Nostr + EVM keys for a specific node type.
|
|
384
|
+
*/
|
|
385
|
+
private deriveNodeKeys;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Wallet encryption/decryption for Townhouse (Story 21.4, Task 2).
|
|
390
|
+
*
|
|
391
|
+
* Uses Node.js crypto: scrypt for KDF, AES-256-GCM for authenticated encryption.
|
|
392
|
+
* The mnemonic is the plaintext being encrypted.
|
|
393
|
+
*/
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Encrypt a mnemonic with a password using scrypt + AES-256-GCM.
|
|
397
|
+
*/
|
|
398
|
+
declare function encryptWallet(mnemonic: string, password: string): EncryptedWallet;
|
|
399
|
+
/**
|
|
400
|
+
* Decrypt an encrypted wallet with a password.
|
|
401
|
+
* Throws on wrong password (GCM auth tag verification failure).
|
|
402
|
+
*/
|
|
403
|
+
declare function decryptWallet(encrypted: EncryptedWallet, password: string): string;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Wallet file I/O for Townhouse (Story 21.4, Task 2.2).
|
|
407
|
+
*
|
|
408
|
+
* Persists encrypted wallet to disk with 0o600 permissions (owner-only).
|
|
409
|
+
* Warns if existing file has world-readable permissions.
|
|
410
|
+
*/
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Save encrypted wallet to disk with restrictive permissions.
|
|
414
|
+
* Creates parent directory if missing.
|
|
415
|
+
*/
|
|
416
|
+
declare function saveWallet(path: string, encrypted: EncryptedWallet): Promise<void>;
|
|
417
|
+
/**
|
|
418
|
+
* Load encrypted wallet from disk.
|
|
419
|
+
* Returns null if file does not exist.
|
|
420
|
+
* Warns (via returned flag) if file permissions are too open.
|
|
421
|
+
*/
|
|
422
|
+
declare function loadWallet(path: string): Promise<{
|
|
423
|
+
wallet: EncryptedWallet;
|
|
424
|
+
permissionsWarning?: string;
|
|
425
|
+
} | null>;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Docker Orchestration Engine for Townhouse (Story 21.2).
|
|
429
|
+
*
|
|
430
|
+
* Manages the full container lifecycle: network creation, image pulling,
|
|
431
|
+
* container creation/start/stop/removal, and health check polling.
|
|
432
|
+
* Uses dockerode for programmatic Docker control with DI for testability.
|
|
433
|
+
*/
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* DockerOrchestrator manages the lifecycle of Townhouse containers.
|
|
437
|
+
*
|
|
438
|
+
* Constructor accepts a dockerode instance (DI for testability) and config.
|
|
439
|
+
* Emits typed events defined in OrchestratorEvents: pullProgress,
|
|
440
|
+
* containerState, and healthCheck.
|
|
441
|
+
*/
|
|
442
|
+
declare class DockerOrchestrator extends EventEmitter {
|
|
443
|
+
private readonly docker;
|
|
444
|
+
private readonly config;
|
|
445
|
+
private readonly configGenerator;
|
|
446
|
+
private readonly walletManager;
|
|
447
|
+
private activeNodes;
|
|
448
|
+
private readonly statsCache;
|
|
449
|
+
constructor(docker: Docker, config: TownhouseConfig, walletManager?: WalletManager);
|
|
450
|
+
/**
|
|
451
|
+
* Orchestrate full startup sequence:
|
|
452
|
+
* 1. Ensure network exists
|
|
453
|
+
* 2. Pull images (with progress)
|
|
454
|
+
* 3. Start connector, wait for health
|
|
455
|
+
* 4. Start enabled node containers in parallel
|
|
456
|
+
*/
|
|
457
|
+
up(profiles: NodeType$1[]): Promise<void>;
|
|
458
|
+
/**
|
|
459
|
+
* Regenerate connector config and restart the connector container
|
|
460
|
+
* with updated environment variables (peer list).
|
|
461
|
+
*
|
|
462
|
+
* Sequence: emit connectorRestarting -> stop -> remove -> create -> start -> health -> emit connectorRestarted
|
|
463
|
+
*/
|
|
464
|
+
regenerateConnectorConfig(activeNodes: NodeType$1[]): Promise<void>;
|
|
465
|
+
/**
|
|
466
|
+
* Hot-add a node after initial startup.
|
|
467
|
+
* Starts the node container, then restarts the connector with updated peer list.
|
|
468
|
+
*/
|
|
469
|
+
addNode(type: NodeType$1): Promise<void>;
|
|
470
|
+
/**
|
|
471
|
+
* Hot-remove a node.
|
|
472
|
+
* Stops the node container, then restarts the connector with updated peer list.
|
|
473
|
+
*/
|
|
474
|
+
removeNode(type: NodeType$1): Promise<void>;
|
|
475
|
+
/**
|
|
476
|
+
* Graceful shutdown — stops containers in reverse order:
|
|
477
|
+
* 1. Stop all node containers in parallel
|
|
478
|
+
* 2. Stop connector
|
|
479
|
+
* 3. Remove network
|
|
480
|
+
*/
|
|
481
|
+
down(): Promise<void>;
|
|
482
|
+
/**
|
|
483
|
+
* Resolve the Nostr relay WebSocket URL for a Town node instance.
|
|
484
|
+
*
|
|
485
|
+
* Inspects the container's port bindings to get the host-bound port for
|
|
486
|
+
* the relay WebSocket (7100/tcp). Falls back to the Docker-internal URL
|
|
487
|
+
* when the server is running inside the Docker network or bindings are absent.
|
|
488
|
+
*
|
|
489
|
+
* @param nodeId - The `NodeInfo.id` value (e.g. 'town', 'dev-town-01')
|
|
490
|
+
*/
|
|
491
|
+
getNodeRelayEndpoint(nodeId: string): Promise<string>;
|
|
492
|
+
/**
|
|
493
|
+
* Resolve the BLS health HTTP URL for a node instance.
|
|
494
|
+
*
|
|
495
|
+
* Inspects the container's port bindings to find the host-bound port for the
|
|
496
|
+
* node's health endpoint. Falls back to Docker-internal URL when running
|
|
497
|
+
* inside the Docker network or when bindings are absent.
|
|
498
|
+
*
|
|
499
|
+
* @param nodeId - The `NodeInfo.id` value (e.g. 'mill', 'dev-mill-01')
|
|
500
|
+
* @param type - Node type (determines which internal port to use)
|
|
501
|
+
*/
|
|
502
|
+
getNodeHealthEndpoint(nodeId: string, type: 'town' | 'mill' | 'dvm'): Promise<string>;
|
|
503
|
+
/**
|
|
504
|
+
* Fetch network I/O stats for a container.
|
|
505
|
+
* Results are cached for 5 seconds to avoid per-request Docker API overhead.
|
|
506
|
+
*
|
|
507
|
+
* @param containerName - Full container name (e.g. 'townhouse-town')
|
|
508
|
+
* @returns Bandwidth stats or null when container is not running
|
|
509
|
+
*/
|
|
510
|
+
getContainerStats(containerName: string): Promise<BandwidthStats | null>;
|
|
511
|
+
/**
|
|
512
|
+
* Return status for all townhouse containers.
|
|
513
|
+
*
|
|
514
|
+
* Discovers both single-instance (townhouse-<type>) and multi-instance
|
|
515
|
+
* (townhouse-<prefix>-<type>-<n>) containers. Multi-instance containers
|
|
516
|
+
* are returned with a `name` matching their instance suffix so callers
|
|
517
|
+
* can build per-instance NodeInfo entries (e.g. "dev-town-01").
|
|
518
|
+
*/
|
|
519
|
+
status(): Promise<{
|
|
520
|
+
name: string;
|
|
521
|
+
type: 'connector' | 'town' | 'mill' | 'dvm';
|
|
522
|
+
state: string;
|
|
523
|
+
health?: string;
|
|
524
|
+
startedAt?: string;
|
|
525
|
+
}[]>;
|
|
526
|
+
/**
|
|
527
|
+
* Pull required images before starting containers.
|
|
528
|
+
* Skips images that already exist locally.
|
|
529
|
+
* Emits pullProgress events during download.
|
|
530
|
+
*/
|
|
531
|
+
pullImages(profiles: NodeType$1[]): Promise<void>;
|
|
532
|
+
/**
|
|
533
|
+
* Poll container health status via inspect().
|
|
534
|
+
* Retries at configurable interval, throws on timeout.
|
|
535
|
+
*/
|
|
536
|
+
healthCheck(containerName: string, options?: HealthCheckOptions): Promise<string>;
|
|
537
|
+
/**
|
|
538
|
+
* Create the townhouse-net bridge network if it doesn't exist.
|
|
539
|
+
*/
|
|
540
|
+
private ensureNetwork;
|
|
541
|
+
/**
|
|
542
|
+
* Start the connector container — always runs first.
|
|
543
|
+
*
|
|
544
|
+
* The connector image at 3.3.x reads its config from a YAML file pointed
|
|
545
|
+
* to by the `CONFIG_FILE` env var (default `./config.yaml`). We write the
|
|
546
|
+
* generated YAML to `<configDir>/connector.yaml` (sibling to wallet.enc),
|
|
547
|
+
* mount it as `/config/connector.yaml`, and set CONFIG_FILE accordingly.
|
|
548
|
+
*
|
|
549
|
+
* (Env-var-based config was set on the container historically but the
|
|
550
|
+
* connector image silently ignored them — see the YAML fix landing with
|
|
551
|
+
* this comment block.)
|
|
552
|
+
*/
|
|
553
|
+
private startConnector;
|
|
554
|
+
/**
|
|
555
|
+
* Start a node container (town, mill, or dvm).
|
|
556
|
+
* Retries up to MAX_START_RETRIES on failure.
|
|
557
|
+
*/
|
|
558
|
+
private startNode;
|
|
559
|
+
/**
|
|
560
|
+
* Wait for a container's health check to pass.
|
|
561
|
+
*/
|
|
562
|
+
private waitForHealth;
|
|
563
|
+
/**
|
|
564
|
+
* Start the relay-side ator sidecar that publishes a v3 hidden service
|
|
565
|
+
* forwarding inbound traffic to the town container's Nostr WebSocket port.
|
|
566
|
+
*
|
|
567
|
+
* The keypair directory is mounted read-write because the sidecar's
|
|
568
|
+
* entrypoint writes the `hostname` file on first boot (see
|
|
569
|
+
* docker/townhouse-ator-sidecar/Dockerfile). The town container picks up
|
|
570
|
+
* the resulting .anyone URL via the operator-set externalUrl field.
|
|
571
|
+
*/
|
|
572
|
+
private startRelayAtorSidecar;
|
|
573
|
+
/**
|
|
574
|
+
* Stop and remove a single container.
|
|
575
|
+
*/
|
|
576
|
+
private stopAndRemove;
|
|
577
|
+
/**
|
|
578
|
+
* Remove the townhouse-net network if it exists.
|
|
579
|
+
*/
|
|
580
|
+
private removeNetwork;
|
|
581
|
+
/**
|
|
582
|
+
* Build environment variables for the connector container.
|
|
583
|
+
* Delegates to ConnectorConfigGenerator for consistent config generation.
|
|
584
|
+
*/
|
|
585
|
+
private buildConnectorEnv;
|
|
586
|
+
/**
|
|
587
|
+
* Build environment variables for a node container.
|
|
588
|
+
* If a WalletManager is provided, injects per-node identity keys.
|
|
589
|
+
*/
|
|
590
|
+
private buildNodeEnv;
|
|
591
|
+
/**
|
|
592
|
+
* Follow a Docker pull stream and emit progress events.
|
|
593
|
+
*/
|
|
594
|
+
private followPullProgress;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Connector types for Townhouse (Story 21.3).
|
|
599
|
+
*
|
|
600
|
+
* Defines the runtime configuration shape for the standalone ILP connector,
|
|
601
|
+
* peer entries, and admin API response types.
|
|
602
|
+
*/
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* A peer entry representing a child node connected to the connector via BTP.
|
|
606
|
+
* Each active node becomes a child peer of the connector.
|
|
607
|
+
*/
|
|
608
|
+
interface PeerEntry {
|
|
609
|
+
/** Node type identifier (e.g., 'town', 'mill', 'dvm') */
|
|
610
|
+
id: string;
|
|
611
|
+
/** Relationship to connector — nodes are always children */
|
|
612
|
+
relation: 'child';
|
|
613
|
+
/** BTP WebSocket URL for Docker-internal communication */
|
|
614
|
+
btpUrl: string;
|
|
615
|
+
/** Asset code for ILP packets (e.g., 'USD') */
|
|
616
|
+
assetCode: string;
|
|
617
|
+
/** Asset scale (e.g., 6 for micro-units) */
|
|
618
|
+
assetScale: number;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
/**
|
|
622
|
+
* Hidden-service configuration for the connector's inbound BTP path.
|
|
623
|
+
*
|
|
624
|
+
* When set under transport.hiddenService, the connector boots the
|
|
625
|
+
* `@anyone-protocol/anyone-client` SDK in-process (Story 35.5 of the
|
|
626
|
+
* connector repo, "ManagedAnonClient") which spawns the `anon` binary
|
|
627
|
+
* and publishes a v3 hidden service. The .anyone hostname is read from
|
|
628
|
+
* `${dir}/hostname` after publish and substituted into the connector's
|
|
629
|
+
* externalUrl as `wss://<hostname>.anyone/btp`.
|
|
630
|
+
*
|
|
631
|
+
* Operator surface lives at transport.hiddenService.{dir, port}; the
|
|
632
|
+
* connector's wire format is more verbose (transport.managedOptions.*) —
|
|
633
|
+
* config-generator handles the translation.
|
|
634
|
+
*
|
|
635
|
+
* Per-townhouse-instance keypair: the secret key under `${dir}/` is the
|
|
636
|
+
* .anyone identity. Persist it across redeploys to keep the address stable;
|
|
637
|
+
* delete it to rotate.
|
|
638
|
+
*/
|
|
639
|
+
interface HiddenServiceConfig {
|
|
640
|
+
/**
|
|
641
|
+
* Absolute path to the v3 hidden-service key directory inside the
|
|
642
|
+
* connector container. The connector creates `hs_ed25519_secret_key`,
|
|
643
|
+
* `hs_ed25519_public_key`, and `hostname` here on first boot, then
|
|
644
|
+
* reuses them on subsequent boots if present.
|
|
645
|
+
*/
|
|
646
|
+
dir: string;
|
|
647
|
+
/**
|
|
648
|
+
* Hidden service port advertised on the .anyone address. Forwards to
|
|
649
|
+
* the connector's BTP server (typically same port as connector's
|
|
650
|
+
* btpServerPort, since the HS port = the port external peers dial).
|
|
651
|
+
*/
|
|
652
|
+
port: number;
|
|
653
|
+
/**
|
|
654
|
+
* Optional override for the externalUrl the connector advertises.
|
|
655
|
+
* Defaults to the literal "auto" when unset, which makes the connector
|
|
656
|
+
* resolve the .anyone hostname from `${dir}/hostname` at startup.
|
|
657
|
+
* Set explicitly only when forcing a specific .anyone address (rare).
|
|
658
|
+
*/
|
|
659
|
+
externalUrl?: string;
|
|
660
|
+
/** Overall deadline for SOCKS port readiness (ms). Default 60000. */
|
|
661
|
+
startupTimeoutMs?: number;
|
|
662
|
+
/** Overall deadline for `sdk.stop()` (ms). Default 10000. */
|
|
663
|
+
stopTimeoutMs?: number;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Runtime configuration for the standalone ILP connector.
|
|
667
|
+
* Generated by ConnectorConfigGenerator from TownhouseConfig.
|
|
668
|
+
*/
|
|
669
|
+
interface ConnectorRuntimeConfig {
|
|
670
|
+
/** Admin API listen port */
|
|
671
|
+
adminPort: number;
|
|
672
|
+
/** Base ILP address for this connector */
|
|
673
|
+
ilpAddress: string;
|
|
674
|
+
/** List of child peer entries (one per active node) */
|
|
675
|
+
peers: PeerEntry[];
|
|
676
|
+
/** Transport configuration */
|
|
677
|
+
transport: {
|
|
678
|
+
/**
|
|
679
|
+
* Operator-facing transport selection. Maps to the connector's
|
|
680
|
+
* discriminated union internally (mode='ator' → type='socks5').
|
|
681
|
+
*/
|
|
682
|
+
mode: 'ator' | 'direct';
|
|
683
|
+
/** SOCKS5 proxy URL (must be socks5h://) — required when mode='ator'. */
|
|
684
|
+
socksProxy?: string;
|
|
685
|
+
/**
|
|
686
|
+
* Externally reachable BTP URL the connector advertises to peers.
|
|
687
|
+
* Required when mode='ator' AND hiddenService is unset (operator runs
|
|
688
|
+
* their own anon binary externally). Ignored for mode='direct'.
|
|
689
|
+
* When hiddenService IS set and externalUrl is unset, the generator
|
|
690
|
+
* emits the literal "auto" so the connector resolves the .anyone
|
|
691
|
+
* hostname at startup.
|
|
692
|
+
*/
|
|
693
|
+
externalUrl?: string;
|
|
694
|
+
/** Inbound hidden-service publication (Story 35.5 path). */
|
|
695
|
+
hiddenService?: HiddenServiceConfig;
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Response from GET /health on the connector's healthCheckPort.
|
|
700
|
+
* Mirrors `HealthStatus` from `@toon-protocol/connector`.
|
|
701
|
+
*/
|
|
702
|
+
interface HealthResponse {
|
|
703
|
+
status: 'healthy' | 'unhealthy' | 'starting' | 'degraded';
|
|
704
|
+
uptime: number;
|
|
705
|
+
peersConnected: number;
|
|
706
|
+
totalPeers: number;
|
|
707
|
+
timestamp: string;
|
|
708
|
+
nodeId?: string;
|
|
709
|
+
version?: string;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Per-peer ILP counter snapshot from GET /admin/metrics.json.
|
|
713
|
+
* Mirrors `AdminMetricsJsonPeer` from `@toon-protocol/connector`.
|
|
714
|
+
*/
|
|
715
|
+
interface MetricsPeerEntry {
|
|
716
|
+
peerId: string;
|
|
717
|
+
connected: boolean;
|
|
718
|
+
packetsForwarded: number;
|
|
719
|
+
packetsRejected: number;
|
|
720
|
+
bytesSent: number;
|
|
721
|
+
lastPacketAt: string | null;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Response from GET /admin/metrics.json on the connector's adminApi port.
|
|
725
|
+
* Mirrors `AdminMetricsJsonResponse` from `@toon-protocol/connector`.
|
|
726
|
+
*/
|
|
727
|
+
interface MetricsResponse {
|
|
728
|
+
uptimeSeconds: number;
|
|
729
|
+
aggregate: {
|
|
730
|
+
packetsForwarded: number;
|
|
731
|
+
packetsRejected: number;
|
|
732
|
+
bytesSent: number;
|
|
733
|
+
};
|
|
734
|
+
peers: MetricsPeerEntry[];
|
|
735
|
+
timestamp: string;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Peer entry from GET /admin/peers on the connector's adminApi port.
|
|
739
|
+
* Mirrors the response of the connector's `router.get('/peers', …)` handler.
|
|
740
|
+
* Note: per-peer packet counters live on /admin/metrics.json, not here.
|
|
741
|
+
*/
|
|
742
|
+
interface PeerStatus {
|
|
743
|
+
id: string;
|
|
744
|
+
connected: boolean;
|
|
745
|
+
ilpAddresses: string[];
|
|
746
|
+
routeCount: number;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Wrapper response from GET /admin/peers.
|
|
750
|
+
* Mirrors the connector's `router.get('/peers')` JSON envelope.
|
|
751
|
+
*/
|
|
752
|
+
interface PeersResponse {
|
|
753
|
+
nodeId: string;
|
|
754
|
+
peerCount: number;
|
|
755
|
+
connectedCount: number;
|
|
756
|
+
peers: PeerStatus[];
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Filter params for GET /packets on the connector admin API.
|
|
760
|
+
*
|
|
761
|
+
* Townhouse-Side Contract (§getPacketLog) — added in story 21.10.
|
|
762
|
+
* The connector must expose `GET /packets?ilpAddress=<>&since=<>&limit=<>`.
|
|
763
|
+
* If the running connector image does not expose this endpoint, the
|
|
764
|
+
* /nodes/:type/packets/timeseries route returns 503 and the contract canary
|
|
765
|
+
* will catch the drift. See packages/sdk/CONNECTOR_MIGRATION.md §Townhouse-Side Contract.
|
|
766
|
+
*/
|
|
767
|
+
interface PacketLogFilter {
|
|
768
|
+
ilpAddress?: string;
|
|
769
|
+
since?: number;
|
|
770
|
+
limit?: number;
|
|
771
|
+
}
|
|
772
|
+
/** A single packet log entry returned by GET /packets */
|
|
773
|
+
interface PacketLogEntry {
|
|
774
|
+
ts: number;
|
|
775
|
+
ilpAddressFrom: string;
|
|
776
|
+
ilpAddressTo: string;
|
|
777
|
+
amount: string;
|
|
778
|
+
result: 'fulfill' | 'reject' | 'timeout';
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Connector Config Generator for Townhouse (Story 21.3).
|
|
783
|
+
*
|
|
784
|
+
* Generates runtime configuration for the standalone ILP connector
|
|
785
|
+
* based on the Townhouse config and currently active nodes.
|
|
786
|
+
*/
|
|
787
|
+
|
|
788
|
+
/** Default ATOR SOCKS proxy address */
|
|
789
|
+
declare const DEFAULT_ATOR_PROXY = "socks5h://proxy.ator.io:9050";
|
|
790
|
+
/**
|
|
791
|
+
* ConnectorConfigGenerator produces runtime configuration for the standalone
|
|
792
|
+
* ILP connector based on Townhouse config and active node list.
|
|
793
|
+
*
|
|
794
|
+
* Key design: peer BTP URLs are deterministic Docker DNS names, so nodes
|
|
795
|
+
* don't need to be running for config generation to work.
|
|
796
|
+
*/
|
|
797
|
+
declare class ConnectorConfigGenerator {
|
|
798
|
+
private readonly config;
|
|
799
|
+
constructor(config: TownhouseConfig);
|
|
800
|
+
/**
|
|
801
|
+
* Generate a ConnectorRuntimeConfig for the given set of active nodes.
|
|
802
|
+
*
|
|
803
|
+
* @param activeNodes - Node types currently running or about to start
|
|
804
|
+
* @returns Typed configuration object (not serialized)
|
|
805
|
+
*/
|
|
806
|
+
generate(activeNodes: NodeType$1[]): ConnectorRuntimeConfig;
|
|
807
|
+
/**
|
|
808
|
+
* Serialize a ConnectorRuntimeConfig into environment variable key-value pairs.
|
|
809
|
+
*
|
|
810
|
+
* @returns Record of env var name to string value
|
|
811
|
+
*/
|
|
812
|
+
toEnvVars(runtimeConfig: ConnectorRuntimeConfig): Record<string, string>;
|
|
813
|
+
/**
|
|
814
|
+
* Convert a ConnectorRuntimeConfig into the string[] format expected by
|
|
815
|
+
* dockerode's container create API (Env option: ['KEY=VALUE', ...]).
|
|
816
|
+
*
|
|
817
|
+
* @returns Array of 'KEY=VALUE' strings
|
|
818
|
+
*/
|
|
819
|
+
toEnvArray(runtimeConfig: ConnectorRuntimeConfig): string[];
|
|
820
|
+
/**
|
|
821
|
+
* Render a connector YAML config string the connector image at 3.3.x can
|
|
822
|
+
* load via its `CONFIG_FILE` env var (default `./config.yaml`).
|
|
823
|
+
*
|
|
824
|
+
* The shape mirrors `docker/configs/townhouse-dev-connector.yaml` (the
|
|
825
|
+
* working dev fixture) — peers list is empty because child nodes dial
|
|
826
|
+
* INTO the connector at startup; the connector accepts BTP connections
|
|
827
|
+
* (no-auth in dev) without needing pre-configured peer entries.
|
|
828
|
+
*
|
|
829
|
+
* Added in the orchestrator-bug-fix: env vars set on the container were
|
|
830
|
+
* silently ignored by the connector image, which only reads from this
|
|
831
|
+
* YAML file. Caller writes the returned string to disk and mounts it
|
|
832
|
+
* at `/config/connector.yaml` in the container.
|
|
833
|
+
*/
|
|
834
|
+
toYaml(runtimeConfig: ConnectorRuntimeConfig): string;
|
|
835
|
+
/**
|
|
836
|
+
* Translate the runtime config's transport block into the discriminated-
|
|
837
|
+
* union shape the connector expects. See toYaml's note for why this was
|
|
838
|
+
* silently broken before.
|
|
839
|
+
*/
|
|
840
|
+
private buildConnectorTransportBlock;
|
|
841
|
+
/**
|
|
842
|
+
* Generate PeerEntry list for each active node type.
|
|
843
|
+
* BTP URLs use Docker DNS: btp+ws://townhouse-{type}:3000 // nosemgrep: javascript.lang.security.detect-insecure-websocket.detect-insecure-websocket
|
|
844
|
+
*/
|
|
845
|
+
private generatePeerList;
|
|
846
|
+
/**
|
|
847
|
+
* Generate transport config from Townhouse config.
|
|
848
|
+
* When mode is 'ator', includes SOCKS proxy (uses default if not configured).
|
|
849
|
+
* Carries forward externalUrl + hiddenService when set; downstream
|
|
850
|
+
* buildConnectorTransportBlock handles translation to the connector's
|
|
851
|
+
* wire shape.
|
|
852
|
+
*/
|
|
853
|
+
private generateTransportConfig;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Connector Admin Client for Townhouse (Story 21.3, contract aligned in 21.7.5).
|
|
858
|
+
*
|
|
859
|
+
* HTTP client for the connector's admin API endpoints. Paths and response
|
|
860
|
+
* shapes mirror the connector source-of-truth — see
|
|
861
|
+
* `@toon-protocol/connector` `packages/connector/src/http/{types,admin-api}.ts`.
|
|
862
|
+
*
|
|
863
|
+
* Uses Node.js native fetch (available in Node 20+).
|
|
864
|
+
*
|
|
865
|
+
* Two distinct HTTP servers live on the connector image:
|
|
866
|
+
* - healthCheckPort serves /health and /health/{live,ready}
|
|
867
|
+
* - adminApi.port serves /admin/* (peers, metrics.json, routes, channels, …)
|
|
868
|
+
*
|
|
869
|
+
* The base URL passed to this client must point at whichever server hosts
|
|
870
|
+
* the endpoint being called: pass the healthCheckPort base for `getHealth`
|
|
871
|
+
* and the adminApi.port base for `getPeers` / `getMetrics`. In practice
|
|
872
|
+
* Townhouse currently runs both ports on the same host, so callers either
|
|
873
|
+
* construct two clients or hit a shared base URL when the ports overlap.
|
|
874
|
+
*/
|
|
875
|
+
|
|
876
|
+
declare class ConnectorAdminClient {
|
|
877
|
+
private readonly baseUrl;
|
|
878
|
+
private readonly timeoutMs;
|
|
879
|
+
/**
|
|
880
|
+
* @param baseUrl - Base URL for the connector admin API (e.g., 'http://localhost:9402')
|
|
881
|
+
* @param timeoutMs - Request timeout in milliseconds (default: 5000)
|
|
882
|
+
*/
|
|
883
|
+
constructor(baseUrl: string, timeoutMs?: number);
|
|
884
|
+
/**
|
|
885
|
+
* GET /health — returns the connector's HealthStatus from the healthCheckPort server.
|
|
886
|
+
*
|
|
887
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
888
|
+
*/
|
|
889
|
+
getHealth(): Promise<HealthResponse>;
|
|
890
|
+
/**
|
|
891
|
+
* GET /admin/metrics.json — returns the connector's per-peer ILP counters
|
|
892
|
+
* with an aggregate rollup, mirroring `AdminMetricsJsonResponse`.
|
|
893
|
+
*
|
|
894
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
895
|
+
*/
|
|
896
|
+
getMetrics(): Promise<MetricsResponse>;
|
|
897
|
+
/**
|
|
898
|
+
* GET /admin/peers — returns the connector's peer roster with route counts
|
|
899
|
+
* and ILP addresses. Returns the unwrapped peers array (the wrapper's
|
|
900
|
+
* nodeId / peerCount / connectedCount fields are dropped).
|
|
901
|
+
*
|
|
902
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
903
|
+
*/
|
|
904
|
+
getPeers(): Promise<PeerStatus[]>;
|
|
905
|
+
/**
|
|
906
|
+
* GET /packets — returns the connector's raw packet log filtered by the
|
|
907
|
+
* given criteria. Used by the timeseries aggregation route (story 21.10).
|
|
908
|
+
*
|
|
909
|
+
* Townhouse-Side Contract: see packages/sdk/CONNECTOR_MIGRATION.md §getPacketLog.
|
|
910
|
+
* If the connector image does not expose GET /packets, this method throws
|
|
911
|
+
* with a `ConnectorEndpointNotFound` error code so the route can return 503.
|
|
912
|
+
*
|
|
913
|
+
* @throws Error with code='ConnectorEndpointNotFound' when connector returns 404
|
|
914
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
915
|
+
*/
|
|
916
|
+
getPacketLog(filter?: PacketLogFilter): Promise<PacketLogEntry[]>;
|
|
917
|
+
/**
|
|
918
|
+
* Perform an HTTP GET request to the connector admin API.
|
|
919
|
+
* Wraps fetch with error handling for connection refused and non-200 responses.
|
|
920
|
+
*/
|
|
921
|
+
private fetch;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* ATOR transport probe — periodically TCP-connects to the configured SOCKS5
|
|
926
|
+
* proxy host:port and measures direct HTTPS latency for comparison.
|
|
927
|
+
*
|
|
928
|
+
* The probe answers a single operator question: "is my configured ATOR proxy
|
|
929
|
+
* contactable from this host?" TCP connect is the right granularity — if the
|
|
930
|
+
* TCP listener is up, the connector's real BTP traffic will succeed.
|
|
931
|
+
*
|
|
932
|
+
* The probe NEVER makes a real SOCKS5 handshake or proxied request — only a
|
|
933
|
+
* plain TCP connect to the proxy host:port.
|
|
934
|
+
*/
|
|
935
|
+
interface TransportProbeOptions {
|
|
936
|
+
proxyUrl: string;
|
|
937
|
+
intervalMs?: number;
|
|
938
|
+
/** Override the direct-latency probe URL (for tests — avoids real network). */
|
|
939
|
+
directProbeUrl?: string;
|
|
940
|
+
}
|
|
941
|
+
interface TransportProbeStatus {
|
|
942
|
+
reachable: boolean;
|
|
943
|
+
latencyProxyMs: number | null;
|
|
944
|
+
latencyDirectMs: number | null;
|
|
945
|
+
lastProbedAt: number;
|
|
946
|
+
probeError: string | null;
|
|
947
|
+
}
|
|
948
|
+
declare class TransportProbe {
|
|
949
|
+
private proxyUrl;
|
|
950
|
+
private readonly intervalMs;
|
|
951
|
+
private readonly directProbeUrl;
|
|
952
|
+
private running;
|
|
953
|
+
private timer;
|
|
954
|
+
private status;
|
|
955
|
+
constructor(opts: TransportProbeOptions);
|
|
956
|
+
/** Start the probe loop. Idempotent — calling twice while running is a no-op. */
|
|
957
|
+
start(): void;
|
|
958
|
+
/** Stop the probe loop. Idempotent. */
|
|
959
|
+
stop(): void;
|
|
960
|
+
/** Returns the latest probe snapshot synchronously. Never blocks. */
|
|
961
|
+
getStatus(): TransportProbeStatus;
|
|
962
|
+
/**
|
|
963
|
+
* Update the target proxy URL.
|
|
964
|
+
* The next tick will use the new URL; the current tick may complete against the old URL.
|
|
965
|
+
*/
|
|
966
|
+
setProxyUrl(url: string): void;
|
|
967
|
+
private tick;
|
|
968
|
+
private probeTcp;
|
|
969
|
+
private probeDirectLatency;
|
|
970
|
+
private logTransition;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
type ComposeProfile = 'dev' | 'hs';
|
|
974
|
+
interface ComposeLoaderOptions {
|
|
975
|
+
/** Override default `~/.townhouse/` write target. Used by tests. */
|
|
976
|
+
townhouseHome?: string;
|
|
977
|
+
/** Override the package-relative dist directory the loader reads from.
|
|
978
|
+
* Defaults to the `dist/` adjacent to compose-loader.js at runtime.
|
|
979
|
+
* Tests use this to point at fixture directories. */
|
|
980
|
+
distDir?: string;
|
|
981
|
+
}
|
|
982
|
+
declare class ComposeLoaderError extends Error {
|
|
983
|
+
constructor(message: string);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Returns the rendered compose YAML for the requested profile.
|
|
987
|
+
* For 'hs', digest substitutions are already applied (resolved at build time).
|
|
988
|
+
* For 'dev', the YAML is returned verbatim (uses local `toon:*` image tags).
|
|
989
|
+
* Throws `ComposeLoaderError` if the requested profile's YAML is unreadable.
|
|
990
|
+
*/
|
|
991
|
+
declare function loadComposeTemplate(profile: ComposeProfile, options?: ComposeLoaderOptions): string;
|
|
992
|
+
/**
|
|
993
|
+
* Writes the resolved compose YAML to `<townhouseHome>/compose/<profile>.yml`
|
|
994
|
+
* and copies `dist/image-manifest.json` to `<townhouseHome>/image-manifest.json`.
|
|
995
|
+
* BOTH output files are written with mode 0o600 (NFR8 — operator-secret file mode).
|
|
996
|
+
* Returns the absolute paths of the two files written.
|
|
997
|
+
*/
|
|
998
|
+
declare function materializeComposeTemplate(profile: ComposeProfile, options?: ComposeLoaderOptions): {
|
|
999
|
+
composePath: string;
|
|
1000
|
+
manifestPath: string;
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Townhouse API — type definitions.
|
|
1005
|
+
*/
|
|
1006
|
+
|
|
1007
|
+
/** Per-kind job count for jobsRecent.byKind */
|
|
1008
|
+
interface DvmJobsByKindEntry {
|
|
1009
|
+
kind: number;
|
|
1010
|
+
count: number;
|
|
1011
|
+
}
|
|
1012
|
+
/** Per-status job counts for the sliding window */
|
|
1013
|
+
interface DvmJobsByStatus {
|
|
1014
|
+
processing: number;
|
|
1015
|
+
success: number;
|
|
1016
|
+
error: number;
|
|
1017
|
+
partial: number;
|
|
1018
|
+
}
|
|
1019
|
+
/** Windowed recent-jobs telemetry (default window: 5 min) */
|
|
1020
|
+
interface DvmJobsRecent {
|
|
1021
|
+
total: number;
|
|
1022
|
+
byKind: DvmJobsByKindEntry[];
|
|
1023
|
+
byStatus: DvmJobsByStatus;
|
|
1024
|
+
}
|
|
1025
|
+
/** Response shape for GET /health on the DVM BLS server (port 3400). */
|
|
1026
|
+
interface DvmHealthResponse {
|
|
1027
|
+
status: 'starting' | 'ok' | 'stopping' | 'stopped' | 'error';
|
|
1028
|
+
version: string;
|
|
1029
|
+
nodePubkey: string;
|
|
1030
|
+
uptimeSec: number;
|
|
1031
|
+
/** Registered handler event kinds (e.g. [5094]). */
|
|
1032
|
+
handlerKinds: number[];
|
|
1033
|
+
/** Per-kind pricing in string-encoded bigint (e.g. { "5094": "10" }). */
|
|
1034
|
+
kindPricing: Record<string, string>;
|
|
1035
|
+
basePricePerByte: string;
|
|
1036
|
+
jobsRecent: DvmJobsRecent;
|
|
1037
|
+
}
|
|
1038
|
+
/** Node types supported by Townhouse */
|
|
1039
|
+
type NodeType = 'town' | 'mill' | 'dvm';
|
|
1040
|
+
/** Runtime state of a node container */
|
|
1041
|
+
type NodeState = 'running' | 'stopped' | 'error' | 'not-created';
|
|
1042
|
+
/** Response shape for GET /nodes */
|
|
1043
|
+
interface NodeInfo {
|
|
1044
|
+
/**
|
|
1045
|
+
* Unique instance identifier — equals `type` for single-instance deployments.
|
|
1046
|
+
* Required in responses from this API; undefined in legacy test fixtures.
|
|
1047
|
+
*/
|
|
1048
|
+
id: string;
|
|
1049
|
+
type: NodeType;
|
|
1050
|
+
enabled: boolean;
|
|
1051
|
+
state: NodeState;
|
|
1052
|
+
uptimeSeconds: number | null;
|
|
1053
|
+
image: string;
|
|
1054
|
+
}
|
|
1055
|
+
/** Detailed response shape for GET /nodes/:type */
|
|
1056
|
+
interface NodeDetail extends NodeInfo {
|
|
1057
|
+
config: {
|
|
1058
|
+
feePerEvent?: number;
|
|
1059
|
+
feeBasisPoints?: number;
|
|
1060
|
+
feePerJob?: number;
|
|
1061
|
+
kindPricing?: Record<string, number>;
|
|
1062
|
+
enabled: boolean;
|
|
1063
|
+
};
|
|
1064
|
+
metrics: MetricsPayload | null;
|
|
1065
|
+
}
|
|
1066
|
+
/** Metrics payload from connector admin — narrowed per connector-team agreement 2026-04-21 */
|
|
1067
|
+
interface MetricsPayload {
|
|
1068
|
+
packetsForwarded: number;
|
|
1069
|
+
packetsRejected: number;
|
|
1070
|
+
bytesSent: number;
|
|
1071
|
+
attribution: 'aggregate' | 'per-peer';
|
|
1072
|
+
available: boolean;
|
|
1073
|
+
}
|
|
1074
|
+
/** Nostr event shape forwarded in relayEvents messages */
|
|
1075
|
+
interface NostrEventPayload {
|
|
1076
|
+
id: string;
|
|
1077
|
+
kind: number;
|
|
1078
|
+
pubkey: string;
|
|
1079
|
+
content: string;
|
|
1080
|
+
tags: string[][];
|
|
1081
|
+
sig: string;
|
|
1082
|
+
created_at: number;
|
|
1083
|
+
}
|
|
1084
|
+
/** WebSocket message shapes */
|
|
1085
|
+
interface WsMetricsMessage {
|
|
1086
|
+
type: 'metrics';
|
|
1087
|
+
payload: MetricsPayload;
|
|
1088
|
+
ts: number;
|
|
1089
|
+
}
|
|
1090
|
+
interface WsNodeStateMessage {
|
|
1091
|
+
type: 'nodeState';
|
|
1092
|
+
payload: NodeStatePayload;
|
|
1093
|
+
ts: number;
|
|
1094
|
+
}
|
|
1095
|
+
interface WsHeartbeatMessage {
|
|
1096
|
+
type: 'heartbeat';
|
|
1097
|
+
ts: number;
|
|
1098
|
+
}
|
|
1099
|
+
interface WsBatchMessage {
|
|
1100
|
+
type: 'batch';
|
|
1101
|
+
messages: WsMessage[];
|
|
1102
|
+
ts: number;
|
|
1103
|
+
}
|
|
1104
|
+
/** Forwarded Nostr event from a Town relay subscription */
|
|
1105
|
+
interface WsRelayEventsMessage {
|
|
1106
|
+
type: 'relayEvents';
|
|
1107
|
+
nodeId: string;
|
|
1108
|
+
payload: NostrEventPayload;
|
|
1109
|
+
ts: number;
|
|
1110
|
+
}
|
|
1111
|
+
/** Connector restart notifications (emitted around fee-config PATCH) */
|
|
1112
|
+
interface WsConnectorRestartingMessage {
|
|
1113
|
+
type: 'connectorRestarting';
|
|
1114
|
+
ts: number;
|
|
1115
|
+
}
|
|
1116
|
+
interface WsConnectorRestartedMessage {
|
|
1117
|
+
type: 'connectorRestarted';
|
|
1118
|
+
ts: number;
|
|
1119
|
+
}
|
|
1120
|
+
interface NodeStatePayload {
|
|
1121
|
+
name: string;
|
|
1122
|
+
state: string;
|
|
1123
|
+
}
|
|
1124
|
+
/** Server notification when the upstream relay WebSocket for a nodeId disconnects */
|
|
1125
|
+
interface WsRelayEventsStatusMessage {
|
|
1126
|
+
type: 'relayEventsStatus';
|
|
1127
|
+
nodeId: string;
|
|
1128
|
+
connected: boolean;
|
|
1129
|
+
ts: number;
|
|
1130
|
+
}
|
|
1131
|
+
type WsMessage = WsMetricsMessage | WsNodeStateMessage | WsHeartbeatMessage | WsBatchMessage | WsRelayEventsMessage | WsConnectorRestartingMessage | WsConnectorRestartedMessage | WsRelayEventsStatusMessage;
|
|
1132
|
+
/** Response shape for GET /nodes/:type/bandwidth */
|
|
1133
|
+
interface BandwidthPayload {
|
|
1134
|
+
bytesIn: number;
|
|
1135
|
+
bytesOut: number;
|
|
1136
|
+
sampleAt: number;
|
|
1137
|
+
}
|
|
1138
|
+
/** Packet log time-series bucket */
|
|
1139
|
+
interface TimeseriesBucket {
|
|
1140
|
+
ts: number;
|
|
1141
|
+
count: number;
|
|
1142
|
+
}
|
|
1143
|
+
/** Response shape for GET /nodes/:type/packets/timeseries */
|
|
1144
|
+
interface PacketTimeseriesPayload {
|
|
1145
|
+
buckets: TimeseriesBucket[];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Minimal common health shape; superset emitted by Town containers. */
|
|
1149
|
+
interface TownHealthPayload {
|
|
1150
|
+
status: 'ok' | 'starting' | 'stopping' | 'stopped' | 'error';
|
|
1151
|
+
version?: string;
|
|
1152
|
+
uptimeSec?: number;
|
|
1153
|
+
nodePubkey?: string;
|
|
1154
|
+
}
|
|
1155
|
+
/** Union of all node health response shapes. */
|
|
1156
|
+
type NodeHealthPayload = MillHealthResponse | TownHealthPayload | DvmHealthResponse;
|
|
1157
|
+
/** Per-kind job activity bucket for GET /nodes/:nodeId/jobs/recent */
|
|
1158
|
+
interface JobsByKindEntry {
|
|
1159
|
+
kind: number;
|
|
1160
|
+
count: number;
|
|
1161
|
+
volume: string;
|
|
1162
|
+
}
|
|
1163
|
+
/** Response shape for GET /nodes/:nodeId/jobs/recent */
|
|
1164
|
+
interface JobsRecentPayload {
|
|
1165
|
+
count: number;
|
|
1166
|
+
volume: string;
|
|
1167
|
+
byKind: JobsByKindEntry[];
|
|
1168
|
+
byStatus: {
|
|
1169
|
+
processing: number;
|
|
1170
|
+
success: number;
|
|
1171
|
+
error: number;
|
|
1172
|
+
partial: number;
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
/** Per-pair swap activity bucket */
|
|
1176
|
+
interface SwapByPairEntry {
|
|
1177
|
+
pair: string;
|
|
1178
|
+
count: number;
|
|
1179
|
+
volume: string;
|
|
1180
|
+
}
|
|
1181
|
+
/** Response shape for GET /nodes/mill/swaps/recent */
|
|
1182
|
+
interface MillSwapsRecentPayload {
|
|
1183
|
+
count: number;
|
|
1184
|
+
volume: string;
|
|
1185
|
+
byPair: SwapByPairEntry[];
|
|
1186
|
+
}
|
|
1187
|
+
/** Per-chain deposit address entry */
|
|
1188
|
+
interface DepositAddressEntry {
|
|
1189
|
+
family: 'evm' | 'solana' | 'mina';
|
|
1190
|
+
address: string;
|
|
1191
|
+
}
|
|
1192
|
+
/** Response shape for GET /nodes/:type/deposit-addresses */
|
|
1193
|
+
interface DepositAddressesPayload {
|
|
1194
|
+
chains: DepositAddressEntry[];
|
|
1195
|
+
}
|
|
1196
|
+
/** Per-chain balance entry returned by GET /api/wallet/balances */
|
|
1197
|
+
interface WalletBalanceEntry {
|
|
1198
|
+
nodeType: 'town' | 'mill' | 'dvm';
|
|
1199
|
+
family: 'evm' | 'solana' | 'mina';
|
|
1200
|
+
token: 'ETH' | 'USDC' | 'SOL' | 'MINA';
|
|
1201
|
+
address: string;
|
|
1202
|
+
/** Decimal string in raw units (wei, lamports, etc.) */
|
|
1203
|
+
balance: string;
|
|
1204
|
+
/** Decimal places — 18 for ETH, 6 for USDC, 9 for SOL, 9 for MINA */
|
|
1205
|
+
scale: number;
|
|
1206
|
+
available: boolean;
|
|
1207
|
+
/** Populated when available === false */
|
|
1208
|
+
reason?: string;
|
|
1209
|
+
}
|
|
1210
|
+
/** Response shape for GET /api/wallet/balances */
|
|
1211
|
+
interface WalletBalancesPayload {
|
|
1212
|
+
entries: WalletBalanceEntry[];
|
|
1213
|
+
ts: number;
|
|
1214
|
+
}
|
|
1215
|
+
/** Request body for POST /api/wallet/withdraw.
|
|
1216
|
+
* `chainFamily` lists all values the route accepts at the wire level — the
|
|
1217
|
+
* handler returns 501 for solana/mina with a structured payload pointing the
|
|
1218
|
+
* caller at the deposit-address copy flow. */
|
|
1219
|
+
interface WithdrawRequest {
|
|
1220
|
+
nodeType: 'town' | 'mill' | 'dvm';
|
|
1221
|
+
chainFamily: 'evm' | 'solana' | 'mina';
|
|
1222
|
+
token: 'native' | 'USDC';
|
|
1223
|
+
recipient: string;
|
|
1224
|
+
/** Decimal string in raw units */
|
|
1225
|
+
amount: string;
|
|
1226
|
+
/** When true: returns gas estimate without broadcasting */
|
|
1227
|
+
dryRun?: boolean;
|
|
1228
|
+
}
|
|
1229
|
+
/** Successful broadcast response (dryRun !== true). */
|
|
1230
|
+
interface WithdrawSuccessResponse {
|
|
1231
|
+
txHash: `0x${string}`;
|
|
1232
|
+
chainId: number;
|
|
1233
|
+
}
|
|
1234
|
+
/** Successful dryRun response (no broadcast performed). */
|
|
1235
|
+
interface WithdrawDryRunResponse {
|
|
1236
|
+
estimatedGas: string;
|
|
1237
|
+
estimatedFee: string;
|
|
1238
|
+
}
|
|
1239
|
+
/** Discriminated union — callers narrow by presence of `txHash`. */
|
|
1240
|
+
type WithdrawResponse = WithdrawSuccessResponse | WithdrawDryRunResponse;
|
|
1241
|
+
/** Request body for POST /api/wallet/reveal */
|
|
1242
|
+
interface RevealRequest {
|
|
1243
|
+
password: string;
|
|
1244
|
+
}
|
|
1245
|
+
/** Response shape for POST /api/wallet/reveal */
|
|
1246
|
+
type RevealResponse = {
|
|
1247
|
+
mnemonic: string;
|
|
1248
|
+
} | {
|
|
1249
|
+
error: 'invalid_password' | 'wallet_not_initialized' | 'wallet_corrupted';
|
|
1250
|
+
message?: string;
|
|
1251
|
+
};
|
|
1252
|
+
/** Response shape for GET /api/wallet/transaction/:txHash */
|
|
1253
|
+
interface TransactionReceiptPayload {
|
|
1254
|
+
status: 'pending' | 'success' | 'reverted';
|
|
1255
|
+
blockNumber?: number;
|
|
1256
|
+
txHash: string;
|
|
1257
|
+
}
|
|
1258
|
+
/** Response shape for GET /api/wizard/state */
|
|
1259
|
+
interface WizardStatePayload {
|
|
1260
|
+
config_exists: boolean;
|
|
1261
|
+
wallet_exists: boolean;
|
|
1262
|
+
containers_running: boolean;
|
|
1263
|
+
mode: 'wizard' | 'normal';
|
|
1264
|
+
ts: number;
|
|
1265
|
+
}
|
|
1266
|
+
/** Request body for POST /api/wizard/init.
|
|
1267
|
+
* `mnemonic` is required in BOTH modes — `mnemonic_mode` is purely a UX hint
|
|
1268
|
+
* for the SPA, the server is stateless WRT the mnemonic and validates it on
|
|
1269
|
+
* every init regardless of mode (see story 21.14 Dev Notes). */
|
|
1270
|
+
interface WizardInitRequest {
|
|
1271
|
+
password: string;
|
|
1272
|
+
password_confirm: string;
|
|
1273
|
+
mnemonic_mode: 'generate' | 'import';
|
|
1274
|
+
mnemonic: string;
|
|
1275
|
+
backup_ack: boolean;
|
|
1276
|
+
nodes: {
|
|
1277
|
+
town: {
|
|
1278
|
+
enabled: boolean;
|
|
1279
|
+
feePerEvent?: number;
|
|
1280
|
+
};
|
|
1281
|
+
mill: {
|
|
1282
|
+
enabled: boolean;
|
|
1283
|
+
feeBasisPoints?: number;
|
|
1284
|
+
};
|
|
1285
|
+
dvm: {
|
|
1286
|
+
enabled: boolean;
|
|
1287
|
+
feePerJob?: number;
|
|
1288
|
+
};
|
|
1289
|
+
};
|
|
1290
|
+
transport: {
|
|
1291
|
+
mode: 'direct' | 'ator';
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
/** Progress messages streamed over WS /api/wizard/progress */
|
|
1295
|
+
type WizardProgressMessage = {
|
|
1296
|
+
type: 'pull_progress';
|
|
1297
|
+
image: string;
|
|
1298
|
+
status: string;
|
|
1299
|
+
progress?: string;
|
|
1300
|
+
ts: number;
|
|
1301
|
+
} | {
|
|
1302
|
+
type: 'container_starting';
|
|
1303
|
+
name: string;
|
|
1304
|
+
ts: number;
|
|
1305
|
+
} | {
|
|
1306
|
+
type: 'container_healthy';
|
|
1307
|
+
name: string;
|
|
1308
|
+
ts: number;
|
|
1309
|
+
} | {
|
|
1310
|
+
type: 'container_failed';
|
|
1311
|
+
name: string;
|
|
1312
|
+
reason: string;
|
|
1313
|
+
ts: number;
|
|
1314
|
+
} | {
|
|
1315
|
+
type: 'launch_complete';
|
|
1316
|
+
ts: number;
|
|
1317
|
+
} | {
|
|
1318
|
+
type: 'error';
|
|
1319
|
+
message: string;
|
|
1320
|
+
ts: number;
|
|
1321
|
+
};
|
|
1322
|
+
/** Response shape for GET /api/transport */
|
|
1323
|
+
interface TransportStatusPayload {
|
|
1324
|
+
mode: 'direct' | 'ator';
|
|
1325
|
+
/** Present only when mode === 'ator' */
|
|
1326
|
+
socksProxy?: string;
|
|
1327
|
+
reachable: boolean;
|
|
1328
|
+
latencyProxyMs: number | null;
|
|
1329
|
+
latencyDirectMs: number | null;
|
|
1330
|
+
/** ms epoch; 0 if probe never ran */
|
|
1331
|
+
lastProbedAt: number;
|
|
1332
|
+
probeError: string | null;
|
|
1333
|
+
/** Server timestamp at response build time */
|
|
1334
|
+
ts: number;
|
|
1335
|
+
}
|
|
1336
|
+
/** Request body for PATCH /api/transport */
|
|
1337
|
+
interface TransportPatchRequest {
|
|
1338
|
+
mode: 'direct' | 'ator';
|
|
1339
|
+
socksProxy?: string;
|
|
1340
|
+
}
|
|
1341
|
+
/** Response body for PATCH /api/transport */
|
|
1342
|
+
interface TransportPatchResponse {
|
|
1343
|
+
mode: 'direct' | 'ator';
|
|
1344
|
+
socksProxy?: string;
|
|
1345
|
+
restartTriggered: boolean;
|
|
1346
|
+
/** ms epoch of connector restart; present when restartTriggered === true */
|
|
1347
|
+
restartedAt?: number;
|
|
1348
|
+
}
|
|
1349
|
+
/** API server returned by createApiServer */
|
|
1350
|
+
interface ApiServer {
|
|
1351
|
+
app: FastifyInstance;
|
|
1352
|
+
close: () => Promise<void>;
|
|
1353
|
+
}
|
|
1354
|
+
/** Dependencies required to create the API server */
|
|
1355
|
+
interface ApiDeps {
|
|
1356
|
+
configPath: string;
|
|
1357
|
+
config: TownhouseConfig;
|
|
1358
|
+
orchestrator: DockerOrchestrator;
|
|
1359
|
+
wallet: WalletManager;
|
|
1360
|
+
connectorAdmin: ConnectorAdminClient;
|
|
1361
|
+
/**
|
|
1362
|
+
* Probe instance. Required: callers (createApiServer, createWizardApiServer,
|
|
1363
|
+
* cli, dev API server) construct it from the config and pass it explicitly.
|
|
1364
|
+
*/
|
|
1365
|
+
transportProbe: TransportProbe;
|
|
1366
|
+
logger?: FastifyBaseLogger;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* API Server Factory.
|
|
1371
|
+
*
|
|
1372
|
+
* SECURITY: Only binds to loopback address by default (localhost-only for v1).
|
|
1373
|
+
* Set TOWNHOUSE_API_ALLOW_REMOTE=1 to override this security boundary.
|
|
1374
|
+
*/
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Create the Fastify API server. Caller MUST supply a `transportProbe` in
|
|
1378
|
+
* `deps` (constructed from the config and started if mode === 'ator').
|
|
1379
|
+
*/
|
|
1380
|
+
declare function createApiServer(deps: ApiDeps): Promise<ApiServer>;
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Wizard API Server Factory.
|
|
1384
|
+
*
|
|
1385
|
+
* Starts in wizard mode (only wizard routes), transitions to normal mode
|
|
1386
|
+
* after POST /wizard/init completes and containers are healthy.
|
|
1387
|
+
* SECURITY: Wizard mode hard-rejects non-loopback bind regardless of env var.
|
|
1388
|
+
*/
|
|
1389
|
+
|
|
1390
|
+
interface WizardInitialDeps {
|
|
1391
|
+
/** Directory where ~/.townhouse/ (or override) lives */
|
|
1392
|
+
configDir: string;
|
|
1393
|
+
/** Full path to config.yaml */
|
|
1394
|
+
configPath: string;
|
|
1395
|
+
/** Full path to wallet.enc */
|
|
1396
|
+
walletPath: string;
|
|
1397
|
+
/** Port to bind the API */
|
|
1398
|
+
port: number;
|
|
1399
|
+
/** Bind host — must be a loopback address; defaults to 127.0.0.1 */
|
|
1400
|
+
bindHost?: string;
|
|
1401
|
+
docker: Docker;
|
|
1402
|
+
logger?: FastifyBaseLogger | boolean;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Create the wizard API server. Starts in wizard-only mode.
|
|
1406
|
+
* After POST /wizard/init + orchestrator launch, transitions to normal mode.
|
|
1407
|
+
*/
|
|
1408
|
+
declare function createWizardApiServer(initialDeps: WizardInitialDeps): Promise<ApiServer>;
|
|
1409
|
+
|
|
1410
|
+
export { type ApiConfig, type ApiDeps, type ApiServer, type BandwidthPayload, type BandwidthStats, ComposeLoaderError, type ComposeLoaderOptions, type ComposeProfile, ConfigValidationError, ConnectorAdminClient, type ConnectorConfig, ConnectorConfigGenerator, type ConnectorRuntimeConfig, type ContainerSpec, DEFAULT_ATOR_PROXY, type DepositAddressEntry, type DepositAddressesPayload, type DerivedNodeKeys, DockerOrchestrator, type DvmHealthResponse, type DvmNodeConfig, type EncryptedWallet, type HealthCheckOptions, type HealthResponse, type JobsByKindEntry, type JobsRecentPayload, type LoggingConfig, type MetricsPayload, type MetricsPeerEntry, type MetricsResponse, type MillNodeConfig, type MillSwapsRecentPayload, type NodeDetail, type NodeHealthPayload, type NodeInfo, type NodeKeyInfo, type NodeKeys, type NodeState, type NodeType$1 as NodeType, type NodesConfig, type NostrEventPayload, type OrchestratorEvents, type PacketLogEntry, type PacketLogFilter, type PacketTimeseriesPayload, type PeerEntry, type PeerStatus, type PeersResponse, type RevealRequest, type RevealResponse, type SwapByPairEntry, type TimeseriesBucket, type TownHealthPayload, type TownNodeConfig, type TownhouseConfig, type TransactionReceiptPayload, type TransportConfig, type TransportPatchRequest, type TransportPatchResponse, TransportProbe, type TransportStatusPayload, type WalletBalanceEntry, type WalletBalancesPayload, type WalletConfig, WalletManager, type WalletManagerConfig, type WalletState, type WithdrawDryRunResponse, type WithdrawRequest, type WithdrawResponse, type WithdrawSuccessResponse, type WizardInitRequest, type WizardProgressMessage, type WizardStatePayload, type WsBatchMessage, type WsConnectorRestartedMessage, type WsConnectorRestartingMessage, type WsHeartbeatMessage, type WsMessage, type WsMetricsMessage, type WsNodeStateMessage, type WsRelayEventsMessage, createApiServer, createWizardApiServer, decryptWallet, encryptWallet, getDefaultConfig, loadComposeTemplate, loadConfig, loadWallet, materializeComposeTemplate, saveConfig, saveWallet, validateConfig };
|