import * as z from "zod"; import { UserFactCategorySchema, UserFactSchema, UserProfileSchema } from "./domain.js"; import { Id } from "./ids.js"; export const USER_FACT_VALUE_MAX_LENGTH = 500; export const USER_COMMUNICATION_STYLE_MAX_LENGTH = 500; export const USER_PROFILE_SUMMARY_MAX_LENGTH = 4_000; /** True when `value` is an IANA time zone this runtime can format, e.g. "Europe/Vienna". */ export function isTimeZone(value: string): boolean { try { new Intl.DateTimeFormat("en-US", { timeZone: value }); return true; } catch { return false; } } /** True when `value` is a well-formed BCP-47 language tag, e.g. "de" or "pt-BR". */ export function isLanguageTag(value: string): boolean { try { return Intl.getCanonicalLocales(value).length === 1; } catch { return false; } } /** An IANA time zone, stored in its canonical spelling. */ export const TimeZoneSchema = z .string() .trim() .min(1) .max(64) .refine(isTimeZone, "Unknown time zone") .transform( (value) => new Intl.DateTimeFormat("en-US", { timeZone: value }).resolvedOptions().timeZone, ); /** A BCP-47 language tag, stored in its canonical spelling. */ export const LanguageTagSchema = z .string() .trim() .min(2) .max(35) .refine(isLanguageTag, "Invalid language tag") .transform((value) => Intl.getCanonicalLocales(value)[0] ?? value); export const UserFactValueSchema = z.string().trim().min(1).max(USER_FACT_VALUE_MAX_LENGTH); /** null shows a fact in every space; a list limits it to those spaces. */ const UserFactSpaceIds = z.array(Id).min(1).max(32).nullable(); export const UserProfileStateSchema = z.object({ profile: UserProfileSchema, facts: z.array(UserFactSchema), }); export type UserProfileState = z.infer; export const NewUserFactInput = z.object({ category: UserFactCategorySchema, value: UserFactValueSchema, /** Answers given during onboarding keep that origin; anything else the user adds is "user". */ source: z.enum(["user", "onboarding"]).default("user"), spaceIds: UserFactSpaceIds.optional(), }); export const EditUserFactInput = z.object({ id: Id, category: UserFactCategorySchema.optional(), value: UserFactValueSchema.optional(), spaceIds: UserFactSpaceIds.optional(), }); export const UpdateUserProfileInput = z.object({ /** The account name every bot calls the user by. */ name: z.string().trim().min(1).max(100).optional(), timeZone: TimeZoneSchema.nullable().optional(), locale: LanguageTagSchema.nullable().optional(), language: LanguageTagSchema.nullable().optional(), communicationStyle: z.string().trim().max(USER_COMMUNICATION_STYLE_MAX_LENGTH).optional(), summary: z.string().trim().max(USER_PROFILE_SUMMARY_MAX_LENGTH).optional(), addFacts: z.array(NewUserFactInput).max(50).optional(), editFacts: z.array(EditUserFactInput).max(50).optional(), /** Pending facts the user agrees bots may use. */ confirmFactIds: z.array(Id).max(500).optional(), }); export type UpdateUserProfile = z.infer; /** Hard-deletes facts by id, by category, or all of them. */ export const ForgetUserFactsInput = z .object({ factIds: z.array(Id).min(1).max(500).optional(), category: UserFactCategorySchema.optional(), all: z.literal(true).optional(), }) .superRefine((input, ctx) => { const targets = [input.factIds !== undefined, input.category !== undefined, input.all === true]; if (targets.filter(Boolean).length !== 1) { ctx.addIssue({ code: "custom", message: "Provide exactly one of factIds, category, or all", path: ["factIds"], }); } }); export type ForgetUserFacts = z.infer;