/** * TASK-0133 — Org policy schema (the six enforced security settings). * * Stored on `Organization.policy` (Json, default `{}`). Every field is * OPTIONAL with a default — the DEFAULT policy (an empty object) is the * permissive baseline, so an org that never edits its policy behaves * exactly as it always did (backward compatibility, AGENTS rule 6). * * The PLAN CAP on `maxFileSizeMB` is applied SERVER-SIDE in * `org-policy.service.ts` (effective policy = org policy ∩ plan limits), * NOT here — the schema stores what the admin SET, the service computes * what actually applies (the editor shows both). */ import { z } from "zod"; export const OrgPolicySchema = z .object({ /** Require 2FA (TOTP) at login for users in ANY of the user's orgs. */ require2FA: z.boolean().default(false), /** Force SSO sign-in (password login → 403 sso_required). Mirrored to * `SsoProvider.enforce` by the admin UI (TASK-0131). */ ssoEnforced: z.boolean().default(false), /** Block creating guest (anyone-with-link) public shares. */ disableGuestPublicShare: z.boolean().default(false), /** Max single-file upload size (MB). Plan-capped (free 50 / paid 500). */ maxFileSizeMB: z.number().int().min(1).max(10240).optional(), /** Session lifetime in hours (sliding window). Default 24h… actually the * platform default is the refresh TTL; a SET value clamps it. */ sessionLifetimeHours: z.number().int().min(1).max(720).optional(), /** Allow external embed/video/link-preview fetching. Default true. */ allowExternalEmbeds: z.boolean().default(true) }) .default({}); export type OrgPolicy = z.infer; /** Partial update (PATCH admin/policy) — any subset of the six fields. */ export const OrgPolicyUpdateSchema = z .object({ require2FA: z.boolean().optional(), ssoEnforced: z.boolean().optional(), disableGuestPublicShare: z.boolean().optional(), maxFileSizeMB: z.number().int().min(1).max(10240).nullable().optional(), sessionLifetimeHours: z.number().int().min(1).max(720).nullable().optional(), allowExternalEmbeds: z.boolean().optional() }) .refine((v) => Object.keys(v).length > 0, { message: "No policy fields provided" }); export type OrgPolicyUpdate = z.infer; /** A stored policy is a loose Json object — parse it, tolerating unknown * keys (forward-compat: a newer field written by another release) and a * null/absent column (the @default("{}") only applies to new rows). */ export function parseStoredPolicy(raw: unknown): OrgPolicy { if (raw === null || raw === undefined) return OrgPolicySchema.parse({}); if (typeof raw !== "object") return OrgPolicySchema.parse({}); // Drop unknown keys so the schema's `.default()` fills the rest. const known = Object.fromEntries( Object.entries(raw as Record).filter(([k]) => ["require2FA", "ssoEnforced", "disableGuestPublicShare", "maxFileSizeMB", "sessionLifetimeHours", "allowExternalEmbeds"].includes(k) ) ); return OrgPolicySchema.parse(known); }