@toon-protocol/client 0.9.1 → 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 +168 -34
- package/dist/anon-proxy-W3KMM7GU.js +23 -0
- package/dist/chunk-WHAEQLIW.js +276 -0
- package/dist/chunk-WHAEQLIW.js.map +1 -0
- package/dist/gateway-QOK47RKS.js +13 -0
- package/dist/gateway-QOK47RKS.js.map +1 -0
- package/dist/index.d.ts +1413 -34
- package/dist/index.js +2530 -430
- package/dist/index.js.map +1 -1
- package/dist/socks5-WTJBYGME.js +136 -0
- package/dist/socks5-WTJBYGME.js.map +1 -0
- package/package.json +12 -6
- package/LICENSE +0 -190
- package/dist/chunk-5WRI5ZAA.js +0 -31
- package/dist/mina-signer-J7GFWOGO.js +0 -6317
- package/dist/mina-signer-J7GFWOGO.js.map +0 -1
- /package/dist/{chunk-5WRI5ZAA.js.map → anon-proxy-W3KMM7GU.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
# @toon-protocol/client
|
|
2
2
|
|
|
3
|
-
|
|
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 (
|
|
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
|
|
12
|
-
- **Multi-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
193
|
-
- **02 - Payment Channel Lifecycle
|
|
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":[]}
|