gitoria
All repositories: gitoria
22.2 KB
// 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 } 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); // 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, 'push-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`;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 (push gate)', [BASE]);startServer({ IDENT_API_KEY: APPKEY.key, IDENT_API_SECRET: APPKEY.secret });await serverUp();startProxy();// ---- alice in Chrome: log in, name, create an empty repo --------------------------------------------------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('tokens: signed out, the main page offers no token form', !(await has(A, '#tokenform')));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.type('#description', 'pushed to');await A.click('#createsave');await A.waitForSelector('#created');// ---- the "add code" box of an EMPTY repo (gitoria#20: plain text, only on the Code page) ---------------------await A.goto(REPO('gamma') + '/code');await A.waitFor('!!document.querySelector("#addcode")', { label: 'add code box' });const url = urlOf('gamma', null, null, PORT);check('empty repo: the "add code" box is plain text (no <details>/<summary>)', await A.evaluate('document.querySelector("#addcode").tagName !== "DETAILS" && !document.querySelector("#addcode summary")'));check('empty repo: the clone command names the repo address', (await txt(A, '#clonecommand')) === 'git clone ' + url, await txt(A, '#clonecommand'));const nc = await txt(A, '#newcommands');check('empty repo: the new-project commands are complete', nc.split('\n').length === 6 && /^git init -b main$/m.test(nc) && nc.includes(`git remote add origin ${url}`) && /git push -u origin main$/.test(nc), nc);check('empty repo: the existing-project commands are complete', (await txt(A, '#existingcommands')) === `git remote add origin ${url}\ngit branch -M main\ngit push -u origin main`, await txt(A, '#existingcommands'));check('empty repo: the box links to the access tokens on the main address', (await A.evaluate('document.querySelector("#totokens").getAttribute("href")')) === BASE + '/#tokens');const first = (await hreq('GET', '/code', `gamma.gitoria.test:${PORT}`)).body;check('empty repo: the box is in the FIRST html of /code (no script needed)', /id="addcode"/.test(first) && first.includes(`git clone ${url}`), first.slice(first.indexOf('id="addcode"'), first.indexOf('id="addcode"') + 200));const other = await A.evaluate('1'); // keep the page aliveawait A.goto(REPO('nothere') + '/');await A.waitFor('!!document.querySelector("#missing")');check('unknown address: no "add code" box', !(await has(A, '#addcode')));// ---- the token, made in the browser -------------------------------------------------------------------------await A.goto(BASE + '/');await A.waitFor('!!document.querySelector("#tokenform")', { label: 'token form' });await hydrated(A);check('tokens: logged in, "You have no tokens yet"', await has(A, '#notokens'));await A.click('#tokensave');await sleep(300);await A.type('#tokenname', 'laptop');await A.click('#tokensave');await A.waitFor('!!document.querySelector("#tokensecret")', { label: 'token shown' });const TOKEN = (await txt(A, '#tokensecret')).trim();check('tokens: a token is shown once (gtr_ + 40 hex), listed by its name', /^gtr_[0-9a-f]{40}$/.test(TOKEN) && (await txt(A, '#tokenlist')).includes('laptop') && !(await has(A, '#notokens')), TOKEN);const stored = readFileSync(join(STORE, 'mpackdb', readdirOf(join(STORE, 'mpackdb')).find(n => n.startsWith('tokens.'))), 'latin1');// hybriel 64527baa: the face's ack syncs `tokens` from the server; the new row must not be listed twicecheck('tokens: the first token is listed ONCE', (await A.evaluate('document.querySelectorAll("#tokenlist li").length')) === 1, await txt(A, '#tokenlist'));check('tokens: only the hash is stored, never the token', !stored.includes(TOKEN.slice(4)), 'the token text is in the table file');await A.type('#tokenname', 'second');await A.click('#tokensave');await A.waitFor('document.querySelectorAll("#tokenlist li").length === 2', { label: 'two tokens' });const SECOND = (await txt(A, '#tokensecret')).trim();check('tokens: a second token replaces the shown secret', /^gtr_[0-9a-f]{40}$/.test(SECOND) && SECOND !== TOKEN, J({ TOKEN, SECOND }));// ---- real git: clone the empty repo, push, clone, fetch, push again ---------------------------------------------const W = (n) => join(WORK, n);let g = await git(WORK, ['clone', urlOf('gamma'), 'first'], 'gamma');check('git: cloning the empty repo works (an empty repository)', g.code === 0 && /empty repository/.test(g.out), J(g));writeFileSync(join(W('first'), 'README.md'), '# gamma\n\nPushed with **git**.\n');writeFileSync(join(W('first'), 'big.bin'), randomBytes(4000000));await git(W('first'), ['add', '.']); await git(W('first'), ['commit', '-qm', 'first commit']);g = await git(W('first'), ['push', 'origin', 'main'], 'gamma');check('git: a push without credentials is refused and says how to get a token', g.code !== 0 && /terminal prompts disabled|could not read Username|401/.test(g.out) && g.out.length > 0, g.out);const rawNo = await hreq('POST', '/gamma.git/git-receive-pack', `gamma.gitoria.test:${PORT}`, {}, '0000');check('git: the push endpoint answers 401 with a Basic challenge and the hint', rawNo.status === 401 && /Basic/.test(rawNo.headers['www-authenticate'] || '') && /token/.test(rawNo.body), J(rawNo));g = await git(W('first'), ['push', urlOf('gamma', 'alice', 'gtr_' + '0'.repeat(40)), 'main'], 'gamma');check('git: a token nobody made is refused', g.code !== 0 && /Authentication failed|not valid|401/.test(g.out), g.out);// bob logs in through his own ident identity: another user's token must 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];const bobName = 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 bt = await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', cookie: bobCookie }, body: J({ t: 'emit', i: 2, event: 'gitoriaMakeToken', payload: ['bobs'] }) });const BOBTOKEN = (JSON.parse(await bt.text()).value || {}).token;check('tokens: bob makes his own token (a face, own session)', /^gtr_[0-9a-f]{40}$/.test(BOBTOKEN || ''), String(BOBTOKEN));g = await git(W('first'), ['push', urlOf('gamma', 'bob', BOBTOKEN), 'main'], 'gamma');check('git: another user\'s token may not push — only the owner may', g.code !== 0 && /only the owner|403/.test(g.out), g.out);g = await git(W('first'), ['push', urlOf('gamma', 'alice', TOKEN), 'main'], 'gamma');check('git: the owner pushes with the token (a 4 MB pack too)', g.code === 0 && /main -> main/.test(g.out), g.out);g = await git(WORK, ['clone', '-q', urlOf('gamma'), 'second'], 'gamma');check('git: a clone needs no login (public read) and has the pushed commit and file', g.code === 0 && readFileSync(join(W('second'), 'README.md'), 'utf8').includes('Pushed with') && readFileSync(join(W('second'), 'big.bin')).equals(readFileSync(join(W('first'), 'big.bin'))), g.out);check('git: the bare repo on the server holds the commit', await (await git(W('first'), ['--git-dir', join(STORE, 'git', 'gamma.git'), 'log', '--format=%s'])).out.trim() === 'first commit');// hybriel#89: hl:http1 reads a chunked request body — a big push straight to the server (no proxy) works (was a 411 before)writeFileSync(join(W('first'), 'direct.bin'), randomBytes(4000000));await git(W('first'), ['add', '.']); await git(W('first'), ['commit', '-qm', 'direct push']);g = await git(W('first'), ['-c', 'http.postBuffer=65536', 'push', urlOf('gamma', 'alice', TOKEN, PORT), 'main'], 'gamma', { port: PORT });check('git: a big push straight to hl:http1 (chunked, no proxy) works (hybriel#89)', g.code === 0 && /main -> main/.test(g.out) && (await git(W('first'), ['--git-dir', join(STORE, 'git', 'gamma.git'), 'log', '-1', '--format=%s'])).out.trim() === 'direct push', g.out);// a fetch with many local commits sends a gzip-compressed requestfor (let i = 0; i < 300; i++) { await git(W('second'), ['commit', '-q', '--allow-empty', '-m', 'local ' + i]); }writeFileSync(join(W('first'), 'more.txt'), 'more\n'); await git(W('first'), ['add', '.']); await git(W('first'), ['commit', '-qm', 'second commit']);g = await git(W('first'), ['push', urlOf('gamma', 'alice', TOKEN), 'main'], 'gamma');check('git: a second push (fast-forward) works', g.code === 0, g.out);g = await git(W('second'), ['fetch', 'origin'], 'gamma');check('git: a fetch with 300 unpushed local commits (gzip request) works', g.code === 0, g.out);g = await git(W('second'), ['pull', '--no-rebase', '--no-edit', 'origin', 'main'], 'gamma');check('git: pull merges the new commit', g.code === 0 && existsSync(join(W('second'), 'more.txt')), g.out);g = await git(W('first'), ['-c', 'protocol.version=0', 'ls-remote', urlOf('gamma')], 'gamma');check('git: protocol v0 works too', g.code === 0 && /refs\/heads\/main/.test(g.out), g.out);g = await git(W('first'), ['-c', 'protocol.version=2', 'ls-remote', urlOf('gamma')], 'gamma');check('git: protocol v2 works', g.code === 0 && /refs\/heads\/main/.test(g.out), g.out);// ---- odd requests ---------------------------------------------------------------------------------------------const st = async (path, o = {}) => (await hreq(o.method || 'GET', path, `gamma.gitoria.test:${PORT}`, o.headers || {}, o.body)).status;check('transport: dumb-protocol / unknown service → 403', (await st('/gamma.git/info/refs')) === 403 && (await st('/gamma.git/info/refs?service=git-evil')) === 403);check('transport: another repo name in the path / an unknown repo → 404', (await st('/beta.git/info/refs?service=git-upload-pack')) === 404 && (await st('/gamma/info/refs?service=git-upload-pack')) === 404);check('transport: wrong methods → 405', (await st('/gamma.git/git-upload-pack')) === 405 && (await st('/gamma.git/info/refs?service=git-upload-pack', { method: 'POST' })) === 405);check('transport: the main address has no git', (await hreq('GET', '/gamma.git/info/refs?service=git-upload-pack', `gitoria.test:${PORT}`)).status === 404);check('transport: a Basic header that is not base64 is just "no credentials" → 401', (await st('/gamma.git/git-receive-pack', { method: 'POST', body: '0000', headers: { authorization: 'Basic $(touch /tmp/pwned)' } })) === 401 && !existsSync('/tmp/pwned'));check('transport: no temp files left behind', !existsSync(join(STORE, 'git', '.tmp')) || readdirOf(join(STORE, 'git', '.tmp')).length === 0, J(readdirOf(join(STORE, 'git', '.tmp'))));// ---- the box on a repo with code; the pushed code shows; remove a token -----------------------------------------await A.goto(REPO('gamma') + '/');await A.waitFor('!!document.querySelector("#homepage")', { label: 'README of the pushed code' });check('pushed code: the Readme page shows the pushed README', /Pushed with/.test(await txt(A, '#homepage')));check('pushed code: no "add code" box on the Readme', !(await has(A, '#addcode')));await A.goto(REPO('gamma') + '/code');await A.waitFor('!!document.querySelector("#crumbs")', { label: '/code' });check('pushed code: /code has no "add code" box once there is a commit (gitoria#20)', !(await has(A, '#addcode')));await A.goto(BASE + '/');await A.waitFor('document.querySelectorAll("#tokenlist li").length === 2', { label: 'tokens after reload' });await hydrated(A);check('tokens: the list survives a reload; the secret is not shown again', !(await has(A, '#tokensecret')));await A.evaluate('document.querySelector("#tokenlist li .removetoken").click()');await A.waitFor('document.querySelectorAll("#tokenlist li").length === 1', { label: 'one token removed' });const gone = (await txt(A, '#tokenlist')).includes('laptop') ? 'laptop' : 'second';// removed one is the newest = "second"g = await git(W('first'), ['push', urlOf('gamma', 'alice', SECOND), 'main'], 'gamma');check('tokens: a removed token stops working at once', g.code !== 0 && /not valid|Authentication failed|401/.test(g.out) && gone === 'laptop', g.out + ' / left: ' + gone);g = await git(W('first'), ['push', urlOf('gamma', 'alice', TOKEN), 'main'], 'gamma');check('tokens: the other token still works', g.code === 0, g.out);await A.evaluate('document.querySelector("#tokenlist li .removetoken").click()');await A.waitFor('!document.querySelector("#tokenlist li") && !!document.querySelector("#notokens")', { label: 'all removed' });await A.click('#logout');await A.waitFor('!document.querySelector("#tokenform")', { label: 'token form gone at logout' });check('tokens: logout hides the token form', !(await has(A, '#tokenform')) && !(await has(A, '#tokensecret')));// face forgeryconst fk = await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json' }, body: J({ t: 'emit', i: 3, event: 'gitoriaMakeToken', payload: ['x', { user: { id: 'y' } }] }) });check('tokens: a forged session argument makes no token', !/gtr_/.test(await fk.text()));// layoutfor (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, `push-${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("#tokens") ? document.querySelector("#tokens").outerHTML.slice(0, 1500) : location.href'))); } catch {} }if (pageA) console.log('page problems: ' + J(pageA.problems().map(m => m.text.slice(0, 300))));} finally {for (const b of browsers) { try { await b.close(); } catch {} }if (proxy) proxy.close();await stopServer();if (ident) await ident.stop();writeFileSync(join(SCRATCH, 'push-server.log'), log);console.log(`${passes} passed, ${failures} failed`);process.exit(failures ? 1 : 0);}function readdirOf(d) { try { return require_readdir(d); } catch { return []; } }import { readdirSync as require_readdir } from 'node:fs';
Branches
- mainmain branch
Latest commits
- cc7bf7bamission 002 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (289 lets → plain declarations; 213 left: 125 reassigned, 88 loop-bound; no member/import/param clash). gates 200/0, 46/0, 44/0; real-data reads + writes identical (browser modules: var → const only)mre
- 090a20c6mission 002 (code order) 2/4: one lib/ file per topic — git.hl split into git (calls, branches, init, temp folder) / homepage / code / pulls / releases (+ git-helpers: paths, ids, |||PR/|||RL markers); repos-helpers, tickets-helpers, transport-helpers; util.hl = localtime + env, storage dir, addresses, lists, text checks, one newest-first sort (was 3 copies); the function routes out of project.hl into lib/api.hl (thin; plumbing in api-helpers.hl), sshgate.hl folded into api.hl + sshkeys.hl keyLine + repos.hl mayPush; 'Make main' and the merge answer out of the faces (code.hl makeMain, pulls.hl pullsView), one login helper (users.hl userOfLoginCode); project.hl is the map. Session-writing routes get &req + &server.sessions. gates 200/0, 46/0, 44/0; real-data identical except /login/failed now shows the parked reason for a browser that already had a session (the old copy-of-req lost it)mre
- 110c2799mission 002 (code order) 1/4: .hl files out of the root — lib/ (api, git, localtime, markdown, repos, sshgate, sshkeys, tickets, tokens, transport, users), components/styles.hl; jsoncheck.hl removed (imported nowhere); import paths only. gates 200/0, 46/0, 44/0; real-data reads + writes identicalmre
- fdfb4b1bgitoria: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 200/0, 46/0, 44/0mre
- 5b46ac84antcolony#40: LOG.md — missions 069/072 are antcolony missions (report paths on Byrodin)mre
- 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
- 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