tessl 0.89.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.
Files changed (2) hide show
  1. package/bin/tessl.js +144 -26
  2. 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/channels.ts
8
- async function fetchLatestVersion(channel) {
9
- const url = getChannelUrl(channel);
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 fetch version from ${url}: ${response.status} ${response.statusText}`);
22
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
13
23
  }
14
- const version = (await response.text()).trim();
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
- const baseUrl = process.env.TESSL_INSTALL_BASE_URL || "https://install.tessl.io";
31
- return `${baseUrl}/.well-known/tessl/${channel}.txt`;
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 `${baseUrl}/binaries/${encodedVersion}/tessl-${encodedVersion}-${platform}.tar.gz`;
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.89.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
- const tarballStream = await downloadTarball(url);
362
- const versionsDir = getVersionsDirectory();
363
- await extractTarball(tarballStream, versionsDir);
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("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tessl",
3
- "version": "0.89.0",
3
+ "version": "0.91.0",
4
4
  "description": "Tessl CLI",
5
5
  "author": "Tessl",
6
6
  "license": "SEE LICENSE.md",