stealth-ws 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/bin/stealth-bridge.exe +0 -0
- package/lib/cookie-jar.js +276 -0
- package/lib/fingerprint.js +208 -0
- package/lib/index.d.ts +74 -0
- package/lib/index.js +29 -0
- package/lib/websocket.js +416 -0
- package/package.json +58 -0
- package/prebuilds/darwin-x64/stealth-bridge +0 -0
- package/prebuilds/linux-x64/stealth-bridge +0 -0
- package/prebuilds/win32-x64/stealth-bridge.exe +0 -0
- package/scripts/build-all.js +102 -0
- package/scripts/build-go.js +134 -0
- package/scripts/download-binary.js +50 -0
package/lib/websocket.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket class with TLS fingerprint spoofing
|
|
3
|
+
*
|
|
4
|
+
* Compatible with the 'ws' package API but adds:
|
|
5
|
+
* - TLS fingerprint spoofing via Go bridge
|
|
6
|
+
* - Cookie injection for Cloudflare bypass
|
|
7
|
+
* - Proxy support (SOCKS5, HTTP)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// Default bridge binary location
|
|
19
|
+
const DEFAULT_BRIDGE_DIR = join(__dirname, '..', 'bin');
|
|
20
|
+
|
|
21
|
+
export class WebSocket extends EventEmitter {
|
|
22
|
+
// Ready state constants (RFC 6455)
|
|
23
|
+
static CONNECTING = 0;
|
|
24
|
+
static OPEN = 1;
|
|
25
|
+
static CLOSING = 2;
|
|
26
|
+
static CLOSED = 3;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new WebSocket connection
|
|
30
|
+
*
|
|
31
|
+
* @param {string} url - WebSocket URL
|
|
32
|
+
* @param {Object} options - Connection options
|
|
33
|
+
* @param {string} [options.fingerprint='chrome120'] - TLS fingerprint profile
|
|
34
|
+
* @param {string|Array|Object} [options.cookies] - Cookies to send
|
|
35
|
+
* @param {string} [options.proxy] - Proxy URL (socks5:// or http://)
|
|
36
|
+
* @param {boolean} [options.perMessageDeflate=true] - Enable compression
|
|
37
|
+
* @param {Object} [options.headers] - Additional HTTP headers
|
|
38
|
+
*/
|
|
39
|
+
constructor(url, options = {}) {
|
|
40
|
+
super();
|
|
41
|
+
|
|
42
|
+
this.url = url;
|
|
43
|
+
this.options = options;
|
|
44
|
+
|
|
45
|
+
// Connection state
|
|
46
|
+
this.readyState = WebSocket.CONNECTING;
|
|
47
|
+
this.binaryType = 'nodebuffer';
|
|
48
|
+
this.protocol = '';
|
|
49
|
+
this.extensions = '';
|
|
50
|
+
this.bufferedAmount = 0;
|
|
51
|
+
|
|
52
|
+
// Bridge process
|
|
53
|
+
this._bridge = null;
|
|
54
|
+
this._buffer = '';
|
|
55
|
+
this._closed = false;
|
|
56
|
+
|
|
57
|
+
// Connect asynchronously
|
|
58
|
+
setImmediate(() => this._connect());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Establish connection via the Go bridge
|
|
63
|
+
*/
|
|
64
|
+
async _connect() {
|
|
65
|
+
const bridgePath = this._getBridgePath();
|
|
66
|
+
|
|
67
|
+
// Check if bridge exists, if not try to use prebuild
|
|
68
|
+
let binaryPath = bridgePath;
|
|
69
|
+
if (!existsSync(bridgePath)) {
|
|
70
|
+
binaryPath = this._getPrebuildPath();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If no binary found, emit error
|
|
74
|
+
if (!existsSync(binaryPath)) {
|
|
75
|
+
this._handleError(new Error(
|
|
76
|
+
`Stealth bridge not found. Please run: npm run postinstall\n` +
|
|
77
|
+
`Tried paths:\n - ${bridgePath}\n - ${binaryPath}`
|
|
78
|
+
));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
this._bridge = spawn(binaryPath, [], {
|
|
84
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
85
|
+
windowsHide: true
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Handle stdin errors (e.g., EPIPE when process exits)
|
|
89
|
+
this._bridge.stdin.on('error', (err) => {
|
|
90
|
+
if (err.code !== 'EPIPE' && !this._closed) {
|
|
91
|
+
this._handleError(err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Collect and parse messages from bridge
|
|
96
|
+
this._bridge.stdout.on('data', (chunk) => {
|
|
97
|
+
this._buffer += chunk.toString();
|
|
98
|
+
this._processBuffer();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this._bridge.on('exit', (code) => {
|
|
102
|
+
if (!this._closed) {
|
|
103
|
+
this.readyState = WebSocket.CLOSED;
|
|
104
|
+
this.emit('close', 1006, '');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this._bridge.on('error', (err) => {
|
|
109
|
+
this._handleError(err);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Send configuration to bridge
|
|
113
|
+
this._sendConfig();
|
|
114
|
+
|
|
115
|
+
} catch (err) {
|
|
116
|
+
this._handleError(err);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Send configuration to the bridge
|
|
122
|
+
*/
|
|
123
|
+
_sendConfig() {
|
|
124
|
+
const config = {
|
|
125
|
+
type: 'connect',
|
|
126
|
+
url: this.url,
|
|
127
|
+
fingerprint: this.options.fingerprint || 'chrome120',
|
|
128
|
+
cookies: this._formatCookies(this.options.cookies),
|
|
129
|
+
proxy: this.options.proxy,
|
|
130
|
+
headers: this.options.headers || {},
|
|
131
|
+
perMessageDeflate: this.options.perMessageDeflate !== false
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
this._bridge.stdin.write(JSON.stringify(config) + '\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Process received data from bridge
|
|
139
|
+
*/
|
|
140
|
+
_processBuffer() {
|
|
141
|
+
const lines = this._buffer.split('\n');
|
|
142
|
+
this._buffer = lines.pop();
|
|
143
|
+
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (!line.trim()) continue;
|
|
146
|
+
try {
|
|
147
|
+
const msg = JSON.parse(line);
|
|
148
|
+
this._handleMessage(msg);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
// Ignore malformed messages
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handle messages from the bridge
|
|
157
|
+
*/
|
|
158
|
+
_handleMessage(msg) {
|
|
159
|
+
switch (msg.type) {
|
|
160
|
+
case 'open':
|
|
161
|
+
this.readyState = WebSocket.OPEN;
|
|
162
|
+
this.protocol = msg.protocol || '';
|
|
163
|
+
this.extensions = msg.extensions || '';
|
|
164
|
+
this.emit('open');
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'message':
|
|
168
|
+
this.emit('message', Buffer.from(msg.data), false);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case 'binary':
|
|
172
|
+
this.emit('message', Buffer.from(msg.data), true);
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case 'ping':
|
|
176
|
+
// Auto-pong for ping messages
|
|
177
|
+
if (this.readyState === WebSocket.OPEN) {
|
|
178
|
+
this.pong();
|
|
179
|
+
}
|
|
180
|
+
this.emit('ping', msg.data ? Buffer.from(msg.data) : undefined);
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case 'close':
|
|
184
|
+
this.readyState = WebSocket.CLOSED;
|
|
185
|
+
this.emit('close', msg.code || 1000, msg.reason || '');
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case 'auth_required':
|
|
189
|
+
this.emit('auth_required');
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
case 'error':
|
|
193
|
+
this._handleError(new Error(msg.message));
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case 'debug':
|
|
197
|
+
// Debug messages (can be logged if DEBUG enabled)
|
|
198
|
+
if (this.options.debug) {
|
|
199
|
+
console.log('[stealth-ws:debug]', msg.message);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Handle errors
|
|
207
|
+
*/
|
|
208
|
+
_handleError(err) {
|
|
209
|
+
if (this.readyState === WebSocket.CLOSED) return;
|
|
210
|
+
|
|
211
|
+
this.readyState = WebSocket.CLOSED;
|
|
212
|
+
this.emit('error', err);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Send data through the WebSocket
|
|
217
|
+
*
|
|
218
|
+
* @param {string|Buffer} data - Data to send
|
|
219
|
+
* @param {Object} options - Send options
|
|
220
|
+
* @param {Function} callback - Callback when sent
|
|
221
|
+
*/
|
|
222
|
+
send(data, options = {}, callback) {
|
|
223
|
+
if (this.readyState !== WebSocket.OPEN) {
|
|
224
|
+
const err = new Error('WebSocket is not open');
|
|
225
|
+
if (callback) callback(err);
|
|
226
|
+
else this.emit('error', err);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const msg = {
|
|
231
|
+
type: 'send',
|
|
232
|
+
data: Buffer.isBuffer(data) ? data.toString('base64') : String(data),
|
|
233
|
+
binary: options.binary ?? Buffer.isBuffer(data)
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
this._bridge.stdin.write(JSON.stringify(msg) + '\n');
|
|
238
|
+
if (callback) callback();
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (callback) callback(err);
|
|
241
|
+
else this.emit('error', err);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Send a ping frame
|
|
247
|
+
*/
|
|
248
|
+
ping(data, mask, callback) {
|
|
249
|
+
if (this.readyState !== WebSocket.OPEN) return;
|
|
250
|
+
|
|
251
|
+
const msg = {
|
|
252
|
+
type: 'ping',
|
|
253
|
+
data: data ? (Buffer.isBuffer(data) ? data.toString('base64') : String(data)) : undefined
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
this._bridge.stdin.write(JSON.stringify(msg) + '\n');
|
|
258
|
+
if (callback) callback();
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (callback) callback(err);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Send a pong frame
|
|
266
|
+
*/
|
|
267
|
+
pong(data, mask, callback) {
|
|
268
|
+
if (this.readyState !== WebSocket.OPEN) return;
|
|
269
|
+
|
|
270
|
+
const msg = {
|
|
271
|
+
type: 'pong',
|
|
272
|
+
data: data ? (Buffer.isBuffer(data) ? data.toString('base64') : String(data)) : undefined
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
this._bridge.stdin.write(JSON.stringify(msg) + '\n');
|
|
277
|
+
if (callback) callback();
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (callback) callback(err);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Close the WebSocket connection
|
|
285
|
+
*
|
|
286
|
+
* @param {number} code - Close code
|
|
287
|
+
* @param {string} reason - Close reason
|
|
288
|
+
*/
|
|
289
|
+
close(code = 1000, reason = '') {
|
|
290
|
+
if (this.readyState === WebSocket.CLOSED) return;
|
|
291
|
+
if (this.readyState === WebSocket.CLOSING) return;
|
|
292
|
+
|
|
293
|
+
this.readyState = WebSocket.CLOSING;
|
|
294
|
+
this._closed = true;
|
|
295
|
+
|
|
296
|
+
const msg = {
|
|
297
|
+
type: 'close',
|
|
298
|
+
code,
|
|
299
|
+
reason: String(reason)
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
this._bridge.stdin.write(JSON.stringify(msg) + '\n');
|
|
304
|
+
} catch (err) {
|
|
305
|
+
// Ignore errors during close
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Forcibly terminate the connection
|
|
311
|
+
*/
|
|
312
|
+
terminate() {
|
|
313
|
+
this._closed = true;
|
|
314
|
+
this.readyState = WebSocket.CLOSED;
|
|
315
|
+
|
|
316
|
+
if (this._bridge) {
|
|
317
|
+
try {
|
|
318
|
+
this._bridge.kill();
|
|
319
|
+
} catch (err) {
|
|
320
|
+
// Ignore kill errors
|
|
321
|
+
}
|
|
322
|
+
this._bridge = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Pause the connection
|
|
328
|
+
*/
|
|
329
|
+
pause() {
|
|
330
|
+
// Not implemented in bridge mode
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Resume the connection
|
|
335
|
+
*/
|
|
336
|
+
resume() {
|
|
337
|
+
// Not implemented in bridge mode
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Format cookies for bridge
|
|
342
|
+
*/
|
|
343
|
+
_formatCookies(cookies) {
|
|
344
|
+
if (!cookies) return '';
|
|
345
|
+
if (typeof cookies === 'string') return cookies;
|
|
346
|
+
if (Array.isArray(cookies)) {
|
|
347
|
+
return cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
348
|
+
}
|
|
349
|
+
if (typeof cookies === 'object' && cookies.getCookieString) {
|
|
350
|
+
return cookies.getCookieString(this.url);
|
|
351
|
+
}
|
|
352
|
+
return String(cookies);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get bridge binary path for current platform
|
|
357
|
+
*/
|
|
358
|
+
_getBridgePath() {
|
|
359
|
+
return join(DEFAULT_BRIDGE_DIR, this._getBridgeName());
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get prebuild path for current platform
|
|
364
|
+
*/
|
|
365
|
+
_getPrebuildPath() {
|
|
366
|
+
const platform = process.platform;
|
|
367
|
+
const arch = process.arch;
|
|
368
|
+
|
|
369
|
+
const platformMap = {
|
|
370
|
+
'win32': 'win32',
|
|
371
|
+
'linux': 'linux',
|
|
372
|
+
'darwin': 'darwin'
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const archMap = {
|
|
376
|
+
'x64': 'x64',
|
|
377
|
+
'arm64': 'arm64'
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const p = platformMap[platform] || platform;
|
|
381
|
+
const a = archMap[arch] || arch;
|
|
382
|
+
|
|
383
|
+
const ext = platform === 'win32' ? '.exe' : '';
|
|
384
|
+
return join(__dirname, '..', 'prebuilds', `${p}-${a}`, `stealth-bridge${ext}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Get bridge binary name for current platform
|
|
389
|
+
*/
|
|
390
|
+
_getBridgeName() {
|
|
391
|
+
if (process.platform === 'win32') {
|
|
392
|
+
return 'stealth-bridge.exe';
|
|
393
|
+
}
|
|
394
|
+
return 'stealth-bridge';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Add EventTarget compatibility
|
|
399
|
+
WebSocket.prototype.addEventListener = function(type, listener, options) {
|
|
400
|
+
if (options && options.once) {
|
|
401
|
+
this.on(type, function wrapper(...args) {
|
|
402
|
+
this.removeListener(type, wrapper);
|
|
403
|
+
listener.apply(this, args);
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
this.on(type, listener);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
WebSocket.prototype.removeEventListener = function(type, listener) {
|
|
411
|
+
this.removeListener(type, listener);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
WebSocket.prototype.dispatchEvent = function(event) {
|
|
415
|
+
this.emit(event.type, event);
|
|
416
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stealth-ws",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WebSocket client with TLS fingerprint spoofing",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test test/*.test.js",
|
|
13
|
+
"postinstall": "node scripts/download-binary.js",
|
|
14
|
+
"build:go": "node scripts/build-go.js",
|
|
15
|
+
"prebuild": "node scripts/build-all.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"websocket",
|
|
19
|
+
"stealth",
|
|
20
|
+
"fingerprint",
|
|
21
|
+
"tls",
|
|
22
|
+
"utls",
|
|
23
|
+
"proxy",
|
|
24
|
+
"browser-emulation"
|
|
25
|
+
],
|
|
26
|
+
"author": "safra36",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/yourusername/stealth-ws.git"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {},
|
|
33
|
+
"devDependencies": {},
|
|
34
|
+
"optionalDependencies": {
|
|
35
|
+
"@stealth-ws/linux-x64": "1.0.0",
|
|
36
|
+
"@stealth-ws/darwin-x64": "1.0.0",
|
|
37
|
+
"@stealth-ws/win32-x64": "1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"os": [
|
|
43
|
+
"linux",
|
|
44
|
+
"darwin",
|
|
45
|
+
"win32"
|
|
46
|
+
],
|
|
47
|
+
"cpu": [
|
|
48
|
+
"x64"
|
|
49
|
+
],
|
|
50
|
+
"files": [
|
|
51
|
+
"lib/",
|
|
52
|
+
"bin/",
|
|
53
|
+
"prebuilds/",
|
|
54
|
+
"scripts/",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build script for all supported platform binaries
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const ROOT_DIR = join(__dirname, '..');
|
|
14
|
+
const PREBUILTS_DIR = join(ROOT_DIR, 'prebuilds');
|
|
15
|
+
|
|
16
|
+
// Supported x64 platforms only (ARM builds not supported - build from source with: npm run build:go)
|
|
17
|
+
const PLATFORMS = [
|
|
18
|
+
{ name: 'win32-x64', os: 'windows', arch: 'amd64', ext: '.exe' },
|
|
19
|
+
{ name: 'linux-x64', os: 'linux', arch: 'amd64', ext: '' },
|
|
20
|
+
{ name: 'darwin-x64', os: 'darwin', arch: 'amd64', ext: '' }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
console.log('Note: ARM builds not supported (build from source with: npm run build:go)\n');
|
|
24
|
+
|
|
25
|
+
function runCommand(cmd, args, options = {}) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const proc = spawn(cmd, args, {
|
|
28
|
+
...options,
|
|
29
|
+
stdio: 'inherit'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
proc.on('close', (code) => {
|
|
33
|
+
if (code === 0) {
|
|
34
|
+
resolve();
|
|
35
|
+
} else {
|
|
36
|
+
reject(new Error(`Command failed with exit code ${code}`));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
proc.on('error', reject);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function buildForPlatform(platform) {
|
|
45
|
+
const outputName = `stealth-bridge${platform.ext}`;
|
|
46
|
+
const outputDir = join(PREBUILTS_DIR, platform.name);
|
|
47
|
+
const outputPath = join(outputDir, outputName);
|
|
48
|
+
|
|
49
|
+
console.log(`Building for ${platform.name}...`);
|
|
50
|
+
|
|
51
|
+
if (!existsSync(outputDir)) {
|
|
52
|
+
mkdirSync(outputDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const goCmd = process.platform === 'win32' ? 'go.exe' : 'go';
|
|
56
|
+
const srcDir = join(ROOT_DIR, 'src', 'bridge');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await runCommand(goCmd, [
|
|
60
|
+
'build',
|
|
61
|
+
'-o', outputPath,
|
|
62
|
+
'-ldflags', '-s -w',
|
|
63
|
+
srcDir
|
|
64
|
+
], {
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
GOOS: platform.os,
|
|
68
|
+
GOARCH: platform.arch,
|
|
69
|
+
CGO_ENABLED: '0'
|
|
70
|
+
},
|
|
71
|
+
cwd: srcDir
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(`✓ Built: ${outputPath}`);
|
|
75
|
+
return true;
|
|
76
|
+
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(`✗ Build failed for ${platform.name}:`, err.message);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function buildAll() {
|
|
84
|
+
console.log('Building stealth-bridge binaries for all platforms...\n');
|
|
85
|
+
|
|
86
|
+
let successCount = 0;
|
|
87
|
+
for (const platform of PLATFORMS) {
|
|
88
|
+
const success = await buildForPlatform(platform);
|
|
89
|
+
if (success) successCount++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`\nBuild complete: ${successCount}/${PLATFORMS.length} platforms succeeded`);
|
|
93
|
+
|
|
94
|
+
if (successCount === PLATFORMS.length) {
|
|
95
|
+
console.log('\nAll binaries built successfully!');
|
|
96
|
+
} else {
|
|
97
|
+
console.log('\nSome builds failed. Check errors above.');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
buildAll().catch(console.error);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build script for the Go bridge
|
|
3
|
+
*
|
|
4
|
+
* Builds the stealth-bridge binary for the current platform.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const ROOT_DIR = join(__dirname, '..');
|
|
14
|
+
const SRC_DIR = join(ROOT_DIR, 'src', 'bridge');
|
|
15
|
+
const BIN_DIR = join(ROOT_DIR, 'bin');
|
|
16
|
+
|
|
17
|
+
// Platform configurations
|
|
18
|
+
const PLATFORMS = {
|
|
19
|
+
'win32': { os: 'windows', arch: 'amd64', ext: '.exe' },
|
|
20
|
+
'win32-arm64': { os: 'windows', arch: 'arm64', ext: '.exe' },
|
|
21
|
+
'linux': { os: 'linux', arch: 'amd64', ext: '' },
|
|
22
|
+
'linux-arm64': { os: 'linux', arch: 'arm64', ext: '' },
|
|
23
|
+
'darwin': { os: 'darwin', arch: 'amd64', ext: '' },
|
|
24
|
+
'darwin-arm64': { os: 'darwin', arch: 'arm64', ext: '' }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function getPlatform() {
|
|
28
|
+
const platform = process.platform;
|
|
29
|
+
const arch = process.arch;
|
|
30
|
+
|
|
31
|
+
if (platform === 'win32') {
|
|
32
|
+
return arch === 'arm64' ? 'win32-arm64' : 'win32';
|
|
33
|
+
}
|
|
34
|
+
if (platform === 'linux') {
|
|
35
|
+
return arch === 'arm64' ? 'linux-arm64' : 'linux';
|
|
36
|
+
}
|
|
37
|
+
if (platform === 'darwin') {
|
|
38
|
+
return arch === 'arm64' ? 'darwin-arm64' : 'darwin';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runCommand(cmd, args, options = {}) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const proc = spawn(cmd, args, {
|
|
47
|
+
...options,
|
|
48
|
+
stdio: 'inherit'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
proc.on('close', (code) => {
|
|
52
|
+
if (code === 0) {
|
|
53
|
+
resolve();
|
|
54
|
+
} else {
|
|
55
|
+
reject(new Error(`Command failed with exit code ${code}`));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
proc.on('error', reject);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function buildForPlatform(platform) {
|
|
64
|
+
const config = PLATFORMS[platform];
|
|
65
|
+
if (!config) {
|
|
66
|
+
console.log(`Skipping unsupported platform: ${platform}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const outputName = `stealth-bridge${config.ext}`;
|
|
71
|
+
const outputPath = join(BIN_DIR, outputName);
|
|
72
|
+
|
|
73
|
+
console.log(`Building for ${platform}...`);
|
|
74
|
+
|
|
75
|
+
// Ensure bin directory exists
|
|
76
|
+
if (!existsSync(BIN_DIR)) {
|
|
77
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build command - detect Go command
|
|
81
|
+
const goCmd = process.platform === 'win32' ? 'go.exe' : 'go';
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Step 1: Download dependencies
|
|
85
|
+
console.log('Downloading Go dependencies...');
|
|
86
|
+
await runCommand(goCmd, ['mod', 'download'], { cwd: SRC_DIR });
|
|
87
|
+
|
|
88
|
+
// Step 2: Tidy modules
|
|
89
|
+
console.log('Tidying Go modules...');
|
|
90
|
+
await runCommand(goCmd, ['mod', 'tidy'], { cwd: SRC_DIR });
|
|
91
|
+
|
|
92
|
+
// Step 3: Build binary
|
|
93
|
+
console.log('Building binary...');
|
|
94
|
+
await runCommand(goCmd, [
|
|
95
|
+
'build',
|
|
96
|
+
'-o', outputPath,
|
|
97
|
+
'-ldflags', '-s -w',
|
|
98
|
+
SRC_DIR
|
|
99
|
+
], {
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
GOOS: config.os,
|
|
103
|
+
GOARCH: config.arch,
|
|
104
|
+
CGO_ENABLED: '0'
|
|
105
|
+
},
|
|
106
|
+
cwd: SRC_DIR
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.log(`✓ Built: ${outputPath}`);
|
|
110
|
+
return true;
|
|
111
|
+
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`✗ Build failed for ${platform}:`, err.message);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function buildAll() {
|
|
119
|
+
console.log('Building stealth-bridge binaries...\n');
|
|
120
|
+
|
|
121
|
+
// Build for current platform
|
|
122
|
+
const platform = getPlatform();
|
|
123
|
+
if (platform) {
|
|
124
|
+
await buildForPlatform(platform);
|
|
125
|
+
} else {
|
|
126
|
+
console.error('Unsupported platform');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Run if called directly
|
|
132
|
+
buildAll().catch(console.error);
|
|
133
|
+
|
|
134
|
+
export { buildForPlatform, buildAll };
|