// The pseudo-terminal that `s3d login` needs (it reads the recovery phrase // with term.ReadPassword, which fails on a plain pipe). // // Hosts that ship node_modules (web console, desktop) have @lydell/node-pty // installed. The Theseus add-on leaves it out (the Windows build is 12 MB of // native code), so on first use it fetches the one platform package it needs // from the npm registry, checks it against the integrity hash pinned below, // and unpacks it into the add-on's data folder. import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import zlib from 'node:zlib'; import { pathToFileURL } from 'node:url'; const VERSION = '1.2.0-beta.15'; // dist.integrity from the npm registry for each platform package. const PACKAGES = { 'win32-x64': 'sha512-2f8twEmDVxZ7drchAXjtevpmSPhFok0avAnzXro4t5gmz0xsPNKkoZvymwtuIS3xo7PzQqZOPQ/YzwEMb7oIzQ==', 'win32-arm64': 'sha512-pyAk91w7wnnKrD4mrHXtIXRfmzSWV5bEzvRhurXcMCtCc2TJ424ciUskIgWMhAPP6y3KyUnqElj+U6kY3iOt0A==', 'darwin-x64': 'sha512-yDT2oqPqYMBScyuk1U9Rg5VKcrbMOD9o9jWYYamDADA3NSbUISroPChrqYRQ74Y7BQtNH4gqYAiWOZRi5uQZ0Q==', 'darwin-arm64': 'sha512-6TSBbzdcLiNTHl1mTuzflqXrkmcC36USVGvERoDgvHk2ItEDaMaFZuAJ1CqPmwYj0DyhCS16TVS8OGK9xZnjyQ==', 'linux-x64': 'sha512-+U/5AVvHT6W+8OCYcnJgN0Qgc0ycO3TfD6aaFJHK+WHij797f8gsi5dV1HEO9l6YQmWCD+VL5gaLDhx3mxHwCA==', 'linux-arm64': 'sha512-wkbNF7dYAmtJv+o2+iztVlNwnUB4B0uX0wh/UD+mwMcmE2gNMnW9GChXO7fEE5XJokD0vB5idiHpGegaN+G/sg==', }; let cached; // Returns the pty module, or null when none is available and none can be // installed. installDir: where a downloaded package lives (null = never download). export async function loadPty({ installDir, onProgress = () => {} } = {}) { if (cached) return cached; try { const m = await import('@lydell/node-pty'); return (cached = m.default || m); } catch { /* not shipped with this host */ } if (!installDir) return null; const plat = `${process.platform}-${process.arch}`; if (!PACKAGES[plat]) return null; const pkgDir = path.join(installDir, `node-pty-${plat}-${VERSION}`); const entry = path.join(pkgDir, 'lib', 'index.js'); if (!fs.existsSync(entry)) await install(plat, pkgDir, onProgress); const m = await import(pathToFileURL(entry).href); return (cached = m.default || m); } async function install(plat, pkgDir, onProgress) { const name = `node-pty-${plat}`; const url = `https://registry.npmjs.org/@lydell/${name}/-/${name}-${VERSION}.tgz`; onProgress({ phase: 'download', what: 'terminal support', url }); const res = await fetch(url); if (!res.ok) throw new Error(`could not download terminal support: HTTP ${res.status}`); const tgz = Buffer.from(await res.arrayBuffer()); const want = PACKAGES[plat]; const got = 'sha512-' + crypto.createHash('sha512').update(tgz).digest('base64'); if (got !== want) throw new Error('terminal support package failed its integrity check'); onProgress({ phase: 'extract', what: 'terminal support' }); const tmp = pkgDir + '.tmp'; fs.rmSync(tmp, { recursive: true, force: true }); for (const { name: file, data } of untar(zlib.gunzipSync(tgz))) { // npm tarballs put everything under package/. const rel = file.replace(/^package\//, ''); if (!rel || rel === file) continue; const out = path.join(tmp, rel); if (!out.startsWith(tmp + path.sep)) throw new Error('unsafe path in terminal support package'); fs.mkdirSync(path.dirname(out), { recursive: true }); fs.writeFileSync(out, data); } fs.rmSync(pkgDir, { recursive: true, force: true }); fs.renameSync(tmp, pkgDir); onProgress({ phase: 'done', what: 'terminal support' }); } // Minimal ustar reader: regular files only, with pax "path" overrides. function* untar(buf) { let off = 0; let paxPath = null; while (off + 512 <= buf.length) { const h = buf.subarray(off, off + 512); if (h.every((b) => b === 0)) break; const str = (a, b) => h.toString('utf8', a, b).replace(/\0.*$/s, ''); let name = str(0, 100); const prefix = str(345, 500); if (prefix) name = prefix + '/' + name; const size = parseInt(str(124, 136).trim() || '0', 8); const type = String.fromCharCode(h[156] || 48); const data = buf.subarray(off + 512, off + 512 + size); off += 512 + Math.ceil(size / 512) * 512; if (type === 'x') { const m = /\d+ path=([^\n]+)\n/.exec(data.toString('utf8')); paxPath = m ? m[1] : null; continue; } if (type === '0' || type === '\0') yield { name: paxPath || name, data: Buffer.from(data) }; paxPath = null; } }