@toon-protocol/client 0.9.0 → 0.9.2

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 CHANGED
@@ -1,15 +1,29 @@
1
1
  # @toon-protocol/client
2
2
 
3
- High-level TypeScript client for publishing Nostr events to the TOON protocol an ILP-gated Nostr relay that enables sustainable relay operation through micropayments.
3
+ The **client library** for TOON Protocol — _pay-to-write Nostr over Interledger (ILP)_. Use it to **pay for and publish** writes to a network of service nodes. **Reads are free; writes cost a signed EIP-712 payment-channel claim** against an on-chain deposit.
4
+
5
+ > **`client` vs `townhouse`.** This package (`@toon-protocol/client`) is what an _app or end user_ uses to **pay** and publish. It does **not** run any relay or node. The nodes are operated separately by **`@toon-protocol/townhouse`** (the operator product, which runs an _apex_ connector plus `town` / `mill` / `dvm` children). Don't confuse the **client** (pays) with **townhouse** (operates), or **`town`** (a single Nostr-relay node) with **townhouse** (the whole operator stack).
6
+
7
+ ## Which call pays which node
8
+
9
+ Every write is an ILP packet carrying a signed payment-channel claim. The client reaches all node types **through a townhouse apex** (`g.townhouse`): the apex validates the claim, takes its fee, and forwards the packet to the destination node, which returns FULFILL (accepted) or REJECT. The method you call determines which node type you pay:
10
+
11
+ | Client call | Node type | What it does |
12
+ | --------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `client.publishEvent(event)` | **town** | Publish a Nostr event (e.g. `kind:1`) to the relay. |
14
+ | `requestBlobStorage(client, …)` | **dvm** | NIP-90 compute/storage. Builds and publishes a `kind:5094` event that uploads a blob to Arweave — the job request **is** the payment — and decodes the Arweave tx ID from the FULFILL response. |
15
+ | `client.sendSwapPacket(…)` | **mill** | Multi-chain token swap (low-level). Most callers use the higher-level `streamSwap()` from `@toon-protocol/sdk`, which is built on `sendSwapPacket`. |
4
16
 
5
17
  ## What It Does
6
18
 
7
19
  This client handles:
8
20
 
9
- - **ILP Micropayments**: Pay to publish Nostr events (read is free)
21
+ - **ILP Micropayments**: Pay to publish Nostr events (reads are free)
10
22
  - **Payment Channels**: Automatic on-chain channel creation with off-chain settlement via signed balance proofs
