import {z} from 'zod'; import {createHash} from 'node:crypto'; const id=z.string().min(1).max(80).regex(/^[a-zA-Z0-9_-]+$/); // eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input. const label=z.string().trim().max(80).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)); export const LibraryPolicySchema=z.object({aiSuggestionsEnabled:z.boolean().default(false),aiAutoApplyEnabled:z.boolean().default(false)}).strict().prefault({}); // eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input. export const LibraryTabsSchema=z.array(z.object({id,title:z.string().max(120).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)),url:z.string().max(2048).url().refine(v=>['https:','http:'].includes(new URL(v).protocol))}).strict()).max(100).refine(v=>new Set(v.map(t=>t.id)).size===v.length); export const LibraryPlanSchema=z.object({bookmarks:z.array(z.object({id,folder:label}).strict()).max(100),tabs:z.array(z.object({id,group:label.refine(v=>v.length<=40)}).strict()).max(100)}).strict().superRefine((v,c)=>{for(const key of ['bookmarks','tabs'])if(new Set(v[key].map(i=>i.id)).size!==v[key].length)c.addIssue({code:'custom',message:'Duplicate organization IDs'});}); export function librarySnapshotHash(tabs){return createHash('sha256').update(JSON.stringify(LibraryTabsSchema.parse(tabs))).digest('hex');} export function validateLibraryPlan(value,{bookmarks,tabs}){ const plan=LibraryPlanSchema.parse(value); for(const [key,items] of [['bookmarks',bookmarks],['tabs',tabs]]){ const ids=new Set(items.map(i=>i.id)); if(plan[key].length!==ids.size||plan[key].some(i=>!ids.has(i.id)))throw Error('Organization must contain each existing item exactly once'); } return plan; } // Query strings, fragments, credentials and path components are never needed // to group a library. Titles remain untrusted data and are inspected by DLP. export function libraryMetadata(items){return items.map(({id,title,url})=>({id,title,host:new URL(url).hostname}));}