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.
59 lines
3.2 KiB
Markdown
59 lines
3.2 KiB
Markdown
# 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 —
|
|
preserving `owner_id`, `trusted`, and `created_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
|
|
|
|
1. **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`.
|
|
2. **Browser → API**: session cookie (`__Host-`, HttpOnly, SameSite=Lax,
|
|
hashed in D1) + Origin check on mutating routes.
|
|
3. **Worker → GitHub**: client secret only ever used server-side.
|