Findings
Admin gate accepts the session only
users.is_admin existed since the first schema and nothing read it. The admin
area derives admin from ADMIN_EMAILS instead.
authenticate()inapi/routes.tsaccepts the API key header as well as the session JWT. Reusing it for/api/admin/*would let an admin's MCP key act as admin, soresolveAdminverifies the session JWT directly.- The email is compared from
users.email, not the JWT claim. - The gate is a Fastify
preHandlerhook on an encapsulated/api/adminscope, so a route added to it cannot omit the check. parseAdminEmailsdrops empty entries:ADMIN_EMAILS=","must not match a user whose email is null.- Suites that mock
../src/confighave noADMIN_EMAILS; readers tolerateundefined. - Portal tests must run through
npm run test: the script setsNODE_OPTIONS=--no-experimental-webstorage, and a barenpx vitest runon Node 25 failsThemeToggleandCdpScreencastfor an unrelated reason.