Skip to main content

max / makenotwork

Seal pricing-format/slug drift and add Cloudflare quarantine cache-purge Phase A structural seals for the recurring drift chronic: - route every cents->dollars conversion through formatting:: helpers (new format_dollars_plain) and add a grep guard banning ad-hoc `as f64 / 100.0` - create_project auto-suffixes via insert_with_unique_slug; create_collection maps the slug unique-violation to a clean 400 for all callers, killing the CLI raw 500 on collision - BoundedRecipients<T> enforces the broadcast cap in its constructor (grep guard confines the cap constant); both broadcast handlers route through it - refund-by-payment-intent/by-id and the git repo openers tightened to pub(crate) with scope docs Phase C: new cloudflare module edge-purges a quarantined object's CDN URL after origin deletion; no-op + warn until CF_API_TOKEN/CF_ZONE_ID are set.
Co-Authored-By
Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-23 19:11 UTC
Signed with PGP, not checked
Commit: 14b7fbf2c163d6fe3e6f7d43ce455a9f72bd9ae1
Parent: b07943d
26 files changed, +483 insertions, -101 deletions
@@ -46,6 +46,25 @@
46 46 format!("{sign}${dollars}.{frac:02}")
47 47 }
48 48
49 + /// Format a price in cents as a plain decimal string: no currency symbol, no
50 + /// thousands separators, always two decimal places (e.g. "9.99", "1234.50").
51 + ///
52 + /// For CSV cells and form `value=` attributes where a bare numeric is required
53 + /// and the surrounding context (spreadsheet column, template `$` prefix)
54 + /// supplies its own framing. For human-facing display use [`format_price`]
55 + /// (shows "Free" / drops trailing `.00`) or [`format_revenue`] (always `$X.XX`).
56 + ///
57 + /// This is the canonical cents→decimal conversion: bypassing it with a raw
58 + /// `as f64 / 100.0` is what the pricing-format drift chronic keeps re-finding,
59 + /// so a grep-enforcing test (below) bans that idiom outside this module.
60 + pub fn format_dollars_plain(cents: impl Into<i64>) -> String {
61 + let cents: i64 = cents.into();
62 + let neg = cents < 0;
63 + let abs = cents.unsigned_abs();
64 + let sign = if neg { "-" } else { "" };
65 + format!("{sign}{}.{:02}", abs / 100, (abs % 100) as u32)
66 + }
67 +
49 68 /// Format a byte count as a human-readable file size string.
50 69 /// Returns "N/A" for zero bytes (useful for optional file sizes).
51 70 pub fn format_file_size(bytes: i64) -> String {
@@ -168,6 +187,29 @@
168 187 mod tests {
169 188 use super::*;
170 189
190 + // ── format_dollars_plain ──
191 +
192 + #[test]
193 + fn dollars_plain_basic() {
194 + assert_eq!(format_dollars_plain(0), "0.00");
195 + assert_eq!(format_dollars_plain(999), "9.99");
196 + assert_eq!(format_dollars_plain(100), "1.00");
197 + assert_eq!(format_dollars_plain(1), "0.01");
198 + }
199 +
200 + #[test]
201 + fn dollars_plain_no_thousands_separator() {
202 + // CSV cells and form values must stay machine-parseable — no commas.
203 + assert_eq!(format_dollars_plain(123_456), "1234.56");
204 + assert_eq!(format_dollars_plain(1_000_000), "10000.00");
205 + }
206 +
207 + #[test]
208 + fn dollars_plain_negative() {
209 + assert_eq!(format_dollars_plain(-999), "-9.99");
210 + assert_eq!(format_dollars_plain(-5), "-0.05");
211 + }
212 +
171 213 // ── format_price ──
172 214
173 215 #[test]
@@ -714,3 +756,64 @@
714 756 }
715 757 }
716 758 }
759 +
760 + /// Seal for the pricing-format drift chronic.
761 + ///
762 + /// The audit kept re-finding ad-hoc `cents as f64 / 100.0` conversions scattered
763 + /// across routes, exports, and admin tooling — each one a place where a future
764 + /// edit could silently reintroduce a rounding or display inconsistency. This
765 + /// test fails the build if that idiom appears anywhere under `src/` outside this
766 + /// module, forcing every cents→dollars conversion through [`format_price`],
767 + /// [`format_revenue`], or [`format_dollars_plain`]. The seal is resolved when the
768 + /// drifted variant can no longer compile-and-pass.
769 + #[cfg(test)]
770 + mod pricing_format_seal_guard {
771 + use std::path::Path;
772 +
773 + #[test]
774 + fn no_ad_hoc_cents_to_dollars_conversion() {
775 + let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
776 + let this_file = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/formatting.rs");
777 + let mut offenders = Vec::new();
778 + walk(&src_dir, &mut |path, contents| {
779 + // This module documents the banned idiom; skip it.
780 + if path == this_file {
781 + return;
782 + }
783 + for (i, line) in contents.lines().enumerate() {
784 + // Comment/doc lines may legitimately mention the idiom.
785 + if line.trim_start().starts_with("//") {
786 + continue;
787 + }
788 + // Normalize whitespace so spacing variations all match.
789 + let squished: String = line.chars().filter(|c| !c.is_whitespace()).collect();
790 + if squished.contains("asf64/100") {
791 + offenders.push(format!("{}:{}: {}", path.display(), i + 1, line.trim()));
792 + }
793 + }
794 + });
795 + assert!(
796 + offenders.is_empty(),
797 + "pricing-format seal violated — convert cents to dollars via \
798 + formatting::format_price / format_revenue / format_dollars_plain, never a raw \
799 + `as f64 / 100.0`. Offending lines:\n{}",
800 + offenders.join("\n")
801 + );
802 + }
803 +
804 + fn walk(dir: &Path, f: &mut impl FnMut(&Path, &str)) {
805 + let Ok(entries) = std::fs::read_dir(dir) else {
806 + return;
807 + };
808 + for entry in entries.flatten() {
809 + let path = entry.path();
810 + if path.is_dir() {
811 + walk(&path, f);
812 + } else if path.extension().is_some_and(|e| e == "rs")
813 + && let Ok(contents) = std::fs::read_to_string(&path)
814 + {
815 + f(&path, &contents);
816 + }
817 + }
818 + }
819 + }
@@ -4,6 +4,7 @@
4 4 pub mod auth;
5 5 pub mod background;
6 6 pub mod build_runner;
7 + pub mod cloudflare;
7 8 pub mod config;
8 9 pub mod constants;
9 10 pub mod csrf;
@@ -402,6 +402,8 @@
402 402 scan_semaphore: state.scan_semaphore.clone(),
403 403 wam: state.wam.clone(),
404 404 clamav_healthy: clamav_healthy.clone(),
405 + cloudflare: makenotwork::cloudflare::CloudflarePurger::from_env(),
406 + cdn_base_url: state.config.cdn_base_url.as_deref().map(std::sync::Arc::from),
405 407 });
406 408 let worker_count = makenotwork::constants::SCAN_WORKER_COUNT;
407 409 let worker_shutdown_rx = shutdown_tx.subscribe();
@@ -319,8 +319,8 @@
319 319 let min = self.min_cents.unwrap_or(0);
320 320 if amount_cents < min {
321 321 return Err(format!(
322 - "Amount must be at least ${:.2}",
323 - min as f64 / 100.0
322 + "Amount must be at least {}",
323 + crate::formatting::format_revenue(min as i64)
324 324 ));
325 325 }
326 326 // Cap at $10,000 (same ceiling as tips) to prevent accidental mega-charges
@@ -448,10 +448,8 @@
448 448 let (revenue_cents, completed, refunded) =
449 449 db::transactions::get_platform_revenue_stats(pool).await?;
450 450
451 - let dollars = revenue_cents as f64 / 100.0;
452 -
453 451 println!("Platform Revenue");
454 - println!(" Total revenue: ${:.2}", dollars);
452 + println!(" Total revenue: {}", makenotwork::formatting::format_revenue(revenue_cents));
455 453 println!(" Total sales: {}", completed);
456 454 println!(" Total refunds: {}", refunded);
457 455
@@ -499,9 +497,9 @@
499 497 }
500 498
501 499 println!(
502 - "\n{} transaction(s), ${:.2} total revenue.",
500 + "\n{} transaction(s), {} total revenue.",
503 501 txs.len(),
504 - total_cents as f64 / 100.0
502 + makenotwork::formatting::format_revenue(total_cents)
505 503 );
506 504 Ok(())
507 505 }
@@ -16,6 +16,12 @@
16 16 description: Option<&str>,
17 17 is_public: bool,
18 18 ) -> Result<DbCollection> {
19 + // Collections intentionally REJECT a slug clash rather than auto-suffix
20 + // (unlike projects/items/sections): the slug is user-chosen and they should
21 + // be told it's taken. Map the per-user unique violation to a clean
22 + // validation error here so every caller — web and the CLI/service route —
23 + // gets the same 400 instead of a raw 500. This is the seal: no caller can
24 + // surface an unhandled 23505 from a collection insert.
19 25 let collection = sqlx::query_as::<_, DbCollection>(
20 26 r#"
21 27 INSERT INTO collections (user_id, slug, title, description, is_public)
@@ -29,7 +35,17 @@
29 35 .bind(description)
30 36 .bind(is_public)
31 37 .fetch_one(pool)
32 - .await?;
38 + .await
39 + .map_err(|e| {
40 + let e = crate::error::AppError::from(e);
41 + if crate::helpers::is_unique_violation(&e) {
42 + crate::error::AppError::validation(
43 + "You already have a collection with this slug".to_string(),
44 + )
45 + } else {
46 + e
47 + }
48 + })?;
33 49
34 50 Ok(collection)
35 51 }
@@ -20,23 +20,32 @@
20 20 features: &[String],
21 21 ) -> Result<DbProject> {
22 22 let project_type = super::ProjectFeature::derive_project_type(features);
23 - let project = sqlx::query_as::<_, DbProject>(
24 - r#"
25 - INSERT INTO projects (user_id, slug, title, description, project_type, features)
26 - VALUES ($1, $2, $3, $4, $5, $6)
27 - RETURNING *
28 - "#,
29 - )
30 - .bind(user_id)
31 - .bind(slug)
32 - .bind(title)
33 - .bind(description)
34 - .bind(project_type)
35 - .bind(features)
36 - .fetch_one(pool)
37 - .await?;
38 -
39 - Ok(project)
23 + // Slug uniqueness is enforced by the per-table unique indexes — including the
24 + // cross-creator `idx_projects_public_slug` (migration 062). Route the bare
25 + // INSERT through `insert_with_unique_slug` so a collision auto-suffixes
26 + // (`slug`, `slug-2`, ...) and retries instead of surfacing a raw 500 (the
27 + // CHRONIC slug-dedup drift, ultra-fuzz Run 2 UX). This is the seal: there is
28 + // no public bare-insert constructor for projects.
29 + crate::helpers::insert_with_unique_slug(slug.as_str(), |candidate| async move {
30 + let candidate = Slug::from_trusted(candidate);
31 + sqlx::query_as::<_, DbProject>(
32 + r#"
33 + INSERT INTO projects (user_id, slug, title, description, project_type, features)
34 + VALUES ($1, $2, $3, $4, $5, $6)
35 + RETURNING *
36 + "#,
37 + )
38 + .bind(user_id)
39 + .bind(&candidate)
40 + .bind(title)
41 + .bind(description)
42 + .bind(project_type)
43 + .bind(features)
44 + .fetch_one(pool)
45 + .await
46 + .map_err(Into::into)
47 + })
48 + .await
40 49 }
41 50
42 51 /// Fetch a project by primary key. Returns `None` if not found.