Skip to main content

max / makenotwork

Bump the tower/axum middleware cluster (server, kberg) tower-http 0.6 -> 0.7, tower_governor 0.6 -> 0.8, governor 0.8 -> 0.10.4, axum-extra 0.10 -> 0.12.6, as one batch so the layer types stay compatible. kberg's tower-http moves in the same commit; it was held back deliberately to stay in step with the server. tower-sessions 0.14 -> 0.15 is NOT here and stays blocked upstream: tower-sessions-sqlx-store 0.15.0 is still the newest release and still builds against tower-sessions-core 0.14, so PostgresStore would not implement the 0.15 SessionStore trait. Rechecked against crates.io today. Same crate blocks the sqlx 0.9 task. Only code change: tower_governor made GovernorLayer's fields private, so 37 `GovernorLayer { config }` literals across 12 route modules became GovernorLayer::new(config). 36 were a mechanical rewrite; the last one wraps a multi-argument call and was done by hand. A mechanical rewrite at that scale can compile and still be wrong, so the behavior was checked rather than assumed: the rate-limiting suite confirms burst still trips, Retry-After is still emitted, and per-IP limits stay independent. axum-extra crossing two majors puts the cookie layer at risk too, so the session, login, and CSRF workflows were run as a group (45 tests). Full suite: 1889 lib + 1190 integration green, clippy clean at --all-targets --all-features. kberg's lock now carries tower-http twice; the 0.6.11 copy is reqwest 0.13.4's own dependency, not ours to move.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 19:40 UTC
Signed with PGP, not checked
Commit: 3935637feb94064b0399005e0ba15033fd0610e8
Parent: 99cc2d1
16 files changed, +140 insertions, -135 deletions
M server/Cargo.lock +75 -19
@@ -1214,15 +1214,16 @@
1214 1214
1215 1215 [[package]]
1216 1216 name = "axum-extra"
1217 - version = "0.10.3"
1217 + version = "0.12.6"
1218 1218 source = "registry+https://github.com/rust-lang/crates.io-index"
1219 - checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
1219 + checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970"
1220 1220 dependencies = [
1221 1221 "axum",
1222 1222 "axum-core",
1223 1223 "bytes",
1224 1224 "cookie",
1225 1225 "form_urlencoded",
1226 + "futures-core",
1226 1227 "futures-util",
1227 1228 "headers",
1228 1229 "http 1.4.2",
@@ -1230,7 +1231,6 @@
1230 1231 "http-body-util",
1231 1232 "mime",
1232 1233 "pin-project-lite",
1233 - "rustversion",
1234 1234 "serde_core",
1235 1235 "serde_html_form",
1236 1236 "serde_path_to_error",
@@ -3233,9 +3233,9 @@
3233 3233
3234 3234 [[package]]
3235 3235 name = "governor"
3236 - version = "0.8.1"
3236 + version = "0.10.4"
3237 3237 source = "registry+https://github.com/rust-lang/crates.io-index"
3238 - checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb"
3238 + checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8"
3239 3239 dependencies = [
3240 3240 "cfg-if",
3241 3241 "dashmap",
@@ -3243,7 +3243,7 @@
3243 3243 "futures-timer",
3244 3244 "futures-util",
3245 3245 "getrandom 0.3.4",
3246 - "no-std-compat",
3246 + "hashbrown 0.16.1",
3247 3247 "nonzero_ext",
3248 3248 "parking_lot",
3249 3249 "portable-atomic",
@@ -3558,6 +3558,19 @@
3558 3558 "webpki-roots",
3559 3559 ]
3560 3560
3561 + [[package]]
3562 + name = "hyper-timeout"
3563 + version = "0.5.2"
3564 + source = "registry+https://github.com/rust-lang/crates.io-index"
3565 + checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
3566 + dependencies = [
3567 + "hyper",
3568 + "hyper-util",
3569 + "pin-project-lite",
3570 + "tokio",
3571 + "tower-service",
3572 + ]
3573 +
3561 3574 [[package]]
3562 3575 name = "hyper-tls"
3563 3576 version = "0.6.0"
@@ -4368,7 +4381,7 @@
4368 4381 "toml 1.1.3+spec-1.1.0",
4369 4382 "totp-rs",
4370 4383 "tower",
4371 - "tower-http",
4384 + "tower-http 0.7.0",
4372 4385 "tower-sessions",
4373 4386 "tower-sessions-sqlx-store",
4374 4387 "tower_governor",
@@ -4647,12 +4660,6 @@
4647 4660 source = "registry+https://github.com/rust-lang/crates.io-index"
4648 4661 checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
4649 4662
4650 - [[package]]
4651 - name = "no-std-compat"
4652 - version = "0.4.1"
4653 - source = "registry+https://github.com/rust-lang/crates.io-index"
4654 - checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
4655 -
4656 4663 [[package]]
4657 4664 name = "nom"
4658 4665 version = "7.1.3"
@@ -5967,7 +5974,7 @@
5967 5974 "tokio",
5968 5975 "tokio-rustls",
5969 5976 "tower",
5970 - "tower-http",
5977 + "tower-http 0.6.11",
5971 5978 "tower-service",
5972 5979 "url",
5973 5980 "wasm-bindgen",
@@ -6011,7 +6018,7 @@
6011 6018 "tokio",
6012 6019 "tokio-rustls",
6013 6020 "tower",
6014 - "tower-http",
6021 + "tower-http 0.6.11",
6015 6022 "tower-service",
6016 6023 "url",
6017 6024 "wasm-bindgen",
@@ -7555,6 +7562,35 @@
7555 7562 source = "registry+https://github.com/rust-lang/crates.io-index"
7556 7563 checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
7557 7564
7565 + [[package]]
7566 + name = "tonic"
7567 + version = "0.14.6"
7568 + source = "registry+https://github.com/rust-lang/crates.io-index"
7569 + checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
7570 + dependencies = [
7571 + "async-trait",
7572 + "axum",
7573 + "base64 0.22.1",
7574 + "bytes",
7575 + "h2",
7576 + "http 1.4.2",
7577 + "http-body 1.0.1",
7578 + "http-body-util",
7579 + "hyper",
7580 + "hyper-timeout",
7581 + "hyper-util",
7582 + "percent-encoding",
7583 + "pin-project",
7584 + "socket2",
7585 + "sync_wrapper",
7586 + "tokio",
7587 + "tokio-stream",
7588 + "tower",
7589 + "tower-layer",
7590 + "tower-service",
7591 + "tracing",
7592 + ]
7593 +
7558 7594 [[package]]
7559 7595 name = "totp-rs"
7560 7596 version = "5.7.2"
@@ -7579,9 +7615,12 @@
7579 7615 dependencies = [
7580 7616 "futures-core",
7581 7617 "futures-util",
7618 + "indexmap",
7582 7619 "pin-project-lite",
7620 + "slab",
7583 7621 "sync_wrapper",
7584 7622 "tokio",
7623 + "tokio-util",
7585 7624 "tower-layer",
7586 7625 "tower-service",
7587 7626 "tracing",
@@ -7608,6 +7647,24 @@
7608 7647 version = "0.6.11"
7609 7648 source = "registry+https://github.com/rust-lang/crates.io-index"
7610 7649 checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
7650 + dependencies = [
7651 + "bitflags 2.13.0",
7652 + "bytes",
7653 + "futures-util",
7654 + "http 1.4.2",
7655 + "http-body 1.0.1",
7656 + "pin-project-lite",
7657 + "tower",
7658 + "tower-layer",
7659 + "tower-service",
7660 + "url",
7661 + ]
7662 +
7663 + [[package]]
7664 + name = "tower-http"
7665 + version = "0.7.0"
7666 + source = "registry+https://github.com/rust-lang/crates.io-index"
7667 + checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
7611 7668 dependencies = [
7612 7669 "bitflags 2.13.0",
7613 7670 "bytes",
@@ -7624,11 +7681,9 @@
7624 7681 "pin-project-lite",
7625 7682 "tokio",
7626 7683 "tokio-util",
7627 - "tower",
7628 7684 "tower-layer",
7629 7685 "tower-service",
7630 7686 "tracing",
7631 - "url",
7632 7687 "uuid",
7633 7688 ]
7634 7689
@@ -7711,9 +7766,9 @@
7711 7766
7712 7767 [[package]]
7713 7768 name = "tower_governor"
7714 - version = "0.6.0"
7769 + version = "0.8.0"
7715 7770 source = "registry+https://github.com/rust-lang/crates.io-index"
7716 - checksum = "57a2ccff6830fa835371af7541e561a90e4c07b84f72991ebac4b3cb6790dc0d"
7771 + checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6"
7717 7772 dependencies = [
7718 7773 "axum",
7719 7774 "forwarded-header-value",
@@ -7721,6 +7776,7 @@
7721 7776 "http 1.4.2",
7722 7777 "pin-project",
7723 7778 "thiserror 2.0.18",
7779 + "tonic",
7724 7780 "tower",
7725 7781 "tracing",
7726 7782 ]
@@ -16,7 +16,7 @@
16 16
17 17 # Web framework
18 18 axum = { version = "0.8.8", features = ["macros"] }
19 - axum-extra = { version = "0.10.3", features = ["cookie", "form", "query", "typed-header"] }
19 + axum-extra = { version = "0.12.6", features = ["cookie", "form", "query", "typed-header"] }
20 20
21 21 # OpenAPI spec generation
22 22 utoipa = { version = "5", features = ["axum_extras", "chrono", "uuid"] }
@@ -29,7 +29,7 @@
29 29 tokio-stream = { version = "0.1", features = ["sync"] }
30 30 tokio-util = { version = "0.7", features = ["io"] }
31 31 tower = "0.5.3"
32 - tower-http = { version = "0.6.8", features = ["trace", "fs", "limit", "request-id", "propagate-header", "set-header"] }
32 + tower-http = { version = "0.7.0", features = ["trace", "fs", "limit", "request-id", "propagate-header", "set-header"] }
33 33 tracing = "0.1.44"
34 34 tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] }
35 35
@@ -53,8 +53,8 @@
53 53 dashmap = "6"
54 54
55 55 # Rate Limiting
56 - tower_governor = "0.6.0"
57 - governor = "0.8.1"
56 + tower_governor = "0.8.0"
57 + governor = "0.10.4"
58 58
59 59 # JWT (SyncKit)
60 60 jsonwebtoken = "9.3.1"
@@ -681,7 +681,7 @@
681 681 "tokio",
682 682 "tokio-stream",
683 683 "tower",
684 - "tower-http",
684 + "tower-http 0.7.0",
685 685 "tracing",
686 686 "tracing-subscriber",
687 687 "uuid",
@@ -959,7 +959,7 @@
959 959 "tokio",
960 960 "tokio-rustls",
961 961 "tower",
962 - "tower-http",
962 + "tower-http 0.6.11",
963 963 "tower-service",
964 964 "url",
965 965 "wasm-bindgen",
@@ -1459,10 +1459,26 @@
1459 1459 "tower",
1460 1460 "tower-layer",
1461 1461 "tower-service",
1462 - "tracing",
1463 1462 "url",
1464 1463 ]
1465 1464
1465 + [[package]]
1466 + name = "tower-http"
1467 + version = "0.7.0"
1468 + source = "registry+https://github.com/rust-lang/crates.io-index"
1469 + checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
1470 + dependencies = [
1471 + "bitflags",
1472 + "bytes",
1473 + "http",
1474 + "http-body",
1475 + "percent-encoding",
1476 + "pin-project-lite",
1477 + "tower-layer",
1478 + "tower-service",
1479 + "tracing",
1480 + ]
1481 +
1466 1482 [[package]]
1467 1483 name = "tower-layer"
1468 1484 version = "0.3.3"
@@ -23,7 +23,7 @@
23 23
24 24 axum = { version = "0.8", optional = true }
25 25 tower = { version = "0.5", optional = true }
26 - tower-http = { version = "0.6", optional = true, features = ["trace"] }
26 + tower-http = { version = "0.7", optional = true, features = ["trace"] }
27 27
28 28 reqwest = { version = "0.13", optional = true, features = ["json"] }
29 29
@@ -53,17 +53,17 @@
53 53 .route("/login", with_csrf_manual(
54 54 "POST validates via validate_token_consuming (defense-in-depth on top of SameSite=Lax)",
55 55 get(crate::routes::pages::public::landing::login_page)
56 - .post(login_handler.layer(GovernorLayer { config: auth_rate_limit.clone() })),
56 + .post(login_handler.layer(GovernorLayer::new(auth_rate_limit.clone()))),
57 57 ))
58 58 .route("/auth/passkey/start", with_csrf_skip(
59 59 "pre-auth WebAuthn challenge",
60 60 post(passkey_auth_start)
61 - .layer(GovernorLayer { config: auth_rate_limit.clone() }),
61 + .layer(GovernorLayer::new(auth_rate_limit.clone())),
62 62 ))
63 63 .route("/auth/passkey/finish", with_csrf_skip(
64 64 "pre-auth WebAuthn assertion",
65 65 post(passkey_auth_finish)
66 - .layer(GovernorLayer { config: auth_rate_limit }),
66 + .layer(GovernorLayer::new(auth_rate_limit)),
67 67 ))
68 68 // Routes without auth rate limiting
69 69 .route("/logout", post_csrf(logout_handler))
@@ -71,9 +71,7 @@
71 71 // Username validation with its own rate limit
72 72 .route(
73 73 "/api/validate/username",
74 - with_csrf(post(validate_username).layer(GovernorLayer {
75 - config: validate_rate_limit,
76 - })),
74 + with_csrf(post(validate_username).layer(GovernorLayer::new(validate_rate_limit))),
77 75 )
78 76 }
79 77
@@ -546,9 +546,7 @@
546 546 "/api/sync/builds/apps/{app_id}/builds/{build_id}/cancel",
547 547 post_csrf_skip(SYNC_SKIP, cancel_build),
548 548 )
549 - .route_layer(GovernorLayer {
550 - config: write_rate_limit,
551 - });
549 + .route_layer(GovernorLayer::new(write_rate_limit));
552 550
553 551 let trigger_rate_limit = crate::helpers::rate_limiter_per_sec(
554 552 constants::BUILD_TRIGGER_RATE_LIMIT_PER_SEC,
@@ -560,9 +558,7 @@
560 558 "/api/internal/builds/trigger",
561 559 post_csrf_skip("internal CI hook: HMAC bearer auth", hook_trigger),
562 560 )
563 - .route_layer(GovernorLayer {
564 - config: trigger_rate_limit,
565 - });
561 + .route_layer(GovernorLayer::new(trigger_rate_limit));
566 562
567 563 mgmt_routes.merge(internal_routes)
568 564 }
@@ -1078,18 +1078,14 @@
1078 1078 let authorize_routes = CsrfRouter::new()
1079 1079 .route_get("/oauth/authorize", get(authorize_get))
1080 1080 .route("/oauth/authorize", post_csrf_manual("OAuth authorize validates the consent form _csrf in-handler via validate_token_consuming", authorize_post))
1081 - .route_layer(GovernorLayer {
1082 - config: authorize_rate_limit,
1083 - });
1081 + .route_layer(GovernorLayer::new(authorize_rate_limit));
1084 1082
1085 1083 let token_routes = CsrfRouter::new()
1086 1084 .route(
1087 1085 "/oauth/token",
1088 1086 post_csrf_skip("pre-auth OAuth token exchange", token_exchange),
1089 1087 )
1090 - .route_layer(GovernorLayer {
1091 - config: token_rate_limit,
1092 - });
1088 + .route_layer(GovernorLayer::new(token_rate_limit));
1093 1089
1094 1090 // userinfo is DB-amplifying (user + creator-tier lookup) and discovery is a
1095 1091 // public read; govern both so every public OAuth route carries a rate limit
@@ -1104,9 +1100,7 @@
1104 1100 "/.well-known/oauth-authorization-server",
1105 1101 get(discovery_metadata),
1106 1102 )
1107 - .route_layer(GovernorLayer {
1108 - config: read_rate_limit,
1109 - });
1103 + .route_layer(GovernorLayer::new(read_rate_limit));
1110 1104
1111 1105 authorize_routes.merge(token_routes).merge(read_routes)
1112 1106 }
@@ -611,9 +611,7 @@
611 611 "/api/v1/sync/ota/apps/{app_id}/releases/{release_id}/artifacts/confirm",
612 612 post_csrf_skip(OTA_SKIP, confirm_artifact),
613 613 )
614 - .route_layer(GovernorLayer {
615 - config: write_rate_limit,
616 - });
614 + .route_layer(GovernorLayer::new(write_rate_limit));
617 615
618 616 let read_rate_limit = crate::helpers::rate_limiter_ms(
619 617 constants::OTA_READ_RATE_LIMIT_MS,
@@ -637,9 +635,7 @@
637 635 "/api/v1/sync/ota/{slug}/download/{release_id}/{target}/{arch}",
638 636 get(artifact_download),
639 637 )
640 - .route_layer(GovernorLayer {
641 - config: read_rate_limit,
642 - });
638 + .route_layer(GovernorLayer::new(read_rate_limit));
643 639
644 640 mgmt_routes.merge(public_routes)
645 641 }
@@ -548,9 +548,7 @@
548 548 "/api/email-signup",
549 549 post_csrf_skip("pre-auth landing signup, no session", email_signup),
550 550 )
551 - .route_layer(GovernorLayer {
552 - config: write_rate_limit,
553 - });
551 + .route_layer(GovernorLayer::new(write_rate_limit));
554 552
555 553 // Password/code-verifying TOTP mutations — strict auth-strength rate limit
556 554 // (matching login), not the looser API-write limit, so confirm-code and
@@ -566,9 +564,7 @@
566 564 "/api/users/me/totp/backup-codes",
567 565 post_csrf(totp::regenerate_backup_codes),
568 566 )
569 - .route_layer(GovernorLayer {
570 - config: totp_sensitive_rate_limit,
571 - });
567 + .route_layer(GovernorLayer::new(totp_sensitive_rate_limit));
572 568
573 569 // Export routes — stricter rate limit
574 570 let export_routes = CsrfRouter::new()
@@ -589,9 +585,7 @@
589 585 )
590 586 .route("/api/export/content", post_csrf(exports::export_content))
591 587 .route("/api/export/contacts", post_csrf(exports::export_contacts))
592 - .route_layer(GovernorLayer {
593 - config: export_rate_limit,
594 - });
588 + .route_layer(GovernorLayer::new(export_rate_limit));
595 589
596 590 let key_rate_limit = crate::helpers::rate_limiter_ms(
597 591 constants::LICENSE_KEY_RATE_LIMIT_MS,
@@ -639,9 +633,7 @@
639 633 "/api/v1/license/deactivate",
640 634 post_csrf_skip(LICENSE_BEARER_SKIP, license_keys::license_deactivate),
641 635 )
642 - .route_layer(GovernorLayer {
643 - config: key_rate_limit,
644 - });
636 + .route_layer(GovernorLayer::new(key_rate_limit));
645 637
646 638 let read_rate_limit = crate::helpers::rate_limiter_ms(
647 639 constants::API_READ_RATE_LIMIT_MS,
@@ -706,9 +698,7 @@
706 698 "/api/v1/items/{id}/license.txt",
707 699 get(license_keys::license_text),
708 700 )
709 - .route_layer(GovernorLayer {
710 - config: read_rate_limit,
711 - });
701 + .route_layer(GovernorLayer::new(read_rate_limit));
712 702
713 703 let validate_rate_limit = crate::helpers::rate_limiter_per_sec(
714 704 constants::VALIDATE_RATE_LIMIT_PER_SEC,
@@ -728,9 +718,7 @@
728 718 "/api/validate/blog-slug",
729 719 post_csrf(validate::validate_blog_slug),
730 720 )
731 - .route_layer(GovernorLayer {
732 - config: validate_rate_limit,
733 - });
721 + .route_layer(GovernorLayer::new(validate_rate_limit));
734 722
735 723 // Import route needs a higher body limit (base64-encoded CSV up to 10 MB
736 724 // ≈ 14 MB encoded). The global 1 MB RequestBodyLimitLayer would reject it,
@@ -738,12 +726,10 @@
738 726 let import_routes = CsrfRouter::new()
739 727 .route("/api/users/me/import", post_csrf(imports::start_import))
740 728 .layer(axum::extract::DefaultBodyLimit::max(15 * 1024 * 1024))
741 - .route_layer(GovernorLayer {
742 - config: crate::helpers::rate_limiter_ms(
743 - constants::API_WRITE_RATE_LIMIT_MS,
744 - constants::API_WRITE_RATE_LIMIT_BURST,
745 - ),
746 - });
729 + .route_layer(GovernorLayer::new(crate::helpers::rate_limiter_ms(
730 + constants::API_WRITE_RATE_LIMIT_MS,
731 + constants::API_WRITE_RATE_LIMIT_BURST,
732 + )));
747 733
748 734 // Guest checkout routes — public, no auth, CORS-enabled, stricter rate limit
749 735 let guest_checkout_rate_limit = crate::helpers::rate_limiter_per_sec(
@@ -767,9 +753,7 @@
767 753 "/api/purchases/claim",
768 754 post_csrf(guest_checkout::claim_purchase),
769 755 )
770 - .route_layer(GovernorLayer {
771 - config: guest_checkout_rate_limit,
772 - });
756 + .route_layer(GovernorLayer::new(guest_checkout_rate_limit));
773 757
774 758 // Guest download route — separate, more lenient per-IP rate limit. Unauth'd
775 759 // and token-gated; the throttle is defense-in-depth against anonymous flooding
@@ -780,9 +764,7 @@
780 764 );
781 765 let download_routes = CsrfRouter::new()
782 766 .route_get("/download/{token}", get(guest_checkout::guest_download))
783 - .route_layer(GovernorLayer {
784 - config: download_rate_limit,
785 - });
767 + .route_layer(GovernorLayer::new(download_rate_limit));
786 768
787 769 write_routes
788 770 .merge(totp_sensitive_routes)
@@ -74,9 +74,7 @@
74 74 post(raw::smart_http_receive_pack)
75 75 .layer(DefaultBodyLimit::max(constants::GIT_RECEIVE_PACK_MAX_BYTES)),
76 76 )
77 - .route_layer(GovernorLayer {
78 - config: browse_rate_limit,
79 - })
77 + .route_layer(GovernorLayer::new(browse_rate_limit))
80 78 }
81 79
82 80 // ============================================================================