You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3.2 KiB
3.2 KiB
pages — Architecture
Full design + security analysis: plans/2026-07-12-pages-design.md.
Components
┌─────────────────────────────────────────┐
browser ──────────▶ │ Cloudflare Worker (Rust → wasm, axum) │
│ │
GET / │ routes/mod.rs router construction │
GET /auth/* │ routes/auth.rs GitHub OAuth, sessions │──▶ github.com (token exchange)
* /api/* │ routes/api.rs pages + users JSON API │──▶ D1 `DB` (users/sessions/pages)
GET /<slug>/* │ routes/serve.rs page content serving │──▶ R2 `PAGES` (<slug>/<path>)
│ │
│ core/ pure policy logic (native- │
│ tested): slug, mime, upload │
│ validation, security headers │
└─────────────────────────────────────────┘
Component interactions
- core/ has no Cloudflare dependencies; it compiles natively and holds every security-relevant decision (slug grammar, reserved names, zip limits, MIME allowlist, CSP header values). Unit tests live here.
- db.rs (wasm-only) wraps D1 queries; routes/ (wasm-only) are thin axum handlers that call core + db + R2.
- auth.rs builds OAuth URLs and cookie/session token logic (pure parts native-tested); the GitHub HTTP calls are wasm-only.
- static/index.html is the whole management UI, embedded at compile time
with
include_str!and served at/. Publishing happens in one modal used by two flows (pages-ytau): "+ New Page" (POST /api/pages?name=<slug>) and a per-row "Update" (PUT /api/pages/<slug>, slug fixed and read-only).
Page content lifecycle
A page's bytes live under one R2 prefix, <slug>/, with a D1 pages row
holding its owner, stats, and trust flag. The three mutating paths differ only
in how they treat that prefix (see design §4.6, §4.6b, §4.3):
- Create (
POST) reserves the slug in D1 first, then writes R2 objects; any failure rolls both back, since the slug held nothing before. - Replace (
PUT) writes the new objects over the prefix, then prunes the old keys the new upload didn't overwrite, then updates the D1 stats — preservingowner_id,trusted, andcreated_at. It cannot roll back (R2 has no multi-object transaction), so the ordering is chosen so a viewer mid-update sees old-or-new content per file, never a blank page. - Delete lists the prefix, deletes every object, then drops the D1 row.
Trust boundaries
- Uploaded content is untrusted: validated in core/upload.rs, stored in
R2, and always served with a sandboxing CSP (opaque origin) so it can
never make credentialed calls to
/api. - Browser → API: session cookie (
__Host-, HttpOnly, SameSite=Lax, hashed in D1) + Origin check on mutating routes. - Worker → GitHub: client secret only ever used server-side.