20 lines
1.8 KiB
JavaScript
20 lines
1.8 KiB
JavaScript
import {z} from 'zod';
|
|||
|
|
import {createHash} from 'node:crypto';
|
||
|
|
|
||
|
|
const id=z.string().min(1).max(80).regex(/^[a-zA-Z0-9_-]+$/);
|
||
|
|
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({});
|
||
|
|
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}));}
|