Files

266 lines
41 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const $=id=>document.getElementById(id);let panel=false,vaultPanel=false,lastState,vaultState,vaultBusy=false,vaultRevision=0;
window.safetynet.onVaultHandoff(()=>{vaultRevision++;vaultPanel=false;$('vault').hidden=true;$('passwords').setAttribute('aria-expanded','false');clearVaultFields();$('vault-list').replaceChildren();});
let sidebarOpen=false,startPageOpen=false,overviewOpen=false,tabs=[],activeTab=null,groupFilter='',navigationRevision=0,pendingNavigation=null,lastLayoutKey='',lastTabsKey='',lastLibraryKey='',nativeTabsRevision=-1;
const preference=(key,fallback)=>{try{return JSON.parse(localStorage.getItem(`sbrowser.${key}`))??fallback;}catch{return fallback;}};
const remember=(key,value)=>{try{localStorage.setItem(`sbrowser.${key}`,JSON.stringify(value));}catch{}};
const safeURL=value=>{try{const u=new URL(value);if(!['https:','http:'].includes(u.protocol)||u.username||u.password)return null;u.search='';u.hash='';return u.href;}catch{return null;}};
const metadata=list=>Array.isArray(list)?list.slice(0,100).filter(v=>v&&safeURL(v.url)&&typeof v.title==='string').map(v=>({url:safeURL(v.url),title:v.title.slice(0,160)})):[];
// Company artwork is signed policy data, never a remote image or executable SVG.
function renderCompany(s){
const name=s.brand?.name==='SafetyNet'?'S Browser':s.brand?.name||'S Browser',logo=typeof s.brand?.logoDataUrl==='string'&&/^data:image\/png;base64,[A-Za-z0-9+/=]+$/.test(s.brand.logoDataUrl)&&s.brand.logoDataUrl.length<=180000?s.brand.logoDataUrl:null;
document.documentElement.style.setProperty('--company-accent',/^#[0-9a-f]{6}$/i.test(s.brand?.color||'')?s.brand.color:'var(--accent)');
const setLogo=(image)=>{image.hidden=!logo;if(logo)image.src=logo;else image.removeAttribute('src');image.alt=logo?`${name} logo`:'';};
setLogo($('start-company-logo'));$('start-brand-name').textContent=name;
const monogram=document.querySelector('.brand-monogram');monogram.replaceChildren();if(logo){const image=document.createElement('img');image.className='company-logo';setLogo(image);monogram.append(image);}else monogram.textContent=name.charAt(0).toUpperCase();
for(const id of ['welcome','settings','ai','vault']){const host=$(id);if(!host)continue;let mark=host.querySelector('.company-surface-brand');if(!mark){mark=document.createElement('div');mark.className='company-surface-brand';mark.append(document.createElement('img'),document.createElement('span'));host.prepend(mark);}setLogo(mark.firstElementChild);mark.firstElementChild.className='company-logo';mark.lastElementChild.textContent=name;}
const start=s.startPage||{};$('start-title').textContent=start.title||'Your space to explore.';$('start-subtitle').textContent=start.subtitle||'';$('start-subtitle').hidden=!start.subtitle;
$('company-news').hidden=!start.newsEnabled;const list=$('company-news-items');list.replaceChildren();
if(start.newsEnabled)for(const item of (Array.isArray(start.news)?start.news:[]).slice(0,12)){let url;try{url=new URL(item.url);if(url.protocol!=='https:'||url.username||url.password)continue;}catch{continue;}if(typeof item.title!=='string')continue;const card=button('',()=>navigateTab(url.href),'company-news-card'),title=document.createElement('strong');title.textContent=item.title.slice(0,240);card.append(title);if(item.publishedAt){const date=new Date(item.publishedAt);if(Number.isFinite(date.getTime())){const time=document.createElement('time');time.dateTime=date.toISOString();time.textContent=date.toLocaleDateString();card.append(time);}}list.append(card);}
$('company-news-empty').hidden=!!list.children.length;
}
let bookmarks=[],readingList=metadata(preference('reading-list',[])),groups=[];
function currentTab(){return tabs.find(t=>t.id===activeTab)||{id:null,url:'',title:'New Tab',group:'',startPage:true};}
async function updateChrome(){
document.body.classList.toggle('sidebar-open',sidebarOpen);if($('library'))$('library').hidden=!sidebarOpen;if($('start-page'))$('start-page').hidden=!startPageOpen||overviewOpen||!lastState?.ready;if($('overview'))$('overview').hidden=!overviewOpen||!lastState?.ready;
for(const id of ['bookmark','reading-add','share-url'])if($(id))$(id).disabled=!lastState?.ready||startPageOpen||overviewOpen||!currentTab().url||(id==='bookmark'&&(!sectionEditable('library')||!lastState?.bookmarks?.allowUserAdd))||(id==='reading-add'&&!sectionEditable('library'));
const height=Math.ceil($('chrome')?.getBoundingClientRect().bottom||104),layout={height,sidebarOpen,startPageOpen:startPageOpen&&!!lastState?.ready,overviewOpen:overviewOpen&&!!lastState?.ready,toolsOpen:!!($('tools-menu')&&!$('tools-menu').hidden)||!!$('bookmark-editor')?.open||!!$('bookmark-manager')?.open||!!$('agent-pairing')?.open},key=JSON.stringify(layout);document.documentElement.style.setProperty('--chrome-height',`${height}px`);if(window.safetynet.setChromeLayout&&key!==lastLayoutKey){lastLayoutKey=key;try{await call('setChromeLayout',layout);}catch(e){lastLayoutKey='';throw e;}}
}
async function navigateTab(value){
const token=++navigationRevision,tab=currentTab();if(!tab.id)throw Error('No managed tab is available');pendingNavigation={token,tabId:tab.id};overviewOpen=false;
try{const result=await call('navigate',{url:value,tabId:tab.id});if(token===navigationRevision)show(result);}
finally{if(pendingNavigation?.token===token)pendingNavigation=null;}
}
// Native WebContents own identity, history and lifetime. Switching never reloads a URL.
async function selectTab(id){if(!tabs.some(t=>t.id===id))return;navigationRevision++;overviewOpen=false;show(await call('activateTab',id));$('address').value=currentTab().startPage?'':currentTab().url;await updateChrome();}
async function newTab(){navigationRevision++;overviewOpen=false;show(await call('createTab',{group:groupFilter}));$('address').value=currentTab().startPage?'':currentTab().url;await updateChrome();$('address').focus();}
async function closeTab(id){if(!tabs.some(t=>t.id===id))return;navigationRevision++;const previous=activeTab;show(await call('closeTab',id));if(previous!==activeTab)$('address').value=currentTab().startPage?'':currentTab().url;await updateChrome();}
function button(text,fn,cls=''){const b=document.createElement('button');b.type='button';b.className=cls;b.textContent=text;b.onclick=task(fn);return b;}
function renderTabs(){
const strip=$('tab-strip');if(!strip)return;const key=JSON.stringify({tabs,activeTab,groupFilter,ready:!!lastState?.ready});if(key===lastTabsKey)return;lastTabsKey=key;strip.replaceChildren();for(const tab of tabs.filter(t=>!groupFilter||t.group===groupFilter)){
const el=document.createElement('div');el.className=`browser-tab${tab.id===activeTab?' active':''}`;el.setAttribute('role','tab');el.setAttribute('aria-selected',String(tab.id===activeTab));el.title=tab.url||'New Tab';
const select=button(tab.title||'New Tab',()=>selectTab(tab.id),'tab-label'),close=button('×',()=>closeTab(tab.id),'tab-close');close.setAttribute('aria-label',`Close ${tab.title}`);select.disabled=!lastState?.ready;close.disabled=!lastState?.ready;el.dataset.tabId=tab.id;el.append(select,close);strip.append(el);}
if($('overview-grid')){$('overview-grid').replaceChildren();for(const tab of tabs){const card=button('',()=>selectTab(tab.id),'overview-card');const title=document.createElement('strong'),url=document.createElement('span');title.textContent=tab.title;url.textContent=safeURL(tab.url)||'Favorites and a fresh start';card.append(title,url);$('overview-grid').append(card);}}
}
function renderLinks(id,items){if(!$(id))return;$(id).replaceChildren();for(const item of items){const b=button('',()=>navigateTab(item.url),'favorite-card');const initial=document.createElement('span'),label=document.createElement('strong');initial.className='favorite-icon';initial.textContent=new URL(item.url).hostname.charAt(0).toUpperCase();label.textContent=item.title||new URL(item.url).hostname;b.title=item.url;b.append(initial,label);const badge=document.createElement('small');badge.className='favorite-badge';badge.textContent=item.managed?'Company':'';b.append(badge);$(id).append(b);}}
let bookmarkEditingId=null;
async function openBookmarkEditor(item){
requireSection('library',true);const rules=lastState?.bookmarks;
if(item?.managed||!(item?rules?.allowUserEdit:rules?.allowUserAdd))throw Error('This bookmark is managed by your administrator');
bookmarkEditingId=item?.id||null;$('bookmark-editor-title').textContent=item?'Edit bookmark':'Add bookmark';
$('bookmark-title').value=(item?.title||lastState?.pageTitle||'New bookmark').slice(0,120);$('bookmark-url').value=item?.url||safeURL(lastState?.url)||'';$('bookmark-folder').value=item?.folder||'';
$('bookmark-editor-error').textContent='';$('bookmark-editor').showModal();await updateChrome();$('bookmark-title').focus();
}
async function mutateBookmark(action,bookmark,id){
const snapshot=lastState;const result=await call('mutateBookmarks',{action,...(bookmark?{bookmark}:{}),...(id?{id}:{}),expectedRevision:snapshot.bookmarks.personal.revision,policyVersion:snapshot.policyVersion});show({...snapshot,...result});
}
function renderBookmarkLibrary(){
const target=$('library-bookmarks');target.replaceChildren();
for(const [managed,label] of [[true,'Company bookmarks (managed)'],[false,'Your bookmarks']]){
const heading=document.createElement('p');heading.className='bookmark-section-label';heading.textContent=label;target.append(heading);
let folder=null;for(const item of bookmarks.filter(item=>!!item.managed===managed).sort((a,b)=>(a.folder||'').localeCompare(b.folder||''))){
if(item.folder&&item.folder!==folder){const group=document.createElement('small');group.textContent=item.folder;target.append(group);}folder=item.folder;
const row=document.createElement('div');row.className='bookmark-row';const link=button(item.title,()=>navigateTab(item.url),'bookmark-link');link.title=item.url;row.append(link);
if(!managed&&sectionEditable('library')){if(lastState?.bookmarks?.allowUserEdit){const edit=button('Edit',()=>openBookmarkEditor(item),'bookmark-action');edit.setAttribute('aria-label',`Edit ${item.title}`);row.append(edit);}if(lastState?.bookmarks?.allowUserDelete){const remove=button('Remove',()=>mutateBookmark('delete',null,item.id),'bookmark-action');remove.setAttribute('aria-label',`Remove ${item.title}`);row.append(remove);}}
target.append(row);
}
}
const legacy=metadata(preference('bookmarks',[]));
if(legacy.length&&sectionVisible('library')){const heading=document.createElement('p');heading.className='bookmark-section-label';heading.textContent='Previous local bookmarks';target.append(heading);const note=document.createElement('small');note.textContent='These existing links remain local. Add them to your bookmarks to sync them when company policy permits.';target.append(note);for(const item of legacy){const link=button(item.title,()=>navigateTab(item.url),'bookmark-link');link.title=item.url;target.append(link);}}
}
$('bookmark-cancel').onclick=()=>$('bookmark-editor').close();
$('bookmark-editor').addEventListener('close',()=>updateChrome().catch(()=>{}));
$('bookmark-form').onsubmit=async event=>{event.preventDefault();const submit=$('bookmark-save');submit.disabled=true;try{
requireSection('library',true);await mutateBookmark(bookmarkEditingId?'update':'add',{id:bookmarkEditingId||crypto.randomUUID(),title:$('bookmark-title').value,url:$('bookmark-url').value,folder:$('bookmark-folder').value});$('bookmark-editor').close();
}catch(e){$('bookmark-editor-error').textContent=e.message;}finally{submit.disabled=false;}};
function renderLibrary(){
const key=JSON.stringify({bookmarks,bookmarkPermissions:lastState?.bookmarks,libraryAI:lastState?.libraryAI,settings:lastState?.settings,readingList,groups,tabs,activeTab});if(key===lastLibraryKey)return;lastLibraryKey=key;
const favorites=sectionVisible('library')?bookmarks.slice(0,12):[];renderLinks('favorites-grid',favorites);renderLinks('library-favorites',favorites);renderBookmarkLibrary();if($('bookmark-manager')?.open){libraryProposal=null;$('manager-proposal').hidden=true;renderBookmarkManager();}renderLinks('library-reading',readingList);
if($('library-groups')){$('library-groups').replaceChildren();$('library-groups').append(button('All tabs',()=>{groupFilter='';renderTabs();},'group-item'));for(const group of groups){const row=document.createElement('div');row.className='group-manager-row';row.append(button(group,()=>{groupFilter=group;renderTabs();},'group-item'));if(sectionEditable('library')){const rename=button('Rename',async()=>{const label=$('group-name').value.trim();if(!label||label.length>40)throw Error('Enter a new group name, up to 40 characters');for(const tab of [...tabs].filter(t=>t.group===group))show(await call('groupTab',{id:tab.id,group:label}));groupFilter=label;renderTabs();});rename.setAttribute('aria-label',`Rename group ${group}`);const remove=button('Ungroup',async()=>{for(const tab of [...tabs].filter(t=>t.group===group))show(await call('groupTab',{id:tab.id,group:''}));groupFilter='';renderTabs();});remove.setAttribute('aria-label',`Ungroup tabs in ${group}`);row.append(rename,remove);}$('library-groups').append(row);}}
if($('favorites-empty'))$('favorites-empty').hidden=!!favorites.length;
if($('library-tabs')){$('library-tabs').replaceChildren();for(const tab of tabs)$('library-tabs').append(button(tab.title,()=>selectTab(tab.id),`library-tab${tab.id===activeTab?' active':''}`));}
renderLinks('recent-list',tabs.filter(t=>t.url).slice(-5).map(t=>({url:safeURL(t.url),title:t.title})).filter(t=>t.url));
}
const systemAppearance=window.matchMedia('(prefers-color-scheme: dark)');
const systemIsDark=()=>window.matchMedia('(prefers-color-scheme: dark)').matches;
const accentPresets={blue:{base:'#0969da',light:'#0969da',dark:'#75baff'},teal:{base:'#007d76',light:'#00736d',dark:'#6cdbca'},violet:{base:'#7042c1',light:'#7042c1',dark:'#c5a4ff'}};
function applyAppearance(){
// Appearance comes exclusively from the privileged process, including unlocked saved choices.
const appearance=lastState?.settings?.appearance||{mode:'system',accent:'blue',tabMode:'separate'},theme=appearance.mode==='system'?'auto':appearance.mode,mode=appearance.tabMode;
const effective=theme==='auto'?(systemIsDark()?'dark':'light'):theme,savedAccent=appearance.accent,accent=typeof savedAccent==='string'&&Object.hasOwn(accentPresets,savedAccent)?savedAccent:'blue';
document.documentElement.dataset.theme=theme;document.documentElement.dataset.effectiveTheme=effective;document.documentElement.dataset.accent=accent;document.documentElement.dataset.tabMode=mode==='compact'?'compact':'separate';
document.body.dataset.theme=effective;document.body.dataset.tabMode=document.documentElement.dataset.tabMode;document.documentElement.style.colorScheme=effective;
document.documentElement.style.setProperty('--accent',accentPresets[accent].base);document.documentElement.style.setProperty('--accent-text',accentPresets[accent][effective]);
if($('theme-mode'))$('theme-mode').value=theme;if($('tab-mode'))$('tab-mode').value=document.documentElement.dataset.tabMode;if($('accent-mode'))$('accent-mode').value=accent;
if($('theme-status'))$('theme-status').textContent=theme==='auto'?`System · ${effective==='dark'?'Dark':'Light'}. Changes follow your device.`:`${effective==='dark'?'Dark':'Light'} theme · browser workspace only.`;
updateChrome().catch(()=>{});
}
systemAppearance.addEventListener('change',()=>{if((!lastState?.settings||lastState.settings.appearance.mode==='system'))applyAppearance();});
function refreshSystemAppearance(){if((!lastState?.settings||lastState.settings.appearance.mode==='system')&&document.body.dataset.theme!==(systemIsDark()?'dark':'light'))applyAppearance();}
window.addEventListener('focus',refreshSystemAppearance);
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refreshSystemAppearance();});
// Some Windows compositor/theme transitions update the query without a change event.
setInterval(refreshSystemAppearance,1000);
// Hidden controls are removed from keyboard navigation as well as visual presentation.
// Native IPC repeats every check: DOM visibility is never the security boundary.
function sectionVisible(name){const settings=lastState?.settings;return !!settings?.available&&(settings.visible||['library','ai','passwords','documentScan','agents'].includes(name))&&!settings.hiddenSections.includes(name);}
function sectionEditable(name){return sectionVisible(name)&&(name!=='library'||lastState.settings.visible)&&!lastState.settings.lockedSections.includes(name);}
function requireSection(name,edit=false){if(!(edit?sectionEditable(name):sectionVisible(name)))throw Error('This action is managed by your administrator.');}
function applyManagedSettings(){
const settings=lastState?.settings,visible=!!settings?.available&&settings.visible;
$('tools-toggle').hidden=!visible;$('tools-toggle').disabled=!visible;
if(!visible){$('tools-menu').hidden=true;$('tools-toggle').setAttribute('aria-expanded','false');}
const sections={appearance:['appearance-section'],library:['bookmark','reading-add','sidebar-toggle','bookmark-manager-open','favorites-manage'],ai:['ai-toggle','sidebar'],passwords:['passwords'],documentScan:['inspect'],agents:['agents']};
for(const [name,ids] of Object.entries(sections))for(const id of ids){const el=$(id);if(!el)continue;el.hidden=!sectionVisible(name);if(!sectionVisible(name)||(['library','ai','passwords','documentScan','agents'].includes(name)&&!sectionEditable(name)))el.disabled=true;}
for(const id of ['theme-mode','accent-mode','tab-mode'])$(id).disabled=!sectionEditable('appearance');
for(const id of ['bookmark','reading-add','group-add','group-name'])if($(id))$(id).disabled=!sectionEditable('library')||!lastState?.ready||(id==='bookmark'&&!lastState?.bookmarks?.allowUserAdd);
$('send').disabled=!lastState?.aiEnabled||!sectionEditable('ai');$('prompt').disabled=!sectionEditable('ai');
for(const [quick,original,name,enabled] of [['quick-ai','ai-toggle','ai',lastState?.aiEnabled],['quick-passwords','passwords','passwords',lastState?.ready&&lastState?.passwordManagerEnabled!==false],['quick-inspect','inspect','documentScan',lastState?.documentScanEnabled],['quick-agents','agents','agents',lastState?.agentsEnabled]]){const el=$(quick);el.hidden=visible||!sectionVisible(name)||!enabled;el.disabled=!sectionEditable(name)||!enabled;el.setAttribute('aria-expanded',$(original).getAttribute('aria-expanded')||'false');}
$('settings-managed').hidden=!sectionVisible('appearance')||sectionEditable('appearance');
if(!sectionVisible('library')){if($('bookmark-manager')?.open)$('bookmark-manager').close();sidebarOpen=false;$('sidebar-toggle').setAttribute('aria-expanded','false');}
if(!sectionEditable('ai')&&panel)closeAI().catch(()=>{});
if(!sectionEditable('passwords')&&vaultPanel)closeVault().catch(()=>{});
if($('agent-pairing')?.open&&(!lastState?.ready||!lastState?.agentsEnabled||!sectionEditable('agents')))$('agent-pairing').close();
applyAppearance();
}
async function call(method,...args){const r=await window.safetynet[method](...args);if(!r.ok)throw Error(r.error);return r.data;}
function clearVaultFields(){for(const id of ['vault-master','vault-confirm','vault-password','vault-origin','vault-label','vault-username'])$(id).value='';}
async function closeVault(){vaultRevision++;vaultPanel=false;$('vault').hidden=true;$('passwords').setAttribute('aria-expanded','false');clearVaultFields();$('vault-list').replaceChildren();$('vault-add').open=false;await call('vaultLock');await call('toggleVault',false);}
async function closeAI(){panel=false;$('ai').hidden=true;for(const id of ['sidebar','ai-toggle'])$(id).setAttribute('aria-expanded','false');await call('toggleAI',false);}
async function openAI(){requireSection('ai',true);if(vaultPanel)await closeVault();panel=true;$('ai').hidden=false;for(const id of ['sidebar','ai-toggle'])$(id).setAttribute('aria-expanded','true');await call('toggleAI',true);}
function show(s){
// A delayed invoke response must not replace a newer tab switch or navigation event.
if(Number.isSafeInteger(s.tabsRevision)&&s.tabsRevision<nativeTabsRevision)return;
if(Array.isArray(s.tabs)){nativeTabsRevision=s.tabsRevision??nativeTabsRevision;tabs=s.tabs;groups=[...new Set(tabs.map(t=>t.group).filter(Boolean))];activeTab=s.activeTabId;startPageOpen=!!currentTab().startPage;if(groupFilter&&currentTab().group!==groupFilter)groupFilter=currentTab().group;}
document.body.classList.toggle('window-maximized',!!s.windowMaximized);
lastState=s;renderCompany(s);bookmarks=[...(s.bookmarks?.managed||[]).map(item=>({...item,managed:true})),...(s.bookmarks?.personal?.items||[])];if(!s.ready||!sectionEditable('library')||($('bookmark-editor').open&&!(bookmarkEditingId?s.bookmarks?.allowUserEdit:s.bookmarks?.allowUserAdd)))$('bookmark-editor').close();const brand=s.brand.name==='SafetyNet'?'S Browser':s.brand.name;$('brand').textContent=brand;document.title=brand;$('managed').textContent=s.ready?(s.loginCheckPending?'Login check required':`Managed · policy ${s.policyVersion}`):'Access paused';$('managed').parentElement.classList.toggle('paused',!s.ready);
$('welcome').hidden=s.ready;$('enrollment').hidden=s.enrolled;$('message').textContent=s.message;
if(document.activeElement!==$('address'))$('address').value=startPageOpen?'':s.url||'';
$('managed').title=s.message;$('address-form').querySelector('.address-lock').src=s.url?.startsWith('https:')?'icons/lock-keyhole.svg':'icons/globe.svg';
$('ai-toggle').disabled=!s.aiEnabled;$('sidebar').disabled=!s.ready;$('send').disabled=!s.aiEnabled;
$('agents').disabled=!s.agentsEnabled;$('inspect').disabled=!s.documentScanEnabled;$('passwords').disabled=!s.ready||s.passwordManagerEnabled===false;
$('inspect').title=s.documentScanEnabled?'Inspect a document locally':'Document analysis is disabled by company policy';
$('back').disabled=!s.ready||!currentTab().canGoBack;$('forward').disabled=!s.ready||!currentTab().canGoForward;$('reload').disabled=!s.ready||currentTab().startPage;$('address').disabled=!(s.canNavigate??s.ready);
if(vaultPanel&&s.vaultLocked===true&&vaultState?.locked!==true){vaultRevision++;clearVaultFields();$('vault-list').replaceChildren();$('vault-open').hidden=true;vaultState={...vaultState,locked:true};refreshVault().catch(()=>{});}
if(vaultPanel&&s.passwordManagerEnabled===false)closeVault().catch(()=>{});
if(!s.ready){if(panel)closeAI().catch(()=>{});if(vaultPanel)closeVault().catch(()=>{});clearVaultFields();}
document.documentElement.style.setProperty('--page-tint',/^#[a-f0-9]{6}$/i.test(s.pageColor||'')?s.pageColor:'#b8c7dd');
if($('start-greeting'))$('start-greeting').textContent='A fresh place to begin';
if($('sidebar-toggle'))$('sidebar-toggle').disabled=!s.ready;
if($('new-tab'))$('new-tab').disabled=!s.ready;if($('tab-overview'))$('tab-overview').disabled=!s.ready;
reconcileAgentPairing();applyManagedSettings();updateChrome().catch(()=>{});renderTabs();renderLibrary();
}
function task(fn){return async event=>{event?.preventDefault();$('error').textContent='';try{await fn();}catch(e){$('error').textContent=e.message;$('managed').textContent=e.message;}};}
function vaultTask(fn){return async event=>{event?.preventDefault();if(vaultBusy)return;vaultBusy=true;$('vault-error').textContent='';try{await fn();}catch{$('vault-error').textContent='The vault action could not be completed. Check your password and company policy.';}finally{vaultBusy=false;$('vault-master').value='';$('vault-confirm').value='';}};}
function input(){return {server:$('server').value,code:$('code').value};}
$('enroll').onclick=task(async()=>{$('enroll').disabled=true;try{show(await call('enroll',input()));$('code').value='';}finally{$('enroll').disabled=false;}});
$('sso').onclick=task(async()=>{$('sso').disabled=true;try{show(await call('sso',input()));}finally{$('sso').disabled=false;}});
$('address-form').onsubmit=task(async()=>navigateTab($('address').value));
for(const method of ['back','forward','reload'])$(method).onclick=task(()=>call(method));
$('ai-toggle').onclick=$('sidebar').onclick=task(async()=>{if(panel)await closeAI();else await openAI();});
$('ai-close').onclick=task(closeAI);
$('inspect').onclick=task(async()=>{
requireSection('documentScan',true);$('inspect').disabled=true;try{const result=await call('inspectFile');if(!result)return;await openAI();entry(`Inspection: ${result.name}\nDecision: ${result.decision}\nDetector: ${result.ruleId||'No match'}\nType: ${result.contentType}\nComplete coverage: ${result.coverage.complete}\n${result.model?`Model: ${result.model}`:''}\nAnalyzed locally. This result does not authorize a website upload.`);}finally{show(await call('state'));}
});
function entry(text){const empty=$('conversation').querySelector('.empty-chat');empty?.remove();const e=document.createElement('div');e.className='entry';e.textContent=text;$('conversation').append(e);e.scrollIntoView();}
// Pairing selects a signed central identity. It never creates a local grant or exposes tokens.
let agentPairingRevision=0,agentPairingFingerprint='';
function agentGrantFingerprint(){return JSON.stringify({mode:lastState?.agentIdentityMode,grants:approvedAgentGrants()});}
function reconcileAgentPairing(){if($('agent-pairing')?.open&&agentPairingFingerprint!==agentGrantFingerprint())$('agent-pairing').close();}
setInterval(reconcileAgentPairing,1000);
function approvedAgentGrants(){return (Array.isArray(lastState?.agentGrants)?lastState.agentGrants:[]).filter(g=>g&&typeof g.id==='string'&&typeof g.name==='string'&&Number.isSafeInteger(g.generation)&&g.generation>=1&&Date.parse(g.expiresAt)>Date.now());}
function agentPairingDialog(){
let dialog=$('agent-pairing');if(dialog)return dialog;
dialog=document.createElement('dialog');dialog.id='agent-pairing';dialog.setAttribute('aria-labelledby','agent-pairing-title');
const title=document.createElement('h2');title.id='agent-pairing-title';title.textContent='Connect a local agent';
const description=document.createElement('p');description.id='agent-pairing-description';dialog.setAttribute('aria-describedby',description.id);
const list=document.createElement('div');list.id='agent-grant-list';list.className='agent-grant-list';
const result=document.createElement('pre');result.id='agent-pairing-result';result.setAttribute('role','status');result.setAttribute('aria-live','polite');
const close=button('Close',()=>dialog.close(),'secondary');close.setAttribute('aria-label','Close agent connection');
dialog.append(title,description,list,result,close);document.body.append(dialog);
dialog.addEventListener('close',()=>{agentPairingRevision++;agentPairingFingerprint='';list.replaceChildren();result.textContent='';updateChrome().catch(()=>{});});
return dialog;
}
async function openAgentPairing(){
requireSection('agents',true);if(!lastState?.ready||!lastState?.agentsEnabled)throw Error('Local agent control is disabled by your administrator.');
const dialog=agentPairingDialog(),revision=++agentPairingRevision,mode=lastState.agentIdentityMode==='individual'?'individual':'legacy',list=$('agent-grant-list'),result=$('agent-pairing-result');list.replaceChildren();result.textContent='';
$('agent-pairing-description').textContent=mode==='individual'?'Choose an identity approved by your administrator. Each identity has its own permissions and encrypted pairing file.':'Shared legacy credential: all connected agents share the same permissions. Your administrator can require individual identities.';
const pair=async grant=>{
requireSection('agents',true);if(revision!==agentPairingRevision)return;
if(mode==='individual'&&!approvedAgentGrants().some(g=>g.id===grant.id&&g.generation===grant.generation))throw Error('This agent identity is no longer approved.');
const connection=await call('agentConnection',grant?.id);
requireSection('agents',true);if(revision!==agentPairingRevision||!dialog.open)return;
if(!lastState?.ready||!lastState?.agentsEnabled||(lastState.agentIdentityMode==='individual'?'individual':'legacy')!==mode||(grant&&!approvedAgentGrants().some(g=>g.id===grant.id&&g.generation===grant.generation)))throw Error('Agent permissions changed. Open the connection dialog again.');
result.textContent=`${grant?`Identity: ${grant.name}\n`:'Shared legacy credential\n'}Endpoint: ${connection.address}\nEncrypted pairing file: ${connection.credentialPath}\nUse the trusted local helper. Every action follows company policy.`;
};
if(mode==='individual'){
const grants=approvedAgentGrants();if(!grants.length){const empty=document.createElement('p');empty.textContent='No active agent identities are approved. Ask your administrator to publish an agent grant.';list.append(empty);}
for(const grant of grants){const choice=button('',()=>pair(grant),'agent-grant-choice'),name=document.createElement('strong'),detail=document.createElement('small');name.textContent=grant.name;detail.textContent=`Expires ${new Date(grant.expiresAt).toLocaleString()} · generation ${grant.generation}`;choice.append(name,detail);list.append(choice);}
}else list.append(button('Get shared legacy connection',()=>pair(null),'agent-grant-choice'));
agentPairingFingerprint=agentGrantFingerprint();dialog.showModal();await updateChrome();
}
$('agents').onclick=task(openAgentPairing);
$('ai-form').onsubmit=task(async()=>{const prompt=$('prompt').value;if(!prompt.trim())return;entry(`You: ${prompt}`);$('send').disabled=true;try{const result=await call('ai',prompt);entry(result.answer);$('prompt').value='';}catch(e){entry(e.message);}finally{$('send').disabled=!lastState?.aiEnabled;}});
async function refreshVault(){
const revision=++vaultRevision;const status=await call('vaultStatus');if(!vaultPanel||revision!==vaultRevision)return;const previous=vaultState;vaultState=status;
$('vault-access').hidden=!status.enabled||!status.locked&&status.exists;$('vault-open').hidden=!status.enabled||status.locked||!status.exists;
$('vault-access-submit').textContent=status.exists?'Unlock vault':'Create vault';$('vault-master').autocomplete=status.exists?'current-password':'new-password';
$('vault-confirm').hidden=status.exists;$('vault-confirm-label').hidden=status.exists;$('vault-confirm').required=!status.exists&&status.enabled;
$('master-label').textContent=status.exists?'Master password':'Choose a master password';
$('vault-description').textContent=!status.enabled?'Password storage is disabled by your administrator.':status.locked||!status.exists?'Your encrypted logins stay on this device. Unlock with your master password.':`Unlocked · locks after ${status.idleLockSeconds} seconds of inactivity.`;
$('vault-add').hidden=!status.allowSaving;$('vault-save-submit').disabled=!status.allowSaving;$('vault-generate').disabled=!status.allowSaving;
if(!status.enabled||status.locked||!status.exists){if(!status.enabled||previous?.locked===false)clearVaultFields();$('vault-list').replaceChildren();return;}
const entries=await call('vaultList');if(!vaultPanel||revision!==vaultRevision)return;$('vault-list').replaceChildren();
if(!entries.length){const empty=document.createElement('div');empty.className='empty-vault';empty.textContent='No saved logins yet.';$('vault-list').append(empty);}
for(const item of entries){const card=document.createElement('div');card.className='login-card';const label=document.createElement('strong');label.textContent=item.label||item.origin;const origin=document.createElement('p');origin.textContent=item.origin;const username=document.createElement('p');username.textContent=item.username||'No username';const buttons=document.createElement('div');buttons.className='login-buttons';const fill=document.createElement('button');fill.textContent='Fill login';fill.disabled=!status.allowAutofill;fill.onclick=vaultTask(async()=>{await call('vaultFill',item.id);$('vault-error').textContent='Login filled. Submission remains subject to company DLP policy.';});const remove=document.createElement('button');remove.className='secondary';remove.textContent='Delete';remove.disabled=!status.allowSaving;remove.onclick=vaultTask(async()=>{await call('vaultDelete',item.id);await refreshVault();});const prepare=document.createElement('button');prepare.textContent='Prepare approved login';prepare.title='Fill and permit one approved login POST for up to 15 seconds. Submit on the website; other fields still pass company DLP.';prepare.hidden=!status.allowControlledSubmission;prepare.disabled=!status.allowAutofill;prepare.onclick=vaultTask(async()=>{await call('vaultPrepareSubmit',item.id);});buttons.append(fill,prepare,remove);card.append(label,origin,username,buttons);$('vault-list').append(card);}
}
$('passwords').onclick=vaultTask(async()=>{requireSection('passwords',true);if(vaultPanel){await closeVault();return;}await closeAI();vaultPanel=true;$('vault').hidden=false;$('passwords').setAttribute('aria-expanded','true');await call('toggleVault',true);await refreshVault();});
$('vault-close').onclick=vaultTask(closeVault);
$('vault-access').onsubmit=vaultTask(async()=>{const exists=vaultState?.exists,master=$('vault-master').value;if(!exists&&master!==$('vault-confirm').value){$('vault-error').textContent='The master passwords do not match.';return;}await call(exists?'vaultUnlock':'vaultCreate',master);$('vault-master').value='';$('vault-confirm').value='';await refreshVault();});
$('vault-lock').onclick=vaultTask(async()=>{vaultRevision++;clearVaultFields();$('vault-list').replaceChildren();await call('vaultLock');await refreshVault();});
$('vault-save').onsubmit=vaultTask(async()=>{await call('vaultSave',{origin:$('vault-origin').value,label:$('vault-label').value,username:$('vault-username').value,password:$('vault-password').value});clearVaultFields();$('vault-add').open=false;await refreshVault();});
$('vault-generate').onclick=vaultTask(async()=>{const generated=await call('vaultGenerate');$('vault-password').value=typeof generated==='string'?generated:generated.password;});
$('vault-add').ontoggle=()=>{if($('vault-add').open&&lastState?.url&&!$('vault-origin').value){try{$('vault-origin').value=new URL(lastState.url).origin;}catch{}}else if(!$('vault-add').open)$('vault-password').value='';};
window.addEventListener('blur',()=>{vaultRevision++;clearVaultFields();$('vault-list').replaceChildren();if(vaultPanel)call('vaultLock').then(refreshVault).catch(()=>{});});
window.addEventListener('pagehide',()=>{clearVaultFields();if(vaultPanel)call('vaultLock').catch(()=>{});});
setInterval(()=>{if(vaultPanel&&!vaultBusy)refreshVault().catch(()=>{clearVaultFields();$('vault-list').replaceChildren();});},5000);
function wire(id,handler){if($(id))$(id).onclick=task(handler);}
wire('sidebar-toggle',async()=>{requireSection('library');sidebarOpen=!sidebarOpen;$('sidebar-toggle').setAttribute('aria-expanded',String(sidebarOpen));await updateChrome();});
wire('new-tab',newTab);wire('tab-overview',async()=>{overviewOpen=!overviewOpen;startPageOpen=!!currentTab().startPage;$('tab-overview').setAttribute('aria-expanded',String(overviewOpen));renderTabs();await updateChrome();});
wire('overview-close',async()=>{overviewOpen=false;startPageOpen=!!currentTab().startPage;await updateChrome();});
for(const action of ['minimize','maximize','close'])wire(`window-${action}`,()=>call('windowControls',action));
wire('share-url',()=>call('shareURL'));
wire('bookmark',async()=>{requireSection('library',true);if(!lastState?.bookmarks?.allowUserAdd)throw Error('Adding bookmarks is disabled by your administrator');await openBookmarkEditor();});
wire('reading-add',async()=>{requireSection('library',true);await call('authorizeSetting','library');requireSection('library',true);if(!lastState?.ready)throw Error('Open an approved page first');const url=safeURL(lastState.url);if(!url)throw Error('This page cannot be saved');if(!readingList.some(b=>b.url===url)){readingList.push({url,title:String(lastState.pageTitle||new URL(url).hostname).slice(0,160)});readingList=readingList.slice(-100);remember('reading-list',readingList);renderLibrary();}});
wire('group-add',async()=>{requireSection('library',true);await call('authorizeSetting','library');requireSection('library',true);const field=$('group-name'),label=field?.value.trim();if(!label)throw Error('Enter a tab group name');if(label.length>40)throw Error('Tab group names are limited to 40 characters');if(!groups.includes(label)){if(groups.length>=20)throw Error('At most 20 tab groups are supported');groups.push(label);}show(await call('groupTab',{id:currentTab().id,group:label}));groupFilter=label;field.value='';renderLibrary();renderTabs();});
for(const [quick,original] of [['quick-ai','ai-toggle'],['quick-passwords','passwords'],['quick-inspect','inspect'],['quick-agents','agents']])wire(quick,()=>$(original).click());
wire('tools-toggle',async()=>{if(!lastState?.settings?.available||!lastState.settings.visible)throw Error('Settings are managed by your administrator.');const menu=$('tools-menu');if(menu){menu.hidden=!menu.hidden;$('tools-toggle').setAttribute('aria-expanded',String(!menu.hidden));await updateChrome();}});
for(const id of ['theme-mode','accent-mode','tab-mode'])$(id).onchange=task(async()=>{
requireSection('appearance',true);
try{const settings=await call('updateSettings',{mode:$('theme-mode').value==='auto'?'system':$('theme-mode').value,accent:$('accent-mode').value,tabMode:$('tab-mode').value});lastState={...lastState,settings};}
finally{applyManagedSettings();}
});
window.addEventListener('resize',()=>updateChrome().catch(()=>{}));
document.addEventListener('click',event=>{const menu=$('tools-menu');if(!menu||menu.hidden)return;if(event.target.closest('.menu-item')||(!event.target.closest('#tools-menu')&&!event.target.closest('#tools-toggle'))){menu.hidden=true;$('tools-toggle')?.setAttribute('aria-expanded','false');updateChrome().catch(()=>{});}});
window.addEventListener('keydown',event=>{if(event.ctrlKey&&event.key.toLowerCase()==='t'&&lastState?.ready){event.preventDefault();newTab().catch(()=>{});}if(event.key==='Escape'&&overviewOpen){overviewOpen=false;startPageOpen=!!currentTab().startPage;updateChrome().catch(()=>{});}});
applyAppearance();renderTabs();renderLibrary();
window.safetynet.onState(show);call('state').then(show).catch(e=>{$('error').textContent=e.message;});
let libraryProposal=null;
function renderBookmarkManager(){
const query=$('bookmark-search').value.trim().toLowerCase(),list=$('manager-list');list.replaceChildren();
const matches=bookmarks.filter(item=>[item.title,item.url,item.folder||''].some(value=>value.toLowerCase().includes(query)));
let section='';for(const item of matches){const key=`${item.managed?'Company':'Personal'} - ${item.folder||'Unfiled'}`;if(key!==section){const label=document.createElement('h3');label.className='manager-folder';label.textContent=key;list.append(label);section=key;}
const row=document.createElement('div');row.className='manager-row';row.dataset.bookmarkId=item.id;const link=button('',()=>navigateTab(item.url),'manager-link'),title=document.createElement('strong'),detail=document.createElement('small');title.textContent=item.title;detail.textContent=item.url;link.append(title,detail);row.append(link);
if(item.managed){const badge=document.createElement('span');badge.className='manager-managed';badge.textContent='Managed';row.append(badge);}else if(sectionEditable('library')){const actions=document.createElement('div');actions.className='manager-row-actions';if(lastState.bookmarks.allowUserEdit)actions.append(button('Edit / move',()=>openBookmarkEditor(item)));if(lastState.bookmarks.allowUserDelete)actions.append(button('Remove',async()=>{await mutateBookmark('delete',null,item.id);renderBookmarkManager();}));row.append(actions);}list.append(row);}
$('manager-empty').hidden=matches.length>0;$('manager-add').disabled=!sectionEditable('library')||!lastState?.bookmarks?.allowUserAdd;$('manager-ai').disabled=!sectionEditable('library')||!lastState?.libraryAI?.suggestionsEnabled||!!lastState?.libraryAI?.busy;
$('manager-policy').textContent=sectionEditable('library')?'Company bookmarks are read-only. Personal bookmarks follow company policy. AI organization shares titles and website domains with your approved model.':'Your administrator manages this library. Changes are disabled.';
}
async function openBookmarkManager(){requireSection('library');$('tools-menu').hidden=true;renderBookmarkManager();$('bookmark-manager').showModal();await updateChrome();$('bookmark-search').focus();}
for(const id of ['bookmark-manager-open','favorites-manage'])wire(id,openBookmarkManager);
$('bookmark-search').addEventListener('input',renderBookmarkManager);$('bookmark-manager-close').onclick=()=>$('bookmark-manager').close();$('bookmark-manager').addEventListener('close',()=>updateChrome().catch(()=>{}));wire('manager-add',()=>openBookmarkEditor());
wire('manager-ai',async()=>{requireSection('library',true);libraryProposal=await call('organizeLibrary');const diff=$('manager-diff');diff.replaceChildren();for(const [index,item] of (libraryProposal.plan.bookmarks||[]).entries()){const existing=bookmarks.find(b=>b.id===item.id);if(!existing||existing.managed)throw Error('Organization proposal no longer matches your library');const row=document.createElement('p');row.textContent=`${index+1}. ${existing.title}: ${existing.folder||'Unfiled'} -> ${item.folder||'Unfiled'}`;diff.append(row);}for(const item of libraryProposal.plan.tabs||[]){const existing=tabs.find(t=>t.id===item.id);if(!existing)throw Error('Organization proposal no longer matches your tabs');const row=document.createElement('p');row.textContent=`Tab ${existing.title}: ${existing.group||'Ungrouped'} -> ${item.group||'Ungrouped'}`;diff.append(row);}$('manager-proposal').hidden=false;});
wire('manager-apply',async()=>{if(!libraryProposal)throw Error('Request a fresh organization proposal');await call('applyLibraryOrganization',{proposalId:libraryProposal.proposalId,confirm:true});libraryProposal=null;$('manager-proposal').hidden=true;show(await call('state'));renderBookmarkManager();});$('manager-discard').onclick=()=>{libraryProposal=null;$('manager-proposal').hidden=true;};