@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
package/build/build.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Empir3 Bridge — build pipeline (Phase 9: native Go bootstrapper).
|
|
4
|
+
*
|
|
5
|
+
* Empir3Setup.exe is now a small native **Go** stub (build/bootstrap-go/),
|
|
6
|
+
* ~6.6 MB, that fetches a signed payload + a signed pinned Node runtime at
|
|
7
|
+
* first run and caches both. The old 86 MB Node-SEA bootstrapper is gone.
|
|
8
|
+
*
|
|
9
|
+
* One `node build/build.js` run produces four independently-signed artifact
|
|
10
|
+
* streams plus one signed manifest:
|
|
11
|
+
*
|
|
12
|
+
* 1. Empir3Setup.exe ← `go build` of build/bootstrap-go. Embeds the
|
|
13
|
+
* Ed25519 pubkey + BOOTSTRAP_VERSION as source
|
|
14
|
+
* constants (asserted against
|
|
15
|
+
* payload-signing-pub.json here) and an
|
|
16
|
+
* asInvoker manifest (committed .syso) so the
|
|
17
|
+
* "*Setup.exe" name does not trip UAC.
|
|
18
|
+
*
|
|
19
|
+
* 2. node-win-x64-v<ver>.tar.gz (+ .sig)
|
|
20
|
+
* ← a pinned official Node runtime (node-pin.json),
|
|
21
|
+
* downloaded, sha-verified, repacked flat
|
|
22
|
+
* (node.exe + LICENSE) and Ed25519-signed. The
|
|
23
|
+
* ABI is pinned to match node-pty's prebuilds.
|
|
24
|
+
*
|
|
25
|
+
* 3. bridge-payload-vX.Y.Z.tar.gz (+ .sig)
|
|
26
|
+
* ← bundle-daemon/bridge/server/installer/mcp +
|
|
27
|
+
* entry.js + installer-ui + tray + node-pty
|
|
28
|
+
* runtime, deterministic tar.gz, Ed25519-signed.
|
|
29
|
+
* Contains NO Node runtime (kept lean).
|
|
30
|
+
*
|
|
31
|
+
* 4. bridge-version.json ← the SIGNED release manifest. Legacy fields
|
|
32
|
+
* (version/payloadUrl/signatureUrl/sha256) kept
|
|
33
|
+
* verbatim for old SEA installs; new node + trust
|
|
34
|
+
* fields added alongside; an embedded
|
|
35
|
+
* `manifestSignature` (Ed25519 over the canonical
|
|
36
|
+
* form — see build/manifest-canonical.js, which is
|
|
37
|
+
* byte-identical to the Go stub's verifier).
|
|
38
|
+
*
|
|
39
|
+
* Run:
|
|
40
|
+
* cd bridge && node build/build.js (full Windows build)
|
|
41
|
+
* cd bridge && node build/build.js --check (release preflight, bundles only)
|
|
42
|
+
*
|
|
43
|
+
* Outputs (under bridge/build/dist/):
|
|
44
|
+
* Empir3Setup.exe
|
|
45
|
+
* node-win-x64-v<nodeVer>.tar.gz + .sig
|
|
46
|
+
* bridge-payload-vX.Y.Z.tar.gz + .sig
|
|
47
|
+
* bridge-version.json
|
|
48
|
+
*/
|
|
49
|
+
const fs = require('fs');
|
|
50
|
+
const path = require('path');
|
|
51
|
+
const crypto = require('crypto');
|
|
52
|
+
const zlib = require('zlib');
|
|
53
|
+
const os = require('os');
|
|
54
|
+
const { spawnSync, execFileSync } = require('child_process');
|
|
55
|
+
const { canonicalizeManifest, signManifest, verifyManifestBytes } = require('./manifest-canonical');
|
|
56
|
+
const { buildDeterministicTarGz, extractTarGz } = require('./tar-util');
|
|
57
|
+
|
|
58
|
+
const BRIDGE_DIR = path.resolve(__dirname, '..');
|
|
59
|
+
const BUILD_DIR = __dirname;
|
|
60
|
+
const DIST_DIR = path.join(BUILD_DIR, 'dist');
|
|
61
|
+
const CHECK_DIR = path.join(BUILD_DIR, 'check');
|
|
62
|
+
const CHECK_ONLY = process.argv.includes('--check');
|
|
63
|
+
|
|
64
|
+
// ── Go bootstrapper (becomes Empir3Setup.exe) ──────────────────────
|
|
65
|
+
const GO_DIR = path.join(BUILD_DIR, 'bootstrap-go');
|
|
66
|
+
const GO_MAIN = path.join(GO_DIR, 'main.go');
|
|
67
|
+
const EXE_OUT = path.join(DIST_DIR, 'Empir3Setup.exe');
|
|
68
|
+
|
|
69
|
+
// ── Signing ─────────────────────────────────────────────────────────
|
|
70
|
+
const SIGNING_KEY = path.join(BUILD_DIR, 'payload-signing-key.pem');
|
|
71
|
+
const PUB_JSON = path.join(BUILD_DIR, 'payload-signing-pub.json');
|
|
72
|
+
|
|
73
|
+
// ── Pinned Node runtime ─────────────────────────────────────────────
|
|
74
|
+
const NODE_PIN_FILE = path.join(BUILD_DIR, 'node-pin.json');
|
|
75
|
+
const NODE_CACHE_DIR = path.join(BUILD_DIR, 'node-cache'); // verified upstream zips
|
|
76
|
+
|
|
77
|
+
// ── Payload (signed tarball, served from CDN) ──────────────────────
|
|
78
|
+
const PAYLOAD_VERSION = readPayloadVersion();
|
|
79
|
+
const PAYLOAD_STAGING = path.join(BUILD_DIR, 'payload-staging');
|
|
80
|
+
const PAYLOAD_TARBALL = path.join(DIST_DIR, `bridge-payload-v${PAYLOAD_VERSION}.tar.gz`);
|
|
81
|
+
const PAYLOAD_SIG = path.join(DIST_DIR, `bridge-payload-v${PAYLOAD_VERSION}.sig`);
|
|
82
|
+
const VERSION_MANIFEST = path.join(DIST_DIR, 'bridge-version.json');
|
|
83
|
+
|
|
84
|
+
const DAEMON_SRC = path.join(BRIDGE_DIR, 'src', 'payload-daemon.ts');
|
|
85
|
+
const BRIDGE_SRC = path.join(BRIDGE_DIR, 'src', 'bridge.ts');
|
|
86
|
+
const SERVER_SRC = path.join(BRIDGE_DIR, 'src', 'server.ts');
|
|
87
|
+
const INSTALLER_SRC = path.join(BRIDGE_DIR, 'installer', 'server.js');
|
|
88
|
+
const MCP_SERVER_SRC = path.join(BRIDGE_DIR, 'src', 'mcp-server.ts');
|
|
89
|
+
const PAIR_CLAIM_SRC = path.join(BRIDGE_DIR, 'src', 'pair-claim.ts');
|
|
90
|
+
const PAYLOAD_ENTRY_SRC = path.join(BUILD_DIR, 'payload-entry.js');
|
|
91
|
+
|
|
92
|
+
// ── Tray (PyInstaller — Windows-only) ──────────────────────────────
|
|
93
|
+
const TRAY_DIR = path.join(BRIDGE_DIR, 'tray');
|
|
94
|
+
const TRAY_PY = path.join(TRAY_DIR, 'tray.py');
|
|
95
|
+
const TRAY_BUILD_PY = path.join(TRAY_DIR, 'build.py');
|
|
96
|
+
const TRAY_EXE = path.join(DIST_DIR, 'Empir3Tray.exe');
|
|
97
|
+
|
|
98
|
+
const PAYLOAD_BASE_URL = process.env.EMPIR3_PAYLOAD_PUBLIC_URL_BASE
|
|
99
|
+
|| 'https://app.empir3.com/downloads';
|
|
100
|
+
|
|
101
|
+
function readPayloadVersion() {
|
|
102
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(BRIDGE_DIR, 'package.json'), 'utf8'));
|
|
103
|
+
return pkg.version || '0.0.0';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function step(label) { console.log(`\n[build] ${label}`); }
|
|
107
|
+
function run(cmd, args, opts = {}) {
|
|
108
|
+
const executable = process.platform === 'win32' && cmd === 'npx' ? 'npx.cmd' : cmd;
|
|
109
|
+
const r = spawnSync(executable, args, { stdio: 'inherit', shell: false, ...opts });
|
|
110
|
+
if (r.status !== 0) {
|
|
111
|
+
throw new Error(`Command failed (${r.status}): ${cmd} ${args.join(' ')}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function rmIfExists(p) { try { fs.rmSync(p, { force: true, recursive: true }); } catch {} }
|
|
115
|
+
function sha256OfFile(p) {
|
|
116
|
+
return crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|
117
|
+
}
|
|
118
|
+
function sha256OfBuf(b) { return crypto.createHash('sha256').update(b).digest('hex'); }
|
|
119
|
+
|
|
120
|
+
function loadPubKeyHex() {
|
|
121
|
+
const pub = JSON.parse(fs.readFileSync(PUB_JSON, 'utf8'));
|
|
122
|
+
if (!pub.publicKeyHex || !/^[0-9a-f]{64}$/i.test(pub.publicKeyHex)) {
|
|
123
|
+
throw new Error(`${PUB_JSON} publicKeyHex missing or not 32-byte hex`);
|
|
124
|
+
}
|
|
125
|
+
return pub.publicKeyHex.toLowerCase();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function loadPrivateKey() {
|
|
129
|
+
if (!fs.existsSync(SIGNING_KEY)) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Missing payload signing key at ${SIGNING_KEY}. The full Windows build needs the private signing key.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return crypto.createPrivateKey(fs.readFileSync(SIGNING_KEY));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sign arbitrary bytes with the Ed25519 key; verify against the embedded
|
|
138
|
+
// pubkey before returning (catches a key/pub mismatch immediately).
|
|
139
|
+
function signBytes(buf, privateKey, pubKeyHex) {
|
|
140
|
+
const sig = crypto.sign(null, buf, privateKey);
|
|
141
|
+
const spki = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), Buffer.from(pubKeyHex, 'hex')]);
|
|
142
|
+
const pub = crypto.createPublicKey({ key: spki, format: 'der', type: 'spki' });
|
|
143
|
+
if (!crypto.verify(null, buf, pub, sig)) {
|
|
144
|
+
throw new Error('Self-verify of signature FAILED — signing key + pub mismatch?');
|
|
145
|
+
}
|
|
146
|
+
return sig;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function bundle(inputPath, outputPath, label) {
|
|
150
|
+
step(`Bundling ${label} → ${path.relative(BRIDGE_DIR, outputPath)}`);
|
|
151
|
+
const esbuild = path.join(BRIDGE_DIR, 'node_modules', 'esbuild', 'bin', 'esbuild');
|
|
152
|
+
run(process.execPath, [
|
|
153
|
+
esbuild,
|
|
154
|
+
inputPath,
|
|
155
|
+
'--bundle',
|
|
156
|
+
'--platform=node',
|
|
157
|
+
'--target=node20',
|
|
158
|
+
'--format=cjs',
|
|
159
|
+
`--outfile=${outputPath}`,
|
|
160
|
+
'--external:node:sea',
|
|
161
|
+
'--external:node-pty',
|
|
162
|
+
], { cwd: BRIDGE_DIR });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── STAGE 1: Go bootstrapper → Empir3Setup.exe ──────────────────────
|
|
166
|
+
|
|
167
|
+
// Read a `const Name = "value"` string constant out of a Go source file.
|
|
168
|
+
function readGoStringConst(src, name) {
|
|
169
|
+
const m = src.match(new RegExp(`const\\s+${name}\\s*=\\s*"([^"]*)"`));
|
|
170
|
+
return m ? m[1] : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The Go stub embeds the pubkey + bootstrap version as source constants
|
|
174
|
+
// (golden-tested). Assert they agree with payload-signing-pub.json so the exe
|
|
175
|
+
// we ship can never verify against a different trust root than the artifacts.
|
|
176
|
+
function assertGoConstants(pubKeyHex) {
|
|
177
|
+
const src = fs.readFileSync(GO_MAIN, 'utf8');
|
|
178
|
+
const goPub = (readGoStringConst(src, 'PubKeyHex') || '').toLowerCase();
|
|
179
|
+
const goVer = readGoStringConst(src, 'BootstrapVersion') || '';
|
|
180
|
+
if (goPub !== pubKeyHex) {
|
|
181
|
+
throw new Error(`Go stub PubKeyHex (${goPub}) != payload-signing-pub.json (${pubKeyHex})`);
|
|
182
|
+
}
|
|
183
|
+
if (!/^\d+\.\d+\.\d+$/.test(goVer)) {
|
|
184
|
+
throw new Error(`Go stub BootstrapVersion looks wrong: ${goVer}`);
|
|
185
|
+
}
|
|
186
|
+
step(`Go constants OK — pubkey matches, BOOTSTRAP_VERSION=${goVer}`);
|
|
187
|
+
return goVer;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildBootstrapExe(pubKeyHex) {
|
|
191
|
+
step('=== STAGE 1: Go bootstrapper → Empir3Setup.exe ===');
|
|
192
|
+
const bootVer = assertGoConstants(pubKeyHex);
|
|
193
|
+
|
|
194
|
+
rmIfExists(EXE_OUT);
|
|
195
|
+
// GOOS/GOARCH pinned so the windows/amd64 .syso (asInvoker manifest + icon)
|
|
196
|
+
// is always linked, even when building from a non-Windows host. GOPROXY=off
|
|
197
|
+
// + GOTOOLCHAIN=local: the stub has zero module deps, so it must build fully
|
|
198
|
+
// offline and never fetch a toolchain.
|
|
199
|
+
const goEnv = {
|
|
200
|
+
...process.env,
|
|
201
|
+
GOOS: 'windows',
|
|
202
|
+
GOARCH: 'amd64',
|
|
203
|
+
GOPROXY: 'off',
|
|
204
|
+
GOTOOLCHAIN: 'local',
|
|
205
|
+
CGO_ENABLED: '0',
|
|
206
|
+
};
|
|
207
|
+
run('go', ['build', '-trimpath', '-ldflags', '-s -w', '-o', EXE_OUT, '.'],
|
|
208
|
+
{ cwd: GO_DIR, env: goEnv });
|
|
209
|
+
if (!fs.existsSync(EXE_OUT)) throw new Error('go build produced no exe');
|
|
210
|
+
|
|
211
|
+
verifyExeManifestAndLaunch(bootVer);
|
|
212
|
+
|
|
213
|
+
const exeHash = sha256OfFile(EXE_OUT);
|
|
214
|
+
const sz = (fs.statSync(EXE_OUT).size / 1024 / 1024).toFixed(1);
|
|
215
|
+
step(`Empir3Setup.exe ready — ${sz} MB, sha256=${exeHash}`);
|
|
216
|
+
return { hash: exeHash, bootstrapVersion: bootVer };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// CI guard (Codex): the built exe MUST carry an asInvoker manifest and MUST
|
|
220
|
+
// launch without elevation. We assert the manifest string is embedded, and —
|
|
221
|
+
// when building on Windows — that `--bootstrap-version` runs non-elevated and
|
|
222
|
+
// prints the expected version (an installer-detected exe would force a UAC
|
|
223
|
+
// prompt and the non-elevated spawn would fail).
|
|
224
|
+
function verifyExeManifestAndLaunch(bootVer) {
|
|
225
|
+
const bytes = fs.readFileSync(EXE_OUT);
|
|
226
|
+
if (bytes.includes(Buffer.from('asInvoker', 'utf16le')) || bytes.includes(Buffer.from('asInvoker', 'latin1'))) {
|
|
227
|
+
step('asInvoker manifest present in exe');
|
|
228
|
+
} else {
|
|
229
|
+
throw new Error('Built exe is MISSING the asInvoker manifest (resource_windows_amd64.syso not linked?)');
|
|
230
|
+
}
|
|
231
|
+
if (process.platform !== 'win32') {
|
|
232
|
+
step('skipping non-elevated launch check (not on Windows)');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const r = spawnSync(EXE_OUT, ['--bootstrap-version'], { encoding: 'utf8' });
|
|
236
|
+
if (r.status !== 0 || !String(r.stdout || '').includes(bootVer)) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Non-elevated launch check failed (status=${r.status}, stdout=${JSON.stringify(r.stdout)}, ` +
|
|
239
|
+
`stderr=${JSON.stringify(r.stderr)}). The exe may be tripping UAC installer detection.`);
|
|
240
|
+
}
|
|
241
|
+
step(`Non-elevated launch OK — printed "${String(r.stdout).trim()}"`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── STAGE 2: pinned Node runtime artifact ───────────────────────────
|
|
245
|
+
|
|
246
|
+
function readNodePin() {
|
|
247
|
+
const pin = JSON.parse(fs.readFileSync(NODE_PIN_FILE, 'utf8'));
|
|
248
|
+
for (const k of ['version', 'url', 'sha256', 'abi']) {
|
|
249
|
+
if (!pin[k]) throw new Error(`node-pin.json missing ${k}`);
|
|
250
|
+
}
|
|
251
|
+
if (!/^[0-9a-f]{64}$/i.test(pin.sha256)) throw new Error('node-pin.json sha256 not 64-hex');
|
|
252
|
+
return pin;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fetch a URL to a Buffer via Node's built-in fetch (Node 18+). Bounded.
|
|
256
|
+
async function fetchToBuffer(url) {
|
|
257
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
258
|
+
if (!res.ok) throw new Error(`GET ${url}: HTTP ${res.status}`);
|
|
259
|
+
const ab = await res.arrayBuffer();
|
|
260
|
+
return Buffer.from(ab);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Download (or reuse a verified cache of) the official Node win-x64 zip, verify
|
|
264
|
+
// its sha256 against the pin BEFORE touching it.
|
|
265
|
+
async function fetchPinnedNodeZip(pin) {
|
|
266
|
+
fs.mkdirSync(NODE_CACHE_DIR, { recursive: true });
|
|
267
|
+
const zipName = path.basename(new URL(pin.url).pathname);
|
|
268
|
+
const cached = path.join(NODE_CACHE_DIR, zipName);
|
|
269
|
+
if (fs.existsSync(cached) && sha256OfFile(cached) === pin.sha256.toLowerCase()) {
|
|
270
|
+
step(`Using cached verified Node zip: ${path.relative(BRIDGE_DIR, cached)}`);
|
|
271
|
+
return cached;
|
|
272
|
+
}
|
|
273
|
+
step(`Downloading pinned Node ${pin.version} from ${pin.url}`);
|
|
274
|
+
const buf = await fetchToBuffer(pin.url);
|
|
275
|
+
const got = sha256OfBuf(buf).toLowerCase();
|
|
276
|
+
if (got !== pin.sha256.toLowerCase()) {
|
|
277
|
+
throw new Error(`Pinned Node sha256 mismatch: got ${got}, pinned ${pin.sha256}`);
|
|
278
|
+
}
|
|
279
|
+
fs.writeFileSync(cached, buf);
|
|
280
|
+
step(`Pinned Node zip verified (sha256 ok) → ${path.relative(BRIDGE_DIR, cached)}`);
|
|
281
|
+
return cached;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Extract node.exe + LICENSE (+ any root *.dll) from the official zip. The
|
|
285
|
+
// official archive nests everything under node-vX.Y.Z-win-x64/. We pull the
|
|
286
|
+
// minimal runtime into a flat staging dir so the repacked tarball extracts to
|
|
287
|
+
// <nodeDir>/node.exe (what the Go stub's nodeExe() expects).
|
|
288
|
+
function extractMinimalNode(zipPath, pin, stagingDir) {
|
|
289
|
+
rmIfExists(stagingDir);
|
|
290
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
291
|
+
const unzipDir = path.join(stagingDir, '_unzip');
|
|
292
|
+
fs.mkdirSync(unzipDir, { recursive: true });
|
|
293
|
+
|
|
294
|
+
// PowerShell Expand-Archive is present on every Win10/11. (tar.exe can read
|
|
295
|
+
// zips too, but Expand-Archive is the most portable on Windows.)
|
|
296
|
+
if (process.platform === 'win32') {
|
|
297
|
+
run('powershell', ['-NoProfile', '-NonInteractive', '-Command',
|
|
298
|
+
`Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${unzipDir.replace(/'/g, "''")}' -Force`]);
|
|
299
|
+
} else {
|
|
300
|
+
run('unzip', ['-q', zipPath, '-d', unzipDir]);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const inner = path.join(unzipDir, `node-v${pin.version}-win-x64`);
|
|
304
|
+
if (!fs.existsSync(inner)) throw new Error(`expected ${inner} inside node zip`);
|
|
305
|
+
|
|
306
|
+
const flat = path.join(stagingDir, 'flat');
|
|
307
|
+
fs.mkdirSync(flat, { recursive: true });
|
|
308
|
+
let copied = 0;
|
|
309
|
+
for (const name of fs.readdirSync(inner)) {
|
|
310
|
+
const abs = path.join(inner, name);
|
|
311
|
+
const st = fs.statSync(abs);
|
|
312
|
+
if (!st.isFile()) continue;
|
|
313
|
+
if (name === 'node.exe' || name === 'LICENSE' || /\.dll$/i.test(name)) {
|
|
314
|
+
fs.copyFileSync(abs, path.join(flat, name));
|
|
315
|
+
copied++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!fs.existsSync(path.join(flat, 'node.exe'))) {
|
|
319
|
+
throw new Error('node.exe not found in pinned Node zip');
|
|
320
|
+
}
|
|
321
|
+
step(`Repacked minimal Node runtime: ${copied} file(s) (node.exe + LICENSE${copied > 2 ? ' + dll(s)' : ''})`);
|
|
322
|
+
return flat;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function buildNodeArtifact(privateKey, pubKeyHex) {
|
|
326
|
+
step('=== STAGE 2: pinned Node runtime (signed) ===');
|
|
327
|
+
if (process.platform !== 'win32') {
|
|
328
|
+
console.warn('[build] WARN: Node artifact repack/verify needs Windows (must run node.exe). Skipping.');
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const pin = readNodePin();
|
|
332
|
+
const zip = await fetchPinnedNodeZip(pin);
|
|
333
|
+
const staging = path.join(BUILD_DIR, 'node-staging');
|
|
334
|
+
const flat = extractMinimalNode(zip, pin, staging);
|
|
335
|
+
|
|
336
|
+
const tarball = path.join(DIST_DIR, `node-win-x64-v${pin.version}.tar.gz`);
|
|
337
|
+
const sig = path.join(DIST_DIR, `node-win-x64-v${pin.version}.sig`);
|
|
338
|
+
fs.mkdirSync(DIST_DIR, { recursive: true });
|
|
339
|
+
|
|
340
|
+
step(`Packing Node tarball → ${path.relative(BRIDGE_DIR, tarball)}`);
|
|
341
|
+
const tarGz = buildDeterministicTarGz(flat);
|
|
342
|
+
fs.writeFileSync(tarball, tarGz);
|
|
343
|
+
|
|
344
|
+
step(`Signing Node tarball → ${path.relative(BRIDGE_DIR, sig)}`);
|
|
345
|
+
fs.writeFileSync(sig, signBytes(tarGz, privateKey, pubKeyHex));
|
|
346
|
+
|
|
347
|
+
const sha256 = sha256OfFile(tarball);
|
|
348
|
+
|
|
349
|
+
// Verify against the PACKED artifact (not the prepack dir): extract the
|
|
350
|
+
// just-built tarball to a temp dir and run THAT node.exe.
|
|
351
|
+
const verifyDir = path.join(staging, 'verify');
|
|
352
|
+
extractTarGz(tarball, verifyDir);
|
|
353
|
+
const verifyNode = path.join(verifyDir, 'node.exe');
|
|
354
|
+
const verStr = execFileSync(verifyNode, ['--version'], { encoding: 'utf8' }).trim();
|
|
355
|
+
if (verStr !== `v${pin.version}`) {
|
|
356
|
+
throw new Error(`packed node --version = ${verStr}, expected v${pin.version}`);
|
|
357
|
+
}
|
|
358
|
+
const abiStr = execFileSync(verifyNode, ['-p', 'process.versions.modules'], { encoding: 'utf8' }).trim();
|
|
359
|
+
if (abiStr !== String(pin.abi)) {
|
|
360
|
+
throw new Error(`packed node ABI = ${abiStr}, pin.abi = ${pin.abi} (node-pty prebuilds would mismatch)`);
|
|
361
|
+
}
|
|
362
|
+
step(`Packed Node verified: ${verStr}, ABI ${abiStr}`);
|
|
363
|
+
|
|
364
|
+
rmIfExists(staging);
|
|
365
|
+
|
|
366
|
+
const sz = (tarGz.length / 1024 / 1024).toFixed(1);
|
|
367
|
+
step(`Node artifact v${pin.version} — ${sz} MB, sha256=${sha256}`);
|
|
368
|
+
return {
|
|
369
|
+
version: pin.version,
|
|
370
|
+
abi: String(pin.abi),
|
|
371
|
+
platform: pin.platform || 'win32',
|
|
372
|
+
arch: pin.arch || 'x64',
|
|
373
|
+
tarballName: path.basename(tarball),
|
|
374
|
+
sigName: path.basename(sig),
|
|
375
|
+
tarballPath: tarball,
|
|
376
|
+
sha256,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── deterministic tar.gz writer (shared by node + payload artifacts) ──
|
|
381
|
+
|
|
382
|
+
function copyTreeFlat(srcDir, destDir, files) {
|
|
383
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
384
|
+
for (const f of files) {
|
|
385
|
+
const abs = path.join(srcDir, f);
|
|
386
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
|
|
387
|
+
fs.copyFileSync(abs, path.join(destDir, f));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function copyTreeRecursive(srcDir, destDir, opts = {}) {
|
|
392
|
+
if (!fs.existsSync(srcDir)) return;
|
|
393
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
394
|
+
for (const name of fs.readdirSync(srcDir)) {
|
|
395
|
+
if (opts.skip && opts.skip(name)) continue;
|
|
396
|
+
const src = path.join(srcDir, name);
|
|
397
|
+
const dest = path.join(destDir, name);
|
|
398
|
+
const st = fs.statSync(src);
|
|
399
|
+
if (st.isDirectory()) copyTreeRecursive(src, dest, opts);
|
|
400
|
+
else if (st.isFile()) fs.copyFileSync(src, dest);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function copyNodePtyRuntime(stagingDir) {
|
|
405
|
+
const src = path.join(BRIDGE_DIR, 'node_modules', 'node-pty');
|
|
406
|
+
if (!fs.existsSync(src)) {
|
|
407
|
+
throw new Error('node-pty dependency missing. Run `npm install` before building the bridge payload.');
|
|
408
|
+
}
|
|
409
|
+
const dest = path.join(stagingDir, 'node_modules', 'node-pty');
|
|
410
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
411
|
+
for (const file of ['package.json', 'LICENSE']) {
|
|
412
|
+
const abs = path.join(src, file);
|
|
413
|
+
if (fs.existsSync(abs)) fs.copyFileSync(abs, path.join(dest, file));
|
|
414
|
+
}
|
|
415
|
+
const skipDebugSymbols = (name) => /\.pdb$/i.test(name);
|
|
416
|
+
copyTreeRecursive(path.join(src, 'lib'), path.join(dest, 'lib'));
|
|
417
|
+
copyTreeRecursive(path.join(src, 'prebuilds'), path.join(dest, 'prebuilds'), { skip: skipDebugSymbols });
|
|
418
|
+
copyTreeRecursive(path.join(src, 'build', 'Release'), path.join(dest, 'build', 'Release'), { skip: skipDebugSymbols });
|
|
419
|
+
step(`Copied node-pty runtime into payload (${path.relative(BRIDGE_DIR, dest)})`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── STAGE 3: payload tarball (signed) ───────────────────────────────
|
|
423
|
+
|
|
424
|
+
function buildPayload(privateKey, pubKeyHex) {
|
|
425
|
+
step('=== STAGE 3: payload (daemon + installer + assets, signed) ===');
|
|
426
|
+
|
|
427
|
+
rmIfExists(PAYLOAD_STAGING);
|
|
428
|
+
fs.mkdirSync(PAYLOAD_STAGING, { recursive: true });
|
|
429
|
+
|
|
430
|
+
bundle(DAEMON_SRC, path.join(PAYLOAD_STAGING, 'bundle-daemon.js'), 'daemon');
|
|
431
|
+
bundle(BRIDGE_SRC, path.join(PAYLOAD_STAGING, 'bundle-bridge.js'), 'cdp bridge');
|
|
432
|
+
bundle(SERVER_SRC, path.join(PAYLOAD_STAGING, 'bundle-server.js'), 'http wrapper');
|
|
433
|
+
bundle(INSTALLER_SRC, path.join(PAYLOAD_STAGING, 'bundle-installer.js'), 'installer');
|
|
434
|
+
bundle(MCP_SERVER_SRC, path.join(PAYLOAD_STAGING, 'bundle-mcp-server.js'), 'mcp-server');
|
|
435
|
+
bundle(PAIR_CLAIM_SRC, path.join(PAYLOAD_STAGING, 'bundle-pair-claim.js'), 'pair-claim');
|
|
436
|
+
|
|
437
|
+
copyNodePtyRuntime(PAYLOAD_STAGING);
|
|
438
|
+
|
|
439
|
+
fs.copyFileSync(PAYLOAD_ENTRY_SRC, path.join(PAYLOAD_STAGING, 'entry.js'));
|
|
440
|
+
|
|
441
|
+
const uiSrc = path.join(BRIDGE_DIR, 'installer', 'ui');
|
|
442
|
+
copyTreeFlat(uiSrc, path.join(PAYLOAD_STAGING, 'installer-ui'),
|
|
443
|
+
fs.readdirSync(uiSrc).filter((f) => fs.statSync(path.join(uiSrc, f)).isFile()));
|
|
444
|
+
|
|
445
|
+
if (fs.existsSync(TRAY_EXE)) {
|
|
446
|
+
const dest = path.join(PAYLOAD_STAGING, 'Empir3Tray.exe');
|
|
447
|
+
fs.copyFileSync(TRAY_EXE, dest);
|
|
448
|
+
step(`Bundled tray into payload: ${path.relative(BRIDGE_DIR, dest)}`);
|
|
449
|
+
} else if (process.platform === 'win32') {
|
|
450
|
+
// RELEASE INVARIANT (Codex): a Windows release payload MUST carry the tray.
|
|
451
|
+
// Without it the --daemon launcher path has no GUI surface and would take
|
|
452
|
+
// the supervised-fallback relaunch — acceptable as a runtime safety net, but
|
|
453
|
+
// shipping a payload with no tray at all is a defect, not a warning.
|
|
454
|
+
throw new Error(`Empir3Tray.exe missing at ${TRAY_EXE} — refusing to build a tray-less release payload. Build the tray first (python ${path.relative(BRIDGE_DIR, TRAY_BUILD_PY)}).`);
|
|
455
|
+
} else {
|
|
456
|
+
console.warn(`[build] WARN: tray exe not found at ${TRAY_EXE} (non-Windows dev build) — payload will use the headless fallback.`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const accuracyLabSrc = path.join(BRIDGE_DIR, 'assets', 'accuracy-lab.html');
|
|
460
|
+
if (fs.existsSync(accuracyLabSrc)) {
|
|
461
|
+
fs.copyFileSync(accuracyLabSrc, path.join(PAYLOAD_STAGING, 'accuracy-lab.html'));
|
|
462
|
+
step('Bundled accuracy-lab.html into payload');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
fs.writeFileSync(path.join(PAYLOAD_STAGING, '.payload-version'), PAYLOAD_VERSION);
|
|
466
|
+
|
|
467
|
+
step(`Packing payload tarball → ${path.relative(BRIDGE_DIR, PAYLOAD_TARBALL)}`);
|
|
468
|
+
fs.mkdirSync(DIST_DIR, { recursive: true });
|
|
469
|
+
const tarGz = buildDeterministicTarGz(PAYLOAD_STAGING);
|
|
470
|
+
fs.writeFileSync(PAYLOAD_TARBALL, tarGz);
|
|
471
|
+
|
|
472
|
+
step(`Signing payload → ${path.relative(BRIDGE_DIR, PAYLOAD_SIG)}`);
|
|
473
|
+
fs.writeFileSync(PAYLOAD_SIG, signBytes(tarGz, privateKey, pubKeyHex));
|
|
474
|
+
|
|
475
|
+
const sha256 = sha256OfFile(PAYLOAD_TARBALL);
|
|
476
|
+
step(`Payload v${PAYLOAD_VERSION} — ${(tarGz.length / 1024 / 1024).toFixed(1)} MB, sha256=${sha256}`);
|
|
477
|
+
return {
|
|
478
|
+
version: PAYLOAD_VERSION,
|
|
479
|
+
tarballName: path.basename(PAYLOAD_TARBALL),
|
|
480
|
+
sigName: path.basename(PAYLOAD_SIG),
|
|
481
|
+
sha256,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── STAGE 4: signed release manifest ────────────────────────────────
|
|
486
|
+
|
|
487
|
+
function buildManifest(payload, node, privateKey, pubKeyHex) {
|
|
488
|
+
step('=== STAGE 4: signed release manifest (bridge-version.json) ===');
|
|
489
|
+
if (!node) {
|
|
490
|
+
throw new Error('Cannot build manifest without the Node artifact (Windows build required).');
|
|
491
|
+
}
|
|
492
|
+
// Cache-bust query string keeps Cloudflare from serving a stale artifact at a
|
|
493
|
+
// reused filename. It is part of the signed URL strings.
|
|
494
|
+
const t = Date.now();
|
|
495
|
+
const bust = (v) => `?v=${encodeURIComponent(v)}&t=${t}`;
|
|
496
|
+
const base = PAYLOAD_BASE_URL;
|
|
497
|
+
|
|
498
|
+
// EVERY value is a string (no numbers/floats/nulls) — required by the
|
|
499
|
+
// canonicalization contract. Legacy fields kept verbatim for old SEA
|
|
500
|
+
// bootstrappers (version/payloadUrl/signatureUrl/sha256).
|
|
501
|
+
const fields = {
|
|
502
|
+
// ── legacy (do NOT rename) ──
|
|
503
|
+
version: payload.version,
|
|
504
|
+
payloadUrl: `${base}/${payload.tarballName}${bust(payload.version)}`,
|
|
505
|
+
signatureUrl: `${base}/${payload.sigName}${bust(payload.version)}`,
|
|
506
|
+
sha256: payload.sha256,
|
|
507
|
+
// ── new (Go stub only; old bootstrappers ignore) ──
|
|
508
|
+
schemaVersion: '2',
|
|
509
|
+
nodeUrl: `${base}/${node.tarballName}${bust(node.version)}`,
|
|
510
|
+
nodeSignatureUrl: `${base}/${node.sigName}${bust(node.version)}`,
|
|
511
|
+
nodeSha256: node.sha256,
|
|
512
|
+
nodeVersion: node.version,
|
|
513
|
+
nodeAbi: node.abi,
|
|
514
|
+
platform: node.platform,
|
|
515
|
+
arch: node.arch,
|
|
516
|
+
publishedAt: new Date(t).toISOString(),
|
|
517
|
+
};
|
|
518
|
+
// Embed manifestSignature (Ed25519 over the canonical form of all OTHER
|
|
519
|
+
// fields). build/manifest-canonical.js is byte-identical to the Go verifier.
|
|
520
|
+
fields.manifestSignature = signManifest(fields, privateKey);
|
|
521
|
+
|
|
522
|
+
fs.writeFileSync(VERSION_MANIFEST, JSON.stringify(fields, null, 2) + '\n');
|
|
523
|
+
step(`Manifest written → ${path.relative(BRIDGE_DIR, VERSION_MANIFEST)}`);
|
|
524
|
+
return fields;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ── Self-verify everything the stub will check, the way the stub checks it ──
|
|
528
|
+
|
|
529
|
+
function selfVerifyAll(payload, node, pubKeyHex) {
|
|
530
|
+
step('=== Self-verify: payload sig, node sig, manifest sig ===');
|
|
531
|
+
|
|
532
|
+
const verifyDetached = (label, tarball, sigFile) => {
|
|
533
|
+
const data = fs.readFileSync(tarball);
|
|
534
|
+
const sig = fs.readFileSync(sigFile);
|
|
535
|
+
const spki = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), Buffer.from(pubKeyHex, 'hex')]);
|
|
536
|
+
const pub = crypto.createPublicKey({ key: spki, format: 'der', type: 'spki' });
|
|
537
|
+
if (!crypto.verify(null, data, pub, sig)) throw new Error(`${label} signature self-verify FAILED`);
|
|
538
|
+
if (sha256OfFile(tarball) !== (label === 'payload' ? payload.sha256 : node.sha256)) {
|
|
539
|
+
throw new Error(`${label} sha256 self-check FAILED`);
|
|
540
|
+
}
|
|
541
|
+
step(` ${label} sig + sha256 OK`);
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
verifyDetached('payload', PAYLOAD_TARBALL, PAYLOAD_SIG);
|
|
545
|
+
verifyDetached('node', node.tarballPath, path.join(DIST_DIR, node.sigName));
|
|
546
|
+
|
|
547
|
+
// Re-parse the manifest from disk and verify exactly like the stub.
|
|
548
|
+
const raw = fs.readFileSync(VERSION_MANIFEST);
|
|
549
|
+
if (!verifyManifestBytes(raw, pubKeyHex)) {
|
|
550
|
+
throw new Error('manifest signature self-verify FAILED (would be refused by the stub)');
|
|
551
|
+
}
|
|
552
|
+
step(' manifest sig OK (re-parsed like the stub)');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Smoke: extract payload + node to a temp dir and run the PACKED node against
|
|
556
|
+
// entry.js (--version) and require('node-pty') (ABI match).
|
|
557
|
+
function smokePackedRuntime(node) {
|
|
558
|
+
step('=== Smoke: packed node runs entry.js + loads node-pty ===');
|
|
559
|
+
if (process.platform !== 'win32' || !node) {
|
|
560
|
+
console.warn('[build] WARN: skipping runtime smoke (needs Windows + node artifact).');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'empir3-smoke-'));
|
|
564
|
+
try {
|
|
565
|
+
const payloadDir = path.join(tmp, 'payload');
|
|
566
|
+
const nodeDir = path.join(tmp, 'node');
|
|
567
|
+
extractTarGz(PAYLOAD_TARBALL, payloadDir);
|
|
568
|
+
extractTarGz(node.tarballPath, nodeDir);
|
|
569
|
+
const nodeExe = path.join(nodeDir, 'node.exe');
|
|
570
|
+
|
|
571
|
+
const v = execFileSync(nodeExe, [path.join(payloadDir, 'entry.js'), '--version'],
|
|
572
|
+
{ cwd: payloadDir, encoding: 'utf8', env: { ...process.env, EMPIR3_BRIDGE_PAYLOAD_DIR: payloadDir } }).trim();
|
|
573
|
+
if (v !== PAYLOAD_VERSION) throw new Error(`entry.js --version = ${v}, expected ${PAYLOAD_VERSION}`);
|
|
574
|
+
step(` node entry.js --version → ${v}`);
|
|
575
|
+
|
|
576
|
+
execFileSync(nodeExe, ['-e', "require('node-pty'); console.error('node-pty ok')"],
|
|
577
|
+
{ cwd: payloadDir, encoding: 'utf8' });
|
|
578
|
+
step(' node -e "require(\'node-pty\')" OK (ABI matches)');
|
|
579
|
+
} finally {
|
|
580
|
+
rmIfExists(tmp);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── Tray exe (PyInstaller) ──────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
function buildTrayExe() {
|
|
587
|
+
step('=== STAGE 2b: tray (Empir3Tray.exe via PyInstaller) ===');
|
|
588
|
+
|
|
589
|
+
if (process.platform !== 'win32') {
|
|
590
|
+
console.warn('[build] WARN: skipping tray build — only supported on Windows for now.');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (!fs.existsSync(TRAY_PY) || !fs.existsSync(TRAY_BUILD_PY)) {
|
|
594
|
+
console.warn(`[build] WARN: tray sources missing (${TRAY_PY}); skipping tray build.`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Clear any prior tray exe first so a failed PyInstaller run can never let a
|
|
599
|
+
// stale tray slip into the payload (Codex hardening).
|
|
600
|
+
rmIfExists(TRAY_EXE);
|
|
601
|
+
const py = process.env.PYTHON || 'python';
|
|
602
|
+
const r = spawnSync(py, [TRAY_BUILD_PY], { stdio: 'inherit', cwd: TRAY_DIR, shell: false });
|
|
603
|
+
if (r.status !== 0) {
|
|
604
|
+
console.warn(`[build] WARN: tray PyInstaller build failed (exit ${r.status}). Payload will fall back to headless --daemon.`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (!fs.existsSync(TRAY_EXE)) {
|
|
608
|
+
console.warn(`[build] WARN: PyInstaller reported success but ${TRAY_EXE} missing.`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const sz = (fs.statSync(TRAY_EXE).size / 1024 / 1024).toFixed(1);
|
|
612
|
+
step(`Tray exe ready → ${path.relative(BRIDGE_DIR, TRAY_EXE)} (${sz} MB)`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function checkReleaseInputs() {
|
|
616
|
+
step('=== Release preflight: bundle inputs only ===');
|
|
617
|
+
rmIfExists(CHECK_DIR);
|
|
618
|
+
fs.mkdirSync(CHECK_DIR, { recursive: true });
|
|
619
|
+
|
|
620
|
+
bundle(DAEMON_SRC, path.join(CHECK_DIR, 'bundle-daemon.js'), 'daemon');
|
|
621
|
+
bundle(BRIDGE_SRC, path.join(CHECK_DIR, 'bundle-bridge.js'), 'cdp bridge');
|
|
622
|
+
bundle(SERVER_SRC, path.join(CHECK_DIR, 'bundle-server.js'), 'http wrapper');
|
|
623
|
+
bundle(INSTALLER_SRC, path.join(CHECK_DIR, 'bundle-installer.js'), 'installer');
|
|
624
|
+
bundle(MCP_SERVER_SRC, path.join(CHECK_DIR, 'bundle-mcp-server.js'), 'mcp-server');
|
|
625
|
+
bundle(PAIR_CLAIM_SRC, path.join(CHECK_DIR, 'bundle-pair-claim.js'), 'pair-claim');
|
|
626
|
+
|
|
627
|
+
// Assert the Go trust root matches the pub json (cheap, no Go toolchain).
|
|
628
|
+
assertGoConstants(loadPubKeyHex());
|
|
629
|
+
|
|
630
|
+
const required = [
|
|
631
|
+
PAYLOAD_ENTRY_SRC,
|
|
632
|
+
path.join(BRIDGE_DIR, 'installer', 'ui', 'index.html'),
|
|
633
|
+
TRAY_PY,
|
|
634
|
+
TRAY_BUILD_PY,
|
|
635
|
+
PUB_JSON,
|
|
636
|
+
NODE_PIN_FILE,
|
|
637
|
+
GO_MAIN,
|
|
638
|
+
];
|
|
639
|
+
for (const file of required) {
|
|
640
|
+
if (!fs.existsSync(file)) throw new Error(`Missing release input: ${file}`);
|
|
641
|
+
}
|
|
642
|
+
step(`Release preflight OK for payload v${PAYLOAD_VERSION}`);
|
|
643
|
+
step('Full Windows build still requires: Go toolchain, the private signing key, Windows tray tooling, network for the pinned Node download.');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ── Main ───────────────────────────────────────────────────────────
|
|
647
|
+
|
|
648
|
+
async function main() {
|
|
649
|
+
if (CHECK_ONLY) {
|
|
650
|
+
checkReleaseInputs();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fs.mkdirSync(DIST_DIR, { recursive: true });
|
|
655
|
+
|
|
656
|
+
const pubKeyHex = loadPubKeyHex();
|
|
657
|
+
const privateKey = loadPrivateKey();
|
|
658
|
+
|
|
659
|
+
const exe = buildBootstrapExe(pubKeyHex);
|
|
660
|
+
const node = await buildNodeArtifact(privateKey, pubKeyHex);
|
|
661
|
+
buildTrayExe(); // before buildPayload so the tarball includes it
|
|
662
|
+
const payload = buildPayload(privateKey, pubKeyHex);
|
|
663
|
+
const manifest = buildManifest(payload, node, privateKey, pubKeyHex);
|
|
664
|
+
|
|
665
|
+
selfVerifyAll(payload, node, pubKeyHex);
|
|
666
|
+
smokePackedRuntime(node);
|
|
667
|
+
|
|
668
|
+
step('=== Done ===');
|
|
669
|
+
step(`Empir3Setup.exe sha256: ${exe.hash} (bootstrap v${exe.bootstrapVersion})`);
|
|
670
|
+
step(`Node runtime: v${node.version} (ABI ${node.abi}) sha256 ${node.sha256}`);
|
|
671
|
+
step(`Payload: v${manifest.version} sha256 ${manifest.sha256}`);
|
|
672
|
+
step('');
|
|
673
|
+
step('Publish (enforces order: node+payload → manifest → exe):');
|
|
674
|
+
step(' npm run publish:downloads');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
main().catch((e) => {
|
|
678
|
+
console.error('\n[build] FAILED:', e.stack || e.message);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
});
|