Skip to content

Commit a4e4085

Browse files
mikimclaude
andcommitted
docs: document security upgrade, write protection, and deferred plan
- README: Next 16 / React 18 / 92 tests / clean-audit badges; write-path protection highlight; per-route rate-limit budgets in the API section; WRITE_RATE_LIMIT / WRITE_RATE_WINDOW_MS config; eslint flat-config script - docs/SECURITY.md (new): current posture, write-path protections, and known limitations (no per-user auth, in-memory rate-limit state, IP trust) - docs/ROADMAP.md: mark hardening/security done; add an organized, prioritized "deferred future plan" (auth, LLM StrategySpec, shared rate-limit store, season data model, replay cap, React 19, LICENSE) - docs/ARCHITECTURE.md: Next 16 async APIs, enforceWrite in the tick flow, serverExternalPackages; docs/PRD.md + .env.example kept in sync Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2df905a commit a4e4085

6 files changed

Lines changed: 104 additions & 13 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ COINGECKO_COIN_ID=mossland
88
# Weekly seasons — starting paper cash for auto-created seasons (USD)
99
# DEFAULT_STARTING_CASH_USD=1000
1010

11+
# Write-endpoint rate limiting (per IP, per route). Defaults shown.
12+
# WRITE_RATE_LIMIT=30
13+
# WRITE_RATE_WINDOW_MS=60000
14+
1115
# Ops check (`npm run ops:check`) — probes the deployed site
1216
# OPERATIONS_BASE_URL=https://pf.moss.land
1317
# OPERATIONS_RETRIES=2

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
55
![Status](https://img.shields.io/badge/Status-Active_Development-0ea5e9)
66
![Domain](https://img.shields.io/badge/Domain-Simulation_Trading-black)
7-
![Stack](https://img.shields.io/badge/Next.js_14-React_18-black)
8-
![Tests](https://img.shields.io/badge/Tests-87_passing-22c55e)
7+
![Stack](https://img.shields.io/badge/Next.js_16-React_18-black)
8+
![Tests](https://img.shields.io/badge/Tests-92_passing-22c55e)
9+
![Audit](https://img.shields.io/badge/prod_audit-0_vulns-22c55e)
910
![i18n](https://img.shields.io/badge/UI-EN_·_KO-black)
1011

1112
## ◼ Background
@@ -32,6 +33,7 @@ season can be replayed tick by tick.
3233
- **Bilingual UI (EN/KO)** — cookie-persisted locale toggle, server-rendered; strategy keywords work in both languages.
3334
- **Prompt governance** — prompt edits are limited to once per calendar day, with full version history.
3435
- **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`.
3537

3638
## ◼ Pages
3739

@@ -74,7 +76,7 @@ flowchart LR
7476
| `/api/tick` | POST | Run one simulation tick; JSON with `x-pf-ajax: 1` header, otherwise redirects to `/leaderboard` |
7577
| `/api/locale` | POST | Persist `en` \| `ko` in a 1-year `pf_locale` cookie |
7678

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.
7880

7981
## ◼ Quick Start
8082

@@ -95,6 +97,8 @@ Open `http://localhost:6200` (dev and production both bind port 6200).
9597
| `COINGECKO_BASE_URL` | `https://api.coingecko.com/api/v3` | Price feed base URL |
9698
| `COINGECKO_COIN_ID` | `mossland` | CoinGecko coin id for the feed |
9799
| `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 |
98102
| `OPERATIONS_BASE_URL` | `https://pf.moss.land` | Target of `npm run ops:check` |
99103
| `PROMPTFOLIO_STALE_HOURS` | `168` | Repo-staleness threshold for ops checks (warn-only unless `PROMPTFOLIO_STRICT_STALE_FAIL=1`) |
100104

@@ -106,9 +110,9 @@ Open `http://localhost:6200` (dev and production both bind port 6200).
106110
|---|---|
107111
| `npm run dev` | Dev server on port 6200 |
108112
| `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) |
110114
| `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) |
112116
| `npm run db:init` | Create/upgrade the SQLite schema — safe to re-run |
113117
| `npm run ops:check` | Probe the deployed site (`/`, `/api/health`, `/season`) with retries + repo-staleness check, emitting a JSON summary |
114118

@@ -122,16 +126,17 @@ Six SQLite tables (better-sqlite3, WAL): `agents`, `seasons`, `portfolios` (PK `
122126

123127
## ◼ Tech Stack
124128

125-
- Next.js 14 (App Router) + React 18, TypeScript strict
129+
- Next.js 16 (App Router, Turbopack) + React 18, TypeScript strict
126130
- better-sqlite3 local persistence, zod validation
127131
- CoinGecko price feed (simulation context)
128-
- Node built-in test runner; no external test framework
132+
- Node built-in test runner; ESLint 9 flat config; no external test framework
129133

130134
## ◼ Docs
131135

132136
- [PRD](docs/PRD.md) — product goals, shipped scope, next features
133137
- [Architecture](docs/ARCHITECTURE.md) — runtime shape, data model, request flow
134-
- [Roadmap](docs/ROADMAP.md) — version-by-version progress (한국어)
138+
- [Roadmap](docs/ROADMAP.md) — version-by-version progress + deferred future plan (한국어)
139+
- [Security](docs/SECURITY.md) — current posture, write-path protections, and known limitations
135140

136141
## ◼ Disclaimer
137142

docs/ARCHITECTURE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# Architecture
22

3-
- Next.js 14 App Router, React 18, TypeScript strict
3+
- Next.js 16 App Router (Turbopack), React 18, TypeScript strict
44
- 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
56
- 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)
69
- 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
10+
- i18n: `pf_locale` cookie (1 year) → server-side `await getLocale()` → typed EN/KO dictionary, applied to `<html lang>` and page copy
811

912
## Why this shape
1013

@@ -16,6 +19,7 @@ lines are hash-picked, and replay PnL is fully reconstructible from the `trades`
1619

1720
```
1821
POST /api/tick
22+
→ enforceWrite(req, 'tick') # same-origin (CSRF) + per-IP rate limit → 403 / 429
1923
→ ensureWeeklySeason() # idempotent: season_YYYYwWW, "Weekly Season YYYY-Www"
2024
→ fetchMocUsd() # CoinGecko live price, throws on failure
2125
→ runTick(seasonId, price) # per agent: prompt → target allocation → rebalance

docs/PRD.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,7 @@ rules (no LLM yet — see `src/lib/engine.ts`); an LLM decision engine is a plan
4747
- One-click share card image (OG image)
4848
- LLM decision engine (prompt → target allocation) with budget guardrails
4949
- Public seasons + spectator mode
50-
- Anti-cheat extensions: agent-creation rate limits, seeded price simulation
51-
(prompt-edit lock and deterministic engine are already in place)
50+
- Anti-cheat extensions: seeded price simulation (prompt-edit lock, deterministic
51+
engine, and per-route write rate limits are already in place)
52+
- Per-user auth / ownership before public multi-tenant operation (see
53+
[SECURITY.md](SECURITY.md) and the deferred plan in [ROADMAP.md](ROADMAP.md))

docs/ROADMAP.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@
2424
- [ ] **SSE 실시간 스트리밍** — 현재는 폴링/리프레시 기반, 진짜 push 미구현
2525
- [ ] **스코어링 확장** — max drawdown, 거래 빈도 페널티 (PnL/ROI는 완료)
2626

27+
## ✅ v0.1.1 (Hardening & Security, 완료)
28+
- [x] **데이터 무결성** — 틱/에이전트 생성/프롬프트 수정을 SQLite 트랜잭션으로 원자화(부분 반영 롤백)
29+
- [x] **POST-redirect-GET** — 폼 리다이렉트를 307→303으로 정정
30+
- [x] **헬스 준비성 검사**`/api/health`가 DB/스키마 확인, 불가 시 `503`
31+
- [x] **최초 프롬프트 이력화** — 생성 시 프롬프트를 v1로 기록
32+
- [x] **보안 업그레이드** — Next.js 14→16 (advisory 9건 해소), postcss override, `npm audit --omit=dev` 0건
33+
- [x] **쓰기 API 보호** — 동일 출처(CSRF) 검사 + 라우트별 IP 레이트리밋(`429`/`Retry-After`)
34+
- [x] **툴링** — ESLint 9 flat config, `typecheck` 스크립트, GitHub Actions CI
35+
2736
## 🚀 v0.2 (Share & LLM)
2837
- [ ] OG image 생성 — 리더보드 상위 3명 카드 이미지
2938
- [ ] 에이전트 공유 링크(`/agents/{id}/card.png`)
@@ -37,10 +46,31 @@
3746
- [ ] 리더보드 공개 페이지(`/public/season/{id}`)
3847

3948
## 🔐 v0.4 (Anti-cheat & Polish)
40-
- [ ] Rate limit(시간당 N개 에이전트까지) — 프롬프트 수정 1일 1회 제한은 완료
49+
- [x] Rate limit — 라우트별 IP 레이트리밋 완료(에이전트/시즌/틱/프롬프트/로케일)
4150
- [ ] Seeded price simulation(같은 시드 → 같은 결과) — 전략 엔진 자체는 이미 결정적
4251
- [ ] 사기 전략 방지(초단타 100번 → 페널티)
4352

53+
## 🧭 향후 계획 (Deferred — 우선순위순)
54+
55+
리뷰에서 제기됐지만 이번 범위에 넣지 않은 항목들. 규모가 크거나 제품/법적 결정이 필요해
56+
별도 작업으로 남긴다.
57+
58+
1. **사용자 인증 / 소유권 모델** — 현재 사용자 모델이 없어 동일 출처면 누구나 쓰기 가능.
59+
퍼블릭·멀티테넌트 운영 전 세션 + 에이전트/프롬프트 소유권 검사가 필요. (레이트리밋·CSRF는
60+
남용을 줄일 뿐 호출자를 인증하지는 않음 — [SECURITY.md](SECURITY.md) 참고)
61+
2. **LLM 전략 엔진 (StrategySpec)** — 자연어 프롬프트를 검증 가능한 JSON 정책으로 1회 컴파일하고,
62+
시즌은 그 정책을 결정적으로 실행. 현재는 키워드 매처(v0.2 항목과 연계).
63+
3. **레이트리밋 공유 스토어** — 현재 인메모리/프로세스별. 다중 인스턴스 시 SQLite/Redis 등
64+
공유 저장소 필요.
65+
4. **시즌 데이터 모델 강화** — 시즌 roster/state, 수수료·슬리피지 반영 `fills`,
66+
`market_snapshots`(출처·시간·해시), idempotency key로 중복 틱 방지, FK/CHECK 제약.
67+
5. **Replay 200건 상한 정리** — 현재 `ORDER BY ts ASC LIMIT 200`(오래된 200건). 원가기준 PnL이
68+
전체 이력에 의존하므로 "전체 계산 + 최근 N행 표시"로 분리 필요(표시 정책 결정 사항).
69+
6. **밈 배지 자동 부여 / drawdown·거래빈도 스코어링 / SSE 스트리밍** (위 v0.1 미완 항목).
70+
7. **React 19 업그레이드** — Next 16은 React 18 지원(현행 유지). 향후 View Transitions 등
71+
활용 시 이전.
72+
8. **LICENSE 선언** — 오픈소스 라이선스 미선언(현재 all-rights-reserved). 소유자 결정 필요.
73+
4474
## ✨ Future
4575
- [ ] 에이전트 마켓플레이스(프롬프트 거래/평가)
4676
- [ ] 상금 시즌(MOC 보상)

docs/SECURITY.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Security posture
2+
3+
This is a **paper-trading simulation** — no funds, custody, or execution. The
4+
threat model is therefore about integrity and abuse resistance, not asset loss.
5+
6+
## What is in place
7+
8+
- **Dependency hygiene** — runs on Next.js 16; `npm audit --omit=dev` reports
9+
**0 vulnerabilities**. (One low-severity, dev-only `esbuild` advisory remains
10+
via `tsx`; it affects only esbuild's own dev server on Windows and is not
11+
shipped.) CI runs typecheck → lint → test → build on every push/PR.
12+
- **Input validation** — every write endpoint validates with zod and caps
13+
sizes (agent name ≤ 40, avatar ≤ 8, prompt ≤ 2,000 chars; season cash
14+
$1–$1,000,000).
15+
- **Write-path protection** (`src/lib/rate-limit.ts`, `src/lib/guard.ts`):
16+
- **CSRF / same-origin** — a POST carrying a cross-origin `Origin` header is
17+
rejected with `403`. Absent-Origin requests (monitors, curl) are allowed,
18+
as they are not a CSRF vector.
19+
- **Rate limiting** — per-IP, per-route fixed-window budgets (`429` +
20+
`Retry-After`). Defaults: agents 10, season 6, tick 30, update-prompt 10,
21+
locale 30 per minute; tune via `WRITE_RATE_LIMIT` / `WRITE_RATE_WINDOW_MS`.
22+
- **Atomicity** — ticks, agent creation, and prompt edits run in SQLite
23+
transactions, so a mid-operation failure rolls back cleanly.
24+
- **Prompt governance** — prompt edits are limited to once per calendar day,
25+
with the full version history retained.
26+
- **Readiness probe**`GET /api/health` verifies the DB/schema and returns
27+
`503` when unreachable (so uptime monitors catch a broken deploy).
28+
29+
## Known limitations (deferred — see docs/ROADMAP.md)
30+
31+
- **No per-user auth / ownership.** There is no user model; any same-origin
32+
client can create agents/seasons and run ticks. Rate limiting + CSRF reduce
33+
abuse but do not authenticate callers. A real auth layer (sessions + owner
34+
checks on agents/prompts) is the main outstanding item before public,
35+
multi-tenant operation.
36+
- **Rate-limit state is in-memory per process.** Correct for a single-instance
37+
PM2 deployment; multiple instances would each keep their own counters. A
38+
shared store (e.g. SQLite/Redis) is needed to throttle globally.
39+
- **Client IP trust.** `x-forwarded-for` is trusted as-is; only deploy behind a
40+
proxy that sets it reliably, or the first hop can be spoofed.
41+
42+
## Reporting
43+
44+
This is an experimental project. Open an issue for anything security-relevant;
45+
do not include exploit details for anything that could affect a live deployment
46+
in a public issue.

0 commit comments

Comments
 (0)