@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.
- package/README.md +117 -0
- package/dist/{chunk-IB6TNCUQ.js → chunk-4WCMVIO4.js} +3922 -473
- package/dist/chunk-4WCMVIO4.js.map +1 -0
- package/dist/chunk-GQNBZJ6F.js +39 -0
- package/dist/chunk-GQNBZJ6F.js.map +1 -0
- package/dist/{chunk-UTFWPLTB.js → chunk-I2R4CRUX.js} +2 -22
- package/dist/chunk-I2R4CRUX.js.map +1 -0
- package/dist/chunk-JCOFMUPL.js +65 -0
- package/dist/chunk-JCOFMUPL.js.map +1 -0
- package/dist/cli.d.ts +94 -2
- package/dist/cli.js +3115 -111
- package/dist/cli.js.map +1 -1
- package/dist/compose/townhouse-dev.yml +1 -1
- package/dist/compose/townhouse-hs.yml +126 -19
- package/dist/{demo-MJR47QHZ.js → demo-3DWRDMYY.js} +3 -2
- package/dist/{demo-MJR47QHZ.js.map → demo-3DWRDMYY.js.map} +1 -1
- package/dist/image-manifest.json +12 -12
- package/dist/index.d.ts +1258 -659
- package/dist/index.js +36 -140
- package/dist/index.js.map +1 -1
- package/dist/manager-SsneW_Mj.d.ts +519 -0
- package/dist/rsa-from-seed-VMNLNDZM.js +62 -0
- package/dist/rsa-from-seed-VMNLNDZM.js.map +1 -0
- package/dist/tui-OIFXGBTL.js +625 -0
- package/dist/tui-OIFXGBTL.js.map +1 -0
- package/package.json +18 -2
- package/dist/chunk-IB6TNCUQ.js.map +0 -1
- package/dist/chunk-UTFWPLTB.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,150 +1,50 @@
|
|
|
1
|
+
import { E as EncryptedWallet, T as TownhouseConfig, W as WalletManager, a as ComposeProfile, N as NodeType$1, B as BandwidthStats, H as HealthCheckOptions } from './manager-SsneW_Mj.js';
|
|
2
|
+
export { A as ApiConfig, b as ComposeLoaderError, C as ComposeLoaderOptions, c as ConnectorConfig, d as ContainerSpec, D as DerivedNodeKeys, e as DvmNodeConfig, L as LoggingConfig, M as MillNodeConfig, f as NodeKeyInfo, g as NodeKeys, h as NodesConfig, O as OrchestratorEvents, i as TownNodeConfig, j as TransportConfig, k as WalletConfig, l as WalletManagerConfig, m as WalletState, n as loadComposeTemplate, o as materializeComposeTemplate } from './manager-SsneW_Mj.js';
|
|
1
3
|
import { EventEmitter } from 'node:events';
|
|
2
4
|
import Docker from 'dockerode';
|
|
3
5
|
import { FastifyBaseLogger, FastifyInstance } from 'fastify';
|
|
4
6
|
import { MillHealthResponse } from '@toon-protocol/mill';
|
|
5
7
|
export { MillHealthResponse } from '@toon-protocol/mill';
|
|
8
|
+
import { z } from 'zod';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
11
|
+
* Wallet encryption/decryption for Townhouse (Story 21.4, Task 2).
|
|
12
|
+
*
|
|
13
|
+
* Uses Node.js crypto: scrypt for KDF, AES-256-GCM for authenticated encryption.
|
|
14
|
+
* The mnemonic is the plaintext being encrypted.
|
|
10
15
|
*/
|
|
11
|
-
|
|
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
|
-
}
|
|
16
|
+
|
|
66
17
|
/**
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
18
|
+
* Encrypt a mnemonic with a password using scrypt + AES-256-GCM.
|
|
19
|
+
*/
|
|
20
|
+
declare function encryptWallet(mnemonic: string, password: string): EncryptedWallet;
|
|
21
|
+
/**
|
|
22
|
+
* Decrypt an encrypted wallet with a password.
|
|
23
|
+
* Throws on wrong password (GCM auth tag verification failure).
|
|
24
|
+
*/
|
|
25
|
+
declare function decryptWallet(encrypted: EncryptedWallet, password: string): string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wallet file I/O for Townhouse (Story 21.4, Task 2.2).
|
|
74
29
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* here until a real use case demands them.
|
|
30
|
+
* Persists encrypted wallet to disk with 0o600 permissions (owner-only).
|
|
31
|
+
* Warns if existing file has world-readable permissions.
|
|
78
32
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Save encrypted wallet to disk with restrictive permissions.
|
|
36
|
+
* Creates parent directory if missing.
|
|
37
|
+
*/
|
|
38
|
+
declare function saveWallet(path: string, encrypted: EncryptedWallet): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Load encrypted wallet from disk.
|
|
41
|
+
* Returns null if file does not exist.
|
|
42
|
+
* Warns (via returned flag) if file permissions are too open.
|
|
43
|
+
*/
|
|
44
|
+
declare function loadWallet(path: string): Promise<{
|
|
45
|
+
wallet: EncryptedWallet;
|
|
46
|
+
permissionsWarning?: string;
|
|
47
|
+
} | null>;
|
|
148
48
|
|
|
149
49
|
/**
|
|
150
50
|
* Sensible default configuration. All nodes disabled by default —
|
|
@@ -185,244 +85,476 @@ declare class ConfigValidationError extends Error {
|
|
|
185
85
|
declare function validateConfig(raw: unknown): TownhouseConfig;
|
|
186
86
|
|
|
187
87
|
/**
|
|
188
|
-
*
|
|
88
|
+
* Connector types for Townhouse (Story 21.3).
|
|
189
89
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
90
|
+
* Defines the runtime configuration shape for the standalone ILP connector,
|
|
91
|
+
* peer entries, and admin API response types.
|
|
192
92
|
*/
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
|
|
232
|
-
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A peer entry representing a child node connected to the connector via BTP.
|
|
96
|
+
* Each active node becomes a child peer of the connector.
|
|
97
|
+
*/
|
|
98
|
+
interface PeerEntry {
|
|
99
|
+
/** Node type identifier (e.g., 'town', 'mill', 'dvm') */
|
|
100
|
+
id: string;
|
|
101
|
+
/** Relationship to connector — nodes are always children */
|
|
102
|
+
relation: 'child';
|
|
103
|
+
/** BTP WebSocket URL for Docker-internal communication */
|
|
104
|
+
btpUrl: string;
|
|
105
|
+
/** Asset code for ILP packets (e.g., 'USD') */
|
|
106
|
+
assetCode: string;
|
|
107
|
+
/** Asset scale (e.g., 6 for micro-units) */
|
|
108
|
+
assetScale: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
/**
|
|
112
|
+
* Hidden-service configuration for the connector's inbound BTP path.
|
|
113
|
+
*
|
|
114
|
+
* When set under transport.hiddenService, the connector boots the
|
|
115
|
+
* `@anyone-protocol/anyone-client` SDK in-process (Story 35.5 of the
|
|
116
|
+
* connector repo, "ManagedAnonClient") which spawns the `anon` binary
|
|
117
|
+
* and publishes a v3 hidden service. The .anyone hostname is read from
|
|
118
|
+
* `${dir}/hostname` after publish and substituted into the connector's
|
|
119
|
+
* externalUrl as `wss://<hostname>.anyone/btp`.
|
|
120
|
+
*
|
|
121
|
+
* Operator surface lives at transport.hiddenService.{dir, port}; the
|
|
122
|
+
* connector's wire format is more verbose (transport.managedOptions.*) —
|
|
123
|
+
* config-generator handles the translation.
|
|
124
|
+
*
|
|
125
|
+
* Per-townhouse-instance keypair: the secret key under `${dir}/` is the
|
|
126
|
+
* .anyone identity. Persist it across redeploys to keep the address stable;
|
|
127
|
+
* delete it to rotate.
|
|
128
|
+
*/
|
|
129
|
+
interface HiddenServiceConfig {
|
|
130
|
+
/**
|
|
131
|
+
* Absolute path to the v3 hidden-service key directory inside the
|
|
132
|
+
* connector container. The connector creates `hs_ed25519_secret_key`,
|
|
133
|
+
* `hs_ed25519_public_key`, and `hostname` here on first boot, then
|
|
134
|
+
* reuses them on subsequent boots if present.
|
|
135
|
+
*/
|
|
136
|
+
dir: string;
|
|
137
|
+
/**
|
|
138
|
+
* Hidden service port advertised on the .anyone address. Forwards to
|
|
139
|
+
* the connector's BTP server (typically same port as connector's
|
|
140
|
+
* btpServerPort, since the HS port = the port external peers dial).
|
|
141
|
+
*/
|
|
142
|
+
port: number;
|
|
143
|
+
/**
|
|
144
|
+
* Optional override for the externalUrl the connector advertises.
|
|
145
|
+
* Defaults to the literal "auto" when unset, which makes the connector
|
|
146
|
+
* resolve the .anyone hostname from `${dir}/hostname` at startup.
|
|
147
|
+
* Set explicitly only when forcing a specific .anyone address (rare).
|
|
148
|
+
*/
|
|
149
|
+
externalUrl?: string;
|
|
150
|
+
/** Overall deadline for SOCKS port readiness (ms). Default 60000. */
|
|
151
|
+
startupTimeoutMs?: number;
|
|
152
|
+
/** Overall deadline for `sdk.stop()` (ms). Default 10000. */
|
|
153
|
+
stopTimeoutMs?: number;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Runtime configuration for the standalone ILP connector.
|
|
157
|
+
* Generated by ConnectorConfigGenerator from TownhouseConfig.
|
|
158
|
+
*/
|
|
159
|
+
interface ConnectorRuntimeConfig {
|
|
160
|
+
/** Admin API listen port */
|
|
161
|
+
adminPort: number;
|
|
162
|
+
/** Base ILP address for this connector */
|
|
163
|
+
ilpAddress: string;
|
|
164
|
+
/** List of child peer entries (one per active node) */
|
|
165
|
+
peers: PeerEntry[];
|
|
166
|
+
/** Transport configuration */
|
|
167
|
+
transport: {
|
|
168
|
+
/**
|
|
169
|
+
* Operator-facing transport selection. Maps to the connector's
|
|
170
|
+
* discriminated union internally (mode='ator' → type='socks5').
|
|
171
|
+
*/
|
|
172
|
+
mode: 'ator' | 'direct';
|
|
173
|
+
/** SOCKS5 proxy URL (must be socks5h://) — required when mode='ator'. */
|
|
174
|
+
socksProxy?: string;
|
|
175
|
+
/**
|
|
176
|
+
* Externally reachable BTP URL the connector advertises to peers.
|
|
177
|
+
* Required when mode='ator' AND hiddenService is unset (operator runs
|
|
178
|
+
* their own anon binary externally). Ignored for mode='direct'.
|
|
179
|
+
* When hiddenService IS set and externalUrl is unset, the generator
|
|
180
|
+
* emits the literal "auto" so the connector resolves the .anyone
|
|
181
|
+
* hostname at startup.
|
|
182
|
+
*/
|
|
183
|
+
externalUrl?: string;
|
|
184
|
+
/** Inbound hidden-service publication (Story 35.5 path). */
|
|
185
|
+
hiddenService?: HiddenServiceConfig;
|
|
233
186
|
};
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Response from GET /health on the connector's healthCheckPort.
|
|
190
|
+
* Mirrors `HealthStatus` from `@toon-protocol/connector`.
|
|
191
|
+
*/
|
|
192
|
+
interface HealthResponse {
|
|
193
|
+
status: 'healthy' | 'unhealthy' | 'starting' | 'degraded';
|
|
194
|
+
uptime: number;
|
|
195
|
+
peersConnected: number;
|
|
196
|
+
totalPeers: number;
|
|
197
|
+
timestamp: string;
|
|
198
|
+
nodeId?: string;
|
|
199
|
+
version?: string;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Per-peer ILP counter snapshot from GET /admin/metrics.json.
|
|
203
|
+
* Mirrors `AdminMetricsJsonPeer` from `@toon-protocol/connector`.
|
|
204
|
+
*/
|
|
205
|
+
interface MetricsPeerEntry {
|
|
206
|
+
peerId: string;
|
|
207
|
+
connected: boolean;
|
|
208
|
+
packetsForwarded: number;
|
|
209
|
+
packetsRejected: number;
|
|
210
|
+
bytesSent: number;
|
|
211
|
+
/** Connector v3.7.0+ (toon-protocol/connector#73). Counts packets accepted
|
|
212
|
+
* via the self-delivery code path (route nextHop === connector's own
|
|
213
|
+
* nodeId). Older connectors omit this field — default to 0 at consumers. */
|
|
214
|
+
packetsLocallyDelivered?: number;
|
|
215
|
+
lastPacketAt: string | null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Response from GET /admin/metrics.json on the connector's adminApi port.
|
|
219
|
+
* Mirrors `AdminMetricsJsonResponse` from `@toon-protocol/connector`.
|
|
220
|
+
*/
|
|
221
|
+
interface MetricsResponse {
|
|
222
|
+
uptimeSeconds: number;
|
|
223
|
+
aggregate: {
|
|
224
|
+
packetsForwarded: number;
|
|
225
|
+
packetsRejected: number;
|
|
226
|
+
bytesSent: number;
|
|
227
|
+
/** Connector v3.7.0+ — see MetricsPeerEntry.packetsLocallyDelivered. */
|
|
228
|
+
packetsLocallyDelivered?: number;
|
|
237
229
|
};
|
|
230
|
+
peers: MetricsPeerEntry[];
|
|
231
|
+
timestamp: string;
|
|
238
232
|
}
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Peer entry from GET /admin/peers on the connector's adminApi port.
|
|
235
|
+
* Mirrors the response of the connector's `router.get('/peers', …)` handler.
|
|
236
|
+
* Note: per-peer packet counters live on /admin/metrics.json, not here.
|
|
237
|
+
*/
|
|
238
|
+
interface PeerStatus {
|
|
239
|
+
id: string;
|
|
240
|
+
connected: boolean;
|
|
241
|
+
ilpAddresses: string[];
|
|
242
|
+
routeCount: number;
|
|
245
243
|
}
|
|
246
|
-
/**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Wrapper response from GET /admin/peers.
|
|
246
|
+
* Mirrors the connector's `router.get('/peers')` JSON envelope.
|
|
247
|
+
*/
|
|
248
|
+
interface PeersResponse {
|
|
249
|
+
nodeId: string;
|
|
250
|
+
peerCount: number;
|
|
251
|
+
connectedCount: number;
|
|
252
|
+
peers: PeerStatus[];
|
|
251
253
|
}
|
|
252
|
-
|
|
253
254
|
/**
|
|
254
|
-
*
|
|
255
|
+
* Channel summary entry from GET /admin/channels on the connector's adminApi port.
|
|
256
|
+
* Mirrors `ChannelSummary` from `@toon-protocol/connector`
|
|
257
|
+
* (packages/connector/src/http/admin-api.ts ChannelSummary at v3.x).
|
|
255
258
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
259
|
+
* `status` is kept as `string` (not a union) because the connector's enum may
|
|
260
|
+
* grow without warning — the contract canary catches shape drift, not enum domain.
|
|
258
261
|
*/
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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;
|
|
262
|
+
interface ChannelSummary {
|
|
263
|
+
channelId: string;
|
|
264
|
+
peerId: string;
|
|
265
|
+
chain: string;
|
|
266
|
+
status: string;
|
|
267
|
+
deposit: string;
|
|
268
|
+
lastActivity: string;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Filter params for GET /packets on the connector admin API.
|
|
272
|
+
*
|
|
273
|
+
* Townhouse-Side Contract (§getPacketLog) — added in story 21.10.
|
|
274
|
+
* The connector must expose `GET /packets?ilpAddress=<>&since=<>&limit=<>`.
|
|
275
|
+
* If the running connector image does not expose this endpoint, the
|
|
276
|
+
* /nodes/:type/packets/timeseries route returns 503 and the contract canary
|
|
277
|
+
* will catch the drift. See packages/sdk/CONNECTOR_MIGRATION.md §Townhouse-Side Contract.
|
|
278
|
+
*/
|
|
279
|
+
interface PacketLogFilter {
|
|
280
|
+
ilpAddress?: string;
|
|
281
|
+
since?: number;
|
|
282
|
+
limit?: number;
|
|
283
|
+
}
|
|
284
|
+
/** A single packet log entry returned by GET /packets */
|
|
285
|
+
interface PacketLogEntry {
|
|
286
|
+
ts: number;
|
|
287
|
+
ilpAddressFrom: string;
|
|
288
|
+
ilpAddressTo: string;
|
|
289
|
+
amount: string;
|
|
290
|
+
result: 'fulfill' | 'reject' | 'timeout';
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Response from GET /admin/hs-hostname on the connector's adminApi port.
|
|
294
|
+
* Mirrors the connector v3.5.0+ `HsHostnameResponse` (Story 44.1 / AC FR35).
|
|
295
|
+
* Both fields are null while the .anyone hidden-service bootstrap is in
|
|
296
|
+
* progress; both become non-null once the descriptor is published.
|
|
297
|
+
*/
|
|
298
|
+
interface HsHostnameResponse {
|
|
299
|
+
hostname: string | null;
|
|
300
|
+
publishedAt: string | null;
|
|
322
301
|
}
|
|
323
302
|
|
|
324
303
|
/**
|
|
325
|
-
*
|
|
304
|
+
* Per-asset earnings row from GET /admin/earnings.json (endpoint added in connector v3.2.0).
|
|
305
|
+
* Mirrors `AdminEarningsByAsset` from `@toon-protocol/connector` admin-api.
|
|
326
306
|
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
|
|
331
|
-
|
|
307
|
+
* Cumulative amounts are decimal-string bigints (JSON-safe for any asset scale).
|
|
308
|
+
* `claimsReceivedTotal` tracks value received from the peer (they paid us);
|
|
309
|
+
* `claimsSentTotal` tracks value forwarded to the peer (they earned).
|
|
310
|
+
*/
|
|
311
|
+
interface AssetEarnings {
|
|
312
|
+
assetCode: string;
|
|
313
|
+
assetScale: number;
|
|
314
|
+
claimsReceivedTotal: string;
|
|
315
|
+
claimsSentTotal: string;
|
|
316
|
+
netBalance: string;
|
|
317
|
+
lastClaimAt: string | null;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Per-peer earnings entry from GET /admin/earnings.json.
|
|
321
|
+
* Mirrors `AdminEarningsJsonPeer` from
|
|
322
|
+
* `@toon-protocol/connector packages/connector/src/http/admin-api.ts:278-281`.
|
|
323
|
+
*/
|
|
324
|
+
interface PeerEarnings {
|
|
325
|
+
peerId: string;
|
|
326
|
+
byAsset: AssetEarnings[];
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Connector fee rollup from GET /admin/earnings.json.
|
|
330
|
+
* Mirrors `AdminEarningsConnectorFee` from
|
|
331
|
+
* `@toon-protocol/connector packages/connector/src/http/admin-api.ts:283-287`.
|
|
332
|
+
*/
|
|
333
|
+
interface ConnectorFeeEntry {
|
|
334
|
+
assetCode: string;
|
|
335
|
+
assetScale: number;
|
|
336
|
+
total: string;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* A single recent claim entry from GET /admin/earnings.json.
|
|
340
|
+
* Mirrors `AdminEarningsRecentClaim` from
|
|
341
|
+
* `@toon-protocol/connector packages/connector/src/http/admin-api.ts:289-296`.
|
|
342
|
+
*/
|
|
343
|
+
interface RecentClaim {
|
|
344
|
+
peerId: string;
|
|
345
|
+
assetCode: string;
|
|
346
|
+
assetScale: number;
|
|
347
|
+
amount: string;
|
|
348
|
+
direction: 'inbound' | 'outbound';
|
|
349
|
+
at: string;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Typed wrapper around the connector's wire-level `timestamp: string` field.
|
|
353
|
+
* The connector's `AdminEarningsJsonResponse` carries a plain ISO-8601 string
|
|
354
|
+
* (`packages/connector/src/http/admin-api.ts:298-304`). Townhouse wraps it as
|
|
355
|
+
* a value object so dashboard consumers have a typed handle and the field can
|
|
356
|
+
* be extended (`epoch?`, `timezone?`) without breaking callers.
|
|
332
357
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
358
|
+
* `ConnectorAdminClient.getEarnings()` performs the wrap on the way out:
|
|
359
|
+
* `timestamp: { iso: rawString }`.
|
|
335
360
|
*/
|
|
361
|
+
interface EarningsTimestamp {
|
|
362
|
+
iso: string;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Response from GET /admin/earnings.json on the connector's adminApi port.
|
|
366
|
+
* Mirrors `AdminEarningsJsonResponse` from
|
|
367
|
+
* `@toon-protocol/connector packages/connector/src/http/admin-api.ts:298-304`.
|
|
368
|
+
*
|
|
369
|
+
* NOTE: The connector's wire shape carries `timestamp: string`. Townhouse's
|
|
370
|
+
* `getEarnings()` adapts it to `timestamp: EarningsTimestamp` (value object).
|
|
371
|
+
*
|
|
372
|
+
* Returns HTTP 503 when the connector is started without settlement config
|
|
373
|
+
* (accountManager / claimReceiver not wired). Townhouse's apex always wires
|
|
374
|
+
* both; 503 in production indicates connector misconfiguration.
|
|
375
|
+
*/
|
|
376
|
+
interface EarningsResponse {
|
|
377
|
+
uptimeSeconds: number;
|
|
378
|
+
peers: PeerEarnings[];
|
|
379
|
+
connectorFees: ConnectorFeeEntry[];
|
|
380
|
+
recentClaims: RecentClaim[];
|
|
381
|
+
timestamp: EarningsTimestamp;
|
|
382
|
+
}
|
|
336
383
|
|
|
337
384
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
385
|
+
* Connector Admin Client for Townhouse (Story 21.3, contract aligned in 21.7.5).
|
|
386
|
+
*
|
|
387
|
+
* HTTP client for the connector's admin API endpoints. Paths and response
|
|
388
|
+
* shapes mirror the connector source-of-truth — see
|
|
389
|
+
* `@toon-protocol/connector` `packages/connector/src/http/{types,admin-api}.ts`.
|
|
390
|
+
*
|
|
391
|
+
* Uses Node.js native fetch (available in Node 20+).
|
|
392
|
+
*
|
|
393
|
+
* Two distinct HTTP servers live on the connector image:
|
|
394
|
+
* - healthCheckPort serves /health and /health/{live,ready}
|
|
395
|
+
* - adminApi.port serves /admin/* (peers, metrics.json, routes, channels, …)
|
|
396
|
+
*
|
|
397
|
+
* The base URL passed to this client must point at whichever server hosts
|
|
398
|
+
* the endpoint being called: pass the healthCheckPort base for `getHealth`
|
|
399
|
+
* and the adminApi.port base for `getPeers` / `getMetrics`. In practice
|
|
400
|
+
* Townhouse currently runs both ports on the same host, so callers either
|
|
401
|
+
* construct two clients or hit a shared base URL when the ports overlap.
|
|
340
402
|
*/
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
private
|
|
344
|
-
|
|
345
|
-
/** Path to the encrypted wallet file */
|
|
346
|
-
get encryptedPath(): string;
|
|
403
|
+
|
|
404
|
+
declare class ConnectorAdminClient {
|
|
405
|
+
private readonly baseUrl;
|
|
406
|
+
private readonly timeoutMs;
|
|
347
407
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
408
|
+
* @param baseUrl - Base URL for the connector admin API (e.g., 'http://localhost:9402')
|
|
409
|
+
* @param timeoutMs - Request timeout in milliseconds (default: 5000)
|
|
350
410
|
*/
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}>;
|
|
411
|
+
constructor(baseUrl: string, timeoutMs?: number);
|
|
412
|
+
/** Public read of the configured base URL (used by drill-command probes to derive a sibling client). */
|
|
413
|
+
getBaseUrl(): string;
|
|
355
414
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
415
|
+
* GET /health on the admin-API port — checks HTTP reachability of the
|
|
416
|
+
* connector without validating the rich HealthStatus shape. Use this from
|
|
417
|
+
* the drill-command health probe when only the admin URL is available
|
|
418
|
+
* (port 9401), not the healthCheckPort (8080). The admin server's /health
|
|
419
|
+
* returns `{status:'healthy', service:'admin-api', nodeId, timestamp}` —
|
|
420
|
+
* a different shape from `getHealth()`'s validator. This method returns
|
|
421
|
+
* a coarse status from a 200 response and reads `nodeId` if present.
|
|
422
|
+
*
|
|
423
|
+
* @throws Error when connector is unreachable or returns non-2xx.
|
|
358
424
|
*/
|
|
359
|
-
|
|
425
|
+
pingAdminLive(): Promise<{
|
|
426
|
+
status: 'healthy';
|
|
427
|
+
nodeId?: string;
|
|
428
|
+
}>;
|
|
360
429
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
430
|
+
* GET /health — returns the connector's HealthStatus from the healthCheckPort server.
|
|
431
|
+
*
|
|
432
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
363
433
|
*/
|
|
364
|
-
|
|
434
|
+
getHealth(): Promise<HealthResponse>;
|
|
365
435
|
/**
|
|
366
|
-
*
|
|
436
|
+
* GET /admin/hs-hostname — returns the connector's published .anyone hidden-service
|
|
437
|
+
* hostname (Epic 45 / Story 44.1). Returns 200 with {hostname, publishedAt} both
|
|
438
|
+
* possibly null while bootstrap is in progress, both non-null once anon publishes.
|
|
439
|
+
* Returns 503 when the connector is anon-disabled (anon.enabled: false in config).
|
|
440
|
+
*
|
|
441
|
+
* @throws Error('connector is anon-disabled (HTTP 503)') on 503 — caller can match
|
|
442
|
+
* on this exact prefix for actionable diagnostics.
|
|
443
|
+
* @throws Error on non-200/503 status, network error, or shape-validation failure.
|
|
367
444
|
*/
|
|
368
|
-
|
|
445
|
+
getHsHostname(): Promise<HsHostnameResponse>;
|
|
369
446
|
/**
|
|
370
|
-
*
|
|
447
|
+
* GET /admin/metrics.json — returns the connector's per-peer ILP counters
|
|
448
|
+
* with an aggregate rollup, mirroring `AdminMetricsJsonResponse`.
|
|
449
|
+
*
|
|
450
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
371
451
|
*/
|
|
372
|
-
|
|
452
|
+
getMetrics(): Promise<MetricsResponse>;
|
|
373
453
|
/**
|
|
374
|
-
*
|
|
375
|
-
*
|
|
454
|
+
* GET /admin/earnings.json — returns the connector's per-peer per-asset
|
|
455
|
+
* earnings projection, mirroring `AdminEarningsJsonResponse` (connector v3.2.0+).
|
|
456
|
+
*
|
|
457
|
+
* Source of truth: @toon-protocol/connector
|
|
458
|
+
* packages/connector/src/http/admin-api.ts:1864-1945
|
|
459
|
+
*
|
|
460
|
+
* Returns HTTP 503 when the connector is started without settlement config
|
|
461
|
+
* (accountManager / claimReceiver not wired). Townhouse's apex always wires
|
|
462
|
+
* both; 503 in production indicates connector misconfiguration.
|
|
463
|
+
*
|
|
464
|
+
* Wire-shape adaptation: the connector's `timestamp: string` field is
|
|
465
|
+
* wrapped into `{ iso: string }` on the way out (EarningsTimestamp).
|
|
466
|
+
*
|
|
467
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
376
468
|
*/
|
|
377
|
-
|
|
469
|
+
getEarnings(): Promise<EarningsResponse>;
|
|
378
470
|
/**
|
|
379
|
-
*
|
|
471
|
+
* GET /admin/peers — returns the connector's peer roster with route counts
|
|
472
|
+
* and ILP addresses. Returns the unwrapped peers array (the wrapper's
|
|
473
|
+
* nodeId / peerCount / connectedCount fields are dropped).
|
|
474
|
+
*
|
|
475
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
380
476
|
*/
|
|
381
|
-
|
|
477
|
+
getPeers(): Promise<PeerStatus[]>;
|
|
382
478
|
/**
|
|
383
|
-
*
|
|
479
|
+
* GET /admin/channels — returns the connector's payment-channel summaries
|
|
480
|
+
* across all registered chain providers. Multi-chain: one entry per channel
|
|
481
|
+
* regardless of chain.
|
|
482
|
+
*
|
|
483
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
384
484
|
*/
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
485
|
+
getChannels(): Promise<ChannelSummary[]>;
|
|
486
|
+
/**
|
|
487
|
+
* POST /admin/peers — register (or re-register, idempotent) a child peer
|
|
488
|
+
* with the connector. Used by the boot reconciler (Story 46.1) to
|
|
489
|
+
* re-register peers present in `nodes.yaml` but missing from the
|
|
490
|
+
* connector's runtime peer roster (e.g., after a connector restart).
|
|
491
|
+
*
|
|
492
|
+
* The connector's POST /admin/peers handler treats a POST whose `id`
|
|
493
|
+
* matches an existing peer as a re-registration (no-op for the peer
|
|
494
|
+
* itself; routes are appended). A POST with a new `id` triggers
|
|
495
|
+
* `addPeer()` and BTP connection setup.
|
|
496
|
+
*
|
|
497
|
+
* @param input.id - peer identifier (matches `nodes.yaml`'s `peerId` and
|
|
498
|
+
* the connector's `PeerStatus.id`).
|
|
499
|
+
* @param input.url - BTP WebSocket URL the connector dials. MUST start
|
|
500
|
+
* with `ws://` or `wss://` (the connector validates this).
|
|
501
|
+
* @param input.authToken - shared auth token; pass empty string for
|
|
502
|
+
* internal Townhouse peers (no auth).
|
|
503
|
+
* @param input.routes - optional ILP route prefixes to register against
|
|
504
|
+
* this peer. The reconciler passes the peer's ilpAddress.
|
|
505
|
+
* @param input.transport - optional per-peer transport selection
|
|
506
|
+
* (connector >= 3.6.2). `'direct'` forces the connector to bypass the
|
|
507
|
+
* global SOCKS5 transport for this peer, even when the apex itself
|
|
508
|
+
* runs in `transport.type: socks5` mode. Required for Docker-sibling
|
|
509
|
+
* peers in HS mode — the anon SOCKS5 proxy cannot resolve internal
|
|
510
|
+
* Docker hostnames. When omitted, the peer inherits the connector's
|
|
511
|
+
* global transport (back-compat with pre-3.6.2 connectors).
|
|
512
|
+
*
|
|
513
|
+
* @throws Error on non-2xx response, timeout, or connection refused.
|
|
514
|
+
*/
|
|
515
|
+
registerPeer(input: {
|
|
516
|
+
id: string;
|
|
517
|
+
url: string;
|
|
518
|
+
authToken: string;
|
|
519
|
+
routes?: {
|
|
520
|
+
prefix: string;
|
|
521
|
+
priority?: number;
|
|
522
|
+
}[];
|
|
523
|
+
transport?: 'direct' | 'socks5';
|
|
524
|
+
relation?: 'parent' | 'peer' | 'child';
|
|
525
|
+
}): Promise<void>;
|
|
526
|
+
/**
|
|
527
|
+
* DELETE /admin/peers/:peerId?removeRoutes=true — deregister a child peer.
|
|
528
|
+
*
|
|
529
|
+
* Idempotent: a 404 from the connector (peer already removed) is treated as
|
|
530
|
+
* success so callers can safely use this as a rollback step without knowing
|
|
531
|
+
* whether the peer was ever registered.
|
|
532
|
+
*
|
|
533
|
+
* `removeRoutes=true` is always sent so the connector drops the ILP routing
|
|
534
|
+
* entries for this peer along with the BTP connection config.
|
|
535
|
+
*
|
|
536
|
+
* @throws Error on empty peerId (rejected at client, no network request made)
|
|
537
|
+
* @throws Error on non-2xx/404 response, timeout, or connection refused
|
|
538
|
+
*/
|
|
539
|
+
removePeer(peerId: string): Promise<void>;
|
|
540
|
+
/**
|
|
541
|
+
* GET /packets — returns the connector's raw packet log filtered by the
|
|
542
|
+
* given criteria. Used by the timeseries aggregation route (story 21.10).
|
|
543
|
+
*
|
|
544
|
+
* Townhouse-Side Contract: see packages/sdk/CONNECTOR_MIGRATION.md §getPacketLog.
|
|
545
|
+
* If the connector image does not expose GET /packets, this method throws
|
|
546
|
+
* with a `ConnectorEndpointNotFound` error code so the route can return 503.
|
|
547
|
+
*
|
|
548
|
+
* @throws Error with code='ConnectorEndpointNotFound' when connector returns 404
|
|
549
|
+
* @throws Error when connector is not running, returns non-200, or shape is invalid
|
|
550
|
+
*/
|
|
551
|
+
getPacketLog(filter?: PacketLogFilter): Promise<PacketLogEntry[]>;
|
|
552
|
+
/**
|
|
553
|
+
* Perform an HTTP GET request to the connector admin API.
|
|
554
|
+
* Wraps fetch with error handling for connection refused and non-200 responses.
|
|
555
|
+
*/
|
|
556
|
+
private fetch;
|
|
557
|
+
}
|
|
426
558
|
|
|
427
559
|
/**
|
|
428
560
|
* Docker Orchestration Engine for Townhouse (Story 21.2).
|
|
@@ -432,6 +564,33 @@ declare function loadWallet(path: string): Promise<{
|
|
|
432
564
|
* Uses dockerode for programmatic Docker control with DI for testability.
|
|
433
565
|
*/
|
|
434
566
|
|
|
567
|
+
interface RunDockerOptions {
|
|
568
|
+
timeout?: number;
|
|
569
|
+
maxBuffer?: number;
|
|
570
|
+
inheritStdio?: boolean;
|
|
571
|
+
/** Override the subprocess env. Defaults to process.env when omitted. */
|
|
572
|
+
env?: NodeJS.ProcessEnv;
|
|
573
|
+
}
|
|
574
|
+
type ExecFileAsyncSignature = (file: string, args: readonly string[], options?: RunDockerOptions) => Promise<{
|
|
575
|
+
stdout: string;
|
|
576
|
+
stderr: string;
|
|
577
|
+
}>;
|
|
578
|
+
/**
|
|
579
|
+
* Error thrown by DockerOrchestrator HS-path failures (Story 45.3).
|
|
580
|
+
* Carries the failed-service name + subprocess diagnostics so CLI consumers
|
|
581
|
+
* (Story 45.4) can render Sally's failure-state copy library (UX-DR5).
|
|
582
|
+
*/
|
|
583
|
+
declare class OrchestratorError extends Error {
|
|
584
|
+
readonly service?: string;
|
|
585
|
+
readonly exitCode?: number;
|
|
586
|
+
readonly stderr?: string;
|
|
587
|
+
constructor(message: string, options?: {
|
|
588
|
+
service?: string;
|
|
589
|
+
exitCode?: number;
|
|
590
|
+
stderr?: string;
|
|
591
|
+
cause?: Error;
|
|
592
|
+
});
|
|
593
|
+
}
|
|
435
594
|
/**
|
|
436
595
|
* DockerOrchestrator manages the lifecycle of Townhouse containers.
|
|
437
596
|
*
|
|
@@ -446,15 +605,47 @@ declare class DockerOrchestrator extends EventEmitter {
|
|
|
446
605
|
private readonly walletManager;
|
|
447
606
|
private activeNodes;
|
|
448
607
|
private readonly statsCache;
|
|
449
|
-
|
|
608
|
+
private readonly profile;
|
|
609
|
+
private readonly composePath;
|
|
610
|
+
private readonly execFileAsync;
|
|
611
|
+
private readonly adminClientFactory;
|
|
612
|
+
constructor(docker: Docker, config: TownhouseConfig, walletManager?: WalletManager, options?: {
|
|
613
|
+
profile?: ComposeProfile;
|
|
614
|
+
composePath?: string;
|
|
615
|
+
execFileAsync?: ExecFileAsyncSignature;
|
|
616
|
+
adminClientFactory?: (baseUrl: string, timeoutMs: number) => ConnectorAdminClient;
|
|
617
|
+
});
|
|
450
618
|
/**
|
|
451
|
-
* Orchestrate full startup sequence:
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
* 3. Start connector, wait for health
|
|
455
|
-
* 4. Start enabled node containers in parallel
|
|
619
|
+
* Orchestrate full startup sequence. Branches on profile:
|
|
620
|
+
* - 'dev' (default): dockerode-based, preserves existing dev-stack behavior
|
|
621
|
+
* - 'hs': docker compose subprocess + HS hostname readiness gate
|
|
456
622
|
*/
|
|
457
623
|
up(profiles: NodeType$1[]): Promise<void>;
|
|
624
|
+
private upDev;
|
|
625
|
+
/**
|
|
626
|
+
* Narrow `this.composePath` to a definite string. The constructor enforces
|
|
627
|
+
* this invariant for `profile: 'hs'`; this helper exists so the HS-path
|
|
628
|
+
* methods don't need a non-null assertion (lint-clean) and so a constructor
|
|
629
|
+
* regression surfaces as an `OrchestratorError` rather than a `TypeError`.
|
|
630
|
+
*/
|
|
631
|
+
private requireComposePath;
|
|
632
|
+
/**
|
|
633
|
+
* validate that composePath is absolute and exists on disk before
|
|
634
|
+
* passing it to any subprocess call. Defence-in-depth — callers pass paths
|
|
635
|
+
* from materializeComposeTemplate so this should never fire in normal use.
|
|
636
|
+
*/
|
|
637
|
+
private validateComposePath;
|
|
638
|
+
/** HS-mode startup: shell out to `docker compose up -d`, wait for HS hostname. */
|
|
639
|
+
private upHs;
|
|
640
|
+
/**
|
|
641
|
+
* Parse Docker Compose stderr for failed-service names and emit a
|
|
642
|
+
* containerState event per failed service so callers see the failure via
|
|
643
|
+
* the same channel dev-mode uses (AC #6 — "for each failed service
|
|
644
|
+
* identified, it emits..."). When no pattern matches, emit a single
|
|
645
|
+
* fallback event with name `'compose-up'`.
|
|
646
|
+
*/
|
|
647
|
+
private surfaceComposeFailure;
|
|
648
|
+
private waitForHsHostname;
|
|
458
649
|
/**
|
|
459
650
|
* Regenerate connector config and restart the connector container
|
|
460
651
|
* with updated environment variables (peer list).
|
|
@@ -473,12 +664,13 @@ declare class DockerOrchestrator extends EventEmitter {
|
|
|
473
664
|
*/
|
|
474
665
|
removeNode(type: NodeType$1): Promise<void>;
|
|
475
666
|
/**
|
|
476
|
-
* Graceful shutdown
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* 3. Remove network
|
|
667
|
+
* Graceful shutdown. Branches on profile:
|
|
668
|
+
* - 'dev' (default): dockerode-based teardown
|
|
669
|
+
* - 'hs': docker compose subprocess
|
|
480
670
|
*/
|
|
481
671
|
down(): Promise<void>;
|
|
672
|
+
private downDev;
|
|
673
|
+
private downHs;
|
|
482
674
|
/**
|
|
483
675
|
* Resolve the Nostr relay WebSocket URL for a Town node instance.
|
|
484
676
|
*
|
|
@@ -529,6 +721,39 @@ declare class DockerOrchestrator extends EventEmitter {
|
|
|
529
721
|
* Emits pullProgress events during download.
|
|
530
722
|
*/
|
|
531
723
|
pullImages(profiles: NodeType$1[]): Promise<void>;
|
|
724
|
+
/**
|
|
725
|
+
* Pull a single image by its reference (tag or digest form).
|
|
726
|
+
*
|
|
727
|
+
* Skips the pull when the image already exists locally (matches against
|
|
728
|
+
* both RepoTags and RepoDigests so digest-form refs like
|
|
729
|
+
* `ghcr.io/toon-protocol/town@sha256:abc...` are found correctly).
|
|
730
|
+
* Throws `OrchestratorError` on pull failure.
|
|
731
|
+
*/
|
|
732
|
+
pullImage(image: string): Promise<void>;
|
|
733
|
+
/**
|
|
734
|
+
* Start a child peer node via `docker compose --profile <type> up -d <type>`.
|
|
735
|
+
*
|
|
736
|
+
* HS-profile only — throws `OrchestratorError` when called on the dev profile.
|
|
737
|
+
*
|
|
738
|
+
* The `env` parameter supplies the per-node wallet secrets (e.g.
|
|
739
|
+
* `TOWN_SECRET_KEY`, `MILL_MNEMONIC`). It is layered on top of `process.env`
|
|
740
|
+
* so that PATH, HOME, and other process-level env vars are preserved for the
|
|
741
|
+
* docker CLI subprocess.
|
|
742
|
+
*
|
|
743
|
+
* Logging guard: the caller (nodes-lifecycle route) must NOT log the `env`
|
|
744
|
+
* argument — it contains secret keys and the wallet mnemonic.
|
|
745
|
+
*/
|
|
746
|
+
startNodeViaCompose(type: NodeType$1, env: Record<string, string>): Promise<void>;
|
|
747
|
+
/**
|
|
748
|
+
* Stop and remove a child peer node via `docker compose stop` + `rm -f`.
|
|
749
|
+
*
|
|
750
|
+
* HS-profile only — throws `OrchestratorError` when called on the dev profile.
|
|
751
|
+
* Idempotent: stderr patterns indicating the service/container is already gone
|
|
752
|
+
* (`'no such service'`, `'no containers to remove'`, `'No such container'`)
|
|
753
|
+
* are treated as success so callers can run this as a rollback without
|
|
754
|
+
* worrying about the container's prior state.
|
|
755
|
+
*/
|
|
756
|
+
stopNodeViaCompose(type: NodeType$1): Promise<void>;
|
|
532
757
|
/**
|
|
533
758
|
* Poll container health status via inspect().
|
|
534
759
|
* Retries at configurable interval, throws on timeout.
|
|
@@ -565,217 +790,37 @@ declare class DockerOrchestrator extends EventEmitter {
|
|
|
565
790
|
* forwarding inbound traffic to the town container's Nostr WebSocket port.
|
|
566
791
|
*
|
|
567
792
|
* 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
|
-
|
|
591
|
-
|
|
592
|
-
*
|
|
593
|
-
*/
|
|
594
|
-
private
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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';
|
|
793
|
+
* entrypoint writes the `hostname` file on first boot (see
|
|
794
|
+
* docker/townhouse-ator-sidecar/Dockerfile). The town container picks up
|
|
795
|
+
* the resulting .anyone URL via the operator-set externalUrl field.
|
|
796
|
+
*/
|
|
797
|
+
private startRelayAtorSidecar;
|
|
798
|
+
/**
|
|
799
|
+
* Stop and remove a single container.
|
|
800
|
+
*/
|
|
801
|
+
private stopAndRemove;
|
|
802
|
+
/**
|
|
803
|
+
* Remove the townhouse-net network if it exists.
|
|
804
|
+
*/
|
|
805
|
+
private removeNetwork;
|
|
806
|
+
/**
|
|
807
|
+
* Build environment variables for the connector container.
|
|
808
|
+
* Delegates to ConnectorConfigGenerator for consistent config generation.
|
|
809
|
+
*/
|
|
810
|
+
private buildConnectorEnv;
|
|
811
|
+
/**
|
|
812
|
+
* Build environment variables for a node container.
|
|
813
|
+
* If a WalletManager is provided, injects per-node identity keys.
|
|
814
|
+
*
|
|
815
|
+
* Async because the DVM path may need to derive an RSA-4096 Arweave key
|
|
816
|
+
* via `walletManager.ensureArweaveKey('dvm')` — that derivation takes
|
|
817
|
+
* 5–30s on first call per unlocked wallet (cached thereafter).
|
|
818
|
+
*/
|
|
819
|
+
private buildNodeEnv;
|
|
820
|
+
/**
|
|
821
|
+
* Follow a Docker pull stream and emit progress events.
|
|
822
|
+
*/
|
|
823
|
+
private followPullProgress;
|
|
779
824
|
}
|
|
780
825
|
|
|
781
826
|
/**
|
|
@@ -853,74 +898,6 @@ declare class ConnectorConfigGenerator {
|
|
|
853
898
|
private generateTransportConfig;
|
|
854
899
|
}
|
|
855
900
|
|
|
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
901
|
/**
|
|
925
902
|
* ATOR transport probe — periodically TCP-connects to the configured SOCKS5
|
|
926
903
|
* proxy host:port and measures direct HTTPS latency for comparison.
|
|
@@ -970,36 +947,6 @@ declare class TransportProbe {
|
|
|
970
947
|
private logTransition;
|
|
971
948
|
}
|
|
972
949
|
|
|
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
950
|
/**
|
|
1004
951
|
* Townhouse API — type definitions.
|
|
1005
952
|
*/
|
|
@@ -1407,4 +1354,656 @@ interface WizardInitialDeps {
|
|
|
1407
1354
|
*/
|
|
1408
1355
|
declare function createWizardApiServer(initialDeps: WizardInitialDeps): Promise<ApiServer>;
|
|
1409
1356
|
|
|
1410
|
-
|
|
1357
|
+
/**
|
|
1358
|
+
* Hourly earnings snapshot writer (Story 47.3).
|
|
1359
|
+
*
|
|
1360
|
+
* Persists `claimsReceivedTotal` per (peerId × assetCode) — plus apex
|
|
1361
|
+
* `connectorFees[]` rows under `peerId: '__apex__'` — to
|
|
1362
|
+
* `${dirname(configPath)}/earnings-snapshots.jsonl` once per hour. Consumed
|
|
1363
|
+
* by `snapshot-reader.ts`'s `DeltaComputer` factory. Failure mode: any
|
|
1364
|
+
* per-tick error is logged via `logger.warn` and swallowed (the writer NEVER
|
|
1365
|
+
* throws into the apex event loop) — the next tick retries cleanly. Pruning
|
|
1366
|
+
* runs after each successful append (entries older than 13 months are
|
|
1367
|
+
* rewritten atomically). File mode is `0o600` on every write.
|
|
1368
|
+
*
|
|
1369
|
+
* @module
|
|
1370
|
+
* @since 47.3
|
|
1371
|
+
*/
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* One JSONL row in `earnings-snapshots.jsonl`.
|
|
1375
|
+
*
|
|
1376
|
+
* NOTE: apex routing-fee rows use `peerId: '__apex__'`. The field name
|
|
1377
|
+
* `claimsReceivedTotal` is technically a misnomer for apex rows — those
|
|
1378
|
+
* are connector routing fees, not received claims — but the uniform column
|
|
1379
|
+
* name keeps the JSONL schema simple and the reader doesn't need a special
|
|
1380
|
+
* case.
|
|
1381
|
+
*/
|
|
1382
|
+
interface SnapshotEntry {
|
|
1383
|
+
/** ISO-8601 UTC timestamp of the tick boundary (e.g. '2026-05-12T15:00:00.000Z'). */
|
|
1384
|
+
ts: string;
|
|
1385
|
+
/** Connector peerId, OR the literal `'__apex__'` for apex routing-fee rows. */
|
|
1386
|
+
peerId: string;
|
|
1387
|
+
assetCode: string;
|
|
1388
|
+
/** Decimal-string cumulative (claims received for peers, routing-fee total for apex). */
|
|
1389
|
+
claimsReceivedTotal: string;
|
|
1390
|
+
}
|
|
1391
|
+
interface SnapshotWriterOptions {
|
|
1392
|
+
connectorAdmin: ConnectorAdminClient;
|
|
1393
|
+
/** Absolute path to `earnings-snapshots.jsonl`. */
|
|
1394
|
+
snapshotPath: string;
|
|
1395
|
+
/** Tick interval (ms). Default 3_600_000 (1 hour). */
|
|
1396
|
+
tickIntervalMs?: number;
|
|
1397
|
+
/** Injected clock for tests. Default `() => new Date()`. */
|
|
1398
|
+
now?: () => Date;
|
|
1399
|
+
/** Retention window in months. Default 13. */
|
|
1400
|
+
retentionMonths?: number;
|
|
1401
|
+
/** pino/Fastify-compatible logger; warn-only. */
|
|
1402
|
+
logger?: {
|
|
1403
|
+
warn(obj: object, msg?: string): void;
|
|
1404
|
+
};
|
|
1405
|
+
/**
|
|
1406
|
+
* Fire one tick immediately on `start()` instead of waiting for the first
|
|
1407
|
+
* interval. Default `false` (production). Tests set this to `true` to
|
|
1408
|
+
* assert append behavior without advancing fake timers.
|
|
1409
|
+
*/
|
|
1410
|
+
fireOnStart?: boolean;
|
|
1411
|
+
}
|
|
1412
|
+
declare class SnapshotWriter {
|
|
1413
|
+
private readonly opts;
|
|
1414
|
+
private timer;
|
|
1415
|
+
private tickPending;
|
|
1416
|
+
constructor(opts: SnapshotWriterOptions);
|
|
1417
|
+
start(): void;
|
|
1418
|
+
stop(): void;
|
|
1419
|
+
/** Exposed for test ergonomics — runs one full append+prune cycle. */
|
|
1420
|
+
tick(): Promise<void>;
|
|
1421
|
+
private runTick;
|
|
1422
|
+
private appendEntries;
|
|
1423
|
+
private pruneIfNeeded;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* `nodes.yaml` schema + read/write helpers (Story 46.1).
|
|
1428
|
+
*
|
|
1429
|
+
* `~/.townhouse/nodes.yaml` is the operator-managed source of truth for
|
|
1430
|
+
* enabled child nodes. The reconciler (see `../reconciler.ts`) converges
|
|
1431
|
+
* connector peer state to this file on every `townhouse hs up`.
|
|
1432
|
+
*
|
|
1433
|
+
* Architectural rule (Epic 46.2 dependency): yaml writes happen BEFORE
|
|
1434
|
+
* connector registration. The drift window resolves in the safe direction —
|
|
1435
|
+
* a yaml entry without a connector peer is re-registered on next boot; a
|
|
1436
|
+
* connector peer without a yaml entry is treated as `'external'` and left
|
|
1437
|
+
* alone.
|
|
1438
|
+
*/
|
|
1439
|
+
|
|
1440
|
+
declare const NodesYamlEntrySchema: z.ZodObject<{
|
|
1441
|
+
id: z.ZodString;
|
|
1442
|
+
type: z.ZodEnum<["town", "mill", "dvm"]>;
|
|
1443
|
+
peerId: z.ZodString;
|
|
1444
|
+
ilpAddress: z.ZodString;
|
|
1445
|
+
derivationIndex: z.ZodNumber;
|
|
1446
|
+
enabledAt: z.ZodString;
|
|
1447
|
+
lastSeenAt: z.ZodNullable<z.ZodString>;
|
|
1448
|
+
}, "strict", z.ZodTypeAny, {
|
|
1449
|
+
type: "town" | "mill" | "dvm";
|
|
1450
|
+
id: string;
|
|
1451
|
+
peerId: string;
|
|
1452
|
+
ilpAddress: string;
|
|
1453
|
+
derivationIndex: number;
|
|
1454
|
+
enabledAt: string;
|
|
1455
|
+
lastSeenAt: string | null;
|
|
1456
|
+
}, {
|
|
1457
|
+
type: "town" | "mill" | "dvm";
|
|
1458
|
+
id: string;
|
|
1459
|
+
peerId: string;
|
|
1460
|
+
ilpAddress: string;
|
|
1461
|
+
derivationIndex: number;
|
|
1462
|
+
enabledAt: string;
|
|
1463
|
+
lastSeenAt: string | null;
|
|
1464
|
+
}>;
|
|
1465
|
+
declare const NodesYamlSchema: z.ZodEffects<z.ZodObject<{
|
|
1466
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
1467
|
+
id: z.ZodString;
|
|
1468
|
+
type: z.ZodEnum<["town", "mill", "dvm"]>;
|
|
1469
|
+
peerId: z.ZodString;
|
|
1470
|
+
ilpAddress: z.ZodString;
|
|
1471
|
+
derivationIndex: z.ZodNumber;
|
|
1472
|
+
enabledAt: z.ZodString;
|
|
1473
|
+
lastSeenAt: z.ZodNullable<z.ZodString>;
|
|
1474
|
+
}, "strict", z.ZodTypeAny, {
|
|
1475
|
+
type: "town" | "mill" | "dvm";
|
|
1476
|
+
id: string;
|
|
1477
|
+
peerId: string;
|
|
1478
|
+
ilpAddress: string;
|
|
1479
|
+
derivationIndex: number;
|
|
1480
|
+
enabledAt: string;
|
|
1481
|
+
lastSeenAt: string | null;
|
|
1482
|
+
}, {
|
|
1483
|
+
type: "town" | "mill" | "dvm";
|
|
1484
|
+
id: string;
|
|
1485
|
+
peerId: string;
|
|
1486
|
+
ilpAddress: string;
|
|
1487
|
+
derivationIndex: number;
|
|
1488
|
+
enabledAt: string;
|
|
1489
|
+
lastSeenAt: string | null;
|
|
1490
|
+
}>, "many">;
|
|
1491
|
+
}, "strict", z.ZodTypeAny, {
|
|
1492
|
+
entries: {
|
|
1493
|
+
type: "town" | "mill" | "dvm";
|
|
1494
|
+
id: string;
|
|
1495
|
+
peerId: string;
|
|
1496
|
+
ilpAddress: string;
|
|
1497
|
+
derivationIndex: number;
|
|
1498
|
+
enabledAt: string;
|
|
1499
|
+
lastSeenAt: string | null;
|
|
1500
|
+
}[];
|
|
1501
|
+
}, {
|
|
1502
|
+
entries: {
|
|
1503
|
+
type: "town" | "mill" | "dvm";
|
|
1504
|
+
id: string;
|
|
1505
|
+
peerId: string;
|
|
1506
|
+
ilpAddress: string;
|
|
1507
|
+
derivationIndex: number;
|
|
1508
|
+
enabledAt: string;
|
|
1509
|
+
lastSeenAt: string | null;
|
|
1510
|
+
}[];
|
|
1511
|
+
}>, {
|
|
1512
|
+
entries: {
|
|
1513
|
+
type: "town" | "mill" | "dvm";
|
|
1514
|
+
id: string;
|
|
1515
|
+
peerId: string;
|
|
1516
|
+
ilpAddress: string;
|
|
1517
|
+
derivationIndex: number;
|
|
1518
|
+
enabledAt: string;
|
|
1519
|
+
lastSeenAt: string | null;
|
|
1520
|
+
}[];
|
|
1521
|
+
}, {
|
|
1522
|
+
entries: {
|
|
1523
|
+
type: "town" | "mill" | "dvm";
|
|
1524
|
+
id: string;
|
|
1525
|
+
peerId: string;
|
|
1526
|
+
ilpAddress: string;
|
|
1527
|
+
derivationIndex: number;
|
|
1528
|
+
enabledAt: string;
|
|
1529
|
+
lastSeenAt: string | null;
|
|
1530
|
+
}[];
|
|
1531
|
+
}>;
|
|
1532
|
+
type NodesYamlEntry = z.infer<typeof NodesYamlEntrySchema>;
|
|
1533
|
+
type NodesYaml = z.infer<typeof NodesYamlSchema>;
|
|
1534
|
+
/**
|
|
1535
|
+
* Read and validate `nodes.yaml` at the given path.
|
|
1536
|
+
*
|
|
1537
|
+
* Returns `{ entries: [] }` if the file does not exist (graceful first-run).
|
|
1538
|
+
* Throws a `ZodError` with a useful path if the file is present but invalid.
|
|
1539
|
+
*/
|
|
1540
|
+
declare function readNodesYaml(path: string): Promise<NodesYaml>;
|
|
1541
|
+
/**
|
|
1542
|
+
* Write `nodes.yaml` atomically with file mode `0o600`.
|
|
1543
|
+
*
|
|
1544
|
+
* Atomic = write to `<path>.tmp` then `fs.rename`. On POSIX, rename is
|
|
1545
|
+
* atomic when source + destination live on the same filesystem (always true
|
|
1546
|
+
* for `~/.townhouse/nodes.yaml`). Prevents partial-write corruption if the
|
|
1547
|
+
* process is killed mid-write.
|
|
1548
|
+
*/
|
|
1549
|
+
declare function writeNodesYaml(path: string, data: NodesYaml): Promise<void>;
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* `PeerTypeResolver` (Story 46.1).
|
|
1553
|
+
*
|
|
1554
|
+
* The connector is a generic ILP router — it has no concept of
|
|
1555
|
+
* `'town' | 'mill' | 'dvm'`. Townhouse owns the type concept entirely
|
|
1556
|
+
* via this resolver, which is the single translation layer between
|
|
1557
|
+
* connector `peerId` values and operator-meaningful node types.
|
|
1558
|
+
*
|
|
1559
|
+
* Architectural rule (Epic 46 planning §Architectural Layering):
|
|
1560
|
+
* downstream consumers (Epic 47 aggregator, Epic 48 TUI, Epic 49 telemetry)
|
|
1561
|
+
* MUST call through this resolver — they never hardcode peer-to-type
|
|
1562
|
+
* mappings.
|
|
1563
|
+
*
|
|
1564
|
+
* The resolver is rebuilt from a `NodesYaml` snapshot — prefer immutable
|
|
1565
|
+
* rebuild (re-instantiate) over mutable update for testability.
|
|
1566
|
+
*/
|
|
1567
|
+
|
|
1568
|
+
declare class PeerTypeResolver {
|
|
1569
|
+
private readonly map;
|
|
1570
|
+
constructor(yaml: NodesYaml);
|
|
1571
|
+
/**
|
|
1572
|
+
* Resolve a connector `peerId` to its operator-declared node type.
|
|
1573
|
+
* Returns `'external'` for unknown peerIds (legitimate non-Townhouse
|
|
1574
|
+
* peers running through the same connector).
|
|
1575
|
+
*/
|
|
1576
|
+
resolvePeerType(peerId: string): NodeType$1 | 'external';
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Earnings aggregator (Story 47.2).
|
|
1581
|
+
*
|
|
1582
|
+
* Aggregates connector-reported earnings into the canonical
|
|
1583
|
+
* `{ status, apex, peers }` shape consumed by the host-API
|
|
1584
|
+
* `/api/earnings` endpoint.
|
|
1585
|
+
*
|
|
1586
|
+
* Source of truth: `connectorAdmin.getEarnings()` (Story 47.1).
|
|
1587
|
+
* Peer-type attribution via `PeerTypeResolver` (Story 46.1); the resolver
|
|
1588
|
+
* buckets unmatched peerIds as `'external'` (enforcement lives in the
|
|
1589
|
+
* resolver, not here — we trust its contract).
|
|
1590
|
+
*
|
|
1591
|
+
* Failure mode: if `getEarnings()` throws (network, 503-when-disabled,
|
|
1592
|
+
* shape drift), returns the empty payload with
|
|
1593
|
+
* `status: 'connector_unavailable'`. The route returns 200 either way;
|
|
1594
|
+
* operators see zeros plus a UI banner rather than a 5xx. An injected
|
|
1595
|
+
* `logger.warn` (Fastify / pino-compatible) is called on failure so ops
|
|
1596
|
+
* can distinguish "connector outage" from "no earnings yet."
|
|
1597
|
+
*
|
|
1598
|
+
* @module
|
|
1599
|
+
* @since 47.2
|
|
1600
|
+
*/
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* Per-asset cumulative + delta breakdown. `lifetime` is the connector's
|
|
1604
|
+
* cumulative `claimsReceivedTotal` (decimal-string bigint at `assetScale`
|
|
1605
|
+
* decimals). `today` / `month` / `year` are deltas computed by Story 47.3's
|
|
1606
|
+
* snapshot-reader; until the `deltaComputer` dep is provided, they stub
|
|
1607
|
+
* to '0'. Asset-scale interpretation (USD: 6, ETH: 18, sats: 0) is the
|
|
1608
|
+
* dashboard's job — the aggregator never collapses to a unit.
|
|
1609
|
+
*/
|
|
1610
|
+
interface PerAsset {
|
|
1611
|
+
lifetime: string;
|
|
1612
|
+
today: string;
|
|
1613
|
+
month: string;
|
|
1614
|
+
year: string;
|
|
1615
|
+
}
|
|
1616
|
+
/** Per-peer earnings entry in the aggregator output. */
|
|
1617
|
+
interface NodeEarnings {
|
|
1618
|
+
id: string;
|
|
1619
|
+
type: NodeType$1 | 'external';
|
|
1620
|
+
byAsset: Record<string, PerAsset>;
|
|
1621
|
+
/** Max `lastClaimAt` across this peer's assets, or `null` if none. Added in 47.4. */
|
|
1622
|
+
lastClaimAt: string | null;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Wire-level status for the aggregator response.
|
|
1626
|
+
*
|
|
1627
|
+
* `'ok'` — `getEarnings()` succeeded; payload reflects connector state.
|
|
1628
|
+
* `'connector_unavailable'` — `getEarnings()` threw (network, 503, shape
|
|
1629
|
+
* drift); apex + peers are empty. The dashboard renders a banner.
|
|
1630
|
+
*/
|
|
1631
|
+
type AggregatedEarningsStatus = 'ok' | 'connector_unavailable';
|
|
1632
|
+
/** Top-level aggregator output. Extended in 47.4 with dashboard fields. */
|
|
1633
|
+
interface AggregatedEarnings {
|
|
1634
|
+
status: AggregatedEarningsStatus;
|
|
1635
|
+
apex: {
|
|
1636
|
+
routingFees: Record<string, PerAsset>;
|
|
1637
|
+
};
|
|
1638
|
+
peers: NodeEarnings[];
|
|
1639
|
+
/** Pass-through from connector `recentClaims`. Empty array on connector outage. */
|
|
1640
|
+
recentClaims: RecentClaim[];
|
|
1641
|
+
/** Sum of `getMetrics().peers[].packetsForwarded` PLUS `packetsLocallyDelivered`
|
|
1642
|
+
* (connector v3.7.0+, toon-protocol/connector#73 — counts events that landed
|
|
1643
|
+
* via the self-delivery route, where the connector's in-process relay accepts
|
|
1644
|
+
* the event locally rather than forwarding to a remote peer). 0 on connector
|
|
1645
|
+
* outage or metrics failure. */
|
|
1646
|
+
eventsRelayed: number;
|
|
1647
|
+
/** From `getMetrics().uptimeSeconds`. 0 on connector outage or metrics failure. */
|
|
1648
|
+
uptimeSeconds: number;
|
|
1649
|
+
}
|
|
1650
|
+
/** Resolves TODAY / MONTH / YEAR deltas for a (scope, assetCode) tuple. */
|
|
1651
|
+
type DeltaComputer = (params: {
|
|
1652
|
+
/** Either a connector peerId or the literal `'__apex__'` for routing-fee rows. */
|
|
1653
|
+
scope: string;
|
|
1654
|
+
assetCode: string;
|
|
1655
|
+
/** Current cumulative (matches the lifetime value in the response). */
|
|
1656
|
+
currentLifetime: string;
|
|
1657
|
+
}) => Promise<{
|
|
1658
|
+
today: string;
|
|
1659
|
+
month: string;
|
|
1660
|
+
year: string;
|
|
1661
|
+
}>;
|
|
1662
|
+
/**
|
|
1663
|
+
* Minimal logger contract; Fastify `request.log` and pino satisfy it.
|
|
1664
|
+
* Kept narrow so tests can pass a `{ warn: vi.fn() }` stub.
|
|
1665
|
+
*/
|
|
1666
|
+
interface AggregatorLogger {
|
|
1667
|
+
warn(obj: object, msg?: string): void;
|
|
1668
|
+
}
|
|
1669
|
+
interface AggregateEarningsInput {
|
|
1670
|
+
connectorAdmin: ConnectorAdminClient;
|
|
1671
|
+
peerTypeResolver: PeerTypeResolver;
|
|
1672
|
+
/**
|
|
1673
|
+
* Optional delta computer (Story 47.3). When omitted, all PerAsset
|
|
1674
|
+
* `today` / `month` / `year` fields stub to '0'. The route layer (47.4)
|
|
1675
|
+
* wires the snapshot-backed implementation. A rejection on a single
|
|
1676
|
+
* asset stubs that asset's deltas to '0' and emits `logger.warn`; one
|
|
1677
|
+
* bad asset never breaks the aggregate.
|
|
1678
|
+
*/
|
|
1679
|
+
deltaComputer?: DeltaComputer;
|
|
1680
|
+
/**
|
|
1681
|
+
* Optional logger. When provided, `getEarnings()` failures and
|
|
1682
|
+
* `deltaComputer` rejections are surfaced via `logger.warn` so ops can
|
|
1683
|
+
* distinguish a connector outage from "no earnings yet."
|
|
1684
|
+
*/
|
|
1685
|
+
logger?: AggregatorLogger;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Snapshot reader + `DeltaComputer` factory (Story 47.3).
|
|
1690
|
+
*
|
|
1691
|
+
* Reads `earnings-snapshots.jsonl` and computes TODAY/MONTH/YEAR deltas vs.
|
|
1692
|
+
* UTC boundaries (midnight, 1st-of-month, 1st-of-year). Tolerates malformed
|
|
1693
|
+
* lines (skip) and clock-skewed snapshots (filter `ts > now`). Returns `'0'`
|
|
1694
|
+
* when no boundary snapshot exists yet.
|
|
1695
|
+
*
|
|
1696
|
+
* @module
|
|
1697
|
+
* @since 47.3
|
|
1698
|
+
*/
|
|
1699
|
+
|
|
1700
|
+
/** ISO of the most recent UTC midnight <= ref. */
|
|
1701
|
+
declare function utcDayBoundary(ref: Date): string;
|
|
1702
|
+
/** ISO of the first instant of the current calendar month in UTC. */
|
|
1703
|
+
declare function utcMonthBoundary(ref: Date): string;
|
|
1704
|
+
/** ISO of the first instant of the current calendar year in UTC. */
|
|
1705
|
+
declare function utcYearBoundary(ref: Date): string;
|
|
1706
|
+
/**
|
|
1707
|
+
* Construct a `DeltaComputer` (Story 47.2's type) backed by the snapshot
|
|
1708
|
+
* file at `snapshotPath`. The returned function is the one wired into
|
|
1709
|
+
* `aggregateEarnings({ ..., deltaComputer })` by Story 47.4's route.
|
|
1710
|
+
*
|
|
1711
|
+
* Reads the snapshot file once per DeltaComputer call (single-pass), then
|
|
1712
|
+
* resolves all three boundaries (today/month/year) in-memory from the parsed
|
|
1713
|
+
* map. No cross-call cache in v1 — see Open Question 6 in story notes.
|
|
1714
|
+
*/
|
|
1715
|
+
declare function createDeltaComputer(opts: {
|
|
1716
|
+
snapshotPath: string;
|
|
1717
|
+
/** Optional clock injection for tests. Default `() => new Date()`. */
|
|
1718
|
+
now?: () => Date;
|
|
1719
|
+
}): DeltaComputer;
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* Image manifest reader for Townhouse (Story 46.2).
|
|
1723
|
+
*
|
|
1724
|
+
* `~/.townhouse/image-manifest.json` is materialized by `compose-loader.ts`
|
|
1725
|
+
* during `townhouse hs up`. It maps node types to their digest-pinned image
|
|
1726
|
+
* refs, consumed by `POST /api/nodes` step 2 (pull image).
|
|
1727
|
+
*/
|
|
1728
|
+
|
|
1729
|
+
declare const ImageManifestSchema: z.ZodObject<{
|
|
1730
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
1731
|
+
townhouseVersion: z.ZodString;
|
|
1732
|
+
builtAt: z.ZodString;
|
|
1733
|
+
images: z.ZodObject<{
|
|
1734
|
+
'townhouse-api': z.ZodObject<{
|
|
1735
|
+
name: z.ZodString;
|
|
1736
|
+
tag: z.ZodString;
|
|
1737
|
+
digest: z.ZodString;
|
|
1738
|
+
}, "strict", z.ZodTypeAny, {
|
|
1739
|
+
name: string;
|
|
1740
|
+
tag: string;
|
|
1741
|
+
digest: string;
|
|
1742
|
+
}, {
|
|
1743
|
+
name: string;
|
|
1744
|
+
tag: string;
|
|
1745
|
+
digest: string;
|
|
1746
|
+
}>;
|
|
1747
|
+
town: z.ZodObject<{
|
|
1748
|
+
name: z.ZodString;
|
|
1749
|
+
tag: z.ZodString;
|
|
1750
|
+
digest: z.ZodString;
|
|
1751
|
+
}, "strict", z.ZodTypeAny, {
|
|
1752
|
+
name: string;
|
|
1753
|
+
tag: string;
|
|
1754
|
+
digest: string;
|
|
1755
|
+
}, {
|
|
1756
|
+
name: string;
|
|
1757
|
+
tag: string;
|
|
1758
|
+
digest: string;
|
|
1759
|
+
}>;
|
|
1760
|
+
mill: z.ZodObject<{
|
|
1761
|
+
name: z.ZodString;
|
|
1762
|
+
tag: z.ZodString;
|
|
1763
|
+
digest: z.ZodString;
|
|
1764
|
+
}, "strict", z.ZodTypeAny, {
|
|
1765
|
+
name: string;
|
|
1766
|
+
tag: string;
|
|
1767
|
+
digest: string;
|
|
1768
|
+
}, {
|
|
1769
|
+
name: string;
|
|
1770
|
+
tag: string;
|
|
1771
|
+
digest: string;
|
|
1772
|
+
}>;
|
|
1773
|
+
dvm: z.ZodObject<{
|
|
1774
|
+
name: z.ZodString;
|
|
1775
|
+
tag: z.ZodString;
|
|
1776
|
+
digest: z.ZodString;
|
|
1777
|
+
}, "strict", z.ZodTypeAny, {
|
|
1778
|
+
name: string;
|
|
1779
|
+
tag: string;
|
|
1780
|
+
digest: string;
|
|
1781
|
+
}, {
|
|
1782
|
+
name: string;
|
|
1783
|
+
tag: string;
|
|
1784
|
+
digest: string;
|
|
1785
|
+
}>;
|
|
1786
|
+
connector: z.ZodObject<{
|
|
1787
|
+
name: z.ZodString;
|
|
1788
|
+
tag: z.ZodString;
|
|
1789
|
+
digest: z.ZodString;
|
|
1790
|
+
}, "strict", z.ZodTypeAny, {
|
|
1791
|
+
name: string;
|
|
1792
|
+
tag: string;
|
|
1793
|
+
digest: string;
|
|
1794
|
+
}, {
|
|
1795
|
+
name: string;
|
|
1796
|
+
tag: string;
|
|
1797
|
+
digest: string;
|
|
1798
|
+
}>;
|
|
1799
|
+
}, "strict", z.ZodTypeAny, {
|
|
1800
|
+
town: {
|
|
1801
|
+
name: string;
|
|
1802
|
+
tag: string;
|
|
1803
|
+
digest: string;
|
|
1804
|
+
};
|
|
1805
|
+
mill: {
|
|
1806
|
+
name: string;
|
|
1807
|
+
tag: string;
|
|
1808
|
+
digest: string;
|
|
1809
|
+
};
|
|
1810
|
+
dvm: {
|
|
1811
|
+
name: string;
|
|
1812
|
+
tag: string;
|
|
1813
|
+
digest: string;
|
|
1814
|
+
};
|
|
1815
|
+
'townhouse-api': {
|
|
1816
|
+
name: string;
|
|
1817
|
+
tag: string;
|
|
1818
|
+
digest: string;
|
|
1819
|
+
};
|
|
1820
|
+
connector: {
|
|
1821
|
+
name: string;
|
|
1822
|
+
tag: string;
|
|
1823
|
+
digest: string;
|
|
1824
|
+
};
|
|
1825
|
+
}, {
|
|
1826
|
+
town: {
|
|
1827
|
+
name: string;
|
|
1828
|
+
tag: string;
|
|
1829
|
+
digest: string;
|
|
1830
|
+
};
|
|
1831
|
+
mill: {
|
|
1832
|
+
name: string;
|
|
1833
|
+
tag: string;
|
|
1834
|
+
digest: string;
|
|
1835
|
+
};
|
|
1836
|
+
dvm: {
|
|
1837
|
+
name: string;
|
|
1838
|
+
tag: string;
|
|
1839
|
+
digest: string;
|
|
1840
|
+
};
|
|
1841
|
+
'townhouse-api': {
|
|
1842
|
+
name: string;
|
|
1843
|
+
tag: string;
|
|
1844
|
+
digest: string;
|
|
1845
|
+
};
|
|
1846
|
+
connector: {
|
|
1847
|
+
name: string;
|
|
1848
|
+
tag: string;
|
|
1849
|
+
digest: string;
|
|
1850
|
+
};
|
|
1851
|
+
}>;
|
|
1852
|
+
}, "strict", z.ZodTypeAny, {
|
|
1853
|
+
schemaVersion: 1;
|
|
1854
|
+
townhouseVersion: string;
|
|
1855
|
+
builtAt: string;
|
|
1856
|
+
images: {
|
|
1857
|
+
town: {
|
|
1858
|
+
name: string;
|
|
1859
|
+
tag: string;
|
|
1860
|
+
digest: string;
|
|
1861
|
+
};
|
|
1862
|
+
mill: {
|
|
1863
|
+
name: string;
|
|
1864
|
+
tag: string;
|
|
1865
|
+
digest: string;
|
|
1866
|
+
};
|
|
1867
|
+
dvm: {
|
|
1868
|
+
name: string;
|
|
1869
|
+
tag: string;
|
|
1870
|
+
digest: string;
|
|
1871
|
+
};
|
|
1872
|
+
'townhouse-api': {
|
|
1873
|
+
name: string;
|
|
1874
|
+
tag: string;
|
|
1875
|
+
digest: string;
|
|
1876
|
+
};
|
|
1877
|
+
connector: {
|
|
1878
|
+
name: string;
|
|
1879
|
+
tag: string;
|
|
1880
|
+
digest: string;
|
|
1881
|
+
};
|
|
1882
|
+
};
|
|
1883
|
+
}, {
|
|
1884
|
+
schemaVersion: 1;
|
|
1885
|
+
townhouseVersion: string;
|
|
1886
|
+
builtAt: string;
|
|
1887
|
+
images: {
|
|
1888
|
+
town: {
|
|
1889
|
+
name: string;
|
|
1890
|
+
tag: string;
|
|
1891
|
+
digest: string;
|
|
1892
|
+
};
|
|
1893
|
+
mill: {
|
|
1894
|
+
name: string;
|
|
1895
|
+
tag: string;
|
|
1896
|
+
digest: string;
|
|
1897
|
+
};
|
|
1898
|
+
dvm: {
|
|
1899
|
+
name: string;
|
|
1900
|
+
tag: string;
|
|
1901
|
+
digest: string;
|
|
1902
|
+
};
|
|
1903
|
+
'townhouse-api': {
|
|
1904
|
+
name: string;
|
|
1905
|
+
tag: string;
|
|
1906
|
+
digest: string;
|
|
1907
|
+
};
|
|
1908
|
+
connector: {
|
|
1909
|
+
name: string;
|
|
1910
|
+
tag: string;
|
|
1911
|
+
digest: string;
|
|
1912
|
+
};
|
|
1913
|
+
};
|
|
1914
|
+
}>;
|
|
1915
|
+
type ImageManifest = z.infer<typeof ImageManifestSchema>;
|
|
1916
|
+
/**
|
|
1917
|
+
* Read and validate `image-manifest.json` at the given path.
|
|
1918
|
+
*
|
|
1919
|
+
* Throws ENOENT if the file is missing — there is no graceful fallback for a
|
|
1920
|
+
* missing manifest; it means `townhouse hs up` was not run first.
|
|
1921
|
+
* Throws `ZodError` with a useful path if the file is present but invalid.
|
|
1922
|
+
*/
|
|
1923
|
+
declare function readImageManifest(path: string): Promise<ImageManifest>;
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* Boot reconciler (Story 46.1).
|
|
1927
|
+
*
|
|
1928
|
+
* Converges connector peer state to `~/.townhouse/nodes.yaml` (truth) on
|
|
1929
|
+
* every `townhouse hs up`. Reads yaml + connector peers, diffs them,
|
|
1930
|
+
* re-registers any yaml entries missing from the connector, and logs
|
|
1931
|
+
* connector peers without yaml entries as `'external'` (left alone).
|
|
1932
|
+
*
|
|
1933
|
+
* Container lifecycle is OUT of scope — that lives in Epic 46.2.
|
|
1934
|
+
*/
|
|
1935
|
+
|
|
1936
|
+
/** Action recorded for a single divergence. */
|
|
1937
|
+
type DivergenceAction = 'reregistered' | 'reregister-failed' | 'external';
|
|
1938
|
+
/** A single divergence record for the reconciler log. */
|
|
1939
|
+
interface DivergenceLog {
|
|
1940
|
+
timestamp: string;
|
|
1941
|
+
peerId: string;
|
|
1942
|
+
action: DivergenceAction;
|
|
1943
|
+
detail?: string;
|
|
1944
|
+
}
|
|
1945
|
+
/** Summary returned by `reconcile()` so callers can surface partial-failure counts. */
|
|
1946
|
+
interface ReconcileSummary {
|
|
1947
|
+
reregistered: number;
|
|
1948
|
+
failed: number;
|
|
1949
|
+
external: number;
|
|
1950
|
+
}
|
|
1951
|
+
declare class BootReconciler {
|
|
1952
|
+
private readonly adminClient;
|
|
1953
|
+
private readonly nodesYamlPath;
|
|
1954
|
+
private readonly reconcilerLogPath;
|
|
1955
|
+
private logDirEnsured;
|
|
1956
|
+
private logFileChmodEnsured;
|
|
1957
|
+
constructor(adminClient: Pick<ConnectorAdminClient, 'getPeers' | 'registerPeer'>, nodesYamlPath: string, reconcilerLogPath: string);
|
|
1958
|
+
/**
|
|
1959
|
+
* Diff `nodes.yaml` (truth) against `GET /admin/peers` (derived state)
|
|
1960
|
+
* and converge.
|
|
1961
|
+
*
|
|
1962
|
+
* Ordering rule (Epic 46.2 dependency — load-bearing):
|
|
1963
|
+
* `nodes.yaml` write happens BEFORE connector registration
|
|
1964
|
+
* (`POST /admin/peers`).
|
|
1965
|
+
*
|
|
1966
|
+
* The drift window resolves in the safe direction:
|
|
1967
|
+
* - yaml entry without a connector peer = harmless. The reconciler
|
|
1968
|
+
* re-registers it on next `hs up` (this method does that).
|
|
1969
|
+
* - connector peer without a yaml entry = treated as `'external'` and
|
|
1970
|
+
* left alone (operators may legitimately route non-Townhouse peers
|
|
1971
|
+
* through the same connector).
|
|
1972
|
+
*
|
|
1973
|
+
* The unsafe direction (register first, then write yaml) creates a
|
|
1974
|
+
* window where the connector routes to a peer Townhouse cannot clean
|
|
1975
|
+
* up. Epic 46.2's provisioning pipeline MUST honor the yaml-first rule.
|
|
1976
|
+
*
|
|
1977
|
+
* Failures fetching `getPeers()` are surfaced (not swallowed) so the
|
|
1978
|
+
* caller in `handleHsUp` can decide whether to treat reconciler
|
|
1979
|
+
* divergence as fatal. (Today: non-fatal — see cli.ts wire point.)
|
|
1980
|
+
*
|
|
1981
|
+
* Per-divergence appendLog failures are caught so a single log-write
|
|
1982
|
+
* failure does not abort the rest of the reconciliation pass.
|
|
1983
|
+
*/
|
|
1984
|
+
reconcile(): Promise<ReconcileSummary>;
|
|
1985
|
+
/**
|
|
1986
|
+
* Compute divergences without mutating the connector. Exposed for
|
|
1987
|
+
* testability — production callers use `reconcile()`.
|
|
1988
|
+
*/
|
|
1989
|
+
private diff;
|
|
1990
|
+
/**
|
|
1991
|
+
* Append one divergence record without aborting the whole reconciliation
|
|
1992
|
+
* pass on a single log-write failure (disk full, EACCES, etc.). Failures
|
|
1993
|
+
* are themselves logged to stderr — not silently swallowed — so the
|
|
1994
|
+
* operator can see them in the same `hs up` session.
|
|
1995
|
+
*/
|
|
1996
|
+
private tryAppendLog;
|
|
1997
|
+
/**
|
|
1998
|
+
* Append one divergence record to the reconciler log as a single line of
|
|
1999
|
+
* JSON (jsonl-style — easy to grep, easy to parse).
|
|
2000
|
+
*
|
|
2001
|
+
* `mkdir` runs once per reconciler instance. `chmod 0o600` on the log file
|
|
2002
|
+
* also runs once — `fs.appendFile`'s `mode` option only applies on
|
|
2003
|
+
* creation, so without a post-create chmod a pre-existing log file with
|
|
2004
|
+
* permissive mode would never be tightened.
|
|
2005
|
+
*/
|
|
2006
|
+
private appendLog;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
export { type AggregateEarningsInput, type AggregatedEarnings, type AggregatedEarningsStatus, type AggregatorLogger, type ApiDeps, type ApiServer, type BandwidthPayload, BandwidthStats, BootReconciler, ComposeProfile, ConfigValidationError, ConnectorAdminClient, ConnectorConfigGenerator, type ConnectorRuntimeConfig, DEFAULT_ATOR_PROXY, type DeltaComputer, type DepositAddressEntry, type DepositAddressesPayload, type DivergenceAction, type DivergenceLog, DockerOrchestrator, type DvmHealthResponse, EncryptedWallet, HealthCheckOptions, type HealthResponse, type HsHostnameResponse, type ImageManifest, ImageManifestSchema, type JobsByKindEntry, type JobsRecentPayload, type MetricsPayload, type MetricsPeerEntry, type MetricsResponse, type MillSwapsRecentPayload, type NodeDetail, type NodeEarnings, type NodeHealthPayload, type NodeInfo, type NodeState, NodeType$1 as NodeType, type NodesYaml, type NodesYamlEntry, NodesYamlEntrySchema, NodesYamlSchema, type NostrEventPayload, OrchestratorError, type PacketLogEntry, type PacketLogFilter, type PacketTimeseriesPayload, type PeerEntry, type PeerStatus, PeerTypeResolver, type PeersResponse, type PerAsset, type ReconcileSummary, type RevealRequest, type RevealResponse, type SnapshotEntry, SnapshotWriter, type SnapshotWriterOptions, type SwapByPairEntry, type TimeseriesBucket, type TownHealthPayload, TownhouseConfig, type TransactionReceiptPayload, type TransportPatchRequest, type TransportPatchResponse, TransportProbe, type TransportStatusPayload, type WalletBalanceEntry, type WalletBalancesPayload, WalletManager, 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, createDeltaComputer, createWizardApiServer, decryptWallet, encryptWallet, getDefaultConfig, loadConfig, loadWallet, readImageManifest, readNodesYaml, saveConfig, saveWallet, utcDayBoundary, utcMonthBoundary, utcYearBoundary, validateConfig, writeNodesYaml };
|