@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.
- package/README.md +90 -0
- package/dist/browser.d.ts +48 -0
- package/dist/browser.js +96 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +24 -0
- package/dist/node.d.ts +34 -0
- package/dist/node.js +275 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.js +23 -0
- package/node_modules/tor-js/README.md +166 -0
- package/node_modules/tor-js/dist/Log.d.ts +24 -0
- package/node_modules/tor-js/dist/Log.d.ts.map +1 -0
- package/node_modules/tor-js/dist/TorClient.d.ts +37 -0
- package/node_modules/tor-js/dist/TorClient.d.ts.map +1 -0
- package/node_modules/tor-js/dist/commonExports.d.ts +6 -0
- package/node_modules/tor-js/dist/commonExports.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.d.ts +3 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.js +2139 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.d.ts +4 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.js +2187 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.d.ts +3 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.js +2242 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.d.ts +4 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.js +2290 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/index.d.ts +3 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/index.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/index.js +2139 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/index.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.d.ts +4 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.js +2187 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.js.map +1 -0
- package/node_modules/tor-js/dist/helpers.d.ts +7 -0
- package/node_modules/tor-js/dist/helpers.d.ts.map +1 -0
- package/node_modules/tor-js/dist/polyfills.d.ts +1 -0
- package/node_modules/tor-js/dist/polyfills.d.ts.map +1 -0
- package/node_modules/tor-js/dist/singleton.d.ts +24 -0
- package/node_modules/tor-js/dist/singleton.d.ts.map +1 -0
- package/node_modules/tor-js/dist/socketProvider.d.ts +76 -0
- package/node_modules/tor-js/dist/socketProvider.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/filesystem.d.ts +18 -0
- package/node_modules/tor-js/dist/storage/filesystem.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/index.d.ts +7 -0
- package/node_modules/tor-js/dist/storage/index.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/indexeddb.d.ts +14 -0
- package/node_modules/tor-js/dist/storage/indexeddb.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/locking.d.ts +15 -0
- package/node_modules/tor-js/dist/storage/locking.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/memory.d.ts +13 -0
- package/node_modules/tor-js/dist/storage/memory.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/node-deps.d.ts +8 -0
- package/node_modules/tor-js/dist/storage/node-deps.d.ts.map +1 -0
- package/node_modules/tor-js/dist/tor_js_bg.wasm +0 -0
- package/node_modules/tor-js/dist/types.d.ts +41 -0
- package/node_modules/tor-js/dist/types.d.ts.map +1 -0
- package/node_modules/tor-js/dist/wasm-B6es-efC.d.ts +302 -0
- package/node_modules/tor-js/dist/wasm-pkg/tor_js.d.ts +311 -0
- package/node_modules/tor-js/dist/wasm-pkg/tor_js.js +1159 -0
- package/node_modules/tor-js/dist/wasm.d.ts +31 -0
- package/node_modules/tor-js/dist/wasm.d.ts.map +1 -0
- package/node_modules/tor-js/package.json +61 -0
- package/node_modules/tor-js/src/Log.ts +100 -0
- package/node_modules/tor-js/src/TorClient.ts +134 -0
- package/node_modules/tor-js/src/commonExports.ts +7 -0
- package/node_modules/tor-js/src/entryPoints/wasm-base64/index.ts +17 -0
- package/node_modules/tor-js/src/entryPoints/wasm-base64/singleton.ts +7 -0
- package/node_modules/tor-js/src/entryPoints/wasm-cdn/index.ts +155 -0
- package/node_modules/tor-js/src/entryPoints/wasm-cdn/singleton.ts +7 -0
- package/node_modules/tor-js/src/entryPoints/wasm-file/index.ts +19 -0
- package/node_modules/tor-js/src/entryPoints/wasm-file/singleton.ts +7 -0
- package/node_modules/tor-js/src/globals.d.ts +2 -0
- package/node_modules/tor-js/src/helpers.ts +20 -0
- package/node_modules/tor-js/src/polyfills.ts +4 -0
- package/node_modules/tor-js/src/singleton.ts +54 -0
- package/node_modules/tor-js/src/socketProvider.ts +405 -0
- package/node_modules/tor-js/src/storage/filesystem.ts +171 -0
- package/node_modules/tor-js/src/storage/index.ts +21 -0
- package/node_modules/tor-js/src/storage/indexeddb.ts +99 -0
- package/node_modules/tor-js/src/storage/locking.ts +195 -0
- package/node_modules/tor-js/src/storage/memory.ts +42 -0
- package/node_modules/tor-js/src/storage/node-deps.ts +23 -0
- package/node_modules/tor-js/src/types.ts +48 -0
- package/node_modules/tor-js/src/wasm-base64-data.d.ts +3 -0
- package/node_modules/tor-js/src/wasm.ts +135 -0
- package/package.json +67 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import './polyfills.js';
|
|
2
|
+
import { TorClient as WasmTorClient, TorClientOptions as WasmTorClientOptions } from '#wasm';
|
|
3
|
+
export { WasmTorClient, WasmTorClientOptions };
|
|
4
|
+
type WasmLogCallback = (level: string, target: string, message: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Register a log callback at a given level. The WASM subscriber is
|
|
7
|
+
* automatically widened to the broadest level across all listeners.
|
|
8
|
+
* Each listener only receives events at or above its own level.
|
|
9
|
+
* Returns an unregister function.
|
|
10
|
+
*/
|
|
11
|
+
export declare function addLogListener(cb: WasmLogCallback, level?: string): () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Update the level for an existing listener and re-sync the WASM filter.
|
|
14
|
+
*/
|
|
15
|
+
export declare function setListenerLevel(cb: WasmLogCallback, level: string): void;
|
|
16
|
+
type WasmSourceProvider = () => Promise<BufferSource | Uint8Array>;
|
|
17
|
+
/**
|
|
18
|
+
* Override the WASM binary URL. Must be called before any TorClient is created.
|
|
19
|
+
*/
|
|
20
|
+
export declare function setWasmUrl(url: string | URL): void;
|
|
21
|
+
/**
|
|
22
|
+
* Set a custom WASM source provider. Called by entry points to configure
|
|
23
|
+
* how the WASM binary is loaded (e.g. base64 decode, CDN fetch).
|
|
24
|
+
* Must be called before any TorClient is created.
|
|
25
|
+
*/
|
|
26
|
+
export declare function setWasmSourceProvider(provider: WasmSourceProvider): void;
|
|
27
|
+
/**
|
|
28
|
+
* Ensures the WASM module is loaded and initialized. Idempotent.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ensureWasmInitialized(): Promise<void>;
|
|
31
|
+
//# sourceMappingURL=wasm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasm.d.ts","sourceRoot":"","sources":["../src/wasm.ts"],"names":[],"mappings":"AAAA,OAAO,gBAAgB,CAAC;AACxB,OAAiB,EAIf,SAAS,IAAI,aAAa,EAC1B,gBAAgB,IAAI,oBAAoB,EACzC,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,CAAC;AAM/C,KAAK,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AA2BhF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,eAAe,EAAE,KAAK,GAAE,MAAgB,GAAG,MAAM,IAAI,CASvF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAMzE;AAMD,KAAK,kBAAkB,GAAG,MAAM,OAAO,CAAC,YAAY,GAAG,UAAU,CAAC,CAAC;AAMnE;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAKlD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAKxE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAI3D"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tor-js",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "TypeScript Tor client using arti via WASM",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/entryPoints/wasm-cdn/index.js",
|
|
7
|
+
"types": "dist/entryPoints/wasm-cdn/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/entryPoints/wasm-cdn/index.d.ts",
|
|
11
|
+
"import": "./dist/entryPoints/wasm-cdn/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./wasm-cdn": {
|
|
14
|
+
"types": "./dist/entryPoints/wasm-cdn/index.d.ts",
|
|
15
|
+
"import": "./dist/entryPoints/wasm-cdn/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./wasm-file": {
|
|
18
|
+
"types": "./dist/entryPoints/wasm-file/index.d.ts",
|
|
19
|
+
"import": "./dist/entryPoints/wasm-file/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./wasm-base64": {
|
|
22
|
+
"types": "./dist/entryPoints/wasm-base64/index.d.ts",
|
|
23
|
+
"import": "./dist/entryPoints/wasm-base64/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./singleton": {
|
|
26
|
+
"types": "./dist/entryPoints/wasm-cdn/singleton.d.ts",
|
|
27
|
+
"import": "./dist/entryPoints/wasm-cdn/singleton.js"
|
|
28
|
+
},
|
|
29
|
+
"./wasm-cdn/singleton": {
|
|
30
|
+
"types": "./dist/entryPoints/wasm-cdn/singleton.d.ts",
|
|
31
|
+
"import": "./dist/entryPoints/wasm-cdn/singleton.js"
|
|
32
|
+
},
|
|
33
|
+
"./wasm-file/singleton": {
|
|
34
|
+
"types": "./dist/entryPoints/wasm-file/singleton.d.ts",
|
|
35
|
+
"import": "./dist/entryPoints/wasm-file/singleton.js"
|
|
36
|
+
},
|
|
37
|
+
"./wasm-base64/singleton": {
|
|
38
|
+
"types": "./dist/entryPoints/wasm-base64/singleton.d.ts",
|
|
39
|
+
"import": "./dist/entryPoints/wasm-base64/singleton.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"imports": {
|
|
43
|
+
"#wasm": "./dist/wasm-pkg/tor_js.js",
|
|
44
|
+
"#wasm-base64-data": "./dist/wasm-base64-data.js"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"src"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node build.mjs",
|
|
52
|
+
"clean": "rm -rf dist src/wasm-pkg"
|
|
53
|
+
},
|
|
54
|
+
"author": "Andrew Morris",
|
|
55
|
+
"license": "MIT OR Apache-2.0",
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^25.3.0",
|
|
58
|
+
"tsup": "^8.3.5",
|
|
59
|
+
"typescript": "~5.7.3"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
|
|
3
|
+
interface LogConstructorParams {
|
|
4
|
+
rawLog?: (level: LogLevel, ...args: unknown[]) => void;
|
|
5
|
+
parentStartTime?: number;
|
|
6
|
+
namePrefix?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Log {
|
|
10
|
+
private rawLog: (level: LogLevel, ...args: unknown[]) => void;
|
|
11
|
+
private parentStartTime: number;
|
|
12
|
+
private namePrefix: string;
|
|
13
|
+
|
|
14
|
+
constructor(params: LogConstructorParams = {}) {
|
|
15
|
+
this.parentStartTime = params.parentStartTime ?? Date.now();
|
|
16
|
+
this.namePrefix = params.namePrefix ?? '';
|
|
17
|
+
this.rawLog = params.rawLog ?? this.defaultRawLog.bind(this);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
child(name: string): Log {
|
|
21
|
+
const newPrefix = this.namePrefix ? `${this.namePrefix}.${name}` : name;
|
|
22
|
+
return new Log({
|
|
23
|
+
rawLog: this.rawLog,
|
|
24
|
+
parentStartTime: this.parentStartTime,
|
|
25
|
+
namePrefix: newPrefix,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
trace(...args: unknown[]): void {
|
|
30
|
+
this.log('trace', ...args);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
debug(...args: unknown[]): void {
|
|
34
|
+
this.log('debug', ...args);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
info(...args: unknown[]): void {
|
|
38
|
+
this.log('info', ...args);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
warn(...args: unknown[]): void {
|
|
42
|
+
this.log('warn', ...args);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
error(...args: unknown[]): void {
|
|
46
|
+
this.log('error', ...args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @internal Create a callback for WASM setLogCallback */
|
|
50
|
+
_makeWasmCallback(): (level: string, target: string, message: string) => void {
|
|
51
|
+
const levels: ReadonlySet<string> = new Set(['trace', 'debug', 'info', 'warn', 'error']);
|
|
52
|
+
return (level: string, target: string, message: string) => {
|
|
53
|
+
if (!levels.has(level)) {
|
|
54
|
+
this.log('error', `unexpected log level from WASM: ${JSON.stringify(level)}`);
|
|
55
|
+
level = 'debug';
|
|
56
|
+
}
|
|
57
|
+
this.child(target).log(level as LogLevel, message);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private log(level: LogLevel, ...args: unknown[]): void {
|
|
62
|
+
const elapsed = Date.now() - this.parentStartTime;
|
|
63
|
+
const timestamp = formatTimestamp(elapsed);
|
|
64
|
+
if (this.namePrefix) {
|
|
65
|
+
this.rawLog(level, `[${timestamp}]`, `[${this.namePrefix}]`, ...args);
|
|
66
|
+
} else {
|
|
67
|
+
this.rawLog(level, `[${timestamp}]`, ...args);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private defaultRawLog(level: LogLevel, ...args: unknown[]): void {
|
|
72
|
+
console[level](...args);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatTimestamp(elapsedMs: number): string {
|
|
77
|
+
const totalSeconds = Math.floor(elapsedMs / 1000);
|
|
78
|
+
const milliseconds = elapsedMs % 1000;
|
|
79
|
+
const ms = String(milliseconds).padStart(3, '0');
|
|
80
|
+
|
|
81
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
82
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
83
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
84
|
+
const seconds = totalSeconds % 60;
|
|
85
|
+
|
|
86
|
+
if (days > 0) {
|
|
87
|
+
return `${days}d ${p2(hours)}:${p2(minutes)}:${p2(seconds)}.${ms}`;
|
|
88
|
+
}
|
|
89
|
+
if (hours > 0) {
|
|
90
|
+
return `${p2(hours)}:${p2(minutes)}:${p2(seconds)}.${ms}`;
|
|
91
|
+
}
|
|
92
|
+
if (minutes > 0) {
|
|
93
|
+
return `${p2(minutes)}:${p2(seconds)}.${ms}`;
|
|
94
|
+
}
|
|
95
|
+
return `${p2(seconds)}.${ms}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function p2(n: number): string {
|
|
99
|
+
return String(n).padStart(2, '0');
|
|
100
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureWasmInitialized,
|
|
3
|
+
WasmTorClient,
|
|
4
|
+
WasmTorClientOptions,
|
|
5
|
+
addLogListener,
|
|
6
|
+
setListenerLevel,
|
|
7
|
+
} from './wasm.js';
|
|
8
|
+
import type { TorClientOptions, FetchInit, LogLevel } from './types.js';
|
|
9
|
+
import { Log } from './Log.js';
|
|
10
|
+
import { createAutoStorage } from './storage/index.js';
|
|
11
|
+
import { ArtiSocketProvider } from './socketProvider.js';
|
|
12
|
+
|
|
13
|
+
export class TorClient {
|
|
14
|
+
private log: Log;
|
|
15
|
+
private clientPromise: Promise<WasmTorClient>;
|
|
16
|
+
private removeLogListener: (() => void) | null = null;
|
|
17
|
+
private wasmCallback: ((level: string, target: string, message: string) => void) | null = null;
|
|
18
|
+
private closed = false;
|
|
19
|
+
private readyPromise: Promise<void> | null = null;
|
|
20
|
+
private socketProvider: ArtiSocketProvider | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(options: TorClientOptions = {}) {
|
|
23
|
+
this.log = (options.log ?? new Log({ rawLog: () => {} }));
|
|
24
|
+
this.clientPromise = this.bootstrap(options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async bootstrap(options: TorClientOptions): Promise<WasmTorClient> {
|
|
28
|
+
await ensureWasmInitialized();
|
|
29
|
+
|
|
30
|
+
// Register log listener with per-client level filtering.
|
|
31
|
+
// The WASM subscriber auto-widens to the broadest level across all listeners.
|
|
32
|
+
this.wasmCallback = this.log._makeWasmCallback();
|
|
33
|
+
this.removeLogListener = addLogListener(this.wasmCallback, options.logLevel);
|
|
34
|
+
|
|
35
|
+
// ArtiSocketProvider handles relay connections. In browsers it needs a gateway
|
|
36
|
+
// URL for WebRTC/WebSocket proxying; in Node.js/Deno it connects via direct TCP.
|
|
37
|
+
this.socketProvider = options.socketProvider ?? new ArtiSocketProvider({ gateway: options.gateway });
|
|
38
|
+
const sp = this.socketProvider;
|
|
39
|
+
|
|
40
|
+
let wasmOptions = new WasmTorClientOptions(
|
|
41
|
+
(addr: string) => sp.connect(addr),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const storage = options.storage ?? createAutoStorage();
|
|
45
|
+
wasmOptions = wasmOptions.withStorage(storage);
|
|
46
|
+
|
|
47
|
+
// Auto-attempt fast bootstrap from gateway — only when a URL is available
|
|
48
|
+
if (options.gateway) {
|
|
49
|
+
const gatewayBase = options.gateway.replace(/\/+$/, '');
|
|
50
|
+
wasmOptions = wasmOptions.withFastBootstrap(async (): Promise<Uint8Array> => {
|
|
51
|
+
this.log.info('Fast bootstrap: fetching bootstrap.zip.br...');
|
|
52
|
+
const res = await fetch(`${gatewayBase}/bootstrap.zip.br`);
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new Error(`Fast bootstrap fetch failed: ${res.status} ${res.statusText}`);
|
|
55
|
+
}
|
|
56
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
57
|
+
this.log.info(`Fast bootstrap: received ${bytes.byteLength} bytes`);
|
|
58
|
+
return bytes;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create client (WASM constructor returns a Promise)
|
|
63
|
+
this.log.info('Bootstrapping...');
|
|
64
|
+
const client = await WasmTorClient.create(wasmOptions);
|
|
65
|
+
this.log.info('Bootstrap complete');
|
|
66
|
+
return client;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Make an HTTP fetch request through Tor.
|
|
71
|
+
* Returns a standard browser Response object.
|
|
72
|
+
*/
|
|
73
|
+
async fetch(url: string, init?: FetchInit): Promise<Response> {
|
|
74
|
+
if (this.closed) throw new Error('TorClient is closed');
|
|
75
|
+
const client = await this.clientPromise;
|
|
76
|
+
await this.ready();
|
|
77
|
+
this.log.info(`Fetching ${url}`);
|
|
78
|
+
return client.fetch(url, init);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wait for the Tor client to be ready for traffic
|
|
83
|
+
* (guard connected, usable consensus, and sufficient microdescs).
|
|
84
|
+
*
|
|
85
|
+
* Parallel callers share the same underlying promise — a single WS
|
|
86
|
+
* connection failure rejects all waiters. The cached promise is cleared
|
|
87
|
+
* on settle so the next call creates a fresh attempt.
|
|
88
|
+
*/
|
|
89
|
+
async ready(): Promise<void> {
|
|
90
|
+
if (this.closed) throw new Error('TorClient is closed');
|
|
91
|
+
if (this.readyPromise) return this.readyPromise;
|
|
92
|
+
|
|
93
|
+
const p = (async () => {
|
|
94
|
+
const startTime = Date.now();
|
|
95
|
+
this.log.info('Waiting for client');
|
|
96
|
+
const client = await this.clientPromise;
|
|
97
|
+
this.log.info('Waiting for client to be ready');
|
|
98
|
+
await client.ready();
|
|
99
|
+
this.log.info(`Client ready in ${Date.now() - startTime}ms`);
|
|
100
|
+
})();
|
|
101
|
+
|
|
102
|
+
this.readyPromise = p;
|
|
103
|
+
p.finally(() => { this.readyPromise = null; });
|
|
104
|
+
return p;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Change the log level for this client's listener.
|
|
109
|
+
* Also re-syncs the global WASM filter to the broadest level across all clients.
|
|
110
|
+
*/
|
|
111
|
+
setLogLevel(level: LogLevel): void {
|
|
112
|
+
if (this.wasmCallback) {
|
|
113
|
+
setListenerLevel(this.wasmCallback, level);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Close the TorClient and release resources.
|
|
119
|
+
*/
|
|
120
|
+
close(): void {
|
|
121
|
+
if (this.closed) return;
|
|
122
|
+
this.closed = true;
|
|
123
|
+
this.removeLogListener?.();
|
|
124
|
+
this.removeLogListener = null;
|
|
125
|
+
this.wasmCallback = null;
|
|
126
|
+
this.socketProvider?.close();
|
|
127
|
+
this.socketProvider = null;
|
|
128
|
+
this.clientPromise.then(client => client.close()).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
[Symbol.dispose](): void {
|
|
132
|
+
this.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Common exports shared by all entry points.
|
|
2
|
+
|
|
3
|
+
export type { TorClientOptions, FetchInit, TorStorage } from './types.js';
|
|
4
|
+
export { Log, type LogLevel } from './Log.js';
|
|
5
|
+
export * as storage from './storage/index.js';
|
|
6
|
+
export { setWasmUrl } from './wasm.js';
|
|
7
|
+
export { ArtiSocketProvider, ArtiSocket, type ArtiSocketProviderOptions } from './socketProvider.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Entry point: tor-js/wasm-base64
|
|
2
|
+
// Includes WASM as a base64-encoded string. Self-contained, no external file needed.
|
|
3
|
+
|
|
4
|
+
import { setWasmSourceProvider } from '../../wasm.js';
|
|
5
|
+
import { wasmBase64 } from '#wasm-base64-data';
|
|
6
|
+
|
|
7
|
+
export { TorClient } from '../../TorClient.js';
|
|
8
|
+
export * from '../../commonExports.js';
|
|
9
|
+
|
|
10
|
+
setWasmSourceProvider(async () => {
|
|
11
|
+
const binaryString = atob(wasmBase64);
|
|
12
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
13
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
14
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
15
|
+
}
|
|
16
|
+
return bytes;
|
|
17
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Entry point: tor-js/wasm-base64/singleton
|
|
2
|
+
// Singleton with WASM as a base64-encoded string. Self-contained, no external file needed.
|
|
3
|
+
|
|
4
|
+
import './index.js'; // side effect: registers base64 WASM source provider
|
|
5
|
+
|
|
6
|
+
export { tor } from '../../singleton.js';
|
|
7
|
+
export * from '../../commonExports.js';
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Entry point: tor-js (default), tor-js/wasm-cdn
|
|
2
|
+
// Downloads WASM from CDN with SHA256 hash verification.
|
|
3
|
+
// Caches the downloaded WASM using createAutoStorage for faster subsequent loads.
|
|
4
|
+
// The GitHub CDN source stores AES-256-GCM encrypted files keyed by the WASM hash,
|
|
5
|
+
// with filenames derived from hash(hash) so the key isn't revealed by the URL.
|
|
6
|
+
|
|
7
|
+
import { setWasmSourceProvider } from '../../wasm.js';
|
|
8
|
+
import { createAutoStorage } from '../../storage/index.js';
|
|
9
|
+
|
|
10
|
+
export { TorClient } from '../../TorClient.js';
|
|
11
|
+
export * from '../../commonExports.js';
|
|
12
|
+
|
|
13
|
+
const CACHE_KEY = 'wasm';
|
|
14
|
+
|
|
15
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
16
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
17
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
18
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
19
|
+
}
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
24
|
+
if (typeof Buffer !== 'undefined') {
|
|
25
|
+
return Buffer.from(bytes).toString('base64');
|
|
26
|
+
}
|
|
27
|
+
let binary = '';
|
|
28
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
29
|
+
binary += String.fromCharCode(bytes[i]);
|
|
30
|
+
}
|
|
31
|
+
return btoa(binary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function base64ToBytes(base64: string): Uint8Array {
|
|
35
|
+
if (typeof Buffer !== 'undefined') {
|
|
36
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
37
|
+
}
|
|
38
|
+
const binary = atob(base64);
|
|
39
|
+
const bytes = new Uint8Array(binary.length);
|
|
40
|
+
for (let i = 0; i < binary.length; i++) {
|
|
41
|
+
bytes[i] = binary.charCodeAt(i);
|
|
42
|
+
}
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function sha256hex(bytes: ArrayBuffer | Uint8Array): Promise<string> {
|
|
47
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
48
|
+
const buf = bytes instanceof Uint8Array ? bytes.buffer as ArrayBuffer : bytes;
|
|
49
|
+
const hashBuf = await crypto.subtle.digest('SHA-256', buf);
|
|
50
|
+
return [...new Uint8Array(hashBuf)].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
51
|
+
}
|
|
52
|
+
// Node.js fallback
|
|
53
|
+
const { createHash } = await import('node:crypto');
|
|
54
|
+
return createHash('sha256').update(new Uint8Array(bytes)).digest('hex');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function decryptAesGcm(encrypted: ArrayBuffer, keyBytes: Uint8Array): Promise<ArrayBuffer> {
|
|
58
|
+
const iv = keyBytes.slice(0, 12);
|
|
59
|
+
|
|
60
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
61
|
+
const key = await crypto.subtle.importKey('raw', keyBytes.buffer as ArrayBuffer, 'AES-GCM', false, ['decrypt']);
|
|
62
|
+
return crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, encrypted);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Node.js fallback — crypto.subtle output is ciphertext || 16-byte auth tag
|
|
66
|
+
const { createDecipheriv } = await import('node:crypto');
|
|
67
|
+
const data = new Uint8Array(encrypted);
|
|
68
|
+
const authTag = data.slice(-16);
|
|
69
|
+
const ciphertext = data.slice(0, -16);
|
|
70
|
+
const decipher = createDecipheriv('aes-256-gcm', keyBytes, iv);
|
|
71
|
+
decipher.setAuthTag(authTag);
|
|
72
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
73
|
+
return decrypted.buffer.slice(decrypted.byteOffset, decrypted.byteOffset + decrypted.byteLength);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setWasmSourceProvider(async () => {
|
|
77
|
+
let cache: ReturnType<typeof createAutoStorage> | undefined;
|
|
78
|
+
try {
|
|
79
|
+
cache = createAutoStorage('tor-js-wasm');
|
|
80
|
+
} catch {
|
|
81
|
+
// No persistent storage available — proceed without caching
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Try loading from cache
|
|
85
|
+
if (cache) {
|
|
86
|
+
try {
|
|
87
|
+
const cached = await cache.get(CACHE_KEY);
|
|
88
|
+
if (cached) {
|
|
89
|
+
const bytes = base64ToBytes(cached);
|
|
90
|
+
const hash = await sha256hex(bytes);
|
|
91
|
+
if (hash === __WASM_SHA256__) {
|
|
92
|
+
return bytes;
|
|
93
|
+
}
|
|
94
|
+
// Hash mismatch — stale cache, delete it
|
|
95
|
+
await cache.delete(CACHE_KEY);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Cache read failed — proceed to download
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Download from CDN
|
|
103
|
+
const hashBytes = hexToBytes(__WASM_SHA256__);
|
|
104
|
+
const hashHash = await sha256hex(hashBytes);
|
|
105
|
+
const hashHashPrefix = hashHash.slice(0, 2);
|
|
106
|
+
|
|
107
|
+
const githubBase = `https://raw.githubusercontent.com/voltrevo/arti/hash-artifacts/`;
|
|
108
|
+
|
|
109
|
+
type Source = { urls: string[], encrypted: boolean };
|
|
110
|
+
|
|
111
|
+
const sources: Source[] = [
|
|
112
|
+
{ urls: [`https://cdn.jsdelivr.net/npm/tor-js@${__PACKAGE_VERSION__}/dist/tor_js_bg.wasm`], encrypted: false },
|
|
113
|
+
{ urls: [`https://unpkg.com/tor-js@${__PACKAGE_VERSION__}/dist/tor_js_bg.wasm`], encrypted: false },
|
|
114
|
+
{ urls: [`${githubBase}${hashHashPrefix}/${hashHash}`, `${githubBase}tmp/${hashHash}`], encrypted: true },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// Shuffle sources for load balancing
|
|
118
|
+
for (let i = sources.length - 1; i > 0; i--) {
|
|
119
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
120
|
+
[sources[i], sources[j]] = [sources[j], sources[i]];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const errors: string[] = [];
|
|
124
|
+
for (const source of sources) {
|
|
125
|
+
for (const url of source.urls) {
|
|
126
|
+
try {
|
|
127
|
+
const resp = await fetch(url);
|
|
128
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
129
|
+
let bytes = await resp.arrayBuffer();
|
|
130
|
+
|
|
131
|
+
if (source.encrypted) {
|
|
132
|
+
bytes = await decryptAesGcm(bytes, hashBytes);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hash = await sha256hex(bytes);
|
|
136
|
+
if (hash !== __WASM_SHA256__) {
|
|
137
|
+
throw new Error(`SHA256 mismatch: expected ${__WASM_SHA256__}, got ${hash}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const result = new Uint8Array(bytes);
|
|
141
|
+
|
|
142
|
+
// Cache for next time (fire and forget)
|
|
143
|
+
if (cache) {
|
|
144
|
+
cache.set(CACHE_KEY, bytesToBase64(result)).catch(() => {});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
errors.push(`${url}: ${err instanceof Error ? err.message : err}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(`Failed to load WASM from any CDN:\n ${errors.join('\n ')}`);
|
|
155
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Entry point: tor-js/wasm-cdn/singleton
|
|
2
|
+
// Singleton that downloads WASM from CDN with SHA256 hash verification.
|
|
3
|
+
|
|
4
|
+
import './index.js'; // side effect: registers CDN WASM source provider
|
|
5
|
+
|
|
6
|
+
export { tor } from '../../singleton.js';
|
|
7
|
+
export * from '../../commonExports.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Entry point: tor-js/wasm-file
|
|
2
|
+
// Loads WASM from a file alongside the JS module (Node.js filesystem or browser URL).
|
|
3
|
+
|
|
4
|
+
import { setWasmSourceProvider } from '../../wasm.js';
|
|
5
|
+
|
|
6
|
+
export { TorClient } from '../../TorClient.js';
|
|
7
|
+
export * from '../../commonExports.js';
|
|
8
|
+
|
|
9
|
+
setWasmSourceProvider(async () => {
|
|
10
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
11
|
+
const { readFile } = await import('node:fs/promises');
|
|
12
|
+
const { fileURLToPath } = await import('node:url');
|
|
13
|
+
const wasmPath = fileURLToPath(new URL('./tor_js_bg.wasm', import.meta.url));
|
|
14
|
+
return readFile(wasmPath);
|
|
15
|
+
}
|
|
16
|
+
const resp = await fetch(new URL('./tor_js_bg.wasm', import.meta.url));
|
|
17
|
+
if (!resp.ok) throw new Error(`Failed to fetch WASM: HTTP ${resp.status}`);
|
|
18
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
19
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Entry point: tor-js/wasm-file/singleton
|
|
2
|
+
// Singleton that loads WASM from a file alongside the JS module.
|
|
3
|
+
|
|
4
|
+
import './index.js'; // side effect: registers file WASM source provider
|
|
5
|
+
|
|
6
|
+
export { tor } from '../../singleton.js';
|
|
7
|
+
export * from '../../commonExports.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time exhaustiveness check. Accepts only `never`, so TypeScript
|
|
3
|
+
* emits an error if the value could still be a valid type (i.e., a case
|
|
4
|
+
* was not handled).
|
|
5
|
+
*/
|
|
6
|
+
export function never(value: never): never {
|
|
7
|
+
throw new Error(`Unexpected value: ${safeToString(value)}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function safeToString(value: unknown) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
} catch {}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
return `${value}`;
|
|
17
|
+
} catch {}
|
|
18
|
+
|
|
19
|
+
return '(string conversion failed)';
|
|
20
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { TorClient } from './TorClient.js';
|
|
2
|
+
import type { TorClientOptions, FetchInit } from './types.js';
|
|
3
|
+
|
|
4
|
+
let client: TorClient | undefined;
|
|
5
|
+
|
|
6
|
+
let config: TorClientOptions = {
|
|
7
|
+
gateway: 'https://tor-js-gateway.voltrevo.com',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const tor = {
|
|
11
|
+
/**
|
|
12
|
+
* Make an HTTP fetch request through Tor.
|
|
13
|
+
* Automatically opens the TorClient on first use.
|
|
14
|
+
*/
|
|
15
|
+
async fetch(url: string, init?: FetchInit): Promise<Response> {
|
|
16
|
+
if (!client) {
|
|
17
|
+
this.open();
|
|
18
|
+
}
|
|
19
|
+
return client!.fetch(url, init);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configure the singleton TorClient.
|
|
24
|
+
* If already open, closes and reopens with the new config.
|
|
25
|
+
*/
|
|
26
|
+
configure(options: TorClientOptions): void {
|
|
27
|
+
config = options;
|
|
28
|
+
if (client) {
|
|
29
|
+
client.close();
|
|
30
|
+
client = undefined;
|
|
31
|
+
this.open();
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Open the singleton TorClient.
|
|
37
|
+
* Optional — fetch() calls this automatically.
|
|
38
|
+
* Call this early if you know you'll need Tor, to start bootstrapping sooner.
|
|
39
|
+
*/
|
|
40
|
+
open(): void {
|
|
41
|
+
if (client) return;
|
|
42
|
+
client = new TorClient(config);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Close the singleton TorClient and release resources.
|
|
47
|
+
*/
|
|
48
|
+
close(): void {
|
|
49
|
+
if (client) {
|
|
50
|
+
client.close();
|
|
51
|
+
client = undefined;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|