gitoria
All repositories: gitoria
22.9 KB
// tests/ssh.mjs — THE GATE OF GIT OVER SSH (ticket gitoria#7): the REAL sshd container (docker/sshd, built here) + the real ssh and git clients// against this app; keys pasted in Chrome. (tests/push.mjs is the HTTPS gate; this is a copy of its set-up.)// was: tests/push.mjs — THE GATE OF PUSH AND PULL (ticket gitoria#7). Its own gitoria server + its own ident (copy, mail sink),// a real Chrome for everything visible (create a repo, the "add code" box, access tokens), and the REAL `git` binary for// clone / push / fetch over HTTP against the repo address <slug>.gitoria.test (git's http.curloptResolve maps it to 127.0.0.1).// node tests/push.mjs (GITORIA_GATE_PORT 8700, GITORIA_GATE_IDENT_PORT 8701, GITORIA_GATE_CHROME "8703-8707")import { spawn, spawnSync } from 'node:child_process';import { request as httpRequest, createServer } from 'node:http';import { rmSync, mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';import { dirname, join, resolve } from 'node:path';import { fileURLToPath } from 'node:url';import { randomBytes } from 'node:crypto';import { launchBrowser } from './cdp.mjs';import { startIdent } from './identkit.mjs';if (!process.env.HL_CHROME && existsSync('/opt/google/chrome/chrome')) process.env.HL_CHROME = '/opt/google/chrome/chrome';const HERE = dirname(fileURLToPath(import.meta.url));const APP = resolve(HERE, '..');const BIN = join(APP, 'bin/hybriel');const PORT = Number(process.env.GITORIA_GATE_PORT || 8700);const IDENT_PORT = Number(process.env.GITORIA_GATE_IDENT_PORT || 8701);const PROXY = Number(process.env.GITORIA_GATE_PROXY || 8702);const SSHPORT = Number(process.env.GITORIA_GATE_SSH_PORT || 8708);const SECRET = 'gate-secret-' + Math.random().toString(16).slice(2);const CONTAINER = 'gitoria-sshd-gate'; // stands in for nginx: buffers a chunked request body and sends it with a lengthconst [CH_FROM, CH_TO] = (process.env.GITORIA_GATE_CHROME || '8703-8707').split('-').map(Number);const API = `http://127.0.0.1:${PORT}`;const BASE = `http://gitoria.test:${PORT}`;const IDENT_B = `http://ident.gitoria.test:${IDENT_PORT}`;const REPO = (slug) => `http://${slug}.gitoria.test:${PORT}`;const hreq = (method, path, host, headers = {}, body = null) => new Promise((res, rej) => {const rq = httpRequest({ host: '127.0.0.1', port: PORT, path, method, headers: { host, ...headers } }, (r) => { let b = ''; r.setEncoding('utf8'); r.on('data', d => b += d); r.on('end', () => res({ status: r.statusCode, headers: r.headers, body: b })); });rq.on('error', rej); if (body) { rq.setHeader('content-length', Buffer.byteLength(body)); rq.write(body); } rq.end();});process.env.HL_CHROME_ARGS = '--host-resolver-rules=MAP *.gitoria.test 127.0.0.1, MAP gitoria.test 127.0.0.1';const SCRATCH = join(APP, '.scratch');const STORE = join(SCRATCH, 'ssh-store');const WORK = join(STORE, 'work');const IDENT_DIR = process.env.GITORIA_GATE_IDENT_DIR || [resolve(APP, '../ident.worldapi.org'), '/media/STORAGE/projects/ident.worldapi.org'].find(d => existsSync(join(d, 'project.hl')));rmSync(STORE, { recursive: true, force: true });mkdirSync(WORK, { recursive: true });let failures = 0, passes = 0;function check(label, ok, detail = '') {console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${ok ? '' : ' — ' + detail}`);if (ok) passes++; else failures++;}const sleep = (ms) => new Promise(r => setTimeout(r, ms));const J = JSON.stringify;let pageA = null;let log = '', server = null, ident = null, proxy = null;function startProxy() {proxy = createServer((req, res) => {const parts = []; req.on('data', d => parts.push(d));req.on('end', () => {const body = Buffer.concat(parts);const headers = { ...req.headers }; delete headers['transfer-encoding']; headers['content-length'] = String(body.length);const up = httpRequest({ host: '127.0.0.1', port: PORT, path: req.url, method: req.method, headers }, (r) => { res.writeHead(r.statusCode, r.headers); r.pipe(res); });up.on('error', () => { res.writeHead(502); res.end(); });up.end(body);});});proxy.listen(PROXY, '127.0.0.1');}const browsers = [];function startServer(extraEnv = {}) {server = spawn(BIN, ['project.hl'], {cwd: APP,env: { ...process.env, GITORIA_PORT: String(PORT), GITORIA_STORAGE: join(STORE, 'mpackdb'), GITORIA_SESSIONS: join(STORE, 'sessions') + '/', GITORIA_WATCH: '0', GITORIA_GIT: join(STORE, 'git'),GITORIA_PUBLIC_URL: BASE, IDENT_URL: IDENT_B, IDENT_EXCHANGE_URL: ident.base, GITORIA_TICKETS_URL: 'http://127.0.0.1:1', ...extraEnv },stdio: ['ignore', 'pipe', 'pipe'],});server.stdout.on('data', d => log += d); server.stderr.on('data', d => log += d);}async function serverUp() {for (let i = 0; i < 80; i++) { try { const r = await fetch(API + '/api/repos'); if (r.ok) return; } catch {} await sleep(250); }throw new Error('gitoria did not come up\n' + log);}async function stopServer() {if (!server) return;try { server.kill('SIGTERM'); } catch {}await new Promise(r => { if (server.exitCode !== null || server.signalCode !== null) return r(); server.once('exit', r); setTimeout(r, 3000); });server = null;}function patient(page) {const waitFor = page.waitFor.bind(page);page.waitFor = (expr, o = {}) => waitFor(expr, { ...o, timeout: (o.timeout || 10000) * 3 });const goto = page.goto.bind(page);page.goto = (url, o = {}) => goto(url, { ...o, timeout: (o.timeout || 15000) * 3 });return page;}const hydrated = (page) => page.waitFor('!!window.__hl && window.__hl.socket && window.__hl.socket.readyState === 1', { label: 'page hydrated' });const txt = (page, sel) => page.evaluate(`(document.querySelector(${J(sel)}) || {}).textContent || null`);const has = (page, sel) => page.evaluate(`!!document.querySelector(${J(sel)})`);const noOverflow = (page) => page.evaluate('document.documentElement.scrollWidth <= window.innerWidth');// the real git client. HOST = the repo address mapped to 127.0.0.1; credentials in the URL when givenconst GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1', HOME: WORK, LC_ALL: 'C' };function git(cwd, args, slug, extra = {}) {const pre = slug ? ['-c', `http.curloptResolve=${slug}.gitoria.test:${extra.port || PROXY}:127.0.0.1`] : [];const { port: _p, ...envExtra } = extra;// async: the proxy of this gate lives in this process, a blocking spawnSync would starve itreturn new Promise((res) => {const c = spawn('git', [...pre, '-c', 'user.name=Gate', '-c', '[email protected]', '-c', 'init.defaultBranch=main', ...args], { cwd, env: { ...GIT_ENV, ...envExtra } });let out = ''; c.stdout.on('data', d => out += d); c.stderr.on('data', d => out += d);const t = setTimeout(() => c.kill('SIGKILL'), 120000);c.on('close', (code) => { clearTimeout(t); res({ code, out }); });});}const urlOf = (slug, user, pass, port = PROXY) => `http://${user ? `${user}:${pass}@` : ''}${slug}.gitoria.test:${port}/${slug}.git`;const KEYDIR = join(STORE, 'keys');mkdirSync(KEYDIR, { recursive: true });const newKey = (name, type = 'ed25519', extra = []) => {spawnSync('ssh-keygen', ['-q', '-t', type, ...extra, '-N', '', '-C', name + '@gate', '-f', join(KEYDIR, name)]);return { file: join(KEYDIR, name), pub: readFileSync(join(KEYDIR, name + '.pub'), 'utf8').trim() };};const sshCmd = (key) => `ssh -i ${key.file} -p ${SSHPORT} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR`;const sgit = (cwd, args, key) => git(cwd, args, null, { GIT_SSH_COMMAND: sshCmd(key) });const sshRaw = (key, cmd) => new Promise((res) => {const c = spawn('sh', ['-c', `${sshCmd(key)} [email protected] ${cmd}`]);let out = ''; c.stdout.on('data', d => out += d); c.stderr.on('data', d => out += d);const t = setTimeout(() => c.kill('SIGKILL'), 30000);c.on('close', (code) => { clearTimeout(t); res({ code, out }); });});const SURL = (slug) => `ssh://[email protected]:${SSHPORT}/${slug}.git`;async function addKeyInChrome(A, name, pub) {await A.evaluate(`(() => { const n = document.querySelector('#keyname'); n.value = ${J(name)}; n.dispatchEvent(new Event('input', { bubbles: true })); const t = document.querySelector('#keytext'); t.value = ${J(pub)}; t.dispatchEvent(new Event('input', { bubbles: true })); })()`);await A.click('#keysave');}try {ident = await startIdent({ identDir: IDENT_DIR, workDir: join(STORE, 'ident'), port: IDENT_PORT });const alice = await ident.signIn('[email protected]');const bob = await ident.signIn('[email protected]');const APPKEY = await ident.registerApp(alice, 'gitoria (ssh gate)', [BASE]);startServer({ IDENT_API_KEY: APPKEY.key, IDENT_API_SECRET: APPKEY.secret, GITORIA_SSH_SECRET: SECRET, GITORIA_SSH_PORT: String(SSHPORT) });await serverUp();// the sshd container: host network (reaches the app on 127.0.0.1), repos = this store's git foldermkdirSync(join(STORE, 'git'), { recursive: true });spawnSync('docker', ['rm', '-f', CONTAINER]);const built = spawnSync('docker', ['build', '-q', '-t', 'gitoria-sshd-gate', join(APP, 'docker/sshd')], { encoding: 'utf8' });check('sshd image builds', built.status === 0, built.stderr);const started = spawnSync('docker', ['run', '-d', '--rm', '--name', CONTAINER, '--network', 'host', '-e', 'GITORIA_SSH_SECRET=' + SECRET, '-e', 'GITORIA_SSH_PORT=' + SSHPORT, '-e', 'GITORIA_URL=' + API, '-v', join(STORE, 'git') + ':/repos', 'gitoria-sshd-gate'], { encoding: 'utf8' });check('sshd container starts', started.status === 0, started.stderr);for (let i = 0; i < 40; i++) { const r = spawnSync('sh', ['-c', `ssh-keyscan -p ${SSHPORT} 127.0.0.1 2>/dev/null | grep -c ssh-`], { encoding: 'utf8' }); if (Number(r.stdout) > 0) break; await sleep(250); }// ---- the internal endpoints: only with the secret, never through the proxy ------------------------------------const H = `gitoria.test:${PORT}`;check('internal: /__git/keys without the secret is 403', (await hreq('GET', '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', H)).status === 403);check('internal: /__git/keys with a wrong secret is 403', (await hreq('GET', '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', H, { 'x-gitoria-secret': 'nope' })).status === 403);check('internal: with the secret but through a proxy (X-Forwarded-For) is 403', (await hreq('GET', '/__git/access?user=x&slug=y&write=0', H, { 'x-gitoria-secret': SECRET, 'x-forwarded-for': '1.2.3.4' })).status === 403);check('internal: an unknown key gets an empty answer', (await hreq('GET', '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', H, { 'x-gitoria-secret': SECRET })).body === '');// ---- alice in Chrome: log in, name, create a repo, add keys -----------------------------------------------------const b = await launchBrowser({ debugPortRange: [CH_FROM, CH_TO] });browsers.push(b);const A = patient(await b.newPage());pageA = A;const [ck, cv] = alice.cookie.split('=');await A.send('Network.enable');await A.send('Network.setCookie', { name: ck, value: cv, url: IDENT_B + '/' });await A.goto(BASE + '/');await A.waitForSelector('#loginbutton');await hydrated(A);check('keys: signed out, the main page offers no key form', !(await has(A, '#keyform')));await A.click('#loginbutton');await A.waitForSelector('#chooselist', { timeout: 10000 });await hydrated(A);await A.click('#chooselist li:nth-child(1) .choose');await A.waitForSelector('#nameform');await hydrated(A);await A.type('#displayname', 'alice');await A.click('#namesave');await A.waitFor('!document.querySelector("#nameform") && !!document.querySelector("#createform")', { label: 'named' });await A.type('#slug', 'gamma');await A.click('#createsave');await A.waitForSelector('#created');await A.waitFor('!!document.querySelector("#keyform")', { label: 'key form' });check('keys: logged in, "You have no SSH keys yet"', await has(A, '#nokeys'));const k1 = newKey('laptop');await addKeyInChrome(A, 'laptop', 'this is not a key');await A.waitFor('document.querySelector("#keyerror").textContent.length > 0', { label: 'error' });check('keys: text that is not a key is refused with a reason', /not a public key/.test(await txt(A, '#keyerror')), await txt(A, '#keyerror'));await addKeyInChrome(A, 'laptop', readFileSync(k1.file, 'utf8').split('\n').slice(0, 2).join('\n'));await A.waitFor('/PRIVATE/.test(document.querySelector("#keyerror").textContent)', { label: 'private key error' });check('keys: a PRIVATE key is refused and never stored', true);await addKeyInChrome(A, 'laptop', 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIabc broken');await A.waitFor('/not valid/.test(document.querySelector("#keyerror").textContent)', { label: 'invalid error' });check('keys: a key with a broken body is refused', true);const weak = newKey('weak', 'rsa', ['-b', '1024']);await addKeyInChrome(A, 'weak', weak.pub);await A.waitFor('/2048|not valid/.test(document.querySelector("#keyerror").textContent)', { label: 'weak error' });check('keys: a 1024-bit RSA key is refused', true);await addKeyInChrome(A, 'laptop', k1.pub);await A.waitFor('document.querySelectorAll("#keylist li").length === 1', { label: 'key added' });await sleep(400);check('keys: the key is added and listed with name and SHA256 fingerprint', /laptop/.test(await txt(A, '#keylist')) && /SHA256:/.test(await txt(A, '#keylist')) && !(await txt(A, '#keyerror')), (await txt(A, '#keylist')) + ' / error: ' + (await txt(A, '#keyerror')));const fp = spawnSync('ssh-keygen', ['-l', '-f', k1.file + '.pub'], { encoding: 'utf8' }).stdout.split(' ')[1];check('keys: the shown fingerprint is the real one', (await txt(A, '#keylist')).includes(fp), fp);await addKeyInChrome(A, 'again', k1.pub);await A.waitFor('/already/.test(document.querySelector("#keyerror").textContent)', { label: 'dup' });check('keys: the same key twice is refused', true);const rsa = newKey('rsa', 'rsa', ['-b', '3072']);await A.evaluate('document.querySelector("#keyerror").textContent = ""');await addKeyInChrome(A, 'old-rsa', rsa.pub);await A.waitFor('document.querySelectorAll("#keylist li").length === 2', { label: 'rsa added' });check('keys: a 3072-bit RSA key is accepted', true);// ---- the box on the (empty) repo's Code page (gitoria#20) ---------------------------------------------------------await A.goto(REPO('gamma') + '/code');await A.waitFor('!!document.querySelector("#addcode")');check('box: the ssh clone command is on the repo page', (await txt(A, '#sshclonecommand')) === `git clone ssh://[email protected]:${SSHPORT}/gamma.git`, await txt(A, '#sshclonecommand'));check('box: it links to the SSH keys on the main address', (await A.evaluate('document.querySelector("#tosshkeys").getAttribute("href")')) === BASE + '/#sshkeys');// ---- real ssh + git ------------------------------------------------------------------------------------------------const W = (n) => join(WORK, n);let g = await sgit(WORK, ['clone', SURL('gamma'), 'first'], k1);check('ssh git: cloning the empty repo works', g.code === 0 && /empty repository/.test(g.out), g.out);writeFileSync(join(W('first'), 'README.md'), '# gamma\n\nPushed over **ssh**.\n');writeFileSync(join(W('first'), 'big.bin'), randomBytes(3000000));await git(W('first'), ['add', '.']); await git(W('first'), ['commit', '-qm', 'first over ssh']);g = await sgit(W('first'), ['push', 'origin', 'main'], k1);check('ssh git: the owner pushes (3 MB) with the key', g.code === 0 && /main -> main/.test(g.out), g.out);g = await sgit(WORK, ['clone', SURL('gamma'), 'second'], rsa);check('ssh git: the owner\'s RSA key clones the pushed commit', g.code === 0 && readFileSync(join(W('second'), 'big.bin')).equals(readFileSync(join(W('first'), 'big.bin'))), g.out);writeFileSync(join(W('second'), 'more.txt'), 'more\n'); await git(W('second'), ['add', '.']); await git(W('second'), ['commit', '-qm', 'second']);g = await sgit(W('second'), ['push', 'origin', 'main'], rsa);check('ssh git: a push with the second key (fast-forward) works', g.code === 0, g.out);g = await sgit(W('first'), ['pull', '--no-rebase', '--no-edit', 'origin', 'main'], k1);check('ssh git: pull brings the new commit', g.code === 0 && existsSync(join(W('first'), 'more.txt')), g.out);await A.goto(REPO('gamma') + '/');await A.waitFor('/Pushed over/.test((document.querySelector("#homepage") || {}).textContent || "")', { label: 'readme after ssh push' });check('pushed code: the Readme page shows what was pushed over ssh', true);const bare = await git(WORK, ['--git-dir', join(STORE, 'git', 'gamma.git'), 'log', '--format=%s']);check('pushed code: the bare repo holds both commits', bare.out.trim().split('\n').join(',') === 'second,first over ssh', bare.out);// ---- what must NOT work ------------------------------------------------------------------------------------------const stranger = newKey('stranger');g = await sgit(WORK, ['clone', SURL('gamma'), 'nokey'], stranger);check('ssh git: a key nobody added is refused', g.code !== 0 && /Permission denied|publickey/.test(g.out), g.out);let r = await sshRaw(k1, 'ls /');check('ssh: no shell — any other command is refused', r.code !== 0 && /only git clone, fetch and push/.test(r.out), J(r));r = await sshRaw(k1, '');check('ssh: an interactive login is refused too', r.code !== 0 && !/repos|bin/.test(r.out), J(r));r = await sshRaw(k1, "\"git-upload-pack '../../etc'\"");check('ssh: a path outside the repos is refused', r.code !== 0 && /no such repository/.test(r.out), J(r));r = await sshRaw(k1, "\"git-upload-pack 'nope.git'\"");check('ssh: an unknown repo is refused', r.code !== 0 && /no such repository/.test(r.out), J(r));r = await sshRaw(k1, "\"git-upload-pack 'gamma.git; id'\"");check('ssh: a command smuggled behind the repo name is refused', r.code !== 0 && !/uid=/.test(r.out), J(r));const fwd = spawn('sh', ['-c', `${sshCmd(k1)} -o ExitOnForwardFailure=no -L 8709:127.0.0.1:${PORT} -N [email protected]`]);await sleep(1500);const viaTunnel = spawnSync('curl', ['-s', '-m', '5', '-o', '/dev/null', '-w', '%{http_code}', 'http://127.0.0.1:8709/api/repos'], { encoding: 'utf8' }).stdout;fwd.kill();check('ssh: no port forwarding through a key', viaTunnel !== '200', 'tunnel answered ' + viaTunnel);// bob: a key of his own may read but not push to alice's repoconst bobCode = await ident.selectorCode(bob, APPKEY, BASE);const bl = await fetch(API + '/login/callback?ident_code=' + bobCode, { redirect: 'manual' });const bobCookie = (bl.headers.get('set-cookie') || '').split(';')[0];await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', cookie: bobCookie }, body: J({ t: 'emit', i: 1, event: 'gitoriaSaveName', payload: ['bob'] }) });const bobKey = newKey('bob');const bk = await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', cookie: bobCookie }, body: J({ t: 'emit', i: 2, event: 'gitoriaAddKey', payload: ['bobs', bobKey.pub] }) });const bkt = await bk.text();check('keys: bob adds his own key (a face, own session)', !!(JSON.parse(bkt).value || {}).row, bkt);g = await sgit(WORK, ['clone', SURL('gamma'), 'bobs'], bobKey);check('ssh git: another user\'s key may read (public repo)', g.code === 0 && existsSync(join(W('bobs'), 'more.txt')), g.out);writeFileSync(join(W('bobs'), 'evil.txt'), 'x\n'); await git(W('bobs'), ['add', '.']); await git(W('bobs'), ['commit', '-qm', 'evil']);g = await sgit(W('bobs'), ['push', 'origin', 'main'], bobKey);check('ssh git: another user\'s key may not push — only the owner may', g.code !== 0 && /only the owner/.test(g.out), g.out);const bd = await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', cookie: bobCookie }, body: J({ t: 'emit', i: 3, event: 'gitoriaAddKey', payload: ['stolen', k1.pub] }) });const bdt = await bd.text();check('keys: alice\'s key cannot be added again by bob', /already/.test(bdt), bdt);const fk = await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json' }, body: J({ t: 'emit', i: 4, event: 'gitoriaAddKey', payload: ['x', stranger.pub, { user: { id: 'y' } }] }) });const fkt = await fk.text();const fkr = await sgit(WORK, ['ls-remote', SURL('gamma')], stranger);check('keys: a forged session argument adds no key', fkr.code !== 0, fkt);// remove a key: stops at once; the other one still worksawait A.goto(BASE + '/');await A.waitFor('document.querySelectorAll("#keylist li").length === 2', { label: 'keys after reload' });await hydrated(A);check('keys: the list survives a reload', (await txt(A, '#keylist')).includes('laptop'));await A.evaluate('document.querySelector("#keylist li .removekey").click()');await A.waitFor('document.querySelectorAll("#keylist li").length === 1', { label: 'one removed' });const left = (await txt(A, '#keylist')).includes('laptop') ? 'laptop' : 'old-rsa';g = await sgit(W('first'), ['ls-remote', 'origin'], rsa);const g2 = await sgit(W('first'), ['ls-remote', 'origin'], k1);check('keys: a removed key stops working at once, the other one still works', left === 'laptop' && g.code !== 0 && g2.code === 0, `left=${left} rsa=${g.code} laptop=${g2.code}`);await A.click('#logout');await A.waitFor('!document.querySelector("#keyform")', { label: 'key form gone at logout' });check('keys: logout hides the key form', !(await has(A, '#keyform')) && !(await has(A, '.removekey')));for (const [w, name] of [[390, 'phone'], [1280, 'wide']]) {await A.send('Emulation.setDeviceMetricsOverride', { width: w, height: 900, deviceScaleFactor: 1, mobile: w < 600 });await A.goto(REPO('gamma') + '/code');await A.waitFor('!!document.querySelector("#crumbs")');check(`layout ${w}px: the pushed repo's code page has no horizontal overflow`, await noOverflow(A));const { data } = await A.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true });writeFileSync(join(SCRATCH, `ssh-${name}.png`), Buffer.from(data, 'base64'));}const problems = A.problems().filter(m => !/favicon|ERR_|Failed to load resource/.test(m.text));check('browser: no console errors', problems.length === 0, problems.map(m => m.text).join(' | '));check('server log: no error other than absorbed ones', !/error(?!: absorbed)/i.test(log.replace(/error absorbed[^\n]*/g, '')), log.split('\n').filter(l => /error/i.test(l)).slice(0, 5).join(' | '));} catch (e) {failures++;console.log('FAIL gate crashed — ' + (e && e.stack || e));if (pageA) { try { console.log('DOM: ' + (await pageA.evaluate('document.querySelector("#sshkeys") ? document.querySelector("#sshkeys").outerHTML.slice(0, 1500) : location.href'))); } catch {} }console.log('sshd log: ' + spawnSync('docker', ['logs', '--tail', '30', CONTAINER], { encoding: 'utf8' }).stderr);} finally {for (const b of browsers) { try { await b.close(); } catch {} }spawnSync('docker', ['rm', '-f', CONTAINER]);await stopServer();if (ident) await ident.stop();writeFileSync(join(SCRATCH, 'ssh-server.log'), log);console.log(`${passes} passed, ${failures} failed`);process.exit(failures ? 1 : 0);}
Branches
- mainmain branch
Latest commits
- e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
- 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
- 3dc43108antcolony#40: mission references point to the moved missionsmre
- 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
- 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
- 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
- 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
- fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
- 4a2d7125initial commitmre