// The operator surface, assembled: sign-in, then the pages, with the gate between them.
//
// ⛔ THE GATE IS HERE AND NOWHERE ELSE, so there is one place to read to know what is protected. Every
// path that is not sign-in or a static asset requires a live session, and "live" is re-checked against
// the identity registry per request rather than trusted from the cookie.
//
// ⛔ STATIC IS SERVED FROM A FIXED LIST, not from a directory join. A path taken off the wire and
// joined to a root is a traversal bug; three files that never change do not need a filesystem walk.
import fs from 'bare-fs'
import { join } from 'bare-path'
import { authRoutes, currentAdmin } from './admin-auth.js'
import { adminRoutes } from './admin.js'
const TYPES = { '.js': 'text/javascript; charset=utf-8' }
// The browser half of the identity layer. Vendored, and the same files favela-surface and pear-git
// serve — noble is pure JS so signing works without crypto.subtle; the SHA-256 in the client does use
// it, which is why this surface must be reached over HTTPS or localhost.
// ⛔ THE TRANSITIVE SET, not just the entry. anproto-client.js imports noble-ed25519, noble-sha512 and
// noble-sha256, and two of THOSE import noble-crypto — a list holding only the entry point serves a
// module graph with a hole in it, and the browser reports a bare network error on an import rather than
// anything about signing in. assets.test.js walks the imports and asserts this set is closed.
const ASSETS = new Set([
'anproto-client.js', 'noble-ed25519.js', 'noble-sha512.js', 'noble-sha256.js', 'noble-crypto.js'
])
export function staticAsset (name, root) {
if (!ASSETS.has(name)) return null
try {
return { status: 200, headers: { 'content-type': TYPES['.js'] }, body: fs.readFileSync(join(root, 'static', name)) }
} catch {
return null
}
}
export async function serveAdmin (req, ctx) {
const path = req.path || '/'
if (path.startsWith('/static/')) {
const asset = staticAsset(path.slice('/static/'.length), ctx.root)
return asset || { status: 404, headers: {}, body: 'not found' }
}
// Sign-in first: these are the only routes reachable without a session, by definition.
const auth = await authRoutes(req, ctx)
if (auth) return auth
// ⛔ NO SESSION, NO SURFACE. A redirect rather than a 403 because the operator's next act is always
// to sign in, and a bare 403 on a page they navigated to reads as "this is broken".
const admin = await currentAdmin(req, ctx)
if (!admin) {
// ⚠ Carry where they were going. Sessions die with the process, so a deploy bounces an operator
// out of whatever page they were reading; sending them to /admin afterwards loses their place for
// no reason, and silent re-auth makes the whole round trip invisible when it works.
const back = encodeURIComponent(req.path + (req.query && Object.keys(req.query).length
? '?' + new URLSearchParams(req.query).toString()
: ''))
return { status: 303, headers: { location: '/admin/login?next=' + back }, body: '' }
}
return adminRoutes(req, ctx)
}
export default { serveAdmin, staticAsset }