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
@@ -32,6 +33,7 @@ season can be replayed tick by tick.
32
33
-**Bilingual UI (EN/KO)** — cookie-persisted locale toggle, server-rendered; strategy keywords work in both languages.
33
34
-**Prompt governance** — prompt edits are limited to once per calendar day, with full version history.
34
35
-**Live health surface** — `GET /api/health` runs a real DB readiness probe (HTTP 503 + `db:"down"` if the schema is missing), plus a footer badge that polls it every 30s with age counter and manual refresh.
36
+
-**Hardened write path** — every mutating endpoint is atomic (SQLite transactions), same-origin-guarded (cross-origin POSTs get `403`), and per-IP rate-limited (`429` + `Retry-After`). Runs on Next.js 16 with a clean production `npm audit`.
35
37
36
38
## ◼ Pages
37
39
@@ -74,7 +76,7 @@ flowchart LR
74
76
|`/api/tick`| POST | Run one simulation tick; JSON with `x-pf-ajax: 1` header, otherwise redirects to `/leaderboard`|
75
77
|`/api/locale`| POST | Persist `en`\|`ko` in a 1-year `pf_locale` cookie |
76
78
77
-
Form endpoints use POST-redirect-GET (`303 See Other`); validation errors return `400` with flattened zod issues.
79
+
Form endpoints use POST-redirect-GET (`303 See Other`); validation errors return `400` with flattened zod issues.**Every POST is guarded**: cross-origin requests are rejected with `403`, and each is per-IP rate-limited (`429` + `Retry-After`) — budgets per minute: agents 10, season 6, tick 30, update-prompt 10, locale 30.
78
80
79
81
## ◼ Quick Start
80
82
@@ -95,6 +97,8 @@ Open `http://localhost:6200` (dev and production both bind port 6200).
95
97
|`COINGECKO_BASE_URL`|`https://api.coingecko.com/api/v3`| Price feed base URL |
96
98
|`COINGECKO_COIN_ID`|`mossland`| CoinGecko coin id for the feed |
97
99
|`DEFAULT_STARTING_CASH_USD`|`1000`| Starting paper cash for auto-created weekly seasons |
100
+
|`WRITE_RATE_LIMIT`|`30`| Default per-IP write budget per window (per route can be stricter) |
101
+
|`WRITE_RATE_WINDOW_MS`|`60000`| Rate-limit window length in ms |
98
102
|`OPERATIONS_BASE_URL`|`https://pf.moss.land`| Target of `npm run ops:check`|
99
103
|`PROMPTFOLIO_STALE_HOURS`|`168`| Repo-staleness threshold for ops checks (warn-only unless `PROMPTFOLIO_STRICT_STALE_FAIL=1`) |
100
104
@@ -106,9 +110,9 @@ Open `http://localhost:6200` (dev and production both bind port 6200).
106
110
|---|---|
107
111
|`npm run dev`| Dev server on port 6200 |
108
112
|`npm run build` / `npm start`| Production build / serve |
109
-
|`npm test`|87 unit tests via Node's built-in `node:test` runner (no Jest/Vitest) |
113
+
|`npm test`|92 unit tests via Node's built-in `node:test` runner (no Jest/Vitest) |
110
114
|`npm run typecheck`|`tsc --noEmit` — strict type check across app + tests |
111
-
|`npm run lint`|`next lint` (core-web-vitals) |
115
+
|`npm run lint`|`eslint .` (ESLint 9 flat config, `eslint-config-next`core-web-vitals) |
112
116
|`npm run db:init`| Create/upgrade the SQLite schema — safe to re-run |
113
117
|`npm run ops:check`| Probe the deployed site (`/`, `/api/health`, `/season`) with retries + repo-staleness check, emitting a JSON summary |
- All pages `force-dynamic` server-rendered against local SQLite (better-sqlite3, WAL)
5
+
- Async Request APIs (Next 16): `cookies()` and route `params`/`searchParams` are awaited; `getLocale()` is async
5
6
- 6 API route handlers: `/api/health` (GET, DB readiness probe — `503` when the schema is unreachable) + 5 POST endpoints (`agents`, `agents/[id]/update-prompt`, `season`, `tick`, `locale`); zod-validated wherever input is accepted (`tick` takes no body), form flows use POST-redirect-GET (`303`)
7
+
- Every POST is gated by `enforceWrite()` (`src/lib/guard.ts`): same-origin/CSRF check (`403`) + per-IP per-route rate limit (`429` + `Retry-After`), with primitives in `src/lib/rate-limit.ts`
8
+
-`better-sqlite3` is declared in `serverExternalPackages` (native addon kept out of the Turbopack server bundle)
6
9
- Price feed: CoinGecko `simple/price` (`COINGECKO_BASE_URL` / `COINGECKO_COIN_ID`), `no-store`, throws on failure — no synthetic fallback
7
-
- i18n: `pf_locale` cookie (1 year) → server-side `getLocale()` → typed EN/KO dictionary, applied to `<html lang>` and page copy
0 commit comments