@toon-protocol/hub 0.34.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -0
- package/dist/chunk-5O4SBV5O.js +538 -0
- package/dist/chunk-5O4SBV5O.js.map +1 -0
- package/dist/chunk-I2R4CRUX.js +39 -0
- 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/chunk-L2U4G4OK.js +30219 -0
- package/dist/chunk-L2U4G4OK.js.map +1 -0
- package/dist/chunk-MNVIN5XK.js +125 -0
- package/dist/chunk-MNVIN5XK.js.map +1 -0
- package/dist/cli.d.ts +209 -0
- package/dist/cli.js +4809 -0
- package/dist/cli.js.map +1 -0
- package/dist/compose/townhouse-dev.yml +415 -0
- package/dist/compose/townhouse-direct.yml +391 -0
- package/dist/compose/townhouse-hs.yml +468 -0
- package/dist/demo-UJ37MLCG.js +118 -0
- package/dist/demo-UJ37MLCG.js.map +1 -0
- package/dist/index.d.ts +1342 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator-dGq7CeaO.d.ts +1507 -0
- package/dist/rsa-from-seed-XIT6EU73.js +67 -0
- package/dist/rsa-from-seed-XIT6EU73.js.map +1 -0
- package/dist/tui-QE3ZRZO3.js +638 -0
- package/dist/tui-QE3ZRZO3.js.map +1 -0
- package/package.json +89 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
import { E as EncryptedWallet, T as TownhouseConfig, N as NodeType$1, a as ConnectorRuntimeConfig, D as DockerOrchestrator, W as WalletManager, b as ConnectorAdminClient, c as ChainProviderEntry, R as RecentClaim } from './orchestrator-dGq7CeaO.js';
|
|
2
|
+
export { A as ApiConfig, B as BandwidthStats, d as ChainType, e as ComposeLoaderError, C as ComposeLoaderOptions, f as ComposeProfile, g as ConnectorConfig, h as ContainerSpec, i as DerivedNodeKeys, j as DvmNodeConfig, k as EvmChainProvider, H as HealthCheckOptions, l as HealthResponse, m as HsHostnameResponse, L as LoggingConfig, M as MetricsPeerEntry, n as MetricsResponse, o as MillNodeConfig, p as MinaChainProvider, q as NodeKeyInfo, r as NodeKeys, s as NodesConfig, O as OrchestratorError, t as OrchestratorEvents, P as PacketLogEntry, u as PacketLogFilter, v as PeerEntry, w as PeerStatus, x as PeersResponse, S as SolanaChainProvider, y as TownNodeConfig, z as TransportConfig, F as WalletConfig, G as WalletManagerConfig, I as WalletState, J as loadComposeTemplate, K as materializeComposeTemplate } from './orchestrator-dGq7CeaO.js';
|
|
3
|
+
import { FastifyBaseLogger, FastifyInstance } from 'fastify';
|
|
4
|
+
import Docker from 'dockerode';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export { NetworkMode } from '@toon-protocol/core';
|
|
7
|
+
import 'node:events';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wallet encryption/decryption for Townhouse (Story 21.4, Task 2).
|
|
11
|
+
*
|
|
12
|
+
* Uses Node.js crypto: scrypt for KDF, AES-256-GCM for authenticated encryption.
|
|
13
|
+
* The mnemonic is the plaintext being encrypted.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Encrypt a mnemonic with a password using scrypt + AES-256-GCM.
|
|
18
|
+
*/
|
|
19
|
+
declare function encryptWallet(mnemonic: string, password: string): EncryptedWallet;
|
|
20
|
+
/**
|
|
21
|
+
* Decrypt an encrypted wallet with a password.
|
|
22
|
+
* Throws on wrong password (GCM auth tag verification failure).
|
|
23
|
+
*/
|
|
24
|
+
declare function decryptWallet(encrypted: EncryptedWallet, password: string): string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wallet file I/O for Townhouse (Story 21.4, Task 2.2).
|
|
28
|
+
*
|
|
29
|
+
* Persists encrypted wallet to disk with 0o600 permissions (owner-only).
|
|
30
|
+
* Warns if existing file has world-readable permissions.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save encrypted wallet to disk with restrictive permissions.
|
|
35
|
+
* Creates parent directory if missing.
|
|
36
|
+
*/
|
|
37
|
+
declare function saveWallet(path: string, encrypted: EncryptedWallet): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Load encrypted wallet from disk.
|
|
40
|
+
* Returns null if file does not exist.
|
|
41
|
+
* Warns (via returned flag) if file permissions are too open.
|
|
42
|
+
*/
|
|
43
|
+
declare function loadWallet(path: string): Promise<{
|
|
44
|
+
wallet: EncryptedWallet;
|
|
45
|
+
permissionsWarning?: string;
|
|
46
|
+
} | null>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sensible default configuration. All nodes disabled by default —
|
|
50
|
+
* operator must explicitly enable what they want to run.
|
|
51
|
+
*/
|
|
52
|
+
declare function getDefaultConfig(): TownhouseConfig;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Config file loader — reads YAML, validates, writes.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load and validate a Townhouse config from a YAML file.
|
|
60
|
+
* Environment variables override YAML values for key settings:
|
|
61
|
+
* - TOWNHOUSE_API_PORT
|
|
62
|
+
* - TOWNHOUSE_TRANSPORT_MODE
|
|
63
|
+
* - TOWNHOUSE_LOG_LEVEL
|
|
64
|
+
*/
|
|
65
|
+
declare function loadConfig(configPath: string): TownhouseConfig;
|
|
66
|
+
/**
|
|
67
|
+
* Save a config to a YAML file atomically.
|
|
68
|
+
* Writes to a temp file first, then renames to the target (atomic on POSIX).
|
|
69
|
+
*/
|
|
70
|
+
declare function saveConfig(configPath: string, config: TownhouseConfig): void;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Runtime validation for Townhouse configuration.
|
|
74
|
+
* Validates shape, narrows types, returns typed config or throws.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
declare class ConfigValidationError extends Error {
|
|
78
|
+
constructor(message: string);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate raw input and return a typed TownhouseConfig.
|
|
82
|
+
* Throws ConfigValidationError with descriptive messages on invalid input.
|
|
83
|
+
*/
|
|
84
|
+
declare function validateConfig(raw: unknown): TownhouseConfig;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Connector Config Generator for Townhouse (Story 21.3).
|
|
88
|
+
*
|
|
89
|
+
* Generates runtime configuration for the standalone ILP connector
|
|
90
|
+
* based on the Townhouse config and currently active nodes.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/** Default ATOR SOCKS proxy address */
|
|
94
|
+
declare const DEFAULT_ATOR_PROXY = "socks5h://proxy.ator.io:9050";
|
|
95
|
+
/**
|
|
96
|
+
* ConnectorConfigGenerator produces runtime configuration for the standalone
|
|
97
|
+
* ILP connector based on Townhouse config and active node list.
|
|
98
|
+
*
|
|
99
|
+
* Key design: peer BTP URLs are deterministic Docker DNS names, so nodes
|
|
100
|
+
* don't need to be running for config generation to work.
|
|
101
|
+
*/
|
|
102
|
+
declare class ConnectorConfigGenerator {
|
|
103
|
+
private readonly config;
|
|
104
|
+
constructor(config: TownhouseConfig);
|
|
105
|
+
/**
|
|
106
|
+
* Generate a ConnectorRuntimeConfig for the given set of active nodes.
|
|
107
|
+
*
|
|
108
|
+
* @param activeNodes - Node types currently running or about to start
|
|
109
|
+
* @returns Typed configuration object (not serialized)
|
|
110
|
+
*/
|
|
111
|
+
generate(activeNodes: NodeType$1[]): ConnectorRuntimeConfig;
|
|
112
|
+
/**
|
|
113
|
+
* Serialize a ConnectorRuntimeConfig into environment variable key-value pairs.
|
|
114
|
+
*
|
|
115
|
+
* @returns Record of env var name to string value
|
|
116
|
+
*/
|
|
117
|
+
toEnvVars(runtimeConfig: ConnectorRuntimeConfig): Record<string, string>;
|
|
118
|
+
/**
|
|
119
|
+
* Convert a ConnectorRuntimeConfig into the string[] format expected by
|
|
120
|
+
* dockerode's container create API (Env option: ['KEY=VALUE', ...]).
|
|
121
|
+
*
|
|
122
|
+
* @returns Array of 'KEY=VALUE' strings
|
|
123
|
+
*/
|
|
124
|
+
toEnvArray(runtimeConfig: ConnectorRuntimeConfig): string[];
|
|
125
|
+
/**
|
|
126
|
+
* Render a connector YAML config string the connector image at 3.3.x can
|
|
127
|
+
* load via its `CONFIG_FILE` env var (default `./config.yaml`).
|
|
128
|
+
*
|
|
129
|
+
* The shape mirrors `docker/configs/townhouse-dev-connector.yaml` (the
|
|
130
|
+
* working dev fixture) — peers list is empty because child nodes dial
|
|
131
|
+
* INTO the connector at startup; the connector accepts BTP connections
|
|
132
|
+
* (no-auth in dev) without needing pre-configured peer entries.
|
|
133
|
+
*
|
|
134
|
+
* Added in the orchestrator-bug-fix: env vars set on the container were
|
|
135
|
+
* silently ignored by the connector image, which only reads from this
|
|
136
|
+
* YAML file. Caller writes the returned string to disk and mounts it
|
|
137
|
+
* at `/config/connector.yaml` in the container.
|
|
138
|
+
*/
|
|
139
|
+
toYaml(runtimeConfig: ConnectorRuntimeConfig): string;
|
|
140
|
+
/**
|
|
141
|
+
* Translate the runtime config's transport block into the discriminated-
|
|
142
|
+
* union shape the connector expects. See toYaml's note for why this was
|
|
143
|
+
* silently broken before.
|
|
144
|
+
*/
|
|
145
|
+
private buildConnectorTransportBlock;
|
|
146
|
+
/**
|
|
147
|
+
* Generate PeerEntry list for each active node type.
|
|
148
|
+
* BTP URLs use Docker DNS: btp+ws://townhouse-{type}:3000 // nosemgrep: javascript.lang.security.detect-insecure-websocket.detect-insecure-websocket
|
|
149
|
+
*/
|
|
150
|
+
private generatePeerList;
|
|
151
|
+
/**
|
|
152
|
+
* Generate transport config from Townhouse config.
|
|
153
|
+
* When mode is 'hs', includes SOCKS proxy (uses default if not configured).
|
|
154
|
+
* Carries forward externalUrl + hiddenService when set; downstream
|
|
155
|
+
* buildConnectorTransportBlock handles translation to the connector's
|
|
156
|
+
* wire shape.
|
|
157
|
+
*/
|
|
158
|
+
private generateTransportConfig;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* ATOR transport probe — periodically TCP-connects to the configured SOCKS5
|
|
163
|
+
* proxy host:port and measures direct HTTPS latency for comparison.
|
|
164
|
+
*
|
|
165
|
+
* The probe answers a single operator question: "is my configured ATOR proxy
|
|
166
|
+
* contactable from this host?" TCP connect is the right granularity — if the
|
|
167
|
+
* TCP listener is up, the connector's real BTP traffic will succeed.
|
|
168
|
+
*
|
|
169
|
+
* The probe NEVER makes a real SOCKS5 handshake or proxied request — only a
|
|
170
|
+
* plain TCP connect to the proxy host:port.
|
|
171
|
+
*/
|
|
172
|
+
interface TransportProbeOptions {
|
|
173
|
+
proxyUrl: string;
|
|
174
|
+
intervalMs?: number;
|
|
175
|
+
/** Override the direct-latency probe URL (for tests — avoids real network). */
|
|
176
|
+
directProbeUrl?: string;
|
|
177
|
+
}
|
|
178
|
+
interface TransportProbeStatus {
|
|
179
|
+
reachable: boolean;
|
|
180
|
+
latencyProxyMs: number | null;
|
|
181
|
+
latencyDirectMs: number | null;
|
|
182
|
+
lastProbedAt: number;
|
|
183
|
+
probeError: string | null;
|
|
184
|
+
}
|
|
185
|
+
declare class TransportProbe {
|
|
186
|
+
private proxyUrl;
|
|
187
|
+
private readonly intervalMs;
|
|
188
|
+
private readonly directProbeUrl;
|
|
189
|
+
private running;
|
|
190
|
+
private timer;
|
|
191
|
+
private status;
|
|
192
|
+
constructor(opts: TransportProbeOptions);
|
|
193
|
+
/** Start the probe loop. Idempotent — calling twice while running is a no-op. */
|
|
194
|
+
start(): void;
|
|
195
|
+
/** Stop the probe loop. Idempotent. */
|
|
196
|
+
stop(): void;
|
|
197
|
+
/** Returns the latest probe snapshot synchronously. Never blocks. */
|
|
198
|
+
getStatus(): TransportProbeStatus;
|
|
199
|
+
/**
|
|
200
|
+
* Update the target proxy URL.
|
|
201
|
+
* The next tick will use the new URL; the current tick may complete against the old URL.
|
|
202
|
+
*/
|
|
203
|
+
setProxyUrl(url: string): void;
|
|
204
|
+
private tick;
|
|
205
|
+
private probeTcp;
|
|
206
|
+
private probeDirectLatency;
|
|
207
|
+
private logTransition;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Townhouse API — type definitions.
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
/** Per-kind job count for jobsRecent.byKind */
|
|
215
|
+
interface DvmJobsByKindEntry {
|
|
216
|
+
kind: number;
|
|
217
|
+
count: number;
|
|
218
|
+
}
|
|
219
|
+
/** Per-status job counts for the sliding window */
|
|
220
|
+
interface DvmJobsByStatus {
|
|
221
|
+
processing: number;
|
|
222
|
+
success: number;
|
|
223
|
+
error: number;
|
|
224
|
+
partial: number;
|
|
225
|
+
}
|
|
226
|
+
/** Windowed recent-jobs telemetry (default window: 5 min) */
|
|
227
|
+
interface DvmJobsRecent {
|
|
228
|
+
total: number;
|
|
229
|
+
byKind: DvmJobsByKindEntry[];
|
|
230
|
+
byStatus: DvmJobsByStatus;
|
|
231
|
+
}
|
|
232
|
+
/** Response shape for GET /health on the DVM BLS server (port 3400). */
|
|
233
|
+
interface DvmHealthResponse {
|
|
234
|
+
status: 'starting' | 'ok' | 'stopping' | 'stopped' | 'error';
|
|
235
|
+
version: string;
|
|
236
|
+
nodePubkey: string;
|
|
237
|
+
uptimeSec: number;
|
|
238
|
+
/** Registered handler event kinds (e.g. [5094]). */
|
|
239
|
+
handlerKinds: number[];
|
|
240
|
+
/** Per-kind pricing in string-encoded bigint (e.g. { "5094": "10" }). */
|
|
241
|
+
kindPricing: Record<string, string>;
|
|
242
|
+
basePricePerByte: string;
|
|
243
|
+
jobsRecent: DvmJobsRecent;
|
|
244
|
+
}
|
|
245
|
+
/** Chains a Mill node can settle on. Inlined from packages/mill/src/wallet.ts. */
|
|
246
|
+
type MillChainKind = 'evm' | 'mina' | 'solana';
|
|
247
|
+
/** A configured swap pair, inlined from packages/core/src/types.ts (SwapPair). */
|
|
248
|
+
interface MillSwapPair {
|
|
249
|
+
from: {
|
|
250
|
+
assetCode: string;
|
|
251
|
+
assetScale: number;
|
|
252
|
+
chain: string;
|
|
253
|
+
};
|
|
254
|
+
to: {
|
|
255
|
+
assetCode: string;
|
|
256
|
+
assetScale: number;
|
|
257
|
+
chain: string;
|
|
258
|
+
};
|
|
259
|
+
rate: string;
|
|
260
|
+
minAmount?: string;
|
|
261
|
+
maxAmount?: string;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Response shape for GET /health on the Mill BLS server. Inlined from
|
|
265
|
+
* packages/mill/src/mill.ts (MillHealthResponse) — keep in sync.
|
|
266
|
+
*/
|
|
267
|
+
interface MillHealthResponse {
|
|
268
|
+
status: 'ok' | 'starting' | 'stopping' | 'stopped';
|
|
269
|
+
version: string;
|
|
270
|
+
nodePubkey: string;
|
|
271
|
+
swapPairsCount: number;
|
|
272
|
+
chains: readonly MillChainKind[];
|
|
273
|
+
uptimeSec: number;
|
|
274
|
+
inventory: Record<string, string>;
|
|
275
|
+
/** Configured swap pairs — operator-config, no secrets. */
|
|
276
|
+
swapPairs: MillSwapPair[];
|
|
277
|
+
/** Per-asset available reserves, parallel to `inventory` (which is total). */
|
|
278
|
+
inventoryAvailable: Record<string, string>;
|
|
279
|
+
}
|
|
280
|
+
/** Node types supported by Townhouse */
|
|
281
|
+
type NodeType = 'town' | 'mill' | 'dvm';
|
|
282
|
+
/** Runtime state of a node container */
|
|
283
|
+
type NodeState = 'running' | 'stopped' | 'error' | 'not-created';
|
|
284
|
+
/** Response shape for GET /nodes */
|
|
285
|
+
interface NodeInfo {
|
|
286
|
+
/**
|
|
287
|
+
* Unique instance identifier — equals `type` for single-instance deployments.
|
|
288
|
+
* Required in responses from this API; undefined in legacy test fixtures.
|
|
289
|
+
*/
|
|
290
|
+
id: string;
|
|
291
|
+
type: NodeType;
|
|
292
|
+
enabled: boolean;
|
|
293
|
+
state: NodeState;
|
|
294
|
+
uptimeSeconds: number | null;
|
|
295
|
+
image: string;
|
|
296
|
+
}
|
|
297
|
+
/** Detailed response shape for GET /nodes/:type */
|
|
298
|
+
interface NodeDetail extends NodeInfo {
|
|
299
|
+
config: {
|
|
300
|
+
feePerEvent?: number;
|
|
301
|
+
feeBasisPoints?: number;
|
|
302
|
+
feePerJob?: number;
|
|
303
|
+
kindPricing?: Record<string, number>;
|
|
304
|
+
enabled: boolean;
|
|
305
|
+
};
|
|
306
|
+
metrics: MetricsPayload | null;
|
|
307
|
+
}
|
|
308
|
+
/** Metrics payload from connector admin — narrowed per connector-team agreement 2026-04-21 */
|
|
309
|
+
interface MetricsPayload {
|
|
310
|
+
packetsForwarded: number;
|
|
311
|
+
packetsRejected: number;
|
|
312
|
+
bytesSent: number;
|
|
313
|
+
attribution: 'aggregate' | 'per-peer';
|
|
314
|
+
available: boolean;
|
|
315
|
+
}
|
|
316
|
+
/** Nostr event shape forwarded in relayEvents messages */
|
|
317
|
+
interface NostrEventPayload {
|
|
318
|
+
id: string;
|
|
319
|
+
kind: number;
|
|
320
|
+
pubkey: string;
|
|
321
|
+
content: string;
|
|
322
|
+
tags: string[][];
|
|
323
|
+
sig: string;
|
|
324
|
+
created_at: number;
|
|
325
|
+
}
|
|
326
|
+
/** WebSocket message shapes */
|
|
327
|
+
interface WsMetricsMessage {
|
|
328
|
+
type: 'metrics';
|
|
329
|
+
payload: MetricsPayload;
|
|
330
|
+
ts: number;
|
|
331
|
+
}
|
|
332
|
+
interface WsNodeStateMessage {
|
|
333
|
+
type: 'nodeState';
|
|
334
|
+
payload: NodeStatePayload;
|
|
335
|
+
ts: number;
|
|
336
|
+
}
|
|
337
|
+
interface WsHeartbeatMessage {
|
|
338
|
+
type: 'heartbeat';
|
|
339
|
+
ts: number;
|
|
340
|
+
}
|
|
341
|
+
interface WsBatchMessage {
|
|
342
|
+
type: 'batch';
|
|
343
|
+
messages: WsMessage[];
|
|
344
|
+
ts: number;
|
|
345
|
+
}
|
|
346
|
+
/** Forwarded Nostr event from a Town relay subscription */
|
|
347
|
+
interface WsRelayEventsMessage {
|
|
348
|
+
type: 'relayEvents';
|
|
349
|
+
nodeId: string;
|
|
350
|
+
payload: NostrEventPayload;
|
|
351
|
+
ts: number;
|
|
352
|
+
}
|
|
353
|
+
/** Connector restart notifications (emitted around fee-config PATCH) */
|
|
354
|
+
interface WsConnectorRestartingMessage {
|
|
355
|
+
type: 'connectorRestarting';
|
|
356
|
+
ts: number;
|
|
357
|
+
}
|
|
358
|
+
interface WsConnectorRestartedMessage {
|
|
359
|
+
type: 'connectorRestarted';
|
|
360
|
+
ts: number;
|
|
361
|
+
}
|
|
362
|
+
interface NodeStatePayload {
|
|
363
|
+
name: string;
|
|
364
|
+
state: string;
|
|
365
|
+
}
|
|
366
|
+
/** Server notification when the upstream relay WebSocket for a nodeId disconnects */
|
|
367
|
+
interface WsRelayEventsStatusMessage {
|
|
368
|
+
type: 'relayEventsStatus';
|
|
369
|
+
nodeId: string;
|
|
370
|
+
connected: boolean;
|
|
371
|
+
ts: number;
|
|
372
|
+
}
|
|
373
|
+
type WsMessage = WsMetricsMessage | WsNodeStateMessage | WsHeartbeatMessage | WsBatchMessage | WsRelayEventsMessage | WsConnectorRestartingMessage | WsConnectorRestartedMessage | WsRelayEventsStatusMessage;
|
|
374
|
+
/** Response shape for GET /nodes/:type/bandwidth */
|
|
375
|
+
interface BandwidthPayload {
|
|
376
|
+
bytesIn: number;
|
|
377
|
+
bytesOut: number;
|
|
378
|
+
sampleAt: number;
|
|
379
|
+
}
|
|
380
|
+
/** Packet log time-series bucket */
|
|
381
|
+
interface TimeseriesBucket {
|
|
382
|
+
ts: number;
|
|
383
|
+
count: number;
|
|
384
|
+
}
|
|
385
|
+
/** Response shape for GET /nodes/:type/packets/timeseries */
|
|
386
|
+
interface PacketTimeseriesPayload {
|
|
387
|
+
buckets: TimeseriesBucket[];
|
|
388
|
+
}
|
|
389
|
+
/** Minimal common health shape; superset emitted by Town containers. */
|
|
390
|
+
interface TownHealthPayload {
|
|
391
|
+
status: 'ok' | 'starting' | 'stopping' | 'stopped' | 'error';
|
|
392
|
+
version?: string;
|
|
393
|
+
uptimeSec?: number;
|
|
394
|
+
nodePubkey?: string;
|
|
395
|
+
}
|
|
396
|
+
/** Union of all node health response shapes. */
|
|
397
|
+
type NodeHealthPayload = MillHealthResponse | TownHealthPayload | DvmHealthResponse;
|
|
398
|
+
/** Per-kind job activity bucket for GET /nodes/:nodeId/jobs/recent */
|
|
399
|
+
interface JobsByKindEntry {
|
|
400
|
+
kind: number;
|
|
401
|
+
count: number;
|
|
402
|
+
volume: string;
|
|
403
|
+
}
|
|
404
|
+
/** Response shape for GET /nodes/:nodeId/jobs/recent */
|
|
405
|
+
interface JobsRecentPayload {
|
|
406
|
+
count: number;
|
|
407
|
+
volume: string;
|
|
408
|
+
byKind: JobsByKindEntry[];
|
|
409
|
+
byStatus: {
|
|
410
|
+
processing: number;
|
|
411
|
+
success: number;
|
|
412
|
+
error: number;
|
|
413
|
+
partial: number;
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/** Per-pair swap activity bucket */
|
|
417
|
+
interface SwapByPairEntry {
|
|
418
|
+
pair: string;
|
|
419
|
+
count: number;
|
|
420
|
+
volume: string;
|
|
421
|
+
}
|
|
422
|
+
/** Response shape for GET /nodes/mill/swaps/recent */
|
|
423
|
+
interface MillSwapsRecentPayload {
|
|
424
|
+
count: number;
|
|
425
|
+
volume: string;
|
|
426
|
+
byPair: SwapByPairEntry[];
|
|
427
|
+
}
|
|
428
|
+
/** Per-chain deposit address entry */
|
|
429
|
+
interface DepositAddressEntry {
|
|
430
|
+
family: 'evm' | 'solana' | 'mina';
|
|
431
|
+
address: string;
|
|
432
|
+
}
|
|
433
|
+
/** Response shape for GET /nodes/:type/deposit-addresses */
|
|
434
|
+
interface DepositAddressesPayload {
|
|
435
|
+
chains: DepositAddressEntry[];
|
|
436
|
+
}
|
|
437
|
+
/** Per-chain balance entry returned by GET /api/wallet/balances */
|
|
438
|
+
interface WalletBalanceEntry {
|
|
439
|
+
nodeType: 'town' | 'mill' | 'dvm';
|
|
440
|
+
family: 'evm' | 'solana' | 'mina';
|
|
441
|
+
token: 'ETH' | 'USDC' | 'SOL' | 'MINA';
|
|
442
|
+
address: string;
|
|
443
|
+
/** Decimal string in raw units (wei, lamports, etc.) */
|
|
444
|
+
balance: string;
|
|
445
|
+
/** Decimal places — 18 for ETH, 6 for USDC, 9 for SOL, 9 for MINA */
|
|
446
|
+
scale: number;
|
|
447
|
+
available: boolean;
|
|
448
|
+
/** Populated when available === false */
|
|
449
|
+
reason?: string;
|
|
450
|
+
}
|
|
451
|
+
/** Response shape for GET /api/wallet/balances */
|
|
452
|
+
interface WalletBalancesPayload {
|
|
453
|
+
entries: WalletBalanceEntry[];
|
|
454
|
+
ts: number;
|
|
455
|
+
}
|
|
456
|
+
/** Request body for POST /api/wallet/withdraw.
|
|
457
|
+
* `chainFamily` lists all values the route accepts at the wire level — the
|
|
458
|
+
* handler returns 501 for solana/mina with a structured payload pointing the
|
|
459
|
+
* caller at the deposit-address copy flow. */
|
|
460
|
+
interface WithdrawRequest {
|
|
461
|
+
nodeType: 'town' | 'mill' | 'dvm';
|
|
462
|
+
chainFamily: 'evm' | 'solana' | 'mina';
|
|
463
|
+
token: 'native' | 'USDC';
|
|
464
|
+
recipient: string;
|
|
465
|
+
/** Decimal string in raw units */
|
|
466
|
+
amount: string;
|
|
467
|
+
/** When true: returns gas estimate without broadcasting */
|
|
468
|
+
dryRun?: boolean;
|
|
469
|
+
}
|
|
470
|
+
/** Successful broadcast response (dryRun !== true). */
|
|
471
|
+
interface WithdrawSuccessResponse {
|
|
472
|
+
txHash: `0x${string}`;
|
|
473
|
+
chainId: number;
|
|
474
|
+
}
|
|
475
|
+
/** Successful dryRun response (no broadcast performed). */
|
|
476
|
+
interface WithdrawDryRunResponse {
|
|
477
|
+
estimatedGas: string;
|
|
478
|
+
estimatedFee: string;
|
|
479
|
+
}
|
|
480
|
+
/** Discriminated union — callers narrow by presence of `txHash`. */
|
|
481
|
+
type WithdrawResponse = WithdrawSuccessResponse | WithdrawDryRunResponse;
|
|
482
|
+
/** Request body for POST /api/wallet/reveal */
|
|
483
|
+
interface RevealRequest {
|
|
484
|
+
password: string;
|
|
485
|
+
}
|
|
486
|
+
/** Response shape for POST /api/wallet/reveal */
|
|
487
|
+
type RevealResponse = {
|
|
488
|
+
mnemonic: string;
|
|
489
|
+
} | {
|
|
490
|
+
error: 'invalid_password' | 'wallet_not_initialized' | 'wallet_corrupted';
|
|
491
|
+
message?: string;
|
|
492
|
+
};
|
|
493
|
+
/** Response shape for GET /api/wallet/transaction/:txHash */
|
|
494
|
+
interface TransactionReceiptPayload {
|
|
495
|
+
status: 'pending' | 'success' | 'reverted';
|
|
496
|
+
blockNumber?: number;
|
|
497
|
+
txHash: string;
|
|
498
|
+
}
|
|
499
|
+
/** Response shape for GET /api/wizard/state */
|
|
500
|
+
interface WizardStatePayload {
|
|
501
|
+
config_exists: boolean;
|
|
502
|
+
wallet_exists: boolean;
|
|
503
|
+
containers_running: boolean;
|
|
504
|
+
mode: 'wizard' | 'normal';
|
|
505
|
+
ts: number;
|
|
506
|
+
}
|
|
507
|
+
/** Request body for POST /api/wizard/init.
|
|
508
|
+
* `mnemonic` is required in BOTH modes — `mnemonic_mode` is purely a UX hint
|
|
509
|
+
* for the SPA, the server is stateless WRT the mnemonic and validates it on
|
|
510
|
+
* every init regardless of mode (see story 21.14 Dev Notes). */
|
|
511
|
+
interface WizardInitRequest {
|
|
512
|
+
password: string;
|
|
513
|
+
password_confirm: string;
|
|
514
|
+
mnemonic_mode: 'generate' | 'import';
|
|
515
|
+
mnemonic: string;
|
|
516
|
+
backup_ack: boolean;
|
|
517
|
+
nodes: {
|
|
518
|
+
town: {
|
|
519
|
+
enabled: boolean;
|
|
520
|
+
feePerEvent?: number;
|
|
521
|
+
};
|
|
522
|
+
mill: {
|
|
523
|
+
enabled: boolean;
|
|
524
|
+
feeBasisPoints?: number;
|
|
525
|
+
};
|
|
526
|
+
dvm: {
|
|
527
|
+
enabled: boolean;
|
|
528
|
+
feePerJob?: number;
|
|
529
|
+
};
|
|
530
|
+
};
|
|
531
|
+
transport: {
|
|
532
|
+
mode: 'direct' | 'hs';
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* Optional settlement chains (connector chainProviders) to configure during
|
|
536
|
+
* first-run setup. Omitted/empty → the connector uses the dev-Anvil default.
|
|
537
|
+
* Validated deeply when the resulting config is saved.
|
|
538
|
+
*/
|
|
539
|
+
chainProviders?: ChainProviderEntry[];
|
|
540
|
+
}
|
|
541
|
+
/** Progress messages streamed over WS /api/wizard/progress */
|
|
542
|
+
type WizardProgressMessage = {
|
|
543
|
+
type: 'pull_progress';
|
|
544
|
+
image: string;
|
|
545
|
+
status: string;
|
|
546
|
+
progress?: string;
|
|
547
|
+
ts: number;
|
|
548
|
+
} | {
|
|
549
|
+
type: 'container_starting';
|
|
550
|
+
name: string;
|
|
551
|
+
ts: number;
|
|
552
|
+
} | {
|
|
553
|
+
type: 'container_healthy';
|
|
554
|
+
name: string;
|
|
555
|
+
ts: number;
|
|
556
|
+
} | {
|
|
557
|
+
type: 'container_failed';
|
|
558
|
+
name: string;
|
|
559
|
+
reason: string;
|
|
560
|
+
ts: number;
|
|
561
|
+
} | {
|
|
562
|
+
type: 'launch_complete';
|
|
563
|
+
ts: number;
|
|
564
|
+
} | {
|
|
565
|
+
type: 'error';
|
|
566
|
+
message: string;
|
|
567
|
+
ts: number;
|
|
568
|
+
};
|
|
569
|
+
/** Response shape for GET /api/transport */
|
|
570
|
+
interface TransportStatusPayload {
|
|
571
|
+
mode: 'direct' | 'hs';
|
|
572
|
+
/** Present only when mode === 'hs' */
|
|
573
|
+
socksProxy?: string;
|
|
574
|
+
reachable: boolean;
|
|
575
|
+
latencyProxyMs: number | null;
|
|
576
|
+
latencyDirectMs: number | null;
|
|
577
|
+
/** ms epoch; 0 if probe never ran */
|
|
578
|
+
lastProbedAt: number;
|
|
579
|
+
probeError: string | null;
|
|
580
|
+
/** Server timestamp at response build time */
|
|
581
|
+
ts: number;
|
|
582
|
+
}
|
|
583
|
+
/** Request body for PATCH /api/transport */
|
|
584
|
+
interface TransportPatchRequest {
|
|
585
|
+
mode: 'direct' | 'hs';
|
|
586
|
+
socksProxy?: string;
|
|
587
|
+
}
|
|
588
|
+
/** Response body for PATCH /api/transport */
|
|
589
|
+
interface TransportPatchResponse {
|
|
590
|
+
mode: 'direct' | 'hs';
|
|
591
|
+
socksProxy?: string;
|
|
592
|
+
restartTriggered: boolean;
|
|
593
|
+
/** ms epoch of connector restart; present when restartTriggered === true */
|
|
594
|
+
restartedAt?: number;
|
|
595
|
+
}
|
|
596
|
+
/** API server returned by createApiServer */
|
|
597
|
+
interface ApiServer {
|
|
598
|
+
app: FastifyInstance;
|
|
599
|
+
close: () => Promise<void>;
|
|
600
|
+
}
|
|
601
|
+
/** Dependencies required to create the API server */
|
|
602
|
+
interface ApiDeps {
|
|
603
|
+
configPath: string;
|
|
604
|
+
config: TownhouseConfig;
|
|
605
|
+
orchestrator: DockerOrchestrator;
|
|
606
|
+
wallet: WalletManager;
|
|
607
|
+
connectorAdmin: ConnectorAdminClient;
|
|
608
|
+
/**
|
|
609
|
+
* Probe instance. Required: callers (createApiServer, createWizardApiServer,
|
|
610
|
+
* cli, dev API server) construct it from the config and pass it explicitly.
|
|
611
|
+
*/
|
|
612
|
+
transportProbe: TransportProbe;
|
|
613
|
+
logger?: FastifyBaseLogger;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* API Server Factory.
|
|
618
|
+
*
|
|
619
|
+
* SECURITY: Only binds to loopback address by default (localhost-only for v1).
|
|
620
|
+
* Set TOWNHOUSE_API_ALLOW_REMOTE=1 to override this security boundary.
|
|
621
|
+
*/
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Create the Fastify API server. Caller MUST supply a `transportProbe` in
|
|
625
|
+
* `deps` (constructed from the config and started if mode === 'hs').
|
|
626
|
+
*/
|
|
627
|
+
declare function createApiServer(deps: ApiDeps): Promise<ApiServer>;
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Wizard API Server Factory.
|
|
631
|
+
*
|
|
632
|
+
* Starts in wizard mode (only wizard routes), transitions to normal mode
|
|
633
|
+
* after POST /wizard/init completes and containers are healthy.
|
|
634
|
+
* SECURITY: Wizard mode hard-rejects non-loopback bind regardless of env var.
|
|
635
|
+
*/
|
|
636
|
+
|
|
637
|
+
interface WizardInitialDeps {
|
|
638
|
+
/** Directory where ~/.townhouse/ (or override) lives */
|
|
639
|
+
configDir: string;
|
|
640
|
+
/** Full path to config.yaml */
|
|
641
|
+
configPath: string;
|
|
642
|
+
/** Full path to wallet.enc */
|
|
643
|
+
walletPath: string;
|
|
644
|
+
/** Port to bind the API */
|
|
645
|
+
port: number;
|
|
646
|
+
/** Bind host — must be a loopback address; defaults to 127.0.0.1 */
|
|
647
|
+
bindHost?: string;
|
|
648
|
+
docker: Docker;
|
|
649
|
+
logger?: FastifyBaseLogger | boolean;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Create the wizard API server. Starts in wizard-only mode.
|
|
653
|
+
* After POST /wizard/init + orchestrator launch, transitions to normal mode.
|
|
654
|
+
*/
|
|
655
|
+
declare function createWizardApiServer(initialDeps: WizardInitialDeps): Promise<ApiServer>;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Hourly earnings snapshot writer (Story 47.3).
|
|
659
|
+
*
|
|
660
|
+
* Persists `claimsReceivedTotal` per (peerId × assetCode) — plus apex
|
|
661
|
+
* `connectorFees[]` rows under `peerId: '__apex__'` — to
|
|
662
|
+
* `${dirname(configPath)}/earnings-snapshots.jsonl` once per hour. Consumed
|
|
663
|
+
* by `snapshot-reader.ts`'s `DeltaComputer` factory. Failure mode: any
|
|
664
|
+
* per-tick error is logged via `logger.warn` and swallowed (the writer NEVER
|
|
665
|
+
* throws into the apex event loop) — the next tick retries cleanly. Pruning
|
|
666
|
+
* runs after each successful append (entries older than 13 months are
|
|
667
|
+
* rewritten atomically). File mode is `0o600` on every write.
|
|
668
|
+
*
|
|
669
|
+
* @module
|
|
670
|
+
* @since 47.3
|
|
671
|
+
*/
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* One JSONL row in `earnings-snapshots.jsonl`.
|
|
675
|
+
*
|
|
676
|
+
* NOTE: apex routing-fee rows use `peerId: '__apex__'`. The field name
|
|
677
|
+
* `claimsReceivedTotal` is technically a misnomer for apex rows — those
|
|
678
|
+
* are connector routing fees, not received claims — but the uniform column
|
|
679
|
+
* name keeps the JSONL schema simple and the reader doesn't need a special
|
|
680
|
+
* case.
|
|
681
|
+
*/
|
|
682
|
+
interface SnapshotEntry {
|
|
683
|
+
/** ISO-8601 UTC timestamp of the tick boundary (e.g. '2026-05-12T15:00:00.000Z'). */
|
|
684
|
+
ts: string;
|
|
685
|
+
/** Connector peerId, OR the literal `'__apex__'` for apex routing-fee rows. */
|
|
686
|
+
peerId: string;
|
|
687
|
+
assetCode: string;
|
|
688
|
+
/** Decimal-string cumulative (claims received for peers, routing-fee total for apex). */
|
|
689
|
+
claimsReceivedTotal: string;
|
|
690
|
+
}
|
|
691
|
+
interface SnapshotWriterOptions {
|
|
692
|
+
connectorAdmin: ConnectorAdminClient;
|
|
693
|
+
/** Absolute path to `earnings-snapshots.jsonl`. */
|
|
694
|
+
snapshotPath: string;
|
|
695
|
+
/** Tick interval (ms). Default 3_600_000 (1 hour). */
|
|
696
|
+
tickIntervalMs?: number;
|
|
697
|
+
/** Injected clock for tests. Default `() => new Date()`. */
|
|
698
|
+
now?: () => Date;
|
|
699
|
+
/** Retention window in months. Default 13. */
|
|
700
|
+
retentionMonths?: number;
|
|
701
|
+
/** pino/Fastify-compatible logger; warn-only. */
|
|
702
|
+
logger?: {
|
|
703
|
+
warn(obj: object, msg?: string): void;
|
|
704
|
+
};
|
|
705
|
+
/**
|
|
706
|
+
* Fire one tick immediately on `start()` instead of waiting for the first
|
|
707
|
+
* interval. Default `false` (production). Tests set this to `true` to
|
|
708
|
+
* assert append behavior without advancing fake timers.
|
|
709
|
+
*/
|
|
710
|
+
fireOnStart?: boolean;
|
|
711
|
+
}
|
|
712
|
+
declare class SnapshotWriter {
|
|
713
|
+
private readonly opts;
|
|
714
|
+
private timer;
|
|
715
|
+
private tickPending;
|
|
716
|
+
constructor(opts: SnapshotWriterOptions);
|
|
717
|
+
start(): void;
|
|
718
|
+
stop(): void;
|
|
719
|
+
/** Exposed for test ergonomics — runs one full append+prune cycle. */
|
|
720
|
+
tick(): Promise<void>;
|
|
721
|
+
private runTick;
|
|
722
|
+
private appendEntries;
|
|
723
|
+
private pruneIfNeeded;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* `nodes.yaml` schema + read/write helpers (Story 46.1).
|
|
728
|
+
*
|
|
729
|
+
* `~/.townhouse/nodes.yaml` is the operator-managed source of truth for
|
|
730
|
+
* enabled child nodes. The reconciler (see `../reconciler.ts`) converges
|
|
731
|
+
* connector peer state to this file on every `townhouse hs up`.
|
|
732
|
+
*
|
|
733
|
+
* Architectural rule (Epic 46.2 dependency): yaml writes happen BEFORE
|
|
734
|
+
* connector registration. The drift window resolves in the safe direction —
|
|
735
|
+
* a yaml entry without a connector peer is re-registered on next boot; a
|
|
736
|
+
* connector peer without a yaml entry is treated as `'external'` and left
|
|
737
|
+
* alone.
|
|
738
|
+
*/
|
|
739
|
+
|
|
740
|
+
declare const NodesYamlEntrySchema: z.ZodObject<{
|
|
741
|
+
id: z.ZodString;
|
|
742
|
+
type: z.ZodEnum<["town", "mill", "dvm"]>;
|
|
743
|
+
peerId: z.ZodString;
|
|
744
|
+
ilpAddress: z.ZodString;
|
|
745
|
+
derivationIndex: z.ZodNumber;
|
|
746
|
+
nostrPubkey: z.ZodOptional<z.ZodString>;
|
|
747
|
+
enabledAt: z.ZodString;
|
|
748
|
+
lastSeenAt: z.ZodNullable<z.ZodString>;
|
|
749
|
+
}, "strict", z.ZodTypeAny, {
|
|
750
|
+
type: "town" | "mill" | "dvm";
|
|
751
|
+
id: string;
|
|
752
|
+
peerId: string;
|
|
753
|
+
ilpAddress: string;
|
|
754
|
+
derivationIndex: number;
|
|
755
|
+
enabledAt: string;
|
|
756
|
+
lastSeenAt: string | null;
|
|
757
|
+
nostrPubkey?: string | undefined;
|
|
758
|
+
}, {
|
|
759
|
+
type: "town" | "mill" | "dvm";
|
|
760
|
+
id: string;
|
|
761
|
+
peerId: string;
|
|
762
|
+
ilpAddress: string;
|
|
763
|
+
derivationIndex: number;
|
|
764
|
+
enabledAt: string;
|
|
765
|
+
lastSeenAt: string | null;
|
|
766
|
+
nostrPubkey?: string | undefined;
|
|
767
|
+
}>;
|
|
768
|
+
declare const NodesYamlSchema: z.ZodEffects<z.ZodObject<{
|
|
769
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
770
|
+
id: z.ZodString;
|
|
771
|
+
type: z.ZodEnum<["town", "mill", "dvm"]>;
|
|
772
|
+
peerId: z.ZodString;
|
|
773
|
+
ilpAddress: z.ZodString;
|
|
774
|
+
derivationIndex: z.ZodNumber;
|
|
775
|
+
nostrPubkey: z.ZodOptional<z.ZodString>;
|
|
776
|
+
enabledAt: z.ZodString;
|
|
777
|
+
lastSeenAt: z.ZodNullable<z.ZodString>;
|
|
778
|
+
}, "strict", z.ZodTypeAny, {
|
|
779
|
+
type: "town" | "mill" | "dvm";
|
|
780
|
+
id: string;
|
|
781
|
+
peerId: string;
|
|
782
|
+
ilpAddress: string;
|
|
783
|
+
derivationIndex: number;
|
|
784
|
+
enabledAt: string;
|
|
785
|
+
lastSeenAt: string | null;
|
|
786
|
+
nostrPubkey?: string | undefined;
|
|
787
|
+
}, {
|
|
788
|
+
type: "town" | "mill" | "dvm";
|
|
789
|
+
id: string;
|
|
790
|
+
peerId: string;
|
|
791
|
+
ilpAddress: string;
|
|
792
|
+
derivationIndex: number;
|
|
793
|
+
enabledAt: string;
|
|
794
|
+
lastSeenAt: string | null;
|
|
795
|
+
nostrPubkey?: string | undefined;
|
|
796
|
+
}>, "many">;
|
|
797
|
+
}, "strict", z.ZodTypeAny, {
|
|
798
|
+
entries: {
|
|
799
|
+
type: "town" | "mill" | "dvm";
|
|
800
|
+
id: string;
|
|
801
|
+
peerId: string;
|
|
802
|
+
ilpAddress: string;
|
|
803
|
+
derivationIndex: number;
|
|
804
|
+
enabledAt: string;
|
|
805
|
+
lastSeenAt: string | null;
|
|
806
|
+
nostrPubkey?: string | undefined;
|
|
807
|
+
}[];
|
|
808
|
+
}, {
|
|
809
|
+
entries: {
|
|
810
|
+
type: "town" | "mill" | "dvm";
|
|
811
|
+
id: string;
|
|
812
|
+
peerId: string;
|
|
813
|
+
ilpAddress: string;
|
|
814
|
+
derivationIndex: number;
|
|
815
|
+
enabledAt: string;
|
|
816
|
+
lastSeenAt: string | null;
|
|
817
|
+
nostrPubkey?: string | undefined;
|
|
818
|
+
}[];
|
|
819
|
+
}>, {
|
|
820
|
+
entries: {
|
|
821
|
+
type: "town" | "mill" | "dvm";
|
|
822
|
+
id: string;
|
|
823
|
+
peerId: string;
|
|
824
|
+
ilpAddress: string;
|
|
825
|
+
derivationIndex: number;
|
|
826
|
+
enabledAt: string;
|
|
827
|
+
lastSeenAt: string | null;
|
|
828
|
+
nostrPubkey?: string | undefined;
|
|
829
|
+
}[];
|
|
830
|
+
}, {
|
|
831
|
+
entries: {
|
|
832
|
+
type: "town" | "mill" | "dvm";
|
|
833
|
+
id: string;
|
|
834
|
+
peerId: string;
|
|
835
|
+
ilpAddress: string;
|
|
836
|
+
derivationIndex: number;
|
|
837
|
+
enabledAt: string;
|
|
838
|
+
lastSeenAt: string | null;
|
|
839
|
+
nostrPubkey?: string | undefined;
|
|
840
|
+
}[];
|
|
841
|
+
}>;
|
|
842
|
+
type NodesYamlEntry = z.infer<typeof NodesYamlEntrySchema>;
|
|
843
|
+
type NodesYaml = z.infer<typeof NodesYamlSchema>;
|
|
844
|
+
/**
|
|
845
|
+
* Read and validate `nodes.yaml` at the given path.
|
|
846
|
+
*
|
|
847
|
+
* Returns `{ entries: [] }` if the file does not exist (graceful first-run).
|
|
848
|
+
* Throws a `ZodError` with a useful path if the file is present but invalid.
|
|
849
|
+
*/
|
|
850
|
+
declare function readNodesYaml(path: string): Promise<NodesYaml>;
|
|
851
|
+
/**
|
|
852
|
+
* Write `nodes.yaml` atomically with file mode `0o600`.
|
|
853
|
+
*
|
|
854
|
+
* Atomic = write to `<path>.tmp` then `fs.rename`. On POSIX, rename is
|
|
855
|
+
* atomic when source + destination live on the same filesystem (always true
|
|
856
|
+
* for `~/.townhouse/nodes.yaml`). Prevents partial-write corruption if the
|
|
857
|
+
* process is killed mid-write.
|
|
858
|
+
*/
|
|
859
|
+
declare function writeNodesYaml(path: string, data: NodesYaml): Promise<void>;
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* `PeerTypeResolver` (Story 46.1).
|
|
863
|
+
*
|
|
864
|
+
* The connector is a generic ILP router — it has no concept of
|
|
865
|
+
* `'town' | 'mill' | 'dvm'`. Townhouse owns the type concept entirely
|
|
866
|
+
* via this resolver, which is the single translation layer between
|
|
867
|
+
* connector `peerId` values and operator-meaningful node types.
|
|
868
|
+
*
|
|
869
|
+
* Architectural rule (Epic 46 planning §Architectural Layering):
|
|
870
|
+
* downstream consumers (Epic 47 aggregator, Epic 48 TUI, Epic 49 telemetry)
|
|
871
|
+
* MUST call through this resolver — they never hardcode peer-to-type
|
|
872
|
+
* mappings.
|
|
873
|
+
*
|
|
874
|
+
* The resolver is rebuilt from a `NodesYaml` snapshot — prefer immutable
|
|
875
|
+
* rebuild (re-instantiate) over mutable update for testability.
|
|
876
|
+
*/
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Minimal shape of a peer entry from the connector's `GET /admin/peers`
|
|
880
|
+
* response (mirrors `PeerStatus` in `../connector/types.ts`). Only the fields
|
|
881
|
+
* the type-inference heuristic reads are required, so callers can pass the
|
|
882
|
+
* connector response verbatim.
|
|
883
|
+
*/
|
|
884
|
+
interface ConnectorPeerLike {
|
|
885
|
+
/** Connector `peerId` (the value the peer authenticates as on its BTP session). */
|
|
886
|
+
id: string;
|
|
887
|
+
/** ILP route prefixes registered against this peer as nextHop. */
|
|
888
|
+
ilpAddresses?: string[];
|
|
889
|
+
}
|
|
890
|
+
declare class PeerTypeResolver {
|
|
891
|
+
private readonly map;
|
|
892
|
+
constructor(yaml: NodesYaml);
|
|
893
|
+
/**
|
|
894
|
+
* Build a resolver from the connector's `GET /admin/peers` roster instead of
|
|
895
|
+
* `nodes.yaml`. This is the resolution path for compose-rendered deployments
|
|
896
|
+
* (`townhouse hs up`), where the connector knows its child peers but no
|
|
897
|
+
* `nodes.yaml` was ever written (only the `townhouse node add` provisioning
|
|
898
|
+
* path writes one). The node type is inferred from each peer's `id` /
|
|
899
|
+
* `ilpAddresses` (see `inferTypeFromConnectorPeer`); peers whose type cannot
|
|
900
|
+
* be inferred are simply omitted from the map and therefore resolve to
|
|
901
|
+
* `'external'`.
|
|
902
|
+
*/
|
|
903
|
+
static fromConnectorPeers(peers: ConnectorPeerLike[]): PeerTypeResolver;
|
|
904
|
+
/**
|
|
905
|
+
* Resolve a connector `peerId` to its operator-declared node type.
|
|
906
|
+
* Returns `'external'` for unknown peerIds (legitimate non-Townhouse
|
|
907
|
+
* peers running through the same connector).
|
|
908
|
+
*/
|
|
909
|
+
resolvePeerType(peerId: string): NodeType$1 | 'external';
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Earnings aggregator (Story 47.2).
|
|
914
|
+
*
|
|
915
|
+
* Aggregates connector-reported earnings into the canonical
|
|
916
|
+
* `{ status, apex, peers }` shape consumed by the host-API
|
|
917
|
+
* `/api/earnings` endpoint.
|
|
918
|
+
*
|
|
919
|
+
* Source of truth: `connectorAdmin.getEarnings()` (Story 47.1).
|
|
920
|
+
* Peer-type attribution via `PeerTypeResolver` (Story 46.1); the resolver
|
|
921
|
+
* buckets unmatched peerIds as `'external'` (enforcement lives in the
|
|
922
|
+
* resolver, not here — we trust its contract).
|
|
923
|
+
*
|
|
924
|
+
* Failure mode: if `getEarnings()` throws (network, 503-when-disabled,
|
|
925
|
+
* shape drift), returns the empty payload with
|
|
926
|
+
* `status: 'connector_unavailable'`. The route returns 200 either way;
|
|
927
|
+
* operators see zeros plus a UI banner rather than a 5xx. An injected
|
|
928
|
+
* `logger.warn` (Fastify / pino-compatible) is called on failure so ops
|
|
929
|
+
* can distinguish "connector outage" from "no earnings yet."
|
|
930
|
+
*
|
|
931
|
+
* @module
|
|
932
|
+
* @since 47.2
|
|
933
|
+
*/
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Per-asset cumulative + delta breakdown. `lifetime` is the connector's
|
|
937
|
+
* cumulative `claimsReceivedTotal` (decimal-string bigint at `assetScale`
|
|
938
|
+
* decimals). `today` / `month` / `year` are deltas computed by Story 47.3's
|
|
939
|
+
* snapshot-reader; until the `deltaComputer` dep is provided, they stub
|
|
940
|
+
* to '0'. Asset-scale interpretation (USD: 6, ETH: 18, sats: 0) is the
|
|
941
|
+
* dashboard's job — the aggregator never collapses to a unit.
|
|
942
|
+
*/
|
|
943
|
+
interface PerAsset {
|
|
944
|
+
lifetime: string;
|
|
945
|
+
today: string;
|
|
946
|
+
month: string;
|
|
947
|
+
year: string;
|
|
948
|
+
}
|
|
949
|
+
/** Per-peer earnings entry in the aggregator output. */
|
|
950
|
+
interface NodeEarnings {
|
|
951
|
+
id: string;
|
|
952
|
+
type: NodeType$1 | 'external';
|
|
953
|
+
byAsset: Record<string, PerAsset>;
|
|
954
|
+
/** Max `lastClaimAt` across this peer's assets, or `null` if none. Added in 47.4. */
|
|
955
|
+
lastClaimAt: string | null;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Wire-level status for the aggregator response.
|
|
959
|
+
*
|
|
960
|
+
* `'ok'` — `getEarnings()` succeeded; payload reflects connector state.
|
|
961
|
+
* `'connector_unavailable'` — `getEarnings()` threw (network, 503, shape
|
|
962
|
+
* drift); apex + peers are empty. The dashboard renders a banner.
|
|
963
|
+
*/
|
|
964
|
+
type AggregatedEarningsStatus = 'ok' | 'connector_unavailable';
|
|
965
|
+
/** Top-level aggregator output. Extended in 47.4 with dashboard fields. */
|
|
966
|
+
interface AggregatedEarnings {
|
|
967
|
+
status: AggregatedEarningsStatus;
|
|
968
|
+
apex: {
|
|
969
|
+
routingFees: Record<string, PerAsset>;
|
|
970
|
+
};
|
|
971
|
+
peers: NodeEarnings[];
|
|
972
|
+
/** Pass-through from connector `recentClaims`. Empty array on connector outage. */
|
|
973
|
+
recentClaims: RecentClaim[];
|
|
974
|
+
/** Sum of `getMetrics().peers[].packetsForwarded` PLUS `packetsLocallyDelivered`
|
|
975
|
+
* (connector v3.7.0+, toon-protocol/connector#73 — counts events that landed
|
|
976
|
+
* via the self-delivery route, where the connector's in-process relay accepts
|
|
977
|
+
* the event locally rather than forwarding to a remote peer). 0 on connector
|
|
978
|
+
* outage or metrics failure. */
|
|
979
|
+
eventsRelayed: number;
|
|
980
|
+
/** From `getMetrics().uptimeSeconds`. 0 on connector outage or metrics failure. */
|
|
981
|
+
uptimeSeconds: number;
|
|
982
|
+
}
|
|
983
|
+
/** Resolves TODAY / MONTH / YEAR deltas for a (scope, assetCode) tuple. */
|
|
984
|
+
type DeltaComputer = (params: {
|
|
985
|
+
/** Either a connector peerId or the literal `'__apex__'` for routing-fee rows. */
|
|
986
|
+
scope: string;
|
|
987
|
+
assetCode: string;
|
|
988
|
+
/** Current cumulative (matches the lifetime value in the response). */
|
|
989
|
+
currentLifetime: string;
|
|
990
|
+
}) => Promise<{
|
|
991
|
+
today: string;
|
|
992
|
+
month: string;
|
|
993
|
+
year: string;
|
|
994
|
+
}>;
|
|
995
|
+
/**
|
|
996
|
+
* Minimal logger contract; Fastify `request.log` and pino satisfy it.
|
|
997
|
+
* Kept narrow so tests can pass a `{ warn: vi.fn() }` stub.
|
|
998
|
+
*/
|
|
999
|
+
interface AggregatorLogger {
|
|
1000
|
+
warn(obj: object, msg?: string): void;
|
|
1001
|
+
}
|
|
1002
|
+
interface AggregateEarningsInput {
|
|
1003
|
+
connectorAdmin: ConnectorAdminClient;
|
|
1004
|
+
peerTypeResolver: PeerTypeResolver;
|
|
1005
|
+
/**
|
|
1006
|
+
* Optional delta computer (Story 47.3). When omitted, all PerAsset
|
|
1007
|
+
* `today` / `month` / `year` fields stub to '0'. The route layer (47.4)
|
|
1008
|
+
* wires the snapshot-backed implementation. A rejection on a single
|
|
1009
|
+
* asset stubs that asset's deltas to '0' and emits `logger.warn`; one
|
|
1010
|
+
* bad asset never breaks the aggregate.
|
|
1011
|
+
*/
|
|
1012
|
+
deltaComputer?: DeltaComputer;
|
|
1013
|
+
/**
|
|
1014
|
+
* Optional logger. When provided, `getEarnings()` failures and
|
|
1015
|
+
* `deltaComputer` rejections are surfaced via `logger.warn` so ops can
|
|
1016
|
+
* distinguish a connector outage from "no earnings yet."
|
|
1017
|
+
*/
|
|
1018
|
+
logger?: AggregatorLogger;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Snapshot reader + `DeltaComputer` factory (Story 47.3).
|
|
1023
|
+
*
|
|
1024
|
+
* Reads `earnings-snapshots.jsonl` and computes TODAY/MONTH/YEAR deltas vs.
|
|
1025
|
+
* UTC boundaries (midnight, 1st-of-month, 1st-of-year). Tolerates malformed
|
|
1026
|
+
* lines (skip) and clock-skewed snapshots (filter `ts > now`). Returns `'0'`
|
|
1027
|
+
* when no boundary snapshot exists yet.
|
|
1028
|
+
*
|
|
1029
|
+
* @module
|
|
1030
|
+
* @since 47.3
|
|
1031
|
+
*/
|
|
1032
|
+
|
|
1033
|
+
/** ISO of the most recent UTC midnight <= ref. */
|
|
1034
|
+
declare function utcDayBoundary(ref: Date): string;
|
|
1035
|
+
/** ISO of the first instant of the current calendar month in UTC. */
|
|
1036
|
+
declare function utcMonthBoundary(ref: Date): string;
|
|
1037
|
+
/** ISO of the first instant of the current calendar year in UTC. */
|
|
1038
|
+
declare function utcYearBoundary(ref: Date): string;
|
|
1039
|
+
/**
|
|
1040
|
+
* Construct a `DeltaComputer` (Story 47.2's type) backed by the snapshot
|
|
1041
|
+
* file at `snapshotPath`. The returned function is the one wired into
|
|
1042
|
+
* `aggregateEarnings({ ..., deltaComputer })` by Story 47.4's route.
|
|
1043
|
+
*
|
|
1044
|
+
* Reads the snapshot file once per DeltaComputer call (single-pass), then
|
|
1045
|
+
* resolves all three boundaries (today/month/year) in-memory from the parsed
|
|
1046
|
+
* map. No cross-call cache in v1 — see Open Question 6 in story notes.
|
|
1047
|
+
*/
|
|
1048
|
+
declare function createDeltaComputer(opts: {
|
|
1049
|
+
snapshotPath: string;
|
|
1050
|
+
/** Optional clock injection for tests. Default `() => new Date()`. */
|
|
1051
|
+
now?: () => Date;
|
|
1052
|
+
}): DeltaComputer;
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Image manifest reader for Townhouse (Story 46.2).
|
|
1056
|
+
*
|
|
1057
|
+
* `~/.townhouse/image-manifest.json` is materialized by `compose-loader.ts`
|
|
1058
|
+
* during `townhouse hs up`. It maps node types to their digest-pinned image
|
|
1059
|
+
* refs, consumed by `POST /api/nodes` step 2 (pull image).
|
|
1060
|
+
*/
|
|
1061
|
+
|
|
1062
|
+
declare const ImageManifestSchema: z.ZodObject<{
|
|
1063
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
1064
|
+
townhouseVersion: z.ZodString;
|
|
1065
|
+
builtAt: z.ZodString;
|
|
1066
|
+
images: z.ZodObject<{
|
|
1067
|
+
'townhouse-api': z.ZodObject<{
|
|
1068
|
+
name: z.ZodString;
|
|
1069
|
+
tag: z.ZodString;
|
|
1070
|
+
digest: z.ZodString;
|
|
1071
|
+
}, "strict", z.ZodTypeAny, {
|
|
1072
|
+
name: string;
|
|
1073
|
+
tag: string;
|
|
1074
|
+
digest: string;
|
|
1075
|
+
}, {
|
|
1076
|
+
name: string;
|
|
1077
|
+
tag: string;
|
|
1078
|
+
digest: string;
|
|
1079
|
+
}>;
|
|
1080
|
+
town: z.ZodObject<{
|
|
1081
|
+
name: z.ZodString;
|
|
1082
|
+
tag: z.ZodString;
|
|
1083
|
+
digest: z.ZodString;
|
|
1084
|
+
}, "strict", z.ZodTypeAny, {
|
|
1085
|
+
name: string;
|
|
1086
|
+
tag: string;
|
|
1087
|
+
digest: string;
|
|
1088
|
+
}, {
|
|
1089
|
+
name: string;
|
|
1090
|
+
tag: string;
|
|
1091
|
+
digest: string;
|
|
1092
|
+
}>;
|
|
1093
|
+
mill: z.ZodObject<{
|
|
1094
|
+
name: z.ZodString;
|
|
1095
|
+
tag: z.ZodString;
|
|
1096
|
+
digest: z.ZodString;
|
|
1097
|
+
}, "strict", z.ZodTypeAny, {
|
|
1098
|
+
name: string;
|
|
1099
|
+
tag: string;
|
|
1100
|
+
digest: string;
|
|
1101
|
+
}, {
|
|
1102
|
+
name: string;
|
|
1103
|
+
tag: string;
|
|
1104
|
+
digest: string;
|
|
1105
|
+
}>;
|
|
1106
|
+
dvm: z.ZodObject<{
|
|
1107
|
+
name: z.ZodString;
|
|
1108
|
+
tag: z.ZodString;
|
|
1109
|
+
digest: z.ZodString;
|
|
1110
|
+
}, "strict", z.ZodTypeAny, {
|
|
1111
|
+
name: string;
|
|
1112
|
+
tag: string;
|
|
1113
|
+
digest: string;
|
|
1114
|
+
}, {
|
|
1115
|
+
name: string;
|
|
1116
|
+
tag: string;
|
|
1117
|
+
digest: string;
|
|
1118
|
+
}>;
|
|
1119
|
+
connector: z.ZodObject<{
|
|
1120
|
+
name: z.ZodString;
|
|
1121
|
+
tag: z.ZodString;
|
|
1122
|
+
digest: z.ZodString;
|
|
1123
|
+
}, "strict", z.ZodTypeAny, {
|
|
1124
|
+
name: string;
|
|
1125
|
+
tag: string;
|
|
1126
|
+
digest: string;
|
|
1127
|
+
}, {
|
|
1128
|
+
name: string;
|
|
1129
|
+
tag: string;
|
|
1130
|
+
digest: string;
|
|
1131
|
+
}>;
|
|
1132
|
+
}, "strict", z.ZodTypeAny, {
|
|
1133
|
+
town: {
|
|
1134
|
+
name: string;
|
|
1135
|
+
tag: string;
|
|
1136
|
+
digest: string;
|
|
1137
|
+
};
|
|
1138
|
+
mill: {
|
|
1139
|
+
name: string;
|
|
1140
|
+
tag: string;
|
|
1141
|
+
digest: string;
|
|
1142
|
+
};
|
|
1143
|
+
dvm: {
|
|
1144
|
+
name: string;
|
|
1145
|
+
tag: string;
|
|
1146
|
+
digest: string;
|
|
1147
|
+
};
|
|
1148
|
+
connector: {
|
|
1149
|
+
name: string;
|
|
1150
|
+
tag: string;
|
|
1151
|
+
digest: string;
|
|
1152
|
+
};
|
|
1153
|
+
'townhouse-api': {
|
|
1154
|
+
name: string;
|
|
1155
|
+
tag: string;
|
|
1156
|
+
digest: string;
|
|
1157
|
+
};
|
|
1158
|
+
}, {
|
|
1159
|
+
town: {
|
|
1160
|
+
name: string;
|
|
1161
|
+
tag: string;
|
|
1162
|
+
digest: string;
|
|
1163
|
+
};
|
|
1164
|
+
mill: {
|
|
1165
|
+
name: string;
|
|
1166
|
+
tag: string;
|
|
1167
|
+
digest: string;
|
|
1168
|
+
};
|
|
1169
|
+
dvm: {
|
|
1170
|
+
name: string;
|
|
1171
|
+
tag: string;
|
|
1172
|
+
digest: string;
|
|
1173
|
+
};
|
|
1174
|
+
connector: {
|
|
1175
|
+
name: string;
|
|
1176
|
+
tag: string;
|
|
1177
|
+
digest: string;
|
|
1178
|
+
};
|
|
1179
|
+
'townhouse-api': {
|
|
1180
|
+
name: string;
|
|
1181
|
+
tag: string;
|
|
1182
|
+
digest: string;
|
|
1183
|
+
};
|
|
1184
|
+
}>;
|
|
1185
|
+
}, "strict", z.ZodTypeAny, {
|
|
1186
|
+
schemaVersion: 1;
|
|
1187
|
+
townhouseVersion: string;
|
|
1188
|
+
builtAt: string;
|
|
1189
|
+
images: {
|
|
1190
|
+
town: {
|
|
1191
|
+
name: string;
|
|
1192
|
+
tag: string;
|
|
1193
|
+
digest: string;
|
|
1194
|
+
};
|
|
1195
|
+
mill: {
|
|
1196
|
+
name: string;
|
|
1197
|
+
tag: string;
|
|
1198
|
+
digest: string;
|
|
1199
|
+
};
|
|
1200
|
+
dvm: {
|
|
1201
|
+
name: string;
|
|
1202
|
+
tag: string;
|
|
1203
|
+
digest: string;
|
|
1204
|
+
};
|
|
1205
|
+
connector: {
|
|
1206
|
+
name: string;
|
|
1207
|
+
tag: string;
|
|
1208
|
+
digest: string;
|
|
1209
|
+
};
|
|
1210
|
+
'townhouse-api': {
|
|
1211
|
+
name: string;
|
|
1212
|
+
tag: string;
|
|
1213
|
+
digest: string;
|
|
1214
|
+
};
|
|
1215
|
+
};
|
|
1216
|
+
}, {
|
|
1217
|
+
schemaVersion: 1;
|
|
1218
|
+
townhouseVersion: string;
|
|
1219
|
+
builtAt: string;
|
|
1220
|
+
images: {
|
|
1221
|
+
town: {
|
|
1222
|
+
name: string;
|
|
1223
|
+
tag: string;
|
|
1224
|
+
digest: string;
|
|
1225
|
+
};
|
|
1226
|
+
mill: {
|
|
1227
|
+
name: string;
|
|
1228
|
+
tag: string;
|
|
1229
|
+
digest: string;
|
|
1230
|
+
};
|
|
1231
|
+
dvm: {
|
|
1232
|
+
name: string;
|
|
1233
|
+
tag: string;
|
|
1234
|
+
digest: string;
|
|
1235
|
+
};
|
|
1236
|
+
connector: {
|
|
1237
|
+
name: string;
|
|
1238
|
+
tag: string;
|
|
1239
|
+
digest: string;
|
|
1240
|
+
};
|
|
1241
|
+
'townhouse-api': {
|
|
1242
|
+
name: string;
|
|
1243
|
+
tag: string;
|
|
1244
|
+
digest: string;
|
|
1245
|
+
};
|
|
1246
|
+
};
|
|
1247
|
+
}>;
|
|
1248
|
+
type ImageManifest = z.infer<typeof ImageManifestSchema>;
|
|
1249
|
+
/**
|
|
1250
|
+
* Read and validate `image-manifest.json` at the given path.
|
|
1251
|
+
*
|
|
1252
|
+
* Throws ENOENT if the file is missing — there is no graceful fallback for a
|
|
1253
|
+
* missing manifest; it means `townhouse hs up` was not run first.
|
|
1254
|
+
* Throws `ZodError` with a useful path if the file is present but invalid.
|
|
1255
|
+
*/
|
|
1256
|
+
declare function readImageManifest(path: string): Promise<ImageManifest>;
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Boot reconciler (Story 46.1).
|
|
1260
|
+
*
|
|
1261
|
+
* Converges connector peer state to `~/.townhouse/nodes.yaml` (truth) on
|
|
1262
|
+
* every `townhouse hs up`. Reads yaml + connector peers, diffs them,
|
|
1263
|
+
* re-registers any yaml entries missing from the connector, and logs
|
|
1264
|
+
* connector peers without yaml entries as `'external'` (left alone).
|
|
1265
|
+
*
|
|
1266
|
+
* Container lifecycle is OUT of scope — that lives in Epic 46.2.
|
|
1267
|
+
*/
|
|
1268
|
+
|
|
1269
|
+
/** Action recorded for a single divergence. */
|
|
1270
|
+
type DivergenceAction = 'reregistered' | 'reregister-failed' | 'external';
|
|
1271
|
+
/** A single divergence record for the reconciler log. */
|
|
1272
|
+
interface DivergenceLog {
|
|
1273
|
+
timestamp: string;
|
|
1274
|
+
peerId: string;
|
|
1275
|
+
action: DivergenceAction;
|
|
1276
|
+
detail?: string;
|
|
1277
|
+
}
|
|
1278
|
+
/** Summary returned by `reconcile()` so callers can surface partial-failure counts. */
|
|
1279
|
+
interface ReconcileSummary {
|
|
1280
|
+
reregistered: number;
|
|
1281
|
+
failed: number;
|
|
1282
|
+
external: number;
|
|
1283
|
+
}
|
|
1284
|
+
declare class BootReconciler {
|
|
1285
|
+
private readonly adminClient;
|
|
1286
|
+
private readonly nodesYamlPath;
|
|
1287
|
+
private readonly reconcilerLogPath;
|
|
1288
|
+
private logDirEnsured;
|
|
1289
|
+
private logFileChmodEnsured;
|
|
1290
|
+
constructor(adminClient: Pick<ConnectorAdminClient, 'getPeers' | 'registerPeer'>, nodesYamlPath: string, reconcilerLogPath: string);
|
|
1291
|
+
/**
|
|
1292
|
+
* Diff `nodes.yaml` (truth) against `GET /admin/peers` (derived state)
|
|
1293
|
+
* and converge.
|
|
1294
|
+
*
|
|
1295
|
+
* Ordering rule (Epic 46.2 dependency — load-bearing):
|
|
1296
|
+
* `nodes.yaml` write happens BEFORE connector registration
|
|
1297
|
+
* (`POST /admin/peers`).
|
|
1298
|
+
*
|
|
1299
|
+
* The drift window resolves in the safe direction:
|
|
1300
|
+
* - yaml entry without a connector peer = harmless. The reconciler
|
|
1301
|
+
* re-registers it on next `hs up` (this method does that).
|
|
1302
|
+
* - connector peer without a yaml entry = treated as `'external'` and
|
|
1303
|
+
* left alone (operators may legitimately route non-Townhouse peers
|
|
1304
|
+
* through the same connector).
|
|
1305
|
+
*
|
|
1306
|
+
* The unsafe direction (register first, then write yaml) creates a
|
|
1307
|
+
* window where the connector routes to a peer Townhouse cannot clean
|
|
1308
|
+
* up. Epic 46.2's provisioning pipeline MUST honor the yaml-first rule.
|
|
1309
|
+
*
|
|
1310
|
+
* Failures fetching `getPeers()` are surfaced (not swallowed) so the
|
|
1311
|
+
* caller in `handleHsUp` can decide whether to treat reconciler
|
|
1312
|
+
* divergence as fatal. (Today: non-fatal — see cli.ts wire point.)
|
|
1313
|
+
*
|
|
1314
|
+
* Per-divergence appendLog failures are caught so a single log-write
|
|
1315
|
+
* failure does not abort the rest of the reconciliation pass.
|
|
1316
|
+
*/
|
|
1317
|
+
reconcile(): Promise<ReconcileSummary>;
|
|
1318
|
+
/**
|
|
1319
|
+
* Compute divergences without mutating the connector. Exposed for
|
|
1320
|
+
* testability — production callers use `reconcile()`.
|
|
1321
|
+
*/
|
|
1322
|
+
private diff;
|
|
1323
|
+
/**
|
|
1324
|
+
* Append one divergence record without aborting the whole reconciliation
|
|
1325
|
+
* pass on a single log-write failure (disk full, EACCES, etc.). Failures
|
|
1326
|
+
* are themselves logged to stderr — not silently swallowed — so the
|
|
1327
|
+
* operator can see them in the same `hs up` session.
|
|
1328
|
+
*/
|
|
1329
|
+
private tryAppendLog;
|
|
1330
|
+
/**
|
|
1331
|
+
* Append one divergence record to the reconciler log as a single line of
|
|
1332
|
+
* JSON (jsonl-style — easy to grep, easy to parse).
|
|
1333
|
+
*
|
|
1334
|
+
* `mkdir` runs once per reconciler instance. `chmod 0o600` on the log file
|
|
1335
|
+
* also runs once — `fs.appendFile`'s `mode` option only applies on
|
|
1336
|
+
* creation, so without a post-create chmod a pre-existing log file with
|
|
1337
|
+
* permissive mode would never be tightened.
|
|
1338
|
+
*/
|
|
1339
|
+
private appendLog;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
export { type AggregateEarningsInput, type AggregatedEarnings, type AggregatedEarningsStatus, type AggregatorLogger, type ApiDeps, type ApiServer, type BandwidthPayload, BootReconciler, ChainProviderEntry, ConfigValidationError, ConnectorAdminClient, ConnectorConfigGenerator, ConnectorRuntimeConfig, DEFAULT_ATOR_PROXY, type DeltaComputer, type DepositAddressEntry, type DepositAddressesPayload, type DivergenceAction, type DivergenceLog, DockerOrchestrator, type DvmHealthResponse, EncryptedWallet, type ImageManifest, ImageManifestSchema, type JobsByKindEntry, type JobsRecentPayload, type MetricsPayload, type MillHealthResponse, type MillSwapsRecentPayload, type NodeDetail, type NodeEarnings, type NodeHealthPayload, type NodeInfo, type NodeState, NodeType$1 as NodeType, type NodesYaml, type NodesYamlEntry, NodesYamlEntrySchema, NodesYamlSchema, type NostrEventPayload, type PacketTimeseriesPayload, PeerTypeResolver, 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 };
|