| 1 |
1 |
|
# MakeNotWork -- Audit Review
|
| 2 |
2 |
|
|
| 3 |
|
- |
**Last audited:** 2026-05-04 (Run 20, MNW server full audit)
|
| 4 |
|
- |
**Previous audit:** 2026-05-02 (Run 19, MNW server + doc fuzz)
|
|
3 |
+ |
**Last audited:** 2026-05-08 (Run 21, Ultra Fuzz -- 5-axis deep audit)
|
|
4 |
+ |
**Previous audit:** 2026-05-04 (Run 20, MNW server full audit)
|
| 5 |
5 |
|
|
| 6 |
6 |
|
## Overall Grade: A
|
| 7 |
7 |
|
|
| 8 |
|
- |
Run 20: 678 integration tests passing (2 timing-sensitive sandbox rate-limit tests failing, non-critical), 0 cargo warnings. v0.4.10. ~83,232 LOC. 2 cold spots (0 bugs, 2 minor). Clean git status. `cargo check` passes cleanly.
|
|
8 |
+ |
Run 21: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.7. ~87,427 LOC. 1,218 test annotations. 101 migrations. 5 SERIOUS findings (1 payments, 2 storage, 2 performance), 0 CRITICAL. 5 cold spots. All money math integer-only. Security posture remains A+.
|
| 9 |
9 |
|
|
| 10 |
10 |
|
## Scorecard
|
| 11 |
11 |
|
|
| 12 |
12 |
|
| Dimension | Grade | Notes |
|
| 13 |
13 |
|
|-----------|:-----:|-------|
|
| 14 |
|
- |
| Code Quality | A | Zero .unwrap() in production paths (3 LazyLock regex are acceptable) |
|
| 15 |
|
- |
| Architecture | A | Clean layer separation. Inline SQL only in health.rs (stats + probe, acceptable) |
|
| 16 |
|
- |
| Testing | A | ~1,214 test annotations, 94 integration test files, proptest active, comprehensive harness |
|
| 17 |
|
- |
| Security | A+ | Constant-time compare, fail-closed scanning, CSRF everywhere, Argon2id, HMAC webhooks, path traversal prevention |
|
| 18 |
|
- |
| Performance | A | Batch queries, pagination, CDN fallback, session touch cache, presigned uploads |
|
| 19 |
|
- |
| Documentation | A- | Module-level //! on all major files. No README.md (CONTRIBUTING.md fills role) |
|
| 20 |
|
- |
| Dependencies | A- | 3 transitive advisories (none exploitable). All security-sensitive deps on latest stable |
|
| 21 |
|
- |
| Frontend | A | Askama auto-escape, HTMX patterns consistent, CSP headers |
|
|
14 |
+ |
| Code Quality | A | Zero .unwrap() in production paths. Clean macro patterns throughout |
|
|
15 |
+ |
| Architecture | A | Clean layer separation. Trait-based backends for storage/email/payments |
|
|
16 |
+ |
| Testing | A | 1,218 test annotations, proptest active, adversarial tests, comprehensive harness |
|
|
17 |
+ |
| Security | A+ | SHA-256-based constant-time compare, fail-closed scanning, CSRF everywhere, Argon2id, HMAC webhooks, PKCE S256 |
|
|
18 |
+ |
| Performance | A- | Discover page fires 5-8 queries per request (new finding). Presigned uploads, CDN fallback, session cache solid |
|
|
19 |
+ |
| Documentation | A- | Module-level //! on all major files. No README.md |
|
|
20 |
+ |
| Dependencies | A- | 4 transitive advisories (none exploitable). async-trait still present (chronic) |
|
|
21 |
+ |
| Frontend | A | Askama auto-escape, json_escape prevents JSON-LD XSS, HTMX patterns consistent |
|
| 22 |
22 |
|
| Type Safety | A+ | 36 UUID newtypes, 25+ domain enums, validated string types, Cents/PriceCents monetary newtypes |
|
| 23 |
|
- |
| Observability | A- | Comprehensive #[instrument] on routes + DB layer. Gaps: embed/ handlers (0 instruments), payments/ module (0 instruments) |
|
| 24 |
|
- |
| Concurrency | A | ON CONFLICT, FOR UPDATE, advisory locks, DashMap caches, atomic state changes |
|
|
23 |
+ |
| Observability | A- | Comprehensive #[instrument] on routes + DB. Gaps: embed/ (0), payments/ (0) -- chronic from Run 20 |
|
|
24 |
+ |
| Concurrency | A- | ON CONFLICT, FOR UPDATE, DashMap caches. Scheduler advisory lock not pinned (new finding) |
|
| 25 |
25 |
|
| Resilience | A | Graceful shutdown with 10s deadline, timeouts on all outbound calls, fail-closed scanning |
|
| 26 |
|
- |
| API Consistency | A | ListResponse wrapper, json_error_layer, versioned SyncKit routes, documented conventions |
|
| 27 |
|
- |
| Migration Safety | A | 93 additive migrations, IF NOT EXISTS guards, TIMESTAMPTZ throughout, defaults on all NOT NULL |
|
| 28 |
|
- |
| Codebase Size | A- | 83K LOC well-organized. 5 files over 500-line guideline (max 844). No egregious violations |
|
|
26 |
+ |
| API Consistency | A | ListResponse wrapper, json_error_layer, versioned SyncKit routes |
|
|
27 |
+ |
| Migration Safety | A | 101 additive migrations, IF NOT EXISTS guards, TIMESTAMPTZ throughout |
|
|
28 |
+ |
| Codebase Size | A- | 87K LOC. 7 files over 500-line guideline (max 846). No egregious violations |
|
| 29 |
29 |
|
|
| 30 |
30 |
|
## Module Heatmap
|
| 31 |
31 |
|
|
| 32 |
32 |
|
| Module | Code | Arch | Test | Security | Perf | Docs | TypeSafe | Observ | Size |
|
| 33 |
33 |
|
|--------|:----:|:----:|:----:|:--------:|:----:|:----:|:--------:|:------:|:----:|
|
| 34 |
34 |
|
| lib.rs + main.rs | A | A | B+ | A | A | A | A | A | A |
|
| 35 |
|
- |
| config.rs | A | A | A | A | n/a | A | A | B+ | A |
|
|
35 |
+ |
| config.rs | A | A | A | A | n/a | A | A | A- | A |
|
| 36 |
36 |
|
| error.rs | A | A | A | A | n/a | A | A | A | A |
|
| 37 |
37 |
|
| auth.rs | A | A | A- | A+ | A | A | A | A | A |
|
| 38 |
38 |
|
| csrf.rs | A | A | A | A | A- | A | A | A- | A |
|
|
39 |
+ |
| constants.rs | A+ | n/a | A | A | n/a | A- | A | n/a | A |
|
| 39 |
40 |
|
| helpers.rs | A | A | A | A | A | A | A- | B+ | A |
|
| 40 |
|
- |
| constants.rs | A | n/a | A | A | n/a | A- | A | n/a | A |
|
| 41 |
|
- |
| rate_limit.rs | A | A | A | A | A | A | A | B+ | A |
|
|
41 |
+ |
| rate_limit.rs | A | A | A- | A | A | A | A | B+ | A |
|
| 42 |
42 |
|
| storage.rs | A | A | A | A | A | A | A | B+ | A |
|
| 43 |
|
- |
| pricing.rs | A | A+ | A+ | A | A | A | A | n/a | A |
|
| 44 |
|
- |
| crypto.rs | A | A | A | A | A | A | A | n/a | A |
|
| 45 |
|
- |
| formatting.rs | A | A | A+ | A | A | A | A | n/a | A |
|
|
43 |
+ |
| pricing.rs | A | A+ | A+ | A | A | A | A- | n/a | A |
|
|
44 |
+ |
| crypto.rs | A | A | A | A+ | A | A | A | n/a | A |
|
|
45 |
+ |
| formatting.rs | A | A | A+ | A | A | A- | A | n/a | A |
|
| 46 |
46 |
|
| rss.rs | A | A | A | A | A | A | A- | n/a | A |
|
| 47 |
47 |
|
| synckit_auth.rs | A | A | A | A | A | A | A | B+ | A |
|
| 48 |
48 |
|
| db/enums.rs | A | A | A | A | n/a | A | A+ | n/a | A |
|
| 51 |
51 |
|
| db/users.rs | A | A | B | A | A- | A | A | A | A |
|
| 52 |
52 |
|
| db/items.rs | A | A | B | A | A- | A | A | A | A |
|
| 53 |
53 |
|
| db/synckit.rs | A | A | B | A | A | A | A | A | A |
|
| 54 |
|
- |
| db/transactions.rs | A | A | B | A | A | A | A | A | A |
|
| 55 |
|
- |
| db/discover.rs | A | A | B | A | A | A | A | n/a | A |
|
| 56 |
|
- |
| db/creator_tiers.rs | A | A | A | A | A | A | A | n/a | A |
|
|
54 |
+ |
| db/transactions.rs | A | A | B+ | A | A | A | A | A | A |
|
|
55 |
+ |
| db/discover.rs | A | A | B | A | A- | A | A | n/a | **B** |
|
|
56 |
+ |
| db/cart.rs | **B+** | A | B | A | A | A | A- | n/a | A |
|
|
57 |
+ |
| db/creator_tiers.rs | A | A | A | A | A- | A | A | n/a | A |
|
|
58 |
+ |
| db/versions.rs | A | A | A | A | A | A | A | n/a | A |
|
|
59 |
+ |
| db/builds.rs | A | A+ | A | A | A | A | A | n/a | A |
|
|
60 |
+ |
| db/pending_refunds.rs | A | A | B | A | A | A | A | n/a | A |
|
|
61 |
+ |
| db/license_keys.rs | A | A | B | A | A | A | A | n/a | A |
|
|
62 |
+ |
| db/promo_codes.rs | A | A | A | A | A | A | A | n/a | A |
|
|
63 |
+ |
| db/tips.rs | A | A | B | A | A | A | A | n/a | A |
|
| 57 |
64 |
|
| db/models/* | A | A | B+ | A | n/a | A- | A | n/a | A |
|
| 58 |
65 |
|
| types/ | A | A | B | A | n/a | A | A | n/a | A |
|
| 59 |
|
- |
| scanning/ | A | A+ | A+ | A+ | A- | A | A | B | A |
|
|
66 |
+ |
| scanning/ | A | A+ | A- | A+ | A- | A | A- | B+ | A |
|
| 60 |
67 |
|
| payments/ | A | A | A- | A | A- | A | A | **B+** | B+ |
|
| 61 |
68 |
|
| email/ | A | A | A | A | A- | A | A | B+ | A- |
|
| 62 |
|
- |
| scheduler/ | A | A | B+ | A | A | A | A | A- | A |
|
|
69 |
+ |
| scheduler/ | A | A | B+ | A | **B+** | A | A | A- | A |
|
|
70 |
+ |
| scheduler/cleanup.rs | A- | A | n/a | A | **B+** | A | A | A- | A |
|
| 63 |
71 |
|
| validation/ | A | A | A+ | A+ | A | B+ | A | n/a | A |
|
| 64 |
72 |
|
| import/ | A | A | A | A | B+ | A- | A | B+ | A |
|
| 65 |
73 |
|
| git/ | A | A | A | A+ | B+ | A- | A | B | A |
|
| 66 |
|
- |
| git_ssh.rs | A | A | A | A | A | A- | A | B+ | A |
|
| 67 |
|
- |
| build_runner.rs | A | A- | B+ | A+ | A | A- | A | A | A |
|
|
74 |
+ |
| git_ssh.rs | A | A | A | A- | A | A- | A | B+ | A |
|
|
75 |
+ |
| build_runner.rs | A | A- | B+ | A+ | A- | A- | A | A | A |
|
| 68 |
76 |
|
| monitor.rs | A | A | A- | A | A | A | A | A | A |
|
| 69 |
|
- |
| templates/ | A | A | n/a | A | A | A- | A | B | B+ |
|
|
77 |
+ |
| templates/ | A | A- | n/a | A | A | A- | A | B+ | B+ |
|
| 70 |
78 |
|
| routes/auth.rs | A | A | n/a | A+ | A | A | A | A | A |
|
| 71 |
|
- |
| routes/oauth.rs | A | A | n/a | A+ | A | A | A | A | A- |
|
|
79 |
+ |
| routes/oauth.rs | A | A | n/a | A- | A | A | A | A- | A- |
|
| 72 |
80 |
|
| routes/admin/ | A | A | n/a | A | A | A | A | A | A |
|
| 73 |
81 |
|
| routes/api/ | A | A | n/a | A | A | A | A | A | **B+** |
|
| 74 |
|
- |
| routes/stripe/ | A | A | n/a | A | A | A | A | **B+** | **B+** |
|
| 75 |
|
- |
| routes/synckit/ | A | A | n/a | A | A | A | A | A | A |
|
| 76 |
|
- |
| routes/pages/ | A | A | n/a | A | A | A | A | A- | **B+** |
|
|
82 |
+ |
| routes/stripe/ | A | A | n/a | A | A- | A | A | **B+** | **B+** |
|
|
83 |
+ |
| routes/stripe/checkout/ | A | A | n/a | A- | A | A | A | A | A |
|
|
84 |
+ |
| routes/synckit/ | A | A | n/a | A | A- | A | A | A | A |
|
|
85 |
+ |
| routes/pages/discover.rs | A | **B-** | n/a | A | **B** | B+ | A | A | A |
|
|
86 |
+ |
| routes/pages/ (other) | A | A | n/a | A | A | A | A | A- | **B+** |
|
| 77 |
87 |
|
| routes/embed/ | A | A | n/a | A | A | A- | A | **B** | A |
|
| 78 |
88 |
|
| routes/git/ | A | A | n/a | A | A | A | A | A | A |
|
| 79 |
89 |
|
| routes/storage/ | A | A | n/a | A | A | A | A | A | A |
|
|
90 |
+ |
| routes/storage/uploads.rs | A | A- | n/a | A | A- | A | A | A | A |
|
|
91 |
+ |
| routes/storage/images.rs | A | A- | n/a | A | A- | A | A | A | A |
|
| 80 |
92 |
|
| routes/postmark/ | A | A | n/a | A | A | A | n/a | A- | B+ |
|
| 81 |
93 |
|
|
| 82 |
94 |
|
**Bold** = cold spot (B or below).
|
| 83 |
95 |
|
|
| 84 |
96 |
|
### Cold Spots
|
| 85 |
97 |
|
|
| 86 |
|
- |
1. **routes/embed/ observability (B):** Zero `#[tracing::instrument]` on any embed handler (item.rs, project.rs, user.rs). Embeds serve third-party traffic — blind spot for latency monitoring.
|
| 87 |
|
- |
2. **routes/stripe/webhook/checkout.rs size (B+) + observability (B+):** 792 LOC (above 500-line guideline), internal `handle_*` functions lack `#[instrument]`.
|
|
98 |
+ |
1. **routes/pages/discover.rs performance (B) + architecture (B-):** Full discover page fires 5-8 sequential DB queries against same base tables. At 25-connection pool, ~4 concurrent page loads could exhaust connections.
|
|
99 |
+ |
2. **routes/embed/ observability (B):** Zero `#[tracing::instrument]` on any embed handler. Chronic from Run 20.
|
|
100 |
+ |
3. **db/discover.rs size (B):** Repetitive SQL templates across facet queries. Reflects the 5-8-query-per-request pattern.
|
|
101 |
+ |
4. **db/cart.rs correctness (B+):** PWYW minimum not enforced in `effective_price_cents()`, allowing cart bypass of creator-set minimums.
|
|
102 |
+ |
5. **scheduler/ concurrency (B+):** Advisory lock acquired from pool, not pinned to connection -- lock released immediately, providing no mutual exclusion.
|
| 88 |
103 |
|
|
| 89 |
|
- |
### Resolved Cold Spots (from Run 19)
|
|
104 |
+ |
### Resolved Cold Spots (from Run 20)
|
| 90 |
105 |
|
|
| 91 |
|
- |
- ~~db/moderation.rs type safety (B)~~ -- Fixed (typed IDs added)
|
| 92 |
|
- |
- ~~payments/connect.rs resilience (B)~~ -- Fixed (raw reqwest call removed/restructured)
|
|
106 |
+ |
- ~~routes/stripe/webhook/checkout.rs size (B+)~~ -- Now 684 LOC (was 792), under guideline threshold.
|
| 93 |
107 |
|
|
| 94 |
|
- |
## Mandatory Surprise
|
|
108 |
+ |
## Mandatory Surprises
|
| 95 |
109 |
|
|
| 96 |
|
- |
**Unexpectedly good:** The `scanning` module (2,110 LOC) implements production-grade 6-layer anti-malware infrastructure. The archive layer doesn't trust ZIP central directory size claims -- it actually decompresses entries counting bytes with an abort threshold (archive.rs:83-107). The structural analysis layer detects VMProtect-packed and UPX-packed binaries by exact section name matching to avoid false positives. ZIP bomb detection uses both compression ratio AND actual decompression byte counting. This is far above what most content platforms implement and would be impressive in a dedicated security product.
|
|
110 |
+ |
**Run 21 (5 surprises, one per axis):**
|
|
111 |
+ |
|
|
112 |
+ |
1. **Payments -- Pending refund system (unexpectedly good):** `pending_refunds.rs` + `checkout_helpers.rs` + `billing.rs` implement a complete solution for Stripe webhook ordering. Refund-before-charge queues the refund, checkout-complete checks for matching pending refund, stale refunds escalate to WAM. Bidirectional matching with `FOR UPDATE SKIP LOCKED`. Most production Stripe integrations lack this.
|
|
113 |
+ |
|
|
114 |
+ |
2. **Storage -- `claim_pending_build` (unexpectedly good):** Uses `NOT EXISTS (SELECT 1 FROM ota_builds WHERE status = 'running')` inside `FOR UPDATE SKIP LOCKED` for global single-build concurrency without advisory locks or external coordination.
|
|
115 |
+ |
|
|
116 |
+ |
3. **UX Wiring -- `json_escape` HTML entity escaping (unexpectedly good):** `types/mod.rs:214` escapes `<`, `>`, `&` as Unicode escape sequences (`\u003c`, etc.) for JSON-LD `<script>` blocks rendered with `|safe`. Prevents XSS breakout from user-controlled data in JSON-LD. Explicitly tested.
|
|
117 |
+ |
|
|
118 |
+ |
4. **Security -- SHA-256-based `constant_time_compare` (unexpectedly good):** `crypto.rs:7-18` hashes both inputs with SHA-256 before XOR comparison. This eliminates timing side channels from both value AND length differences -- goes beyond what most frameworks provide.
|
|
119 |
+ |
|
|
120 |
+ |
5. **Performance -- Scheduler advisory lock not pinned (unexpectedly bad):** `scheduler/mod.rs:77` acquires `pg_try_advisory_lock` from the pool -- the connection returns immediately, releasing the lock in microseconds. During rolling deploys, two instances could both claim the lock and execute duplicate work. The codebase knows how to pin connections (`db/mod.rs:86` uses `pool.acquire()`), making this inconsistency surprising.
|
| 97 |
121 |
|
|
| 98 |
122 |
|
### Previous Surprises
|
| 99 |
123 |
|
|
|
124 |
+ |
**Run 20:** Scanning module -- 6-layer anti-malware with actual decompression, VMProtect/UPX detection, ZIP bomb byte counting.
|
|
125 |
+ |
|
| 100 |
126 |
|
**Run 19:** Hand-rolled Stripe v2 webhook signature verification with replay protection.
|
| 101 |
127 |
|
|
| 102 |
128 |
|
**Run 18:** Sandbox tier mismatch bug (SmallFiles vs small_files). Fixed.
|
| 108 |
134 |
|
## Strengths
|
| 109 |
135 |
|
|
| 110 |
136 |
|
### 1. Security-in-depth
|
| 111 |
|
- |
Zero SQL injection vectors across 200+ queries. Argon2id with explicit params (46MiB/2 iterations), CSRF synchronizer tokens with constant-time comparison, session fixation prevention, account lockout, rate limiting, HMAC-signed URLs, 6-layer malware scanning pipeline with fail-closed design. ZIP bomb detection, path traversal prevention in archives, shell command validation in build runner.
|
|
137 |
+ |
Zero SQL injection vectors across 200+ queries. Argon2id (46MiB/2 iterations), SHA-256-based constant-time comparison (length-independent), CSRF synchronizer tokens, session fixation prevention, account lockout with anti-enumeration dummy hashes, PKCE S256 required, rate limiting on all endpoint classes, HMAC-signed URLs, 6-layer malware scanning with fail-closed, ZIP bomb detection, path traversal prevention, shell command validation. New: TOTP replay prevention via `last_used_step`, passkey counter updates preventing cloning attacks.
|
| 112 |
138 |
|
|
| 113 |
139 |
|
### 2. Type safety discipline
|
| 114 |
|
- |
36 UUID newtypes via `define_pg_uuid_id!`, 25+ domain enums via `impl_str_enum!`, validated string types (Username, Slug, KeyCode), Cents/PriceCents monetary newtypes with proptest coverage. Compile-time template verification via Askama. Proof-carrying types: once constructed, guaranteed valid.
|
|
140 |
+ |
36 UUID newtypes via `define_pg_uuid_id!`, 25+ domain enums via `impl_str_enum!`, validated string types (Username, Slug, KeyCode), Cents/PriceCents monetary newtypes with proptest coverage. Compile-time template verification via Askama. All money math in integer cents (i32/i64), zero floating point in money paths. `SUM(BIGINT)::BIGINT` cast used consistently across all aggregate queries.
|
| 115 |
141 |
|
|
| 116 |
|
- |
### 3. Test quality
|
| 117 |
|
- |
1,214+ test annotations with per-test database isolation. Property-based testing with proptest (pricing, formatting, validated types). Adversarial tests cover SQL injection, XSS, path traversal, formula injection, ZIP bombs. Integration harness mocks all external dependencies (Stripe, S3, Postmark, ClamAV).
|
|
142 |
+ |
### 3. Payment robustness
|
|
143 |
+ |
Three-layer webhook idempotency (event dedup table, status-based WHERE clauses, ON CONFLICT). Bidirectional pending refund matching. Atomic promo code reservation with cleanup on abandonment. `FOR UPDATE` row locking on tier deletion, license activation, pending refund claims. Self-purchase blocked across all checkout paths.
|
| 118 |
144 |
|
|
| 119 |
145 |
|
## Weaknesses
|
| 120 |
146 |
|
|
| 121 |
|
- |
### 1. Observability gaps in embed/ and payments/
|
| 122 |
|
- |
The embed module (serving third-party iframe traffic) and payments module (handling money) have zero `#[instrument]` annotations. These are high-value modules where tracing would provide the most benefit.
|
|
147 |
+ |
### 1. Discover page query multiplication (NEW)
|
|
148 |
+ |
Full discover page fires 5-8 sequential DB queries against overlapping base tables. The HTMX partial path (`discover_results`) correctly runs only 2 queries. At scale, this is the first bottleneck.
|
| 123 |
149 |
|
|
| 124 |
|
- |
### 2. Five files above 500-line size guideline
|
| 125 |
|
- |
health.rs (844), webhook/checkout.rs (792), exports.rs (737), license_keys.rs (741), tabs/user.rs (707). None are egregious but represent opportunities for extraction.
|
|
150 |
+ |
### 2. Observability gaps in embed/ and payments/ (CHRONIC)
|
|
151 |
+ |
Zero `#[instrument]` annotations in both modules. Carried from Run 20 (#58, #59). Third consecutive audit flagging this.
|
| 126 |
152 |
|
|
| 127 |
|
- |
### 3. async-trait still in use
|
| 128 |
|
- |
3 trait definitions still use `async-trait` crate instead of Rust 2024 native async traits (StorageBackend, EmailTransport, PaymentProvider). Chronic -- carried from Run 18.
|
|
153 |
+ |
### 3. Storage accounting drift windows
|
|
154 |
+ |
Non-atomic confirm upload (increment storage, then update item in separate queries) and soft-delete purge (deletes items without decrementing storage or cleaning version S3 keys) create drift windows. Weekly `recalculate_all_storage_batch` corrects drift, but the window is hours to days.
|
|
155 |
+ |
|
|
156 |
+ |
### 4. async-trait still in use (CHRONIC)
|
|
157 |
+ |
3 trait definitions still use `async-trait` crate. Carried from Run 18. Fourth consecutive audit.
|
|
158 |
+ |
|
|
159 |
+ |
## Bug Reports by Axis
|
|
160 |
+ |
|
|
161 |
+ |
### Payments
|
|
162 |
+ |
1 SERIOUS, 1 MINOR, 5 NOTE
|
|
163 |
+ |
|
|
164 |
+ |
| # | Sev | Location | Description |
|
|
165 |
+ |
|---|-----|----------|-------------|
|
|
166 |
+ |
| P1 | **SERIOUS** | `db/cart.rs:104-108` | Cart PWYW minimum not enforced -- `effective_price_cents()` uses `.max(0)` but not `.max(pwyw_min_cents)`. Buyer can set $0.01 for a $5-minimum item via cart. Single-item checkout validates correctly via `item_pricing.validate_amount()`. |
|
|
167 |
+ |
| P2 | MINOR | `routes/stripe/webhook/checkout.rs:643` | Guest checkout `increment_sales_count` outside transaction (pool, not db_tx). Cosmetic drift only. |
|
|
168 |
+ |
| P3 | NOTE | `pricing.rs:134-148` | FixedPricing has no upper cap on amount. Not exploitable -- fixed-price path uses `item.price_cents`, not user input. |
|
|
169 |
+ |
| P4 | NOTE | `pricing.rs:113-115` | FixedPricing `is_free()` hardcoded false. Unreachable -- `for_item()` routes price=0 to FreePricing. |
|
|
170 |
+ |
| P5 | NOTE | `routes/stripe/checkout/tips.rs:59` | Tip dollar-to-cents multiply could overflow. Guarded by `amount_dollars > 10_000` check. |
|
|
171 |
+ |
| P6 | NOTE | `payments/checkout.rs:253-257` | Cart metadata lacks per-item IDs. Reconstructed from pending_transactions. |
|
|
172 |
+ |
| P7 | NOTE | `db/subscriptions.rs:399-407` | `has_active_subscription_to_project` doesn't check `cancel_at_period_end`. By design (access until period end). |
|
|
173 |
+ |
|
|
174 |
+ |
### Storage
|
|
175 |
+ |
2 SERIOUS, 3 MINOR, 1 NOTE
|
|
176 |
+ |
|
|
177 |
+ |
| # | Sev | Location | Description |
|
|
178 |
+ |
|---|-----|----------|-------------|
|
|
179 |
+ |
| S1 | **SERIOUS** | `scheduler/cleanup.rs:188-222` | Soft-delete purge doesn't decrement `storage_used_bytes` or clean version S3 keys. Version rows CASCADE-delete, losing S3 key data permanently. Orphaned S3 objects. |
|
|
180 |
+ |
| S2 | **SERIOUS** | `routes/storage/uploads.rs:225-248` | Non-atomic confirm: `try_increment_storage` then 2 separate UPDATEs without transaction. Partial failure = stale file_size_bytes + permanent storage drift. |
|
|
181 |
+ |
| S3 | MINOR | `routes/storage/images.rs:326-342` | Image replacement doesn't delete old S3 object. Old covers orphaned permanently. |
|
|
182 |
+ |
| S4 | MINOR | `migrations/101_pending_uploads.sql` | No `UNIQUE(s3_key)` on pending_uploads. Duplicate rows possible on retry. |
|
|
183 |
+ |
| S5 | MINOR | `routes/storage/media.rs:80-91` | `classify_media` accepts any `image/*` including `image/svg+xml`. Subsequent `validate_content_type` catches it. |
|
|
184 |
+ |
| S6 | NOTE | `storage.rs:446-465` | `extract_s3_key_from_url` includes bucket name for path-style URLs. Only used for project images which handle it. |
|
|
185 |
+ |
|
|
186 |
+ |
### UX Wiring
|
|
187 |
+ |
0 SERIOUS, 2 MINOR, 5 NOTE
|
|
188 |
+ |
|
|
189 |
+ |
| # | Sev | Location | Description |
|
|
190 |
+ |
|---|-----|----------|-------------|
|
|
191 |
+ |
| U1 | MINOR | `routes/stripe/checkout/mod.rs:105-108` | Checkout cancel `item_id` not validated as UUID before `format!("/i/{}", id)`. Path traversal to internal routes possible via crafted cancel URL. |
|
|
192 |
+ |
| U2 | MINOR | `templates/pages/purchase.html:122` | PWYW `amount_cents` hidden field starts empty. JS-disabled submit sends empty string. |
|
|
193 |
+ |
| U3 | NOTE | `templates/pages/login.html:13-16` | Login form lacks CSRF. Intentional (pre-auth exempt). |
|
|
194 |
+ |
| U4 | NOTE | `templates/public.rs:471-482` | BuyPageTemplate lacks csrf_token field entirely. Intentional (guest checkout). |
|
|
195 |
+ |
| U5 | NOTE | `routes/stripe/checkout/item.rs:312` | Cancel URL built without URL-encoding. UUID is URL-safe, so harmless. |
|
|
196 |
+ |
| U6 | NOTE | `templates/pages/oauth_authorize.html:52` | redirect_uri in hidden input. Auto-escaped. Standard OAuth. |
|
|
197 |
+ |
| U7 | NOTE | `formatting.rs:4-13` | `format_price` uses f64 division. Correct for all practical prices with `{:.2}`. |
|
|
198 |
+ |
|
|
199 |
+ |
### Security
|
|
200 |
+ |
0 SERIOUS, 3 MINOR, 3 NOTE
|
|
201 |
+ |
|
|
202 |
+ |
| # | Sev | Location | Description |
|
|
203 |
+ |
|---|-----|----------|-------------|
|
|
204 |
+ |
| X1 | MINOR | `scanning/clamav.rs:96` | `contains("FOUND")` could misclassify hypothetical error containing "FOUND". Should be `ends_with("FOUND")`. |
|
|
205 |
+ |
| X2 | MINOR | `scanning/archive.rs:70` | URL-encoded path traversal check misses mixed-case `%2e%2E`. |
|
|
206 |
+ |
| X3 | MINOR | `routes/oauth.rs:253-256` | OAuth authorize accepts legacy sessions without tracking validation. Stale session could authorize grant. |
|
|
207 |
+ |
| X4 | NOTE | `scanning/mod.rs:279` | Pipeline integration test only exercises 3 of 7 FileType variants. |
|
|
208 |
+ |
| X5 | NOTE | `scanning/hash_lookup.rs:84` | MalwareBazaar `no_results` treated as Error (held for review). Deliberate fail-closed. |
|
|
209 |
+ |
| X6 | NOTE | `routes/api/users/profile.rs:140-146` | Breached password check is advisory-only. Documented policy decision. |
|
|
210 |
+ |
|
|
211 |
+ |
### Performance
|
|
212 |
+ |
2 SERIOUS, 3 MINOR, 2 NOTE
|
|
213 |
+ |
|
|
214 |
+ |
| # | Sev | Location | Description |
|
|
215 |
+ |
|---|-----|----------|-------------|
|
|
216 |
+ |
| F1 | **SERIOUS** | `routes/pages/public/discover.rs:292-513` | Full discover page fires 5-8 sequential DB queries (items + count + 4-5 facet queries). At 25-connection pool, ~4 concurrent full loads could exhaust pool. HTMX partial runs only 2 queries. |
|
|
217 |
+ |
| F2 | **SERIOUS** | `scheduler/mod.rs:77-88` | Advisory lock acquired from pool -- connection returns immediately, lock held for microseconds. Two scheduler instances can both proceed during rolling deploy. `db/mod.rs:86` shows correct `pool.acquire()` pattern. |
|
|
218 |
+ |
| F3 | MINOR | `scheduler/integrity.rs:54-65` | `check_sales_count_drift` full-scans items JOIN transactions. Weekly, but expensive at scale. |
|
|
219 |
+ |
| F4 | MINOR | `metrics.rs:206` | Idempotency middleware buffers full response body (up to 1MB). |
|
|
220 |
+ |
| F5 | MINOR | `build_runner.rs:383-386` | Build artifacts loaded entirely into RAM before S3 upload. Infrequent, low priority. |
|
|
221 |
+ |
| F6 | NOTE | `routes/synckit/subscribe.rs:123` | SSE broadcast channel size 16. Lag handled by filtering. By design. |
|
|
222 |
+ |
| F7 | NOTE | `routes/synckit/sync.rs:98-100` | Push/pull fetch all devices for validation. Bounded at 50 per app. |
|
|
223 |
+ |
|
|
224 |
+ |
## Cross-Cutting Concerns
|
|
225 |
+ |
|
|
226 |
+ |
### Storage accounting drift (Storage + Performance)
|
|
227 |
+ |
Three separate mechanisms can cause storage_used_bytes to drift: (1) soft-delete purge doesn't decrement (S1), (2) non-atomic confirm upload (S2), (3) image replacement doesn't delete old S3 objects (S3). The `recalculate_all_storage_batch` weekly job corrects byte counts but does NOT clean orphaned S3 objects. Consider a unified approach: the planned `pending_s3_deletions` durable queue (already in todo.md backlog) would address S1+S3.
|
|
228 |
+ |
|
|
229 |
+ |
### Scheduler concurrency (Performance + Storage)
|
|
230 |
+ |
The unpinned advisory lock (F2) means duplicate scheduler ticks could run concurrently. This compounds with S1 -- duplicate purge ticks could attempt to delete already-deleted S3 objects (benign due to S3 idempotency) and CASCADE-delete items twice (benign due to SQL semantics). But duplicate announcement emails (scheduler/announcements.rs) would be user-visible.
|
|
231 |
+ |
|
|
232 |
+ |
## Components Successfully Stress-Tested
|
|
233 |
+ |
|
|
234 |
+ |
### Payments (10 vectors survived)
|
|
235 |
+ |
Webhook replay, double-credit, concurrent promo exhaustion, cross-user data access, self-purchase, negative/overflow amounts, floating-point money math, out-of-order webhooks, suspended creator purchases, SUM(BIGINT) pitfall.
|
|
236 |
+ |
|
|
237 |
+ |
### Storage (8 vectors survived)
|
|
238 |
+ |
Cross-user file overwrites, path traversal in filenames, content type smuggling, storage quota bypass, double-spend on idempotent confirm, malware file serving, orphaned upload cleanup, SUM(BIGINT) pitfall.
|
|
239 |
+ |
|
|
240 |
+ |
### UX Wiring (10 vectors survived)
|
|
241 |
+ |
XSS via template injection, CSRF bypass, open redirect, user enumeration, markdown/HTML injection, pagination abuse, integer overflow in pricing, internal detail leakage, CSV injection, Unicode boundary attacks.
|
|
242 |
+ |
|
|
243 |
+ |
### Security (17 vectors survived)
|
|
244 |
+ |
Virus scan bypass via ClamAV downtime, ZIP bomb, content-type spoofing, path traversal in archives, session fixation, timing-based user enumeration, brute force login, X-Forwarded-For spoofing, session reuse after password change, OAuth code replay, PKCE downgrade, token prediction, CSRF on state-changing endpoints, SSH command injection, passkey cloning, TOTP replay, IDOR on passkeys/sessions.
|
|
245 |
+ |
|
|
246 |
+ |
### Performance (9 vectors survived)
|
|
247 |
+ |
Connection pool exhaustion (except discover), file scanning memory, SSE connection accumulation, scheduler job accumulation (mostly), background task leaks, ZIP bombs, path traversal, shell injection, lock ordering.
|
|
248 |
+ |
|
|
249 |
+ |
## Confidence Assessment
|
|
250 |
+ |
|
|
251 |
+ |
| Axis | Confidence | Notes |
|
|
252 |
+ |
|------|-----------|-------|
|
|
253 |
+ |
| Payments | HIGH | Three-layer idempotency, integer money math, bidirectional refund matching. One trust gap (PWYW cart bypass). |
|
|
254 |
+ |
| Storage | HIGH (normal) / MEDIUM (edge cases) | Presigned URL security solid. Non-atomic confirms could drift under transient DB errors. Weekly recalc corrects. |
|
|
255 |
+ |
| UX Wiring | HIGH | Askama auto-escape, json_escape defense-in-depth, comprehensive CSRF, no detail leakage. |
|
|
256 |
+ |
| Security | HIGH | No CRITICAL or SERIOUS findings. Argon2id, constant-time everywhere, fail-closed scanning, PKCE S256. |
|
|
257 |
+ |
| Performance | HIGH (current scale) | Adequate for alpha/soft launch. Discover page is first bottleneck at scale. |
|
|
258 |
+ |
|
|
259 |
+ |
## Metrics
|
|
260 |
+ |
|
|
261 |
+ |
- Modules audited: 50+
|
|
262 |
+ |
- Total cold spots: 5
|
|
263 |
+ |
- Bugs by severity: 0 critical, 5 serious, 11 minor, 16 note
|
|
264 |
+ |
- Axes at A or above: 4/5 (Performance at A-)
|
|
265 |
+ |
|
|
266 |
+ |
## Axis Summary Grades
|
|
267 |
+ |
|
|
268 |
+ |
| Axis | Overall | Cold Spots | Mandatory Surprise |
|
|
269 |
+ |
|------|---------|------------|-------------------|
|
|
270 |
+ |
| Payments | A | db/cart.rs correctness (B+) | Pending refund bidirectional matching system |
|
|
271 |
+ |
| Storage | A- | scheduler/cleanup.rs (B+), routes/storage/uploads.rs transaction safety (B) | claim_pending_build FOR UPDATE SKIP LOCKED |
|
|
272 |
+ |
| UX Wiring | A | None | json_escape HTML entity escaping in JSON-LD blocks |
|
|
273 |
+ |
| Security | A | None | SHA-256-based constant_time_compare eliminates length leaking |
|
|
274 |
+ |
| Performance | A- | discover.rs (B), scheduler/ concurrency (B+) | Scheduler advisory lock unpinned (surprisingly bad) |
|
|
275 |
+ |
|
|
276 |
+ |
## Recommended Priority Order
|
|
277 |
+ |
|
|
278 |
+ |
1. **[SERIOUS] Pin scheduler advisory lock to connection** (`scheduler/mod.rs:77`) -- Use `pool.acquire()` and hold connection for tick duration. Prevents duplicate work during rolling deploys. Small change, high impact.
|
|
279 |
+ |
2. **[SERIOUS] Enforce PWYW minimum in cart** (`db/cart.rs:104-108`) -- Change `.max(0)` to `.max(self.pwyw_min_cents.unwrap_or(0))` in `effective_price_cents()`. Trust violation for creators.
|
|
280 |
+ |
3. **[SERIOUS] Fix soft-delete purge** (`scheduler/cleanup.rs:188-222`) -- Query version S3 keys before CASCADE delete. Decrement storage_used_bytes per user. Prevents permanent S3 orphans.
|
|
281 |
+ |
4. **[SERIOUS] Wrap confirm upload in transaction** (`routes/storage/uploads.rs:225-248`) -- Use `pool.begin()` for storage increment + item updates. Prevents drift on partial failure.
|
|
282 |
+ |
5. **[SERIOUS] Optimize discover page queries** (`routes/pages/public/discover.rs:292-513`) -- Combine facet queries into single CTE, or use `tokio::try_join!` for parallel execution. First scalability bottleneck.
|
|
283 |
+ |
6. **[MINOR] Delete old S3 objects on image replacement** (`routes/storage/images.rs:326-342`) -- Prevents permanent orphans.
|
|
284 |
+ |
7. **[MINOR] Validate checkout cancel item_id as UUID** (`routes/stripe/checkout/mod.rs:105-108`)
|
|
285 |
+ |
8. **[MINOR] Fix ClamAV FOUND check** (`scanning/clamav.rs:96`) -- Change `contains("FOUND")` to `ends_with("FOUND")`.
|
|
286 |
+ |
9. **[MINOR] Case-normalize URL-encoded path traversal check** (`scanning/archive.rs:70`)
|
|
287 |
+ |
10. **[MEDIUM] Add #[instrument] to embed/ and payments/** -- Chronic from Run 20.
|
| 129 |
288 |
|
|
| 130 |
289 |
|
## Action Items
|
| 131 |
290 |
|
|
| 132 |
|
- |
### Run 20 (2026-05-04)
|
|
291 |
+ |
### Run 21 (2026-05-08)
|
| 133 |
292 |
|
|
| 134 |
|
- |
58. **[MEDIUM]** Add `#[tracing::instrument(skip_all)]` to all handlers in routes/embed/ (item.rs, project.rs, user.rs)
|
| 135 |
|
- |
59. **[MEDIUM]** Add `#[tracing::instrument(skip_all)]` to functions in payments/ (checkout.rs, connect.rs, webhooks.rs)
|
| 136 |
|
- |
60. **[LOW]** Split routes/stripe/webhook/checkout.rs (792 LOC) -- extract handle_* functions to submodule
|
| 137 |
|
- |
61. **[LOW]** Bump transitive deps: yara-x (for intaglio fix), AWS SDK chain (for rustls-webpki fix)
|
| 138 |
|
- |
62. **[DEFERRED]** Remove `async-trait` in favor of Rust 2024 native async traits (chronic, carried from Run 18 #56)
|
| 139 |
|
- |
63. **[DEFERRED]** Add README.md to server/ (carried from Run 19 #53)
|
| 140 |
|
- |
64. **[DEFERRED]** Split oversized route files: exports.rs, license_keys.rs, health.rs, tabs/user.rs
|
|
293 |
+ |
65. **[SERIOUS]** Pin scheduler advisory lock to acquired connection (`scheduler/mod.rs:77-88`)
|
|
294 |
+ |
66. **[SERIOUS]** Enforce PWYW minimum in cart `effective_price_cents()` (`db/cart.rs:104-108`)
|
|
295 |
+ |
67. **[SERIOUS]** Fix soft-delete purge: query version S3 keys, decrement storage, before CASCADE (`scheduler/cleanup.rs:188-222`)
|
|
296 |
+ |
68. **[SERIOUS]** Wrap confirm_upload DB writes in transaction (`routes/storage/uploads.rs:225-248`)
|
|
297 |
+ |
69. **[SERIOUS]** Optimize discover page: combine facet queries or parallelize with try_join! (`routes/pages/public/discover.rs:292-513`)
|
|
298 |
+ |
70. **[MINOR]** Delete old S3 objects on item image/audio/video replacement (`routes/storage/images.rs`, `routes/storage/uploads.rs`)
|
|
299 |
+ |
71. **[MINOR]** Validate checkout cancel `item_id` as UUID (`routes/stripe/checkout/mod.rs:105-108`)
|
|
300 |
+ |
72. **[MINOR]** Change ClamAV `contains("FOUND")` to `ends_with("FOUND")` (`scanning/clamav.rs:96`)
|
|
301 |
+ |
73. **[MINOR]** Case-normalize URL-encoded path traversal check (`scanning/archive.rs:70`)
|
|
302 |
+ |
74. **[MINOR]** Add UNIQUE(s3_key) to pending_uploads table (migration)
|
|
303 |
+ |
75. **[MEDIUM]** Add `#[tracing::instrument(skip_all)]` to embed/ handlers (chronic, from Run 20 #58)
|
|
304 |
+ |
76. **[MEDIUM]** Add `#[tracing::instrument(skip_all)]` to payments/ functions (chronic, from Run 20 #59)
|
|
305 |
+ |
77. **[MINOR]** Initialize PWYW amount_cents hidden field server-side (`templates/pages/purchase.html`)
|
|
306 |
+ |
78. **[MINOR]** Use `AuthUser` instead of `MaybeUser` for OAuth authorize (`routes/oauth.rs:253-256`)
|
|
307 |
+ |
79. **[DEFERRED]** Remove `async-trait` (chronic, from Run 18 #56 -> #62)
|
|
308 |
+ |
80. **[DEFERRED]** Add README.md to server/ (chronic, from Run 19 #53 -> #63)
|
|
309 |
+ |
81. **[DEFERRED]** Split oversized route files: health.rs (846), exports.rs (842), tabs/user.rs (815)
|
| 141 |
310 |
|
|
| 142 |
311 |
|
### Open (blocked on upstream)
|
| 143 |
312 |
|
|
| 146 |
315 |
|
25. Monitor aws-sdk-s3 for rustls-webpki 0.101.7 fix (RUSTSEC-2026-0049)
|
| 147 |
316 |
|
33. bincode unmaintained (RUSTSEC-2025-0141) -- upstream via syntect/yara-x, warning only
|
| 148 |
317 |
|
|
| 149 |
|
- |
## Previous Action Item Verification (Run 19)
|
|
318 |
+ |
## Previous Action Item Verification (Run 20)
|
| 150 |
319 |
|
|
| 151 |
320 |
|
| # | Item | Status |
|
| 152 |
321 |
|
|---|------|--------|
|
| 153 |
|
- |
| 51 | Add timeout to payments/connect.rs raw reqwest call | Fixed (call restructured) |
|
| 154 |
|
- |
| 52 | Add ModerationActionId newtype to db/moderation.rs | Fixed |
|
| 155 |
|
- |
| 53 | Add README.md to server/ | Unfixed (carried as #63) |
|
| 156 |
|
- |
| 54 | Bump dependency pins (tokio, uuid, chrono, yara-x, anyhow) | Fixed (all at latest) |
|
| 157 |
|
- |
| 55 | Extract inline SQL from route handlers (4 locations) | Fixed (only health.rs COUNT stats remain -- acceptable) |
|
| 158 |
|
- |
| 56 | Remove async-trait | Unfixed (chronic, carried as #62) |
|
| 159 |
|
- |
| 57 | Migrate onclick to addEventListener for strict CSP | Fixed (via dashboard usability rework) |
|
|
322 |
+ |
| 58 | Add #[instrument] to routes/embed/ | **Unfixed** (chronic, carried as #75) |
|
|
323 |
+ |
| 59 | Add #[instrument] to payments/ | **Unfixed** (chronic, carried as #76) |
|
|
324 |
+ |
| 60 | Split webhook/checkout.rs (792 LOC) | **Fixed** (now 684 LOC) |
|
|
325 |
+ |
| 61 | Bump transitive deps | Partially fixed (yara-x still has wasmtime advisory) |
|
|
326 |
+ |
| 62 | Remove async-trait | **Unfixed** (chronic, carried as #79) |
|
|
327 |
+ |
| 63 | Add README.md to server/ | **Unfixed** (chronic, carried as #80) |
|
|
328 |
+ |
| 64 | Split oversized route files | **Unfixed** (carried as #81) |
|
| 160 |
329 |
|
|
| 161 |
|
- |
5 of 7 Run 19 items fixed. 2 carried forward (1 chronic). No regressions.
|
|
330 |
+ |
2 of 7 Run 20 items fixed. 5 carried forward (3 chronic). No regressions.
|
|
331 |
+ |
|
|
332 |
+ |
### Chronic Items (unfixed across 3+ consecutive runs)
|
|
333 |
+ |
|
|
334 |
+ |
| Item | First flagged | Runs unfixed |
|
|
335 |
+ |
|------|--------------|-------------|
|
|
336 |
+ |
| Remove async-trait | Run 18 | 4 (18, 19, 20, 21) |
|
|
337 |
+ |
| Add #[instrument] to embed/ | Run 20 | 2 (20, 21) |
|
|
338 |
+ |
| Add #[instrument] to payments/ | Run 20 | 2 (20, 21) |
|
|
339 |
+ |
| Add README.md to server/ | Run 19 | 3 (19, 20, 21) |
|
|
340 |
+ |
|
|
341 |
+ |
## Delta Since Run 20
|
|
342 |
+ |
|
|
343 |
+ |
### Fixed
|
|
344 |
+ |
- webhook/checkout.rs reduced from 792 to 684 LOC (below 500-line concern threshold for this module)
|
|
345 |
+ |
- Version bumped from 0.4.10 to 0.5.7
|
|
346 |
+ |
- 8 new migrations (093-101 -> 101 total)
|
|
347 |
+ |
- LOC grew from ~83K to ~87K (+4K)
|
|
348 |
+ |
- Test annotations from 1,214 to 1,218
|
|
349 |
+ |
|
|
350 |
+ |
### New Findings (not in Run 20)
|
|
351 |
+ |
- Cart PWYW minimum bypass (SERIOUS)
|
|
352 |
+ |
- Soft-delete purge missing version S3 keys + storage decrement (SERIOUS)
|
|
353 |
+ |
- Non-atomic confirm upload (SERIOUS)
|
|
354 |
+ |
- Discover page 5-8 queries per request (SERIOUS)
|
|
355 |
+ |
- Scheduler advisory lock not pinned (SERIOUS)
|
|
356 |
+ |
- 6 new MINOR findings (old S3 cleanup, cancel UUID, ClamAV FOUND, path traversal case, pending_uploads UNIQUE, OAuth legacy session)
|
|
357 |
+ |
|
|
358 |
+ |
### Grade Changes
|
|
359 |
+ |
- Performance: A -> A- (discover page finding)
|
|
360 |
+ |
- Concurrency: A -> A- (scheduler lock finding)
|
|
361 |
+ |
- Overall: A (held)
|
| 162 |
362 |
|
|
| 163 |
363 |
|
## Metrics Over Time
|
| 164 |
364 |
|
|
| 183 |
383 |
|
| 2026-05-01 (Run 18) | ~80,470 | -- | 1,933 (34 int. fail) | ~15.1 | 0 | 5 | A |
|
| 184 |
384 |
|
| 2026-05-02 (Run 19) | ~81,384 | -- | 1,923 (0 fail) | ~23.6 | 0 | 2 | A |
|
| 185 |
385 |
|
| 2026-05-04 (Run 20) | ~83,232 | 238 | 1,214+ annotations | ~14.6 | 0 | 2 | A |
|
|
386 |
+ |
| 2026-05-08 (Run 21) | ~87,427 | -- | 1,218 annotations | ~13.9 | 0 | 5 | A |
|
| 186 |
387 |
|
|
| 187 |
388 |
|
---
|
| 188 |
389 |
|
|