Skip to main content

max / makenotwork

35.1 KB · 703 lines History Blame Raw
1 //! Centralized application constants
2 //!
3 //! Magic numbers that were scattered across modules. Storage-specific limits
4 //! (file sizes, presign expiry, allowed types) remain in storage.rs since
5 //! they're only used there.
6
7 // Database
8 pub const DB_POOL_MAX_CONNECTIONS: u32 = 25;
9 pub const DB_POOL_MIN_CONNECTIONS: u32 = 2;
10 pub const DB_ACQUIRE_TIMEOUT_SECS: u64 = 3;
11 /// Rotate connections after 30 minutes to prevent stale sessions.
12 pub const DB_MAX_LIFETIME_SECS: u64 = 1800;
13 /// Prune idle connections after 10 minutes.
14 pub const DB_IDLE_TIMEOUT_SECS: u64 = 600;
15 /// Per-statement wall-clock ceiling, applied to every pooled connection via
16 /// `SET statement_timeout`. `acquire_timeout` only bounds *getting* a
17 /// connection, not *running* a query, without this a single wedged query pins
18 /// its connection forever and 25 of them exhaust the pool with no recovery.
19 /// Generous so legitimate maintenance sweeps and boot migrations aren't killed;
20 /// a job that genuinely needs longer sets `SET LOCAL statement_timeout = 0` in
21 /// its own transaction.
22 pub const DB_STATEMENT_TIMEOUT_SECS: u64 = 120;
23 /// Ceiling on how long a statement waits to acquire a lock (`SET lock_timeout`).
24 /// A lock-contended query fails fast instead of blocking a connection
25 /// indefinitely, the most common way the pool wedges.
26 pub const DB_LOCK_TIMEOUT_SECS: u64 = 30;
27
28 /// Largest file the browser upload path will accept. The browser issues exactly
29 /// one presigned `PutObject` and a tab cannot resume it, so a transfer that
30 /// drops at 90% starts over from zero. That is the binding constraint, not the
31 /// protocol: S3 / Ceph (Hetzner Object Storage) tolerate a single PUT up to
32 /// 5 GiB, but 2 GiB is as much as we are willing to ask someone to re-send.
33 /// Files above it upload through the CLI / desktop clients, which chunk and
34 /// resume, and the (higher) per-tier `max_file_bytes` still governs there, so
35 /// the advertised 20 GB tier is unaffected. Refusing up front with a pointer to
36 /// those clients beats handing out a presigned URL for a doomed transfer.
37 pub const BROWSER_UPLOAD_MAX_BYTES: u64 = 2 * 1024 * 1024 * 1024;
38
39 /// Largest source a single server-side `CopyObject` can promote. S3 rejects a
40 /// one-shot copy above 5 GiB; a larger source must go through ranged multipart
41 /// `UploadPartCopy`. This is a protocol limit, where
42 /// [`BROWSER_UPLOAD_MAX_BYTES`] is a product decision about resumability, so the
43 /// two are separate constants and neither should be reused for the other.
44 /// Without this branch a large upload succeeds and then fails at promote, the
45 /// worst position to fail in.
46 pub const S3_SINGLE_COPY_MAX_BYTES: u64 = 5 * 1024 * 1024 * 1024;
47
48 /// Lifetime of an internal-API actor assertion, minted at `ssh-key-lookup` and
49 /// forwarded by the CLI for the session. 24h comfortably exceeds any SSH session.
50 pub const INTERNAL_ACTOR_TTL_SECS: i64 = 86_400;
51
52 // Sessions
53 pub const SESSION_EXPIRY_DAYS: i64 = 7;
54 /// Skip DB touch if validated within this window. Doubles as the upper bound on
55 /// session-revocation lag (admin suspend, logout-everywhere, password change),
56 /// shorter = tighter revocation, slightly more DB load on the auth hot path.
57 pub const SESSION_TOUCH_CACHE_SECS: u64 = 5;
58 /// Cap on tracked sessions per user. Each login mints a `user_sessions` row;
59 /// without a bound, repeated logins (or an automated loop) grow the table
60 /// unboundedly. After a new session is recorded, the oldest beyond this many are
61 /// pruned (a sliding window), the current session is always the newest, so it
62 /// is never evicted. Matches the 100-row session-listing cap, so anything pruned
63 /// was already invisible bloat, never a session a user could see or manage.
64 pub const MAX_SESSIONS_PER_USER: i64 = 100;
65
66 // Login security
67 pub const MAX_LOGIN_ATTEMPTS: i32 = 5;
68 pub const LOCKOUT_MINUTES: i64 = 15;
69 /// How long a half-completed login (password verified, awaiting 2FA) stays
70 /// valid before the user must re-enter their password. Defends against the
71 /// "unattended browser one TOTP from logged in" failure mode.
72 pub const PENDING_2FA_TTL_SECS: i64 = 600;
73
74 // Email link expiry (seconds)
75 pub const PASSWORD_RESET_EXPIRY_SECS: i64 = 900; // 15 minutes
76 pub const EMAIL_VERIFICATION_EXPIRY_SECS: i64 = 86400; // 24 hours
77 pub const ACCOUNT_DELETION_EXPIRY_SECS: i64 = 3600; // 1 hour
78
79 // Stripe fees (for display only; actual fees set by Stripe)
80 pub const STRIPE_FEE_PERCENTAGE: f64 = 0.029; // 2.9%
81 pub const STRIPE_FEE_FIXED_CENTS: f64 = 30.0; // $0.30
82 // Stripe's minimum charge is per settlement currency (GBP is 30, the rest 50),
83 // so it lives on SettlementCurrency::minimum_charge_cents rather than here.
84
85 // Page / query limits
86 pub const DASHBOARD_TRANSACTION_LIMIT: i64 = 100;
87
88 // SyncKit
89 pub const SYNCKIT_JWT_EXPIRY_SECS: i64 = 7 * 24 * 3600; // 7 days
90 pub const SYNCKIT_PUSH_MAX_CHANGES: usize = 500;
91 pub const SYNCKIT_PULL_PAGE_SIZE: i64 = 500;
92 pub const SYNCKIT_API_KEY_LENGTH: usize = 32; // 32 bytes = 64 hex chars
93 pub const SYNC_LOG_RETAIN_DAYS: i64 = 90;
94 pub const SYNC_LOG_COMPACT_MIN_AGE_DAYS: i64 = 7; // Safety margin for cursor-based compaction
95 /// Largest blob the one-shot presigned PUT will sign. A single PUT holds the
96 /// whole object in one request the client cannot resume, so this stays modest;
97 /// bigger blobs go through the multipart session below.
98 pub const SYNCKIT_MAX_BLOB_SIZE_BYTES: i64 = 500 * 1024 * 1024; // 500 MB
99 /// Largest blob the multipart session will accept. Set to the per-user-per-app
100 /// storage allowance: a blob above it could never be stored anyway, so the
101 /// session refuses it before it stages S3 parts that confirm would only reject.
102 pub const SYNCKIT_MAX_MULTIPART_BLOB_SIZE_BYTES: i64 = SYNCKIT_MAX_BLOB_STORAGE_BYTES;
103 pub const SYNCKIT_MAX_BLOB_STORAGE_BYTES: i64 = 10 * 1024 * 1024 * 1024; // 10 GB per user per app
104 pub const SYNCKIT_MAX_DEVICES_PER_APP: i64 = 50; // Max devices per user per app
105 pub const SYNCKIT_BLOB_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour
106 pub const SYNCKIT_MAX_SSE_CONNECTIONS_PER_USER: usize = 10;
107 pub const SYNCKIT_ROTATION_STALE_HOURS: i64 = 24;
108 /// Max usage-warning candidates a single scheduler tick fetches. Bounds the
109 /// per-tick scan; the sends fan out on the background pool, and stamped apps
110 /// drop out of the candidate set so subsequent ticks drain any overflow.
111 pub const SYNCKIT_WARNINGS_PER_TICK: i64 = 200;
112 pub const SYNCKIT_ROTATION_BATCH_MAX: usize = 500;
113
114 /// How long an unacknowledged alert waits before it is sent again.
115 pub const ACKNOWLEDGEMENT_REPEAT_DAYS: i64 = 7;
116 /// Unanswered sends before the alert is handed to a person and sending stops.
117 /// Four weekly messages is a month of being ignored, which is long enough to
118 /// conclude that more of the same will not work.
119 pub const ACKNOWLEDGEMENT_ESCALATE_AFTER: i32 = 4;
120 /// Max due alerts one scheduler tick fetches. The sends fan out on the
121 /// background pool and stamped rows drop out of the due set, so any overflow
122 /// drains on the following ticks.
123 pub const ACKNOWLEDGEMENTS_PER_TICK: i64 = 100;
124
125 // Subscriptions
126 pub const MIN_SUBSCRIPTION_PRICE_CENTS: i32 = 100; // $1.00 minimum
127
128 // OAuth
129 pub const OAUTH_CODE_EXPIRY_SECS: i64 = 600; // 10 minutes
130 pub const OAUTH_CODE_LENGTH: usize = 32; // 32 bytes = 64 hex chars
131 /// Lifetime of an OAuth userinfo-scoped access token. Short by design: it is
132 /// used transiently at callback / refresh for one userinfo fetch and never
133 /// persisted by a well-behaved RP, so it never needs to outlive a request.
134 pub const OAUTH_ACCESS_TOKEN_EXPIRY_SECS: i64 = 300; // 5 minutes
135 /// Lifetime of a rotating OAuth refresh token. The RP stores this (not the
136 /// access token); each use rotates it. Long enough that perk-refresh keeps
137 /// working across the RP's own session window without forcing re-login.
138 pub const OAUTH_REFRESH_TOKEN_EXPIRY_SECS: i64 = 30 * 24 * 3600; // 30 days
139 pub const OAUTH_REFRESH_TOKEN_LENGTH: usize = 32; // 32 bytes = 64 hex chars
140
141 // Health monitoring
142 pub const HEALTH_CHECK_INTERVAL_SECS: u64 = 60;
143 pub const ALERT_COOLDOWN_SECS: u64 = 300; // 5 minutes
144 pub const HEALTH_HISTORY_RETAIN_DAYS: i64 = 90;
145
146 // Scheduled publish
147 pub const SCHEDULER_INTERVAL_SECS: u64 = 60;
148
149 // How often the rate-limiter bucket-map sweeper reclaims stale GCRA entries.
150 // Bounds limiter map size by active (not cumulative-unique) client keys.
151 pub const GOVERNOR_SWEEP_INTERVAL_SECS: u64 = 60;
152
153 // TOTP / 2FA
154 pub const TOTP_SKEW: u8 = 1; // Allow +/-1 time step (+/-30s)
155 pub const TOTP_STEP: u64 = 30; // 30-second windows
156 pub const TOTP_DIGITS: usize = 6; // 6-digit codes
157 pub const BACKUP_CODE_COUNT: usize = 10; // Generate 10 codes
158 pub const BACKUP_CODE_LENGTH: usize = 8; // 8 alphanumeric chars
159
160 // Anti-enumeration
161 pub const USERNAME_CHECK_DELAY_MS: u64 = 400;
162
163 // Rate limiting
164 // Auth endpoints (login, join): burst 5, then 2/sec.
165 //
166 // These are the values that ship, unconditionally. They used to be swapped by
167 // `#[cfg(feature = "fast-tests")]`, which meant the limiter under test was never
168 // the limiter in production: the sweep runs `cargo test --all-features`, so CI
169 // only ever exercised the relaxed one, while a plain `cargo test` skipped the
170 // tests that need relaxed values. Neither configuration covered both. The
171 // profile is chosen at runtime now, see [`RateLimits`].
172 pub const AUTH_RATE_LIMIT_MS: u64 = 500;
173 pub const AUTH_RATE_LIMIT_BURST: u32 = 5;
174 // Username validation: burst 10, then 1/sec
175 pub const VALIDATE_RATE_LIMIT_PER_SEC: u64 = 1;
176 pub const VALIDATE_RATE_LIMIT_BURST: u32 = 10;
177 // API write endpoints (CRUD): burst 30, then 2/sec
178 pub const API_WRITE_RATE_LIMIT_MS: u64 = 500;
179 pub const API_WRITE_RATE_LIMIT_BURST: u32 = 30;
180 // API read endpoints (GET): burst 60, then 10/sec (prevents enumeration)
181 pub const API_READ_RATE_LIMIT_MS: u64 = 100;
182 pub const API_READ_RATE_LIMIT_BURST: u32 = 60;
183 // Markdown preview: burst 10, then 2/sec. A preview is a button press rather
184 // than a keystroke, so it sits above the validation limiter and well below the
185 // write one; the work is one `render_permissive` over a bounded body and
186 // nothing is stored.
187 pub const PREVIEW_RATE_LIMIT_PER_SEC: u64 = 2;
188 pub const PREVIEW_RATE_LIMIT_BURST: u32 = 10;
189 // API export endpoints: burst 3, then 1/sec
190 pub const API_EXPORT_RATE_LIMIT_PER_SEC: u64 = 1;
191 pub const API_EXPORT_RATE_LIMIT_BURST: u32 = 3;
192 // Guest checkout (public, no auth): burst 10, then 1/sec
193 pub const GUEST_CHECKOUT_RATE_LIMIT_PER_SEC: u64 = 1;
194 pub const GUEST_CHECKOUT_RATE_LIMIT_BURST: u32 = 10;
195 // Guest download (public, no auth, token-gated): deliberately lenient, a buyer
196 // may pull several files in a row, but still a per-IP ceiling so the endpoint
197 // can't be hammered anonymously. Burst 60, then 2/sec.
198 pub const GUEST_DOWNLOAD_RATE_LIMIT_PER_SEC: u64 = 2;
199 pub const GUEST_DOWNLOAD_RATE_LIMIT_BURST: u32 = 60;
200 // CSP violation reports (public, unauthenticated, browser-posted): burst 20,
201 // then 1/sec. A page that violates the policy on every load would otherwise let
202 // any visitor's browser flood the log for free.
203 pub const CSP_REPORT_RATE_LIMIT_PER_SEC: u64 = 1;
204 pub const CSP_REPORT_RATE_LIMIT_BURST: u32 = 20;
205 // A CSP report is a few hundred bytes; the cap exists so the endpoint cannot be
206 // used to push a megabyte of anything at the log.
207 pub const CSP_REPORT_BODY_LIMIT_BYTES: usize = 16 * 1024;
208 // License key validation (public): burst 20, then 5/sec
209 pub const LICENSE_KEY_RATE_LIMIT_MS: u64 = 200;
210 pub const LICENSE_KEY_RATE_LIMIT_BURST: u32 = 20;
211 // File upload: burst 10, then 2/sec
212 pub const UPLOAD_RATE_LIMIT_MS: u64 = 500;
213 pub const UPLOAD_RATE_LIMIT_BURST: u32 = 10;
214 // OAuth authorize/token: burst 5/10, then 2/sec
215 pub const OAUTH_RATE_LIMIT_MS: u64 = 500;
216 pub const OAUTH_RATE_LIMIT_BURST: u32 = 5;
217 pub const OAUTH_TOKEN_RATE_LIMIT_MS: u64 = 500;
218 pub const OAUTH_TOKEN_RATE_LIMIT_BURST: u32 = 10;
219 // SyncKit auth: burst 5, then 1/sec
220 pub const SYNCKIT_AUTH_RATE_LIMIT_PER_SEC: u64 = 1;
221 pub const SYNCKIT_AUTH_RATE_LIMIT_BURST: u32 = 5;
222 // SyncKit sync (push/pull), per-IP: burst 30, then 10/sec
223 pub const SYNCKIT_SYNC_RATE_LIMIT_MS: u64 = 100;
224 pub const SYNCKIT_SYNC_RATE_LIMIT_BURST: u32 = 30;
225 // SyncKit sync, per-app: burst 60, then 20/sec (higher than per-IP because
226 // a single app may have many users behind different IPs)
227 pub const SYNCKIT_APP_RATE_LIMIT_MS: u64 = 50;
228 pub const SYNCKIT_APP_RATE_LIMIT_BURST: u32 = 60;
229 // 2FA verification: burst 5, then 2/sec (same as auth)
230 pub const TWO_FACTOR_RATE_LIMIT_MS: u64 = 500;
231 pub const TWO_FACTOR_RATE_LIMIT_BURST: u32 = 5;
232
233 // Dashboard tab reads: generous but bounded (5/sec, burst 20)
234 pub const DASHBOARD_READ_RATE_LIMIT_MS: u64 = 200;
235 pub const DASHBOARD_READ_RATE_LIMIT_BURST: u32 = 20;
236
237 // Pagination
238 pub const DISCOVER_PAGE_SIZE: u32 = 25;
239 pub const FEED_PAGE_SIZE: u32 = 25;
240 pub const PAGINATION_WINDOW_SIZE: u32 = 5;
241
242 // Creator broadcast fan-out
243 /// Max concurrent in-flight email sends per broadcast. The outer worker
244 /// task spawns up to this many child tasks, then waits on one to drain
245 /// before spawning the next.
246 pub const BROADCAST_PARALLELISM: usize = 16;
247 /// Delay between successive broadcast send-task spawns. Spreads Postmark
248 /// API load when a creator with thousands of followers fires a broadcast:
249 /// at parallelism 16 + 100 ms cadence, steady-state is ~10 sends/sec.
250 pub const BROADCAST_CHUNK_DELAY_MS: u64 = 100;
251 /// Recipient cap per broadcast send. Above this, the request is refused
252 /// with an instruction to contact support. Bounds Postmark spend exposure
253 /// from any single approved creator. Founder-window cohort is well under
254 /// this; the cap is the floor we'd lift on request, not the ceiling.
255 pub const BROADCAST_MAX_RECIPIENTS: usize = 10_000;
256
257 /// Cap on buyer-departure notification fan-out per creator-deletion event.
258 /// Account deletion notifies historical buyers about content removal. A
259 /// creator with millions of completed sales should not turn one deletion
260 /// into a Postmark bomb. The cap bounds both the in-memory buyer list and
261 /// total outbound email volume; if hit, we log a warning and notify the
262 /// oldest-buyers slice the SQL chose.
263 pub const BUYER_DEPARTURE_MAX_NOTIFICATIONS: i64 = 50_000;
264
265 // File scanning
266 pub const SCAN_MAX_MEMORY_BYTES: usize = 100 * 1024 * 1024; // 100 MB in-memory threshold
267 // Ceiling on in-flight scans, enforced by a semaphore around the CPU/clamd
268 // phase. NOTE: with `SCAN_WORKER_COUNT` workers each scanning one file at a
269 // time, the real concurrency is `min(SCAN_MAX_CONCURRENT, SCAN_WORKER_COUNT)`,
270 // today that's 2 (~200 MB peak), so this semaphore only begins to bind if the
271 // worker count is raised above it. Kept as an explicit ceiling so that raising
272 // `SCAN_WORKER_COUNT` can't silently blow past the memory budget. The
273 // assertion below documents that intent.
274 pub const SCAN_MAX_CONCURRENT: usize = 4; // Memory-budget ceiling on concurrent scans
275 pub const SCAN_WORKER_COUNT: usize = 2; // Background worker tasks draining scan_jobs queue
276 /// Wall-clock ceiling on the CPU-bound scan layers (content-type, structural,
277 /// archive decompress, yara, sha256) as a whole. The per-layer deadlines that
278 /// existed, yara 30s, clamav/urlhaus their own, did not cover the archive
279 /// decompress walk, which was byte-bounded (`SCAN_ZIP_MAX_UNCOMPRESSED`) but not
280 /// time-bounded, so a slow-codec archive under the ratio caps could pin one of
281 /// the two scan workers for its full decompress wall-time (fuzz 2026-07-06 F1).
282 /// On elapse the scan fails closed (held for review) and the worker is freed;
283 /// the orphaned blocking thread runs to completion on the (large) blocking pool.
284 /// Generous enough for a legitimately large object, tight enough to bound the
285 /// two-worker pool against monopolization.
286 pub const SCAN_CPU_LAYERS_TIMEOUT_SECS: u64 = 120;
287 /// Retention window for terminal-state (`done`, `failed`) `scan_jobs` rows.
288 /// Queued/running rows are operational queue state and not affected.
289 pub const SCAN_JOB_RETENTION_DAYS: u32 = 30;
290 /// Directory under which the scanner spools large objects to tempfiles
291 /// before invoking path/stream-based layer entries. On production, systemd
292 /// provisions this via `StateDirectory=makenotwork/scan-spool` so the path
293 /// resolves to `/var/lib/makenotwork/scan-spool`. Override with
294 /// `MNW_SCAN_SPOOL_DIR` for dev.
295 pub const SCAN_SPOOL_DIR: &str = "/var/lib/makenotwork/scan-spool";
296 /// Files in `SCAN_SPOOL_DIR` older than this are considered orphaned
297 /// (a panic, OOM, or hard kill left them behind) and reaped on the
298 /// next sweep. RAII drop in `SpoolHandle` covers the live path; this
299 /// covers process-death.
300 pub const SCAN_SPOOL_ORPHAN_AGE_SECS: u64 = 3600;
301 /// Hard cap on a single spooled object. Above this, the scanner refuses
302 /// the job rather than risk filling the volume. It sits below the 20 GB the
303 /// upload tiers allow, deliberately: objects past this ceiling are held for
304 /// manual admin review (`scanning::worker`) instead of being auto-scanned,
305 /// which is fail-closed and cheap at alpha volume. Raising it to cover the full
306 /// tier would cost spool headroom and add scan latency on every large file.
307 pub const SCAN_SPOOL_MAX_BYTES: u64 = 8 * 1024 * 1024 * 1024;
308 /// Minimum free space the spool volume must retain after writing the
309 /// pending object. The scanner refuses if `statvfs(free) - expected_size`
310 /// would drop below this threshold.
311 pub const SCAN_SPOOL_FREE_RESERVE_BYTES: u64 = 2 * 1024 * 1024 * 1024;
312 /// Slack added to a scan job's claimed object size when bounding how many bytes
313 /// the spool writer will accept. The S3 object size is authoritative, but a
314 /// small margin absorbs benign content-length/multipart rounding without
315 /// letting an under-reported object stream the full `SCAN_SPOOL_MAX_BYTES` to
316 /// scratch before the writer aborts.
317 pub const SCAN_SPOOL_SLACK_BYTES: u64 = 16 * 1024 * 1024; // 16 MiB
318 /// Maximum number of bytes fed to YARA in a single scan. yara-x's `Scanner`
319 /// walks the whole slice, which demand-pages the entire mmap resident, so an
320 /// 8 GiB object would otherwise pin 8 GiB of page cache per scan (×
321 /// `SCAN_MAX_CONCURRENT`). Malware signatures cluster near a file's start, so
322 /// scanning a generous prefix is the right trade. Above this, YARA sees the
323 /// prefix and logs the cap.
324 ///
325 /// Do NOT raise this on the assumption that ClamAV covers the tail. ClamAV is a
326 /// full-file backstop only up to the operator-declared
327 /// [`crate::config::ScanConfig::clamav_max_scan_bytes`], because clamd does not
328 /// expose its own limits over the socket. Undeclared coverage is fail-closed:
329 /// `yara_tail_unscanned` holds anything past this prefix for review rather than
330 /// certifying it Clean (ultra-fuzz Run #24 Security MODERATE).
331 pub const SCAN_YARA_MAX_BYTES: usize = 512 * 1024 * 1024; // 512 MiB
332
333 /// Per-call deadline for the optional external second-opinion lookups
334 /// (MalwareBazaar, MetaDefender). They are FailOpen by design; bounding each
335 /// await keeps a slow or unreachable third party from holding a scan worker slot
336 /// indefinitely (ultra-fuzz Run 6 Performance). On timeout the layer reports
337 /// Skip, the same shape as the disabled case, never blocking the file.
338 pub const SCAN_EXTERNAL_LOOKUP_TIMEOUT_SECS: u64 = 10;
339
340 // Caps concurrent cache-miss DB lookups in `/api/domains/caddy-ask`. Cache hits
341 // are unbounded (DashMap). At capacity, the handler returns 503 so Caddy retries
342 // later instead of stampeding the DB pool or driving ACME issuance for garbage
343 // domains. Sized small because the slow path is one indexed lookup.
344 pub const CADDY_ASK_MAX_CONCURRENT: usize = 8;
345 pub const SCAN_ZIP_MAX_RATIO: f64 = 100.0; // Max compression ratio before ZIP bomb
346 pub const SCAN_ZIP_MAX_DEPTH: u32 = 2; // Max nested archives (detection is 1 level deep; decompressed size limit is the primary defense)
347 pub const SCAN_ZIP_MAX_UNCOMPRESSED: u64 = 2 * 1024 * 1024 * 1024; // 2 GB uncompressed limit
348 // Cap the number of ZIP entries inspected. Depth/ratio/uncompressed-size are
349 // already bounded, but a ZIP with millions of tiny entries forces a full
350 // per-entry decompression pass bounded only by the 2 GB total. 100k entries is
351 // far past any legitimate sample pack / content bundle; beyond it, fail closed.
352 pub const SCAN_ZIP_MAX_ENTRIES: usize = 100_000;
353 pub const SCAN_MALWAREBAZAAR_TIMEOUT_SECS: u64 = 5;
354 /// TCP connect timeout for the external-lookup HTTP clients (MalwareBazaar,
355 /// MetaDefender, URLhaus). Bounds the time spent establishing a connection to a
356 /// hung/blackholed host so a stalled connect can't pin a scan worker (Perf-S2).
357 pub const SCAN_HTTP_CONNECT_TIMEOUT_SECS: u64 = 5;
358 pub const SCAN_CLAMAV_TIMEOUT_SECS: u64 = 30;
359
360 // Invite system
361 pub const INVITES_ENABLED: bool = true;
362 pub const INVITE_LIMIT_PER_CREATOR: i64 = 5; // max unredeemed codes per creator
363
364 // Git source browser
365 pub const GIT_MAX_FILE_SIZE_BYTES: usize = 1_024_000; // 1MB display limit
366 pub const GIT_COMMITS_PER_PAGE: usize = 30;
367
368 /// Annotations in the per-repository notes feed. A feed is a recent-news
369 /// surface rather than an archive; the tab pages through the rest.
370 pub const GIT_NOTES_FEED_ITEMS: i64 = 50;
371 pub const GIT_DIFF_MAX_FILES: usize = 20; // Inline diff hunks for first N files
372 pub const GIT_DIFF_MAX_LINES: usize = 500; // Per-file line cap for diff display
373 pub const GIT_REPOS_PER_PAGE: usize = 30;
374 pub const GIT_FILE_LOG_MAX_WALK: usize = 1000; // Max commits to walk for per-file history
375 pub const GIT_RAW_MAX_BYTES: usize = 100 * 1024 * 1024; // 100 MB raw download limit
376 pub const GIT_UPLOAD_PACK_MAX_BYTES: usize = 10 * 1024 * 1024; // 10 MB upload-pack body limit
377 // Max concurrent git smart-HTTP clone/fetch responses. Each runs a `git
378 // upload-pack` child and streams a packfile; this bounds the process fan-out so
379 // a burst of clones on a large repo can't exhaust processes/memory.
380 pub const GIT_SMART_HTTP_MAX_CONCURRENT: usize = 8;
381 // Hard ceiling on how long a single clone's `git upload-pack` child may run
382 // while holding its concurrency permit. A client that stops reading makes git
383 // block on a full stdout pipe and never exit, pinning the permit; killing it
384 // past this deadline keeps slow/stuck clones from starving the budget. Generous
385 // enough for a large repo over a slow link.
386 pub const GIT_SMART_HTTP_TIMEOUT_SECS: u64 = 300;
387 // Max size of a single git push (receive-pack) request body over HTTP. The pack
388 // is streamed into git's stdin (not buffered), but this caps a single push so a
389 // runaway upload can't fill the repo disk unbounded.
390 pub const GIT_RECEIVE_PACK_MAX_BYTES: usize = 2 * 1024 * 1024 * 1024;
391 // `receive.maxInputSize` written into every bare repo's config at creation. This
392 // is the SSH-side equivalent of `GIT_RECEIVE_PACK_MAX_BYTES` (which only bounds
393 // the HTTP smart path): git-receive-pack aborts a push whose pack exceeds this,
394 // so an authenticated user can't stream an arbitrarily large pack over SSH.
395 pub const GIT_SSH_MAX_PACK_BYTES: i64 = 2 * 1024 * 1024 * 1024;
396 // Hard runaway-backstop on a single SSH git operation (clone/fetch/push). Unlike
397 // the HTTP path (a per-request layer), the SSH path execs git-shell directly, so
398 // a client that stalls mid-transfer would otherwise pin the process indefinitely.
399 // Generous enough for a large repo over a slow link; only kills genuinely stuck
400 // operations.
401 pub const GIT_SSH_OP_TIMEOUT_SECS: u64 = 900; // 15 min
402 // Per-user on-disk git storage ceiling, checked before an SSH push is allowed.
403 // Coarse (summed at authorization time, not atomic), but combined with the
404 // per-push `GIT_SSH_MAX_PACK_BYTES` it bounds total repo growth per account.
405 pub const GIT_USER_DISK_QUOTA_BYTES: u64 = 20 * 1024 * 1024 * 1024; // 20 GiB backstop
406
407 // Webhook security
408 pub const WEBHOOK_TIMESTAMP_TOLERANCE_SECS: u64 = 300; // 5 minutes
409
410 // Collections
411 pub const MAX_COLLECTIONS_PER_USER: i64 = 50;
412 pub const MAX_ITEMS_PER_COLLECTION: i64 = 200;
413
414 // OTA updates
415 pub const OTA_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour
416 // OTA management: burst 10, then 2/sec (same as API write)
417 pub const OTA_WRITE_RATE_LIMIT_MS: u64 = 500;
418 pub const OTA_WRITE_RATE_LIMIT_BURST: u32 = 10;
419 // OTA public (updater check, download): burst 30, then 10/sec
420 pub const OTA_READ_RATE_LIMIT_MS: u64 = 100;
421 pub const OTA_READ_RATE_LIMIT_BURST: u32 = 30;
422
423 // Build pipeline
424 pub const BUILD_TIMEOUT_SECS: u64 = 1800; // 30 min
425 pub const BUILD_MAX_LOG_BYTES: usize = 5_242_880; // 5 MB
426 pub const BUILD_HISTORY_LIMIT: i64 = 50;
427 pub const BUILD_TRIGGER_RATE_LIMIT_PER_SEC: u64 = 1;
428 pub const BUILD_TRIGGER_RATE_LIMIT_BURST: u32 = 3;
429 pub const BUILD_WRITE_RATE_LIMIT_MS: u64 = 500;
430 pub const BUILD_WRITE_RATE_LIMIT_BURST: u32 = 10;
431 // Git browsing: burst 30, then 5/sec (blame/log can be expensive)
432 pub const GIT_BROWSE_RATE_LIMIT_MS: u64 = 200;
433 pub const GIT_BROWSE_RATE_LIMIT_BURST: u32 = 30;
434 pub const BUILD_ALLOWED_TARGETS: &[&str] = &[
435 "linux/x86_64",
436 "linux/aarch64",
437 "darwin/x86_64",
438 "darwin/aarch64",
439 ];
440
441 // Streaming
442 pub const STREAMING_CACHE_MAX_SECS: u64 = 86400; // 24 hours max presigned URL lifetime
443 /// Rate limit for stream/download URL requests: 1 per 3 seconds, burst of 10.
444 pub const STREAM_RATE_LIMIT_MS: u64 = 3000;
445 pub const STREAM_RATE_LIMIT_BURST: u32 = 10;
446
447 // Date display formats
448 pub const DATE_FMT_SHORT: &str = "%b %d"; // "Mar 25"
449 pub const DATE_FMT_FULL: &str = "%b %d, %Y"; // "Mar 25, 2026"
450 pub const DATE_FMT_ISO: &str = "%Y-%m-%d"; // "2026-03-25"
451 pub const DATE_FMT_DATETIME: &str = "%b %d, %Y %H:%M"; // "Mar 25, 2026 14:30"
452 pub const DATE_FMT_DATETIME_UTC: &str = "%b %d, %Y %H:%M UTC"; // "Mar 25, 2026 14:30 UTC"
453
454 // Platform content
455 pub const CHANGELOG_PROJECT_SLUG: &str = "changelog";
456
457 // String / buffer limits
458 pub const USER_AGENT_MAX_LENGTH: usize = 512;
459 pub const SYNCKIT_MAX_KEY_ENVELOPE_BYTES: usize = 4096;
460
461 // SyncKit group invitations. An unredeemed invite link is a standing credential,
462 // so it always expires; these bound how long an admin may leave one open.
463 /// Default life of an invite link when the caller names none.
464 pub const SYNCKIT_INVITE_DEFAULT_HOURS: i64 = 168; // 7 days
465 /// Longest an admin may leave an invite link redeemable.
466 pub const SYNCKIT_INVITE_MAX_HOURS: i64 = 720; // 30 days
467 /// Shortest usable life. Below this the link expires before it can be delivered.
468 pub const SYNCKIT_INVITE_MIN_HOURS: i64 = 1;
469 /// Ceiling on a single price, in the creator's settlement currency. Read it
470 /// through [`crate::currency::SettlementCurrency::max_price_cents`], which is
471 /// what price writers call; this is where the number lives.
472 pub const MAX_PRICE_CENTS: i32 = 1_000_000; // 10,000
473
474 // Sandbox accounts
475 /// How long a sandbox session lasts before auto-cleanup.
476 pub const SANDBOX_EXPIRY_SECS: i64 = 3600; // 1 hour
477 /// How often the cleanup job runs.
478 pub const SANDBOX_CLEANUP_INTERVAL_SECS: u64 = 300; // 5 minutes
479 /// Rate limit: sandbox creation, 1 per 30 seconds, burst 2. The value that
480 /// ships; see [`RateLimits`] for how tests relax it.
481 pub const SANDBOX_RATE_LIMIT_MS: u64 = 30_000;
482 pub const SANDBOX_RATE_LIMIT_BURST: u32 = 2;
483 /// Max concurrent active sandboxes per IP.
484 pub const SANDBOX_MAX_PER_IP: i64 = 3;
485
486 /// Which rate-limit values a router is built with.
487 ///
488 /// Carried on [`crate::config::Config`] and passed to the route builders, so
489 /// the choice is made once where the app is assembled rather than by a compile
490 /// feature. Two consequences worth the plumbing:
491 ///
492 /// - The production limiter is the default and is what every build ships. There
493 /// is no feature flag that can quietly relax it.
494 /// - A single test run can exercise both profiles. The suite builds relaxed
495 /// routers so unrelated tests are not throttled, and the rate-limit tests
496 /// build a production router and assert the real thresholds. Previously those
497 /// tests were `#[ignore]`d under `fast-tests` because the relaxed bucket
498 /// refills faster than a loaded machine can drain it, so on CI they never ran
499 /// at all.
500 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
501 pub struct RateLimits {
502 /// Auth endpoints (login, join, email actions, public pages).
503 pub auth_ms: u64,
504 pub auth_burst: u32,
505 /// Sandbox account creation.
506 pub sandbox_ms: u64,
507 pub sandbox_burst: u32,
508 }
509
510 impl RateLimits {
511 /// The values that ship. Always the default.
512 pub const fn production() -> Self {
513 Self {
514 auth_ms: AUTH_RATE_LIMIT_MS,
515 auth_burst: AUTH_RATE_LIMIT_BURST,
516 sandbox_ms: SANDBOX_RATE_LIMIT_MS,
517 sandbox_burst: SANDBOX_RATE_LIMIT_BURST,
518 }
519 }
520
521 /// Relaxed values for tests that are not about rate limiting and would
522 /// otherwise be throttled by their own setup traffic. A lockout test needs
523 /// to fire more than five auth attempts; a fixture needs more than one
524 /// sandbox per 30 seconds.
525 pub const fn relaxed() -> Self {
526 Self {
527 auth_ms: 10,
528 auth_burst: 20,
529 sandbox_ms: 10,
530 sandbox_burst: 10,
531 }
532 }
533 }
534
535 impl Default for RateLimits {
536 fn default() -> Self {
537 Self::production()
538 }
539 }
540
541 // ── Compile-time invariants on the constants above ───────────────────────────
542 //
543 // Encoded as `const _: () = assert!(...)` rather than `#[test]` functions: these
544 // are checked when the crate is COMPILED, so a bad constant fails the build
545 // (not just a test run), and the whole invariant set sits next to the values.
546
547 // Price constants
548 const _: () = assert!(MAX_PRICE_CENTS > 0);
549 const _: () = assert!(MAX_PRICE_CENTS <= 10_000_000); // <= $100,000
550 const _: () = assert!(MIN_SUBSCRIPTION_PRICE_CENTS > 0);
551 const _: () = assert!(MIN_SUBSCRIPTION_PRICE_CENTS < MAX_PRICE_CENTS);
552
553 // Stripe fee constants
554 const _: () = assert!(STRIPE_FEE_PERCENTAGE > 0.0 && STRIPE_FEE_PERCENTAGE < 0.5);
555 const _: () = assert!(STRIPE_FEE_FIXED_CENTS > 0.0);
556
557 // Database pool
558 const _: () = assert!(DB_POOL_MAX_CONNECTIONS > DB_POOL_MIN_CONNECTIONS);
559 const _: () = assert!(DB_POOL_MIN_CONNECTIONS > 0);
560 const _: () = assert!(DB_ACQUIRE_TIMEOUT_SECS > 0);
561 const _: () = assert!(DB_MAX_LIFETIME_SECS > DB_IDLE_TIMEOUT_SECS);
562
563 // Session constants
564 const _: () = assert!(SESSION_EXPIRY_DAYS > 0 && SESSION_EXPIRY_DAYS <= 365);
565 const _: () = assert!(SESSION_TOUCH_CACHE_SECS > 0 && SESSION_TOUCH_CACHE_SECS < 86400);
566 const _: () = assert!(MAX_SESSIONS_PER_USER > 0);
567
568 // Login security
569 const _: () = assert!(MAX_LOGIN_ATTEMPTS > 0);
570 const _: () = assert!(LOCKOUT_MINUTES > 0);
571
572 // OAuth token lifetimes: access tokens are transient, refresh tokens long-lived.
573 const _: () = assert!(OAUTH_ACCESS_TOKEN_EXPIRY_SECS > 0);
574 const _: () = assert!(OAUTH_ACCESS_TOKEN_EXPIRY_SECS < SYNCKIT_JWT_EXPIRY_SECS);
575 const _: () = assert!(OAUTH_REFRESH_TOKEN_EXPIRY_SECS > OAUTH_ACCESS_TOKEN_EXPIRY_SECS);
576 const _: () = assert!(OAUTH_REFRESH_TOKEN_LENGTH >= 32);
577
578 // Email link expiry ordering
579 const _: () = assert!(PASSWORD_RESET_EXPIRY_SECS > 0);
580 const _: () = assert!(EMAIL_VERIFICATION_EXPIRY_SECS > PASSWORD_RESET_EXPIRY_SECS);
581 const _: () = assert!(ACCOUNT_DELETION_EXPIRY_SECS > 0);
582
583 // Scheduler
584 const _: () = assert!(SCHEDULER_INTERVAL_SECS > 0);
585
586 // Rate-limit bursts all positive
587 const _: () = assert!(AUTH_RATE_LIMIT_BURST > 0);
588 const _: () = assert!(VALIDATE_RATE_LIMIT_BURST > 0);
589 const _: () = assert!(API_WRITE_RATE_LIMIT_BURST > 0);
590 const _: () = assert!(API_READ_RATE_LIMIT_BURST > 0);
591 const _: () = assert!(API_EXPORT_RATE_LIMIT_BURST > 0);
592 const _: () = assert!(PREVIEW_RATE_LIMIT_BURST > 0);
593 const _: () = assert!(LICENSE_KEY_RATE_LIMIT_BURST > 0);
594 const _: () = assert!(UPLOAD_RATE_LIMIT_BURST > 0);
595 const _: () = assert!(OAUTH_RATE_LIMIT_BURST > 0);
596 const _: () = assert!(OAUTH_TOKEN_RATE_LIMIT_BURST > 0);
597 const _: () = assert!(GUEST_CHECKOUT_RATE_LIMIT_BURST > 0);
598 const _: () = assert!(GUEST_DOWNLOAD_RATE_LIMIT_BURST > 0);
599
600 // Rate-limit burst ordering: read > write > auth
601 const _: () = assert!(API_READ_RATE_LIMIT_BURST > API_WRITE_RATE_LIMIT_BURST);
602 const _: () = assert!(API_WRITE_RATE_LIMIT_BURST > AUTH_RATE_LIMIT_BURST);
603
604 // Rate-limit intervals positive
605 const _: () = assert!(AUTH_RATE_LIMIT_MS > 0);
606 const _: () = assert!(API_WRITE_RATE_LIMIT_MS > 0);
607 const _: () = assert!(API_READ_RATE_LIMIT_MS > 0);
608
609 // File size limits
610 const _: () = assert!(SCAN_MAX_MEMORY_BYTES > 0);
611 const _: () = assert!(SCAN_SPOOL_FREE_RESERVE_BYTES < SCAN_SPOOL_MAX_BYTES);
612 const _: () = assert!(SCAN_SPOOL_MAX_BYTES > SCAN_MAX_MEMORY_BYTES as u64);
613 const _: () = assert!(SCAN_JOB_RETENTION_DAYS >= 7); // no same-day purge race
614 // A browser upload must always be promotable by a single server-side copy, so
615 // the browser ceiling can never be raised past what `CopyObject` accepts.
616 const _: () = assert!(BROWSER_UPLOAD_MAX_BYTES <= S3_SINGLE_COPY_MAX_BYTES);
617 // The concurrency ceiling must not sit below the worker count, or the memory
618 // budget it's meant to enforce is unenforceable (workers would exceed it).
619 const _: () = assert!(SCAN_MAX_CONCURRENT >= SCAN_WORKER_COUNT);
620 const _: () = assert!(SCAN_ZIP_MAX_ENTRIES > 0);
621 const _: () = assert!(BROADCAST_PARALLELISM > 0 && BROADCAST_PARALLELISM <= 64);
622 const _: () = assert!(SCAN_ZIP_MAX_UNCOMPRESSED > SCAN_MAX_MEMORY_BYTES as u64);
623 const _: () = assert!(GIT_RAW_MAX_BYTES > GIT_MAX_FILE_SIZE_BYTES);
624 const _: () = assert!(SCAN_ZIP_MAX_RATIO > 0.0);
625 const _: () = assert!(SCAN_ZIP_MAX_DEPTH > 0);
626
627 // SyncKit
628 const _: () = assert!(SYNCKIT_PUSH_MAX_CHANGES > 0);
629 const _: () = assert!(SYNCKIT_PULL_PAGE_SIZE > 0);
630 const _: () = assert!(SYNCKIT_MAX_BLOB_SIZE_BYTES > 0);
631 // Multipart exists to exceed the one-shot ceiling, and nothing above the
632 // storage allowance is storable.
633 const _: () = assert!(SYNCKIT_MAX_MULTIPART_BLOB_SIZE_BYTES > SYNCKIT_MAX_BLOB_SIZE_BYTES);
634 const _: () = assert!(SYNCKIT_MAX_MULTIPART_BLOB_SIZE_BYTES <= SYNCKIT_MAX_BLOB_STORAGE_BYTES);
635 const _: () = assert!(SYNCKIT_JWT_EXPIRY_SECS > 0);
636
637 // TOTP
638 const _: () = assert!(TOTP_DIGITS == 6);
639 const _: () = assert!(TOTP_STEP == 30);
640 const _: () = assert!(BACKUP_CODE_COUNT > 0);
641 const _: () = assert!(BACKUP_CODE_LENGTH > 0);
642
643 // Pagination
644 const _: () = assert!(DISCOVER_PAGE_SIZE > 0);
645 const _: () = assert!(FEED_PAGE_SIZE > 0);
646 const _: () = assert!(PAGINATION_WINDOW_SIZE > 0);
647
648 // String constants non-empty
649 const _: () = assert!(!DATE_FMT_SHORT.is_empty());
650 const _: () = assert!(!DATE_FMT_FULL.is_empty());
651 const _: () = assert!(!DATE_FMT_ISO.is_empty());
652 const _: () = assert!(!DATE_FMT_DATETIME.is_empty());
653 const _: () = assert!(!DATE_FMT_DATETIME_UTC.is_empty());
654 const _: () = assert!(!CHANGELOG_PROJECT_SLUG.is_empty());
655 const _: () = assert!(!BUILD_ALLOWED_TARGETS.is_empty());
656
657 // Collections
658 const _: () = assert!(MAX_COLLECTIONS_PER_USER > 0);
659 const _: () = assert!(MAX_ITEMS_PER_COLLECTION > 0);
660
661 // Build pipeline
662 const _: () = assert!(BUILD_TIMEOUT_SECS > 0);
663 const _: () = assert!(BUILD_MAX_LOG_BYTES > 0);
664
665 // Health monitoring
666 const _: () = assert!(HEALTH_CHECK_INTERVAL_SECS > 0);
667 const _: () = assert!(ALERT_COOLDOWN_SECS > HEALTH_CHECK_INTERVAL_SECS);
668
669 // Sandbox
670 const _: () = assert!(SANDBOX_EXPIRY_SECS > 0);
671 const _: () = assert!(SANDBOX_CLEANUP_INTERVAL_SECS > 0);
672 const _: () = assert!(SANDBOX_CLEANUP_INTERVAL_SECS < SANDBOX_EXPIRY_SECS as u64);
673 const _: () = assert!(SANDBOX_MAX_PER_IP > 0);
674
675 // Webhook
676 const _: () = assert!(WEBHOOK_TIMESTAMP_TOLERANCE_SECS > 0);
677
678 // OAuth
679 const _: () = assert!(OAUTH_CODE_EXPIRY_SECS > 0);
680 const _: () = assert!(OAUTH_CODE_LENGTH > 0);
681
682 // Buffer limits
683 const _: () = assert!(USER_AGENT_MAX_LENGTH > 0);
684 const _: () = assert!(SYNCKIT_MAX_KEY_ENVELOPE_BYTES > 0);
685
686 #[cfg(test)]
687 mod tests {
688 use super::*;
689
690 /// The build-target FORMAT check uses `str::contains`, which isn't const,
691 /// so this invariant stays a runtime test (the rest are compile-time above).
692 #[test]
693 fn build_allowed_targets_are_os_slash_arch() {
694 for target in BUILD_ALLOWED_TARGETS {
695 assert!(!target.is_empty());
696 assert!(
697 target.contains('/'),
698 "target should be os/arch format: {target}"
699 );
700 }
701 }
702 }
703