@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
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Townhouse configuration schema — TypeScript interfaces only.
|
|
3
|
+
* Runtime validation lives in validator.ts.
|
|
4
|
+
*/
|
|
5
|
+
interface TownNodeConfig {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
/** Nostr relay fee in millisatoshis per event */
|
|
8
|
+
feePerEvent?: number;
|
|
9
|
+
/** Docker image override */
|
|
10
|
+
image?: string;
|
|
11
|
+
}
|
|
12
|
+
interface ChainEndpoint {
|
|
13
|
+
rpcUrl: string;
|
|
14
|
+
wsUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
interface MillChainsConfig {
|
|
17
|
+
evm?: ChainEndpoint;
|
|
18
|
+
solana?: ChainEndpoint;
|
|
19
|
+
mina?: ChainEndpoint;
|
|
20
|
+
}
|
|
21
|
+
interface MillNodeConfig {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
/** Swap fee basis points (1 = 0.01%) */
|
|
24
|
+
feeBasisPoints?: number;
|
|
25
|
+
/** Docker image override */
|
|
26
|
+
image?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Chain RPC endpoints the mill should swap against (D2). The orchestrator
|
|
29
|
+
* does not currently forward this directly into MILL_CONFIG_JSON — it
|
|
30
|
+
* round-trips through YAML so the dashboard and future stories can read it.
|
|
31
|
+
*/
|
|
32
|
+
chains?: MillChainsConfig;
|
|
33
|
+
/** Enabled swap pairs, e.g. ['EVM<->SOL']. Informational; D2-introduced. */
|
|
34
|
+
pairs?: string[];
|
|
35
|
+
}
|
|
36
|
+
interface DvmNodeConfig {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
/** DVM job fee in millisatoshis */
|
|
39
|
+
feePerJob?: number;
|
|
40
|
+
/** Per-kind pricing in millisatoshis (key = stringified kind number) */
|
|
41
|
+
kindPricing?: Record<string, number>;
|
|
42
|
+
/** Docker image override */
|
|
43
|
+
image?: string;
|
|
44
|
+
}
|
|
45
|
+
interface NodesConfig {
|
|
46
|
+
town: TownNodeConfig;
|
|
47
|
+
mill: MillNodeConfig;
|
|
48
|
+
dvm: DvmNodeConfig;
|
|
49
|
+
}
|
|
50
|
+
interface WalletConfig {
|
|
51
|
+
/** Path to encrypted wallet file (no plaintext mnemonic in config) */
|
|
52
|
+
encrypted_path: string;
|
|
53
|
+
}
|
|
54
|
+
interface ConnectorConfig {
|
|
55
|
+
/** Docker image for the shared ILP connector */
|
|
56
|
+
image: string;
|
|
57
|
+
/** Admin API port */
|
|
58
|
+
adminPort: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Connector chain-provider entry — surfaces what the connector needs to spin
|
|
62
|
+
* up its settlement subsystem (AccountManager + ClaimReceiver). Without at
|
|
63
|
+
* least one entry, `/admin/earnings.json` returns 503 and Townhouse's
|
|
64
|
+
* earnings data plane breaks (Epic 47 BUG-1).
|
|
65
|
+
*
|
|
66
|
+
* Dev-Anvil deterministic placeholders are exposed via
|
|
67
|
+
* `DEFAULT_HS_CHAIN_PROVIDERS` in `defaults.ts`; operators running on real
|
|
68
|
+
* chains override this in their `config.yaml`.
|
|
69
|
+
*/
|
|
70
|
+
interface ChainProviderEntry {
|
|
71
|
+
/** Currently only 'evm' supported. */
|
|
72
|
+
chainType: 'evm';
|
|
73
|
+
/** Canonical chain id, e.g. 'evm:base:31337' (dev-Anvil) or 'evm:base:8453' (mainnet). */
|
|
74
|
+
chainId: string;
|
|
75
|
+
/** Chain RPC endpoint. May be a dead address in offline/demo mode. */
|
|
76
|
+
rpcUrl: string;
|
|
77
|
+
/** PaymentChannel registry contract. */
|
|
78
|
+
registryAddress: string;
|
|
79
|
+
/** Settlement token (USDC, etc.) contract. */
|
|
80
|
+
tokenAddress: string;
|
|
81
|
+
/** Hex private key the connector signs settlement claims with. */
|
|
82
|
+
keyId: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Hidden-service publication config (Story 35.5 of the connector repo).
|
|
86
|
+
*
|
|
87
|
+
* When set, the connector boots `@anyone-protocol/anyone-client` in-process,
|
|
88
|
+
* spawns the `anon` binary, publishes a v3 hidden service, and advertises
|
|
89
|
+
* its `wss://<addr>.anyone/btp` URL to peers. The keypair lives at `dir`
|
|
90
|
+
* inside the connector container and persists across restarts when that
|
|
91
|
+
* path is on a mounted volume.
|
|
92
|
+
*
|
|
93
|
+
* Operator surface (this type) is intentionally narrow; the connector's
|
|
94
|
+
* own config has more knobs (binaryPath, configFilePath) that we omit
|
|
95
|
+
* here until a real use case demands them.
|
|
96
|
+
*/
|
|
97
|
+
interface HiddenServiceConfig {
|
|
98
|
+
/** Path inside the connector container for hs_ed25519_secret_key etc. */
|
|
99
|
+
dir: string;
|
|
100
|
+
/** Hidden service port — peers dial <addr>.anyone:<port>. */
|
|
101
|
+
port: number;
|
|
102
|
+
/**
|
|
103
|
+
* Optional override of the externalUrl the connector advertises. Default
|
|
104
|
+
* is `"auto"` — the connector reads `${dir}/hostname` at startup and
|
|
105
|
+
* builds `wss://<hostname>.anyone/btp` itself.
|
|
106
|
+
*/
|
|
107
|
+
externalUrl?: string;
|
|
108
|
+
/** Optional override of the SDK's start-up readiness deadline (ms). */
|
|
109
|
+
startupTimeoutMs?: number;
|
|
110
|
+
/** Optional override of the SDK's shutdown deadline (ms). */
|
|
111
|
+
stopTimeoutMs?: number;
|
|
112
|
+
}
|
|
113
|
+
interface TransportConfig {
|
|
114
|
+
/** Transport mode: 'ator' for Tor-based, 'direct' for clearnet */
|
|
115
|
+
mode: 'ator' | 'direct';
|
|
116
|
+
/** SOCKS5 proxy address when using ator transport */
|
|
117
|
+
socksProxy?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Externally reachable BTP URL. Required when mode='ator' AND
|
|
120
|
+
* hiddenService is unset (operator runs their own anon binary external
|
|
121
|
+
* to the connector and is responsible for the URL). Ignored for
|
|
122
|
+
* mode='direct'. When hiddenService is set and externalUrl is unset,
|
|
123
|
+
* the generator emits the literal `"auto"` so the connector resolves
|
|
124
|
+
* the .anyone hostname from disk at startup.
|
|
125
|
+
*/
|
|
126
|
+
externalUrl?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Optional inbound hidden-service publication. When set, the connector
|
|
129
|
+
* manages its own anon binary and publishes a .anyone hidden service.
|
|
130
|
+
*/
|
|
131
|
+
hiddenService?: HiddenServiceConfig;
|
|
132
|
+
/**
|
|
133
|
+
* Town relay hidden service. When set, the orchestrator starts a second
|
|
134
|
+
* ator sidecar (parallel to any connector HS sidecar) that forwards
|
|
135
|
+
* inbound HS traffic to the town container's Nostr WebSocket port (7100),
|
|
136
|
+
* and the town container is configured to advertise the .anyone URL.
|
|
137
|
+
* Reuses HiddenServiceConfig — same shape as connector HS config.
|
|
138
|
+
*/
|
|
139
|
+
relayHiddenService?: HiddenServiceConfig;
|
|
140
|
+
}
|
|
141
|
+
interface ApiConfig {
|
|
142
|
+
/** Dashboard/API port */
|
|
143
|
+
port: number;
|
|
144
|
+
/** Bind address */
|
|
145
|
+
host: string;
|
|
146
|
+
}
|
|
147
|
+
interface LoggingConfig {
|
|
148
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
149
|
+
}
|
|
150
|
+
interface PresetMetadata {
|
|
151
|
+
/** Preset that produced this config (D2). */
|
|
152
|
+
name: 'demo';
|
|
153
|
+
/** Where the chain endpoints came from — leases.json path or 'local-fallback'. */
|
|
154
|
+
chainEndpointSource: string;
|
|
155
|
+
}
|
|
156
|
+
interface TownhouseConfig {
|
|
157
|
+
nodes: NodesConfig;
|
|
158
|
+
wallet: WalletConfig;
|
|
159
|
+
connector: ConnectorConfig;
|
|
160
|
+
transport: TransportConfig;
|
|
161
|
+
api: ApiConfig;
|
|
162
|
+
logging: LoggingConfig;
|
|
163
|
+
/**
|
|
164
|
+
* Connector chain providers — required for the connector's settlement
|
|
165
|
+
* subsystem (AccountManager + ClaimReceiver) to initialize. When unset on
|
|
166
|
+
* `townhouse hs up`, `hs-config-writer.ts` injects
|
|
167
|
+
* `DEFAULT_HS_CHAIN_PROVIDERS` so the earnings route returns 200 out of
|
|
168
|
+
* the box (Epic 47 BUG-1 product fix).
|
|
169
|
+
*/
|
|
170
|
+
chainProviders?: ChainProviderEntry[];
|
|
171
|
+
/** Present only when the config was generated by `init --preset=<name>`. */
|
|
172
|
+
preset?: PresetMetadata;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Docker orchestration types for Townhouse (Story 21.2).
|
|
177
|
+
*
|
|
178
|
+
* STUB FILE: Created by ATDD workflow (red phase).
|
|
179
|
+
* Implementation will be added during the green phase.
|
|
180
|
+
*/
|
|
181
|
+
/** Node types that can be orchestrated by Townhouse. */
|
|
182
|
+
type NodeType = 'town' | 'mill' | 'dvm';
|
|
183
|
+
/** Specification for a container to be created by the orchestrator. */
|
|
184
|
+
interface ContainerSpec {
|
|
185
|
+
/** Container name (e.g., 'townhouse-town') */
|
|
186
|
+
name: string;
|
|
187
|
+
/** Docker image to use */
|
|
188
|
+
image: string;
|
|
189
|
+
/** Environment variables to pass to the container */
|
|
190
|
+
env: Record<string, string>;
|
|
191
|
+
/** Network to attach the container to */
|
|
192
|
+
network: string;
|
|
193
|
+
/** Port mappings (host:container) */
|
|
194
|
+
ports?: Record<string, string>;
|
|
195
|
+
}
|
|
196
|
+
/** Events emitted by the orchestrator during operations. */
|
|
197
|
+
interface OrchestratorEvents {
|
|
198
|
+
/** Emitted during image pull with progress info */
|
|
199
|
+
pullProgress: {
|
|
200
|
+
image: string;
|
|
201
|
+
status: string;
|
|
202
|
+
id?: string;
|
|
203
|
+
progress?: string;
|
|
204
|
+
};
|
|
205
|
+
/** Emitted when a container changes state */
|
|
206
|
+
containerState: {
|
|
207
|
+
name: string;
|
|
208
|
+
state: 'creating' | 'starting' | 'running' | 'stopping' | 'stopped' | 'error';
|
|
209
|
+
/** Additional context for error states (e.g., error message) */
|
|
210
|
+
detail?: string;
|
|
211
|
+
};
|
|
212
|
+
/** Emitted during health check polling */
|
|
213
|
+
healthCheck: {
|
|
214
|
+
name: string;
|
|
215
|
+
status: string;
|
|
216
|
+
attempt: number;
|
|
217
|
+
};
|
|
218
|
+
/** Emitted before connector restart during peer registration update */
|
|
219
|
+
connectorRestarting: {
|
|
220
|
+
reason: string;
|
|
221
|
+
};
|
|
222
|
+
/** Emitted after connector restart and health check passes */
|
|
223
|
+
connectorRestarted: {
|
|
224
|
+
peers: string[];
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Options for health check polling. */
|
|
228
|
+
interface HealthCheckOptions {
|
|
229
|
+
/** Polling interval in milliseconds (default: 2000) */
|
|
230
|
+
interval?: number;
|
|
231
|
+
/** Timeout in milliseconds (default: 60000) */
|
|
232
|
+
timeout?: number;
|
|
233
|
+
}
|
|
234
|
+
/** Network I/O stats for a container (from dockerode stats stream) */
|
|
235
|
+
interface BandwidthStats {
|
|
236
|
+
bytesIn: number;
|
|
237
|
+
bytesOut: number;
|
|
238
|
+
sampleAt: number;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
type ComposeProfile = 'dev' | 'hs';
|
|
242
|
+
interface ComposeLoaderOptions {
|
|
243
|
+
/** Override default `~/.townhouse/` write target. Used by tests. */
|
|
244
|
+
townhouseHome?: string;
|
|
245
|
+
/** Override the package-relative dist directory the loader reads from.
|
|
246
|
+
* Defaults to the `dist/` adjacent to compose-loader.js at runtime.
|
|
247
|
+
* Tests use this to point at fixture directories. */
|
|
248
|
+
distDir?: string;
|
|
249
|
+
}
|
|
250
|
+
declare class ComposeLoaderError extends Error {
|
|
251
|
+
constructor(message: string);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Returns the rendered compose YAML for the requested profile.
|
|
255
|
+
* For 'hs', digest substitutions are already applied (resolved at build time).
|
|
256
|
+
* For 'dev', the YAML is returned verbatim (uses local `toon:*` image tags).
|
|
257
|
+
* Throws `ComposeLoaderError` if the requested profile's YAML is unreadable.
|
|
258
|
+
*/
|
|
259
|
+
declare function loadComposeTemplate(profile: ComposeProfile, options?: ComposeLoaderOptions): string;
|
|
260
|
+
/**
|
|
261
|
+
* Writes the resolved compose YAML to `<townhouseHome>/compose/<profile>.yml`
|
|
262
|
+
* and copies `dist/image-manifest.json` to `<townhouseHome>/image-manifest.json`.
|
|
263
|
+
* BOTH output files are written with mode 0o600 (NFR8 — operator-secret file mode).
|
|
264
|
+
* Returns the absolute paths of the two files written.
|
|
265
|
+
*/
|
|
266
|
+
declare function materializeComposeTemplate(profile: ComposeProfile, options?: ComposeLoaderOptions): {
|
|
267
|
+
composePath: string;
|
|
268
|
+
manifestPath: string;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Wallet management types for Townhouse (Story 21.4).
|
|
273
|
+
*
|
|
274
|
+
* Defines interfaces for HD wallet key derivation, encryption at rest,
|
|
275
|
+
* and node key management.
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
/** Configuration for the WalletManager */
|
|
279
|
+
interface WalletManagerConfig {
|
|
280
|
+
/** Path to encrypted wallet file */
|
|
281
|
+
encryptedPath: string;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Arweave JWK (RSA-4096) — matches `arweave-js` / `@ardrive/turbo-sdk`
|
|
285
|
+
* `JWKInterface` shape. Defined locally to avoid an arweave-js dependency.
|
|
286
|
+
*
|
|
287
|
+
* All fields are base64url-encoded big-endian integers per JWA (RFC 7518).
|
|
288
|
+
*/
|
|
289
|
+
interface ArweaveJwk {
|
|
290
|
+
kty: 'RSA';
|
|
291
|
+
/** Public exponent (typically "AQAB" = 65537) */
|
|
292
|
+
e: string;
|
|
293
|
+
/** Modulus n (base64url) */
|
|
294
|
+
n: string;
|
|
295
|
+
/** Private exponent d */
|
|
296
|
+
d?: string;
|
|
297
|
+
/** First prime factor p */
|
|
298
|
+
p?: string;
|
|
299
|
+
/** Second prime factor q */
|
|
300
|
+
q?: string;
|
|
301
|
+
/** d mod (p-1) */
|
|
302
|
+
dp?: string;
|
|
303
|
+
/** d mod (q-1) */
|
|
304
|
+
dq?: string;
|
|
305
|
+
/** q^-1 mod p */
|
|
306
|
+
qi?: string;
|
|
307
|
+
}
|
|
308
|
+
/** Keys derived for a specific node type */
|
|
309
|
+
interface NodeKeys {
|
|
310
|
+
/** Nostr public key (hex-encoded, 32 bytes) */
|
|
311
|
+
nostrPubkey: string;
|
|
312
|
+
/** Nostr secret key (raw 32-byte scalar) */
|
|
313
|
+
nostrSecretKey: Uint8Array;
|
|
314
|
+
/** EVM address (checksummed, 0x-prefixed) */
|
|
315
|
+
evmAddress: string;
|
|
316
|
+
/** EVM private key (raw 32 bytes) */
|
|
317
|
+
evmPrivateKey: Uint8Array;
|
|
318
|
+
/** BIP-44 derivation path used for Nostr key */
|
|
319
|
+
nostrDerivationPath: string;
|
|
320
|
+
/** BIP-44 derivation path used for EVM key */
|
|
321
|
+
evmDerivationPath: string;
|
|
322
|
+
/** Base58-encoded Solana public key — derived for all node types */
|
|
323
|
+
solanaAddress?: string;
|
|
324
|
+
/** Raw 32-byte Ed25519 Solana private key seed */
|
|
325
|
+
solanaPrivateKey?: Uint8Array;
|
|
326
|
+
/** BIP-44 derivation path used for Solana key (SLIP-0010 all-hardened) */
|
|
327
|
+
solanaDerivationPath?: string;
|
|
328
|
+
/** Mina public key hex — mill only, omitted for town/dvm */
|
|
329
|
+
minaAddress?: string;
|
|
330
|
+
/** Arweave wallet address (base64url SHA-256 of modulus) — DVM only */
|
|
331
|
+
arweaveAddress?: string;
|
|
332
|
+
/**
|
|
333
|
+
* Arweave RSA-4096 JWK — DVM only. The full private JWK is held in
|
|
334
|
+
* memory for credit-buy + upload signing. Zeroed by `lock()`.
|
|
335
|
+
*/
|
|
336
|
+
arweaveJwk?: ArweaveJwk;
|
|
337
|
+
/** BIP-44 derivation path used for the Arweave RSA sub-seed */
|
|
338
|
+
arweaveDerivationPath?: string;
|
|
339
|
+
}
|
|
340
|
+
/** Map of node type to its derived keys */
|
|
341
|
+
interface DerivedNodeKeys {
|
|
342
|
+
town: NodeKeys;
|
|
343
|
+
mill: NodeKeys;
|
|
344
|
+
dvm: NodeKeys;
|
|
345
|
+
}
|
|
346
|
+
/** Summary info for display (no secrets) */
|
|
347
|
+
interface NodeKeyInfo {
|
|
348
|
+
/** Node type */
|
|
349
|
+
nodeType: NodeType;
|
|
350
|
+
/** Nostr public key (hex) */
|
|
351
|
+
nostrPubkey: string;
|
|
352
|
+
/** EVM address (checksummed) */
|
|
353
|
+
evmAddress: string;
|
|
354
|
+
/** Nostr derivation path */
|
|
355
|
+
nostrDerivationPath: string;
|
|
356
|
+
/** EVM derivation path */
|
|
357
|
+
evmDerivationPath: string;
|
|
358
|
+
/** Base58-encoded Solana public key — derived for all node types */
|
|
359
|
+
solanaAddress?: string;
|
|
360
|
+
/** Solana derivation path — present whenever solanaAddress is */
|
|
361
|
+
solanaDerivationPath?: string;
|
|
362
|
+
/** Mina public key hex — mill only, omitted for town/dvm */
|
|
363
|
+
minaAddress?: string;
|
|
364
|
+
/** Arweave wallet address (base64url) — DVM only */
|
|
365
|
+
arweaveAddress?: string;
|
|
366
|
+
/** Arweave derivation path — present whenever arweaveAddress is */
|
|
367
|
+
arweaveDerivationPath?: string;
|
|
368
|
+
}
|
|
369
|
+
/** Persisted wallet state (in memory after decryption) */
|
|
370
|
+
interface WalletState {
|
|
371
|
+
/** All derived node keys */
|
|
372
|
+
keys: DerivedNodeKeys;
|
|
373
|
+
/**
|
|
374
|
+
* BIP-39 mnemonic held in memory for transient re-derivation.
|
|
375
|
+
* Cleared on lock() by setting this.state = null. Never serialized.
|
|
376
|
+
*/
|
|
377
|
+
mnemonic: string;
|
|
378
|
+
}
|
|
379
|
+
/** Encrypted wallet file format (JSON, all fields base64-encoded) */
|
|
380
|
+
interface EncryptedWallet {
|
|
381
|
+
/** scrypt salt (base64) */
|
|
382
|
+
salt: string;
|
|
383
|
+
/** AES-GCM initialization vector (base64) */
|
|
384
|
+
iv: string;
|
|
385
|
+
/** AES-256-GCM ciphertext (base64) */
|
|
386
|
+
ciphertext: string;
|
|
387
|
+
/** AES-GCM authentication tag (base64) */
|
|
388
|
+
tag: string;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* WalletManager — HD key derivation for Townhouse (Story 21.4, Task 1).
|
|
393
|
+
*
|
|
394
|
+
* Single BIP-39 mnemonic, deterministic HD derivation per node type.
|
|
395
|
+
* Uses BIP-44 paths with distinct account indices per node type:
|
|
396
|
+
* - Town: account 0
|
|
397
|
+
* - Mill: account 1
|
|
398
|
+
* - DVM: account 2
|
|
399
|
+
*
|
|
400
|
+
* Nostr keys use NIP-06 coin type 1237: m/44'/1237'/{account}'/0/0
|
|
401
|
+
* EVM keys use standard coin type 60: m/44'/60'/{account}'/0/0
|
|
402
|
+
* Solana keys (all node types) coin 501: m/44'/501'/{account}'/0'/0'
|
|
403
|
+
* (SLIP-0010 all-hardened, via @toon-protocol/mill::deriveMillKeys)
|
|
404
|
+
* Arweave keys (DVM only) coin 472: m/44'/472'/2'/0/0
|
|
405
|
+
* (32-byte BIP-32 sub-seed feeds a deterministic RSA-4096 PRNG via
|
|
406
|
+
* rsa-from-seed.ts (HMAC-DRBG + node-forge 1.3.3). Derivation takes 5–30s per DVM unlock; this runs
|
|
407
|
+
* once at unlock time, not per operation.)
|
|
408
|
+
*/
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* WalletManager handles mnemonic generation, key derivation, and in-memory
|
|
412
|
+
* key lifecycle for Townhouse node operations.
|
|
413
|
+
*/
|
|
414
|
+
declare class WalletManager {
|
|
415
|
+
private readonly config;
|
|
416
|
+
private state;
|
|
417
|
+
constructor(config: WalletManagerConfig);
|
|
418
|
+
/** Path to the encrypted wallet file */
|
|
419
|
+
get encryptedPath(): string;
|
|
420
|
+
/**
|
|
421
|
+
* Generate a new 12-word BIP-39 mnemonic and derive all node keys.
|
|
422
|
+
* Returns the mnemonic (for one-time display) and the derived state.
|
|
423
|
+
*/
|
|
424
|
+
generate(): Promise<{
|
|
425
|
+
mnemonic: string;
|
|
426
|
+
state: WalletState;
|
|
427
|
+
}>;
|
|
428
|
+
/**
|
|
429
|
+
* Import an existing mnemonic (12 or 24 words) and derive all node keys.
|
|
430
|
+
* Throws if mnemonic is invalid (wrong checksum, wrong word count, etc).
|
|
431
|
+
*/
|
|
432
|
+
fromMnemonic(mnemonic: string): Promise<WalletState>;
|
|
433
|
+
/**
|
|
434
|
+
* Get derived keys for a specific node type.
|
|
435
|
+
* Throws if wallet has not been initialized (call generate() or fromMnemonic() first).
|
|
436
|
+
*/
|
|
437
|
+
getNodeKeys(nodeType: NodeType): NodeKeys;
|
|
438
|
+
/**
|
|
439
|
+
* Get display-safe info for all node types (no secrets).
|
|
440
|
+
*/
|
|
441
|
+
getAllKeys(): NodeKeyInfo[];
|
|
442
|
+
/**
|
|
443
|
+
* List keys for all node types (alias for getAllKeys for API compatibility).
|
|
444
|
+
*/
|
|
445
|
+
listKeys(): NodeKeyInfo[];
|
|
446
|
+
/**
|
|
447
|
+
* Zero all in-memory key material. After calling lock(),
|
|
448
|
+
* getNodeKeys() and getAllKeys() will throw.
|
|
449
|
+
*/
|
|
450
|
+
lock(): void;
|
|
451
|
+
/**
|
|
452
|
+
* Return the BIP-39 mnemonic from in-memory wallet state.
|
|
453
|
+
* Returns null when the wallet is locked or not initialized.
|
|
454
|
+
*/
|
|
455
|
+
getMnemonic(): string | null;
|
|
456
|
+
/**
|
|
457
|
+
* Get derived keys for a specific node type at a given derivation index.
|
|
458
|
+
*
|
|
459
|
+
* Pure derivation — does NOT mutate `state`. Re-derives from the stored
|
|
460
|
+
* mnemonic each time it is called. For every node type, also derives the
|
|
461
|
+
* Solana key at the same account index. For 'dvm', also derives Arweave.
|
|
462
|
+
* Throws if the wallet is locked.
|
|
463
|
+
*
|
|
464
|
+
* v1 callers MUST pass `derivationIndex = ACCOUNT_INDEX_{type}` for the
|
|
465
|
+
* first (and only) instance per type. Multi-instance support is out of
|
|
466
|
+
* scope for v1 — the route layer enforces single-instance-per-type.
|
|
467
|
+
*/
|
|
468
|
+
deriveNodeKey(type: NodeType, derivationIndex: number): Promise<NodeKeys>;
|
|
469
|
+
/**
|
|
470
|
+
* Returns the EVM private key for a node as a 64-char lowercase hex string.
|
|
471
|
+
* Throws when the wallet is locked. Callers MUST treat the returned string
|
|
472
|
+
* as sensitive (no logging, no persisting). The underlying Uint8Array is
|
|
473
|
+
* still owned by WalletManager and will be zeroed by `lock()`.
|
|
474
|
+
*/
|
|
475
|
+
getEvmPrivateKeyHex(nodeType: NodeType): string;
|
|
476
|
+
/**
|
|
477
|
+
* Returns the Solana Ed25519 private key seed for a node as a 64-char
|
|
478
|
+
* lowercase hex string (32 raw seed bytes). Throws when the wallet is
|
|
479
|
+
* locked or when the Solana key was not derived for this node type.
|
|
480
|
+
*/
|
|
481
|
+
getSolanaPrivateKeyHex(nodeType: NodeType): string;
|
|
482
|
+
/**
|
|
483
|
+
* Returns the Arweave RSA JWK for a node. Throws when the wallet is locked
|
|
484
|
+
* or when AR derivation has not yet been triggered for this node type.
|
|
485
|
+
*
|
|
486
|
+
* Callers MUST `await ensureArweaveKey(nodeType)` first the first time per
|
|
487
|
+
* unlock — RSA-4096 derivation is 5–30s and is therefore not done eagerly
|
|
488
|
+
* at `fromMnemonic`/`generate` time. After the first `ensureArweaveKey`
|
|
489
|
+
* the JWK is cached on the in-memory state until `lock()`.
|
|
490
|
+
*/
|
|
491
|
+
getArweaveJwk(nodeType: NodeType): ArweaveJwk;
|
|
492
|
+
/**
|
|
493
|
+
* Lazily derive the Arweave RSA-4096 JWK for a node type and cache it on
|
|
494
|
+
* the in-memory wallet state. Subsequent calls (within the same unlocked
|
|
495
|
+
* session) return the cached result without re-deriving.
|
|
496
|
+
*
|
|
497
|
+
* Only meaningful for node types that participate in the Arweave credit
|
|
498
|
+
* flow — `dvm` (account 2) in the current Townhouse layout. Calling on
|
|
499
|
+
* `town` or `mill` will derive a valid AR key at the corresponding
|
|
500
|
+
* account index but those keys are not used by any current code path.
|
|
501
|
+
*
|
|
502
|
+
* Throws if the wallet is locked or RSA derivation fails (unsupported
|
|
503
|
+
* platform, etc.). On success the result is also reflected in subsequent
|
|
504
|
+
* `getAllKeys()` calls (arweaveAddress + arweaveDerivationPath fields).
|
|
505
|
+
*/
|
|
506
|
+
ensureArweaveKey(nodeType: NodeType, password?: string): Promise<ArweaveJwk>;
|
|
507
|
+
/**
|
|
508
|
+
* Derive keys for all node types from a mnemonic.
|
|
509
|
+
*/
|
|
510
|
+
private deriveAllKeys;
|
|
511
|
+
/**
|
|
512
|
+
* Derive Nostr + EVM keys for a specific node type.
|
|
513
|
+
* Accepts an optional `accountIndex` to override the default per-type index.
|
|
514
|
+
* When omitted, uses `NODE_ACCOUNT_INDEX[nodeType]` (existing behavior).
|
|
515
|
+
*/
|
|
516
|
+
private deriveNodeKeys;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export { type ApiConfig as A, type BandwidthStats as B, type ComposeLoaderOptions as C, type DerivedNodeKeys as D, type EncryptedWallet as E, type HealthCheckOptions as H, type LoggingConfig as L, type MillNodeConfig as M, type NodeType as N, type OrchestratorEvents as O, type TownhouseConfig as T, WalletManager as W, type ComposeProfile as a, ComposeLoaderError as b, type ConnectorConfig as c, type ContainerSpec as d, type DvmNodeConfig as e, type NodeKeyInfo as f, type NodeKeys as g, type NodesConfig as h, type TownNodeConfig as i, type TransportConfig as j, type WalletConfig as k, type WalletManagerConfig as l, type WalletState as m, loadComposeTemplate as n, materializeComposeTemplate as o };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
2
|
+
import "./chunk-I2R4CRUX.js";
|
|
3
|
+
|
|
4
|
+
// src/wallet/rsa-from-seed.ts
|
|
5
|
+
import { hmac } from "@noble/hashes/hmac";
|
|
6
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
7
|
+
function hmacSha256(key, ...parts) {
|
|
8
|
+
const h = hmac.create(sha256, key);
|
|
9
|
+
for (const p of parts) h.update(p);
|
|
10
|
+
return h.digest();
|
|
11
|
+
}
|
|
12
|
+
var BYTE_00 = new Uint8Array([0]);
|
|
13
|
+
var BYTE_01 = new Uint8Array([1]);
|
|
14
|
+
function makeHmacDrbgPrng(seedBytes, rawEncode) {
|
|
15
|
+
let K = new Uint8Array(32).fill(0);
|
|
16
|
+
let V = new Uint8Array(32).fill(1);
|
|
17
|
+
K = hmacSha256(K, V, BYTE_00, seedBytes);
|
|
18
|
+
V = hmacSha256(K, V);
|
|
19
|
+
K = hmacSha256(K, V, BYTE_01, seedBytes);
|
|
20
|
+
V = hmacSha256(K, V);
|
|
21
|
+
return {
|
|
22
|
+
getBytesSync(size) {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let total = 0;
|
|
25
|
+
while (total < size) {
|
|
26
|
+
V = hmacSha256(K, V);
|
|
27
|
+
chunks.push(V);
|
|
28
|
+
total += V.length;
|
|
29
|
+
}
|
|
30
|
+
const flat = new Uint8Array(total);
|
|
31
|
+
let offset = 0;
|
|
32
|
+
for (const c of chunks) {
|
|
33
|
+
flat.set(c, offset);
|
|
34
|
+
offset += c.length;
|
|
35
|
+
}
|
|
36
|
+
const out = flat.slice(0, size);
|
|
37
|
+
K = hmacSha256(K, V, BYTE_00);
|
|
38
|
+
V = hmacSha256(K, V);
|
|
39
|
+
return rawEncode(out);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function rsaPrivateKeyPemFromSeed(seed) {
|
|
44
|
+
const forge = (await import("node-forge")).default;
|
|
45
|
+
const prng = makeHmacDrbgPrng(
|
|
46
|
+
seed,
|
|
47
|
+
(bytes) => forge.util.binary.raw.encode(bytes)
|
|
48
|
+
);
|
|
49
|
+
const { privateKey } = await new Promise((resolve, reject) => {
|
|
50
|
+
forge.pki.rsa.generateKeyPair(
|
|
51
|
+
4096,
|
|
52
|
+
65537,
|
|
53
|
+
{ prng, algorithm: "PRIMEINC" },
|
|
54
|
+
(err, kp) => err ? reject(err) : resolve(kp)
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
return forge.pki.privateKeyToPem(privateKey);
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
rsaPrivateKeyPemFromSeed
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=rsa-from-seed-VMNLNDZM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/wallet/rsa-from-seed.ts"],"sourcesContent":["/**\n * rsa-from-seed — CVE-free replacement for human-crypto-keys RSA derivation.\n *\n * Produces byte-for-byte identical RSA-4096 keys as\n * `human-crypto-keys.getKeyPairFromSeed(seed, {id:'rsa', modulusLength:4096},\n * {privateKeyFormat:'pkcs1-pem'})`.\n *\n * Algorithm (preserved exactly to maintain golden test vectors):\n * 1. HMAC-DRBG(SHA-256, entropy=seed, nonce=[], pers=[])\n * Re-implemented with @noble/hashes (already a workspace dep).\n * Byte-for-byte identical to hmac-drbg@1.0.1 used by human-crypto-keys.\n * 2. node-forge 1.3.3 (CVE-free) PRIMEINC prime search driven by (1).\n * Identical to node-forge 0.8.5 because jsbn.js and the PRIMEINC loop\n * are unchanged between versions (verified by diff).\n * 3. Export private key as PKCS#1 PEM string.\n *\n * Why not Node.js crypto.generateKeyPair?\n * It uses OpenSSL's system CSPRNG and cannot be seeded deterministically.\n *\n * Performance: same as before (~5–30s for RSA-4096 on a 2024 desktop).\n * The on-disk ar-cache.ts already amortises this cost after first derivation.\n */\n\nimport { hmac } from '@noble/hashes/hmac';\nimport { sha256 } from '@noble/hashes/sha256';\n\n// ── HMAC-DRBG (SP 800-90A, SHA-256 variant) ─────────────────────────────────\n//\n// Replicates hmac-drbg@1.0.1 with one key behavioural detail:\n// generate(n) in hmac-drbg discards leftover bytes and calls _update()\n// AFTER each call (not per 32-byte V-block). Our getBytesSync() mirrors\n// that exact behaviour: generate fresh V-chain on every call, discard\n// surplus, then do a 2-step _update (no seed data ⇒ only K/V steps 1+2).\n\nfunction hmacSha256(key: Uint8Array, ...parts: Uint8Array[]): Uint8Array {\n const h = hmac.create(sha256, key);\n for (const p of parts) h.update(p);\n return h.digest();\n}\n\nconst BYTE_00 = new Uint8Array([0x00]);\nconst BYTE_01 = new Uint8Array([0x01]);\n\n/**\n * Create a HMAC-DRBG PRNG that returns forge-compatible \"binary strings\"\n * (Latin-1 encoded byte strings used internally by node-forge).\n *\n * @param seedBytes 32-byte BIP-32 sub-seed (raw entropy; hex-decoding step\n * from human-crypto-keys is a no-op and not replicated).\n * @param rawEncode `forge.util.binary.raw.encode` — converts Uint8Array to\n * forge's internal binary string format.\n */\nfunction makeHmacDrbgPrng(\n seedBytes: Uint8Array,\n rawEncode: (bytes: Uint8Array) => string\n): { getBytesSync: (n: number) => string } {\n // Initialise K and V per SP 800-90A §10.1.2.3\n let K = new Uint8Array(32).fill(0x00);\n let V = new Uint8Array(32).fill(0x01);\n\n // _update(seed) — seeding phase (seed data present → full 4-step update)\n K = hmacSha256(K, V, BYTE_00, seedBytes);\n V = hmacSha256(K, V);\n K = hmacSha256(K, V, BYTE_01, seedBytes);\n V = hmacSha256(K, V);\n\n return {\n getBytesSync(size: number): string {\n // Generate V-chain until we have enough bytes (same as hmac-drbg's\n // `while (temp.length < len) { V = HMAC(K, V); temp.concat(V); }`).\n // Surplus bytes beyond `size` are discarded on purpose — matches the\n // original `temp.slice(0, len)` behaviour that ensures determinism.\n const chunks: Uint8Array[] = [];\n let total = 0;\n while (total < size) {\n V = hmacSha256(K, V);\n chunks.push(V);\n total += V.length;\n }\n // Flatten and slice\n const flat = new Uint8Array(total);\n let offset = 0;\n for (const c of chunks) {\n flat.set(c, offset);\n offset += c.length;\n }\n const out = flat.slice(0, size);\n\n // _update(undefined) — post-generate state update (2-step only,\n // since there is no additional data; mirrors the `if(!seed) return`\n // branch in hmac-drbg._update).\n K = hmacSha256(K, V, BYTE_00);\n V = hmacSha256(K, V);\n\n return rawEncode(out);\n },\n };\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Derive a deterministic RSA-4096 PKCS#1 PEM private key from a 32-byte seed.\n *\n * Identical output to:\n * `human-crypto-keys.getKeyPairFromSeed(seed, {id:'rsa', modulusLength:4096},\n * {privateKeyFormat:'pkcs1-pem'}).privateKey`\n *\n * @param seed 32-byte BIP-32 sub-seed. Zeroed by caller after this returns.\n * @returns PKCS#1 PEM-encoded RSA-4096 private key string.\n */\nexport async function rsaPrivateKeyPemFromSeed(\n seed: Uint8Array\n): Promise<string> {\n // Lazy-import node-forge so callers that never touch Arweave don't pay the\n // module-load cost. node-forge is a CJS module; dynamic import wraps it.\n const forge = (await import('node-forge')).default;\n\n const prng = makeHmacDrbgPrng(seed, (bytes) =>\n forge.util.binary.raw.encode(bytes)\n );\n\n // Promisify the callback-based generateKeyPair\n const { privateKey } = await new Promise<{\n privateKey: forge.pki.rsa.PrivateKey;\n publicKey: forge.pki.rsa.PublicKey;\n }>((resolve, reject) => {\n forge.pki.rsa.generateKeyPair(\n 4096,\n 65537,\n { prng, algorithm: 'PRIMEINC' },\n (err, kp) => (err ? reject(err) : resolve(kp))\n );\n });\n\n return forge.pki.privateKeyToPem(privateKey);\n}\n"],"mappings":";;;;AAuBA,SAAS,YAAY;AACrB,SAAS,cAAc;AAUvB,SAAS,WAAW,QAAoB,OAAiC;AACvE,QAAM,IAAI,KAAK,OAAO,QAAQ,GAAG;AACjC,aAAW,KAAK,MAAO,GAAE,OAAO,CAAC;AACjC,SAAO,EAAE,OAAO;AAClB;AAEA,IAAM,UAAU,IAAI,WAAW,CAAC,CAAI,CAAC;AACrC,IAAM,UAAU,IAAI,WAAW,CAAC,CAAI,CAAC;AAWrC,SAAS,iBACP,WACA,WACyC;AAEzC,MAAI,IAAI,IAAI,WAAW,EAAE,EAAE,KAAK,CAAI;AACpC,MAAI,IAAI,IAAI,WAAW,EAAE,EAAE,KAAK,CAAI;AAGpC,MAAI,WAAW,GAAG,GAAG,SAAS,SAAS;AACvC,MAAI,WAAW,GAAG,CAAC;AACnB,MAAI,WAAW,GAAG,GAAG,SAAS,SAAS;AACvC,MAAI,WAAW,GAAG,CAAC;AAEnB,SAAO;AAAA,IACL,aAAa,MAAsB;AAKjC,YAAM,SAAuB,CAAC;AAC9B,UAAI,QAAQ;AACZ,aAAO,QAAQ,MAAM;AACnB,YAAI,WAAW,GAAG,CAAC;AACnB,eAAO,KAAK,CAAC;AACb,iBAAS,EAAE;AAAA,MACb;AAEA,YAAM,OAAO,IAAI,WAAW,KAAK;AACjC,UAAI,SAAS;AACb,iBAAW,KAAK,QAAQ;AACtB,aAAK,IAAI,GAAG,MAAM;AAClB,kBAAU,EAAE;AAAA,MACd;AACA,YAAM,MAAM,KAAK,MAAM,GAAG,IAAI;AAK9B,UAAI,WAAW,GAAG,GAAG,OAAO;AAC5B,UAAI,WAAW,GAAG,CAAC;AAEnB,aAAO,UAAU,GAAG;AAAA,IACtB;AAAA,EACF;AACF;AAcA,eAAsB,yBACpB,MACiB;AAGjB,QAAM,SAAS,MAAM,OAAO,YAAY,GAAG;AAE3C,QAAM,OAAO;AAAA,IAAiB;AAAA,IAAM,CAAC,UACnC,MAAM,KAAK,OAAO,IAAI,OAAO,KAAK;AAAA,EACpC;AAGA,QAAM,EAAE,WAAW,IAAI,MAAM,IAAI,QAG9B,CAAC,SAAS,WAAW;AACtB,UAAM,IAAI,IAAI;AAAA,MACZ;AAAA,MACA;AAAA,MACA,EAAE,MAAM,WAAW,WAAW;AAAA,MAC9B,CAAC,KAAK,OAAQ,MAAM,OAAO,GAAG,IAAI,QAAQ,EAAE;AAAA,IAC9C;AAAA,EACF,CAAC;AAED,SAAO,MAAM,IAAI,gBAAgB,UAAU;AAC7C;","names":[]}
|