First run now lands on a step-by-step guide instead of the overview: create a Sia Storage account (link out, Pithos never sees the password), install s3d, choose or generate the recovery phrase, connect, start the gateway and create the first access key, with Back/Next on every step. The connect step opens the indexer's approval page on its own and moves on as soon as the approval lands; where a browser blocks the pop-up it says so and makes the button the primary action. Hosts without node-pty (the Theseus add-on) now fetch the one platform package they need from the npm registry on first login, pinned to its published sha512 integrity, so the approval link can be read there too. The console-window login stays only as the fallback when that fails. A retry after a failed connect with an existing phrase goes back to the phrase step: the phrase is not kept, and a blank one would make s3d generate a new one.
98 lines
4.5 KiB
JavaScript
98 lines
4.5 KiB
JavaScript
// 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;
|
|
}
|
|
}
|