/** * @factory/shared — platform-wide limits and shared constants. * * Single source of truth for limits so that DTOs, validators and the frontend * agree on what is legal. Values trace back to PRODUCT-SPEC §26 (performance) * and the individual feature specs. */ export const LIMITS = { /** Max operations in one block batch request (PRODUCT-SPEC: batch ops). */ MAX_BLOCK_BATCH: 500, /** Max page title length (chars). */ MAX_PAGE_TITLE: 200, /** Max workspace name length. */ MAX_WORKSPACE_NAME: 50, /** Max comment body length (chars). */ MAX_COMMENT: 20_000, /** Max comment body min length. */ MIN_COMMENT: 1, /** Max file upload size: 500 MB (bytes). */ MAX_FILE_SIZE: 500 * 1024 * 1024, /** Max number of records per page-sized query. */ MAX_RECORDS_PER_PAGE: 200, /** Max search result page size. */ MAX_SEARCH_LIMIT: 50, /** Max password length (bcrypt only handles 72 bytes — keep below). */ MAX_PASSWORD: 72, /** Min password length (policy: 10, letter + number). */ MIN_PASSWORD: 10, /** Max slug length. */ MAX_SLUG: 50, /** Max text stored per single block (protects against pathological blocks). */ MAX_BLOCK_TEXT: 100_000, /** Fractional positions longer than this trigger a rebalance (see positions.ts). */ MAX_POSITION_LENGTH: 64, /** Max child blocks under one parent before virtualization kicks in. */ MAX_INLINE_CHILDREN: 1000, /** Max records materialized client-side by the query engine (Phase 5). */ MAX_CLIENT_SIDE_ROWS: 5000, } as const; export const TOKEN_LIFETIMES = { /** Access JWT lifetime: 15 minutes (SECURITY.md / .env.example JWT_EXPIRES_IN). */ ACCESS_TOKEN: "15m", ACCESS_TOKEN_MS: 15 * 60 * 1000, /** Refresh token lifetime: 30 days (.env.example REFRESH_TOKEN_TTL_DAYS). */ REFRESH_TOKEN_MS: 30 * 24 * 60 * 60 * 1000, /** Password reset token: 1 hour, single use. */ PASSWORD_RESET_MS: 60 * 60 * 1000, /** Email verification token: 24 hours. */ EMAIL_VERIFY_MS: 24 * 60 * 60 * 1000, /** Invitation expiry: 7 days. */ INVITATION_MS: 7 * 24 * 60 * 60 * 1000, /** Signed storage URL max lifetime. */ PRESET_URL_MS: 15 * 60 * 1000, } as const; export const RATE_LIMITS = { /** Default sliding window (ms) — .env.example RATE_LIMIT_WINDOW_MS. */ WINDOW_MS: 60_000, /** Default requests per window — .env.example RATE_LIMIT_MAX. */ MAX: 300, /** Stricter limits for auth endpoints. */ AUTH: { WINDOW_MS: 60_000, MAX: 20 }, /** Login attempts: 10 per 15 min per email+IP. */ LOGIN: { WINDOW_MS: 15 * 60 * 1000, MAX: 10 }, /** Password reset requests: 5 per hour per email. */ PASSWORD_RESET: { WINDOW_MS: 60 * 60 * 1000, MAX: 5 }, /** AI endpoints are budget-gated; this is the coarse per-minute cap. */ AI: { WINDOW_MS: 60_000, MAX: 30 }, } as const; export const BCRYPT_ROUNDS_DEFAULT = 12; export const ID_PREFIXES = { user: "usr", workspace: "wsp", team: "tms", page: "pg", block: "blk", database: "db", databaseProperty: "dprop", databaseView: "dview", record: "rec", recordProperty: "rprop", comment: "cmt", file: "file", share: "shr", template: "tpl", automation: "auto", webhook: "wh", apiKey: "key", notification: "ntf", session: "ses", refreshToken: "rt", } as const; /** * Block text/background color palette — the 18 Notion-like colors: * 9 text hues (gray, brown, orange, yellow, green, blue, purple, pink, red) * and 9 matching backgrounds (`*_bg`). "default" means "no color" and is not * part of the palette itself. * * The UI maps `token` to a CSS variable (`--factory-color-red`, * `--factory-color-red_bg`, …) per PRODUCT-SPEC §27 ("design tokens, no * hard-coded colors"). */ export interface BlockColor { /** Stable id used in block `properties.color` / `properties.backgroundColor`. */ name: string; /** Suffix of the CSS custom property, e.g. "red" → `--factory-color-red`. */ token: string; /** Default (light theme) hex for previews/SSR fallback. */ hex: string; /** Whether this swatch is a background (else a text color). */ background: boolean; } const text = (name: string, hex: string): BlockColor => ({ name, token: name, hex, background: false, }); const bg = (name: string, hex: string): BlockColor => ({ name, token: `${name}_bg`, hex, background: true, }); export const BLOCK_COLORS: readonly BlockColor[] = [ text("gray", "#787774"), text("brown", "#9f6b53"), text("orange", "#d9730d"), text("yellow", "#cb912f"), text("green", "#448361"), text("blue", "#337ea9"), text("purple", "#9065b0"), text("pink", "#c14c8a"), text("red", "#d44c47"), bg("gray", "#f1f1ef"), bg("brown", "#f7e9e7"), bg("orange", "#fbecdd"), bg("yellow", "#fdf3d0"), bg("green", "#edf3ec"), bg("blue", "#e7f0f6"), bg("purple", "#f1edf7"), bg("pink", "#faedf3"), bg("red", "#fdebec"), ] as const; /** Default background for status pills (Notion status colors). */ export const STATUS_COLORS: readonly string[] = [ "gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red", ] as const; /** Default view density options. */ export const DENSITIES = ["cozy", "default", "compact"] as const; export type Density = (typeof DENSITIES)[number]; /** Supported block `properties.color` values for text blocks. */ export const TEXT_COLOR_NAMES = BLOCK_COLORS.filter((c) => !c.token.endsWith("_bg")).map( (c) => c.name, ) as readonly string[]; /** Supported block `properties.backgroundColor` values. */ export const BACKGROUND_COLOR_NAMES = ["default", ...STATUS_COLORS] as readonly string[];