@orbit-tools/cli 0.6.0 → 0.7.1

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 CHANGED
@@ -3,9 +3,9 @@
3
3
  npm proxy for the [Orbit](https://github.com/danieljhkim/orbit) CLI.
4
4
 
5
5
  On install, downloads the matching prebuilt `orbit` binary from
6
- [GitHub Releases](https://github.com/danieljhkim/orbit/releases), verifies its
7
- SHA-256 against the published `orbit-checksums.txt`, and exposes it as the
8
- `orbit` command.
6
+ [GitHub Releases](https://github.com/danieljhkim/orbit/releases), authenticates
7
+ the signed `orbit-checksums.txt` with the package-pinned release trust set,
8
+ verifies the archive SHA-256, and exposes it as the `orbit` command.
9
9
 
10
10
  ## Usage
11
11
 
@@ -31,8 +31,11 @@ Windows is not currently published. Use WSL or build from source.
31
31
 
32
32
  | Variable | Effect |
33
33
  |---|---|
34
- | `ORBIT_BINARY` | Path to a local `orbit` binary; bypasses download. |
35
- | `ORBIT_BINARY_VERSION` | Override the release tag to install (e.g. `v0.3.1`). |
34
+ | `ORBIT_BINARY` | Path to a local `orbit` binary; bypasses download and trusts that path as the binary source. |
35
+ | `ORBIT_RELEASE_PUBLIC_KEY_FILE` | **Deprecated** in favor of `ORBIT_RELEASE_TRUSTED_KEYS_FILE`. Single-key override for the trusted checksum-signing public key; requires `ORBIT_RELEASE_PUBLIC_KEY_FILE_ACKNOWLEDGE_TRUST_CHANGE=1`, logs a deprecation notice when active. |
36
+ | `ORBIT_RELEASE_PUBLIC_KEY_FILE_ACKNOWLEDGE_TRUST_CHANGE=1` | Required acknowledgement that `ORBIT_RELEASE_PUBLIC_KEY_FILE` replaces the release authenticity trust root. |
37
+ | `ORBIT_RELEASE_TRUSTED_KEYS_FILE` | Preferred test/operations override for the full trusted signing-key set, including key IDs, `not_after`, and `revoked_at`; requires `ORBIT_RELEASE_TRUSTED_KEYS_FILE_ACKNOWLEDGE_TRUST_CHANGE=1` and logs when active. |
38
+ | `ORBIT_RELEASE_TRUSTED_KEYS_FILE_ACKNOWLEDGE_TRUST_CHANGE=1` | Required acknowledgement that `ORBIT_RELEASE_TRUSTED_KEYS_FILE` replaces the release authenticity trust root. |
36
39
  | `ORBIT_SKIP_DOWNLOAD=1` | Skip postinstall download (lazy install on first run still works). |
37
40
 
38
41
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbit-tools/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "npm proxy for the Orbit CLI. Downloads the matching native binary from GitHub Releases on install and forwards all invocations.",
5
5
  "bin": {
6
6
  "orbit": "bin/orbit.js"
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "bin/",
14
14
  "scripts/",
15
+ "release-signing.pub",
15
16
  "README.md",
16
17
  "LICENSE"
17
18
  ],
@@ -0,0 +1,11 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAoQGLKOvvsvXriGIQ0oxA
3
+ PcDyVHLM1iqXBCYXg+blQU41haEkG1eYabvDfeGcyGaC4awW7Q2uCZK05+/Hdjpe
4
+ cRUVxP+QWKCAHyretQwOsoXzutZjJgId/ZRiUJPS/FeJOSv0xrayaol0tmfeJ4mH
5
+ gFseCLq+mIIWIPRvXmYiKaUB//bjF79w/m4VXlyBhfi6n+f6x2UPG+gjjsjwG6mn
6
+ Orec31AAFCIIX69YAd21D3MBc4S89/LoYZCq3neDscZ09Y+e6Jg2HpoBstvqSnq/
7
+ 3s34unLuIRlyB8jyK8CrdzT1E6YVB7+riAjycE9XMlLOQ2xA4tl6CKIx5YTKHyeW
8
+ npMLlbzNaVfFT7p3IPTxsoEI0SB3ZtO7/XhzuOvOpklYcqjW2DGw/yzr2epAqHE/
9
+ y4rLO3hkxWhxfgF5KPSR2iftc3LMONRGWELK6jpD5KB7No5vwIvjpVPUc5xA45Xw
10
+ tT/bo0mm4TvrumxYr1xyEHrdum+ej/WYz/0BZQlwDOtXAgMBAAE=
11
+ -----END PUBLIC KEY-----
@@ -10,12 +10,67 @@ const os = require('node:os');
10
10
 
11
11
  const PKG = require('../package.json');
12
12
  const BINARY_REPO = PKG.config.orbit.binaryRepo;
13
- // Convention: npm package version is kept in lockstep with the orbit release tag.
14
- // `0.3.1` fetches `v0.3.1` from GitHub Releases. Override with $ORBIT_BINARY_VERSION.
15
- const BINARY_VERSION = process.env.ORBIT_BINARY_VERSION || `v${PKG.version}`;
13
+ const CHECKSUM_FILE = 'orbit-checksums.txt';
14
+ const CHECKSUM_SIGNATURE_FILE = 'orbit-checksums.txt.sig';
15
+ // npm package version is kept in lockstep with the orbit release tag. `0.3.1`
16
+ // → fetches `v0.3.1` from GitHub Releases. The mapping is intentionally strict:
17
+ // older orbit binaries can no longer be installed through this package, so the
18
+ // trust set only needs to cover the current release.
19
+ const BINARY_VERSION = `v${PKG.version}`;
16
20
  const PKG_ROOT = path.resolve(__dirname, '..');
17
21
  const BIN_DIR = path.join(PKG_ROOT, 'binaries');
18
22
  const BIN_PATH = path.join(BIN_DIR, process.platform === 'win32' ? 'orbit.exe' : 'orbit');
23
+ const PUBLIC_KEY_OVERRIDE = process.env.ORBIT_RELEASE_PUBLIC_KEY_FILE;
24
+ const PUBLIC_KEY_OVERRIDE_ACK_ENV = 'ORBIT_RELEASE_PUBLIC_KEY_FILE_ACKNOWLEDGE_TRUST_CHANGE';
25
+ const TRUSTED_KEYS_OVERRIDE = process.env.ORBIT_RELEASE_TRUSTED_KEYS_FILE;
26
+ const TRUSTED_KEYS_OVERRIDE_ACK_ENV = 'ORBIT_RELEASE_TRUSTED_KEYS_FILE_ACKNOWLEDGE_TRUST_CHANGE';
27
+ const TRUSTED_PUBLIC_KEY_PATH = PUBLIC_KEY_OVERRIDE
28
+ ? path.resolve(PUBLIC_KEY_OVERRIDE)
29
+ : path.join(PKG_ROOT, 'release-signing.pub');
30
+ const TRUSTED_KEYS_OVERRIDE_PATH = TRUSTED_KEYS_OVERRIDE ? path.resolve(TRUSTED_KEYS_OVERRIDE) : null;
31
+ // orbit-release-key-4 is a PLACEHOLDER pre-staged for the next rotation. The
32
+ // PEM below was generated locally and the matching private key is NOT held by
33
+ // release infrastructure — no production signature will ever verify against
34
+ // it. Replace with a real keypair (generated on the signing host) before
35
+ // rotating off key-3.
36
+ const TRUSTED_RELEASE_KEYS = Object.freeze([
37
+ Object.freeze({
38
+ id: 'orbit-release-key-3',
39
+ notAfter: '2029-12-31',
40
+ revokedAt: null,
41
+ publicKeyPem: `-----BEGIN PUBLIC KEY-----
42
+ MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAoQGLKOvvsvXriGIQ0oxA
43
+ PcDyVHLM1iqXBCYXg+blQU41haEkG1eYabvDfeGcyGaC4awW7Q2uCZK05+/Hdjpe
44
+ cRUVxP+QWKCAHyretQwOsoXzutZjJgId/ZRiUJPS/FeJOSv0xrayaol0tmfeJ4mH
45
+ gFseCLq+mIIWIPRvXmYiKaUB//bjF79w/m4VXlyBhfi6n+f6x2UPG+gjjsjwG6mn
46
+ Orec31AAFCIIX69YAd21D3MBc4S89/LoYZCq3neDscZ09Y+e6Jg2HpoBstvqSnq/
47
+ 3s34unLuIRlyB8jyK8CrdzT1E6YVB7+riAjycE9XMlLOQ2xA4tl6CKIx5YTKHyeW
48
+ npMLlbzNaVfFT7p3IPTxsoEI0SB3ZtO7/XhzuOvOpklYcqjW2DGw/yzr2epAqHE/
49
+ y4rLO3hkxWhxfgF5KPSR2iftc3LMONRGWELK6jpD5KB7No5vwIvjpVPUc5xA45Xw
50
+ tT/bo0mm4TvrumxYr1xyEHrdum+ej/WYz/0BZQlwDOtXAgMBAAE=
51
+ -----END PUBLIC KEY-----
52
+ `,
53
+ }),
54
+ Object.freeze({
55
+ id: 'orbit-release-key-4',
56
+ notAfter: '2030-12-31',
57
+ revokedAt: null,
58
+ publicKeyPem: `-----BEGIN PUBLIC KEY-----
59
+ MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAiEVVbwQYDnbPg86xYrI8
60
+ Ddm6qpkEJ6GSJOW9NfR/eLpqwwaeWb3EPR9H/U39Rrt8ABPAGObLG9vuvSzg8YqU
61
+ Rz6NjtrSKQ4k9xO/up+qQ/zRsHkOyISEx+6MnIrw5hY/IfkrZ+3+jm8IfXJ+VAjS
62
+ VepR+o58u73ycrnLG1eXWIHtjED3SQSPffJjxSvVDEb3ogiJAsWClCMWNLnEjsQc
63
+ IHYrNdS5N0m5tfcIb9LiV2cDVXgdwdRUU41Ks9sWvBQIHrNup721UWyMdJoK1hTI
64
+ rc4PST3WTHQwFcQvVaAqod9MDPkQYlgD7IjiPkSGLHyIs52kNgFXY55E5DlU4O4e
65
+ 9QDbyQTGzZI0XlnoqAuCIXbcNXjMZuEn9UjVN35NeObsj6F/yL07YUhvORnxozjL
66
+ 41ouRFtFTWFHNenthtZnH9SUV4+O2cKDmtJpPJd68ZJ/NBqJHM4a6cteT72HJzLb
67
+ t6yto3B43nTeXtp9ozRjetznPnPD7gmI6Zq1P2ce8v49AgMBAAE=
68
+ -----END PUBLIC KEY-----
69
+ `,
70
+ }),
71
+ ]);
72
+ let publicKeyOverrideLogged = false;
73
+ let trustedKeysOverrideLogged = false;
19
74
 
20
75
  function log(msg) {
21
76
  process.stderr.write(`@orbit-tools/cli: ${msg}\n`);
@@ -78,13 +133,215 @@ function parseChecksums(text) {
78
133
  return out;
79
134
  }
80
135
 
81
- function extractTarGz(archivePath, destDir) {
82
- const result = spawnSync('tar', ['-xzf', archivePath, '-C', destDir], { stdio: 'inherit' });
136
+ function acknowledgeTrustedPublicKeyOverride() {
137
+ if (PUBLIC_KEY_OVERRIDE && TRUSTED_KEYS_OVERRIDE) {
138
+ throw new Error('ORBIT_RELEASE_PUBLIC_KEY_FILE and ORBIT_RELEASE_TRUSTED_KEYS_FILE cannot both be set');
139
+ }
140
+ if (!PUBLIC_KEY_OVERRIDE) {
141
+ return;
142
+ }
143
+ if (process.env[PUBLIC_KEY_OVERRIDE_ACK_ENV] !== '1') {
144
+ throw new Error(`ORBIT_RELEASE_PUBLIC_KEY_FILE requires ${PUBLIC_KEY_OVERRIDE_ACK_ENV}=1`);
145
+ }
146
+ if (!publicKeyOverrideLogged) {
147
+ log(`ORBIT_RELEASE_PUBLIC_KEY_FILE=${TRUSTED_PUBLIC_KEY_PATH} set; trusting replacement release signing key`);
148
+ log('ORBIT_RELEASE_PUBLIC_KEY_FILE is deprecated; prefer ORBIT_RELEASE_TRUSTED_KEYS_FILE for the full trust set (key IDs, notAfter, revokedAt)');
149
+ publicKeyOverrideLogged = true;
150
+ }
151
+ }
152
+
153
+ function acknowledgeTrustedKeysOverride() {
154
+ if (PUBLIC_KEY_OVERRIDE && TRUSTED_KEYS_OVERRIDE) {
155
+ throw new Error('ORBIT_RELEASE_PUBLIC_KEY_FILE and ORBIT_RELEASE_TRUSTED_KEYS_FILE cannot both be set');
156
+ }
157
+ if (!TRUSTED_KEYS_OVERRIDE) {
158
+ return;
159
+ }
160
+ if (process.env[TRUSTED_KEYS_OVERRIDE_ACK_ENV] !== '1') {
161
+ throw new Error(`ORBIT_RELEASE_TRUSTED_KEYS_FILE requires ${TRUSTED_KEYS_OVERRIDE_ACK_ENV}=1`);
162
+ }
163
+ if (!trustedKeysOverrideLogged) {
164
+ log(`ORBIT_RELEASE_TRUSTED_KEYS_FILE=${TRUSTED_KEYS_OVERRIDE_PATH} set; trusting replacement release signing key set`);
165
+ trustedKeysOverrideLogged = true;
166
+ }
167
+ }
168
+
169
+ function readTrustedKeysManifest(manifestPath) {
170
+ const manifestDir = path.dirname(manifestPath);
171
+ return fs
172
+ .readFileSync(manifestPath, 'utf8')
173
+ .split(/\r?\n/)
174
+ .map((line) => line.trim())
175
+ .filter((line) => line.length > 0 && !line.startsWith('#'))
176
+ .map((line) => {
177
+ const [id, notAfter, revokedAt, publicKeyPath, ...extra] = line.split('|');
178
+ if (!id || !publicKeyPath || extra.length > 0) {
179
+ throw new Error(`invalid trusted release signing key record: ${line}`);
180
+ }
181
+ const resolvedPublicKeyPath = path.isAbsolute(publicKeyPath)
182
+ ? publicKeyPath
183
+ : path.resolve(manifestDir, publicKeyPath);
184
+ return {
185
+ id,
186
+ notAfter: notAfter || null,
187
+ revokedAt: revokedAt || null,
188
+ publicKeyPem: fs.readFileSync(resolvedPublicKeyPath, 'utf8'),
189
+ };
190
+ });
191
+ }
192
+
193
+ function normalizeTrustedReleaseKeys(trustedKeys) {
194
+ if (typeof trustedKeys === 'string') {
195
+ return [
196
+ {
197
+ id: 'override',
198
+ notAfter: null,
199
+ revokedAt: null,
200
+ publicKeyPem: trustedKeys,
201
+ },
202
+ ];
203
+ }
204
+
205
+ if (!Array.isArray(trustedKeys) || trustedKeys.length === 0) {
206
+ throw new Error('at least one trusted release signing key is required');
207
+ }
208
+
209
+ return trustedKeys.map((key) => {
210
+ if (!key || !key.id || !key.publicKeyPem) {
211
+ throw new Error('trusted release signing keys require id and publicKeyPem');
212
+ }
213
+ const notAfter = key.notAfter || null;
214
+ const revokedAt = key.revokedAt || null;
215
+ // Mirror the shell-side awk regex (release_date_number) so a malformed
216
+ // override like notAfter: "next month" fails closed instead of silently
217
+ // becoming "never expires" under lexicographic comparison.
218
+ if (notAfter !== null && !/^\d{4}-\d{2}-\d{2}$/.test(notAfter)) {
219
+ throw new Error(`trusted release signing key ${key.id} has invalid notAfter: ${notAfter} (expected YYYY-MM-DD)`);
220
+ }
221
+ if (revokedAt !== null && !/^\d{4}-\d{2}-\d{2}$/.test(revokedAt)) {
222
+ throw new Error(`trusted release signing key ${key.id} has invalid revokedAt: ${revokedAt} (expected YYYY-MM-DD)`);
223
+ }
224
+ return {
225
+ id: key.id,
226
+ notAfter,
227
+ revokedAt,
228
+ publicKeyPem: key.publicKeyPem,
229
+ };
230
+ });
231
+ }
232
+
233
+ function readTrustedReleaseKeys() {
234
+ acknowledgeTrustedPublicKeyOverride();
235
+ acknowledgeTrustedKeysOverride();
236
+
237
+ if (PUBLIC_KEY_OVERRIDE) {
238
+ return normalizeTrustedReleaseKeys(fs.readFileSync(TRUSTED_PUBLIC_KEY_PATH, 'utf8'));
239
+ }
240
+ if (TRUSTED_KEYS_OVERRIDE_PATH) {
241
+ return normalizeTrustedReleaseKeys(readTrustedKeysManifest(TRUSTED_KEYS_OVERRIDE_PATH));
242
+ }
243
+ return TRUSTED_RELEASE_KEYS;
244
+ }
245
+
246
+ function releaseDateString(date = new Date()) {
247
+ return date.toISOString().slice(0, 10);
248
+ }
249
+
250
+ function assertTrustedKeyUsable(key, verificationDate = new Date()) {
251
+ if (key.revokedAt) {
252
+ throw new Error(`release checksum signature was made by revoked release signing key ${key.id} (revoked ${key.revokedAt})`);
253
+ }
254
+ if (key.notAfter && releaseDateString(verificationDate) > key.notAfter) {
255
+ throw new Error(`release checksum signature was made by expired release signing key ${key.id} (not_after ${key.notAfter})`);
256
+ }
257
+ }
258
+
259
+ function verifyWithPublicKey(checksumText, signatureBuf, publicKeyPem) {
260
+ const verifier = crypto.createVerify('RSA-SHA256');
261
+ verifier.update(checksumText, 'utf8');
262
+ verifier.end();
263
+ return verifier.verify(publicKeyPem, signatureBuf);
264
+ }
265
+
266
+ function verifyChecksumSignature(
267
+ checksumText,
268
+ signatureBuf,
269
+ trustedKeys = readTrustedReleaseKeys(),
270
+ verificationDate = new Date()
271
+ ) {
272
+ for (const key of normalizeTrustedReleaseKeys(trustedKeys)) {
273
+ if (verifyWithPublicKey(checksumText, signatureBuf, key.publicKeyPem)) {
274
+ assertTrustedKeyUsable(key, verificationDate);
275
+ log(`authenticated ${CHECKSUM_FILE} with release signing key ${key.id}`);
276
+ return key.id;
277
+ }
278
+ }
279
+ throw new Error(`release checksum signature verification failed for ${CHECKSUM_FILE}: no trusted release signing key matched`);
280
+ }
281
+
282
+ function verifyArchiveChecksum(asset, archiveBuf, checksumText) {
283
+ const checksums = parseChecksums(checksumText);
284
+ const expected = checksums[asset];
285
+ if (!expected) {
286
+ throw new Error(`checksum entry for ${asset} was not found in ${CHECKSUM_FILE}`);
287
+ }
288
+ const actual = sha256(archiveBuf);
289
+ if (actual !== expected) {
290
+ throw new Error(`checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
291
+ }
292
+ }
293
+
294
+ function runTar(args, options = {}) {
295
+ const result = spawnSync('tar', args, { encoding: 'utf8', ...options });
296
+ if (result.error) {
297
+ throw result.error;
298
+ }
83
299
  if (result.status !== 0) {
84
- fail(`tar extraction failed (status ${result.status}). Is 'tar' installed?`);
300
+ const stderr = result.stderr ? `: ${result.stderr.trim()}` : '';
301
+ throw new Error(`tar ${args.join(' ')} failed (status ${result.status})${stderr}`);
302
+ }
303
+ return result.stdout || '';
304
+ }
305
+
306
+ function validateArchiveMemberName(member) {
307
+ if (member === 'orbit') {
308
+ return;
309
+ }
310
+ if (!member || member.startsWith('/') || member.includes('..') || path.isAbsolute(member)) {
311
+ throw new Error(`unsafe release archive member: ${member || '<empty>'}`);
312
+ }
313
+ throw new Error(`unexpected release archive member: ${member}`);
314
+ }
315
+
316
+ function validateArchiveMembers(archivePath) {
317
+ const members = runTar(['-tzf', archivePath])
318
+ .split(/\r?\n/)
319
+ .filter((line) => line.length > 0);
320
+ if (members.length !== 1) {
321
+ throw new Error("release archive must contain only the 'orbit' binary");
322
+ }
323
+ validateArchiveMemberName(members[0]);
324
+ }
325
+
326
+ function validateExtractedBinary(extractedBinary) {
327
+ const stat = fs.lstatSync(extractedBinary);
328
+ if (stat.isSymbolicLink()) {
329
+ throw new Error('release archive member is a symlink: orbit');
330
+ }
331
+ if (!stat.isFile()) {
332
+ throw new Error('release archive member must be a regular file: orbit');
333
+ }
334
+ if (stat.nlink !== 1) {
335
+ throw new Error('release archive member must not be a hard link: orbit');
85
336
  }
86
337
  }
87
338
 
339
+ function extractTarGz(archivePath, destDir) {
340
+ validateArchiveMembers(archivePath);
341
+ runTar(['-xzf', archivePath, '-C', destDir, 'orbit'], { stdio: 'inherit' });
342
+ validateExtractedBinary(path.join(destDir, 'orbit'));
343
+ }
344
+
88
345
  async function main() {
89
346
  if (process.env.ORBIT_SKIP_DOWNLOAD === '1') {
90
347
  log('ORBIT_SKIP_DOWNLOAD=1 set; skipping binary download.');
@@ -99,12 +356,23 @@ async function main() {
99
356
  const asset = `orbit-${target}.tar.gz`;
100
357
  const baseUrl = `https://github.com/${BINARY_REPO}/releases/download/${BINARY_VERSION}`;
101
358
  const archiveUrl = `${baseUrl}/${asset}`;
102
- const checksumUrl = `${baseUrl}/orbit-checksums.txt`;
359
+ const checksumUrl = `${baseUrl}/${CHECKSUM_FILE}`;
360
+ const checksumSignatureUrl = `${baseUrl}/${CHECKSUM_SIGNATURE_FILE}`;
103
361
 
104
362
  log(`installing orbit ${BINARY_VERSION} for ${target}...`);
105
363
 
106
364
  fs.mkdirSync(BIN_DIR, { recursive: true });
107
365
 
366
+ let checksumText;
367
+ let signatureBuf;
368
+ try {
369
+ checksumText = (await fetchBuffer(checksumUrl)).toString('utf8');
370
+ signatureBuf = await fetchBuffer(checksumSignatureUrl);
371
+ verifyChecksumSignature(checksumText, signatureBuf);
372
+ } catch (err) {
373
+ fail(`could not authenticate ${CHECKSUM_FILE}: ${err.message}`);
374
+ }
375
+
108
376
  let archiveBuf;
109
377
  try {
110
378
  archiveBuf = await fetchBuffer(archiveUrl);
@@ -113,35 +381,45 @@ async function main() {
113
381
  }
114
382
 
115
383
  try {
116
- const checksumText = (await fetchBuffer(checksumUrl)).toString('utf8');
117
- const checksums = parseChecksums(checksumText);
118
- const expected = checksums[asset];
119
- if (!expected) {
120
- fail(`checksum entry for ${asset} was not found in orbit-checksums.txt`);
121
- } else {
122
- const actual = sha256(archiveBuf);
123
- if (actual !== expected) {
124
- fail(`checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
125
- }
126
- }
384
+ verifyArchiveChecksum(asset, archiveBuf, checksumText);
127
385
  } catch (err) {
128
- fail(`could not verify checksum for ${asset}: ${err.message}`);
386
+ fail(err.message);
129
387
  }
130
388
 
131
389
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orbit-cli-'));
132
- const archivePath = path.join(tmpDir, asset);
133
- fs.writeFileSync(archivePath, archiveBuf);
134
- extractTarGz(archivePath, tmpDir);
390
+ try {
391
+ const archivePath = path.join(tmpDir, asset);
392
+ fs.writeFileSync(archivePath, archiveBuf);
393
+ extractTarGz(archivePath, tmpDir);
135
394
 
136
- const extractedBinary = path.join(tmpDir, 'orbit');
137
- if (!fs.existsSync(extractedBinary)) {
138
- fail(`extracted archive did not contain 'orbit' binary at ${extractedBinary}`);
395
+ const extractedBinary = path.join(tmpDir, 'orbit');
396
+ if (!fs.existsSync(extractedBinary)) {
397
+ fail(`extracted archive did not contain 'orbit' binary at ${extractedBinary}`);
398
+ }
399
+ fs.copyFileSync(extractedBinary, BIN_PATH);
400
+ fs.chmodSync(BIN_PATH, 0o755);
401
+ } finally {
402
+ fs.rmSync(tmpDir, { recursive: true, force: true });
139
403
  }
140
- fs.copyFileSync(extractedBinary, BIN_PATH);
141
- fs.chmodSync(BIN_PATH, 0o755);
142
- fs.rmSync(tmpDir, { recursive: true, force: true });
143
404
 
144
405
  log(`installed orbit binary at ${BIN_PATH}`);
145
406
  }
146
407
 
147
- main().catch((err) => fail(err && err.message ? err.message : String(err)));
408
+ if (require.main === module) {
409
+ main().catch((err) => fail(err && err.message ? err.message : String(err)));
410
+ }
411
+
412
+ module.exports = {
413
+ TRUSTED_RELEASE_KEYS,
414
+ parseChecksums,
415
+ sha256,
416
+ acknowledgeTrustedPublicKeyOverride,
417
+ acknowledgeTrustedKeysOverride,
418
+ extractTarGz,
419
+ normalizeTrustedReleaseKeys,
420
+ readTrustedReleaseKeys,
421
+ validateArchiveMembers,
422
+ validateExtractedBinary,
423
+ verifyArchiveChecksum,
424
+ verifyChecksumSignature,
425
+ };