Skip to main content

max / makenotwork

launch-eve audit pass: Ultra Fuzz Runs #8-9 + cross-cutting sweeps server/ Run #8 (earlier today): all 5 axes brought to A-; new src/background.rs bounded mpsc + semaphore queue replaces 22 per-request tokio::spawn sites; cart min_price_cents/chain-break MEDs fixed; item-wizard pricing_model silent fallback fixed; inline-JS copy-link migrated to delegated handler; cart free-claim N+1 closed; commit_rescan helper extends chronic-disease seal to admin paths; new migrations 123-130, 133 (backup-code prefix, completion_effects, scan_status index, sync_apps loopback, dead-letter table, ip index, non-negative duration); 7-wave backlog sweep closed 24/26 carried items. Run #9 (this session): launch-eve deep pass surfaced + fixed - UX-CRITICAL: signup TOCTOU race 23505 -> 500 + form loss (join_wizard.rs catches 23505 with constraint-name routing) - Sec-SERIOUS: delete_all_sessions_for_user non-atomic JWT bump (sessions.rs wrapped in pool.begin() / tx.commit()) - Sec-SERIOUS: 2FA login-email IP spoofable via bare x-forwarded-for (two_factor.rs uses helpers::extract_client_ip) - Pay-SERIOUS: webhook dual-failure 503 short-circuited Stripe retry (webhook/mod.rs calls unmark_event_processed before 503) Deferred with rationale in docs/audit_review.md + todo.md: 1 SERIOUS (subscription webhook ordering), 3 HIGH (dead-letter unused, reqwest per-request x5, unbounded cleanup spawn), 7 MED, 8 LOW. §1.1 public surface: OG/Twitter meta in base.html, static/manifest.json, error.html contact link, sitemap.rs with in-memory cache. info@ email pin across 8 files. doc-fuzz/exorcise/nitpick/security-review passes complete. sando/ daemon build + main updates, deploy systemd unit + config example, post-receive hook, BOM doc edits. Launchplan §1.5 A- bar holds across all 5 axes.
Author: Max Johnson <me@maxj.phd> · 2026-06-01 00:37 UTC
Signed with PGP, not checked
Commit: 3dc8dca7a120c483dd2fcef8c9f34ccad19847a6
Parent: eee96a7
155 files changed, +4434 insertions, -1099 deletions
@@ -4,9 +4,13 @@
4 4 # unlock promotion *to* the next tier, the nodes it ships to, and the canary
5 5 # policy for shipping within the tier.
6 6 #
7 - # Day-one wiring: MM (local) -> A (testnot.work) -> B (prod-1). C is declared
8 - # but not provisioned; adding the second prod node later is a config edit
9 - # (set provisioned = true, fill in [[tier.node]]).
7 + # Day-one wiring: host (pop-os, local) -> A (testnot.work) -> B (prod-1). C is
8 + # declared but not provisioned; adding the second prod node later is a config
9 + # edit (set provisioned = true, fill in [[tier.node]]).
10 + #
11 + # Note: the host tier is named "mm" for legacy reasons (string identifier baked
12 + # into the schema + code). It refers to whatever machine sandod runs on —
13 + # currently pop-os, not a MakeMachine. Rename is a follow-up cleanup.
10 14
11 15 [repo]
12 16 bare_path = "/srv/sando/mnw.git"
@@ -18,7 +22,7 @@
18 22 source = "rsync://astra/var/backups/mnw/latest.sql.gz"
19 23 local_path = "/srv/sando/backups/latest.sql.gz"
20 24
21 - # ---- MM: local pre-staging gate ----
25 + # ---- host: pop-os local pre-staging gate ----
22 26 [[tier]]
23 27 name = "mm"
24 28 provisioned = true
@@ -28,7 +32,7 @@
28 32 { kind = "migration_dry_run" },
29 33 { kind = "boot_smoke" },
30 34 ]
31 - # MM is the daemon's own host; no remote node row.
35 + # Host is the daemon's own machine (pop-os); no remote node row.
32 36
33 37 # ---- A: testnot.work staging ----
34 38 [[tier]]
M sando/todo.md +25 -33
@@ -4,7 +4,9 @@
4 4
5 5 Format rule: every actionable line is a `- [ ]` checkbox. Headings group phases and themes; do not put status updates in them.
6 6
7 - Roadmap target: replace `server/deploy/deploy.sh` and astra-hosted `server/deploy/run-ci.sh` with Sando running on the MakeMachine, gating Hetzner prod through testnot.work.
7 + Roadmap target: replace `server/deploy/deploy.sh` and astra-hosted `server/deploy/run-ci.sh` with Sando running on **pop-os**, gating Hetzner prod through testnot.work.
8 +
9 + **Host decision:** Sando runs on pop-os (x86_64 Ubuntu-derived, systemd). Architecturally closest to Hetzner prod, no cross-compile, no init-system split. MakeMachine and EveryCycle are now a separate project — not Sando's concern.
8 10
9 11 Phases are ordered for execution. Phase 0 must finish before Phase 1 is meaningful. Phases 5+ are post-cutover hardening.
10 12
@@ -13,11 +15,11 @@
13 15 Read these to orient before working on Sando:
14 16
15 17 - `README.md` — quickstart, API surface, v0 limitations
16 - - `sando.toml` — current topology (MM → A → B; C declared, not provisioned)
18 + - `sando.toml` — current topology (host → A → B; C declared, not provisioned)
17 19 - `daemon/src/main.rs` — startup sequence (config → topology → migrate → sync → bare-repo bootstrap → serve)
18 20 - `daemon/src/routes.rs` — `/state`, `/promote`, `/rollback`, `/rebuild`, `/backup/fetch`, `/events`
19 21 - `daemon/src/gates.rs` — gate runners; the load-bearing logic
20 - - `daemon/src/build.rs` — `build_and_run_mm` is the MM-tier pipeline
22 + - `daemon/src/build.rs` — host-tier build pipeline
21 23 - `daemon/src/deploy.rs` — `deploy_local`; remote SSH stub
22 24 - `daemon/migrations/001_init.sql` — schema (tiers/nodes as rows)
23 25 - `server/deploy/deploy.sh` — current cross-compile + push-to-Hetzner script (what we are replacing)
@@ -26,33 +28,23 @@
26 28
27 29 ---
28 30
29 - ## Phase 0 — MakeMachine bootstrap
31 + ## Phase 0 — pop-os bootstrap
30 32
31 - Hardware and base provisioning. None of the remote-deploy work below matters until MM exists.
32 -
33 - **Platform decision: MM runs Mountaineer.** MM is the first real Mountaineer deployment and Sando is its first real sysop helper (principle 14). Hetzner prod stays on its current distro for now; the Mountaineer-for-prod question is deferred at least a year. If MM-on-Mountaineer ever blocks an MNW deploy for more than a day, fall back to Ubuntu on MM — capture the trigger in `plans/mm-platform-fallback.md` before flipping the install.
34 -
35 - - [ ] Purchase MakeMachine hardware per `plans/mm-hardware-bom.md` (Threadripper Pro 7975WX + WRX90D8-2L/2T + 512 GB ECC RDIMM + 2× 4 TB Gen5 NVMe; ~$10.5K including A1 GPU). Dual-use as Sando host + EveryCycle dev box — see `~/hardware/everycycle/docs/roadmap.md` for the EveryCycle side.
36 - - [ ] Install Mountaineer (ZFS root, s6+s6-rc init, nushell, podman). Use the latest Dull Edge build available, or hand-roll from `side_projects/mountaineer/` if no release has shipped yet.
37 - - [ ] Write `plans/mm-platform-fallback.md`: explicit trigger conditions for re-imaging MM with Ubuntu, plus the swap-in procedure (which env files, which binaries, which directories to preserve).
38 - - [ ] Join MM to tailnet; allocate a stable hostname and record in `_meta/infra_tailnet.md`.
39 - - [ ] Provision `sando` system user; lock down the home dir; set up scoped SSH keys for outbound deploys.
40 - - [ ] Install scratch Postgres locally on MM (via apk); create the `sando_scratch` role + DB used by `migration_dry_run`.
41 - - [ ] Write Sando's s6-rc service definition (`sandod` long-run service, dependency on tailscale and postgres, restart on failure, env from `/etc/sando/sando.env`). Contribute upstream to Alpine if the definition turns out general enough — see Mountaineer principle on giving back.
42 - - [ ] Install `sandod` binary at `/usr/local/bin/sandod`; bring up the s6 service.
43 - - [ ] Write the production `sando.toml`; bare repo path under `/srv/sando/mnw.git`; A node `testnot.work`; B node Hetzner prod. Use `node.init = "systemd"` for the Hetzner nodes (see Phase 1).
44 - - [ ] Verify MNW server builds reproducibly on Mountaineer (musl libc vs glibc — sqlx/tokio/axum should be fine but confirm before relying on it). Capture any musl-specific surprises in `plans/mm-build-notes.md`.
33 + - [x] Provision `sando` system user on pop-os; lock down home dir; generate SSH keypair at `/srv/sando/.ssh/id_ed25519` for outbound deploys.
34 + - [x] Install scratch Postgres locally on pop-os; create `sando_scratch` role + DB used by `migration_dry_run`. (Owner of own DB; non-superuser.)
35 + - [x] Write systemd unit for `sandod` (long-run service, restart on failure, env from `/etc/sando/sando.env`). Installed at `/etc/systemd/system/sandod.service`.
36 + - [x] Write the production `sando.toml`; bare repo path under `/srv/sando/mnw.git`. Installed at `/etc/sando/sando.toml`; daemon config at `/etc/sando/sando-daemon.toml`.
37 + - [x] Install `sandod` binary at `/usr/local/bin/sandod`; enable + start the service. Live on `100.103.89.95:7766`; bare repo auto-bootstrapped at `/srv/sando/mnw.git`.
38 + - [ ] Verify MNW server builds reproducibly on pop-os.
39 + - [ ] Register sando pubkey with Hetzner prod (`deploy@alpha-west-1`) and testnot.work once that node exists. Pubkey: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEK+vhpr1V8VnsEemN9x6tAA2S05kmv/mQ3eVgSXSkJ8 sando@pop-os`.
45 40
46 41 ## Phase 1 — Remote deploy
47 42
48 - The MVP only deploys to `ssh_target=local`. Production needs real SSH/rsync, and the init-system split (MM on s6, Hetzner on systemd) needs a backend abstraction from day one.
43 + The MVP only deploys to `ssh_target=local`. Production needs real SSH/rsync.
49 44
50 - - [ ] Add `node.init` field to `sando.toml`: `"systemd" | "s6" | "local"`. Default `"systemd"` for backwards-compat. Every node declares its init explicitly so a future Hetzner-on-Mountaineer move is a TOML edit, not a Sando code change.
51 - - [ ] Refactor `deploy.rs` around an `InitBackend` trait with `reload_or_restart(unit_name) -> Result<()>` and `unit_path(release_root, version) -> PathBuf`. Two impls: `Systemd` (shells `systemctl reload-or-restart`) and `S6` (shells `s6-svc -r` against the service dir). `Local` impl is a no-op restart for dev.
52 - - [ ] Implement `deploy::deploy_node` remote path: rsync the staged binary to `<ssh_target>:<release_root>/releases/<version>/server`, then `ssh <ssh_target>` runs `ln -sfn releases/<version> current` plus the init-backend-appropriate reload.
53 - - [ ] Settle service-name convention. Current MNW server systemd unit is `makenotwork.service`; on s6 it would be `/etc/s6-rc/sv/mnw-server/`. Capture both names + the migration plan in `plans/service-names.md` before changing anything live.
54 - - [ ] Add `node.service_name` field to `sando.toml` (default derives from tier+role) so the convention is explicit per-node and backend-agnostic.
55 - - [ ] Bootstrap script for adding a fresh node: creates `<release_root>`, installs the init-backend-appropriate service definition pointing at `<release_root>/current/server`, adds the sando SSH key to `authorized_keys`. Idempotent. One script per backend, or one script that branches on init kind.
45 + - [ ] Implement `deploy::deploy_node` remote path: rsync staged binary to `<ssh_target>:<release_root>/releases/<version>/server`, then `ssh <ssh_target>` runs `ln -sfn releases/<version> current` plus `systemctl reload-or-restart <service>`.
46 + - [ ] Add `node.service_name` to `sando.toml` (default `makenotwork.service`).
47 + - [ ] Bootstrap script for adding a fresh node: creates `<release_root>`, installs the systemd unit pointing at `<release_root>/current/server`, adds the sando SSH key to `authorized_keys`. Idempotent.
56 48 - [ ] Garbage-collect old releases on the remote: keep last N (configurable, default 5) per node. Run at end of each successful deploy.
57 49 - [ ] Handle `rsync` failure mid-deploy: leave the previous `current` symlink intact; mark `deploys.outcome = 'failed'`; do not advance `tier_state`.
58 50
@@ -62,7 +54,7 @@
62 54
63 55 - [ ] Confirm astra's offsite replica (per `sync-backup-offsite.sh`) writes a deterministic latest-link path Sando can rsync from. If not, add one.
64 56 - [ ] Wire the production `sando.toml` `backup.source` to the astra rsync URL.
65 - - [ ] Schedule a daily `POST /backup/fetch` (cron or systemd timer on MM) so a fresh backup is always within 24h of any promote attempt.
57 + - [ ] Schedule a daily `POST /backup/fetch` (systemd timer on pop-os) so a fresh backup is always within 24h of any promote attempt.
66 58 - [ ] First end-to-end `migration_dry_run` against a real prod backup; confirm it catches the 2026-05-22 incident class (drop+recreate column migration sequence).
67 59 - [ ] Document the failure modes: what does the operator see in `/state` when the dry-run fails? Capture in `plans/migration-dryrun-failures.md`.
68 60 - [ ] Decide retention on `backups` table — prune rows older than N days so SQLite doesn't grow forever.
@@ -77,7 +69,6 @@
77 69 - [ ] **Error pages** — static HTML in `server/deploy/error-pages/`. Either bake into the binary (preferred — versions with code) or ship as a `releases/<version>/error-pages/` sibling. Capture decision.
78 70 - [ ] **Security configs** — `sshd-git.conf`, `fail2ban-sshd.conf`, `setup-firewall.sh`. Move to node-bootstrap.
79 71 - [ ] **Restart warning** — `deploy.sh send_restart_warning` posts a banner before restart. Decide whether Sando emits this and through what surface (probably the existing in-app banner mechanism).
80 - - [ ] **Cross-compile from macOS** — `deploy.sh` builds on the dev laptop via `cargo-zigbuild`. Sando builds natively on MM (x86_64 Linux). Verify the resulting binaries are byte-identical or at least behavior-equivalent across one full sprint before retiring `deploy.sh`.
81 72 - [ ] **Prod migrations** — today, who runs `sqlx migrate run` against prod? `deploy.sh` doesn't (verify). Sando should run prod migrations as part of `POST /promote/{tier}` for the prod tiers, OR there should be an explicit `POST /migrate/{tier}` operator action. Decide.
82 73
83 74 ## Phase 4 — Cutover
@@ -99,15 +90,15 @@
99 90 - [ ] TUI: actions pane. `p` for promote (prompts for version + tier), `R` for rollback, `b` for backup fetch, `c` for manual_confirm.
100 91 - [ ] `POST /confirm/{tier}` endpoint that inserts a `gate_runs` row with `passed=1, gate_kind='manual_confirm'` for the current pending version. Replaces the hand-SQL workaround.
101 92 - [ ] TUI live log pane that follows the most recent build / gate run; backed by `WS /events`.
102 - - [ ] `POST /promote` body should accept `version` as optional; default to the current MM version when target is A, predecessor's current when target is B+. Reduces ceremony.
93 + - [ ] `POST /promote` body should accept `version` as optional; default to the current host version when target is A, predecessor's current when target is B+. Reduces ceremony.
103 94
104 95 ## Phase 6 — Monitoring + alerting
105 96
106 - - [ ] Wire MM's `/metrics` endpoint into the existing MNW Prometheus scrape config; record where the scrape config lives in `_meta/` or wherever monitoring already runs.
97 + - [ ] Wire pop-os `/metrics` endpoint into the existing MNW Prometheus scrape config; record where the scrape config lives in `_meta/` or wherever monitoring already runs.
107 98 - [ ] Add counters: `sando_builds_total{outcome}`, `sando_gates_total{tier,kind,outcome}`, `sando_deploys_total{tier,outcome}`, `sando_burn_in_remaining_hours{tier}`.
108 99 - [ ] Alert: build failed. Page on first failure (not flap-protected — builds are infrequent).
109 100 - [ ] Alert: migration_dry_run failed. Page immediately. This is the 2026-05-22-class signal.
110 - - [ ] Alert: a tier has had `current_version` unchanged for > N days while MM is green. (Operator forgot to promote.)
101 + - [ ] Alert: a tier has had `current_version` unchanged for > N days while host is green. (Operator forgot to promote.)
111 102
112 103 ## Phase 7 — Multi-node B+C
113 104
@@ -125,7 +116,7 @@
125 116
126 117 - [ ] Provision Postgres-only machine D (modest spec; reliability over performance).
127 118 - [ ] Migrate the prod DB from Hetzner app node to D. Capture procedure in `plans/postgres-d-migration.md`.
128 - - [ ] Update `server` `DATABASE_URL` everywhere (env files on B+C, scratch URL on MM stays local).
119 + - [ ] Update `server` `DATABASE_URL` everywhere (env files on B+C, scratch URL on pop-os stays local).
129 120 - [ ] Replica/HA story stays deferred; D is SPOF for now (per `_meta/preclear/.../decisions.md`).
130 121
131 122 ## Phase 9 — Hardening
@@ -135,10 +126,11 @@
135 126 - [ ] Tailnet ACL audit: confirm only the laptop can reach `sandod:7766`. Document the ACL.
136 127 - [ ] Decide if v0.2 needs token auth on `sandod` endpoints (revisit assumption from `decisions.md` once there's a real second operator).
137 128 - [ ] Sando self-deploy: Sando builds and deploys *itself* through its own pipeline. Bootstraps the bootstrap. Closes the chicken-and-egg loop and is satisfying.
138 - - [ ] Backup-of-Sando-state: nightly SQLite snapshot to astra. The state DB tracks 6 months of deploys; losing it on a MM disk failure would be annoying.
129 + - [ ] Backup-of-Sando-state: nightly SQLite snapshot to astra. The state DB tracks 6 months of deploys; losing it on a pop-os disk failure would be annoying.
139 130
140 131 ## Notes / non-checkbox
141 132
142 - - WS `/events` and the operator-UX work in Phase 5 can run in parallel with Phase 1-3 once MM exists. They are sequenced after for review clarity, not because they block anything.
133 + - WS `/events` and the operator-UX work in Phase 5 can run in parallel with Phase 1-3 once Phase 0 is done. They are sequenced after for review clarity, not because they block anything.
143 134 - "Hotfix override" and `reset_burn_in` flag are already implemented end-to-end (see `decisions.md`); not on this list because there's nothing left to do until prod uses them.
144 135 - C tier exists in the schema as a `provisioned=false` row from day one — adding C in Phase 7 is a TOML edit, not a migration.
136 + - MakeMachine + EveryCycle are now a separate project. The hardware BOM (`plans/mm-hardware-bom.md`) should move there when that project gets its own repo.
M server/todo.md +298 -1
@@ -1,6 +1,303 @@
1 1 # MNW Server — Todo
2 2
3 - Done: Ultra Fuzz Runs #1-#4 fully closed (all phases, the 41-test integration triage, and the third-party credits page at /docs/credits linked from the footer). Active: no Run #4 work remaining.
3 + **Last updated:** 2026-05-31 late evening (post Run #9 — launch-eve pass).
4 +
5 + ## Status
6 +
7 + All 5 axes at A- after Run #9 fixes. **0 CRITICAL open · 1 SERIOUS open (deferred) · 3 HIGH open (deferred) · 7 MED open (deferred).** Launchplan §1.5 A- bar holds. See `docs/audit_review.md` Run #9 section for full triage.
8 +
9 + ## Run #9 — fixed this session (2026-05-31)
10 +
11 + - **UX-CRITICAL** Signup TOCTOU 23505 → 500 + form loss. `join_wizard.rs`: catch 23505 with constraint-name routing, surface as `return_error`. Follow-up: preserve typed form fields on error swap (Phase 4).
12 + - **Sec-SERIOUS** `delete_all_sessions_for_user` non-atomic JWT bump → wrapped in `pool.begin()` / `tx.commit()` (`db/sessions.rs:247`).
13 + - **Sec-SERIOUS** 2FA login-email IP spoofable via bare `x-forwarded-for` → swapped to `crate::helpers::extract_client_ip` (`routes/pages/public/two_factor.rs:308`).
14 + - **Pay-SERIOUS** Webhook dual-failure 503 short-circuited on Stripe retry → call `unmark_event_processed` before returning 503 (`routes/stripe/webhook/mod.rs:81`).
15 +
16 + `cargo check --tests` clean; targeted unit tests (sessions/webhook/two_factor/join_wizard) 33/33 green. Full DB-integration suite needs astra postgres.
17 +
18 + ## Run #9 — deferred with rationale (Phase 4)
19 +
20 + - [ ] **Pay-SERIOUS** Subscription webhook out-of-order events resurrect `active`. Needs `created`-timestamp re-extraction from `UntypedEvent` + `WHERE last_event_at <= $created` guards across Fan+/creator-tier/synckit subscription writes. Cross-cutting; worst case is minutes-window of restored access until next webhook.
21 + - [ ] **Sto-HIGH** Migration 129 dead-letter table never written (`cleanup.rs:453`). Operational visibility, not runtime; one-INSERT fix.
22 + - [ ] **Perf-HIGH** Per-request `reqwest::Client::new()` in 5 hot paths (dashboard/main, public/landing, api/internal/cli_features, api/domains, auth.rs). Hoist to OnceLock or AppState pooled client.
23 + - [ ] **Perf-HIGH** Unbounded `tokio::spawn` in `cleanup.rs:215-220` `spawn_expired_account_cleanups`. Lift existing `CLEANUP_PARALLELISM=4` JoinSet pattern from `cleanup_sandbox_accounts` 100 lines above.
24 + - [ ] **Pay-MED** `pricing.rs::parse_dollars_to_cents` strips European decimal comma; `1,23` → 12300¢.
25 + - [ ] **Pay-MED** SyncKit app-sub checkout silently defaults `storage_limit_bytes` to 0 if metadata missing.
26 + - [ ] **Pay-MED** Guest checkout email sentinel `"unknown@guest"` collision risk.
27 + - [ ] **Sto-MED** `is_s3_key_live` 7 EXISTS subqueries on unindexed s3_key columns — sequential scans per retry. Add partial indexes WHERE NOT NULL.
28 + - [ ] **Sto-MED** `is_s3_key_live` LIKE suffix `'%' || s3_key` false-positives on neighboring keys → S3 object leaks. Anchor with `/`.
29 + - [ ] **UX-MED** `purchase.html:145` `?return_to=` dead-wired; login handler always redirects `/dashboard`.
30 + - [ ] **UX-MED** Admin user filter buttons (`admin-users.html:35-44`) use `class="primary"` instead of `btn-primary` — renders unstyled.
31 +
32 + ## Run #9 — LOW/NOTE (carry forward)
33 +
34 + - [ ] **UX-LOW** Pagination links in `git/issues.html:72,76` don't URL-encode `search` param.
35 + - [ ] **UX-LOW** 5 sites use `.render().unwrap_or_default()` on Askama templates — blank UI on render failure, no log line.
36 + - [ ] **UX-LOW** `slugify` (`formatting.rs:85`) produces `"post"` for any non-ASCII title.
37 + - [ ] **Sec-MINOR** `csrf.rs:176-185` `validate_token_consuming` doesn't actually consume — rename or rotate.
38 + - [ ] **Sec-MINOR** `routes/oauth.rs:101-111` `is_localhost_redirect` allows any port regardless of registered URI.
39 + - [ ] **Sec-MINOR** `scanning/archive.rs:124` path-traversal check misses lone `..` segment (no trailing `/`).
40 + - [ ] **Perf-LOW** `db/page_views.rs` `pending` HashMap has no max-cardinality cap.
41 + - [ ] **Perf-LOW** `build_runner.rs:441` artifact tmpfile leaks if process crashes between SCP and `remove_file`.
42 +
43 + Live state: working tree has 104+ Run #8 files plus 4 Run #9 files (`join_wizard.rs`, `sessions.rs`, `two_factor.rs`, `webhook/mod.rs`, `docs/audit_review.md`, `todo.md`).
44 +
45 + ## Open before launch (Monday 2026-06-01)
46 +
47 + ### Platform-as-product audits (skill-driven, code-review scope; fresh context recommended)
48 + - [ ] `/creator-fuzz` — would a working creator trust this with their livelihood?
49 + - [ ] `/use-fuzz` — discoverability, learnability, first-five-minutes
50 + - [ ] `/business-fuzz` — pricing copy, fee surfacing, refund-policy wording vs actual platform behaviour
51 +
52 + ### Per-project hygiene (manual, my call when ready)
53 + - [ ] README first-screen audit — what is this / who is it for / where to get it / what does it cost. No headliner paragraphs.
54 + - [ ] `Cargo.toml` version bump for the launch deploy (pick the number; I do the edit if needed)
55 + - [ ] CHANGELOG entry for the launch version
56 +
57 + ### Monday browser/prod testing (saved for Monday per current direction)
58 + - [ ] §1.1 Walk every public page: footer present, OG/Twitter meta render correctly in Facebook + Twitter debuggers, error pages render via forced 404/403/500
59 + - [ ] §1.2 First-run creator flow end-to-end in production: signup → Stripe Connect → first item upload
60 + - [ ] §1.3 Each seeded creator's `/{handle}` page renders without empty sections; sample item per medium (audio/video/text/download) reachable from `/discover`
61 + - [ ] §1.4 Production deploy of post-fuzz build + version recorded via `record_deploy`; scheduled jobs running on prod (cleanup, scan jobs retention, build reaper, broadcast fan-out); Stripe webhook reachable from dashboard ping; backup snapshot taken pre-launch + restoration path documented in `_private/docs/mnw/server-docs/`; `/health` green
62 + - [ ] §5 launch-day sequence: final deploy, smoke-test logged-out from non-dev machine, update bios/link-in-bio/handles, confirm `maxj.phd` resolves, tag launch commit (`git tag launch-2026-06-01`)
63 +
64 + ## Open question for the user (action before Monday)
65 +
66 + - [ ] **Confirm all role-based email addresses route to real mailboxes**: `info@`, `security@`, `dmca@`, `privacy@`, `dpo@`, `legal@`, `billing@`, `policy@`, `reports@`, `community@`, `appeals@`, `press@`, `noreply@`. Legal pages (terms, privacy, copyright, appeals) and several role-routed flows reference them. If any are aspirational, that's a launch risk for the legal pages and an inbound-mail blackhole. Verify with Postmark/forwarding setup.
67 +
68 + ## Deferred with rationale (no action; documented)
69 +
70 + - [ ] `build_runner.rs:151` serial-target loop. LOW; builds run rarely; refactor touches denominator + error aggregation + log order. Post-launch.
71 + - [ ] `scheduler/mod.rs:92-279` advisory-lock per-tier granularity. Multi-replica concern; defer until multi-replica is real.
72 + - [ ] Drop unused `completion_effects` table (migration cleanup, schema-only).
73 + - [ ] Templatize founder annual prices in `tiers.md` (e.g. `$54/yr`, `$108/yr`, `$162/yr`, `$324/yr`). docengine substitutions don't support arithmetic; would require adding derived `tiers.founding.basic_annual` etc keys in `shared/docengine/src/assumptions.rs`. Not blocking.
74 + - [ ] `_head_assets.html` apple-touch-icon + manifest link wiring. `static/manifest.json` exists but the `<link rel="manifest">` was reverted; bring back if/when desired.
75 + - [ ] Migrate footer's `What's new` and `Shortcuts` `<a href="#" onclick="...">` to `data-*` attributes following the `data-copy-link` pattern. UX MED, not blocking.
76 +
77 + ## What's done this session (compact summary, full details below)
78 +
79 + - **Ultra Fuzz Run #8** — all 5 axes A-. SERIOUS webhook unbounded spawn closed via new `src/background.rs` (bounded mpsc + semaphore-bounded concurrent execution). `spawn_email!` macro migrated; 17 callers + 5 manual webhook spawns + 5 same-disease per-request email spawns now route through bg queue. Run #8 5 new MEDs all closed (cart `min_price_cents`, cart-all chain-break, item-wizard `pricing_model`, inline-JS templates, cart free-claim N+1). Previously-deferred Payments H2 `claim_free_project` race closed.
80 + - **7-wave backlog sweep** — 24 of 26 carried items across auth/security/scanning/db/storage/UX/perf/payments. New schema migration `133_items_duration_seconds_nonnegative.sql`. New `commit_rescan` helper extends chronic-disease seal to admin paths. Two LOW items deferred above.
81 + - **4 cross-cutting sweeps** — `info@makenot.work` email pin (8 files), localhost/TODO/emoji/secret scans all clean.
82 + - **§1.1 public-surface code work** — OG + Twitter card meta in `base.html` (per-page overridable blocks), `static/manifest.json` created with brand colours, `error.html` drops broken back button + adds contact link, `Contact` link added to footer (mailto:info@), new `routes/pages/public/sitemap.rs` (with in-memory 10-min cache + LIKE-wildcard escape from the security review).
83 + - **Doc-fuzz** — `content-scanning.md` restructured (Malware checks + Authenticity checks sections, added URLhaus/MetaDefender/signing layers), `policy.html` See-also block linking 6 legal pages, `tiers.md` prose prices templatized via `{{ tiers.standard.* | int }}`.
84 + - **Exorcise** — 9 AI-tell removals across compare.md, content-scanning.md, appeals.md, faq.md.
85 + - **Nitpick** — 2 polish edits (dead `let _ = scan_status` removed, unused tuple-name destructure tidied).
86 + - **Security review** — 2 MEDs fixed inline: sitemap.xml in-memory cache to absorb crawler/attacker hammering; LIKE-wildcard escape on `is_s3_key_live` to prevent `_` in s3_keys from false-positive matching.
87 +
88 + ---
89 +
90 + ## Ultra Fuzz 2026-05-31 (Run #8 — final re-grade)
91 +
92 + ### Above-MED items to address before launch (or defer with rationale)
93 +
94 + - [x] **Perf SERIOUS — Webhook hot-path unbounded `tokio::spawn`.** Fixed 2026-05-31. New `src/background.rs` with `BackgroundTx` + bounded mpsc (1024) + semaphore-bounded concurrency (8 workers vs 25 pool conns). `spawn_email!` macro refactored to use the bg queue (covers 17 callers). 5 manual webhook spawns migrated (`checkout_helpers.rs:58, 96, 124, 290` + `checkout.rs:618`). Same-disease per-request email spawns also migrated: postmark issue replies (×2), guest-claim email, join-wizard signup (×2). `cargo test --lib` 1654 / 0. Deferred (different shapes): import pipeline (long-running), MT community create (HTTP not pool), departure/status broadcast (broadcast-class), idempotency store (trivial).
95 +
96 + ### New MED-tier findings (all closed 2026-05-31)
97 +
98 + - [x] **Payments MED — Cart `min_price_cents` bypass.** Fixed. Both cart paths (`process_seller_checkout` and `create_cart_checkout`) now check `pc.min_price_cents` for non-platform Discount codes before applying. Skips the ineligible item (others may still qualify) rather than rejecting the whole cart — matches the existing scope-skip pattern.
99 + - [x] **Payments MED — Cart-all chain-break on all-free first seller.** Fixed. `process_seller_checkout` signature changed `Result<String>` → `Result<Option<String>>`; all-free path now returns `Ok(None)` instead of `Err(BadRequest)`. New `drain_to_paid` helper loops through the queued sellers until a paid one is reached (returns URL) or queue exhausted (returns `Ok(None)` → library redirect). Both callers (`create_cart_checkout_all` and `checkout_success`) updated.
100 + - [x] **UX MED — Item wizard `pricing_model` silent fallback.** Fixed. `save_pricing` now rejects missing pricing_model with `AppError::validation("Select a pricing model")` and rejects unknown values with `format!("Unknown pricing model: {other}")`. Same shape as project wizard Run #6 fix.
101 + - [x] **UX MED — Inline-JS template duplication.** Fixed. Added delegated `data-copy-link` handler to `static/mnw.js` with proper `.catch()` (falls back to `window.prompt` in non-secure contexts). 8 templates migrated from `onclick="navigator.clipboard.writeText(...).then(...)"` to `<a href="..." data-copy-link>Copy link</a>` (audio_player, blog_post, collection, item, project, text_reader, user, video_player). `href` is the real URL so middle-click / no-JS / share menus still work. Cache-bust bumped to `v=0531`.
102 + - [x] **Perf MED — Cart free-claim N+1.** Fixed. Extended `CartItem` with `enable_license_keys` + `default_max_activations` (both cart queries pull them through). Three free-claim loops (single-seller paid path, discount-zeroed promo path, chain-flow path) drop the per-item `get_item_by_id` (saves N roundtrips) and replace per-item `remove_from_cart` DELETE with a single bulk `remove_from_cart_bulk(..., ANY($2))` at the end of each loop. Per-item tx for `claim_free_item` stays (per-item claim-vs-already-purchased return value). Roundtrips per free item: was ~5-7 → now ~3-4; bulk delete = +1 roundtrip total per loop (was N).
103 +
104 + All 5 MEDs landed. `cargo test --lib` 1654 / 0.
105 +
106 + ### Verified closed this run
107 +
108 + - [x] **Storage H1** — `confirm_upload` silent zero-rows + side-effects-already-fired (uploads.rs:295-337). Three-arm match, zero-rows arm rolls back storage + enqueue_s3_orphan.
109 + - [x] **Storage S1** — `media_confirm` three-write atomicity (media.rs:241-293). Single tx wraps storage credit + pending_uploads clear + media_files INSERT.
110 + - [x] DB helpers genericized to `impl PgExecutor<'e>` — all 12 callers (including `synckit/blobs.rs:157`) verified backwards-compatible.
111 +
112 + ### Storage A- standing — remaining MED/LOW (Phase 4 polish or defer)
113 + Carried from Storage code-fuzz 2026-05-31 — see below. All still MED, none A- blockers.
114 +
115 + ---
116 +
117 + ## Audit backlog sweep 2026-05-31 (post-Run #8, 7 waves)
118 +
119 + Sorted by file locality and difficulty. Tests: 1655 / 0 throughout.
120 +
121 + ### Wave 1 — auth/security cluster (8 tiny)
122 + - [x] `synckit_auth.rs:147` `<` → `<=` closes 1-second JWT-revocation collision window.
123 + - [x] `routes/auth.rs:128, 137` malformed-email + invalid-username branches now run DUMMY_HASH equalizer — closes timing oracle.
124 + - [x] `routes/auth.rs:331-336` `validate_username` length switched from `len()` bytes to `chars().count()` — multi-byte usernames treated correctly.
125 + - [x] `git_ssh.rs:162` `parse_repo_path` rejects lone-dot segments.
126 + - [x] `routes/oauth.rs:206` `validate_token` → `validate_token_consuming` (sealed witness type).
127 + - [x] `routes/oauth.rs:213-222` OAuth `state` ≤ 1024 bytes + `code_challenge` ≤ 44 chars.
128 + - [x] `helpers.rs::ip_advisory_lock_key` `DefaultHasher` → SHA-256 (stable across Rust versions).
129 + - [x] `helpers.rs::extract_client_ip` one-shot WARN after 100 cumulative missing `cf-connecting-ip` requests.
130 +
131 + ### Wave 2 — scanning (3)
132 + - [x] `scanning/clamav.rs::ping` + `ScanPipeline::assert_live` at startup; refuses to boot if scanning configured but no AV layer live.
133 + - [x] `scanning/clamav.rs` 16 KB INSTREAM truncation → `LayerVerdict::Fail` (was Error → FailOpen → Pass).
134 + - [x] `scanning/worker.rs:251` inline media UPDATE swapped to `db::scanning::update_media_file_scan_status` helper.
135 +
136 + ### Wave 3 — DB layer polish (4)
137 + - [x] `db/pending_uploads.rs::remove_pending_upload` signature now requires `user_id`; 12 callers updated.
138 + - [x] `db/pending_s3_deletions.rs::is_s3_key_live` now covers `projects.cover_image_url` and `items.cover_image_url` via `LIKE %s3_key`.
139 + - [x] `db/projects.rs::update_project_image_url` returns `Result<bool>`; `images.rs::project_image_confirm` three-arm match fires rollback + orphan-queue on `Ok(false)`.
140 + - [x] `db/items/media.rs::update_item_cover` same shape; same caller treatment in `images.rs::item_image_confirm`.
141 +
142 + ### Wave 4 — storage handlers + admin rescan seal + downloads (5)
143 + - [x] **Migration 133** `items_duration_seconds_nonnegative.sql` CHECK on `duration_seconds` + `video_duration_seconds`.
144 + - [x] `routes/storage/downloads.rs:120` defensive clamp: `duration.max(0) as u64 → saturating_mul(2) → clamp(3600, 86_400)`.
145 + - [x] `routes/storage/mod.rs::commit_rescan` new sibling to `commit_upload` for admin-rescan paths.
146 + - [x] `routes/admin/uploads.rs::rescan_{version,item}_inner` migrated to `commit_rescan`; chronic-disease seal now covers admin paths.
147 + - [x] `routes/pages/dashboard/wizards/item/save.rs:95` wizard `update_item_cover_image_url` call dropped (confirm authoritative, hidden-field desync risk closed).
148 + - [x] `routes/storage/mod.rs` `enqueue_s3_orphan` doc rewritten to match reality (post-credit failures only).
149 +
150 + ### Wave 5 — UX polish (2)
151 + - [x] `pricing.rs::parse_dollars_to_cents` strips `$`, `,`, whitespace; new `strips_clipboard_decoration` test.
152 + - [x] `routes/admin/users.rs:37, 77` page `clamp(1, 1_000_000_000)` to prevent OFFSET overflow → sqlx 500.
153 +
154 + ### Wave 6 — Performance (3 of 5; 2 deferred)
155 + - [x] `metrics::idempotency_middleware` in-memory negative cache (OnceLock<DashMap>, 60s TTL, periodic GC). Skips per-POST `get_cached_response` SELECT for keys recently confirmed not-cached.
156 + - [x] `monitor.rs` `record_storage_fill_stats` gated by 5-min TTL — 60× reduction at 10k+ creators.
157 + - [x] `scheduler/cleanup.rs::cleanup_sandbox_accounts` serial loop → JoinSet `CLEANUP_PARALLELISM=4`.
158 + - [ ] **DEFERRED** `build_runner.rs:151` serial-target loop. LOW; refactor touches denominator + error agg + log order. Post-launch.
159 + - [ ] **DEFERRED** `scheduler/mod.rs:92-279` advisory-lock granularity. Multi-replica concern; defer until multi-replica is real.
160 +
161 + ### Wave 7 — Payments LOW (2)
162 + - [x] `routes/stripe/webhook/mod.rs:73-87` `insert_failed_event` failure → 503 (Stripe redelivers) instead of dropping event with 200.
163 + - [x] `routes/stripe/checkout/cart.rs:73-82, 535-543` `remove_from_cart` already-owned cleanup now logs WARN on Err.
164 +
165 + ---
166 +
167 + ## Storage code-fuzz 2026-05-31 (post-Run #7)
168 +
169 + Targeted Storage-axis fuzz to verify A- before triggering full Run #8.
170 +
171 + ### Above-MED fixes that landed
172 + - [x] **Storage HIGH — `confirm_upload` silent zero-rows + side-effects-already-fired.** `routes/storage/uploads.rs:295-336`. Three-arm match on UPDATE result; zero-rows case rolls storage back, routes new S3 key through `enqueue_s3_orphan`, returns BadRequest. Same shape as Run #7 HIGH-2, one step further along the same handler family.
173 + - [x] **Storage SERIOUS — `media_confirm` three-write atomicity (Run #5 plan #12 reopened).** `routes/storage/media.rs:235-294`. Three writes (storage credit + pending_uploads clear + media_files INSERT) now in a single tx; tx drop rolls all three back on interruption. Only S3 object needs explicit cleanup. 23505 duplicate-filename detection moved outside the tx — same SQLSTATE check, runs after rollback.
174 + - [x] DB-layer support: `creator_tiers::try_increment_storage_on(&mut PgConnection)` new tx-friendly variant; `pending_uploads::remove_pending_upload` and `media_files::create` signatures genericized to `impl PgExecutor<'e>` (backwards compatible — all existing `&PgPool` call sites still compile).
175 +
176 + ### Remaining MED/LOW (below A- bar; defer or Phase 4 polish)
177 + - [ ] Storage MED — `update_project_image_url` / `update_item_cover` ignore `rows_affected()`. Same shape as H1 but only follow-on side-effect is `bump_cache_generation`, so blast radius is small.
178 + - [ ] Storage MED — `downloads.rs:120` `((duration as u64) * 2).max(3600)` with no DB CHECK on `duration_seconds`. Add `CHECK (duration_seconds >= 0)` migration + cap in code (`duration.max(0).saturating_mul(2).clamp(3600, 86400)`).
179 + - [ ] Storage MED — Admin rescan (`routes/admin/uploads.rs:347, 390`) bypasses `commit_upload` seal via direct `db::scan_jobs::enqueue`. Demote to `pub(crate)` and expose `commit_rescan(target, ...)`.
180 + - [ ] Storage MED — `enqueue_s3_orphan` single-policy doc overstates discipline; either tighten doc or migrate remaining direct `delete_object` cleanup sites.
181 + - [ ] Storage MED — `is_s3_key_live` doesn't enumerate project image URLs (no current bug; surface fragile).
182 + - [ ] Storage LOW — `scanning/worker.rs:251` inline UPDATE bypasses `db::scanning::update_media_file_scan_status` helper.
183 + - [ ] Storage LOW — wizard `save.rs:95` updates only `cover_image_url` (not s3_key/size).
184 + - [ ] Storage LOW — `pending_uploads::remove_pending_upload` deletes by s3_key alone (signature broader than needed).
185 +
186 + ---
187 +
188 + ## Ultra Fuzz 2026-05-31 (Runs #6, #7 + S1)
189 +
190 + ### Structural / chronic-disease fixes that landed
191 + - [x] `routes/storage/mod.rs::commit_upload(target, ...)` + `CommitTarget` enum; `enqueue_scan_for` demoted to module-private. All 7 confirm handlers (uploads, versions, project_image, item_image, media, internal/uploads, content_insertions) converted.
192 + - [x] `crate::pricing::parse_dollars_to_cents` + `validate_dollars_f64` shared helpers; 5 callsites converted (item save, project wizard ×2, bulk price, projects API).
193 + - [x] Dead `completion_effects` outbox deleted (`db/completion_effects.rs`, `scheduler/completion_effects.rs`, `routes/stripe/webhook/effects.rs` were orphaned files, no module declarations). Migrations 124/125 left in place — empty table, harmless. Drop-table migration is a future cleanup.
194 +
195 + ### Bug-level fixes that landed
196 + - [x] Storage CRIT Run #6 — `enqueue_s3_orphan` wired at `uploads.rs:325` post-commit old-key delete.
197 + - [x] Storage HIGH Run #6 — `images.rs::project_image_confirm` idempotency check added.
198 + - [x] Storage HIGH Run #6 — `images.rs::item_image_confirm` scan-ordering fixed via commit_upload.
199 + - [x] Storage HIGH Run #7 — 4 idempotent-early-return sites now call `remove_pending_upload` before returning (uploads.rs, versions.rs, images.rs project + item).
200 + - [x] Storage HIGH Run #7 — `update_project_image_url` + `update_item_cover` failures now refund storage + queue new key for orphan deletion.
201 + - [x] Storage MED Run #6 — `media_delete` enqueue moved after `tx.commit()`.
202 + - [x] UX HIGH Run #6 — `routes/api/projects.rs` float-parse via `validate_dollars_f64`.
203 + - [x] UX HIGH Run #6 — project wizard tier-row loop bubbles errors instead of silently `continue`-ing.
204 + - [x] UX HIGH Run #7 — `pricing_model` silent fallback to Free fixed; missing/malformed now rejects.
205 + - [x] Payments S Run #6 — promo `try_increment_use_count` moved after Stripe-readiness checks in item.rs, cart.rs::create_cart_checkout, and cart.rs::process_seller_checkout.
206 + - [x] Payments S Run #6 — `process_seller_checkout` uses bulk `purchased_subset`.
207 + - [x] Payments H Run #6 — `project_members::add_project_member` + `update_member_split` reject split_percent outside [0,100]; upsert subtracts existing row before cap check.
208 + - [x] Payments H Run #6 — `CodePurpose::Discount` with NULL discount_type/value rejected at validation (item.rs, cart.rs:184, cart.rs::process_seller_checkout — third copy fixed in Run #7).
209 + - [x] Payments H Run #6 — free PWYW project checkout clears contact revocation when share_contact=true.
210 + - [x] Payments SERIOUS Run #7 — cart 23505 swallow → buyer charged for unfulfilled items. New `db::transactions::pending_subset` bulk pre-check; both cart paths pre-check before Stripe session; remaining 23505 catch is now hard-error (release promo + abort).
211 +
212 + ### Deferred (with rationale)
213 + - [x] **Payments H2 (Run #7) — `claim_free_project` race.** Fixed 2026-05-31. `db::transactions::claim_free_project` now returns `Result<bool>` (mirrors `claim_free_item`). Caller in `routes/stripe/checkout/project.rs` gates `clear_contact_revocation` on the `claimed` winner — the loser of a concurrent-claim race no longer fires the side-effect. Same shape ready for any future side-effects added below the claim (sale-notification email, split recording, etc).
214 + - [ ] Drop unused `completion_effects` table — schema-only cleanup; harmless empty table.
215 +
216 + ### Notes on remaining MED/LOW (per Run #7 axis reports)
217 + - Storage MED — admin rescan handlers (`routes/admin/uploads.rs:347, 390`) still call `enqueue_scan_for` indirectly via lower-level primitives; functional today but bypasses the chronic-disease seal.
218 + - Storage MED — `update_item_cover` / `update_project_image_url` don't check `rows_affected()`; an ownership-filter mismatch returns Ok(0 rows) silently.
219 + - Storage MED — worker inline media UPDATE at `scanning/worker.rs:251` should use the new `db::scanning::update_media_file_scan_status` helper.
220 + - Storage LOW — internal CLI confirm drops returned `FileScanStatus` (no `pending_review` surfacing).
221 + - Storage LOW — `main.rs:334` comment references now-private `enqueue_scan_for`.
222 + - UX MED — `parse_dollars_to_cents` rejects `"$5"` and `"1,000"` literally; could strip `$`/`,` for clipboard-paste UX.
223 + - UX MED — project wizard skips `validate_tier_price` ($1–$10k); API path enforces it.
224 + - UX LOW — `BundleItemIds.filter_map` silently drops malformed UUIDs.
225 + - Payments M1 — `compute_splits` should `.max(0)` per-member for defense vs legacy negative `split_percent` rows.
226 + - Payments NIT — extract `require_stripe_ready` helper; six near-identical 5-line blocks across checkout files.
227 +
228 + ---
229 +
230 + ## Ultra Fuzz 2026-05-30 (Run #5)
231 +
232 + ## Ultra Fuzz 2026-05-30 (Run #5)
233 +
234 + Full report: `docs/audit_review.md`. 3 CRITICAL, 14 HIGH/SERIOUS. Two-axis regressions (Payments B, Storage B-) are coverage expansion into previously-unaudited paths plus one chronic recurrence; Security improved to A-; all 27 Run #4 plan items verified closed.
235 +
236 + ### Phase 1 — CRITICAL (fix today)
237 +
238 + - [ ] **Storage CRIT — `uploads.rs` file-type gate ordering** — `routes/storage/uploads.rs:204-237`. Move the match-arm rejection of `Download`/`Insertion`/`MediaImage`/`MediaVideo` BEFORE `enqueue_scan_for` and `update_item_scan_status`. Then make `enqueue_scan_for` + `update_*_scan_status` `pub(crate)` and expose a `commit_upload(file_type, item_id, s3_key)` higher-level op used by all three handlers (uploads / versions / images). Closes Phase 5 chronic invariant-in-prose finding.
239 + - [ ] **UX CRIT — Field-aware validation reaches the UI** — `error.rs:216-264` + `templates/error.html`. Either add `fields: Vec<(String, String)>` to `ErrorTemplate` + per-input markup in templates, OR delete the `validation_fields*` API and migrate callers to `validation(summary)`. Audit `validation_fields` callsites and pick a path.
240 + - [ ] **Perf CRIT — `build_runner.rs` partial-failure denominator** — `build_runner.rs:175-180`. Track `failed_count`; report `succeeded/(succeeded+failed)`. Add a test with 3 targets / 2 failures asserting "1/3".
241 +
242 + ### Phase 2 — SERIOUS / HIGH (fix this weekend)
243 +
244 + - [ ] **Payments SERIOUS — NULL `item_id` refund decode bomb** — `db/transactions.rs:699-716`. Return `Vec<(TransactionId, Option<ItemId>)>`; skip `decrement_sales_count`/`revoke_keys_by_transaction` when None. Fixture test against a project-level transaction.
245 + - [ ] **Payments SERIOUS — `compute_splits` over-credit on members > 100%** — `routes/stripe/webhook/checkout_helpers.rs:240-269`. Reject `total_split_pct > 100` at the project_members write site (DB CHECK + validation). Defensively scale or clamp each split. Add test at 60%+60%.
246 + - [ ] **Payments SERIOUS — Tip `project_id` not validated vs recipient** — `routes/stripe/checkout/tips.rs:104-106`. After form accept, assert `project.user_id == recipient_id`; 400 otherwise.
247 + - [ ] **Payments SERIOUS — Cart bypasses item `listed` gate** — `db/cart.rs:94-123` + `get_cart_items` + `get_cart_items_for_seller`. Add `AND i.listed = true` to all three. Add per-seller checkout path check. Regression test: toggle unlisted item into cart → rejection.
248 + - [ ] **Payments SERIOUS — Unknown subscription status retry storm** — `routes/stripe/webhook/subscriptions.rs:117-121`. Replace `?` with a match: known statuses dispatch; unknown statuses `tracing::warn!` and return 200 OK so Stripe stops retrying.
249 + - [ ] **Payments SERIOUS — `is_full_refund` zero-amount** — `payments/webhooks.rs:294-308`. Predicate becomes `amount > 0 && amount_refunded >= amount`. Invert the test at line 517-525.
250 + - [ ] **Storage HIGH — `versions.rs` enqueue-before-idempotency** — `routes/storage/versions.rs:159-174`. Move `version.s3_key == req.s3_key` idempotency check before `enqueue_scan_for`. Apply Phase 1 `commit_upload` helper.
251 + - [ ] **Storage HIGH — `project_image_confirm` probe-failure + no rollback** — `routes/storage/images.rs:179-208`. On `Err`/`Ok(None)` from `s3.object_size`, fall back to recorded size. Move `enqueue_deletions` AFTER `update_project_image_url` success, or wrap in a tx.
252 + - [ ] **Storage HIGH — `media_confirm` non-atomic three-write** — `routes/storage/media.rs:236-293`. Wrap `try_increment_storage` → `remove_pending_upload` → `media_files::create` in a transaction. Refund storage credit on any failure.
253 + - [ ] **UX HIGH — Negative/zero prices via `PriceCents::from_db`** — `routes/pages/dashboard/wizards/item/save.rs:183-185, 214-227`. Use `PriceCents::new(price_cents)?` unconditionally; drop `> 0` guard. Add `min <= suggested` check on PWYW.
254 + - [ ] **UX HIGH — f64 price parsing accepts NaN/saturates** — same file + `routes/api/items/bulk.rs:136-139` + `routes/pages/dashboard/wizards/project.rs:264-298`. Parse as decimal cents (`rust_decimal::Decimal::from_str_exact`); reject NaN/Inf/out-of-range before cast.
255 + - [ ] **UX HIGH — Username live-check fails open on DB error** — `routes/auth.rs:356-361`. Propagate error or treat as "unavailable, try again".
256 + - [ ] **Perf HIGH — Cart checkout 80 sequential roundtrips** — `routes/stripe/checkout/cart.rs:68-248`. Bulk-load `has_purchased_item` with `WHERE item_id = ANY($1)`. Batch `get_item_by_id`. Claim free items in one tx with batched inserts. Target ≤ 5 roundtrips for any cart size.
257 + - [ ] **Perf HIGH — `record_view` unbounded spawn per request** — `db/page_views.rs:18-32`. Replace per-request spawn with `mpsc` channel + single background drainer flushing every 250ms via bulk UPSERT.
258 + - [ ] **Perf HIGH — `check_sales_count_drift` full-table aggregate** — `scheduler/integrity.rs:53-73`. Add `WHERE i.sales_count > 0 OR EXISTS(SELECT 1 FROM transactions WHERE item_id = i.id LIMIT 1)` short-term; long-term trigger-maintained counts.
259 +
260 + ### Phase 3 — MED (fix before Run #6 if cheap)
261 +
262 + - [ ] Storage: advisory-lock leak in `check_sandbox_cap` (`db/mod.rs:92-128`) → `pg_advisory_xact_lock` or RAII guard.
263 + - [ ] Storage: `is_s3_key_live` missing tables (`db/pending_s3_deletions.rs:67-82`).
264 + - [ ] Storage: `delete_version` owner SELECT outside tx + post-commit S3 enqueue (`db/versions.rs:267-315`).
265 + - [ ] Security: ClamAV `FailOpen` startup assertion (`scanning/clamav.rs:19` + `scanning/mod.rs:151-164`) — refuse boot if scan configured but no AV layer live.
266 + - [ ] Security: `helpers.rs:44-50` `DefaultHasher` → stable hasher (sha2 first 8 bytes or `xxh3` constant seed).
267 + - [ ] Security: OAuth `state` size cap (`routes/oauth.rs:379-386`) — reject `> 1024`; cap `code_challenge` at 44 chars.
268 + - [ ] Security: `extract_client_ip` non-Cloudflare fallback warning (`helpers.rs:33-40`).
269 + - [ ] UX: pagination offset overflow (`routes/pages/public/discover.rs:85-87`, `routes/admin/users.rs:37-39`).
270 + - [ ] UX: forms silently render without `_csrf` when handler forgets to populate token — make `csrf_token` non-optional in form-bearing templates.
271 + - [ ] UX: `validate_username` byte-length vs `chars().count()` (`routes/auth.rs:322`).
272 + - [ ] Perf: scheduler advisory-lock connection pinned across S3 (`scheduler/mod.rs:92-279`) → dedicated `max_connections(1)` pool.
273 + - [ ] Perf: cleanup S3 deletes serialized inside scheduler tick (`scheduler/cleanup.rs:77-100`) → `for_each_concurrent(8, ...)`.
274 +
275 + ### Phase 4 — Polish (after Run #6 confirms ≥ A-)
276 +
277 + - [ ] Payments: `has_active_subscription_to_item` period-end clause mirroring (`db/subscriptions.rs:464-470`).
278 + - [ ] Payments: `get_active_creator_tier` + `sync_user_creator_tier` period-end defense (`db/creator_tiers.rs:91-103, 181-194`).
279 + - [ ] Payments: `release_use_count` race messaging (`db/promo_codes.rs:184-200`).
280 + - [ ] Payments: License key `activation_count` recount on revoke (`db/license_keys.rs:343-382`).
281 + - [ ] Payments: Subscription minimum-charge check (`payments/checkout.rs:283-317`).
282 + - [ ] Payments: Webhook v1/v2 unmark-on-failure parity (`routes/stripe/webhook/mod.rs:48-86`).
283 + - [ ] Storage: `media_files.list_folders` scan filter (`db/media_files.rs:73-82`).
284 + - [ ] Storage: `pending_uploads.record_pending_upload` silent user-mismatch (`db/pending_uploads.rs:23-33`).
285 + - [ ] Storage: `append_log_bounded` non-atomic size cap (`build_runner.rs:516-534`).
286 + - [ ] Storage: `downloads.rs:119-122` presigned-URL expiry — cap `duration_seconds` + DB CHECK ≥ 0.
287 + - [ ] Security: `validate_token_consuming` for OAuth POST (`routes/oauth.rs:206`).
288 + - [ ] Security: `parse_repo_path` rejects lone-dot entries (`git_ssh.rs:162`).
289 + - [ ] Security: ClamAV INSTREAM 16K cap → fail-closed on truncation (`scanning/clamav.rs:101-108`).
290 + - [ ] UX: validation error messages stop reflecting user input (`wizards/item/mod.rs:176-179`).
291 + - [ ] UX: CSRF body extraction stops using `from_utf8_lossy` (`csrf.rs:528-543`).
292 + - [ ] Perf: scan-pipeline 400 MiB worst-case capacity note (`constants.rs:156-157`).
293 + - [ ] Perf: announcement fan-out persistence + resume (`scheduler/announcements.rs:59-89, 147-177`).
294 + - [ ] Perf: build log per-line DB roundtrip (`build_runner.rs:516-534`).
295 +
296 + ### Phase 5 — Chronic
297 +
298 + - [ ] **Invariant-in-prose, FOURTH consecutive run.** Phase 1 #1 (constructive `commit_upload` helper sealing the lower-level scan/credit/status ops) is the only acceptable resolution. After it lands, audit `compute_splits` (Payments) and `ErrorTemplate` (UX) for the same shape and apply the same treatment.
299 +
300 + ---
4 301
5 302 ## Ultra Fuzz 2026-05-26 (Run #4)
6 303
@@ -8,6 +8,16 @@
8 8
9 9 set -euo pipefail
10 10
11 + # Pick up SANDO_DAEMON / SANDO_BRANCH from the daemon's env file when present
12 + # (the same file systemd's EnvironmentFile= points at). Lets the deployed hook
13 + # reach a non-loopback listen address without changing the hook source.
14 + if [[ -r /etc/sando/sando.env ]]; then
15 + set -a
16 + # shellcheck disable=SC1091
17 + source /etc/sando/sando.env
18 + set +a
19 + fi
20 +
11 21 DAEMON_URL="${SANDO_DAEMON:-http://127.0.0.1:7766}"
12 22 DEPLOY_BRANCH="${SANDO_BRANCH:-main}"
13 23
@@ -1,6 +1,6 @@
1 1 # MakeMachine Hardware BOM
2 2
3 - Settled 2026-05-23. Top-of-line host platform; GPUs are fungible and live on the EveryCycle GPU thread (see `~/hardware/everycycle/docs/roadmap.md`).
3 + Settled 2026-05-23. Top-of-line host platform; GPUs are fungible and live on the EveryCycle GPU thread (see `~/Code/everycycle/docs/roadmap.md`).
4 4
5 5 The substrate is built once and kept stable; GPU experimentation happens above it without revisiting motherboard, CPU, or RAM.
6 6
@@ -97,7 +97,7 @@
97 97
98 98 ## Database Layer
99 99
100 - PostgreSQL via sqlx with compile-time checked queries. 50 migrations (auto-applied on boot). Connection pool: 25 max connections, 3-second acquire timeout.
100 + PostgreSQL via sqlx with compile-time checked queries. Numbered migrations in `migrations/`, auto-applied on boot; the directory is the source of truth. Connection pool: 25 max connections, 3-second acquire timeout.
101 101
102 102 ### DB Modules
103 103
@@ -229,7 +229,7 @@
229 229
230 230 - **Passwords:** Argon2id with random salt per hash
231 231 - **Sessions:** `tower-sessions` with PostgreSQL-backed store, ID regeneration on login (prevents fixation), 7-day expiry on inactivity
232 - - **Session cache:** DashMap caches recent session validations (30-second TTL) to skip per-request DB touch
232 + - **Session cache:** DashMap caches recent session validations (TTL from `constants::SESSION_TOUCH_CACHE_SECS`, currently 5s) to skip per-request DB touch
233 233 - **2FA:** TOTP (totp-rs, 6-digit, 30-second step, +/-1 skew) + WebAuthn passkeys (webauthn-rs)
234 234 - **Account lockout:** 5 failed attempts triggers 15-minute lockout
235 235 - **New-device notifications:** Email alert on login from unrecognized session
@@ -359,7 +359,7 @@
359 359 | Scheduler | `src/scheduler.rs` |
360 360 | Shared types | `src/types/` |
361 361 | Askama templates | `templates/` |
362 - | Migrations | `migrations/` (001-050) |
362 + | Migrations | `migrations/` (numbered, applied in order) |
363 363 | Static assets | `static/` |
364 364 | Integration tests | `tests/` |
365 365 | Deploy scripts | `deploy/` |
@@ -76,7 +76,7 @@
76 76 2. **Variables** (`:root` custom properties)
77 77 3. **Reset + Base** (global element styles)
78 78 4. **Layout** (`.padded-page`, `.centered-page`, `.container`)
79 - 5. **Buttons** (`.primary`, `.secondary`, `.danger`, `.small`)
79 + 5. **Buttons** (`.btn-primary`, `.btn-secondary`, `.btn-danger`, `.small` modifier)
80 80 6. **Forms** (`.form-group`, `.form-section`, `.checkbox-group`)
81 81 7. **Tables** (`.data-table`, `.compact-table`)
82 82 8. **Utilities** (`.text-sm`, `.muted`, `.scroll-x`, etc.)
@@ -110,15 +110,7 @@
110 110
111 111 ### Button Variants
112 112
113 - | Class | Background | Text | Usage |
114 - |-------|-----------|------|-------|
115 - | `button` (default) | `--light-background` | `--detail` | Generic actions |
116 - | `.primary` | `--primary-dark` (black) | White | Main CTAs |
117 - | `.secondary` | `--surface-muted` | `--detail` | Alternative actions |
118 - | `.danger` | `--danger` (red) | White | Destructive actions |
119 - | `.small` | (modifier) | (modifier) | Compact size for table cells |
120 -
121 - Combine: `class="primary small"`, `class="danger small"`.
113 + Buttons use the `.btn-*` family. See `design-system.md` § Buttons for the current class names, colors, and modifiers. The bare `.primary` / `.secondary` / `.danger` classes were retired; use `.btn-primary`, `.btn-secondary`, `.btn-danger` instead.
122 114
123 115 ## HTMX Patterns
124 116