Skip to main content

max / makenotwork

Fix CSRF, SyncKit rate limiter, CI pipeline; bump to v0.4.10 - Fix CSRF exempt path matching: /postmark/ trailing slash caused double-slash in starts_with check, blocking inbound patch webhooks - Add /api/checkout/guest-free to CSRF exempt list - Fix SyncKit rate limiter: return nil sentinel key for unauthenticated requests instead of GovernorError, which returned 500 before SyncUser could return 401 - Remove dead changelog_page function (superseded by blog-based route) - CI: use DATABASE_URL instead of SQLX_OFFLINE, add --features fast-tests - Bump to v0.4.10 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-04 18:32 UTC
Commit: 99db513ed585780a032bd8bdc59637eb26772844
Parent: 39e6303
8 files changed, +75 insertions, -31 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.4.9"
3 + version = "0.4.10"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -40,7 +40,7 @@ echo "[pull] $COMMIT_MSG"
40 40 # Run CI from the server directory
41 41 cd "$SERVER_DIR" || exit 1
42 42
43 - export SQLX_OFFLINE=true
43 + export DATABASE_URL="${DATABASE_URL:-postgres:///makenotwork_staging}"
44 44 export TEST_DATABASE_URL="${TEST_DATABASE_URL:-postgres:///postgres}"
45 45 export RUST_TEST_THREADS="${RUST_TEST_THREADS:-8}"
46 46 export CARGO_INCREMENTAL=0
@@ -25,7 +25,7 @@ fi
25 25
26 26 cd "$PROJECT_DIR"
27 27
28 - export SQLX_OFFLINE=true
28 + export DATABASE_URL="${DATABASE_URL:-postgres:///makenotwork_staging}"
29 29 export TEST_DATABASE_URL="${TEST_DATABASE_URL:-postgres:///postgres}"
30 30 export RUST_TEST_THREADS="${RUST_TEST_THREADS:-8}"
31 31 export CARGO_INCREMENTAL=0
@@ -62,24 +62,24 @@ cleanup_test_dbs() {
62 62 cleanup_test_dbs
63 63
64 64 # Step 1: Compilation check
65 - run_step "cargo check" cargo check
65 + run_step "cargo check" cargo check --features fast-tests
66 66
67 67 # Step 2: Unit tests
68 68 if [ -n "$FILTER" ]; then
69 - run_step "cargo test --lib ($FILTER)" cargo test --lib "$FILTER"
69 + run_step "cargo test --lib ($FILTER)" cargo test --features fast-tests --lib "$FILTER"
70 70 else
71 - run_step "cargo test --lib" cargo test --lib
71 + run_step "cargo test --lib" cargo test --features fast-tests --lib
72 72 fi
73 73
74 74 # Step 3: Integration tests
75 75 if [ -n "$FILTER" ]; then
76 - run_step "cargo test --test integration ($FILTER)" cargo test --test integration "$FILTER" -- --test-threads=8
76 + run_step "cargo test --test integration ($FILTER)" cargo test --features fast-tests --test integration "$FILTER" -- --test-threads=8
77 77 else
78 - run_step "cargo test --test integration" cargo test --test integration -- --test-threads=8
78 + run_step "cargo test --test integration" cargo test --features fast-tests --test integration -- --test-threads=8
79 79 fi
80 80
81 81 # Step 4: Clippy
82 - run_step "cargo clippy" cargo clippy --all-targets -- -D warnings
82 + run_step "cargo clippy" cargo clippy --features fast-tests --all-targets -- -D warnings
83 83
84 84 # Step 5: Security audit (optional)
85 85 if command -v cargo-audit &>/dev/null; then
@@ -5,7 +5,7 @@
5 5
6 6 ## Overall Grade: A
7 7
8 - Run 19: 1,923 tests passing (1,213 unit + 679 integration + 28 doc + 3 load; 10 ignored), 0 failed. 0 clippy warnings. v0.4.7. ~81,384 LOC. 2 cold spots (0 bugs, 2 minor). Combined with doc fuzz for creator email readiness assessment.
8 + Run 19: 1,930 tests passing (1,220 unit + 679 integration + 28 doc + 3 load; 10 ignored), 0 failed. 0 cargo warnings. v0.4.8. ~81,384 LOC. 2 cold spots (0 bugs, 2 minor). Combined with doc fuzz for creator email readiness assessment. 10 test failures from Run 19 resolved 2026-05-04 (CSRF double-slash, SyncKit rate limiter key extraction, guest-free checkout CSRF exemption).
9 9
10 10 ## Scorecard
11 11
@@ -13,7 +13,7 @@ Run 19: 1,923 tests passing (1,213 unit + 679 integration + 28 doc + 3 load; 10
13 13 |-----------|:-----:|-------|
14 14 | Code Quality | A | unwrap() in git/raw.rs fixed. helpers.rs split into formatting/crypto/rate_limit (395 lines from 1,268) |
15 15 | Architecture | A | Inline SQL in route handlers still present (4 locations) but minor |
16 - | Testing | A | 1,923 tests, 0 failures. proptest active. Lockout test fixed (auth rate limit fast-tests override) |
16 + | Testing | A | 1,930 tests, 0 failures. proptest active. 10 test failures fixed 2026-05-04 (CSRF, SyncKit rate limiter, guest-free exempt) |
17 17 | Security | A+ | Zero SQL injection vectors, constant-time compare everywhere, fail-closed scanning, CSRF on all forms |
18 18 | Performance | A | analytics.rs deduplicated (623->468 LOC). hash_lookup uses static reqwest::Client |
19 19 | Documentation | A- | Module-level //! on every file. No README.md (CONTRIBUTING.md partially fills role) |
@@ -3,7 +3,7 @@
3 3 ## Status
4 4 Done: All pre-beta phases, UX audit remediation, creator trust audit remediation. Active: Creator setup (Stripe), manual testing. Next: Soft launch.
5 5
6 - v0.4.8. Audit grade A (Run 18, 2026-05-01). Code fuzz Run 19 complete (2026-05-03, 17 bugs fixed). ~1,213 unit + ~679 integration = ~1,923 tests (all passing). Mutation kill rate 99.4%. Property-based testing active (proptest). `cargo test --features fast-tests` for fast runs.
6 + v0.4.8. Audit grade A (Run 18, 2026-05-01). Code fuzz Run 19 complete (2026-05-03, 17 bugs fixed). ~1,220 unit + ~679 integration = ~1,930 tests (all passing with `--features fast-tests` as of 2026-05-04). Mutation kill rate 99.4%. Property-based testing active (proptest). `cargo test --features fast-tests` for fast runs.
7 7
8 8 Human tasks (manual testing, outreach, legal, infrastructure) moved to `human_todo.md`.
9 9 Completed items moved to `todo_done.md`.
@@ -18,6 +18,16 @@ All high/serious/minor items fixed. Migration 090 required. Needs deploy.
18 18 ### Pre-deploy
19 19 - [ ] Run migration 090 on production after deploy
20 20
21 + ### Test Failures — FIXED (2026-05-04)
22 + All 10 previously-failing integration tests resolved. Three bugs fixed:
23 + - **SyncKit 500→401**: `SyncAppKeyExtractor` returned `GovernorError::UnableToExtractKey` on missing bearer token, causing tower_governor to respond 500 before `SyncUser` could return 401. Fix: return nil sentinel `SyncAppId` to pass through to handler.
24 + - **CSRF double-slash**: Exempt prefix `/postmark/` caused matching to produce `/postmark//`. Fix: use `/postmark` without trailing slash.
25 + - **CSRF missing guest-free**: `/api/checkout/guest-free` was not in the CSRF exempt list. Fix: added it alongside `/api/checkout/guest`.
26 + - **Sandbox rate limit** (2 tests): Not a code bug — requires `--features fast-tests` as documented.
27 + - Removed dead `changelog_page` function (superseded by blog-based `/changelog` route). Zero warnings.
28 +
29 + Also: CI on astra has compilation errors (separate issue, likely stale paths from monorepo restructure).
30 +
21 31 #### Low (previous)
22 32 - [ ] Add README.md to server/
23 33
@@ -27,6 +37,44 @@ All high/serious/minor items fixed. Migration 090 required. Needs deploy.
27 37 - [ ] Migrate inline `onclick` handlers to `addEventListener` for strict CSP
28 38 - [ ] Monitor scheduler.rs (1249), git/mod.rs (624), license_keys.rs (684) for growth
29 39
40 + ### Dashboard Usability Audit (2026-05-03)
41 +
42 + Grade: B-. Complexity B, Completeness B-, Learnability C+, Discoverability C.
43 +
44 + #### Discoverability (Critical)
45 + - [ ] Reorganize user dashboard tabs — move Analytics and Creator to visible tab bar by default; only keep SSH Keys, Forums in overflow. Move SyncKit from user dashboard to project dashboard (apps are linked to projects, not users). 10 user tabs (4 visible + 6 hidden) is too many hidden
46 + - [ ] Add dashboard tab customization setting — let users choose which tabs are always visible in the tab bar and which go into the overflow menu. Store preference per user. Sensible defaults (Account, Projects, Payments, Analytics, Creator visible; SSH Keys, Media, Forums, Support in overflow) but fully user-configurable
47 + - [ ] Move SyncKit tab to project dashboard — SyncKit apps are tied to projects (Linked To column). Show as a project-level tab alongside Code, filtered to that project's apps. Keep a user-level summary view (or link) for creators managing apps across multiple projects
48 + - [ ] Surface Stripe requirement on Project Overview — persistent banner "Connect Stripe to sell items" with direct link until connected and charges enabled
49 + - [ ] Add Media Library access from content editors — "Insert Image" button in blog editor and item content editor that opens media library. Currently completely disconnected from where users need it
50 + - [ ] Always show Blog tab with empty state — currently only appears if posts exist. Show "No blog posts yet. Start writing to engage your audience." with "New Post" button
51 + - [ ] Add content search/filter to project Content tab — search by title, filter by status (Draft/Published/Scheduled) and type. Table stakes for any content management interface
52 + - [ ] Add "Embed & Share" quick action on Item Overview — embed codes only discoverable by navigating to specific item's Embed tab
53 +
54 + #### Learnability (High)
55 + - [ ] Make breadcrumbs clickable navigation links — currently display-only text; users can't click back to parent; broken with HTMX tab state
56 + - [ ] Add explanatory text to jargon terms: SyncKit ("Cloud sync for indie apps" subtitle), Insertions (rename to "Dynamic Clips" in storage display), AI Classification (add option descriptions: "Handmade — no AI tools", "AI-Assisted — AI tools with human creation", "AI-Generated — primarily created by AI"), Labels ("Platform-curated tags describing your project's commitments"), Revenue Splits (add setup instructions linking to Project Members tab)
57 + - [ ] Improve onboarding checklist context — add brief explanations to each step: "Connect Stripe — required to receive payments, 3% processing only", "Create a project — blog, podcast, course, etc."
58 + - [ ] Add empty state context to analytics — change "No revenue data yet" to "Once you publish items and make sales, revenue data will appear here" with link to publish
59 + - [ ] Add AI Classification option descriptions in item_details dropdown — "Handmade (no AI tools)", "AI-Assisted (AI tools with human creation)", "AI-Generated (primarily created by AI)" with examples
60 +
61 + #### Complexity (Medium)
62 + - [ ] Split Account Details tab into sub-sections — currently 13 sections in one scroll. Group into: Profile (name, bio, links, domain), Security (password, 2FA, passkeys, sessions), Notifications, Data & Privacy (export, import, deletion)
63 + - [ ] Hide UUID from item dashboard header — remove or put behind a "Copy ID" button. Creators don't need to see UUIDs
64 + - [ ] Hide Stripe account ID behind disclosure toggle — currently shown in monospace on Payments tab. Collapse behind "Show details"
65 + - [ ] Simplify Stripe status display — replace raw onboarding states with user-intent language: "Ready to receive payments" (green) or "Action required: [task]" (red)
66 +
67 + #### Feature Completeness (Medium)
68 + - [ ] Add download count analytics per item — standard on Bandcamp, Gumroad; primary consumption metric for digital goods
69 + - [ ] Add cross-project item view — creator with 3 projects can't see all items in one place; no global search
70 + - [ ] Add refund initiation from dashboard — currently must go to Stripe dashboard to issue refunds
71 + - [ ] Add "Export as CSV" button on item sales tables
72 +
73 + #### Discoverability (Lower)
74 + - [ ] Add bulk operations hint on Content tab — show "Select items for bulk actions" tip; show action bar in disabled state so feature is discoverable
75 + - [ ] Add contextual next-step suggestions after key actions — "Next: Set pricing" after creating item; "Next: Create item" after creating project
76 + - [ ] Show all conditional tabs (SyncKit, SSH Keys, Forums) always with empty states explaining prerequisites — instead of hiding them entirely
77 +
30 78 ### UX — Deferred (post-beta table stakes)
31 79 - [ ] Reviews/ratings system for items
32 80 - [ ] Gift purchases at checkout
@@ -140,9 +140,9 @@ pub async fn csrf_middleware(request: Request, next: Next) -> Response {
140 140 "/login", "/join",
141 141 "/api/sync/auth", "/api/sync/push", "/api/sync/pull", "/api/sync/status",
142 142 "/api/sync/devices", "/api/sync/keys", "/api/sync/blobs",
143 - "/oauth", "/auth/passkey", "/postmark/",
143 + "/oauth", "/auth/passkey", "/postmark",
144 144 "/unsubscribe", "/confirm-delete",
145 - "/api/checkout/guest",
145 + "/api/checkout/guest", "/api/checkout/guest-free",
146 146 ];
147 147
148 148 let is_exempt = exempt_prefixes.iter().any(|p| {
@@ -45,12 +45,17 @@ impl KeyExtractor for SyncAppKeyExtractor {
45 45 type Key = SyncAppId;
46 46
47 47 fn extract<T>(&self, req: &axum::http::Request<T>) -> Result<Self::Key, GovernorError> {
48 - let token = req
48 + let token = match req
49 49 .headers()
50 50 .get("authorization")
51 51 .and_then(|v| v.to_str().ok())
52 52 .and_then(|s| s.strip_prefix("Bearer "))
53 - .ok_or(GovernorError::UnableToExtractKey)?;
53 + {
54 + Some(t) => t,
55 + // No bearer token — use a nil sentinel key so the request passes
56 + // through to the handler, where SyncUser will properly return 401.
57 + None => return Ok(SyncAppId::nil()),
58 + };
54 59
55 60 // JWT is header.payload.signature — decode the payload (middle segment)
56 61 // without verifying the signature. We only need the `app` field.
@@ -179,18 +184,20 @@ mod tests {
179 184 }
180 185
181 186 #[test]
182 - fn missing_auth_header_returns_error() {
187 + fn missing_auth_header_returns_nil_sentinel() {
183 188 let req = Request::builder().body(()).unwrap();
184 - assert!(SyncAppKeyExtractor.extract(&req).is_err());
189 + let key = SyncAppKeyExtractor.extract(&req).unwrap();
190 + assert_eq!(key, SyncAppId::nil());
185 191 }
186 192
187 193 #[test]
188 - fn non_bearer_auth_returns_error() {
194 + fn non_bearer_auth_returns_nil_sentinel() {
189 195 let req = Request::builder()
190 196 .header("authorization", "Basic dXNlcjpwYXNz")
191 197 .body(())
192 198 .unwrap();
193 - assert!(SyncAppKeyExtractor.extract(&req).is_err());
199 + let key = SyncAppKeyExtractor.extract(&req).unwrap();
200 + assert_eq!(key, SyncAppId::nil());
194 201 }
195 202
196 203 #[test]
@@ -251,17 +251,6 @@ pub(super) async fn policy_page(
251 251 }
252 252 }
253 253
254 - /// Render the What's New / changelog page.
255 - #[tracing::instrument(skip_all, name = "landing::changelog_page")]
256 - pub(super) async fn changelog_page(
257 - session: Session,
258 - MaybeUser(maybe_user): MaybeUser,
259 - ) -> impl IntoResponse {
260 - ChangelogTemplate {
261 - csrf_token: get_csrf_token(&session).await,
262 - session_user: maybe_user,
263 - }
264 - }
265 254
266 255 /// Query params for the Fan+ page.
267 256 #[derive(Debug, Deserialize)]