//! 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; /// Per-statement wall-clock ceiling, applied to every pooled connection via /// `SET statement_timeout`. `acquire_timeout` only bounds *getting* a /// connection, not *running* a query, without this a single wedged query pins /// its connection forever and 25 of them exhaust the pool with no recovery. /// Generous so legitimate maintenance sweeps and boot migrations aren't killed; /// a job that genuinely needs longer sets `SET LOCAL statement_timeout = 0` in /// its own transaction. pub const DB_STATEMENT_TIMEOUT_SECS: u64 = 120; /// Ceiling on how long a statement waits to acquire a lock (`SET lock_timeout`). /// A lock-contended query fails fast instead of blocking a connection /// indefinitely, the most common way the pool wedges. pub const DB_LOCK_TIMEOUT_SECS: u64 = 30; /// Largest file the browser upload path will accept. The browser issues exactly /// one presigned `PutObject` and a tab cannot resume it, so a transfer that /// drops at 90% starts over from zero. That is the binding constraint, not the /// protocol: S3 / Ceph (Hetzner Object Storage) tolerate a single PUT up to /// 5 GiB, but 2 GiB is as much as we are willing to ask someone to re-send. /// Files above it upload through the CLI / desktop clients, which chunk and /// resume, and the (higher) per-tier `max_file_bytes` still governs there, so /// the advertised 20 GB tier is unaffected. Refusing up front with a pointer to /// those clients beats handing out a presigned URL for a doomed transfer. pub const BROWSER_UPLOAD_MAX_BYTES: u64 = 2 * 1024 * 1024 * 1024; /// Largest source a single server-side `CopyObject` can promote. S3 rejects a /// one-shot copy above 5 GiB; a larger source must go through ranged multipart /// `UploadPartCopy`. This is a protocol limit, where /// [`BROWSER_UPLOAD_MAX_BYTES`] is a product decision about resumability, so the /// two are separate constants and neither should be reused for the other. /// Without this branch a large upload succeeds and then fails at promote, the /// worst position to fail in. pub const S3_SINGLE_COPY_MAX_BYTES: u64 = 5 * 1024 * 1024 * 1024; /// Lifetime of an internal-API actor assertion, minted at `ssh-key-lookup` and /// forwarded by the CLI for the session. 24h comfortably exceeds any SSH session. pub const INTERNAL_ACTOR_TTL_SECS: i64 = 86_400; // 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; /// Cap on tracked sessions per user. Each login mints a `user_sessions` row; /// without a bound, repeated logins (or an automated loop) grow the table /// unboundedly. After a new session is recorded, the oldest beyond this many are /// pruned (a sliding window), the current session is always the newest, so it /// is never evicted. Matches the 100-row session-listing cap, so anything pruned /// was already invisible bloat, never a session a user could see or manage. pub const MAX_SESSIONS_PER_USER: i64 = 100; // 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's minimum charge is per settlement currency (GBP is 30, the rest 50), // so it lives on SettlementCurrency::minimum_charge_cents rather than here. // 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 /// Largest blob the one-shot presigned PUT will sign. A single PUT holds the /// whole object in one request the client cannot resume, so this stays modest; /// bigger blobs go through the multipart session below. pub const SYNCKIT_MAX_BLOB_SIZE_BYTES: i64 = 500 * 1024 * 1024; // 500 MB /// Largest blob the multipart session will accept. Set to the per-user-per-app /// storage allowance: a blob above it could never be stored anyway, so the /// session refuses it before it stages S3 parts that confirm would only reject. pub const SYNCKIT_MAX_MULTIPART_BLOB_SIZE_BYTES: i64 = SYNCKIT_MAX_BLOB_STORAGE_BYTES; 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; /// Max usage-warning candidates a single scheduler tick fetches. Bounds the /// per-tick scan; the sends fan out on the background pool, and stamped apps /// drop out of the candidate set so subsequent ticks drain any overflow. pub const SYNCKIT_WARNINGS_PER_TICK: i64 = 200; pub const SYNCKIT_ROTATION_BATCH_MAX: usize = 500; /// How long an unacknowledged alert waits before it is sent again. pub const ACKNOWLEDGEMENT_REPEAT_DAYS: i64 = 7; /// Unanswered sends before the alert is handed to a person and sending stops. /// Four weekly messages is a month of being ignored, which is long enough to /// conclude that more of the same will not work. pub const ACKNOWLEDGEMENT_ESCALATE_AFTER: i32 = 4; /// Max due alerts one scheduler tick fetches. The sends fan out on the /// background pool and stamped rows drop out of the due set, so any overflow /// drains on the following ticks. pub const ACKNOWLEDGEMENTS_PER_TICK: i64 = 100; // 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 /// Lifetime of an OAuth userinfo-scoped access token. Short by design: it is /// used transiently at callback / refresh for one userinfo fetch and never /// persisted by a well-behaved RP, so it never needs to outlive a request. pub const OAUTH_ACCESS_TOKEN_EXPIRY_SECS: i64 = 300; // 5 minutes /// Lifetime of a rotating OAuth refresh token. The RP stores this (not the /// access token); each use rotates it. Long enough that perk-refresh keeps /// working across the RP's own session window without forcing re-login. pub const OAUTH_REFRESH_TOKEN_EXPIRY_SECS: i64 = 30 * 24 * 3600; // 30 days pub const OAUTH_REFRESH_TOKEN_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. // // These are the values that ship, unconditionally. They used to be swapped by // `#[cfg(feature = "fast-tests")]`, which meant the limiter under test was never // the limiter in production: the sweep runs `cargo test --all-features`, so CI // only ever exercised the relaxed one, while a plain `cargo test` skipped the // tests that need relaxed values. Neither configuration covered both. The // profile is chosen at runtime now, see [`RateLimits`]. pub const AUTH_RATE_LIMIT_MS: u64 = 500; pub const AUTH_RATE_LIMIT_BURST: u32 = 5; // 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; // Markdown preview: burst 10, then 2/sec. A preview is a button press rather // than a keystroke, so it sits above the validation limiter and well below the // write one; the work is one `render_permissive` over a bounded body and // nothing is stored. pub const PREVIEW_RATE_LIMIT_PER_SEC: u64 = 2; pub const PREVIEW_RATE_LIMIT_BURST: u32 = 10; // 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; // Guest download (public, no auth, token-gated): deliberately lenient, a buyer // may pull several files in a row, but still a per-IP ceiling so the endpoint // can't be hammered anonymously. Burst 60, then 2/sec. pub const GUEST_DOWNLOAD_RATE_LIMIT_PER_SEC: u64 = 2; pub const GUEST_DOWNLOAD_RATE_LIMIT_BURST: u32 = 60; // CSP violation reports (public, unauthenticated, browser-posted): burst 20, // then 1/sec. A page that violates the policy on every load would otherwise let // any visitor's browser flood the log for free. pub const CSP_REPORT_RATE_LIMIT_PER_SEC: u64 = 1; pub const CSP_REPORT_RATE_LIMIT_BURST: u32 = 20; // A CSP report is a few hundred bytes; the cap exists so the endpoint cannot be // used to push a megabyte of anything at the log. pub const CSP_REPORT_BODY_LIMIT_BYTES: usize = 16 * 1024; // 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 // Ceiling on in-flight scans, enforced by a semaphore around the CPU/clamd // phase. NOTE: with `SCAN_WORKER_COUNT` workers each scanning one file at a // time, the real concurrency is `min(SCAN_MAX_CONCURRENT, SCAN_WORKER_COUNT)`, // today that's 2 (~200 MB peak), so this semaphore only begins to bind if the // worker count is raised above it. Kept as an explicit ceiling so that raising // `SCAN_WORKER_COUNT` can't silently blow past the memory budget. The // assertion below documents that intent. pub const SCAN_MAX_CONCURRENT: usize = 4; // Memory-budget ceiling on concurrent scans pub const SCAN_WORKER_COUNT: usize = 2; // Background worker tasks draining scan_jobs queue /// Wall-clock ceiling on the CPU-bound scan layers (content-type, structural, /// archive decompress, yara, sha256) as a whole. The per-layer deadlines (yara /// 30s, clamav and urlhaus their own) do not cover the archive decompress walk, /// which is byte-bounded (`SCAN_ZIP_MAX_UNCOMPRESSED`) but not time-bounded, so /// a slow-codec archive under the ratio caps can pin one of the two scan workers /// for its full decompress wall-time. /// On elapse the scan fails closed (held for review) and the worker is freed; /// the orphaned blocking thread runs to completion on the (large) blocking pool. /// Generous enough for a legitimately large object, tight enough to bound the /// two-worker pool against monopolization. pub const SCAN_CPU_LAYERS_TIMEOUT_SECS: u64 = 120; /// 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=makenotwork/scan-spool` so the path /// resolves to `/var/lib/makenotwork/scan-spool`. Override with /// `MNW_SCAN_SPOOL_DIR` for dev. pub const SCAN_SPOOL_DIR: &str = "/var/lib/makenotwork/scan-spool"; /// Marker file the server touches to ask for an `authorized_keys` rebuild. /// /// The rebuild has to run as root: sshd's `StrictModes` refuses an /// `authorized_keys` whose file or `.ssh` directory is group-writable, so the /// service user cannot be given write access to it, and `sudo` cannot raise /// privilege from inside the unit (`NoNewPrivileges` is implied by /// `PrivateDevices`, `MemoryDenyWriteExecute`, `RestrictNamespaces` and the /// rest of the sandbox, and setting it back to `no` does not undo the /// implication). So the service writes here and a systemd `.path` unit runs /// `mnw-admin rebuild-keys` out of process. Both names are written by /// `sando/deploy/bootstrap-node.sh`; changing one means changing the other. /// Override with `MNW_KEYS_REBUILD_MARKER` for dev. pub const KEYS_REBUILD_MARKER: &str = "/var/lib/makenotwork/keys/rebuild"; /// 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. It sits below the 20 GB the /// upload tiers allow, deliberately: objects past this ceiling are held for /// manual admin review (`scanning::worker`) instead of being auto-scanned, /// which is fail-closed and cheap at alpha volume. Raising it to cover the full /// tier would cost spool headroom and add scan latency on every large file. 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; /// Slack added to a scan job's claimed object size when bounding how many bytes /// the spool writer will accept. The S3 object size is authoritative, but a /// small margin absorbs benign content-length/multipart rounding without /// letting an under-reported object stream the full `SCAN_SPOOL_MAX_BYTES` to /// scratch before the writer aborts. pub const SCAN_SPOOL_SLACK_BYTES: u64 = 16 * 1024 * 1024; // 16 MiB /// Maximum number of bytes fed to YARA in a single scan. yara-x's `Scanner` /// walks the whole slice, which demand-pages the entire mmap resident, so an /// 8 GiB object would otherwise pin 8 GiB of page cache per scan (× /// `SCAN_MAX_CONCURRENT`). Malware signatures cluster near a file's start, so /// scanning a generous prefix is the right trade. Above this, YARA sees the /// prefix and logs the cap. /// /// Do NOT raise this on the assumption that ClamAV covers the tail. ClamAV is a /// full-file backstop only up to the operator-declared /// [`crate::config::ScanConfig::clamav_max_scan_bytes`], because clamd does not /// expose its own limits over the socket. Undeclared coverage is fail-closed: /// `yara_tail_unscanned` holds anything past this prefix for review rather than /// certifying it Clean. pub const SCAN_YARA_MAX_BYTES: usize = 512 * 1024 * 1024; // 512 MiB /// Per-call deadline for the optional external second-opinion lookups /// (MalwareBazaar, MetaDefender). They are FailOpen by design; bounding each /// await keeps a slow or unreachable third party from holding a scan worker slot /// indefinitely. On timeout the layer reports /// Skip, the same shape as the disabled case, never blocking the file. pub const SCAN_EXTERNAL_LOOKUP_TIMEOUT_SECS: u64 = 10; // 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 // Cap the number of ZIP entries inspected. Depth/ratio/uncompressed-size are // already bounded, but a ZIP with millions of tiny entries forces a full // per-entry decompression pass bounded only by the 2 GB total. 100k entries is // far past any legitimate sample pack / content bundle; beyond it, fail closed. pub const SCAN_ZIP_MAX_ENTRIES: usize = 100_000; pub const SCAN_MALWAREBAZAAR_TIMEOUT_SECS: u64 = 5; /// TCP connect timeout for the external-lookup HTTP clients (MalwareBazaar, /// MetaDefender, URLhaus). Bounds the time spent establishing a connection to a /// hung/blackholed host so a stalled connect can't pin a scan worker (Perf-S2). pub const SCAN_HTTP_CONNECT_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 /// Repository names ending in this suffix, and the bare stem itself, are /// makenot.work's to create. Reserving a suffix rather than a word keeps a /// creator from ever losing a name they would plausibly have chosen. pub const RESERVED_REPO_SUFFIX: &str = ".mnw"; /// The one repository per account that holds personal annotations. pub const ANNOTATION_REPO_NAME: &str = "annotations.mnw"; pub const GIT_COMMITS_PER_PAGE: usize = 30; /// Annotations in the per-repository notes feed. A feed is a recent-news /// surface rather than an archive; the tab pages through the rest. pub const GIT_NOTES_FEED_ITEMS: i64 = 50; /// Annotations per page on a reader's own annotations listing. pub const GIT_ANNOTATIONS_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 // Max concurrent git smart-HTTP clone/fetch responses. Each runs a `git // upload-pack` child and streams a packfile; this bounds the process fan-out so // a burst of clones on a large repo can't exhaust processes/memory. pub const GIT_SMART_HTTP_MAX_CONCURRENT: usize = 8; // Hard ceiling on how long a single clone's `git upload-pack` child may run // while holding its concurrency permit. A client that stops reading makes git // block on a full stdout pipe and never exit, pinning the permit; killing it // past this deadline keeps slow/stuck clones from starving the budget. Generous // enough for a large repo over a slow link. pub const GIT_SMART_HTTP_TIMEOUT_SECS: u64 = 300; // Max size of a single git push (receive-pack) request body over HTTP. The pack // is streamed into git's stdin (not buffered), but this caps a single push so a // runaway upload can't fill the repo disk unbounded. pub const GIT_RECEIVE_PACK_MAX_BYTES: usize = 2 * 1024 * 1024 * 1024; // `receive.maxInputSize` written into every bare repo's config at creation. This // is the SSH-side equivalent of `GIT_RECEIVE_PACK_MAX_BYTES` (which only bounds // the HTTP smart path): git-receive-pack aborts a push whose pack exceeds this, // so an authenticated user can't stream an arbitrarily large pack over SSH. pub const GIT_SSH_MAX_PACK_BYTES: i64 = 2 * 1024 * 1024 * 1024; // Hard runaway-backstop on a single SSH git operation (clone/fetch/push). Unlike // the HTTP path (a per-request layer), the SSH path execs git-shell directly, so // a client that stalls mid-transfer would otherwise pin the process indefinitely. // Generous enough for a large repo over a slow link; only kills genuinely stuck // operations. pub const GIT_SSH_OP_TIMEOUT_SECS: u64 = 900; // 15 min // Per-user on-disk git storage ceiling, checked before an SSH push is allowed. // Coarse (summed at authorization time, not atomic), but combined with the // per-push `GIT_SSH_MAX_PACK_BYTES` it bounds total repo growth per account. pub const GIT_USER_DISK_QUOTA_BYTES: u64 = 20 * 1024 * 1024 * 1024; // 20 GiB backstop // 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; // Alloy hotfix RPM repo publishing (routes::rpm). /// Presign lifetime for an RPM/repodata PUT. Matches the OTA artifact window: /// the object is uploaded immediately after the mint, and a short window bounds /// what a leaked URL is worth. pub const RPM_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour /// Largest object the RPM publish endpoint will sign. A single unresumable PUT, /// so it stays modest; the biggest thing the repo carries is one package, and a /// package near this size is a packaging mistake rather than a hotfix. pub const RPM_MAX_OBJECT_BYTES: i64 = 2 * 1024 * 1024 * 1024; // 2 GB /// Longest object path the endpoint will accept, counted in bytes over the /// whole key. Well under S3's 1024-byte key limit and far past any real /// `repodata/-primary.xml.zst`. pub const RPM_MAX_KEY_BYTES: usize = 255; /// Most path segments an RPM object key may carry: `alloy/f43/x86_64/repodata/repomd.xml` /// is five. pub const RPM_MAX_KEY_SEGMENTS: usize = 8; // RPM publish: burst 20, then 4/sec. A repodata push is several objects back to // back, so the burst is wider than OTA's while the steady rate stays low. pub const RPM_WRITE_RATE_LIMIT_MS: u64 = 250; pub const RPM_WRITE_RATE_LIMIT_BURST: u32 = 20; // 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; // SyncKit group invitations. An unredeemed invite link is a standing credential, // so it always expires; these bound how long an admin may leave one open. /// Default life of an invite link when the caller names none. pub const SYNCKIT_INVITE_DEFAULT_HOURS: i64 = 168; // 7 days /// Longest an admin may leave an invite link redeemable. pub const SYNCKIT_INVITE_MAX_HOURS: i64 = 720; // 30 days /// Shortest usable life. Below this the link expires before it can be delivered. pub const SYNCKIT_INVITE_MIN_HOURS: i64 = 1; /// Ceiling on a single price, in the creator's settlement currency. Read it /// through [`crate::currency::SettlementCurrency::max_price_cents`], which is /// what price writers call; this is where the number lives. 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, 1 per 30 seconds, burst 2. The value that /// ships; see [`RateLimits`] for how tests relax it. pub const SANDBOX_RATE_LIMIT_MS: u64 = 30_000; pub const SANDBOX_RATE_LIMIT_BURST: u32 = 2; /// Max concurrent active sandboxes per IP. pub const SANDBOX_MAX_PER_IP: i64 = 3; /// Which rate-limit values a router is built with. /// /// Carried on [`crate::config::Config`] and passed to the route builders, so /// the choice is made once where the app is assembled rather than by a compile /// feature. Two consequences worth the plumbing: /// /// - The production limiter is the default and is what every build ships. There /// is no feature flag that can quietly relax it. /// - A single test run can exercise both profiles. The suite builds relaxed /// routers so unrelated tests are not throttled, and the rate-limit tests /// build a production router and assert the real thresholds. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RateLimits { /// Auth endpoints (login, join, email actions, public pages). pub auth_ms: u64, pub auth_burst: u32, /// Sandbox account creation. pub sandbox_ms: u64, pub sandbox_burst: u32, } impl RateLimits { /// The values that ship. Always the default. pub const fn production() -> Self { Self { auth_ms: AUTH_RATE_LIMIT_MS, auth_burst: AUTH_RATE_LIMIT_BURST, sandbox_ms: SANDBOX_RATE_LIMIT_MS, sandbox_burst: SANDBOX_RATE_LIMIT_BURST, } } /// Relaxed values for tests that are not about rate limiting and would /// otherwise be throttled by their own setup traffic. A lockout test needs /// to fire more than five auth attempts; a fixture needs more than one /// sandbox per 30 seconds. pub const fn relaxed() -> Self { Self { auth_ms: 10, auth_burst: 20, sandbox_ms: 10, sandbox_burst: 10, } } } impl Default for RateLimits { fn default() -> Self { Self::production() } } // ── 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); const _: () = assert!(MAX_SESSIONS_PER_USER > 0); // Login security const _: () = assert!(MAX_LOGIN_ATTEMPTS > 0); const _: () = assert!(LOCKOUT_MINUTES > 0); // OAuth token lifetimes: access tokens are transient, refresh tokens long-lived. const _: () = assert!(OAUTH_ACCESS_TOKEN_EXPIRY_SECS > 0); const _: () = assert!(OAUTH_ACCESS_TOKEN_EXPIRY_SECS < SYNCKIT_JWT_EXPIRY_SECS); const _: () = assert!(OAUTH_REFRESH_TOKEN_EXPIRY_SECS > OAUTH_ACCESS_TOKEN_EXPIRY_SECS); const _: () = assert!(OAUTH_REFRESH_TOKEN_LENGTH >= 32); // 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!(PREVIEW_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); const _: () = assert!(GUEST_CHECKOUT_RATE_LIMIT_BURST > 0); const _: () = assert!(GUEST_DOWNLOAD_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 // A browser upload must always be promotable by a single server-side copy, so // the browser ceiling can never be raised past what `CopyObject` accepts. const _: () = assert!(BROWSER_UPLOAD_MAX_BYTES <= S3_SINGLE_COPY_MAX_BYTES); // The concurrency ceiling must not sit below the worker count, or the memory // budget it's meant to enforce is unenforceable (workers would exceed it). const _: () = assert!(SCAN_MAX_CONCURRENT >= SCAN_WORKER_COUNT); const _: () = assert!(SCAN_ZIP_MAX_ENTRIES > 0); 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); // Multipart exists to exceed the one-shot ceiling, and nothing above the // storage allowance is storable. const _: () = assert!(SYNCKIT_MAX_MULTIPART_BLOB_SIZE_BYTES > SYNCKIT_MAX_BLOB_SIZE_BYTES); const _: () = assert!(SYNCKIT_MAX_MULTIPART_BLOB_SIZE_BYTES <= SYNCKIT_MAX_BLOB_STORAGE_BYTES); 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}" ); } } }