//! Centralized application constants //! //! Magic numbers that were scattered across modules. Storage-specific limits //! (file sizes, presign expiry, allowed types) remain in storage.rs since //! they're only used there. // -- Database -- pub const DB_POOL_MAX_CONNECTIONS: u32 = 25; pub const DB_POOL_MIN_CONNECTIONS: u32 = 2; pub const DB_ACQUIRE_TIMEOUT_SECS: u64 = 3; /// Rotate connections after 30 minutes to prevent stale sessions. pub const DB_MAX_LIFETIME_SECS: u64 = 1800; /// Prune idle connections after 10 minutes. pub const DB_IDLE_TIMEOUT_SECS: u64 = 600; // -- Sessions -- pub const SESSION_EXPIRY_DAYS: i64 = 7; /// Skip DB touch if validated within this window. Doubles as the upper bound on /// session-revocation lag (admin suspend, logout-everywhere, password change) — /// shorter = tighter revocation, slightly more DB load on the auth hot path. pub const SESSION_TOUCH_CACHE_SECS: u64 = 5; // -- Login security -- pub const MAX_LOGIN_ATTEMPTS: i32 = 5; pub const LOCKOUT_MINUTES: i64 = 15; /// How long a half-completed login (password verified, awaiting 2FA) stays /// valid before the user must re-enter their password. Defends against the /// "unattended browser one TOTP from logged in" failure mode. pub const PENDING_2FA_TTL_SECS: i64 = 600; // -- Email link expiry (seconds) -- pub const PASSWORD_RESET_EXPIRY_SECS: i64 = 900; // 15 minutes pub const EMAIL_VERIFICATION_EXPIRY_SECS: i64 = 86400; // 24 hours pub const ACCOUNT_DELETION_EXPIRY_SECS: i64 = 3600; // 1 hour // -- Stripe fees (for display only — actual fees set by Stripe) -- pub const STRIPE_FEE_PERCENTAGE: f64 = 0.029; // 2.9% pub const STRIPE_FEE_FIXED_CENTS: f64 = 30.0; // $0.30 /// Stripe minimum charge amount in cents (USD). Charges below this are rejected. pub const STRIPE_MINIMUM_CHARGE_CENTS: i64 = 50; // $0.50 // -- Page / query limits -- pub const DASHBOARD_TRANSACTION_LIMIT: i64 = 100; // -- SyncKit -- pub const SYNCKIT_JWT_EXPIRY_SECS: i64 = 7 * 24 * 3600; // 7 days pub const SYNCKIT_PUSH_MAX_CHANGES: usize = 500; pub const SYNCKIT_PULL_PAGE_SIZE: i64 = 500; pub const SYNCKIT_API_KEY_LENGTH: usize = 32; // 32 bytes = 64 hex chars pub const SYNC_LOG_RETAIN_DAYS: i64 = 90; pub const SYNC_LOG_COMPACT_MIN_AGE_DAYS: i64 = 7; // Safety margin for cursor-based compaction pub const SYNCKIT_MAX_BLOB_SIZE_BYTES: i64 = 500 * 1024 * 1024; // 500 MB pub const SYNCKIT_MAX_BLOB_STORAGE_BYTES: i64 = 10 * 1024 * 1024 * 1024; // 10 GB per user per app pub const SYNCKIT_MAX_DEVICES_PER_APP: i64 = 50; // Max devices per user per app pub const SYNCKIT_BLOB_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour pub const SYNCKIT_MAX_SSE_CONNECTIONS_PER_USER: usize = 10; pub const SYNCKIT_ROTATION_STALE_HOURS: i64 = 24; pub const SYNCKIT_ROTATION_BATCH_MAX: usize = 500; // -- Subscriptions -- pub const MIN_SUBSCRIPTION_PRICE_CENTS: i32 = 100; // $1.00 minimum // -- OAuth -- pub const OAUTH_CODE_EXPIRY_SECS: i64 = 600; // 10 minutes pub const OAUTH_CODE_LENGTH: usize = 32; // 32 bytes = 64 hex chars // -- Health monitoring -- pub const HEALTH_CHECK_INTERVAL_SECS: u64 = 60; pub const ALERT_COOLDOWN_SECS: u64 = 300; // 5 minutes pub const HEALTH_HISTORY_RETAIN_DAYS: i64 = 90; // -- Scheduled publish -- pub const SCHEDULER_INTERVAL_SECS: u64 = 60; // How often the rate-limiter bucket-map sweeper reclaims stale GCRA entries. // Bounds limiter map size by active (not cumulative-unique) client keys. pub const GOVERNOR_SWEEP_INTERVAL_SECS: u64 = 60; // -- TOTP / 2FA -- pub const TOTP_SKEW: u8 = 1; // Allow +/-1 time step (+/-30s) pub const TOTP_STEP: u64 = 30; // 30-second windows pub const TOTP_DIGITS: usize = 6; // 6-digit codes pub const BACKUP_CODE_COUNT: usize = 10; // Generate 10 codes pub const BACKUP_CODE_LENGTH: usize = 8; // 8 alphanumeric chars // -- Anti-enumeration -- pub const USERNAME_CHECK_DELAY_MS: u64 = 400; // -- Rate limiting -- // Auth endpoints (login, join): burst 5, then 2/sec. // fast-tests: relaxed to burst 20 so lockout tests can fire 5+ attempts without hitting rate limiter. #[cfg(not(feature = "fast-tests"))] pub const AUTH_RATE_LIMIT_MS: u64 = 500; #[cfg(not(feature = "fast-tests"))] pub const AUTH_RATE_LIMIT_BURST: u32 = 5; #[cfg(feature = "fast-tests")] pub const AUTH_RATE_LIMIT_MS: u64 = 10; #[cfg(feature = "fast-tests")] pub const AUTH_RATE_LIMIT_BURST: u32 = 20; // Username validation: burst 10, then 1/sec pub const VALIDATE_RATE_LIMIT_PER_SEC: u64 = 1; pub const VALIDATE_RATE_LIMIT_BURST: u32 = 10; // API write endpoints (CRUD): burst 30, then 2/sec pub const API_WRITE_RATE_LIMIT_MS: u64 = 500; pub const API_WRITE_RATE_LIMIT_BURST: u32 = 30; // API read endpoints (GET): burst 60, then 10/sec (prevents enumeration) pub const API_READ_RATE_LIMIT_MS: u64 = 100; pub const API_READ_RATE_LIMIT_BURST: u32 = 60; // API export endpoints: burst 3, then 1/sec pub const API_EXPORT_RATE_LIMIT_PER_SEC: u64 = 1; pub const API_EXPORT_RATE_LIMIT_BURST: u32 = 3; // Guest checkout (public, no auth): burst 10, then 1/sec pub const GUEST_CHECKOUT_RATE_LIMIT_PER_SEC: u64 = 1; pub const GUEST_CHECKOUT_RATE_LIMIT_BURST: u32 = 10; // License key validation (public): burst 20, then 5/sec pub const LICENSE_KEY_RATE_LIMIT_MS: u64 = 200; pub const LICENSE_KEY_RATE_LIMIT_BURST: u32 = 20; // File upload: burst 10, then 2/sec pub const UPLOAD_RATE_LIMIT_MS: u64 = 500; pub const UPLOAD_RATE_LIMIT_BURST: u32 = 10; // OAuth authorize/token: burst 5/10, then 2/sec pub const OAUTH_RATE_LIMIT_MS: u64 = 500; pub const OAUTH_RATE_LIMIT_BURST: u32 = 5; pub const OAUTH_TOKEN_RATE_LIMIT_MS: u64 = 500; pub const OAUTH_TOKEN_RATE_LIMIT_BURST: u32 = 10; // SyncKit auth: burst 5, then 1/sec pub const SYNCKIT_AUTH_RATE_LIMIT_PER_SEC: u64 = 1; pub const SYNCKIT_AUTH_RATE_LIMIT_BURST: u32 = 5; // SyncKit sync (push/pull) — per-IP: burst 30, then 10/sec pub const SYNCKIT_SYNC_RATE_LIMIT_MS: u64 = 100; pub const SYNCKIT_SYNC_RATE_LIMIT_BURST: u32 = 30; // SyncKit sync — per-app: burst 60, then 20/sec (higher than per-IP because // a single app may have many users behind different IPs) pub const SYNCKIT_APP_RATE_LIMIT_MS: u64 = 50; pub const SYNCKIT_APP_RATE_LIMIT_BURST: u32 = 60; // 2FA verification: burst 5, then 2/sec (same as auth) pub const TWO_FACTOR_RATE_LIMIT_MS: u64 = 500; pub const TWO_FACTOR_RATE_LIMIT_BURST: u32 = 5; // Dashboard tab reads: generous but bounded (5/sec, burst 20) pub const DASHBOARD_READ_RATE_LIMIT_MS: u64 = 200; pub const DASHBOARD_READ_RATE_LIMIT_BURST: u32 = 20; // -- Pagination -- pub const DISCOVER_PAGE_SIZE: u32 = 25; pub const FEED_PAGE_SIZE: u32 = 25; pub const PAGINATION_WINDOW_SIZE: u32 = 5; // -- Creator broadcast fan-out -- /// Max concurrent in-flight email sends per broadcast. The outer worker /// task spawns up to this many child tasks, then waits on one to drain /// before spawning the next. pub const BROADCAST_PARALLELISM: usize = 16; /// Delay between successive broadcast send-task spawns. Spreads Postmark /// API load when a creator with thousands of followers fires a broadcast /// — at parallelism 16 + 100 ms cadence, steady-state is ~10 sends/sec. pub const BROADCAST_CHUNK_DELAY_MS: u64 = 100; /// Recipient cap per broadcast send. Above this, the request is refused /// with an instruction to contact support. Bounds Postmark spend exposure /// from any single approved creator. Founder-window cohort is well under /// this; the cap is the floor we'd lift on request, not the ceiling. pub const BROADCAST_MAX_RECIPIENTS: usize = 10_000; /// Cap on buyer-departure notification fan-out per creator-deletion event. /// Account deletion notifies historical buyers about content removal. A /// creator with millions of completed sales should not turn one deletion /// into a Postmark bomb. The cap bounds both the in-memory buyer list and /// total outbound email volume; if hit, we log a warning and notify the /// oldest-buyers slice the SQL chose. pub const BUYER_DEPARTURE_MAX_NOTIFICATIONS: i64 = 50_000; // -- File scanning -- pub const SCAN_MAX_MEMORY_BYTES: usize = 100 * 1024 * 1024; // 100 MB in-memory threshold pub const SCAN_MAX_CONCURRENT: usize = 4; // Max concurrent file scans (each can use up to 100 MB RAM) pub const SCAN_WORKER_COUNT: usize = 2; // Background worker tasks draining scan_jobs queue /// Retention window for terminal-state (`done`, `failed`) `scan_jobs` rows. /// Queued/running rows are operational queue state and not affected. pub const SCAN_JOB_RETENTION_DAYS: u32 = 30; /// Directory under which the scanner spools large objects to tempfiles /// before invoking path/stream-based layer entries. On production, systemd /// provisions this via `StateDirectory=mnw/scan-spool` so the path resolves /// to `/var/lib/mnw/scan-spool`. Override with `MNW_SCAN_SPOOL_DIR` for dev. pub const SCAN_SPOOL_DIR: &str = "/var/lib/makenotwork/scan-spool"; /// Files in `SCAN_SPOOL_DIR` older than this are considered orphaned /// (a panic, OOM, or hard kill left them behind) and reaped on the /// next sweep. RAII drop in `SpoolHandle` covers the live path; this /// covers process-death. pub const SCAN_SPOOL_ORPHAN_AGE_SECS: u64 = 3600; /// Hard cap on a single spooled object. Above this, the scanner refuses /// the job rather than risk filling the volume. 8 GiB matches the largest /// payload the upload tier currently allows. pub const SCAN_SPOOL_MAX_BYTES: u64 = 8 * 1024 * 1024 * 1024; /// Minimum free space the spool volume must retain after writing the /// pending object. The scanner refuses if `statvfs(free) - expected_size` /// would drop below this threshold. pub const SCAN_SPOOL_FREE_RESERVE_BYTES: u64 = 2 * 1024 * 1024 * 1024; // -- Caddy on-demand TLS -- // Caps concurrent cache-miss DB lookups in `/api/domains/caddy-ask`. Cache hits // are unbounded (DashMap). At capacity, the handler returns 503 so Caddy retries // later instead of stampeding the DB pool or driving ACME issuance for garbage // domains. Sized small because the slow path is one indexed lookup. pub const CADDY_ASK_MAX_CONCURRENT: usize = 8; pub const SCAN_ZIP_MAX_RATIO: f64 = 100.0; // Max compression ratio before ZIP bomb pub const SCAN_ZIP_MAX_DEPTH: u32 = 2; // Max nested archives (detection is 1 level deep; decompressed size limit is the primary defense) pub const SCAN_ZIP_MAX_UNCOMPRESSED: u64 = 2 * 1024 * 1024 * 1024; // 2 GB uncompressed limit pub const SCAN_MALWAREBAZAAR_TIMEOUT_SECS: u64 = 5; pub const SCAN_CLAMAV_TIMEOUT_SECS: u64 = 30; // -- Invite system -- pub const INVITES_ENABLED: bool = true; pub const INVITE_LIMIT_PER_CREATOR: i64 = 5; // max unredeemed codes per creator // -- Git source browser -- pub const GIT_MAX_FILE_SIZE_BYTES: usize = 1_024_000; // 1MB display limit pub const GIT_COMMITS_PER_PAGE: usize = 30; pub const GIT_DIFF_MAX_FILES: usize = 20; // Inline diff hunks for first N files pub const GIT_DIFF_MAX_LINES: usize = 500; // Per-file line cap for diff display pub const GIT_REPOS_PER_PAGE: usize = 30; pub const GIT_FILE_LOG_MAX_WALK: usize = 1000; // Max commits to walk for per-file history pub const GIT_RAW_MAX_BYTES: usize = 100 * 1024 * 1024; // 100 MB raw download limit pub const GIT_UPLOAD_PACK_MAX_BYTES: usize = 10 * 1024 * 1024; // 10 MB upload-pack body limit // -- Webhook security -- pub const WEBHOOK_TIMESTAMP_TOLERANCE_SECS: u64 = 300; // 5 minutes // -- Collections -- pub const MAX_COLLECTIONS_PER_USER: i64 = 50; pub const MAX_ITEMS_PER_COLLECTION: i64 = 200; // -- OTA updates -- pub const OTA_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour // OTA management: burst 10, then 2/sec (same as API write) pub const OTA_WRITE_RATE_LIMIT_MS: u64 = 500; pub const OTA_WRITE_RATE_LIMIT_BURST: u32 = 10; // OTA public (updater check, download): burst 30, then 10/sec pub const OTA_READ_RATE_LIMIT_MS: u64 = 100; pub const OTA_READ_RATE_LIMIT_BURST: u32 = 30; // -- Build pipeline -- pub const BUILD_TIMEOUT_SECS: u64 = 1800; // 30 min pub const BUILD_MAX_LOG_BYTES: usize = 5_242_880; // 5 MB pub const BUILD_HISTORY_LIMIT: i64 = 50; pub const BUILD_TRIGGER_RATE_LIMIT_PER_SEC: u64 = 1; pub const BUILD_TRIGGER_RATE_LIMIT_BURST: u32 = 3; pub const BUILD_WRITE_RATE_LIMIT_MS: u64 = 500; pub const BUILD_WRITE_RATE_LIMIT_BURST: u32 = 10; // Git browsing: burst 30, then 5/sec (blame/log can be expensive) pub const GIT_BROWSE_RATE_LIMIT_MS: u64 = 200; pub const GIT_BROWSE_RATE_LIMIT_BURST: u32 = 30; pub const BUILD_ALLOWED_TARGETS: &[&str] = &[ "linux/x86_64", "linux/aarch64", "darwin/x86_64", "darwin/aarch64", ]; // -- Streaming -- pub const STREAMING_CACHE_MAX_SECS: u64 = 86400; // 24 hours max presigned URL lifetime /// Rate limit for stream/download URL requests: 1 per 3 seconds, burst of 10. pub const STREAM_RATE_LIMIT_MS: u64 = 3000; pub const STREAM_RATE_LIMIT_BURST: u32 = 10; // -- Date display formats -- pub const DATE_FMT_SHORT: &str = "%b %d"; // "Mar 25" pub const DATE_FMT_FULL: &str = "%b %d, %Y"; // "Mar 25, 2026" pub const DATE_FMT_ISO: &str = "%Y-%m-%d"; // "2026-03-25" pub const DATE_FMT_DATETIME: &str = "%b %d, %Y %H:%M"; // "Mar 25, 2026 14:30" pub const DATE_FMT_DATETIME_UTC: &str = "%b %d, %Y %H:%M UTC"; // "Mar 25, 2026 14:30 UTC" // -- Platform content -- pub const CHANGELOG_PROJECT_SLUG: &str = "changelog"; // -- String / buffer limits -- pub const USER_AGENT_MAX_LENGTH: usize = 512; pub const SYNCKIT_MAX_KEY_ENVELOPE_BYTES: usize = 4096; pub const MAX_PRICE_CENTS: i32 = 1_000_000; // $10,000 // -- Sandbox accounts -- /// How long a sandbox session lasts before auto-cleanup. pub const SANDBOX_EXPIRY_SECS: i64 = 3600; // 1 hour /// How often the cleanup job runs. pub const SANDBOX_CLEANUP_INTERVAL_SECS: u64 = 300; // 5 minutes /// Rate limit: sandbox creation. /// Production: 1 per 30 seconds, burst 2. fast-tests: 1 per 10ms, burst 10. /// Run integration tests with `cargo test --features fast-tests` to avoid rate-limit failures. #[cfg(not(feature = "fast-tests"))] pub const SANDBOX_RATE_LIMIT_MS: u64 = 30_000; #[cfg(not(feature = "fast-tests"))] pub const SANDBOX_RATE_LIMIT_BURST: u32 = 2; #[cfg(feature = "fast-tests")] pub const SANDBOX_RATE_LIMIT_MS: u64 = 10; #[cfg(feature = "fast-tests")] pub const SANDBOX_RATE_LIMIT_BURST: u32 = 10; /// Max concurrent active sandboxes per IP. pub const SANDBOX_MAX_PER_IP: i64 = 3; // ── Compile-time invariants on the constants above ─────────────────────────── // // Encoded as `const _: () = assert!(...)` rather than `#[test]` functions: these // are checked when the crate is COMPILED, so a bad constant fails the build // (not just a test run), and the whole invariant set sits next to the values. // Price constants const _: () = assert!(MAX_PRICE_CENTS > 0); const _: () = assert!(MAX_PRICE_CENTS <= 10_000_000); // <= $100,000 const _: () = assert!(MIN_SUBSCRIPTION_PRICE_CENTS > 0); const _: () = assert!(MIN_SUBSCRIPTION_PRICE_CENTS < MAX_PRICE_CENTS); // Stripe fee constants const _: () = assert!(STRIPE_FEE_PERCENTAGE > 0.0 && STRIPE_FEE_PERCENTAGE < 0.5); const _: () = assert!(STRIPE_FEE_FIXED_CENTS > 0.0); // Database pool const _: () = assert!(DB_POOL_MAX_CONNECTIONS > DB_POOL_MIN_CONNECTIONS); const _: () = assert!(DB_POOL_MIN_CONNECTIONS > 0); const _: () = assert!(DB_ACQUIRE_TIMEOUT_SECS > 0); const _: () = assert!(DB_MAX_LIFETIME_SECS > DB_IDLE_TIMEOUT_SECS); // Session constants const _: () = assert!(SESSION_EXPIRY_DAYS > 0 && SESSION_EXPIRY_DAYS <= 365); const _: () = assert!(SESSION_TOUCH_CACHE_SECS > 0 && SESSION_TOUCH_CACHE_SECS < 86400); // Login security const _: () = assert!(MAX_LOGIN_ATTEMPTS > 0); const _: () = assert!(LOCKOUT_MINUTES > 0); // Email link expiry ordering const _: () = assert!(PASSWORD_RESET_EXPIRY_SECS > 0); const _: () = assert!(EMAIL_VERIFICATION_EXPIRY_SECS > PASSWORD_RESET_EXPIRY_SECS); const _: () = assert!(ACCOUNT_DELETION_EXPIRY_SECS > 0); // Scheduler const _: () = assert!(SCHEDULER_INTERVAL_SECS > 0); // Rate-limit bursts all positive const _: () = assert!(AUTH_RATE_LIMIT_BURST > 0); const _: () = assert!(VALIDATE_RATE_LIMIT_BURST > 0); const _: () = assert!(API_WRITE_RATE_LIMIT_BURST > 0); const _: () = assert!(API_READ_RATE_LIMIT_BURST > 0); const _: () = assert!(API_EXPORT_RATE_LIMIT_BURST > 0); const _: () = assert!(LICENSE_KEY_RATE_LIMIT_BURST > 0); const _: () = assert!(UPLOAD_RATE_LIMIT_BURST > 0); const _: () = assert!(OAUTH_RATE_LIMIT_BURST > 0); const _: () = assert!(OAUTH_TOKEN_RATE_LIMIT_BURST > 0); // Rate-limit burst ordering: read > write > auth const _: () = assert!(API_READ_RATE_LIMIT_BURST > API_WRITE_RATE_LIMIT_BURST); const _: () = assert!(API_WRITE_RATE_LIMIT_BURST > AUTH_RATE_LIMIT_BURST); // Rate-limit intervals positive const _: () = assert!(AUTH_RATE_LIMIT_MS > 0); const _: () = assert!(API_WRITE_RATE_LIMIT_MS > 0); const _: () = assert!(API_READ_RATE_LIMIT_MS > 0); // File size limits const _: () = assert!(SCAN_MAX_MEMORY_BYTES > 0); const _: () = assert!(SCAN_SPOOL_FREE_RESERVE_BYTES < SCAN_SPOOL_MAX_BYTES); const _: () = assert!(SCAN_SPOOL_MAX_BYTES > SCAN_MAX_MEMORY_BYTES as u64); const _: () = assert!(SCAN_JOB_RETENTION_DAYS >= 7); // no same-day purge race const _: () = assert!(BROADCAST_PARALLELISM > 0 && BROADCAST_PARALLELISM <= 64); const _: () = assert!(SCAN_ZIP_MAX_UNCOMPRESSED > SCAN_MAX_MEMORY_BYTES as u64); const _: () = assert!(GIT_RAW_MAX_BYTES > GIT_MAX_FILE_SIZE_BYTES); const _: () = assert!(SCAN_ZIP_MAX_RATIO > 0.0); const _: () = assert!(SCAN_ZIP_MAX_DEPTH > 0); // SyncKit const _: () = assert!(SYNCKIT_PUSH_MAX_CHANGES > 0); const _: () = assert!(SYNCKIT_PULL_PAGE_SIZE > 0); const _: () = assert!(SYNCKIT_MAX_BLOB_SIZE_BYTES > 0); const _: () = assert!(SYNCKIT_JWT_EXPIRY_SECS > 0); // TOTP const _: () = assert!(TOTP_DIGITS == 6); const _: () = assert!(TOTP_STEP == 30); const _: () = assert!(BACKUP_CODE_COUNT > 0); const _: () = assert!(BACKUP_CODE_LENGTH > 0); // Pagination const _: () = assert!(DISCOVER_PAGE_SIZE > 0); const _: () = assert!(FEED_PAGE_SIZE > 0); const _: () = assert!(PAGINATION_WINDOW_SIZE > 0); // String constants non-empty const _: () = assert!(!DATE_FMT_SHORT.is_empty()); const _: () = assert!(!DATE_FMT_FULL.is_empty()); const _: () = assert!(!DATE_FMT_ISO.is_empty()); const _: () = assert!(!DATE_FMT_DATETIME.is_empty()); const _: () = assert!(!DATE_FMT_DATETIME_UTC.is_empty()); const _: () = assert!(!CHANGELOG_PROJECT_SLUG.is_empty()); const _: () = assert!(!BUILD_ALLOWED_TARGETS.is_empty()); // Collections const _: () = assert!(MAX_COLLECTIONS_PER_USER > 0); const _: () = assert!(MAX_ITEMS_PER_COLLECTION > 0); // Build pipeline const _: () = assert!(BUILD_TIMEOUT_SECS > 0); const _: () = assert!(BUILD_MAX_LOG_BYTES > 0); // Health monitoring const _: () = assert!(HEALTH_CHECK_INTERVAL_SECS > 0); const _: () = assert!(ALERT_COOLDOWN_SECS > HEALTH_CHECK_INTERVAL_SECS); // Sandbox const _: () = assert!(SANDBOX_EXPIRY_SECS > 0); const _: () = assert!(SANDBOX_CLEANUP_INTERVAL_SECS > 0); const _: () = assert!(SANDBOX_CLEANUP_INTERVAL_SECS < SANDBOX_EXPIRY_SECS as u64); const _: () = assert!(SANDBOX_MAX_PER_IP > 0); // Webhook const _: () = assert!(WEBHOOK_TIMESTAMP_TOLERANCE_SECS > 0); // OAuth const _: () = assert!(OAUTH_CODE_EXPIRY_SECS > 0); const _: () = assert!(OAUTH_CODE_LENGTH > 0); // Buffer limits const _: () = assert!(USER_AGENT_MAX_LENGTH > 0); const _: () = assert!(SYNCKIT_MAX_KEY_ENVELOPE_BYTES > 0); #[cfg(test)] mod tests { use super::*; /// The build-target FORMAT check uses `str::contains`, which isn't const — /// so this invariant stays a runtime test (the rest are compile-time above). #[test] fn build_allowed_targets_are_os_slash_arch() { for target in BUILD_ALLOWED_TARGETS { assert!(!target.is_empty()); assert!(target.contains('/'), "target should be os/arch format: {target}"); } } }