---
id: building-the-game/003
title: Sign in on the site with a game account; Kris is admin
epic: building-the-game
state: review
priority: 2
blocked_by: ["building-the-game/002"]
estimate: M
created: 2026-09-03
updated: 2026-09-03
claimed_by: null
claimed_at: null
delivers: []
review_artifact: builder/README.md
---

## What to do

`POST /builder/auth/login` (email + password checked against the game DB's `auth_identities`
through the read-only role, with the Builder's **own copy** of the scrypt verify — the game's
tables gain no columns), minting a `web_sessions` row (token hash, 7-day `expires_at`,
revocable); `POST /builder/auth/logout`; `GET /builder/auth/me`. `Authorization: Bearer` only,
never a query token. Token buckets on every auth and write route. `roles` table
(account id → `admin` | `player`), seeded by `npm run set-role -- <email> admin` on the droplet.

Site: `components/SignIn.astro` modelled on `RepoLinkDialog.astro`; session in
`lor:session` (the footer's "clear site settings" wipe doubles as sign-out); a header chip with
name and role. Admin-only UI is hidden client-side, and every write is enforced server-side.

## Definition of done

Tests: login, expiry, revoked, wrong role 403, 429. Kris signs in on spellgrove.com and
`/builder/auth/me` says `admin`.

## Built (2026-09-03)

Live. `POST /builder/auth/login` (game email + password, checked read-only against the game's
`auth_identities` with the Builder's own scrypt verify; 10 tries per 10 minutes per IP, 20 per
hour per email), `POST /auth/logout`, `GET /auth/me`; bearer web sessions, 7 days; `roles`
table; `npm run set-role -- <email> admin`. Kris's account (Papa, kris.randall@gmail.com) is
admin. The site: a "Sign in" chip in the header opens the dialog; the token lives in
`lor:session`; `<html data-role>` gates editing controls client-side while the Builder enforces
every write. 10 tests. Not yet exercised with a real password — that is Kris's own check.