@corpus-core/colibri-tor 1.1.22

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.
Files changed (93) hide show
  1. package/README.md +90 -0
  2. package/dist/browser.d.ts +48 -0
  3. package/dist/browser.js +96 -0
  4. package/dist/index.d.ts +25 -0
  5. package/dist/index.js +24 -0
  6. package/dist/node.d.ts +34 -0
  7. package/dist/node.js +275 -0
  8. package/dist/types.d.ts +39 -0
  9. package/dist/types.js +23 -0
  10. package/node_modules/tor-js/README.md +166 -0
  11. package/node_modules/tor-js/dist/Log.d.ts +24 -0
  12. package/node_modules/tor-js/dist/Log.d.ts.map +1 -0
  13. package/node_modules/tor-js/dist/TorClient.d.ts +37 -0
  14. package/node_modules/tor-js/dist/TorClient.d.ts.map +1 -0
  15. package/node_modules/tor-js/dist/commonExports.d.ts +6 -0
  16. package/node_modules/tor-js/dist/commonExports.d.ts.map +1 -0
  17. package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.d.ts +3 -0
  18. package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.d.ts.map +1 -0
  19. package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.js +2139 -0
  20. package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.js.map +1 -0
  21. package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.d.ts +4 -0
  22. package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.d.ts.map +1 -0
  23. package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.js +2187 -0
  24. package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.js.map +1 -0
  25. package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.d.ts +3 -0
  26. package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.d.ts.map +1 -0
  27. package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.js +2242 -0
  28. package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.js.map +1 -0
  29. package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.d.ts +4 -0
  30. package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.d.ts.map +1 -0
  31. package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.js +2290 -0
  32. package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.js.map +1 -0
  33. package/node_modules/tor-js/dist/entryPoints/wasm-file/index.d.ts +3 -0
  34. package/node_modules/tor-js/dist/entryPoints/wasm-file/index.d.ts.map +1 -0
  35. package/node_modules/tor-js/dist/entryPoints/wasm-file/index.js +2139 -0
  36. package/node_modules/tor-js/dist/entryPoints/wasm-file/index.js.map +1 -0
  37. package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.d.ts +4 -0
  38. package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.d.ts.map +1 -0
  39. package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.js +2187 -0
  40. package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.js.map +1 -0
  41. package/node_modules/tor-js/dist/helpers.d.ts +7 -0
  42. package/node_modules/tor-js/dist/helpers.d.ts.map +1 -0
  43. package/node_modules/tor-js/dist/polyfills.d.ts +1 -0
  44. package/node_modules/tor-js/dist/polyfills.d.ts.map +1 -0
  45. package/node_modules/tor-js/dist/singleton.d.ts +24 -0
  46. package/node_modules/tor-js/dist/singleton.d.ts.map +1 -0
  47. package/node_modules/tor-js/dist/socketProvider.d.ts +76 -0
  48. package/node_modules/tor-js/dist/socketProvider.d.ts.map +1 -0
  49. package/node_modules/tor-js/dist/storage/filesystem.d.ts +18 -0
  50. package/node_modules/tor-js/dist/storage/filesystem.d.ts.map +1 -0
  51. package/node_modules/tor-js/dist/storage/index.d.ts +7 -0
  52. package/node_modules/tor-js/dist/storage/index.d.ts.map +1 -0
  53. package/node_modules/tor-js/dist/storage/indexeddb.d.ts +14 -0
  54. package/node_modules/tor-js/dist/storage/indexeddb.d.ts.map +1 -0
  55. package/node_modules/tor-js/dist/storage/locking.d.ts +15 -0
  56. package/node_modules/tor-js/dist/storage/locking.d.ts.map +1 -0
  57. package/node_modules/tor-js/dist/storage/memory.d.ts +13 -0
  58. package/node_modules/tor-js/dist/storage/memory.d.ts.map +1 -0
  59. package/node_modules/tor-js/dist/storage/node-deps.d.ts +8 -0
  60. package/node_modules/tor-js/dist/storage/node-deps.d.ts.map +1 -0
  61. package/node_modules/tor-js/dist/tor_js_bg.wasm +0 -0
  62. package/node_modules/tor-js/dist/types.d.ts +41 -0
  63. package/node_modules/tor-js/dist/types.d.ts.map +1 -0
  64. package/node_modules/tor-js/dist/wasm-B6es-efC.d.ts +302 -0
  65. package/node_modules/tor-js/dist/wasm-pkg/tor_js.d.ts +311 -0
  66. package/node_modules/tor-js/dist/wasm-pkg/tor_js.js +1159 -0
  67. package/node_modules/tor-js/dist/wasm.d.ts +31 -0
  68. package/node_modules/tor-js/dist/wasm.d.ts.map +1 -0
  69. package/node_modules/tor-js/package.json +61 -0
  70. package/node_modules/tor-js/src/Log.ts +100 -0
  71. package/node_modules/tor-js/src/TorClient.ts +134 -0
  72. package/node_modules/tor-js/src/commonExports.ts +7 -0
  73. package/node_modules/tor-js/src/entryPoints/wasm-base64/index.ts +17 -0
  74. package/node_modules/tor-js/src/entryPoints/wasm-base64/singleton.ts +7 -0
  75. package/node_modules/tor-js/src/entryPoints/wasm-cdn/index.ts +155 -0
  76. package/node_modules/tor-js/src/entryPoints/wasm-cdn/singleton.ts +7 -0
  77. package/node_modules/tor-js/src/entryPoints/wasm-file/index.ts +19 -0
  78. package/node_modules/tor-js/src/entryPoints/wasm-file/singleton.ts +7 -0
  79. package/node_modules/tor-js/src/globals.d.ts +2 -0
  80. package/node_modules/tor-js/src/helpers.ts +20 -0
  81. package/node_modules/tor-js/src/polyfills.ts +4 -0
  82. package/node_modules/tor-js/src/singleton.ts +54 -0
  83. package/node_modules/tor-js/src/socketProvider.ts +405 -0
  84. package/node_modules/tor-js/src/storage/filesystem.ts +171 -0
  85. package/node_modules/tor-js/src/storage/index.ts +21 -0
  86. package/node_modules/tor-js/src/storage/indexeddb.ts +99 -0
  87. package/node_modules/tor-js/src/storage/locking.ts +195 -0
  88. package/node_modules/tor-js/src/storage/memory.ts +42 -0
  89. package/node_modules/tor-js/src/storage/node-deps.ts +23 -0
  90. package/node_modules/tor-js/src/types.ts +48 -0
  91. package/node_modules/tor-js/src/wasm-base64-data.d.ts +3 -0
  92. package/node_modules/tor-js/src/wasm.ts +135 -0
  93. package/package.json +67 -0
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ <img src="c4_logo.png" alt="C4 Logo" width="300"/>
2
+
3
+ # @corpus-core/colibri-tor
4
+
5
+ Tor network transport for [Colibri Stateless](https://github.com/corpus-core/colibri-stateless). Routes all RPC requests through [Tor](https://www.torproject.org/) for enhanced network-level privacy.
6
+
7
+ - **Browser**: Uses [Arti](https://gitlab.torproject.org/tpo/core/arti) compiled to WebAssembly via [tor-js](https://github.com/voltrevo/arti) -- no browser extension or external software needed.
8
+ - **Node.js**: Connects to a locally running Tor SOCKS5 proxy -- zero dependencies, pure `node:net`/`node:tls` implementation.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm install @corpus-core/colibri-tor @corpus-core/colibri-stateless
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### Browser (Arti WASM)
19
+
20
+ ```typescript
21
+ import Colibri from '@corpus-core/colibri-stateless';
22
+ import { createBrowserFetch } from '@corpus-core/colibri-tor/browser';
23
+
24
+ // Bootstrap starts immediately in the background (no await needed).
25
+ // The first actual request will wait for bootstrap to complete.
26
+ const client = new Colibri({
27
+ fetch: createBrowserFetch(),
28
+ prover: ['https://mainnet.colibri-proof.tech']
29
+ });
30
+
31
+ // This request will await Tor bootstrap if still in progress
32
+ const balance = await client.request({
33
+ method: 'eth_getBalance',
34
+ params: ['0x...', 'latest']
35
+ });
36
+ ```
37
+
38
+ ### Node.js (SOCKS5 Proxy)
39
+
40
+ Start a Tor daemon first (e.g. `tor --SocksPort 9050` or via your system's package manager), then:
41
+
42
+ ```typescript
43
+ import Colibri from '@corpus-core/colibri-stateless';
44
+ import { createSocksFetch } from '@corpus-core/colibri-tor/node';
45
+
46
+ const torFetch = await createSocksFetch({ socksPort: 9050 });
47
+
48
+ const client = new Colibri({
49
+ fetch: torFetch,
50
+ prover: ['https://mainnet.colibri-proof.tech']
51
+ });
52
+ ```
53
+
54
+ ## API
55
+
56
+ ### `createBrowserFetch(options?): typeof fetch`
57
+
58
+ Creates a `fetch`-compatible function that routes requests through Tor via Arti WASM. Browser only. Returns synchronously -- Tor bootstrap starts immediately in the background and the first actual request awaits completion if needed.
59
+
60
+ | Option | Type | Default | Description |
61
+ |--------|------|---------|-------------|
62
+ | `gateway` | `string` | `'https://tor-js-gateway.voltrevo.com'` | WebSocket/WebRTC gateway URL for Tor relay connections |
63
+ | `onBootstrap` | `(ms: number) => void` | `undefined` | Callback when Tor bootstrap completes |
64
+ | `logLevel` | `LogLevel` | `'warn'` | Arti log level (`'trace'` \| `'debug'` \| `'info'` \| `'warn'` \| `'error'`) |
65
+
66
+ ### `createSocksFetch(options?): Promise<typeof fetch>`
67
+
68
+ Creates a `fetch`-compatible function that routes requests through a local Tor SOCKS5 proxy. Node.js only.
69
+
70
+ | Option | Type | Default | Description |
71
+ |--------|------|---------|-------------|
72
+ | `socksHost` | `string` | `'127.0.0.1'` | SOCKS5 proxy hostname |
73
+ | `socksPort` | `number` | `9050` | SOCKS5 proxy port |
74
+
75
+ ## Building tor-js from source
76
+
77
+ Until `tor-js` is published on npm, the CI automatically builds the Arti WASM package from source and bundles it with `@corpus-core/colibri-tor` via `bundleDependencies`. Users installing the published package get `tor-js` included -- no extra steps needed.
78
+
79
+ For local development, you can trigger the same build manually:
80
+
81
+ ```sh
82
+ # Requires: Rust toolchain, wasm-pack, Node.js
83
+ npm run build:arti
84
+ ```
85
+
86
+ This clones [voltrevo/arti](https://github.com/voltrevo/arti), builds the WASM package, and installs the resulting `tor-js` ts-wrapper into `node_modules/`.
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Copyright (c) 2025 corpus.core
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ * this software and associated documentation files (the "Software"), to deal in
6
+ * the Software without restriction, including without limitation the rights to
7
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ * the Software, and to permit persons to whom the Software is furnished to do so,
9
+ * subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ *
21
+ * SPDX-License-Identifier: MIT
22
+ */
23
+ import type { ThorBrowserOptions } from './types.js';
24
+ import { DEFAULT_GATEWAY } from './types.js';
25
+ export type { ThorBrowserOptions };
26
+ export { DEFAULT_GATEWAY };
27
+ /**
28
+ * Create a `fetch`-compatible function that routes HTTP requests through the
29
+ * Tor network using Arti compiled to WebAssembly. Browser only.
30
+ *
31
+ * Tor bootstrap starts immediately but does **not** block the returned
32
+ * function. The first actual `fetch` call will await the bootstrap if it
33
+ * has not completed yet. This allows the application to continue
34
+ * initializing while Tor connects in the background.
35
+ *
36
+ * ```typescript
37
+ * // Bootstrap starts immediately, returns without blocking
38
+ * const torFetch = createBrowserFetch();
39
+ * const client = new Colibri({ fetch: torFetch });
40
+ * // ... app continues initializing ...
41
+ * // First request will await bootstrap completion if still in progress
42
+ * await client.request({ method: 'eth_blockNumber' });
43
+ * ```
44
+ *
45
+ * @param options - Browser transport options (gateway URL, log level, etc.)
46
+ * @return A `fetch`-compatible function that routes through Tor
47
+ */
48
+ export declare function createBrowserFetch(options?: ThorBrowserOptions): typeof globalThis.fetch;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Copyright (c) 2025 corpus.core
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ * this software and associated documentation files (the "Software"), to deal in
6
+ * the Software without restriction, including without limitation the rights to
7
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ * the Software, and to permit persons to whom the Software is furnished to do so,
9
+ * subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ *
21
+ * SPDX-License-Identifier: MIT
22
+ */
23
+ import { DEFAULT_GATEWAY } from './types.js';
24
+ export { DEFAULT_GATEWAY };
25
+ // tor-js is loaded dynamically so the import path can switch between vendored
26
+ // artifacts (built from source via scripts/build-arti.sh) and the future npm
27
+ // package without changing this file.
28
+ let torJsImport = null;
29
+ function getTorJs() {
30
+ if (!torJsImport) {
31
+ // Dynamic import -- resolved at runtime. When tor-js is published on
32
+ // npm this will become `import('tor-js')`. Until then, the vendored
33
+ // build (src/vendor/tor-js.js) is used via the package.json "imports"
34
+ // map or a direct relative path.
35
+ torJsImport = import('tor-js').catch(() => {
36
+ throw new Error('tor-js is not installed. Install it via npm (once published) ' +
37
+ 'or run `npm run build:arti` to build from source.');
38
+ });
39
+ }
40
+ return torJsImport;
41
+ }
42
+ /**
43
+ * Create a `fetch`-compatible function that routes HTTP requests through the
44
+ * Tor network using Arti compiled to WebAssembly. Browser only.
45
+ *
46
+ * Tor bootstrap starts immediately but does **not** block the returned
47
+ * function. The first actual `fetch` call will await the bootstrap if it
48
+ * has not completed yet. This allows the application to continue
49
+ * initializing while Tor connects in the background.
50
+ *
51
+ * ```typescript
52
+ * // Bootstrap starts immediately, returns without blocking
53
+ * const torFetch = createBrowserFetch();
54
+ * const client = new Colibri({ fetch: torFetch });
55
+ * // ... app continues initializing ...
56
+ * // First request will await bootstrap completion if still in progress
57
+ * await client.request({ method: 'eth_blockNumber' });
58
+ * ```
59
+ *
60
+ * @param options - Browser transport options (gateway URL, log level, etc.)
61
+ * @return A `fetch`-compatible function that routes through Tor
62
+ */
63
+ export function createBrowserFetch(options = {}) {
64
+ const gateway = options.gateway ?? DEFAULT_GATEWAY;
65
+ // Kick off bootstrap eagerly -- the promise is shared across all requests.
66
+ const startTime = Date.now();
67
+ const clientReady = getTorJs().then(async ({ TorClient }) => {
68
+ const client = new TorClient({
69
+ gateway,
70
+ logLevel: options.logLevel ?? 'warn',
71
+ });
72
+ await client.ready();
73
+ options.onBootstrap?.(Date.now() - startTime);
74
+ return client;
75
+ });
76
+ const torFetch = async (input, init) => {
77
+ const client = await clientReady;
78
+ const url = typeof input === 'string'
79
+ ? input
80
+ : input instanceof URL
81
+ ? input.href
82
+ : input.url;
83
+ // tor-js TorClient.fetch() returns a Response-compatible object.
84
+ // We validate the essential properties to catch API mismatches early.
85
+ const response = await client.fetch(url, {
86
+ method: init?.method,
87
+ headers: init?.headers,
88
+ body: init?.body,
89
+ });
90
+ if (typeof response?.status !== 'number') {
91
+ throw new Error('tor-js returned an invalid response object (missing status)');
92
+ }
93
+ return response;
94
+ };
95
+ return torFetch;
96
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright (c) 2025 corpus.core
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ * this software and associated documentation files (the "Software"), to deal in
6
+ * the Software without restriction, including without limitation the rights to
7
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ * the Software, and to permit persons to whom the Software is furnished to do so,
9
+ * subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ *
21
+ * SPDX-License-Identifier: MIT
22
+ */
23
+ export { createBrowserFetch, DEFAULT_GATEWAY } from './browser.js';
24
+ export { createSocksFetch } from './node.js';
25
+ export type { ThorBrowserOptions, ThorNodeOptions, LogLevel } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Copyright (c) 2025 corpus.core
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ * this software and associated documentation files (the "Software"), to deal in
6
+ * the Software without restriction, including without limitation the rights to
7
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ * the Software, and to permit persons to whom the Software is furnished to do so,
9
+ * subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ *
21
+ * SPDX-License-Identifier: MIT
22
+ */
23
+ export { createBrowserFetch, DEFAULT_GATEWAY } from './browser.js';
24
+ export { createSocksFetch } from './node.js';
package/dist/node.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Copyright (c) 2025 corpus.core
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ * this software and associated documentation files (the "Software"), to deal in
6
+ * the Software without restriction, including without limitation the rights to
7
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ * the Software, and to permit persons to whom the Software is furnished to do so,
9
+ * subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ *
21
+ * SPDX-License-Identifier: MIT
22
+ */
23
+ import type { ThorNodeOptions } from './types.js';
24
+ export type { ThorNodeOptions };
25
+ /**
26
+ * Create a `fetch`-compatible function that routes HTTP requests through a
27
+ * locally running Tor SOCKS5 proxy. Node.js only.
28
+ *
29
+ * The user must start the Tor daemon themselves (e.g. `tor --SocksPort 9050`).
30
+ *
31
+ * @param options - Node.js transport options (SOCKS host/port)
32
+ * @return A `fetch`-compatible function routing through the Tor SOCKS5 proxy
33
+ */
34
+ export declare function createSocksFetch(options?: ThorNodeOptions): Promise<typeof globalThis.fetch>;
package/dist/node.js ADDED
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Copyright (c) 2025 corpus.core
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ * this software and associated documentation files (the "Software"), to deal in
6
+ * the Software without restriction, including without limitation the rights to
7
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ * the Software, and to permit persons to whom the Software is furnished to do so,
9
+ * subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ *
21
+ * SPDX-License-Identifier: MIT
22
+ */
23
+ const SOCKS5_HANDSHAKE_TIMEOUT_MS = 30000;
24
+ const HTTP_RESPONSE_TIMEOUT_MS = 60000;
25
+ const MAX_RESPONSE_SIZE = 64 * 1024 * 1024; // 64 MiB
26
+ /**
27
+ * Perform a SOCKS5 CONNECT handshake over an existing TCP socket.
28
+ *
29
+ * Implements RFC 1928 (no-auth) + CONNECT to a domain:port target.
30
+ * Uses buffered reads to handle TCP stream fragmentation correctly.
31
+ * The socket is left connected to the remote host on success.
32
+ */
33
+ async function socks5Connect(socksHost, socksPort, targetHost, targetPort) {
34
+ const net = await import('node:net');
35
+ const hostBuf = Buffer.from(targetHost, 'utf-8');
36
+ if (hostBuf.length > 255) {
37
+ throw new Error(`SOCKS5: hostname too long (${hostBuf.length} bytes, max 255)`);
38
+ }
39
+ return new Promise((resolve, reject) => {
40
+ const socket = net.createConnection(socksPort, socksHost, () => {
41
+ socket.write(Buffer.from([0x05, 0x01, 0x00]));
42
+ });
43
+ socket.setTimeout(SOCKS5_HANDSHAKE_TIMEOUT_MS);
44
+ socket.once('timeout', () => {
45
+ socket.destroy();
46
+ reject(new Error('SOCKS5 handshake timeout'));
47
+ });
48
+ let phase = 'greeting';
49
+ let buffer = Buffer.alloc(0);
50
+ socket.once('error', (err) => {
51
+ socket.destroy();
52
+ reject(err);
53
+ });
54
+ socket.on('data', (chunk) => {
55
+ buffer = Buffer.concat([buffer, chunk]);
56
+ if (phase === 'greeting') {
57
+ if (buffer.length < 2)
58
+ return;
59
+ if (buffer[0] !== 0x05 || buffer[1] !== 0x00) {
60
+ socket.destroy();
61
+ reject(new Error(`SOCKS5 handshake failed: server chose method ${buffer[1]}`));
62
+ return;
63
+ }
64
+ buffer = buffer.subarray(2);
65
+ phase = 'connect';
66
+ const req = Buffer.alloc(4 + 1 + hostBuf.length + 2);
67
+ req[0] = 0x05; // VER
68
+ req[1] = 0x01; // CMD: CONNECT
69
+ req[2] = 0x00; // RSV
70
+ req[3] = 0x03; // ATYP: DOMAINNAME
71
+ req[4] = hostBuf.length;
72
+ hostBuf.copy(req, 5);
73
+ req.writeUInt16BE(targetPort, 5 + hostBuf.length);
74
+ socket.write(req);
75
+ }
76
+ if (phase === 'connect') {
77
+ // Minimum CONNECT response: VER(1) + REP(1) + RSV(1) + ATYP(1) + addr + port(2)
78
+ // For ATYP=1 (IPv4): 4+4+2 = 10 bytes total
79
+ if (buffer.length < 4)
80
+ return;
81
+ let expectedLen;
82
+ switch (buffer[3]) {
83
+ case 0x01:
84
+ expectedLen = 10;
85
+ break; // IPv4: 4 header + 4 addr + 2 port
86
+ case 0x04:
87
+ expectedLen = 22;
88
+ break; // IPv6: 4 header + 16 addr + 2 port
89
+ case 0x03: { // Domain: 4 header + 1 len + N + 2 port
90
+ if (buffer.length < 5)
91
+ return;
92
+ expectedLen = 5 + buffer[4] + 2;
93
+ break;
94
+ }
95
+ default:
96
+ expectedLen = 10;
97
+ break;
98
+ }
99
+ if (buffer.length < expectedLen)
100
+ return;
101
+ if (buffer[0] !== 0x05) {
102
+ socket.destroy();
103
+ reject(new Error('SOCKS5 connect: unexpected version'));
104
+ return;
105
+ }
106
+ if (buffer[1] !== 0x00) {
107
+ socket.destroy();
108
+ const codes = {
109
+ 0x01: 'general failure',
110
+ 0x02: 'connection not allowed',
111
+ 0x03: 'network unreachable',
112
+ 0x04: 'host unreachable',
113
+ 0x05: 'connection refused',
114
+ 0x06: 'TTL expired',
115
+ 0x07: 'command not supported',
116
+ 0x08: 'address type not supported',
117
+ };
118
+ reject(new Error(`SOCKS5 connect failed: ${codes[buffer[1]] || `code ${buffer[1]}`}`));
119
+ return;
120
+ }
121
+ socket.setTimeout(0);
122
+ socket.removeAllListeners('data');
123
+ socket.removeAllListeners('error');
124
+ socket.removeAllListeners('timeout');
125
+ resolve(socket);
126
+ }
127
+ });
128
+ });
129
+ }
130
+ /**
131
+ * Perform an HTTP request through an established SOCKS5 tunnel.
132
+ *
133
+ * For HTTPS targets, wraps the raw socket in TLS using `node:tls`.
134
+ * Uses HTTP/1.0 to avoid chunked transfer-encoding complexity.
135
+ * Returns a standard Web API `Response` (Node 18+).
136
+ */
137
+ async function httpOverSocks(socksHost, socksPort, url, init) {
138
+ const parsed = new URL(url);
139
+ const isHttps = parsed.protocol === 'https:';
140
+ const targetPort = parsed.port ? parseInt(parsed.port) : (isHttps ? 443 : 80);
141
+ let socket = await socks5Connect(socksHost, socksPort, parsed.hostname, targetPort);
142
+ if (isHttps) {
143
+ const tls = await import('node:tls');
144
+ const tlsSocket = tls.connect({
145
+ socket: socket,
146
+ servername: parsed.hostname,
147
+ });
148
+ await new Promise((resolve, reject) => {
149
+ tlsSocket.once('secureConnect', resolve);
150
+ tlsSocket.once('error', reject);
151
+ });
152
+ socket = tlsSocket;
153
+ }
154
+ const method = init?.method ?? 'GET';
155
+ if (!/^[A-Z]+$/.test(method)) {
156
+ socket.destroy();
157
+ throw new Error(`Invalid HTTP method: ${method}`);
158
+ }
159
+ const bodyStr = init?.body != null
160
+ ? (typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body))
161
+ : undefined;
162
+ const bodyBuf = bodyStr ? Buffer.from(bodyStr, 'utf-8') : undefined;
163
+ const headers = {
164
+ 'Host': parsed.host,
165
+ 'Connection': 'close',
166
+ };
167
+ if (init?.headers) {
168
+ const h = init.headers;
169
+ if (h instanceof Headers) {
170
+ h.forEach((v, k) => { headers[k] = v; });
171
+ }
172
+ else if (Array.isArray(h)) {
173
+ for (const [k, v] of h)
174
+ headers[k] = v;
175
+ }
176
+ else {
177
+ Object.assign(headers, h);
178
+ }
179
+ }
180
+ if (bodyBuf) {
181
+ headers['Content-Length'] = bodyBuf.length.toString();
182
+ }
183
+ // Validate headers against CRLF injection (CWE-113)
184
+ for (const [k, v] of Object.entries(headers)) {
185
+ if (/[\r\n]/.test(k) || /[\r\n]/.test(v)) {
186
+ socket.destroy();
187
+ throw new Error('Invalid header: CR/LF characters not allowed in header name or value');
188
+ }
189
+ }
190
+ // HTTP/1.0 avoids chunked transfer-encoding; Connection: close ensures
191
+ // the server closes the socket after the response.
192
+ const path = parsed.pathname + parsed.search;
193
+ let reqStr = `${method} ${path} HTTP/1.0\r\n`;
194
+ for (const [k, v] of Object.entries(headers)) {
195
+ reqStr += `${k}: ${v}\r\n`;
196
+ }
197
+ reqStr += '\r\n';
198
+ return new Promise((resolve, reject) => {
199
+ socket.setTimeout(HTTP_RESPONSE_TIMEOUT_MS);
200
+ socket.once('timeout', () => {
201
+ socket.destroy();
202
+ reject(new Error('HTTP response timeout'));
203
+ });
204
+ socket.once('error', (err) => {
205
+ socket.destroy();
206
+ reject(err);
207
+ });
208
+ let totalSize = 0;
209
+ const chunks = [];
210
+ socket.on('data', (chunk) => {
211
+ totalSize += chunk.length;
212
+ if (totalSize > MAX_RESPONSE_SIZE) {
213
+ socket.destroy();
214
+ reject(new Error(`Response too large (>${MAX_RESPONSE_SIZE} bytes)`));
215
+ return;
216
+ }
217
+ chunks.push(chunk);
218
+ });
219
+ socket.on('end', () => {
220
+ socket.destroy();
221
+ try {
222
+ const raw = Buffer.concat(chunks);
223
+ const headerEnd = raw.indexOf('\r\n\r\n');
224
+ if (headerEnd === -1) {
225
+ reject(new Error('Malformed HTTP response: no header/body separator'));
226
+ return;
227
+ }
228
+ const headerStr = raw.subarray(0, headerEnd).toString('utf-8');
229
+ const body = raw.subarray(headerEnd + 4);
230
+ const [statusLine, ...headerLines] = headerStr.split('\r\n');
231
+ const statusMatch = statusLine.match(/^HTTP\/[\d.]+ (\d+)/);
232
+ const status = statusMatch ? parseInt(statusMatch[1]) : 0;
233
+ const respHeaders = new Headers();
234
+ for (const line of headerLines) {
235
+ const idx = line.indexOf(':');
236
+ if (idx > 0) {
237
+ respHeaders.append(line.substring(0, idx).trim(), line.substring(idx + 1).trim());
238
+ }
239
+ }
240
+ resolve(new Response(body, { status, headers: respHeaders }));
241
+ }
242
+ catch (e) {
243
+ reject(e);
244
+ }
245
+ });
246
+ socket.write(reqStr);
247
+ if (bodyBuf)
248
+ socket.write(bodyBuf);
249
+ });
250
+ }
251
+ /**
252
+ * Create a `fetch`-compatible function that routes HTTP requests through a
253
+ * locally running Tor SOCKS5 proxy. Node.js only.
254
+ *
255
+ * The user must start the Tor daemon themselves (e.g. `tor --SocksPort 9050`).
256
+ *
257
+ * @param options - Node.js transport options (SOCKS host/port)
258
+ * @return A `fetch`-compatible function routing through the Tor SOCKS5 proxy
259
+ */
260
+ export async function createSocksFetch(options = {}) {
261
+ const host = options.socksHost ?? '127.0.0.1';
262
+ const port = options.socksPort ?? 9050;
263
+ if (port < 1 || port > 65535 || !Number.isInteger(port)) {
264
+ throw new Error(`Invalid SOCKS port: ${port} (must be 1-65535)`);
265
+ }
266
+ const socksFetch = async (input, init) => {
267
+ const url = typeof input === 'string'
268
+ ? input
269
+ : input instanceof URL
270
+ ? input.href
271
+ : input.url;
272
+ return httpOverSocks(host, port, url, init);
273
+ };
274
+ return socksFetch;
275
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Copyright (c) 2025 corpus.core
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ * this software and associated documentation files (the "Software"), to deal in
6
+ * the Software without restriction, including without limitation the rights to
7
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ * the Software, and to permit persons to whom the Software is furnished to do so,
9
+ * subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ *
21
+ * SPDX-License-Identifier: MIT
22
+ */
23
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
24
+ export declare const DEFAULT_GATEWAY = "https://tor-js-gateway.voltrevo.com";
25
+ export interface ThorBrowserOptions {
26
+ /** WebSocket/WebRTC gateway URL for Tor relay connections.
27
+ * Default: `'https://tor-js-gateway.voltrevo.com'`. */
28
+ gateway?: string;
29
+ /** Callback invoked when Tor bootstrap completes, with elapsed time in ms. */
30
+ onBootstrap?: (elapsedMs: number) => void;
31
+ /** Arti log level. Default: `'warn'`. */
32
+ logLevel?: LogLevel;
33
+ }
34
+ export interface ThorNodeOptions {
35
+ /** SOCKS5 proxy hostname. Default: `'127.0.0.1'`. */
36
+ socksHost?: string;
37
+ /** SOCKS5 proxy port. Default: `9050`. */
38
+ socksPort?: number;
39
+ }
package/dist/types.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Copyright (c) 2025 corpus.core
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ * this software and associated documentation files (the "Software"), to deal in
6
+ * the Software without restriction, including without limitation the rights to
7
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ * the Software, and to permit persons to whom the Software is furnished to do so,
9
+ * subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ *
21
+ * SPDX-License-Identifier: MIT
22
+ */
23
+ export const DEFAULT_GATEWAY = 'https://tor-js-gateway.voltrevo.com';