| import { v } from "convex/values"; | |
| export const channelTypeValidator = v.union( | |
| v.literal("telegram"), | |
| v.literal("slack"), | |
| v.literal("email"), | |
| v.literal("discord"), | |
| v.literal("webhook"), | |
| v.literal("web_push"), | |
| ); | |
| export const sensitivityValidator = v.union( | |
| v.literal("all"), | |
| v.literal("high"), | |
| v.literal("critical"), | |
| ); | |
| export const quietHoursOverrideValidator = v.union( | |
| v.literal("critical_only"), | |
| v.literal("silence_all"), | |
| v.literal("batch_on_wake"), | |
| ); | |
| export const digestModeValidator = v.union( | |
| v.literal("realtime"), | |
| v.literal("daily"), | |
| v.literal("twice_daily"), | |
| v.literal("weekly"), | |
| ); | |
| export const proActivationStepIdValidator = v.union( | |
| v.literal("brief"), | |
| v.literal("alerts"), | |
| v.literal("power"), | |
| ); | |
| export const CURRENT_PREFS_SCHEMA_VERSION = 1; | |
| export const MAX_PREFS_BLOB_SIZE = 65536; | |
| // Cloud preference writes are low-latency but public to authenticated Convex | |
| // clients. Keep the authoritative guard inside the mutation so direct | |
| // *.convex.site callers cannot bypass the Vercel edge limiter. Keep these | |
| // values in lockstep with api/user-prefs.ts; tests/user-prefs-rate-limit.test.mts | |
| // guards the duplicated Edge/Convex constants from drifting. Unlike the | |
| // Redis-backed Vercel pre-gate, this mutation-local backstop intentionally | |
| // fails closed if its own counter storage fails: failing open here would | |
| // reopen the direct-Convex bypass this guard exists to close. | |
| export const USER_PREFS_WRITE_RATE_LIMIT = 30; | |
| export const USER_PREFS_WRITE_RATE_WINDOW_MS = 60 * 1000; | |
| // Followed-countries (watchlist primitive) constants. See | |
| // docs/plans/2026-05-02-001-feat-followed-countries-watchlist-primitive-plan.md | |
| // (U12) for context. These three constants are consumed by the | |
| // `convex/followedCountries.ts` mutations/queries (U13/U14). | |
| // Free-tier ceiling on the number of countries a single user can follow. | |
| // Enforced authoritatively in `followCountry` and `mergeAnonymousLocal`. | |
| // PRO users (entitlement tier >= 1) are unlimited. The cap is also the | |
| // "grandfather floor" — existing rows above the cap on downgrade are | |
| // never auto-deleted; only NEW follows are blocked while free. | |
| export const FREE_TIER_FOLLOW_LIMIT = 3; | |
| // Defensive ceiling on `mergeAnonymousLocal({ countries })` input length. | |
| // Prevents quadratic-cost abuse via patched localStorage shipping a | |
| // pathological array. Inputs larger than this are rejected with | |
| // `ConvexError({ kind: 'INPUT_TOO_LARGE' })`. | |
| export const MAX_MERGE_INPUT = 100; | |
| // Privacy floor for the public `countFollowers` query: counts strictly | |
| // below this threshold are returned as `0` to public callers so a single | |
| // follower can't be deanonymized via the count endpoint. | |
| export const COUNTRY_COUNT_PRIVACY_FLOOR = 5; | |
| // Number of pre-seeded lock shards in `followedCountriesShards`. Every | |
| // `followCountry` / `unfollowCountry` / `mergeAnonymousLocal` mutation | |
| // reads + patches the shard row at `userIdToShard(userId)` so Convex's | |
| // per-document OCC serializes concurrent same-user mutations against an | |
| // always-existing row. Without a pre-seeded shard table, the lazy-create | |
| // path on `followedCountriesUserMeta` had a nested TOCTOU: two parallel | |
| // first-ever mutations from the same brand-new user could both read | |
| // `meta=undefined` and both INSERT, producing duplicate meta rows that | |
| // break the `.unique()` read and re-open the cap-bypass window | |
| // (Codex round-4 P0 v2). | |
| // | |
| // SHARD_COUNT is fixed at deploy time — changing it would require | |
| // re-seeding ALL shard rows AND draining all in-flight mutations. Set | |
| // once and treat as immutable. 64 is enough headroom that two random | |
| // users colliding on the same shard is rare; collisions are correctness- | |
| // preserving (just an extra serialization point), not a bug. | |
| export const SHARD_COUNT = 64; | |