import { z } from 'zod'; import { BookmarkSchema,BookmarkItemsSchema } from '../../packages/core/bookmarks.mjs'; import { PolicySchema,destinationAllowed } from '../../packages/core/policy.mjs'; import { settingsSectionAllowed } from '../../packages/core/managed-settings.mjs'; const mutation=z.discriminatedUnion('action',[ z.object({action:z.literal('add'),expectedRevision:z.number().int().nonnegative(),policyVersion:z.number().int().positive(),bookmark:BookmarkSchema}).strict(), z.object({action:z.literal('update'),expectedRevision:z.number().int().nonnegative(),policyVersion:z.number().int().positive(),bookmark:BookmarkSchema}).strict(), z.object({action:z.literal('delete'),expectedRevision:z.number().int().nonnegative(),policyVersion:z.number().int().positive(),id:BookmarkSchema.shape.id}).strict() ]); export function globalBookmarksValid(store,policy){ if(policy.browser.bookmarks.personal.revision||policy.browser.bookmarks.personal.items.length)return false; const managed=policy.browser.bookmarks.managed.map(b=>b.id); if(!managed.length)return true; // Let SQLite examine IDs without copying every employee's bookmark collection // into the management process on each company-policy publication. return !store.db.prepare(`SELECT 1 AS found FROM bookmarks AS b, json_each(b.items) AS item WHERE json_extract(item.value,'$.id') IN (${managed.map(()=>'?').join(',')}) LIMIT 1`).get(...managed); } export function installBookmarkRoutes(app,store,device,admin){ function active(id){return store.db.prepare('SELECT id FROM devices WHERE id=? AND revoked=0').get(id);} function commit(id,state,items,policy,actor,action){ policy=PolicySchema.parse(policy); store.db.exec('BEGIN IMMEDIATE'); try{ // Requests are synchronous: version and revision comparisons cannot interleave before BEGIN. store.db.prepare('INSERT INTO bookmarks VALUES(?,?,?) ON CONFLICT(device_id) DO UPDATE SET revision=excluded.revision,items=excluded.items').run(id,state.revision+1,JSON.stringify(items)); store.event(id,'bookmarks.changed',{actor,action,count:items.length,revision:state.revision+1,policyVersion:policy.version});store.db.exec('COMMIT'); }catch(error){store.db.exec('ROLLBACK');throw error;} return {revision:state.revision+1,items,version:policy.version}; } app.post('/api/device/bookmarks',device,(req,res)=>{ const b=mutation.parse(req.body),p=store.effectivePolicy(req.device.id),s=store.personalBookmarks(req.device.id),rules=p.browser.bookmarks; if(b.policyVersion!==p.version||b.expectedRevision!==s.revision)return res.status(409).json({error:'Bookmark or policy version conflict'}); if(Date.parse(p.expiresAt)<=Date.now()||!settingsSectionAllowed(p,'library')||!rules[{add:'allowUserAdd',update:'allowUserEdit',delete:'allowUserDelete'}[b.action]])return res.status(403).json({error:'Bookmarks are managed by your administrator'}); const id=b.bookmark?.id||b.id; if(rules.managed.some(i=>i.id===id))return res.status(403).json({error:'Company bookmarks cannot be modified'}); if(b.bookmark&&!destinationAllowed(b.bookmark.url,p))return res.status(403).json({error:'Bookmark destination blocked by policy'}); const index=s.items.findIndex(i=>i.id===id),items=structuredClone(s.items); if(b.action==='add'){if(index!==-1)return res.status(409).json({error:'Bookmark ID already exists'});items.push(b.bookmark);} else {if(index===-1)return res.status(404).json({error:'Bookmark not found'});if(b.action==='delete')items.splice(index,1);else items[index]=b.bookmark;} BookmarkItemsSchema.parse(items);res.json(commit(req.device.id,s,items,p,`device:${req.device.id}`,b.action)); }); app.get('/api/admin/devices/:id/bookmarks',admin,(req,res)=>{ if(!active(req.params.id))return res.status(404).json({error:'Active device not found'}); const p=store.effectivePolicy(req.params.id);if(!p.browser.bookmarks.adminReadUserBookmarks)return res.status(403).json({error:'Remote bookmark review disabled by policy'}); const s=store.personalBookmarks(req.params.id);store.event(req.params.id,'bookmarks.reviewed',{actor:req.principal.sub,count:s.items.length,revision:s.revision});res.json({...s,version:p.version}); }); app.put('/api/admin/devices/:id/bookmarks',admin,(req,res)=>{ const b=z.object({expectedRevision:z.number().int().nonnegative(),policyVersion:z.number().int().positive(),items:BookmarkItemsSchema}).strict().parse(req.body); if(!active(req.params.id))return res.status(404).json({error:'Active device not found'}); const p=store.effectivePolicy(req.params.id),s=store.personalBookmarks(req.params.id); if(!p.browser.bookmarks.adminReadUserBookmarks)return res.status(403).json({error:'Remote bookmark review disabled by policy'}); if(b.policyVersion!==p.version||b.expectedRevision!==s.revision)return res.status(409).json({error:'Bookmark or policy version conflict'}); if(b.items.some(i=>p.browser.bookmarks.managed.some(m=>m.id===i.id)))return res.status(400).json({error:'Personal and company bookmark IDs conflict'}); res.json(commit(req.params.id,s,b.items,p,req.principal.sub,'administrator-update')); }); }