Skip to main content

max / makenotwork

31.3 KB · 618 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 minimum charge amount in cents (USD). Charges below this are rejected.
83 pub const STRIPE_MINIMUM_CHARGE_CENTS: i64 = 50; // $0.50
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 // Subscriptions
115 pub const MIN_SUBSCRIPTION_PRICE_CENTS: i32 = 100; // $1.00 minimum
116
117 // OAuth
118 pub const OAUTH_CODE_EXPIRY_SECS: i64 = 600; // 10 minutes
119 pub const OAUTH_CODE_LENGTH: usize = 32; // 32 bytes = 64 hex chars
120 /// Lifetime of an OAuth userinfo-scoped access token. Short by design: it is
121 /// used transiently at callback / refresh for one userinfo fetch and never
122 /// persisted by a well-behaved RP, so it never needs to outlive a request.
123 pub const OAUTH_ACCESS_TOKEN_EXPIRY_SECS: i64 = 300; // 5 minutes
124 /// Lifetime of a rotating OAuth refresh token. The RP stores this (not the
125 /// access token); each use rotates it. Long enough that perk-refresh keeps
126 /// working across the RP's own session window without forcing re-login.
127 pub const OAUTH_REFRESH_TOKEN_EXPIRY_SECS: i64 = 30 * 24 * 3600; // 30 days
128 pub const OAUTH_REFRESH_TOKEN_LENGTH: usize = 32; // 32 bytes = 64 hex chars
129
130 // Health monitoring
131 pub const HEALTH_CHECK_INTERVAL_SECS: u64 = 60;
132 pub const ALERT_COOLDOWN_SECS: u64 = 300; // 5 minutes
133 pub const HEALTH_HISTORY_RETAIN_DAYS: i64 = 90;
134
135 // Scheduled publish
136 pub const SCHEDULER_INTERVAL_SECS: u64 = 60;
137
138 // How often the rate-limiter bucket-map sweeper reclaims stale GCRA entries.
139 // Bounds limiter map size by active (not cumulative-unique) client keys.
140 pub const GOVERNOR_SWEEP_INTERVAL_SECS: u64 = 60;
141
142 // TOTP / 2FA
143 pub const TOTP_SKEW: u8 = 1; // Allow +/-1 time step (+/-30s)
144 pub const TOTP_STEP: u64 = 30; // 30-second windows
145 pub const TOTP_DIGITS: usize = 6; // 6-digit codes
146 pub const BACKUP_CODE_COUNT: usize = 10; // Generate 10 codes
147 pub const BACKUP_CODE_LENGTH: usize = 8; // 8 alphanumeric chars
148
149 // Anti-enumeration
150 pub const USERNAME_CHECK_DELAY_MS: u64 = 400;
151
152 // Rate limiting
153 // Auth endpoints (login, join): burst 5, then 2/sec.
154 // fast-tests: relaxed to burst 20 so lockout tests can fire 5+ attempts without hitting rate limiter.
155 #[cfg(not(feature = "fast-tests"))]
156 pub const AUTH_RATE_LIMIT_MS: u64 = 500;
157 #[cfg(not(feature = "fast-tests"))]
158 pub const AUTH_RATE_LIMIT_BURST: u32 = 5;
159 #[cfg(feature = "fast-tests")]
160 pub const AUTH_RATE_LIMIT_MS: u64 = 10;
161 #[cfg(feature = "fast-tests")]
162 pub const AUTH_RATE_LIMIT_BURST: u32 = 20;
163 // Username validation: burst 10, then 1/sec
164 pub const VALIDATE_RATE_LIMIT_PER_SEC: u64 = 1;
165 pub const VALIDATE_RATE_LIMIT_BURST: u32 = 10;
166 // API write endpoints (CRUD): burst 30, then 2/sec
167 pub const API_WRITE_RATE_LIMIT_MS: u64 = 500;
168 pub const API_WRITE_RATE_LIMIT_BURST: u32 = 30;
169 // API read endpoints (GET): burst 60, then 10/sec (prevents enumeration)
170 pub const API_READ_RATE_LIMIT_MS: u64 = 100;
171 pub const API_READ_RATE_LIMIT_BURST: u32 = 60;
172 // API export endpoints: burst 3, then 1/sec
173 pub const API_EXPORT_RATE_LIMIT_PER_SEC: u64 = 1;
174 pub const API_EXPORT_RATE_LIMIT_BURST: u32 = 3;
175 // Guest checkout (public, no auth): burst 10, then 1/sec
176 pub const GUEST_CHECKOUT_RATE_LIMIT_PER_SEC: u64 = 1;
177 pub const GUEST_CHECKOUT_RATE_LIMIT_BURST: u32 = 10;
178 // Guest download (public, no auth, token-gated): deliberately lenient, a buyer
179 // may pull several files in a row, but still a per-IP ceiling so the endpoint
180 // can't be hammered anonymously. Burst 60, then 2/sec.
181 pub const GUEST_DOWNLOAD_RATE_LIMIT_PER_SEC: u64 = 2;
182 pub const GUEST_DOWNLOAD_RATE_LIMIT_BURST: u32 = 60;
183 // CSP violation reports (public, unauthenticated, browser-posted): burst 20,
184 // then 1/sec. A page that violates the policy on every load would otherwise let
185 // any visitor's browser flood the log for free.
186 pub const CSP_REPORT_RATE_LIMIT_PER_SEC: u64 = 1;
187 pub const CSP_REPORT_RATE_LIMIT_BURST: u32 = 20;
188 // A CSP report is a few hundred bytes; the cap exists so the endpoint cannot be
189 // used to push a megabyte of anything at the log.
190 pub const CSP_REPORT_BODY_LIMIT_BYTES: usize = 16 * 1024;
191 // License key validation (public): burst 20, then 5/sec
192 pub const LICENSE_KEY_RATE_LIMIT_MS: u64 = 200;
193 pub const LICENSE_KEY_RATE_LIMIT_BURST: u32 = 20;
194 // File upload: burst 10, then 2/sec
195 pub const UPLOAD_RATE_LIMIT_MS: u64 = 500;
196 pub const UPLOAD_RATE_LIMIT_BURST: u32 = 10;
197 // OAuth authorize/token: burst 5/10, then 2/sec
198 pub const OAUTH_RATE_LIMIT_MS: u64 = 500;
199 pub const OAUTH_RATE_LIMIT_BURST: u32 = 5;
200 pub const OAUTH_TOKEN_RATE_LIMIT_MS: u64 = 500;
201 pub const OAUTH_TOKEN_RATE_LIMIT_BURST: u32 = 10;
202 // SyncKit auth: burst 5, then 1/sec
203 pub const SYNCKIT_AUTH_RATE_LIMIT_PER_SEC: u64 = 1;
204 pub const SYNCKIT_AUTH_RATE_LIMIT_BURST: u32 = 5;
205 // SyncKit sync (push/pull), per-IP: burst 30, then 10/sec
206 pub const SYNCKIT_SYNC_RATE_LIMIT_MS: u64 = 100;
207 pub const SYNCKIT_SYNC_RATE_LIMIT_BURST: u32 = 30;
208 // SyncKit sync, per-app: burst 60, then 20/sec (higher than per-IP because
209 // a single app may have many users behind different IPs)
210 pub const SYNCKIT_APP_RATE_LIMIT_MS: u64 = 50;
211 pub const SYNCKIT_APP_RATE_LIMIT_BURST: u32 = 60;
212 // 2FA verification: burst 5, then 2/sec (same as auth)
213 pub const TWO_FACTOR_RATE_LIMIT_MS: u64 = 500;
214 pub const TWO_FACTOR_RATE_LIMIT_BURST: u32 = 5;
215
216 // Dashboard tab reads: generous but bounded (5/sec, burst 20)
217 pub const DASHBOARD_READ_RATE_LIMIT_MS: u64 = 200;
218 pub const DASHBOARD_READ_RATE_LIMIT_BURST: u32 = 20;
219
220 // Pagination
221 pub const DISCOVER_PAGE_SIZE: u32 = 25;
222 pub const FEED_PAGE_SIZE: u32 = 25;
223 pub const PAGINATION_WINDOW_SIZE: u32 = 5;
224
225 // Creator broadcast fan-out
226 /// Max concurrent in-flight email sends per broadcast. The outer worker
227 /// task spawns up to this many child tasks, then waits on one to drain
228 /// before spawning the next.
229 pub const BROADCAST_PARALLELISM: usize = 16;
230 /// Delay between successive broadcast send-task spawns. Spreads Postmark
231 /// API load when a creator with thousands of followers fires a broadcast:
232 /// at parallelism 16 + 100 ms cadence, steady-state is ~10 sends/sec.
233 pub const BROADCAST_CHUNK_DELAY_MS: u64 = 100;
234 /// Recipient cap per broadcast send. Above this, the request is refused
235 /// with an instruction to contact support. Bounds Postmark spend exposure
236 /// from any single approved creator. Founder-window cohort is well under
237 /// this; the cap is the floor we'd lift on request, not the ceiling.
238 pub const BROADCAST_MAX_RECIPIENTS: usize = 10_000;
239
240 /// Cap on buyer-departure notification fan-out per creator-deletion event.
241 /// Account deletion notifies historical buyers about content removal. A
242 /// creator with millions of completed sales should not turn one deletion
243 /// into a Postmark bomb. The cap bounds both the in-memory buyer list and
244 /// total outbound email volume; if hit, we log a warning and notify the
245 /// oldest-buyers slice the SQL chose.
246 pub const BUYER_DEPARTURE_MAX_NOTIFICATIONS: i64 = 50_000;
247
248 // File scanning
249 pub const SCAN_MAX_MEMORY_BYTES: usize = 100 * 1024 * 1024; // 100 MB in-memory threshold
250 // Ceiling on in-flight scans, enforced by a semaphore around the CPU/clamd
251 // phase. NOTE: with `SCAN_WORKER_COUNT` workers each scanning one file at a
252 // time, the real concurrency is `min(SCAN_MAX_CONCURRENT, SCAN_WORKER_COUNT)`,
253 // today that's 2 (~200 MB peak), so this semaphore only begins to bind if the
254 // worker count is raised above it. Kept as an explicit ceiling so that raising
255 // `SCAN_WORKER_COUNT` can't silently blow past the memory budget. The
256 // assertion below documents that intent.
257 pub const SCAN_MAX_CONCURRENT: usize = 4; // Memory-budget ceiling on concurrent scans
258 pub const SCAN_WORKER_COUNT: usize = 2; // Background worker tasks draining scan_jobs queue
259 /// Wall-clock ceiling on the CPU-bound scan layers (content-type, structural,
260 /// archive decompress, yara, sha256) as a whole. The per-layer deadlines that
261 /// existed, yara 30s, clamav/urlhaus their own, did not cover the archive
262 /// decompress walk, which was byte-bounded (`SCAN_ZIP_MAX_UNCOMPRESSED`) but not
263 /// time-bounded, so a slow-codec archive under the ratio caps could pin one of
264 /// the two scan workers for its full decompress wall-time (fuzz 2026-07-06 F1).
265 /// On elapse the scan fails closed (held for review) and the worker is freed;
266 /// the orphaned blocking thread runs to completion on the (large) blocking pool.
267 /// Generous enough for a legitimately large object, tight enough to bound the
268 /// two-worker pool against monopolization.
269 pub const SCAN_CPU_LAYERS_TIMEOUT_SECS: u64 = 120;
270 /// Retention window for terminal-state (`done`, `failed`) `scan_jobs` rows.
271 /// Queued/running rows are operational queue state and not affected.
272 pub const SCAN_JOB_RETENTION_DAYS: u32 = 30;
273 /// Directory under which the scanner spools large objects to tempfiles
274 /// before invoking path/stream-based layer entries. On production, systemd
275 /// provisions this via `StateDirectory=mnw/scan-spool` so the path resolves
276 /// to `/var/lib/mnw/scan-spool`. Override with `MNW_SCAN_SPOOL_DIR` for dev.
277 pub const SCAN_SPOOL_DIR: &str = "/var/lib/makenotwork/scan-spool";
278 /// Files in `SCAN_SPOOL_DIR` older than this are considered orphaned
279 /// (a panic, OOM, or hard kill left them behind) and reaped on the
280 /// next sweep. RAII drop in `SpoolHandle` covers the live path; this
281 /// covers process-death.
282 pub const SCAN_SPOOL_ORPHAN_AGE_SECS: u64 = 3600;
283 /// Hard cap on a single spooled object. Above this, the scanner refuses
284 /// the job rather than risk filling the volume. It sits below the 20 GB the
285 /// upload tiers allow, deliberately: objects past this ceiling are held for
286 /// manual admin review (`scanning::worker`) instead of being auto-scanned,
287 /// which is fail-closed and cheap at alpha volume. Raising it to cover the full
288 /// tier would cost spool headroom and add scan latency on every large file.
289 pub const SCAN_SPOOL_MAX_BYTES: u64 = 8 * 1024 * 1024 * 1024;
290 /// Minimum free space the spool volume must retain after writing the
291 /// pending object. The scanner refuses if `statvfs(free) - expected_size`
292 /// would drop below this threshold.
293 pub const SCAN_SPOOL_FREE_RESERVE_BYTES: u64 = 2 * 1024 * 1024 * 1024;
294 /// Slack added to a scan job's claimed object size when bounding how many bytes
295 /// the spool writer will accept. The S3 object size is authoritative, but a
296 /// small margin absorbs benign content-length/multipart rounding without
297 /// letting an under-reported object stream the full `SCAN_SPOOL_MAX_BYTES` to
298 /// scratch before the writer aborts.
299 pub const SCAN_SPOOL_SLACK_BYTES: u64 = 16 * 1024 * 1024; // 16 MiB
300 /// Maximum number of bytes fed to YARA in a single scan. yara-x's `Scanner`
301 /// walks the whole slice, which demand-pages the entire mmap resident, so an
302 /// 8 GiB object would otherwise pin 8 GiB of page cache per scan (×
303 /// `SCAN_MAX_CONCURRENT`). Malware signatures cluster near a file's start, and
304 /// ClamAV (streamed, uncapped) is the full-file backstop, so scanning a generous
305 /// prefix is the right trade. Above this, YARA sees the prefix and logs the cap.
306 pub const SCAN_YARA_MAX_BYTES: usize = 512 * 1024 * 1024; // 512 MiB
307
308 /// Per-call deadline for the optional external second-opinion lookups
309 /// (MalwareBazaar, MetaDefender). They are FailOpen by design; bounding each
310 /// await keeps a slow or unreachable third party from holding a scan worker slot
311 /// indefinitely (ultra-fuzz Run 6 Performance). On timeout the layer reports
312 /// Skip, the same shape as the disabled case, never blocking the file.
313 pub const SCAN_EXTERNAL_LOOKUP_TIMEOUT_SECS: u64 = 10;
314
315 // Caddy on-demand TLS
316 // Caps concurrent cache-miss DB lookups in `/api/domains/caddy-ask`. Cache hits
317 // are unbounded (DashMap). At capacity, the handler returns 503 so Caddy retries
318 // later instead of stampeding the DB pool or driving ACME issuance for garbage
319 // domains. Sized small because the slow path is one indexed lookup.
320 pub const CADDY_ASK_MAX_CONCURRENT: usize = 8;
321 pub const SCAN_ZIP_MAX_RATIO: f64 = 100.0; // Max compression ratio before ZIP bomb
322 pub const SCAN_ZIP_MAX_DEPTH: u32 = 2; // Max nested archives (detection is 1 level deep; decompressed size limit is the primary defense)
323 pub const SCAN_ZIP_MAX_UNCOMPRESSED: u64 = 2 * 1024 * 1024 * 1024; // 2 GB uncompressed limit
324 // Cap the number of ZIP entries inspected. Depth/ratio/uncompressed-size are
325 // already bounded, but a ZIP with millions of tiny entries forces a full
326 // per-entry decompression pass bounded only by the 2 GB total. 100k entries is
327 // far past any legitimate sample pack / content bundle; beyond it, fail closed.
328 pub const SCAN_ZIP_MAX_ENTRIES: usize = 100_000;
329 pub const SCAN_MALWAREBAZAAR_TIMEOUT_SECS: u64 = 5;
330 /// TCP connect timeout for the external-lookup HTTP clients (MalwareBazaar,
331 /// MetaDefender, URLhaus). Bounds the time spent establishing a connection to a
332 /// hung/blackholed host so a stalled connect can't pin a scan worker (Perf-S2).
333 pub const SCAN_HTTP_CONNECT_TIMEOUT_SECS: u64 = 5;
334 pub const SCAN_CLAMAV_TIMEOUT_SECS: u64 = 30;
335
336 // Invite system
337 pub const INVITES_ENABLED: bool = true;
338 pub const INVITE_LIMIT_PER_CREATOR: i64 = 5; // max unredeemed codes per creator
339
340 // Git source browser
341 pub const GIT_MAX_FILE_SIZE_BYTES: usize = 1_024_000; // 1MB display limit
342 pub const GIT_COMMITS_PER_PAGE: usize = 30;
343 pub const GIT_DIFF_MAX_FILES: usize = 20; // Inline diff hunks for first N files
344 pub const GIT_DIFF_MAX_LINES: usize = 500; // Per-file line cap for diff display
345 pub const GIT_REPOS_PER_PAGE: usize = 30;
346 pub const GIT_FILE_LOG_MAX_WALK: usize = 1000; // Max commits to walk for per-file history
347 pub const GIT_RAW_MAX_BYTES: usize = 100 * 1024 * 1024; // 100 MB raw download limit
348 pub const GIT_UPLOAD_PACK_MAX_BYTES: usize = 10 * 1024 * 1024; // 10 MB upload-pack body limit
349 // Max concurrent git smart-HTTP clone/fetch responses. Each runs a `git
350 // upload-pack` child and streams a packfile; this bounds the process fan-out so
351 // a burst of clones on a large repo can't exhaust processes/memory.
352 pub const GIT_SMART_HTTP_MAX_CONCURRENT: usize = 8;
353 // Hard ceiling on how long a single clone's `git upload-pack` child may run
354 // while holding its concurrency permit. A client that stops reading makes git
355 // block on a full stdout pipe and never exit, pinning the permit; killing it
356 // past this deadline keeps slow/stuck clones from starving the budget. Generous
357 // enough for a large repo over a slow link.
358 pub const GIT_SMART_HTTP_TIMEOUT_SECS: u64 = 300;
359 // Max size of a single git push (receive-pack) request body over HTTP. The pack
360 // is streamed into git's stdin (not buffered), but this caps a single push so a
361 // runaway upload can't fill the repo disk unbounded.
362 pub const GIT_RECEIVE_PACK_MAX_BYTES: usize = 2 * 1024 * 1024 * 1024;
363 // `receive.maxInputSize` written into every bare repo's config at creation. This
364 // is the SSH-side equivalent of `GIT_RECEIVE_PACK_MAX_BYTES` (which only bounds
365 // the HTTP smart path): git-receive-pack aborts a push whose pack exceeds this,
366 // so an authenticated user can't stream an arbitrarily large pack over SSH.
367 pub const GIT_SSH_MAX_PACK_BYTES: i64 = 2 * 1024 * 1024 * 1024;
368 // Hard runaway-backstop on a single SSH git operation (clone/fetch/push). Unlike
369 // the HTTP path (a per-request layer), the SSH path execs git-shell directly, so
370 // a client that stalls mid-transfer would otherwise pin the process indefinitely.
371 // Generous enough for a large repo over a slow link; only kills genuinely stuck
372 // operations.
373 pub const GIT_SSH_OP_TIMEOUT_SECS: u64 = 900; // 15 min
374 // Per-user on-disk git storage ceiling, checked before an SSH push is allowed.
375 // Coarse (summed at authorization time, not atomic), but combined with the
376 // per-push `GIT_SSH_MAX_PACK_BYTES` it bounds total repo growth per account.
377 pub const GIT_USER_DISK_QUOTA_BYTES: u64 = 20 * 1024 * 1024 * 1024; // 20 GiB backstop
378
379 // Webhook security
380 pub const WEBHOOK_TIMESTAMP_TOLERANCE_SECS: u64 = 300; // 5 minutes
381
382 // Collections
383 pub const MAX_COLLECTIONS_PER_USER: i64 = 50;
384 pub const MAX_ITEMS_PER_COLLECTION: i64 = 200;
385
386 // OTA updates
387 pub const OTA_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour
388 // OTA management: burst 10, then 2/sec (same as API write)
389 pub const OTA_WRITE_RATE_LIMIT_MS: u64 = 500;
390 pub const OTA_WRITE_RATE_LIMIT_BURST: u32 = 10;
391 // OTA public (updater check, download): burst 30, then 10/sec
392 pub const OTA_READ_RATE_LIMIT_MS: u64 = 100;
393 pub const OTA_READ_RATE_LIMIT_BURST: u32 = 30;
394
395 // Build pipeline
396 pub const BUILD_TIMEOUT_SECS: u64 = 1800; // 30 min
397 pub const BUILD_MAX_LOG_BYTES: usize = 5_242_880; // 5 MB
398 pub const BUILD_HISTORY_LIMIT: i64 = 50;
399 pub const BUILD_TRIGGER_RATE_LIMIT_PER_SEC: u64 = 1;
400 pub const BUILD_TRIGGER_RATE_LIMIT_BURST: u32 = 3;
401 pub const BUILD_WRITE_RATE_LIMIT_MS: u64 = 500;
402 pub const BUILD_WRITE_RATE_LIMIT_BURST: u32 = 10;
403 // Git browsing: burst 30, then 5/sec (blame/log can be expensive)
404 pub const GIT_BROWSE_RATE_LIMIT_MS: u64 = 200;
405 pub const GIT_BROWSE_RATE_LIMIT_BURST: u32 = 30;
406 pub const BUILD_ALLOWED_TARGETS: &[&str] = &[
407 "linux/x86_64",
408 "linux/aarch64",
409 "darwin/x86_64",
410 "darwin/aarch64",
411 ];
412
413 // Streaming
414 pub const STREAMING_CACHE_MAX_SECS: u64 = 86400; // 24 hours max presigned URL lifetime
415 /// Rate limit for stream/download URL requests: 1 per 3 seconds, burst of 10.
416 pub const STREAM_RATE_LIMIT_MS: u64 = 3000;
417 pub const STREAM_RATE_LIMIT_BURST: u32 = 10;
418
419 // Date display formats
420 pub const DATE_FMT_SHORT: &str = "%b %d"; // "Mar 25"
421 pub const DATE_FMT_FULL: &str = "%b %d, %Y"; // "Mar 25, 2026"
422 pub const DATE_FMT_ISO: &str = "%Y-%m-%d"; // "2026-03-25"
423 pub const DATE_FMT_DATETIME: &str = "%b %d, %Y %H:%M"; // "Mar 25, 2026 14:30"
424 pub const DATE_FMT_DATETIME_UTC: &str = "%b %d, %Y %H:%M UTC"; // "Mar 25, 2026 14:30 UTC"
425
426 // Platform content
427 pub const CHANGELOG_PROJECT_SLUG: &str = "changelog";
428
429 // String / buffer limits
430 pub const USER_AGENT_MAX_LENGTH: usize = 512;
431 pub const SYNCKIT_MAX_KEY_ENVELOPE_BYTES: usize = 4096;
432 pub const MAX_PRICE_CENTS: i32 = 1_000_000; // $10,000
433 /// Minimum for a non-zero buy-once price, Stripe rejects charges under $0.50.
434 pub const MIN_BUY_ONCE_PRICE_CENTS: i32 = 50; // $0.50
435
436 // Sandbox accounts
437 /// How long a sandbox session lasts before auto-cleanup.
438 pub const SANDBOX_EXPIRY_SECS: i64 = 3600; // 1 hour
439 /// How often the cleanup job runs.
440 pub const SANDBOX_CLEANUP_INTERVAL_SECS: u64 = 300; // 5 minutes
441 /// Rate limit: sandbox creation.
442 /// Production: 1 per 30 seconds, burst 2. fast-tests: 1 per 10ms, burst 10.
443 /// Run integration tests with `cargo test --features fast-tests` to avoid rate-limit failures.
444 #[cfg(not(feature = "fast-tests"))]
445 pub const SANDBOX_RATE_LIMIT_MS: u64 = 30_000;
446 #[cfg(not(feature = "fast-tests"))]
447 pub const SANDBOX_RATE_LIMIT_BURST: u32 = 2;
448 #[cfg(feature = "fast-tests")]
449 pub const SANDBOX_RATE_LIMIT_MS: u64 = 10;
450 #[cfg(feature = "fast-tests")]
451 pub const SANDBOX_RATE_LIMIT_BURST: u32 = 10;
452 /// Max concurrent active sandboxes per IP.
453 pub const SANDBOX_MAX_PER_IP: i64 = 3;
454
455 // ── Compile-time invariants on the constants above ───────────────────────────
456 //
457 // Encoded as `const _: () = assert!(...)` rather than `#[test]` functions: these
458 // are checked when the crate is COMPILED, so a bad constant fails the build
459 // (not just a test run), and the whole invariant set sits next to the values.
460
461 // Price constants
462 const _: () = assert!(MAX_PRICE_CENTS > 0);
463 const _: () = assert!(MAX_PRICE_CENTS <= 10_000_000); // <= $100,000
464 const _: () = assert!(MIN_SUBSCRIPTION_PRICE_CENTS > 0);
465 const _: () = assert!(MIN_SUBSCRIPTION_PRICE_CENTS < MAX_PRICE_CENTS);
466 const _: () = assert!(MIN_BUY_ONCE_PRICE_CENTS > 0);
467 const _: () = assert!(MIN_BUY_ONCE_PRICE_CENTS < MAX_PRICE_CENTS);
468
469 // Stripe fee constants
470 const _: () = assert!(STRIPE_FEE_PERCENTAGE > 0.0 && STRIPE_FEE_PERCENTAGE < 0.5);
471 const _: () = assert!(STRIPE_FEE_FIXED_CENTS > 0.0);
472
473 // Database pool
474 const _: () = assert!(DB_POOL_MAX_CONNECTIONS > DB_POOL_MIN_CONNECTIONS);
475 const _: () = assert!(DB_POOL_MIN_CONNECTIONS > 0);
476 const _: () = assert!(DB_ACQUIRE_TIMEOUT_SECS > 0);
477 const _: () = assert!(DB_MAX_LIFETIME_SECS > DB_IDLE_TIMEOUT_SECS);
478
479 // Session constants
480 const _: () = assert!(SESSION_EXPIRY_DAYS > 0 && SESSION_EXPIRY_DAYS <= 365);
481 const _: () = assert!(SESSION_TOUCH_CACHE_SECS > 0 && SESSION_TOUCH_CACHE_SECS < 86400);
482 const _: () = assert!(MAX_SESSIONS_PER_USER > 0);
483
484 // Login security
485 const _: () = assert!(MAX_LOGIN_ATTEMPTS > 0);
486 const _: () = assert!(LOCKOUT_MINUTES > 0);
487
488 // OAuth token lifetimes: access tokens are transient, refresh tokens long-lived.
489 const _: () = assert!(OAUTH_ACCESS_TOKEN_EXPIRY_SECS > 0);
490 const _: () = assert!(OAUTH_ACCESS_TOKEN_EXPIRY_SECS < SYNCKIT_JWT_EXPIRY_SECS);
491 const _: () = assert!(OAUTH_REFRESH_TOKEN_EXPIRY_SECS > OAUTH_ACCESS_TOKEN_EXPIRY_SECS);
492 const _: () = assert!(OAUTH_REFRESH_TOKEN_LENGTH >= 32);
493
494 // Email link expiry ordering
495 const _: () = assert!(PASSWORD_RESET_EXPIRY_SECS > 0);
496 const _: () = assert!(EMAIL_VERIFICATION_EXPIRY_SECS > PASSWORD_RESET_EXPIRY_SECS);
497 const _: () = assert!(ACCOUNT_DELETION_EXPIRY_SECS > 0);
498
499 // Scheduler
500 const _: () = assert!(SCHEDULER_INTERVAL_SECS > 0);
501
502 // Rate-limit bursts all positive
503 const _: () = assert!(AUTH_RATE_LIMIT_BURST > 0);
504 const _: () = assert!(VALIDATE_RATE_LIMIT_BURST > 0);
505 const _: () = assert!(API_WRITE_RATE_LIMIT_BURST > 0);
506 const _: () = assert!(API_READ_RATE_LIMIT_BURST > 0);
507 const _: () = assert!(API_EXPORT_RATE_LIMIT_BURST > 0);
508 const _: () = assert!(LICENSE_KEY_RATE_LIMIT_BURST > 0);
509 const _: () = assert!(UPLOAD_RATE_LIMIT_BURST > 0);
510 const _: () = assert!(OAUTH_RATE_LIMIT_BURST > 0);
511 const _: () = assert!(OAUTH_TOKEN_RATE_LIMIT_BURST > 0);
512 const _: () = assert!(GUEST_CHECKOUT_RATE_LIMIT_BURST > 0);
513 const _: () = assert!(GUEST_DOWNLOAD_RATE_LIMIT_BURST > 0);
514
515 // Rate-limit burst ordering: read > write > auth
516 const _: () = assert!(API_READ_RATE_LIMIT_BURST > API_WRITE_RATE_LIMIT_BURST);
517 const _: () = assert!(API_WRITE_RATE_LIMIT_BURST > AUTH_RATE_LIMIT_BURST);
518
519 // Rate-limit intervals positive
520 const _: () = assert!(AUTH_RATE_LIMIT_MS > 0);
521 const _: () = assert!(API_WRITE_RATE_LIMIT_MS > 0);
522 const _: () = assert!(API_READ_RATE_LIMIT_MS > 0);
523
524 // File size limits
525 const _: () = assert!(SCAN_MAX_MEMORY_BYTES > 0);
526 const _: () = assert!(SCAN_SPOOL_FREE_RESERVE_BYTES < SCAN_SPOOL_MAX_BYTES);
527 const _: () = assert!(SCAN_SPOOL_MAX_BYTES > SCAN_MAX_MEMORY_BYTES as u64);
528 const _: () = assert!(SCAN_JOB_RETENTION_DAYS >= 7); // no same-day purge race
529 // A browser upload must always be promotable by a single server-side copy, so
530 // the browser ceiling can never be raised past what `CopyObject` accepts.
531 const _: () = assert!(BROWSER_UPLOAD_MAX_BYTES <= S3_SINGLE_COPY_MAX_BYTES);
532 // The concurrency ceiling must not sit below the worker count, or the memory
533 // budget it's meant to enforce is unenforceable (workers would exceed it).
534 const _: () = assert!(SCAN_MAX_CONCURRENT >= SCAN_WORKER_COUNT);
535 const _: () = assert!(SCAN_ZIP_MAX_ENTRIES > 0);
536 const _: () = assert!(BROADCAST_PARALLELISM > 0 && BROADCAST_PARALLELISM <= 64);
537 const _: () = assert!(SCAN_ZIP_MAX_UNCOMPRESSED > SCAN_MAX_MEMORY_BYTES as u64);
538 const _: () = assert!(GIT_RAW_MAX_BYTES > GIT_MAX_FILE_SIZE_BYTES);
539 const _: () = assert!(SCAN_ZIP_MAX_RATIO > 0.0);
540 const _: () = assert!(SCAN_ZIP_MAX_DEPTH > 0);
541
542 // SyncKit
543 const _: () = assert!(SYNCKIT_PUSH_MAX_CHANGES > 0);
544 const _: () = assert!(SYNCKIT_PULL_PAGE_SIZE > 0);
545 const _: () = assert!(SYNCKIT_MAX_BLOB_SIZE_BYTES > 0);
546 // Multipart exists to exceed the one-shot ceiling, and nothing above the
547 // storage allowance is storable.
548 const _: () = assert!(SYNCKIT_MAX_MULTIPART_BLOB_SIZE_BYTES > SYNCKIT_MAX_BLOB_SIZE_BYTES);
549 const _: () = assert!(SYNCKIT_MAX_MULTIPART_BLOB_SIZE_BYTES <= SYNCKIT_MAX_BLOB_STORAGE_BYTES);
550 const _: () = assert!(SYNCKIT_JWT_EXPIRY_SECS > 0);
551
552 // TOTP
553 const _: () = assert!(TOTP_DIGITS == 6);
554 const _: () = assert!(TOTP_STEP == 30);
555 const _: () = assert!(BACKUP_CODE_COUNT > 0);
556 const _: () = assert!(BACKUP_CODE_LENGTH > 0);
557
558 // Pagination
559 const _: () = assert!(DISCOVER_PAGE_SIZE > 0);
560 const _: () = assert!(FEED_PAGE_SIZE > 0);
561 const _: () = assert!(PAGINATION_WINDOW_SIZE > 0);
562
563 // String constants non-empty
564 const _: () = assert!(!DATE_FMT_SHORT.is_empty());
565 const _: () = assert!(!DATE_FMT_FULL.is_empty());
566 const _: () = assert!(!DATE_FMT_ISO.is_empty());
567 const _: () = assert!(!DATE_FMT_DATETIME.is_empty());
568 const _: () = assert!(!DATE_FMT_DATETIME_UTC.is_empty());
569 const _: () = assert!(!CHANGELOG_PROJECT_SLUG.is_empty());
570 const _: () = assert!(!BUILD_ALLOWED_TARGETS.is_empty());
571
572 // Collections
573 const _: () = assert!(MAX_COLLECTIONS_PER_USER > 0);
574 const _: () = assert!(MAX_ITEMS_PER_COLLECTION > 0);
575
576 // Build pipeline
577 const _: () = assert!(BUILD_TIMEOUT_SECS > 0);
578 const _: () = assert!(BUILD_MAX_LOG_BYTES > 0);
579
580 // Health monitoring
581 const _: () = assert!(HEALTH_CHECK_INTERVAL_SECS > 0);
582 const _: () = assert!(ALERT_COOLDOWN_SECS > HEALTH_CHECK_INTERVAL_SECS);
583
584 // Sandbox
585 const _: () = assert!(SANDBOX_EXPIRY_SECS > 0);
586 const _: () = assert!(SANDBOX_CLEANUP_INTERVAL_SECS > 0);
587 const _: () = assert!(SANDBOX_CLEANUP_INTERVAL_SECS < SANDBOX_EXPIRY_SECS as u64);
588 const _: () = assert!(SANDBOX_MAX_PER_IP > 0);
589
590 // Webhook
591 const _: () = assert!(WEBHOOK_TIMESTAMP_TOLERANCE_SECS > 0);
592
593 // OAuth
594 const _: () = assert!(OAUTH_CODE_EXPIRY_SECS > 0);
595 const _: () = assert!(OAUTH_CODE_LENGTH > 0);
596
597 // Buffer limits
598 const _: () = assert!(USER_AGENT_MAX_LENGTH > 0);
599 const _: () = assert!(SYNCKIT_MAX_KEY_ENVELOPE_BYTES > 0);
600
601 #[cfg(test)]
602 mod tests {
603 use super::*;
604
605 /// The build-target FORMAT check uses `str::contains`, which isn't const,
606 /// so this invariant stays a runtime test (the rest are compile-time above).
607 #[test]
608 fn build_allowed_targets_are_os_slash_arch() {
609 for target in BUILD_ALLOWED_TARGETS {
610 assert!(!target.is_empty());
611 assert!(
612 target.contains('/'),
613 "target should be os/arch format: {target}"
614 );
615 }
616 }
617 }
618