Files
safetynet/packages/core/user-profiles.mjs
T

27 lines
3.2 KiB
JavaScript
Raw Normal View History

2026-10-13 10:33:31 +02:00
import {z} from 'zod';
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(a=>new Set(a).size===a.length,'Duplicate values');
const host=z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
export const UserAccessSchema=z.object({delegatableFeatures:z.object({devTools:z.boolean().default(false)}).strict().prefault({})}).strict().prefault({});
export const DomainCategorySchema=z.object({id,name:z.string().min(1).max(100),hosts:unique(host,500)}).strict();
export const UserProfileSchema=z.object({id,name:z.string().min(1).max(100),subjects:unique(z.string().min(1).max(512),10000).default([]),deviceIds:unique(id,10000).default([]),allowedHosts:unique(host,500).default([]),blockedHosts:unique(host,500).default([]),blockedCategoryIds:unique(id,100).default([]),features:z.object({devTools:z.enum(['inherit','allow','deny']).default('inherit'),downloads:z.enum(['inherit','deny']).default('inherit'),ai:z.enum(['inherit','deny']).default('inherit'),agents:z.enum(['inherit','deny']).default('inherit')}).strict().prefault({}),requireDocumentScan:z.boolean().default(false)}).strict();
export const UserProfileConfigurationSchema=z.object({profiles:z.array(UserProfileSchema).max(100).default([]),categories:z.array(DomainCategorySchema).max(100).default([])}).strict();
export function profileApplies(profile,device){return profile.deviceIds.includes(device.id)||!!device.verified_subject&&profile.subjects.includes(device.verified_subject);}
const covers=(a,b)=>a===b||a.startsWith('*.')&&b.endsWith(a.slice(1));
export function applyUserProfiles(policy,configuration,device){
const result=structuredClone(policy),config=UserProfileConfigurationSchema.parse(configuration||{}),profiles=config.profiles.filter(p=>profileApplies(p,device)).sort((a,b)=>a.id.localeCompare(b.id));
const union=(a,b)=>[...new Set([...a,...b])].sort();
for(const p of profiles){
result.browser.blockedHosts=union(result.browser.blockedHosts,p.blockedHosts);
for(const categoryId of p.blockedCategoryIds){const category=config.categories.find(c=>c.id===categoryId);if(!category)throw Error('Unknown domain category');result.browser.blockedHosts=union(result.browser.blockedHosts,category.hosts);}
if(p.allowedHosts.length){const existing=result.browser.allowedHosts;result.browser.allowedHosts=existing.length?union(existing.filter(a=>p.allowedHosts.some(b=>covers(b,a))),p.allowedHosts.filter(b=>existing.some(a=>covers(a,b)))):p.allowedHosts;if(!result.browser.allowedHosts.length){result.browser.allowedHosts=['denied.invalid'];result.browser.blockedHosts=union(result.browser.blockedHosts,['denied.invalid']);}}
if(p.features.downloads==='deny')result.browser.allowDownloads=false;
if(p.features.ai==='deny')result.ai.enabled=false;
if(p.features.agents==='deny')result.agents.enabled=false;
if(p.requireDocumentScan)result.inspection.documentAnalysisEnabled=true;
}
if(profiles.some(p=>p.features.devTools==='deny'))result.browser.allowDevTools=false;
else if(result.userAccess.delegatableFeatures.devTools&&profiles.some(p=>p.features.devTools==='allow'))result.browser.allowDevTools=true;
result.assignment={deviceId:device.id,groups:[],...(profiles.length?{profiles:profiles.map(p=>p.id)}:{})};
return result;
}