11
- - **Unified Identity**: One Nostr key = one EVM address (both use secp256k1, derived automatically)
12
- - **Multi-Hop Routing**: Publish to any destination address, not just your direct peer
23
+ - **Unified Identity**: One Nostr key = one EVM address (both secp256k1, derived automatically) — or a single BIP-39 **mnemonic** to derive a full multi-chain identity
24
+ - **Multi-Chain Settlement**: Sign payment-channel claims on EVM (EIP-712), Solana (Ed25519), and Mina (Pallas) from one mnemonic. A Townhouse apex validates the claim and redeems it on-chain on the matching chain (EVM/Solana credit the recipient; Mina redeems each claim on-chain with recipient credit-at-close deferred — see [Multi-Chain Settlement notes](#identity--multi-chain-settlement))
25
+ - **Multi-Hop Routing**: Publish to any destination address (`destination` / `destinationAddress`), not just your direct peer
26
+ - **Private Transport**: Optionally reach a townhouse apex at its ATOR `.anyone` address through a SOCKS5h proxy (see [Connecting over `.anyone`](#connecting-to-an-apex-over-anyone))
13
27
  - **Network Bootstrap**: Automatically discover and register with ILP peers via NIP-02 follow lists
14
28
  - **TOON Encoding**: Native binary format for agent-friendly event encoding
15
29
 
@@ -17,32 +31,47 @@ This client handles:
17
31
 
18
32
  ```bash
19
33
  pnpm add @toon-protocol/client @toon-protocol/core @toon-protocol/relay nostr-tools
34
+
35
+ # Optional — only needed to derive/sign on Mina (Pallas):
36
+ pnpm add mina-signer
20
37
  ```
21
38
 
22
39
  ## Prerequisites
23
40
 
24
- The client requires external services. Use the SDK E2E infrastructure for local development:
25
-
26
- ```bash
27
- # Start SDK E2E infrastructure
28
- ./scripts/sdk-e2e-infra.sh up
29
-
30
- # Verify services are healthy
31
- curl http://localhost:19100/health # Peer 1 BLS
32
- curl http://localhost:19110/health # Peer 2 BLS
33
- # Nostr relays on ws://localhost:19700 and ws://localhost:19710 (WebSocket, no HTTP endpoint)
34
-
35
- # Stop infrastructure
36
- ./scripts/sdk-e2e-infra.sh down
37
- ```
38
-
39
- | Service | Port | Purpose |
40
- | ---------------- | ----- | --------------------------------------------------- |
41
- | **Anvil** | 18545 | Local EVM chain (chain ID 31337) |
42
- | **Peer 1 BLS** | 19100 | Validates events, calculates pricing, stores events |
43
- | **Peer 1 Relay** | 19700 | WebSocket relay for peer discovery (kind:10032) |
44
- | **Peer 2 BLS** | 19110 | Validates events, calculates pricing, stores events |
45
- | **Peer 2 Relay** | 19710 | WebSocket relay for peer discovery (kind:10032) |
41
+ **Reading is free** to subscribe/query you need nothing but this package. To **write (pay)** you need:
42
+
43
+ - **Node.js ≥ 20** — these packages are ESM.
44
+ - **A TOON apex to pay.** You don't run any node yourself; you connect to a running
45
+ [`@toon-protocol/townhouse`](https://www.npmjs.com/package/@toon-protocol/townhouse) apex (or any
46
+ TOON connector) and pay it. From its operator you need:
47
+ - a **connector endpoint** — either an HTTP `connectorUrl`, or an ATOR `.anyone` BTP endpoint reached
48
+ through a **SOCKS5h** proxy (see [Connecting over `.anyone`](#connecting-to-an-apex-over-anyone));
49
+ - a **settlement-chain RPC URL** and a **funded key** on that chain, so the client can open a
50
+ payment channel and sign EIP-712 claims;
51
+ - the **token** and **TokenNetwork** contract addresses that apex accepts on that chain (e.g. USDC).
52
+
53
+ These coordinates go straight into the [`ToonClient` config](#quick-start) below.
54
+
55
+ > **Local development (from a clone of this repo, not the npm package).** To try the client
56
+ > end-to-end against a throwaway local network, start the monorepo's SDK E2E stack — Anvil + two peer
57
+ > nodes + relays. This script ships with the repo, **not** the published package:
58
+ >
59
+ > ```bash
60
+ > ./scripts/sdk-e2e-infra.sh up # start (Ctrl-C-safe; `down` to stop)
61
+ > curl http://localhost:19100/health # peer 1 health
62
+ > ./scripts/sdk-e2e-infra.sh down # stop
63
+ > ```
64
+ >
65
+ > | Service | Port | Purpose |
66
+ > | ---------------- | ----- | --------------------------------------------------- |
67
+ > | **Anvil** | 18545 | Local EVM chain (chain ID 31337) |
68
+ > | **Peer 1 BLS** | 19100 | Validates events, calculates pricing, stores events |
69
+ > | **Peer 1 Relay** | 19700 | WebSocket relay for peer discovery (kind:10032) |
70
+ > | **Peer 2 BLS** | 19110 | Validates events, calculates pricing, stores events |
71
+ > | **Peer 2 Relay** | 19710 | WebSocket relay for peer discovery (kind:10032) |
72
+ >
73
+ > The Quick Start below is wired for this local stack (chain `evm:anvil:31337`, TokenNetwork
74
+ > `0xCafac3dD…052c`); swap in your apex's real coordinates for any other network.
46
75
 
47
76
  ---
48
77
 
@@ -93,9 +122,86 @@ await client.stop();
93
122
 
94
123
  ---
95
124
 
125
+ ## Identity & Multi-Chain Settlement
126
+
127
+ There are two ways to give the client an identity:
128
+
129
+ ### 1. Raw `secretKey` — Nostr + EVM (secp256k1)
130
+
131
+ A 32-byte Nostr key. Because Nostr and EVM both use secp256k1, the same key provides your EVM identity automatically. This is the path shown in the Quick Start, and it only supports EVM settlement.
132
+
133
+ ### 2. `mnemonic` — full multi-chain identity (recommended for non-EVM)
134
+
135
+ A single BIP-39 phrase derives **all** chain identities: Nostr (NIP-06) + EVM (secp256k1), Solana (Ed25519), and Mina (Pallas). This is required to settle on Solana or Mina — a raw secp256k1 `secretKey` cannot represent those curves.
136
+
137
+ ```typescript
138
+ import { ToonClient, generateMnemonic, deriveFullIdentity } from '@toon-protocol/client';
139
+ import { encodeEventToToon, decodeEventFromToon } from '@toon-protocol/relay';
140
+
141
+ const mnemonic = generateMnemonic(); // or restore an existing 12-word phrase
142
+ const { nostr } = await deriveFullIdentity(mnemonic); // peek at the derived keys if needed
143
+
144
+ const client = new ToonClient({
145
+ connectorUrl: 'http://localhost:8080',
146
+ mnemonic, // derives Nostr/EVM synchronously; Solana/Mina during start()
147
+ ilpInfo: {
148
+ pubkey: nostr.pubkey,
149
+ ilpAddress: `g.toon.${nostr.pubkey.slice(0, 8)}`,
150
+ btpEndpoint: 'ws://localhost:3000',
151
+ },
152
+ toonEncoder: encodeEventToToon,
153
+ toonDecoder: decodeEventFromToon,
154
+ });
155
+
156
+ await client.start();
157
+
158
+ // EVM is available before start(); Solana/Mina are derived during start()
159
+ console.log('Nostr: ', client.getPublicKey());
160
+ console.log('EVM: ', client.getEvmAddress());
161
+ console.log('Solana:', client.getSolanaAddress()); // base58, after start()
162
+ console.log('Mina: ', client.getMinaAddress()); // base58, after start() (needs mina-signer)
163
+ ```
164
+
165
+ **Notes & rules:**
166
+
167
+ - **Precedence**: `mnemonic` and `secretKey` are mutually exclusive (a separate `secretKey` would split the Nostr identity from the Solana/Mina identity — the client rejects it). An `evmPrivateKey` override **is** allowed alongside `mnemonic` (e.g. a hardware-wallet EVM key while still deriving Solana/Mina from the phrase).
168
+ - **Solana/Mina addresses** (`getSolanaAddress()`, `getMinaAddress()`) are only available **after `start()`** — those keys are derived asynchronously. `getEvmAddress()`/`getPublicKey()` work before `start()`.
169
+ - **Mina is optional**: it requires the `mina-signer` peer dependency (see Installation). Without it, the client still works for Nostr/EVM/Solana and `getMinaAddress()` returns `undefined`.
170
+ - **Security**: JavaScript strings can't be zeroed from memory, so a `mnemonic` may linger in the heap. For high-security contexts, derive keys yourself (e.g. via `KeyManager`) and pass a pre-derived `secretKey`.
171
+ - **Per-chain claim formats.** Each publish carries a balance-proof claim in the format that destination chain's connector verifier expects — EVM via EIP-712, Solana as a raw Ed25519 message over the on-chain payment-channel message (`channel_pda ‖ nonce ‖ transferredAmount`), Mina as a Pallas-Schnorr claim over a Poseidon `balanceCommitment`. `ToonClient` selects the right signer for the negotiated channel automatically; you do not pick the format. Canonical layouts live in `@toon-protocol/core` (`packages/core/src/settlement/`) so client signers and connector verifiers cannot drift.
172
+ - **On-chain redemption is automatic, and driven by the apex — not the client.** You sign off-chain claims; the Townhouse apex validates them, fulfills, and (once a per-channel threshold is crossed) submits the on-chain redemption itself. EVM and Solana credit the recipient on-chain (Solana at channel close, `SETTLE_CHANNEL`). On **Mina** each paid publish redeems on-chain (`claimFromChannel`, the apex co-signs the counterparty signature; the zkApp nonce and balance commitment advance), and **the recipient's tokens are credited at channel close** via the Story 34.4 fund-custody zkApp (`@toon-protocol/connector` ≥3.10.0): the deposit is escrowed on the zkApp account and `settle()` drains it to the participants (recipient + depositor refund). Verified against `@toon-protocol/connector` 3.10.0.
173
+
174
+ ---
175
+
176
+ ## Uploading a blob to a DVM (Arweave storage)
177
+
178
+ To store a blob permanently on Arweave, pay a **dvm** node with a `kind:5094` NIP-90 request. The `requestBlobStorage` helper builds the signed event, publishes it through your `ToonClient` (reusing its claim/channel plumbing), and decodes the Arweave transaction ID from the FULFILL response:
179
+
180
+ ```typescript
181
+ import { ToonClient, requestBlobStorage } from '@toon-protocol/client';
182
+
183
+ // `client` is a started ToonClient (see Quick Start). `secretKey` signs the kind:5094 event.
184
+ const result = await requestBlobStorage(client, secretKey, {
185
+ blobData: new Uint8Array([1, 2, 3, 4]),
186
+ contentType: 'application/octet-stream',
187
+ ilpAmount: 50_000n, // USDC micro-units; also used as the event's `bid` if `bid` is omitted
188
+ destination: 'g.toon.peer1', // the DVM's ILP address (defaults to the client's destinationAddress)
189
+ });
190
+
191
+ if (result.success) {
192
+ console.log(`Stored on Arweave: https://arweave.net/${result.txId}`);
193
+ } else {
194
+ console.error(`Upload failed: ${result.error}`);
195
+ }
196
+ ```
197
+
198
+ `requestBlobStorage(client, secretKey, params)` returns `{ success, txId?, eventId?, error? }` (`RequestBlobStorageParams` / `RequestBlobStorageResult` are exported for typing). It covers the **single-packet** case; for large chunked uploads, drive `client.publishEvent()` with `kind:5094` events directly.
199
+
200
+ ---
201
+
96
202
  ## Payment Channels
97
203
 
98
- The client supports EVM-based payment channels for off-chain settlement. Your EVM identity is derived from your Nostr `secretKey` automatically — no separate EVM key needed.
204
+ The client supports payment channels for off-chain settlement on EVM, Solana, and Mina. With a raw `secretKey` you get EVM only; construct from a `mnemonic` (above) to settle on Solana/Mina. Your EVM identity is derived automatically — no separate EVM key needed.
99
205
 
100
206
  ### Enabling Payment Channels
101
207
 
@@ -131,10 +237,11 @@ await client.publishEvent(event, { claim });
131
237
 
132
238
  ### How It Works
133
239
 
134
- 1. **Bootstrap**: Client discovers peers via NIP-02 and kind:10032 events
135
- 2. **Channel Creation**: Opens on-chain payment channel using your derived EVM address
136
- 3. **Off-chain Payments**: Signed balance proofs settle payments off-chain
240
+ 1. **Bootstrap**: Client discovers peers via NIP-02 and kind:10032 events, negotiating a settlement chain with each
241
+ 2. **Channel Creation**: Opens an on-chain payment channel on the negotiated chain — using your derived EVM address (EVM), the Ed25519 channel PDA (Solana), or the deployed zkApp account (Mina) when the matching `solanaChannel` / `minaChannel` config is provided
242
+ 3. **Off-chain Payments**: Signed balance proofs (chain-appropriate format) settle payments off-chain
137
243
  4. **Auto-tracking**: ChannelManager automatically tracks channels and increments nonces
244
+ 5. **On-chain redemption**: A Townhouse apex auto-redeems claims on-chain once a per-channel threshold is crossed (see [Multi-Chain Settlement](#identity--multi-chain-settlement) for the EVM/Solana/Mina specifics and the Mina credit-at-close deferral)
138
245
 
139
246
  ### Using a Separate EVM Key (Advanced)
140
247
 
@@ -149,6 +256,29 @@ const client = new ToonClient({
149
256
 
150
257
  ---
151
258
 
259
+ ## Connecting to an apex over `.anyone`
260
+
261
+ In production a townhouse apex is reachable at an ATOR `.anyone` hidden-service address rather than a plain `ws://` host. To dial it, route the client's BTP/HTTP traffic through a **SOCKS5h** proxy via the `transport` option. The `socks5h://` scheme is required so DNS resolution happens at the proxy (no DNS leaks):
262
+
263
+ ```typescript
264
+ const client = new ToonClient({
265
+ connectorUrl: 'http://localhost:8080', // local connector admin, if any
266
+ secretKey,
267
+ ilpInfo: { pubkey, ilpAddress: `g.toon.${pubkey.slice(0, 8)}`, btpEndpoint: 'ws://abc...xyz.anyone:3000' },
268
+ btpUrl: 'ws://abc...xyz.anyone:3000', // the apex's BTP endpoint at its .anyone address
269
+ destinationAddress: 'g.townhouse', // pay the apex; it forwards to the town/dvm/mill child
270
+ toonEncoder: encodeEventToToon,
271
+ toonDecoder: decodeEventFromToon,
272
+
273
+ // Route the connection through a SOCKS5h proxy (Node.js only).
274
+ transport: { type: 'socks5', socksProxy: 'socks5h://127.0.0.1:9050' },
275
+ });
276
+ ```
277
+
278
+ In the browser, use `transport: { type: 'gateway', gatewayUrl: 'https://…' }` to proxy through an ator gateway server-side instead. (Default is `{ type: 'direct' }`.)
279
+
280
+ ---
281
+
152
282
  ## Documentation
153
283
 
154
284
  - **[API Reference](docs/api-reference.md)** — Constructor, config interface, and all methods
@@ -189,16 +319,20 @@ See [tests/e2e/README.md](tests/e2e/README.md) for detailed E2E setup.
189
319
 
190
320
  See [examples/client-example/](../../examples/client-example/) for standalone client examples:
191
321
 
192
- - **01 - Publish Event**: Full client lifecycle with self-describing claims
193
- - **02 - Payment Channel Lifecycle**: Multiple events with incrementing balance proofs
322
+ - **01 - Publish Event** (`01-publish-event.ts`): Full client lifecycle with self-describing claims
323
+ - **02 - Payment Channel Lifecycle** (`02-payment-channel.ts`): Multiple events with incrementing balance proofs
324
+ - **03 - Multi-Chain Publish** (`03-multi-chain-publish.ts`): Publishing with multiple settlement chains configured
325
+ - **04 - Subscribe to Events** (`04-subscribe-events.ts`): Reading events back from the relay (free)
194
326
 
195
327
  ---
196
328
 
197
329
  ## Related Packages
198
330
 
199
- - **[@toon-protocol/core](../core/)** — Core protocol (peer discovery, bootstrap)
200
- - **[@toon-protocol/relay](../relay/)** — Nostr relay with ILP payment gating
331
+ - **[@toon-protocol/core](../core/)** — Core protocol (peer discovery, bootstrap, `buildBlobStorageRequest`)
332
+ - **[@toon-protocol/relay](../relay/)** — Nostr relay with ILP payment gating (`encodeEventToToon` / `decodeEventFromToon`)
333
+ - **[@toon-protocol/sdk](../sdk/)** — Higher-level helpers including `streamSwap()` for multi-chain swaps via a **mill**
201
334
  - **[@toon-protocol/bls](../bls/)** — Business Logic Server (pricing, validation, storage)
335
+ - **[@toon-protocol/townhouse](../townhouse/)** — The operator product that runs the apex + town/mill/dvm nodes you pay
202
336
 
203
337
  ---
204
338
 
@@ -0,0 +1,23 @@
1
+ import {
2
+ ANON_ASSETS,
3
+ ANON_VERSION,
4
+ defaultCacheDir,
5
+ ensureAnonBinary,
6
+ renderTorrc,
7
+ selectAnonAsset,
8
+ startManagedAnonProxy,
9
+ tcpProbe,
10
+ waitForAnonSocks
11
+ } from "./chunk-WHAEQLIW.js";
12
+ export {
13
+ ANON_ASSETS,
14
+ ANON_VERSION,
15
+ defaultCacheDir,
16
+ ensureAnonBinary,
17
+ renderTorrc,
18
+ selectAnonAsset,
19
+ startManagedAnonProxy,
20
+ tcpProbe,
21
+ waitForAnonSocks
22
+ };
23
+ //# sourceMappingURL=anon-proxy-W3KMM7GU.js.map
@@ -0,0 +1,276 @@
1
+ // src/transport/anon-proxy.ts
2
+ import { createRequire } from "module";
3
+ var nodeRequire = createRequire(import.meta.url);
4
+ var ANON_VERSION = "v0.4.10.0-beta";
5
+ var RELEASE_BASE = `https://github.com/anyone-protocol/ator-protocol/releases/download/${ANON_VERSION}`;
6
+ var ANON_ASSETS = {
7
+ "darwin-arm64": {
8
+ assetName: "anon-beta-macos-arm64.zip",
9
+ sha256: "3b8724afc56354aa93d2fe804d6b8a685d3bff65dac0ca3384cae1ef010977b2"
10
+ },
11
+ "darwin-x64": {
12
+ assetName: "anon-beta-macos-amd64.zip",
13
+ sha256: "aad277849b1e63baa75891b9e5109683534e488776ff190e884e34caa04a6d54"
14
+ },
15
+ "linux-x64": {
16
+ assetName: "anon-beta-linux-amd64.zip",
17
+ sha256: "370c86f366e7f4cad896e2ef4bbd366a4e78a832c8d58064012f86c88c411a6b"
18
+ },
19
+ "linux-arm64": {
20
+ assetName: "anon-beta-linux-arm64.zip",
21
+ sha256: "382d21db1052b6a0f1581bf38c9cf79b370719e313781c0eba53ef0d9570334a"
22
+ }
23
+ };
24
+ function selectAnonAsset(platform, arch) {
25
+ const key = `${platform}-${arch}`;
26
+ const asset = ANON_ASSETS[key];
27
+ if (!asset) {
28
+ throw new Error(
29
+ `No managed anon binary available for platform "${platform}" arch "${arch}". Supported: ${Object.keys(ANON_ASSETS).join(", ")}. Provide an explicit transport.socksProxy or set ANYONE_PROXY_URLS to use your own proxy.`
30
+ );
31
+ }
32
+ return asset;
33
+ }
34
+ function defaultCacheDir() {
35
+ const os = nodeRequire("node:os");
36
+ const path = nodeRequire("node:path");
37
+ const xdg = process.env["XDG_CACHE_HOME"];
38
+ if (xdg) {
39
+ return path.join(xdg, "toon-client", "anon", ANON_VERSION);
40
+ }
41
+ return path.join(os.homedir(), ".toon-client", "anon", ANON_VERSION);
42
+ }
43
+ function renderTorrc(cacheDir, socksPort) {
44
+ const path = nodeRequire("node:path");
45
+ return [
46
+ "AgreeToTerms 1",
47
+ `DataDirectory ${path.join(cacheDir, "data")}`,
48
+ `SOCKSPort 127.0.0.1:${socksPort}`,
49
+ "SOCKSPolicy accept *",
50
+ `GeoIPFile ${path.join(cacheDir, "geoip")}`,
51
+ `GeoIPv6File ${path.join(cacheDir, "geoip6")}`,
52
+ "Log notice stdout",
53
+ "RunAsDaemon 0",
54
+ ""
55
+ ].join("\n");
56
+ }
57
+ async function tcpProbe(host, port, timeoutMs) {
58
+ const net = nodeRequire("node:net");
59
+ return new Promise((resolve, reject) => {
60
+ const sock = net.createConnection({ host, port }, () => {
61
+ sock.destroy();
62
+ resolve();
63
+ });
64
+ sock.once("error", (err) => {
65
+ sock.destroy();
66
+ reject(err);
67
+ });
68
+ sock.setTimeout(timeoutMs, () => {
69
+ sock.destroy();
70
+ reject(new Error("timeout"));
71
+ });
72
+ });
73
+ }
74
+ async function sha256File(filePath) {
75
+ const fs = nodeRequire("node:fs");
76
+ const crypto = nodeRequire("node:crypto");
77
+ return new Promise((resolve, reject) => {
78
+ const hash = crypto.createHash("sha256");
79
+ const stream = fs.createReadStream(filePath);
80
+ stream.on("error", reject);
81
+ stream.on("data", (chunk) => hash.update(chunk));
82
+ stream.on("end", () => resolve(hash.digest("hex")));
83
+ });
84
+ }
85
+ async function downloadToFile(url, destPath) {
86
+ const fs = nodeRequire("node:fs");
87
+ const https = nodeRequire("node:https");
88
+ const fetchOnce = (u, redirectsLeft) => new Promise((resolve, reject) => {
89
+ const req = https.get(u, (res) => {
90
+ const status = res.statusCode ?? 0;
91
+ if (status >= 300 && status < 400 && res.headers.location) {
92
+ res.resume();
93
+ if (redirectsLeft <= 0) {
94
+ reject(new Error(`Too many redirects downloading ${url}`));
95
+ return;
96
+ }
97
+ resolve(fetchOnce(res.headers.location, redirectsLeft - 1));
98
+ return;
99
+ }
100
+ if (status !== 200) {
101
+ res.resume();
102
+ reject(new Error(`Download failed (HTTP ${status}) for ${u}`));
103
+ return;
104
+ }
105
+ const out = fs.createWriteStream(destPath);
106
+ res.pipe(out);
107
+ out.on("error", reject);
108
+ out.on("finish", () => out.close(() => resolve()));
109
+ });
110
+ req.on("error", reject);
111
+ req.setTimeout(12e4, () => {
112
+ req.destroy(new Error(`Download timeout for ${u}`));
113
+ });
114
+ });
115
+ await fetchOnce(url, 5);
116
+ }
117
+ async function extractZip(zipPath, destDir) {
118
+ const cp = nodeRequire("node:child_process");
119
+ await new Promise((resolve, reject) => {
120
+ const child = cp.spawn("unzip", ["-o", zipPath, "-d", destDir], {
121
+ stdio: ["ignore", "ignore", "pipe"]
122
+ });
123
+ let stderr = "";
124
+ child.stderr?.on("data", (d) => {
125
+ stderr += d.toString();
126
+ });
127
+ child.on(
128
+ "error",
129
+ (err) => reject(
130
+ new Error(`Failed to spawn unzip (is it installed?): ${err.message}`)
131
+ )
132
+ );
133
+ child.on("exit", (code) => {
134
+ if (code === 0) resolve();
135
+ else
136
+ reject(
137
+ new Error(`unzip exited ${code} extracting ${zipPath}: ${stderr}`)
138
+ );
139
+ });
140
+ });
141
+ }
142
+ async function ensureAnonBinary(opts) {
143
+ const fs = nodeRequire("node:fs");
144
+ const path = nodeRequire("node:path");
145
+ const download = opts.download ?? downloadToFile;
146
+ const extract = opts.extract ?? extractZip;
147
+ const asset = selectAnonAsset(opts.platform, opts.arch);
148
+ const anonPath = path.join(opts.cacheDir, "anon");
149
+ if (fs.existsSync(anonPath)) {
150
+ return anonPath;
151
+ }
152
+ if (asset.sha256 === null) {
153
+ throw new Error(
154
+ `Managed anon binary for "${opts.platform}-${opts.arch}" (${asset.assetName}) has no pinned checksum yet (see issue #204). Provide an explicit transport.socksProxy to use your own proxy.`
155
+ );
156
+ }
157
+ fs.mkdirSync(opts.cacheDir, { recursive: true });
158
+ const zipPath = path.join(opts.cacheDir, asset.assetName);
159
+ const url = `${RELEASE_BASE}/${asset.assetName}`;
160
+ await download(url, zipPath);
161
+ const actual = await sha256File(zipPath);
162
+ if (actual !== asset.sha256) {
163
+ try {
164
+ fs.rmSync(zipPath, { force: true });
165
+ } catch {
166
+ }
167
+ throw new Error(
168
+ `Checksum mismatch for ${asset.assetName}: expected ${asset.sha256}, got ${actual}. Refusing to run an unverified anon binary.`
169
+ );
170
+ }
171
+ await extract(zipPath, opts.cacheDir);
172
+ if (!fs.existsSync(anonPath)) {
173
+ throw new Error(
174
+ `Extraction of ${asset.assetName} did not produce an "anon" binary at ${anonPath}.`
175
+ );
176
+ }
177
+ try {
178
+ fs.chmodSync(anonPath, 493);
179
+ } catch {
180
+ }
181
+ return anonPath;
182
+ }
183
+ async function waitForAnonSocks(opts) {
184
+ const probe = opts.probe ?? tcpProbe;
185
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
186
+ opts.log(`[anon] waiting for SOCKS5 bind on 127.0.0.1:${opts.port}\u2026`);
187
+ let lastErr = null;
188
+ while (Date.now() < opts.deadlineMs) {
189
+ if (opts.childExited()) {
190
+ throw new Error("[anon] process exited before SOCKS5 port bound");
191
+ }
192
+ try {
193
+ await probe("127.0.0.1", opts.port, 2e3);
194
+ opts.log(`[anon] SOCKS5 bound on 127.0.0.1:${opts.port}`);
195
+ return;
196
+ } catch (err) {
197
+ const msg = err.message;
198
+ if (msg !== lastErr) {
199
+ opts.log(`[anon] SOCKS5 not ready: ${msg}`);
200
+ lastErr = msg;
201
+ }
202
+ }
203
+ await sleep(2e3);
204
+ }
205
+ throw new Error(
206
+ `[anon] SOCKS5 never bound on 127.0.0.1:${opts.port} by deadline`
207
+ );
208
+ }
209
+ async function startManagedAnonProxy(options = {}) {
210
+ const fs = nodeRequire("node:fs");
211
+ const path = nodeRequire("node:path");
212
+ const os = nodeRequire("node:os");
213
+ const cp = nodeRequire("node:child_process");
214
+ const platform = options.platform ?? os.platform();
215
+ const arch = options.arch ?? os.arch();
216
+ const cacheDir = options.cacheDir ?? defaultCacheDir();
217
+ const socksPort = options.socksPort ?? 9050;
218
+ const bootstrapTimeoutMs = options.bootstrapTimeoutMs ?? 18e4;
219
+ const log = options.log ?? (() => {
220
+ });
221
+ const anonPath = await ensureAnonBinary({ cacheDir, platform, arch });
222
+ fs.mkdirSync(path.join(cacheDir, "data"), { recursive: true });
223
+ const torrcPath = path.join(cacheDir, "torrc");
224
+ fs.writeFileSync(torrcPath, renderTorrc(cacheDir, socksPort), {
225
+ mode: 420
226
+ });
227
+ log(`[anon] spawning: ${anonPath} -f ${torrcPath}`);
228
+ const child = cp.spawn(anonPath, ["-f", torrcPath], {
229
+ stdio: ["ignore", "inherit", "inherit"],
230
+ detached: false
231
+ });
232
+ let exited = false;
233
+ child.on("exit", (code, signal) => {
234
+ exited = true;
235
+ log(`[anon] child exited code=${code} signal=${signal}`);
236
+ });
237
+ child.on("error", (err) => {
238
+ log(`[anon] spawn error: ${err.message}`);
239
+ });
240
+ const stop = async () => {
241
+ if (!child.killed && !exited) {
242
+ try {
243
+ child.kill("SIGTERM");
244
+ } catch {
245
+ }
246
+ }
247
+ };
248
+ try {
249
+ await waitForAnonSocks({
250
+ port: socksPort,
251
+ deadlineMs: Date.now() + bootstrapTimeoutMs,
252
+ childExited: () => exited,
253
+ log
254
+ });
255
+ } catch (err) {
256
+ await stop();
257
+ throw err;
258
+ }
259
+ return {
260
+ socksProxy: `socks5h://127.0.0.1:${socksPort}`,
261
+ stop
262
+ };
263
+ }
264
+
265
+ export {
266
+ ANON_VERSION,
267
+ ANON_ASSETS,
268
+ selectAnonAsset,
269
+ defaultCacheDir,
270
+ renderTorrc,
271
+ tcpProbe,
272
+ ensureAnonBinary,
273
+ waitForAnonSocks,
274
+ startManagedAnonProxy
275
+ };
276
+ //# sourceMappingURL=chunk-WHAEQLIW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/anon-proxy.ts"],"sourcesContent":["/**\n * Self-managed `anon` (anyone-protocol / ATOR) SOCKS5h proxy (Node.js only).\n *\n * Lets a `@toon-protocol/client` consumer reach a `.anyone` hidden service with\n * ZERO manual proxy setup: the SDK downloads, verifies, extracts, and spawns its\n * own `anon` daemon, waits for it to bootstrap + bind a loopback SOCKS5 port, and\n * hands back a `socks5h://127.0.0.1:<port>` URL. The proven reference is the\n * server-side pod entrypoint `docker/src/entrypoint-toon-client.ts` (`writeTorrc`,\n * `spawnAnon`, `waitForAnonSocks`, `tcpProbe`); this module ports that daemon\n * logic into the client package and adds the binary download + checksum gate so it\n * works without an OS-level `anon` install.\n *\n * BROWSER SAFETY: this module is dynamically imported only from `resolveTransport`\n * when a managed proxy is actually needed (Node-only path). Every Node built-in is\n * pulled in lazily via the ESM-safe `require(...)` built off `import.meta.url`\n * (the same pattern as `socks5.ts`), so a browser bundler that statically analyses\n * the package never reaches `node:child_process`/`node:fs`/`node:https`/`node:net`.\n */\n\nimport { createRequire } from 'node:module';\nimport type childProcessModule from 'node:child_process';\nimport type fsModule from 'node:fs';\nimport type netModule from 'node:net';\nimport type osModule from 'node:os';\nimport type pathModule from 'node:path';\nimport type httpsModule from 'node:https';\nimport type * as cryptoModule from 'node:crypto';\n\n// ESM-safe require — see socks5.ts for the full rationale. The published bundle\n// is ESM with Node built-ins external; a bare `require` would be rewritten into a\n// throwing `__require` shim. Building a real require off import.meta.url keeps the\n// synchronous, browser-guarded `require(...)` calls below working. This file is\n// only ever dynamically imported on the Node path, so browser bundlers that tree-\n// shake the static graph never include it.\nconst nodeRequire = createRequire(import.meta.url);\n\n/**\n * Pinned `anon` release. \"beta\" is the channel slug embedded in the per-platform\n * zip asset names (e.g. `anon-beta-macos-arm64.zip`).\n */\nexport const ANON_VERSION = 'v0.4.10.0-beta';\n\nconst RELEASE_BASE = `https://github.com/anyone-protocol/ator-protocol/releases/download/${ANON_VERSION}`;\n\n/**\n * Per-platform `anon` zip asset descriptor. `sha256` is the pinned checksum of the\n * release zip. All supported platforms are pinned (issue #204); the type stays\n * `string | null` and the download gate still defensively refuses a `null` entry,\n * so adding a new (not-yet-hashed) platform fails closed rather than skipping\n * verification.\n */\nexport interface AnonAsset {\n /** Release asset file name, e.g. `anon-beta-macos-arm64.zip`. */\n assetName: string;\n /** Pinned sha256 of the zip, or null when not yet pinned (issue #204). */\n sha256: string | null;\n}\n\n/**\n * Platform → asset map keyed by `${os.platform()}-${os.arch()}` (Node values).\n * Only macOS + Linux on x64/arm64 are supported (the `anon` releases that ship a\n * SOCKS-capable binary). Windows is intentionally absent.\n *\n * Pinned checksums (issue #204): all four supported platforms are pinned to the\n * sha256 of the `v0.4.10.0-beta` release zips (downloaded + hashed; the\n * darwin-arm64 value matches the previously-verified manual flow).\n */\nexport const ANON_ASSETS: Record<string, AnonAsset> = {\n 'darwin-arm64': {\n assetName: 'anon-beta-macos-arm64.zip',\n sha256: '3b8724afc56354aa93d2fe804d6b8a685d3bff65dac0ca3384cae1ef010977b2',\n },\n 'darwin-x64': {\n assetName: 'anon-beta-macos-amd64.zip',\n sha256: 'aad277849b1e63baa75891b9e5109683534e488776ff190e884e34caa04a6d54',\n },\n 'linux-x64': {\n assetName: 'anon-beta-linux-amd64.zip',\n sha256: '370c86f366e7f4cad896e2ef4bbd366a4e78a832c8d58064012f86c88c411a6b',\n },\n 'linux-arm64': {\n assetName: 'anon-beta-linux-arm64.zip',\n sha256: '382d21db1052b6a0f1581bf38c9cf79b370719e313781c0eba53ef0d9570334a',\n },\n};\n\n/**\n * Resolves the `anon` release asset for a platform/arch pair (Node\n * `os.platform()` / `os.arch()` values).\n *\n * @throws If the platform/arch combination has no known `anon` asset.\n */\nexport function selectAnonAsset(platform: string, arch: string): AnonAsset {\n const key = `${platform}-${arch}`;\n const asset = ANON_ASSETS[key];\n if (!asset) {\n throw new Error(\n `No managed anon binary available for platform \"${platform}\" arch \"${arch}\". ` +\n `Supported: ${Object.keys(ANON_ASSETS).join(', ')}. ` +\n 'Provide an explicit transport.socksProxy or set ANYONE_PROXY_URLS to use your own proxy.'\n );\n }\n return asset;\n}\n\n/**\n * Default cache directory for the downloaded/extracted `anon` binary.\n * Honours `XDG_CACHE_HOME`; otherwise `~/.toon-client/anon`.\n */\nexport function defaultCacheDir(): string {\n const os = nodeRequire('node:os') as typeof osModule;\n const path = nodeRequire('node:path') as typeof pathModule;\n const xdg = process.env['XDG_CACHE_HOME'];\n if (xdg) {\n return path.join(xdg, 'toon-client', 'anon', ANON_VERSION);\n }\n return path.join(os.homedir(), '.toon-client', 'anon', ANON_VERSION);\n}\n\n/**\n * Renders a SOCKS-only torrc. Mirrors `writeTorrc` in the proven docker\n * entrypoint. `AgreeToTerms 1` is REQUIRED — omitting it makes `anon` exit\n * immediately.\n */\nexport function renderTorrc(cacheDir: string, socksPort: number): string {\n const path = nodeRequire('node:path') as typeof pathModule;\n return [\n 'AgreeToTerms 1',\n `DataDirectory ${path.join(cacheDir, 'data')}`,\n `SOCKSPort 127.0.0.1:${socksPort}`,\n 'SOCKSPolicy accept *',\n `GeoIPFile ${path.join(cacheDir, 'geoip')}`,\n `GeoIPv6File ${path.join(cacheDir, 'geoip6')}`,\n 'Log notice stdout',\n 'RunAsDaemon 0',\n '',\n ].join('\\n');\n}\n\n/**\n * Simple TCP connect probe — confirms the SOCKS5 port has bound and accepts\n * connections. Mirrors `tcpProbe` in the docker entrypoint / `probeSocks5Proxy`.\n */\nexport async function tcpProbe(\n host: string,\n port: number,\n timeoutMs: number\n): Promise<void> {\n const net = nodeRequire('node:net') as typeof netModule;\n return new Promise<void>((resolve, reject) => {\n const sock = net.createConnection({ host, port }, () => {\n sock.destroy();\n resolve();\n });\n sock.once('error', (err: Error) => {\n sock.destroy();\n reject(err);\n });\n sock.setTimeout(timeoutMs, () => {\n sock.destroy();\n reject(new Error('timeout'));\n });\n });\n}\n\n/**\n * Computes the sha256 (hex) of a file using node:crypto streaming.\n */\nasync function sha256File(filePath: string): Promise<string> {\n const fs = nodeRequire('node:fs') as typeof fsModule;\n const crypto = nodeRequire('node:crypto') as typeof cryptoModule;\n return new Promise<string>((resolve, reject) => {\n const hash = crypto.createHash('sha256');\n const stream = fs.createReadStream(filePath);\n stream.on('error', reject);\n stream.on('data', (chunk) => hash.update(chunk));\n stream.on('end', () => resolve(hash.digest('hex')));\n });\n}\n\n/**\n * Downloads a URL to a file, following GitHub release redirects. Node-only\n * (node:https + node:fs).\n */\nasync function downloadToFile(url: string, destPath: string): Promise<void> {\n const fs = nodeRequire('node:fs') as typeof fsModule;\n const https = nodeRequire('node:https') as typeof httpsModule;\n\n const fetchOnce = (u: string, redirectsLeft: number): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const req = https.get(u, (res) => {\n const status = res.statusCode ?? 0;\n // GitHub release assets redirect to a signed S3 URL.\n if (status >= 300 && status < 400 && res.headers.location) {\n res.resume();\n if (redirectsLeft <= 0) {\n reject(new Error(`Too many redirects downloading ${url}`));\n return;\n }\n resolve(fetchOnce(res.headers.location, redirectsLeft - 1));\n return;\n }\n if (status !== 200) {\n res.resume();\n reject(new Error(`Download failed (HTTP ${status}) for ${u}`));\n return;\n }\n const out = fs.createWriteStream(destPath);\n res.pipe(out);\n out.on('error', reject);\n out.on('finish', () => out.close(() => resolve()));\n });\n req.on('error', reject);\n req.setTimeout(120_000, () => {\n req.destroy(new Error(`Download timeout for ${u}`));\n });\n });\n\n await fetchOnce(url, 5);\n}\n\n/**\n * Extracts a zip into a directory by shelling out to the system `unzip` binary\n * (present on macOS + Linux). Kept here (not a JS unzip dep) to avoid adding a\n * runtime dependency to the browser-facing client package.\n */\nasync function extractZip(zipPath: string, destDir: string): Promise<void> {\n const cp = nodeRequire('node:child_process') as typeof childProcessModule;\n await new Promise<void>((resolve, reject) => {\n const child = cp.spawn('unzip', ['-o', zipPath, '-d', destDir], {\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n let stderr = '';\n child.stderr?.on('data', (d: Buffer) => {\n stderr += d.toString();\n });\n child.on('error', (err: Error) =>\n reject(\n new Error(`Failed to spawn unzip (is it installed?): ${err.message}`)\n )\n );\n child.on('exit', (code) => {\n if (code === 0) resolve();\n else\n reject(\n new Error(`unzip exited ${code} extracting ${zipPath}: ${stderr}`)\n );\n });\n });\n}\n\n/**\n * Ensures a verified `anon` binary exists in the cache directory, downloading +\n * checksum-verifying + extracting it if not. Returns the absolute path to the\n * extracted `anon` executable.\n *\n * Skips re-download when a previously extracted `anon` binary is already present\n * (the checksum gate runs on the freshly downloaded zip; an already-extracted\n * binary in a version-pinned cache dir is trusted).\n */\nexport async function ensureAnonBinary(opts: {\n cacheDir: string;\n platform: string;\n arch: string;\n /** Injectable downloader (tests). Default: node:https GET with redirects. */\n download?: (url: string, destPath: string) => Promise<void>;\n /** Injectable extractor (tests). Default: shell out to `unzip`. */\n extract?: (zipPath: string, destDir: string) => Promise<void>;\n}): Promise<string> {\n const fs = nodeRequire('node:fs') as typeof fsModule;\n const path = nodeRequire('node:path') as typeof pathModule;\n\n const download = opts.download ?? downloadToFile;\n const extract = opts.extract ?? extractZip;\n\n const asset = selectAnonAsset(opts.platform, opts.arch);\n const anonPath = path.join(opts.cacheDir, 'anon');\n\n // Fast path: already extracted (version-pinned cache dir).\n if (fs.existsSync(anonPath)) {\n return anonPath;\n }\n\n if (asset.sha256 === null) {\n throw new Error(\n `Managed anon binary for \"${opts.platform}-${opts.arch}\" ` +\n `(${asset.assetName}) has no pinned checksum yet (see issue #204). ` +\n 'Provide an explicit transport.socksProxy to use your own proxy.'\n );\n }\n\n fs.mkdirSync(opts.cacheDir, { recursive: true });\n const zipPath = path.join(opts.cacheDir, asset.assetName);\n const url = `${RELEASE_BASE}/${asset.assetName}`;\n\n await download(url, zipPath);\n\n const actual = await sha256File(zipPath);\n if (actual !== asset.sha256) {\n // Remove the bad artifact so a retry re-downloads cleanly.\n try {\n fs.rmSync(zipPath, { force: true });\n } catch {\n /* best-effort cleanup */\n }\n throw new Error(\n `Checksum mismatch for ${asset.assetName}: expected ${asset.sha256}, got ${actual}. ` +\n 'Refusing to run an unverified anon binary.'\n );\n }\n\n await extract(zipPath, opts.cacheDir);\n\n if (!fs.existsSync(anonPath)) {\n throw new Error(\n `Extraction of ${asset.assetName} did not produce an \"anon\" binary at ${anonPath}.`\n );\n }\n // Ensure executable (zip may not preserve the bit on all platforms).\n try {\n fs.chmodSync(anonPath, 0o755);\n } catch {\n /* best-effort */\n }\n return anonPath;\n}\n\n/**\n * Polls for the SOCKS5 port to bind. `anon` typically takes 30-90s to bootstrap\n * (build a circuit + consensus) before SOCKS5 accepts connections. Mirrors\n * `waitForAnonSocks` in the docker entrypoint, but also fails fast if the child\n * exits before binding.\n */\nexport async function waitForAnonSocks(opts: {\n port: number;\n deadlineMs: number;\n childExited: () => boolean;\n log: (msg: string) => void;\n probe?: (host: string, port: number, timeoutMs: number) => Promise<void>;\n sleep?: (ms: number) => Promise<void>;\n}): Promise<void> {\n const probe = opts.probe ?? tcpProbe;\n const sleep =\n opts.sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms)));\n opts.log(`[anon] waiting for SOCKS5 bind on 127.0.0.1:${opts.port}…`);\n let lastErr: string | null = null;\n while (Date.now() < opts.deadlineMs) {\n if (opts.childExited()) {\n throw new Error('[anon] process exited before SOCKS5 port bound');\n }\n try {\n await probe('127.0.0.1', opts.port, 2_000);\n opts.log(`[anon] SOCKS5 bound on 127.0.0.1:${opts.port}`);\n return;\n } catch (err) {\n const msg = (err as Error).message;\n if (msg !== lastErr) {\n opts.log(`[anon] SOCKS5 not ready: ${msg}`);\n lastErr = msg;\n }\n }\n await sleep(2_000);\n }\n throw new Error(\n `[anon] SOCKS5 never bound on 127.0.0.1:${opts.port} by deadline`\n );\n}\n\n/**\n * Handle returned by `startManagedAnonProxy`. `socksProxy` is the loopback\n * `socks5h://` URL to wire into `transport: { type: 'socks5', socksProxy }`.\n * `stop()` SIGTERMs the daemon and is idempotent.\n */\nexport interface ManagedAnonProxy {\n socksProxy: string;\n stop(): Promise<void>;\n}\n\n/**\n * Options for `startManagedAnonProxy`. All have sensible defaults; tests inject\n * the deps to avoid real downloads/spawns.\n */\nexport interface StartManagedAnonProxyOptions {\n /** Cache dir for the binary + torrc + data. Default: {@link defaultCacheDir}. */\n cacheDir?: string;\n /** Loopback SOCKS5 port. Default 9050. */\n socksPort?: number;\n /** Bootstrap deadline in ms. Default 180_000. */\n bootstrapTimeoutMs?: number;\n /** Logger. Default: no-op. */\n log?: (msg: string) => void;\n /** os.platform() override (tests). */\n platform?: string;\n /** os.arch() override (tests). */\n arch?: string;\n}\n\n/**\n * Downloads (if needed) + spawns a managed `anon` daemon and waits for its SOCKS5\n * port to bind. Returns a {@link ManagedAnonProxy} whose `socksProxy` is ready for\n * `transport: { type: 'socks5', socksProxy }`.\n *\n * @throws If the platform is unsupported, the checksum fails, or anon never binds.\n */\nexport async function startManagedAnonProxy(\n options: StartManagedAnonProxyOptions = {}\n): Promise<ManagedAnonProxy> {\n const fs = nodeRequire('node:fs') as typeof fsModule;\n const path = nodeRequire('node:path') as typeof pathModule;\n const os = nodeRequire('node:os') as typeof osModule;\n const cp = nodeRequire('node:child_process') as typeof childProcessModule;\n\n const platform = options.platform ?? os.platform();\n const arch = options.arch ?? os.arch();\n const cacheDir = options.cacheDir ?? defaultCacheDir();\n const socksPort = options.socksPort ?? 9050;\n const bootstrapTimeoutMs = options.bootstrapTimeoutMs ?? 180_000;\n const log =\n options.log ??\n ((): void => {\n /* default: silent */\n });\n\n const anonPath = await ensureAnonBinary({ cacheDir, platform, arch });\n\n // Write the SOCKS-only torrc.\n fs.mkdirSync(path.join(cacheDir, 'data'), { recursive: true });\n const torrcPath = path.join(cacheDir, 'torrc');\n fs.writeFileSync(torrcPath, renderTorrc(cacheDir, socksPort), {\n mode: 0o644,\n });\n\n log(`[anon] spawning: ${anonPath} -f ${torrcPath}`);\n const child = cp.spawn(anonPath, ['-f', torrcPath], {\n stdio: ['ignore', 'inherit', 'inherit'],\n detached: false,\n });\n let exited = false;\n child.on('exit', (code, signal) => {\n exited = true;\n log(`[anon] child exited code=${code} signal=${signal}`);\n });\n child.on('error', (err: Error) => {\n log(`[anon] spawn error: ${err.message}`);\n });\n\n const stop = async (): Promise<void> => {\n if (!child.killed && !exited) {\n try {\n child.kill('SIGTERM');\n } catch {\n /* best-effort */\n }\n }\n };\n\n try {\n await waitForAnonSocks({\n port: socksPort,\n deadlineMs: Date.now() + bootstrapTimeoutMs,\n childExited: () => exited,\n log,\n });\n } catch (err) {\n await stop();\n throw err;\n }\n\n return {\n socksProxy: `socks5h://127.0.0.1:${socksPort}`,\n stop,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,13 @@
1
+ // src/transport/gateway.ts
2
+ function rewriteUrlsForGateway(gatewayUrl, btpUrl, connectorUrl) {
3
+ const base = gatewayUrl.replace(/\/$/, "");
4
+ const wsBase = base.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
5
+ return {
6
+ btpUrl: btpUrl ? `${wsBase}/btp` : void 0,
7
+ connectorUrl: connectorUrl ? `${base}/api` : void 0
8
+ };
9
+ }
10
+ export {
11
+ rewriteUrlsForGateway
12
+ };
13
+ //# sourceMappingURL=gateway-QOK47RKS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/gateway.ts"],"sourcesContent":["/**\n * Gateway transport for browser environments.\n *\n * Rewrites BTP and HTTP URLs to route through an ator gateway that handles\n * SOCKS5 proxying server-side. Browser clients connect to the gateway via\n * standard WebSocket/HTTP — no special transport code needed.\n */\n\n/**\n * Rewrites btpUrl and connectorUrl to route through a gateway.\n *\n * Gateway endpoint conventions:\n * - WebSocket: `ws(s)://<gateway>/btp` — proxies BTP connections\n * - HTTP: `http(s)://<gateway>/api` — proxies connector admin API\n *\n * @param gatewayUrl - Base URL of the ator gateway (http:// or https://)\n * @param btpUrl - Original BTP WebSocket URL (optional)\n * @param connectorUrl - Original connector HTTP URL (optional)\n */\nexport function rewriteUrlsForGateway(\n gatewayUrl: string,\n btpUrl?: string,\n connectorUrl?: string\n): { btpUrl?: string; connectorUrl?: string } {\n const base = gatewayUrl.replace(/\\/$/, '');\n\n // Derive WebSocket scheme from HTTP scheme\n const wsBase = base.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');\n\n return {\n btpUrl: btpUrl ? `${wsBase}/btp` : undefined,\n connectorUrl: connectorUrl ? `${base}/api` : undefined,\n };\n}\n"],"mappings":";AAmBO,SAAS,sBACd,YACA,QACA,cAC4C;AAC5C,QAAM,OAAO,WAAW,QAAQ,OAAO,EAAE;AAGzC,QAAM,SAAS,KAAK,QAAQ,WAAW,MAAM,EAAE,QAAQ,UAAU,KAAK;AAEtE,SAAO;AAAA,IACL,QAAQ,SAAS,GAAG,MAAM,SAAS;AAAA,IACnC,cAAc,eAAe,GAAG,IAAI,SAAS;AAAA,EAC/C;AACF;","names":[]}