48 lines
3.8 KiB
JavaScript
48 lines
3.8 KiB
JavaScript
import {z} from 'zod';
|
|||
|
|
import {SETTINGS_SECTIONS} from './managed-settings.mjs';
|
||
|
|
import {BookmarkItemsSchema} from './bookmarks.mjs';
|
||
|
|
|
||
|
|
const id=z.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
||
|
|
const unique=(schema,max)=>z.array(schema).max(max).refine(items=>new Set(items).size===items.length,'Values must be unique');
|
||
|
|
const host=z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
|
||
|
|
export const GroupRestrictionsSchema=z.object({
|
||
|
|
blockedHosts:unique(host,200).default([]),blockedPhrases:unique(z.string().min(1).max(4096),500).default([]),
|
||
|
|
disableDownloads:z.boolean().default(false),disableAI:z.boolean().default(false),disableAgents:z.boolean().default(false),
|
||
|
|
hideSettings:z.boolean().default(false),requireDocumentScan:z.boolean().default(false),
|
||
|
|
hiddenSections:unique(z.enum(SETTINGS_SECTIONS),SETTINGS_SECTIONS.length).default([]),
|
||
|
|
lockedSections:unique(z.enum(SETTINGS_SECTIONS),SETTINGS_SECTIONS.length).default([]),
|
||
|
|
managedBookmarks:BookmarkItemsSchema.default([])
|
||
|
|
}).strict();
|
||
|
|
export const PolicyGroupSchema=z.object({id,name:z.string().min(1).max(100).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)),
|
||
|
|
memberDeviceIds:unique(id,10000),restrictions:GroupRestrictionsSchema,
|
||
|
|
rollout:z.object({stage:z.enum(['pilot','expanded','all']),deviceIds:unique(id,10000).default([])}).strict()
|
||
|
|
}).strict().superRefine((group,ctx)=>{
|
||
|
|
if(group.rollout.stage==='all'&&group.rollout.deviceIds.length)ctx.addIssue({code:'custom',message:'All-members rollout requires empty cohort'});
|
||
|
|
if(group.rollout.stage!=='all'&&(!group.rollout.deviceIds.length||group.rollout.deviceIds.some(i=>!group.memberDeviceIds.includes(i))))ctx.addIssue({code:'custom',message:'Pilot and expanded cohorts must be nonempty subsets of members'});
|
||
|
|
if(Buffer.byteLength(JSON.stringify(group))>250000)ctx.addIssue({code:'custom',message:'Group exceeds the 250 KB feature budget'});
|
||
|
|
});
|
||
|
|
export const GroupAssignmentSchema=z.object({deviceId:id,groups:unique(id,100),profiles:unique(id,100).optional()}).strict();
|
||
|
|
export function groupApplies(group,deviceId){return group.memberDeviceIds.includes(deviceId)&&(group.rollout.stage==='all'||group.rollout.deviceIds.includes(deviceId));}
|
||
|
|
// The company baseline always survives. Boolean restrictions combine with OR;
|
||
|
|
// block lists and hidden sections combine with a stable union, never replacement.
|
||
|
|
export function applyPolicyGroups(policy,groups,deviceId){
|
||
|
|
const result=structuredClone(policy),applied=groups.map(g=>PolicyGroupSchema.parse(g)).filter(g=>groupApplies(g,deviceId)).sort((a,b)=>a.id.localeCompare(b.id,'en'));
|
||
|
|
const union=(a,b)=>[...new Set([...a,...b])].sort();
|
||
|
|
for(const group of applied){const r=group.restrictions;
|
||
|
|
result.browser.blockedHosts=union(result.browser.blockedHosts,r.blockedHosts);
|
||
|
|
result.security.content.blockedPhrases=union(result.security.content.blockedPhrases,r.blockedPhrases);
|
||
|
|
if(r.disableDownloads)result.browser.allowDownloads=false;
|
||
|
|
if(r.disableAI)result.ai.enabled=false;
|
||
|
|
if(r.disableAgents)result.agents.enabled=false;
|
||
|
|
if(r.hideSettings)result.browser.settings.visible=false;
|
||
|
|
if(r.requireDocumentScan)result.inspection.documentAnalysisEnabled=true;
|
||
|
|
result.browser.settings.hiddenSections=union(result.browser.settings.hiddenSections,r.hiddenSections);
|
||
|
|
result.browser.settings.lockedSections=union(result.browser.settings.lockedSections,r.lockedSections);
|
||
|
|
for(const bookmark of r.managedBookmarks){const existing=result.browser.bookmarks.managed.find(b=>b.id===bookmark.id);
|
||
|
|
if(existing&&JSON.stringify(existing)!==JSON.stringify(bookmark))throw Error('Managed bookmark ID conflicts across policies');
|
||
|
|
if(!existing)result.browser.bookmarks.managed.push(bookmark);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
result.assignment={deviceId,groups:applied.map(g=>g.id),...(result.assignment?.profiles?{profiles:result.assignment.profiles}:{})};
|
||
|
|
return result;
|
||
|
|
}
|