|
|
# pages — Planning
|
|
|
|
|
|
## Phases
|
|
|
|
|
|
| Phase | Content | Status |
|
|
|
|---|---|---|
|
|
|
| 0 | Security analysis + design (`plans/2026-07-12-pages-design.md`) | done |
|
|
|
| 1 | v0 implementation on branch `pages-v0` (beans below) | done |
|
|
|
| 2 | Deploy + manual QA against real Cloudflare account | deployed; manual QA (login/upload/delete) pending |
|
|
|
| 3 | v1 candidates: per-page subdomains, page overwrite, Yew UI, OpenAPI | not started |
|
|
|
|
|
|
## v0 tickets (beans, serial)
|
|
|
|
|
|
1. scaffold — crate skeleton, wrangler.toml, hello-world worker builds
|
|
|
2. core — slug/mime/upload/header policy modules + unit tests
|
|
|
3. db — D1 schema + query layer
|
|
|
4. auth — GitHub OAuth, sessions, auth middleware
|
|
|
5. api — pages + users JSON API
|
|
|
6. serve — page content serving from R2
|
|
|
7. frontend — embedded management UI
|
|
|
8. infra — OpenTofu + deployment docs
|
|
|
|
|
|
## Work log
|
|
|
|
|
|
- **2026-07-12** — Project initialized. Security analysis and v0 design
|
|
|
written; beans created; implementation pipeline started (Sonnet
|
|
|
implementation agents, Opus review agents, one commit per ticket).
|
|
|
- **2026-07-13** — infra ticket (pages-ihlp) done: `infra/*.tf` (R2 bucket,
|
|
|
D1 database, DNS placeholder record, rate limiting ruleset) mirroring
|
|
|
`../quotesdb/infra` style, `infra/README.md` deployment guide, `tofu fmt`
|
|
|
+ `tofu validate` passing.
|
|
|
- **2026-07-13** — v0 implementation complete: all 8 beans done on
|
|
|
`pages-v0`, one reviewed commit per ticket (Sonnet implementation, Opus
|
|
|
review). 142 unit tests + 17 doctests; both-target clippy clean. Next:
|
|
|
phase 2 (real deploy + manual QA per `infra/README.md`).
|
|
|
- **2026-07-13** — Deployed. tofu applied R2 + D1 + DNS (rate-limit ruleset
|
|
|
deliberately NOT applied — quotesdb's live http_ratelimit ruleset owns
|
|
|
the zone's single slot; merge required if wanted). Remote schema applied;
|
|
|
GITHUB_CLIENT_SECRET secret set; route pages.elijah.run/* live. Smoke
|
|
|
tests pass (/, /api/me, /auth/login 302, reserved + unknown slugs 404
|
|
|
with sandbox CSP). Pending manual QA: GitHub login as admin, upload,
|
|
|
view, delete.
|
|
|
- **2026-07-13** — pages-shqc: per-page/per-user "trusted" escape hatch from
|
|
|
the CSP sandbox (`pages.trusted OR owner's users.trusted`, computed at
|
|
|
serve time). `migrations/0002-trust.sql` added for the live DB (not yet
|
|
|
applied remotely — manual step); schema.sql, db.rs, api.rs, serve.rs,
|
|
|
core/headers.rs, and the admin UI all updated; 148 unit + 18 doctests,
|
|
|
both-target clippy clean.
|
|
|
- **2026-07-14** — pages-ytau: in-place page updates, retiring the design's
|
|
|
"no page overwrite: delete + re-upload" scope cut. New `PUT
|
|
|
/api/pages/:slug` (design §4.6b) replaces a page's content — owner-or-admin
|
|
|
*and* `can_upload`, new objects written before stale keys are pruned, D1
|
|
|
stats refreshed while `owner_id`/`trusted`/`created_at` are preserved. No
|
|
|
schema change. The UI's always-visible upload card became a modal driven by
|
|
|
a "+ New Page" button and a per-row "Update" button (slug pre-filled,
|
|
|
read-only). 152 unit + 19 doctests, both-target clippy clean. Verified end
|
|
|
to end against `wrangler dev` with local D1/R2 (create → serve → replace →
|
|
|
stale assets 404 → stats/trust/ownership preserved; 401/403/404/400/413
|
|
|
paths) and the UI driven in jsdom (37 assertions).
|
|
|
|
|
|
Local-dev gotcha found while verifying: `wrangler dev` rewrites the request
|
|
|
`Origin` to the `[[routes]]` host (`pages.elijah.run`), so a local
|
|
|
`BASE_URL=http://localhost:8787` makes every mutating route 403 on the
|
|
|
Origin check. Set `BASE_URL=http://pages.elijah.run` in `.dev.vars` when
|
|
|
exercising the API locally.
|
|
|
- **2026-07-21** — pages-9lsk: page **aliases** (design §4.1). A page keeps
|
|
|
one canonical `slug` and gains up to 10 removable, long-lived alternate
|
|
|
names in the same URL namespace. New `aliases` table
|
|
|
(`migrations/0003-aliases.sql`, idempotent `CREATE TABLE IF NOT EXISTS`) +
|
|
|
`idx_aliases_slug`; namespace uniqueness across `pages.slug`+`aliases.alias`
|
|
|
is enforced in the handlers (SQLite can't express a cross-table rule).
|
|
|
Serving is **serve-direct**: `routes::serve` falls back to
|
|
|
`Db::get_page_by_alias` on a canonical miss and serves the canonical page's
|
|
|
R2 content under its `<slug>/` prefix — no redirect, no second copy, and a
|
|
|
real page always wins over an alias of the same name. New `POST
|
|
|
/api/pages/:slug/aliases` (can_upload + owner-or-admin) and `DELETE
|
|
|
/api/pages/:slug/aliases/:alias` (owner-or-admin); `create_page` now rejects
|
|
|
a name held by an alias; `delete_page` removes a page's aliases;
|
|
|
`GET /api/pages` returns each page's `aliases[]`. Management UI shows alias
|
|
|
chips (with × to remove) and an "Add alias" button. No R2/behavioral change
|
|
|
to existing pages. Replaced the earlier rename+conditional-redirect idea
|
|
|
(dropped — 301-cache fragility). 196 unit + 20 doctests, both-target clippy
|
|
|
clean.
|