You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There is no admin surface today. We need a server-rendered shell with a login flow, CSRF protection, and a hardened CSP — the foundation for the keys / usage / traces pages.
Goal
/admin/* mounted on the same Hono app/port with hono/jsx baseline, login, session, navigation, layout, and security headers; zero new build pipeline.
Tasks
hono/jsx layout: src/admin/layout.tsx with header, nav (Models, Keys, Usage, Traces, Audit, Settings), footer
Mount adminApp at /admin in src/server.ts; static assets at /admin/assets/* (vendored uPlot ≈ 12 KB, inline CSS)
Index page /admin shows status: config version, db row counts, auth mode, current admin's id (last 4 of sha256)
Login flow (per security S11):
GET /admin/login — paste-key form
POST /admin/login — validates the key (must be admin tier), creates a server-side session row in SQLite (sessions table from a new migration), sets cookie sid with HttpOnly; Secure; SameSite=Strict; Path=/admin; Max-Age=28800
Refuse to serve /admin/* over plain HTTP on non-loopback addresses (require HTTPS or 127.0.0.1/::1)
Logout: DELETE /admin/session removes server row + clears cookie
8h sliding expiry; key never echoed back, never stored client-side
CSRF (per security S9):
Double-submit pattern: csrf cookie (HMAC-bound to session id) + X-CSRF-Token header on every mutating request
Verify both Sec-Fetch-Site: same-origin AND token on every state-changing route
Part of #23. Depends on F2.C.
Background
There is no admin surface today. We need a server-rendered shell with a login flow, CSRF protection, and a hardened CSP — the foundation for the keys / usage / traces pages.
Goal
/admin/*mounted on the same Hono app/port withhono/jsxbaseline, login, session, navigation, layout, and security headers; zero new build pipeline.Tasks
hono/jsxlayout:src/admin/layout.tsxwith header, nav (Models, Keys, Usage, Traces, Audit, Settings), footeradminAppat/admininsrc/server.ts; static assets at/admin/assets/*(vendored uPlot ≈ 12 KB, inline CSS)/adminshows status: config version, db row counts, auth mode, current admin's id (last 4 of sha256)/admin/login— paste-key form/admin/login— validates the key (must be admin tier), creates a server-side session row in SQLite (sessionstable from a new migration), sets cookiesidwithHttpOnly; Secure; SameSite=Strict; Path=/admin; Max-Age=28800/admin/*over plain HTTP on non-loopback addresses (require HTTPS or127.0.0.1/::1)/admin/sessionremoves server row + clears cookiecsrfcookie (HMAC-bound to session id) +X-CSRF-Tokenheader on every mutating requestSec-Fetch-Site: same-originAND token on every state-changing route<Csrf />Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'X-Frame-Options: DENY,Referrer-Policy: no-referrer,X-Content-Type-Options: nosniff/healthz(always 200, no auth) and/readyz(verifies DB + Copilot token freshness, no auth) outside the/adminprefixAcceptance criteria
GET /adminredirects to/admin/loginwhen not authenticatedpackage.json; everything served by Bun at runtimeFile pointers
src/admin/{layout,index,login,session}.tsx,src/admin/csrf.ts,src/admin/assets/{style.css,uplot.min.js,uplot.min.css},src/lib/migrations/004_sessions.sqlsrc/server.tsDependencies
Depends on F2.C. Blocks F2.E, F3.B, F4.B.