tessl 0.90.0 → 0.91.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/bin/tessl.js +144 -26
- package/package.json +1 -1
package/bin/tessl.js
CHANGED
|
@@ -3,19 +3,67 @@
|
|
|
3
3
|
// installer/src/main.ts
|
|
4
4
|
import { existsSync as existsSync3, rmSync } from "node:fs";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
+
import { Readable as Readable2 } from "node:stream";
|
|
6
7
|
|
|
7
|
-
// installer/src/
|
|
8
|
-
async function
|
|
9
|
-
const
|
|
8
|
+
// installer/src/download.ts
|
|
9
|
+
async function downloadTarball(url) {
|
|
10
|
+
const response = await fetch(url);
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
throw new Error(`Failed to download binary: ${response.status} ${response.statusText}`);
|
|
13
|
+
}
|
|
14
|
+
if (!response.body) {
|
|
15
|
+
throw new Error("Response body is null");
|
|
16
|
+
}
|
|
17
|
+
return response.body;
|
|
18
|
+
}
|
|
19
|
+
async function downloadBytes(url, maxBytes = 512 * 1024 * 1024) {
|
|
10
20
|
const response = await fetch(url);
|
|
11
21
|
if (!response.ok) {
|
|
12
|
-
throw new Error(`Failed to
|
|
22
|
+
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
13
23
|
}
|
|
14
|
-
const
|
|
24
|
+
const advertised = Number(response.headers.get("content-length"));
|
|
25
|
+
if (Number.isFinite(advertised) && advertised > maxBytes) {
|
|
26
|
+
throw new Error(`Response from ${url} exceeds size limit (${advertised} > ${maxBytes} bytes)`);
|
|
27
|
+
}
|
|
28
|
+
if (!response.body) {
|
|
29
|
+
throw new Error(`Response from ${url} has no body`);
|
|
30
|
+
}
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let total = 0;
|
|
33
|
+
const reader = response.body.getReader();
|
|
34
|
+
for (;; ) {
|
|
35
|
+
const chunk = await reader.read();
|
|
36
|
+
if (chunk.done)
|
|
37
|
+
break;
|
|
38
|
+
total += chunk.value.byteLength;
|
|
39
|
+
if (total > maxBytes) {
|
|
40
|
+
await reader.cancel();
|
|
41
|
+
throw new Error(`Response from ${url} exceeds size limit (> ${maxBytes} bytes)`);
|
|
42
|
+
}
|
|
43
|
+
chunks.push(chunk.value);
|
|
44
|
+
}
|
|
45
|
+
return Buffer.concat(chunks);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// installer/src/channels.ts
|
|
49
|
+
var MAX_CHANNEL_RESPONSE_BYTES = 8 * 1024;
|
|
50
|
+
var VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.+-]{0,127}$/;
|
|
51
|
+
function assertPathSafeVersion(version, source) {
|
|
52
|
+
if (!VERSION_PATTERN.test(version)) {
|
|
53
|
+
const shown = version.length > 40 ? `${JSON.stringify(version.slice(0, 40))}… (${version.length} chars)` : JSON.stringify(version);
|
|
54
|
+
const where = source ? ` from ${source}` : "";
|
|
55
|
+
throw new Error(`Refusing to install: release channel returned a malformed version${where}: ${shown}.`);
|
|
56
|
+
}
|
|
57
|
+
return version;
|
|
58
|
+
}
|
|
59
|
+
async function fetchLatestVersion(channel) {
|
|
60
|
+
const url = getChannelUrl(channel);
|
|
61
|
+
const bytes = await downloadBytes(url, MAX_CHANNEL_RESPONSE_BYTES);
|
|
62
|
+
const version = bytes.toString("utf8").trim();
|
|
15
63
|
if (!version) {
|
|
16
64
|
throw new Error(`Empty version returned from ${url}`);
|
|
17
65
|
}
|
|
18
|
-
return version;
|
|
66
|
+
return assertPathSafeVersion(version, url);
|
|
19
67
|
}
|
|
20
68
|
function getChannelFromVersion(version) {
|
|
21
69
|
if (version.includes("head"))
|
|
@@ -26,26 +74,24 @@ function getChannelFromVersion(version) {
|
|
|
26
74
|
return "beta";
|
|
27
75
|
return "latest";
|
|
28
76
|
}
|
|
77
|
+
function getBaseUrl() {
|
|
78
|
+
return process.env.TESSL_INSTALL_BASE_URL || "https://install.tessl.io";
|
|
79
|
+
}
|
|
29
80
|
function getChannelUrl(channel) {
|
|
30
|
-
|
|
31
|
-
|
|
81
|
+
return `${getBaseUrl()}/.well-known/tessl/${channel}.txt`;
|
|
82
|
+
}
|
|
83
|
+
function getManifestUrl(version) {
|
|
84
|
+
return `${getBaseUrl()}/binaries/${encodeURIComponent(version)}/SHA256SUMS`;
|
|
85
|
+
}
|
|
86
|
+
function getManifestSigUrl(version) {
|
|
87
|
+
return `${getBaseUrl()}/binaries/${encodeURIComponent(version)}/SHA256SUMS.sig`;
|
|
88
|
+
}
|
|
89
|
+
function getBinaryFileName(version, platform) {
|
|
90
|
+
return `tessl-${version}-${platform}.tar.gz`;
|
|
32
91
|
}
|
|
33
92
|
function getBinaryUrl(version, platform) {
|
|
34
|
-
const baseUrl = process.env.TESSL_INSTALL_BASE_URL || "https://install.tessl.io";
|
|
35
93
|
const encodedVersion = encodeURIComponent(version);
|
|
36
|
-
return `${
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// installer/src/download.ts
|
|
40
|
-
async function downloadTarball(url) {
|
|
41
|
-
const response = await fetch(url);
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
throw new Error(`Failed to download binary: ${response.status} ${response.statusText}`);
|
|
44
|
-
}
|
|
45
|
-
if (!response.body) {
|
|
46
|
-
throw new Error("Response body is null");
|
|
47
|
-
}
|
|
48
|
-
return response.body;
|
|
94
|
+
return `${getBaseUrl()}/binaries/${encodedVersion}/${getBinaryFileName(encodedVersion, platform)}`;
|
|
49
95
|
}
|
|
50
96
|
|
|
51
97
|
// installer/src/extract.ts
|
|
@@ -75,6 +121,19 @@ async function extractTarball(stream, destination) {
|
|
|
75
121
|
await tarExit;
|
|
76
122
|
}
|
|
77
123
|
|
|
124
|
+
// installer/src/keys.ts
|
|
125
|
+
var RELEASE_SIGNING_PUBLIC_KEYS = [
|
|
126
|
+
{
|
|
127
|
+
id: "kms-2026-06",
|
|
128
|
+
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
129
|
+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE81DxbddrC8wywZa2W0R+dq5q7xsB
|
|
130
|
+
TaNvYdQeUrdR5LGZIaYN4XziIK27qwZEl4xJIEB+sqgsx2limHUjf/OKwA==
|
|
131
|
+
-----END PUBLIC KEY-----
|
|
132
|
+
`
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
var RELEASE_VERIFICATION_ENABLED = false;
|
|
136
|
+
|
|
78
137
|
// installer/src/path.ts
|
|
79
138
|
import { existsSync } from "node:fs";
|
|
80
139
|
import {
|
|
@@ -299,10 +358,65 @@ function validateBinary(binaryPath) {
|
|
|
299
358
|
}
|
|
300
359
|
}
|
|
301
360
|
|
|
361
|
+
// installer/src/verify.ts
|
|
362
|
+
import { createHash, verify } from "node:crypto";
|
|
363
|
+
function parseManifest(manifest) {
|
|
364
|
+
const map = new Map;
|
|
365
|
+
for (const line of manifest.split(`
|
|
366
|
+
`)) {
|
|
367
|
+
const trimmed = line.trim();
|
|
368
|
+
if (!trimmed)
|
|
369
|
+
continue;
|
|
370
|
+
const match = trimmed.match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
|
|
371
|
+
if (!match?.[1] || !match[2])
|
|
372
|
+
continue;
|
|
373
|
+
map.set(match[2], match[1].toLowerCase());
|
|
374
|
+
}
|
|
375
|
+
return map;
|
|
376
|
+
}
|
|
377
|
+
function expectedHashFor(manifest, filename) {
|
|
378
|
+
const hash = parseManifest(manifest).get(filename);
|
|
379
|
+
if (!hash) {
|
|
380
|
+
throw new Error(`No checksum for ${filename} in SHA256SUMS manifest`);
|
|
381
|
+
}
|
|
382
|
+
return hash;
|
|
383
|
+
}
|
|
384
|
+
function verifyManifestSignature(manifest, signature, publicKeyPem) {
|
|
385
|
+
try {
|
|
386
|
+
return verify("sha256", manifest, publicKeyPem, signature);
|
|
387
|
+
} catch {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function verifyManifestSignatureWithKeys(manifest, signature, trustedKeys) {
|
|
392
|
+
return trustedKeys.some((key) => verifyManifestSignature(manifest, signature, key.publicKeyPem));
|
|
393
|
+
}
|
|
394
|
+
function sha256Hex(data) {
|
|
395
|
+
return createHash("sha256").update(data).digest("hex");
|
|
396
|
+
}
|
|
397
|
+
|
|
302
398
|
// installer/src/main.ts
|
|
303
399
|
function isInteractive() {
|
|
304
400
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
305
401
|
}
|
|
402
|
+
async function downloadVerifyAndExtract(version, platform, tarballUrl, versionsDir) {
|
|
403
|
+
const fileName = getBinaryFileName(version, platform);
|
|
404
|
+
const [tarballBytes, manifestBytes, manifestSig] = await Promise.all([
|
|
405
|
+
downloadBytes(tarballUrl),
|
|
406
|
+
downloadBytes(getManifestUrl(version), 2 * 1024 * 1024),
|
|
407
|
+
downloadBytes(getManifestSigUrl(version), 64 * 1024)
|
|
408
|
+
]);
|
|
409
|
+
if (!verifyManifestSignatureWithKeys(manifestBytes, manifestSig, RELEASE_SIGNING_PUBLIC_KEYS)) {
|
|
410
|
+
throw new Error("SHA256SUMS signature verification failed — refusing to install. " + "The download may be corrupt or tampered with.");
|
|
411
|
+
}
|
|
412
|
+
const expected = expectedHashFor(manifestBytes.toString("utf8"), fileName);
|
|
413
|
+
const actual = sha256Hex(tarballBytes);
|
|
414
|
+
if (actual !== expected) {
|
|
415
|
+
throw new Error(`Checksum mismatch for ${fileName} (expected ${expected}, got ${actual}) — refusing to install.`);
|
|
416
|
+
}
|
|
417
|
+
const verifiedStream = Readable2.toWeb(Readable2.from(tarballBytes));
|
|
418
|
+
await extractTarball(verifiedStream, versionsDir);
|
|
419
|
+
}
|
|
306
420
|
async function promptConfirmation(message) {
|
|
307
421
|
return new Promise((resolve2) => {
|
|
308
422
|
const rl = createInterface({
|
|
@@ -327,7 +441,7 @@ function printUnsupportedPlatformMessage() {
|
|
|
327
441
|
async function prepareBinary() {
|
|
328
442
|
const tesslBinPath = getTesslBinPath();
|
|
329
443
|
const existingBinary = await getExistingBinaryPath(tesslBinPath);
|
|
330
|
-
const installerVersion = "0.
|
|
444
|
+
const installerVersion = "0.91.0";
|
|
331
445
|
const isTestOrDevBuild = installerVersion.includes("test") || installerVersion.includes("dev");
|
|
332
446
|
let versionMismatch = false;
|
|
333
447
|
if (isTestOrDevBuild && existingBinary) {
|
|
@@ -356,11 +470,15 @@ async function prepareBinary() {
|
|
|
356
470
|
}
|
|
357
471
|
console.log(`Downloading ${version} for ${platform}`);
|
|
358
472
|
const url = getBinaryUrl(version, platform);
|
|
473
|
+
const versionsDir = getVersionsDirectory();
|
|
359
474
|
const progressInterval = setInterval(() => process.stdout.write("."), 500).unref();
|
|
360
475
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
476
|
+
if (RELEASE_VERIFICATION_ENABLED) {
|
|
477
|
+
await downloadVerifyAndExtract(version, platform, url, versionsDir);
|
|
478
|
+
} else {
|
|
479
|
+
const tarballStream = await downloadTarball(url);
|
|
480
|
+
await extractTarball(tarballStream, versionsDir);
|
|
481
|
+
}
|
|
364
482
|
} finally {
|
|
365
483
|
clearInterval(progressInterval);
|
|
366
484
|
console.log("");
|