gitoria
All repositories: gitoria
6.4 KB
// lib/api.hl — THE FUNCTION ROUTES (project.hl wires them): thin wrappers — check the request and the session, call the// topic, answer. The plumbing (JSON / Markdown / plain answers, redirects, the ssh guard, `?next=`) is lib/api-helpers.hl.// /api/repos, /api/repos/:slug the public reads (repos.hl; Accept: text/markdown → the Markdown read view)// /login/callback the login button's return (users.hl)// /settings/connect, /settings/connected connecting a repo to a tickets project (tickets.hl)// /__git/keys, /__git/access what the sshd container asks (sshkeys.hl, repos.hl)// (The git protocol's routes are lib/transport.hl gitTransport.)// A route that WRITES THE SESSION gets the request and the framework's session store BY REFERENCE (`&req`, `&sessions`,// from project.hl): copied into a second call, the session inside `req` would be a copy too and the write would be lost.import { Response } from 'hl:http1'import { randomBytes } from 'hl:crypto'import { NL } from './util.hl'import { userOfLoginCode } from './users.hl'import { repoRows, repoRow, repoBySlug, ownsRepo, mayPush } from './repos.hl'import { slugError, repoDocument, listDocument } from './repos-helpers.hl'import { connectHref, finishConnect } from './tickets.hl'import { keyLine } from './sshkeys.hl'import { fail, wantsMarkdown, markdownReply, textReply, goTo, sshGuard, repoOfRequest, safeNext } from './api-helpers.hl'// ---- the API: reads are public (creating a repo is a web action for now) ------------------------static apiRepos = (route, req) => {if (req.method != 'GET') { return fail(405, 'GET only') }let rows = repoRows()if (wantsMarkdown(req)) { return markdownReply(listDocument(rows)) }return { repos = rows }}static apiRepo = (route, req) => {if (req.method != 'GET') { return fail(405, 'GET only') }let row = repoRow(route.params.slug)if (row == null) { return fail(404, 'no such repository') }if (wantsMarkdown(req)) { return markdownReply(repoDocument(row)) }return row}// ---- THE LOGIN BUTTON'S RETURN (ident README "How apps use ident") ----------------------------// ident sends the browser back with ?ident_code= (and our ?next=); the code is exchanged here (users.hl userOfLoginCode),// the user goes into THIS browser's session (`req.session`, the cookie's — hybriel #11; minted when it brought none),// then → `?next=` (api-helpers.hl safeNext). A FAILED LOGIN is a page (components/loginfailed.hl): the reason is parked// in the session, then → /login/failed.static loginFailed = (&req, &sessions, why) => {let s = req.sessionlet fresh = s == nullif (fresh) { s = sessions.mint() }s.data.loginError = whysessions.save(s)let res = new Response('login failed: ' + why, { status = 302 headers = { 'Location' = '/login/failed' 'Cache-Control' = 'no-store' 'Content-Type' = 'text/plain; charset=utf-8' } })if (fresh) { res.headers['Set-Cookie'] = sessions.cookieHeader(s.id) }return res}static loginCallback = (route, &req, &sessions) => {if (req.method != 'GET') { return loginFailed(&req, &sessions, 'GET only') }let q = req.query != null ? req.query : {}let code = q.ident_codeif (code == null || code == '') { return loginFailed(&req, &sessions, 'ident sent no login code') }let x = userOfLoginCode(code)if (x.error != null) { return loginFailed(&req, &sessions, x.error) }let s = req.sessionlet fresh = s == nullif (fresh) { s = sessions.mint() }s.user = { id = x.user.id }s.data.tag = randomBytes(16)s.data.loginError = nullsessions.save(s)let res = new Response('logged in', { status = 302 headers = { 'Location' = safeNext(q.next) 'Cache-Control' = 'no-store' 'Content-Type' = 'text/plain; charset=utf-8' } })if (fresh) { res.headers['Set-Cookie'] = sessions.cookieHeader(s.id) }return res}// ---- CONNECT A REPO TO A TICKETS PROJECT (gitoria#18; tickets.hl, components/settings.hl) ----------------------------// /settings/connect: the owner's click → a fresh nonce parked in the session (bound to the repo) → tickets' /connect.// /settings/connected: tickets' return with ?code&state → tickets.hl finishConnect (nonce, exchange, key stored per// repo) → back to /settings. A failure is a note on the settings page.static connectStart = (route, &req, &sessions) => {if (req.method != 'GET') { return fail(405, 'GET only') }let repo = repoOfRequest(req)if (repo == null) { return fail(404, 'no such repository') }let s = req.sessionif (s == null || !ownsRepo(repo.slug, s)) { return goTo('/settings') }let nonce = randomBytes(16)s.data.connectState = repo.slug + '.' + nonces.data.connectNote = nullsessions.save(s)return goTo(connectHref(repo.slug, nonce))}static connectBack = (route, &req, &sessions) => {if (req.method != 'GET') { return fail(405, 'GET only') }let repo = repoOfRequest(req)if (repo == null) { return fail(404, 'no such repository') }let s = req.sessionif (s == null || !ownsRepo(repo.slug, s)) { return goTo('/settings') }let q = req.query != null ? req.query : {}let want = s.data.connectStates.data.connectState = nulllet note = finishConnect(repo.slug, want, q)s.data.connectNote = note == '' ? null : notesessions.save(s)return goTo('/settings')}// ---- WHAT THE SSHD CONTAINER ASKS (ticket gitoria#7; docker/sshd) — only with the shared secret (api-helpers.hl sshGuard)// GET /__git/keys?type=<ssh-ed25519>&key=<base64> sshd AuthorizedKeysCommand: the authorized_keys line for a known key// (sshkeys.hl keyLine), empty for an unknown one// GET /__git/access?user=<id>&slug=<slug>&write=0|1 gitoria-shell before it starts git: `ok` or 403. Read = every repo (public);// write = the repo's owner only (repos.hl mayPush — the same rule as the token on HTTPS, transport.hl)static gitKeys = (route, req) => {let no = sshGuard(req)if (no != null) { return no }let q = req.query != null ? req.query : {}return textReply(200, keyLine(q.type, q.key))}static gitAccess = (route, req) => {let no = sshGuard(req)if (no != null) { return no }let q = req.query != null ? req.query : {}let slug = q.slugif (slug == null || hlTypeName(slug) != 'String' || slugError(slug) != null) { return textReply(404, 'no such repository' + NL) }let repo = repoBySlug(slug)if (repo == null) { return textReply(404, 'no such repository' + NL) }if (q.write == '1' && !mayPush(q.user, repo)) { return textReply(403, 'only the owner of this repository can push to it' + NL) }return textReply(200, 'ok' + NL)}
Branches
- mainmain branch
Latest commits
- 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