@empir3/empir3-bridge 0.3.21
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/CHANGELOG.md +1531 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +464 -0
- package/SECURITY.md +130 -0
- package/assets/accuracy-lab.html +2639 -0
- package/assets/api-clis-real.jpg +0 -0
- package/assets/bridge-console-hero.jpg +0 -0
- package/assets/browser-privacy.svg +151 -0
- package/assets/demo-orchestration.svg +74 -0
- package/assets/desktop-select-region.jpg +0 -0
- package/assets/in-page-chat.gif +0 -0
- package/assets/orchestration-hero.svg +126 -0
- package/assets/social-preview.png +0 -0
- package/assets/zara-accent.png +0 -0
- package/build/bootstrap.js +548 -0
- package/build/build.js +680 -0
- package/build/payload-entry.js +649 -0
- package/build/payload-signing-pub.json +7 -0
- package/docs/AGENT_GUIDE.md +259 -0
- package/docs/RELEASE.md +106 -0
- package/docs/SAFETY.md +112 -0
- package/docs/TESTING.md +181 -0
- package/installer/server.js +231 -0
- package/installer/ui/app.js +278 -0
- package/installer/ui/index.html +24 -0
- package/installer/ui/styles.css +146 -0
- package/package.json +95 -0
- package/scripts/bootstrap-e2e.mjs +650 -0
- package/scripts/certify-bridge.mjs +636 -0
- package/scripts/check-companion-surface.mjs +118 -0
- package/scripts/extract-welcome.mjs +64 -0
- package/scripts/gh-route-handler-check.mjs +57 -0
- package/scripts/gh-wire-test.mjs +107 -0
- package/scripts/publish-downloads.mjs +180 -0
- package/scripts/smoke-all-tools.mjs +509 -0
- package/scripts/smoke-live-bridge.mjs +696 -0
- package/scripts/splice-welcome.mjs +63 -0
- package/scripts/welcome-body.txt +2733 -0
- package/src/anthropic-client.ts +192 -0
- package/src/bootstrap-exe.ts +69 -0
- package/src/bridge.ts +2444 -0
- package/src/chat.ts +345 -0
- package/src/cli-runner.ts +239 -0
- package/src/cli.ts +649 -0
- package/src/config.ts +199 -0
- package/src/desktop-overlay.ps1 +121 -0
- package/src/executable-resolver.ts +330 -0
- package/src/handlers/agy-imagegen.ts +179 -0
- package/src/handlers/github-cli.ts +399 -0
- package/src/handlers/higgsfield-cli.ts +783 -0
- package/src/launch.js +337 -0
- package/src/mcp-server.ts +1265 -0
- package/src/pair-claim.ts +218 -0
- package/src/payload-daemon.ts +168 -0
- package/src/server.ts +21036 -0
- package/src/tool-defaults.ts +230 -0
- package/src/update-check.js +136 -0
- package/tray/build.py +76 -0
- package/tray/requirements.txt +2 -0
- package/tray/tray.py +1843 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pair-claim.ts — redeem a PRE-AUTHORIZED Empir3 pairing code on first run.
|
|
3
|
+
*
|
|
4
|
+
* The standard pairing flow (server.ts `startPairPoll`) has the BRIDGE create a
|
|
5
|
+
* session and the user approve it in a browser at `/connect-bridge?code=`. This
|
|
6
|
+
* module is the inverse, used by the install one-liner:
|
|
7
|
+
*
|
|
8
|
+
* Empir3Setup.exe --pair <code>
|
|
9
|
+
*
|
|
10
|
+
* In that flow the user is ALREADY logged into Empir3 (they got the install link
|
|
11
|
+
* from Vincent in chat), so Empir3 pre-authorizes a pairing session for them and
|
|
12
|
+
* bakes the `code` into the command. The bridge just CLAIMS it on first boot —
|
|
13
|
+
* no second login, no browser round-trip.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately self-contained:
|
|
16
|
+
* - It runs in the bootstrapper / first-run context BEFORE the daemon
|
|
17
|
+
* (server.ts) is listening. Importing server.ts would boot the whole bridge
|
|
18
|
+
* and bind ports, so this module re-implements the small slice it needs.
|
|
19
|
+
* - It writes `bridge-auth.json` in the EXACT shape and location server.ts
|
|
20
|
+
* reads (see server.ts `saveBridgeAuth` / `AUTH_FILE` / `BridgeAuth`). Keep
|
|
21
|
+
* these in sync if the auth schema changes.
|
|
22
|
+
*
|
|
23
|
+
* Contract with Empir3 (must match `startPairPoll` in server.ts):
|
|
24
|
+
* GET <server>/api/auth/pairing-sessions/<code>
|
|
25
|
+
* 200 { status: 'pending' } → keep polling
|
|
26
|
+
* 200 { status: 'claimed', token, userId, email,
|
|
27
|
+
* name, role, channelId, serverUrl, wsUrl } → write auth, done
|
|
28
|
+
* 404 → expired / unknown code
|
|
29
|
+
*
|
|
30
|
+
* Never hangs the install: bounded poll, then a graceful give-up so first-run
|
|
31
|
+
* falls through to the normal (interactive) pairing path.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
35
|
+
import { homedir } from 'os';
|
|
36
|
+
import { join } from 'path';
|
|
37
|
+
import { request as httpRequest } from 'http';
|
|
38
|
+
import { request as httpsRequest } from 'https';
|
|
39
|
+
|
|
40
|
+
const DEFAULT_EMPIR3_SERVER = 'https://app.empir3.com';
|
|
41
|
+
const LOCAL_DEV_EMPIR3_SERVER = 'http://localhost:3005';
|
|
42
|
+
|
|
43
|
+
// Mirror server.ts: %APPDATA%\Empir3 on Windows, ~/.empir3/Empir3 elsewhere.
|
|
44
|
+
const SETTINGS_DIR = join(process.env.APPDATA || join(homedir(), '.empir3'), 'Empir3');
|
|
45
|
+
const AUTH_FILE = join(SETTINGS_DIR, 'bridge-auth.json');
|
|
46
|
+
|
|
47
|
+
export interface ClaimResult {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
status: 'claimed' | 'expired' | 'timed_out' | 'invalid' | 'error';
|
|
50
|
+
reason?: string;
|
|
51
|
+
user?: { id?: string; email?: string };
|
|
52
|
+
authFile?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type ClaimOptions = {
|
|
56
|
+
serverUrl?: string;
|
|
57
|
+
tries?: number;
|
|
58
|
+
intervalMs?: number;
|
|
59
|
+
log?: (msg: string) => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ─── server-url helpers (minimal mirror of server.ts) ───────────────────────
|
|
63
|
+
|
|
64
|
+
function normalizeServer(input?: string | null): string {
|
|
65
|
+
const raw = String(input || '').trim();
|
|
66
|
+
if (!raw) return DEFAULT_EMPIR3_SERVER;
|
|
67
|
+
const withProtocol = /^https?:\/\//i.test(raw)
|
|
68
|
+
? raw
|
|
69
|
+
: (/^(localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(raw) ? `http://${raw}` : `https://${raw}`);
|
|
70
|
+
try {
|
|
71
|
+
const u = new URL(withProtocol);
|
|
72
|
+
u.pathname = u.pathname.replace(/\/+$/, '');
|
|
73
|
+
if (u.pathname === '/') u.pathname = '';
|
|
74
|
+
u.search = '';
|
|
75
|
+
u.hash = '';
|
|
76
|
+
return u.toString().replace(/\/+$/, '');
|
|
77
|
+
} catch {
|
|
78
|
+
return DEFAULT_EMPIR3_SERVER;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function classifyServer(serverUrl?: string | null): 'production' | 'local-dev' | 'custom' {
|
|
83
|
+
const normalized = normalizeServer(serverUrl);
|
|
84
|
+
let host = '';
|
|
85
|
+
try { host = new URL(normalized).host.toLowerCase(); } catch { /* keep '' */ }
|
|
86
|
+
if (normalized === DEFAULT_EMPIR3_SERVER || host === 'app.empir3.com') return 'production';
|
|
87
|
+
if (normalized === LOCAL_DEV_EMPIR3_SERVER || host === 'localhost:3005' || host === '127.0.0.1:3005') return 'local-dev';
|
|
88
|
+
return 'custom';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function defaultWsUrl(serverUrl: string): string {
|
|
92
|
+
try {
|
|
93
|
+
const u = new URL(normalizeServer(serverUrl));
|
|
94
|
+
u.protocol = u.protocol === 'http:' ? 'ws:' : 'wss:';
|
|
95
|
+
u.pathname = '/ws';
|
|
96
|
+
u.search = '';
|
|
97
|
+
u.hash = '';
|
|
98
|
+
return u.toString();
|
|
99
|
+
} catch {
|
|
100
|
+
return 'wss://app.empir3.com/ws';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeWsUrl(wsUrl: string | undefined | null, serverUrl: string): string {
|
|
105
|
+
const fallback = defaultWsUrl(serverUrl);
|
|
106
|
+
if (!wsUrl) return fallback;
|
|
107
|
+
try {
|
|
108
|
+
const u = new URL(wsUrl);
|
|
109
|
+
if (u.pathname.replace(/\/+$/, '') === '/relay') return fallback;
|
|
110
|
+
return u.toString();
|
|
111
|
+
} catch {
|
|
112
|
+
return fallback;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getJson(urlStr: string, timeoutMs: number): Promise<{ status: number; body: any }> {
|
|
117
|
+
return new Promise((resolvePromise, reject) => {
|
|
118
|
+
let u: URL;
|
|
119
|
+
try { u = new URL(urlStr); } catch (e) { reject(e); return; }
|
|
120
|
+
const lib = u.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
121
|
+
const req = lib({
|
|
122
|
+
hostname: u.hostname,
|
|
123
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
124
|
+
path: u.pathname + (u.search || ''),
|
|
125
|
+
method: 'GET',
|
|
126
|
+
headers: { 'User-Agent': 'empir3-bridge-pair', Accept: 'application/json' },
|
|
127
|
+
}, (response) => {
|
|
128
|
+
let chunks = '';
|
|
129
|
+
response.on('data', (c) => { chunks += c; });
|
|
130
|
+
response.on('end', () => {
|
|
131
|
+
let parsed: any = null;
|
|
132
|
+
try { parsed = JSON.parse(chunks); } catch { /* leave null */ }
|
|
133
|
+
resolvePromise({ status: response.statusCode || 0, body: parsed });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
req.on('error', reject);
|
|
137
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error('request timed out')));
|
|
138
|
+
req.end();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
143
|
+
|
|
144
|
+
/** A pairing code is opaque but should look sane before we put it in a URL. */
|
|
145
|
+
function looksLikeCode(code: string): boolean {
|
|
146
|
+
return /^[A-Za-z0-9._-]{6,128}$/.test(code);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Claim a pre-authorized pairing code and persist bridge-auth.json.
|
|
151
|
+
*
|
|
152
|
+
* Bounded: at most `tries` polls (default 10) spaced `intervalMs` (default
|
|
153
|
+
* 1500ms) apart — ~15s worst case. A pre-authorized session normally returns
|
|
154
|
+
* `claimed` on the first poll; the loop only exists to absorb a brief
|
|
155
|
+
* server-side propagation delay. Returns a structured result; never throws for
|
|
156
|
+
* an expected outcome (expired / timeout / bad code).
|
|
157
|
+
*/
|
|
158
|
+
export async function claimPairingCode(code: string, opts: ClaimOptions = {}): Promise<ClaimResult> {
|
|
159
|
+
const log = opts.log || (() => {});
|
|
160
|
+
const trimmed = String(code || '').trim();
|
|
161
|
+
if (!looksLikeCode(trimmed)) {
|
|
162
|
+
return { ok: false, status: 'invalid', reason: 'pairing code missing or malformed' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const serverUrl = normalizeServer(opts.serverUrl || process.env.EMPIR3_SERVER || DEFAULT_EMPIR3_SERVER);
|
|
166
|
+
const tries = Math.max(1, opts.tries ?? 10);
|
|
167
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 1500);
|
|
168
|
+
const sessionUrl = `${serverUrl}/api/auth/pairing-sessions/${encodeURIComponent(trimmed)}`;
|
|
169
|
+
|
|
170
|
+
let lastReason = 'no response';
|
|
171
|
+
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
172
|
+
try {
|
|
173
|
+
const r = await getJson(sessionUrl, 5000);
|
|
174
|
+
|
|
175
|
+
if (r.status === 404) {
|
|
176
|
+
log(`pairing code expired or unknown (404) after ${attempt} attempt(s)`);
|
|
177
|
+
return { ok: false, status: 'expired', reason: 'code expired or unknown' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const status = r.body?.status;
|
|
181
|
+
|
|
182
|
+
if (status === 'claimed' && r.body?.token) {
|
|
183
|
+
const sUrl = normalizeServer(r.body.serverUrl || serverUrl);
|
|
184
|
+
const auth = {
|
|
185
|
+
legacyToken: r.body.token as string,
|
|
186
|
+
user: {
|
|
187
|
+
id: r.body.userId,
|
|
188
|
+
email: r.body.email,
|
|
189
|
+
name: r.body.name,
|
|
190
|
+
role: r.body.role,
|
|
191
|
+
},
|
|
192
|
+
channelId: r.body.channelId || null,
|
|
193
|
+
serverUrl: sUrl,
|
|
194
|
+
wsUrl: normalizeWsUrl(r.body.wsUrl || r.body.relayUrl, sUrl),
|
|
195
|
+
environment: classifyServer(sUrl),
|
|
196
|
+
};
|
|
197
|
+
mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
198
|
+
writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
|
|
199
|
+
log(`paired as ${auth.user.email || auth.user.id || 'unknown user'} → ${AUTH_FILE}`);
|
|
200
|
+
return { ok: true, status: 'claimed', user: { id: auth.user.id, email: auth.user.email }, authFile: AUTH_FILE };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (status === 'pending') {
|
|
204
|
+
lastReason = 'session still pending authorization';
|
|
205
|
+
} else {
|
|
206
|
+
lastReason = r.body?.error || `unexpected response (HTTP ${r.status}${status ? `, status=${status}` : ''})`;
|
|
207
|
+
}
|
|
208
|
+
log(`attempt ${attempt}/${tries}: ${lastReason}`);
|
|
209
|
+
} catch (e: any) {
|
|
210
|
+
lastReason = e?.message || String(e);
|
|
211
|
+
log(`attempt ${attempt}/${tries} errored: ${lastReason}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (attempt < tries) await sleep(intervalMs);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { ok: false, status: 'timed_out', reason: lastReason };
|
|
218
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
const PAYLOAD_DIR = process.env.EMPIR3_BRIDGE_PAYLOAD_DIR || __dirname;
|
|
8
|
+
const BRIDGE_BUNDLE = join(PAYLOAD_DIR, 'bundle-bridge.js');
|
|
9
|
+
const SERVER_BUNDLE = join(PAYLOAD_DIR, 'bundle-server.js');
|
|
10
|
+
|
|
11
|
+
// ── Single-instance guard ───────────────────────────────────────────
|
|
12
|
+
// This daemon binds the bridge + wrapper ports in-process. Only ONE bridge
|
|
13
|
+
// can own them, so any predecessor still holding a port must be reaped before
|
|
14
|
+
// we bind — otherwise a second daemon launched while a stale/wedged one is
|
|
15
|
+
// still up collides (EADDRINUSE, zombie Chrome, "CDP direct timeout"). A new
|
|
16
|
+
// daemon launch always means "replace whatever is there", so we reap then bind.
|
|
17
|
+
// (Callers only launch a fresh daemon when they want one — the MCP server
|
|
18
|
+
// reuses a daemon that already answers /api/status instead of relaunching.)
|
|
19
|
+
|
|
20
|
+
/** PIDs LISTENING on a TCP port (Windows). */
|
|
21
|
+
function listenerPids(port: number): number[] {
|
|
22
|
+
if (process.platform !== 'win32') return [];
|
|
23
|
+
try {
|
|
24
|
+
const out = execSync('netstat -ano -p tcp', { encoding: 'utf-8' });
|
|
25
|
+
const pids = new Set<number>();
|
|
26
|
+
for (const line of out.split('\n')) {
|
|
27
|
+
const parts = line.trim().split(/\s+/);
|
|
28
|
+
// Proto LocalAddress ForeignAddress State PID
|
|
29
|
+
if (parts.length >= 5 && /^LISTENING$/i.test(parts[3]) && parts[1].endsWith(`:${port}`)) {
|
|
30
|
+
const pid = Number(parts[4]);
|
|
31
|
+
if (pid > 0) pids.add(pid);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...pids];
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Image name for a PID (e.g. "node.exe"), lowercased. */
|
|
41
|
+
function processName(pid: number): string {
|
|
42
|
+
try {
|
|
43
|
+
const out = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf-8' });
|
|
44
|
+
const m = out.match(/^"([^"]+)"/);
|
|
45
|
+
return m ? m[1].toLowerCase() : '';
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function killPid(pid: number): void {
|
|
52
|
+
try { execSync(`taskkill /PID ${pid} /F /T`, { stdio: 'ignore' }); } catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reap any predecessor bridge holding our ports so we can bind a fresh one.
|
|
57
|
+
* Only kills node.exe (a bridge daemon) and chrome.exe (its driven Chrome) —
|
|
58
|
+
* never an unrelated app that merely happens to use the port.
|
|
59
|
+
*/
|
|
60
|
+
function reapPredecessors(bridgePort: number, wrapperPort: number, cdpPort: number): void {
|
|
61
|
+
if (process.platform !== 'win32') return;
|
|
62
|
+
const pids = new Set<number>([
|
|
63
|
+
...listenerPids(bridgePort),
|
|
64
|
+
...listenerPids(wrapperPort),
|
|
65
|
+
...listenerPids(cdpPort),
|
|
66
|
+
]);
|
|
67
|
+
pids.delete(process.pid);
|
|
68
|
+
let reaped = 0;
|
|
69
|
+
for (const pid of pids) {
|
|
70
|
+
const name = processName(pid);
|
|
71
|
+
if (name === 'node.exe' || name === 'chrome.exe') {
|
|
72
|
+
console.log(`[empir3-bridge] reaping predecessor ${name} PID ${pid} (held bridge port)`);
|
|
73
|
+
killPid(pid);
|
|
74
|
+
reaped++;
|
|
75
|
+
} else if (name) {
|
|
76
|
+
console.log(`[empir3-bridge] port held by ${name} PID ${pid} — leaving it alone`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (reaped > 0) {
|
|
80
|
+
// Give the OS a moment to release the ports (TIME_WAIT) before we bind.
|
|
81
|
+
const until = Date.now() + 1500;
|
|
82
|
+
while (Date.now() < until) { /* brief spin so binding doesn't race the kill */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ensureBridgeNonce(): string {
|
|
87
|
+
const nonce = process.env.EMPIR3_BRIDGE_NONCE || randomBytes(8).toString('hex');
|
|
88
|
+
process.env.EMPIR3_BRIDGE_NONCE = nonce;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const dir = join(homedir(), '.empir3-bridge');
|
|
92
|
+
mkdirSync(dir, { recursive: true });
|
|
93
|
+
writeFileSync(join(dir, 'nonce'), nonce, 'utf-8');
|
|
94
|
+
} catch (e: any) {
|
|
95
|
+
console.warn(`[empir3-bridge] failed to write bridge nonce: ${e?.message || e}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return nonce;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function wait(ms: number): Promise<void> {
|
|
102
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function waitFor(
|
|
106
|
+
url: string,
|
|
107
|
+
label: string,
|
|
108
|
+
maxWaitMs: number,
|
|
109
|
+
isReady: (body: any) => boolean = () => true,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
while (Date.now() - start < maxWaitMs) {
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(url);
|
|
115
|
+
if (res.ok) {
|
|
116
|
+
const text = await res.text();
|
|
117
|
+
let body: any = null;
|
|
118
|
+
try { body = text ? JSON.parse(text) : null; } catch {}
|
|
119
|
+
if (isReady(body)) return;
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
await wait(500);
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`${label} did not become ready at ${url}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function loadBundle(script: string, label: string): void {
|
|
128
|
+
if (!existsSync(script)) {
|
|
129
|
+
throw new Error(`Missing ${label} bundle: ${script}`);
|
|
130
|
+
}
|
|
131
|
+
console.log(`[empir3-bridge] loading ${label}: ${script}`);
|
|
132
|
+
require(script);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function start() {
|
|
136
|
+
const bridgePort = Number(process.env.EMPIR3_BRIDGE_PORT || process.env.EMPIR3_BRIDGE_HTTP_PORT || 9867);
|
|
137
|
+
const wrapperPort = Number(process.env.EMPIR3_PW_PORT || process.env.PW_PORT || 3006);
|
|
138
|
+
const cdpPort = Number(process.env.CDP_PORT || 9222);
|
|
139
|
+
process.env.EMPIR3_BRIDGE_PORT = String(bridgePort);
|
|
140
|
+
process.env.BRIDGE_PORT = String(bridgePort);
|
|
141
|
+
process.env.PW_PORT = String(wrapperPort);
|
|
142
|
+
const nonce = ensureBridgeNonce();
|
|
143
|
+
|
|
144
|
+
console.log(`[empir3-bridge] starting payload runtime v${process.env.EMPIR3_BRIDGE_PAYLOAD_VERSION || 'dev'} nonce=${nonce.slice(0, 6)}...`);
|
|
145
|
+
|
|
146
|
+
// Replace any stale/wedged predecessor holding our ports before we bind.
|
|
147
|
+
reapPredecessors(bridgePort, wrapperPort, cdpPort);
|
|
148
|
+
|
|
149
|
+
loadBundle(BRIDGE_BUNDLE, 'cdp bridge');
|
|
150
|
+
await waitFor(
|
|
151
|
+
`http://127.0.0.1:${bridgePort}/health`,
|
|
152
|
+
'CDP bridge HTTP server',
|
|
153
|
+
30_000,
|
|
154
|
+
(body) => body?.port === bridgePort && typeof body?.status === 'string',
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
loadBundle(SERVER_BUNDLE, 'http wrapper');
|
|
158
|
+
await waitFor(`http://127.0.0.1:${wrapperPort}/api/status`, 'HTTP wrapper', 30_000);
|
|
159
|
+
|
|
160
|
+
await new Promise<void>(() => {});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (require.main === module) {
|
|
164
|
+
start().catch((e) => {
|
|
165
|
+
console.error('[empir3-bridge] payload runtime failed:', e?.stack || e?.message || e);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
|
168
|
+
}
|