Files
safetynet/apps/browser/bookmarks.cjs
T

27 lines
2.9 KiB
JavaScript

const {requireSection}=require('./managed-settings.cjs');
function libraryVisible(settings){return !!settings?.available&&!settings.hiddenSections.includes('library');}
function bookmarkState({policy,ready,settings}){
const rules=policy?.browser?.bookmarks;
if(!ready||!libraryVisible(settings)||!rules)return {managed:[],personal:{revision:0,items:[]},allowUserAdd:false,allowUserEdit:false,allowUserDelete:false};
const result=structuredClone(rules);if(!settings.visible||settings.lockedSections.includes('library'))result.allowUserAdd=result.allowUserEdit=result.allowUserDelete=false;return result;
}
function validateMutation(input,{policy,ready,settings,destinationAllowed,now=Date.now()}){
if(!ready||!policy||Date.parse(policy.expiresAt)<=now)throw Error('Managed browser access is paused');
requireSection(settings,'library');
const rules=policy.browser.bookmarks;
if(!input||!['add','update','delete'].includes(input.action)||Object.keys(input).some(key=>!['action','bookmark','id','expectedRevision','policyVersion'].includes(key)))throw Error('Invalid bookmark operation');
if(input.expectedRevision!==rules.personal.revision||input.policyVersion!==policy.version)throw Error('Bookmarks changed. Refresh before editing.');
if(!rules[{add:'allowUserAdd',update:'allowUserEdit',delete:'allowUserDelete'}[input.action]])throw Error('Bookmark changes are controlled by your administrator');
const id=input.action==='delete'?input.id:input.bookmark?.id;
if(typeof id!=='string'||!/^[A-Za-z0-9_-]{1,80}$/.test(id)||rules.managed.some(item=>item.id===id))throw Error('Company bookmarks cannot be changed');
if(input.action==='add'&&rules.personal.items.some(item=>item.id===id)||input.action!=='add'&&!rules.personal.items.some(item=>item.id===id))throw Error('Bookmark identity is unavailable');
if(input.action==='delete')return {action:input.action,id,expectedRevision:input.expectedRevision,policyVersion:input.policyVersion};
const item=input.bookmark;
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
if(Object.keys(item).some(key=>!['id','title','url','folder'].includes(key))||typeof item.title!=='string'||!item.title.trim()||item.title.length>120||/[\u0000-\u001f\u007f]/.test(item.title)||typeof item.folder!=='string'||item.folder.length>80||/[\u0000-\u001f\u007f]/.test(item.folder)||typeof item.url!=='string'||item.url.length>2048)throw Error('Invalid bookmark details');
let url;try{url=new URL(item.url);}catch{throw Error('Invalid bookmark URL');}
if(!['https:','http:'].includes(url.protocol)||url.username||url.password||url.search||url.hash||!destinationAllowed(url.href,policy))throw Error('Bookmark destination is not approved');
return {action:input.action,bookmark:{id,title:item.title.trim(),url:url.href,folder:item.folder.trim()},expectedRevision:input.expectedRevision,policyVersion:input.policyVersion};
}
module.exports={bookmarkState,validateMutation,libraryVisible};