/* Typed convenience editor; backend schema and independent review remain authority. */ (function(root){ function validateCredentialRuleDraft(rules){ if(!Array.isArray(rules)||rules.length>32)throw Error('Use at most 32 approved login POST rules'); const ids=new Set(),targets=new Set();return rules.map((rule,index)=>{ const label='Login rule '+(index+1);if(!rule||typeof rule!=='object'||Array.isArray(rule)||Object.keys(rule).sort().join(',')!=='actionUrl,id,passwordField,usernameField')throw Error(label+' must contain only rule ID, action URL and credential field names'); if(typeof rule.id!=='string'||rule.id.length>80||!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(rule.id)||ids.has(rule.id))throw Error(label+' needs a unique valid identifier');ids.add(rule.id); // eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input. let url;try{url=new URL(rule.actionUrl);}catch{throw Error(label+' needs an exact HTTPS action URL');}if(typeof rule.actionUrl!=='string'||rule.actionUrl.length>4096||url.protocol!=='https:'||url.username||url.password||url.search||url.hash||rule.actionUrl.includes('?')||rule.actionUrl.includes('#')||url.href!==rule.actionUrl||/[\u0000-\u0020\u007f]/.test(rule.actionUrl))throw Error(label+' needs a canonical HTTPS URL without credentials, query or fragment'); for(const key of ['usernameField','passwordField'])if(typeof rule[key]!=='string'||!/^[\x21-\x7e]{1,128}$/.test(rule[key]))throw Error(label+' needs printable ASCII field names (maximum 128 characters)');if(rule.usernameField===rule.passwordField)throw Error(label+' needs different username and password fields');const target=JSON.stringify([rule.actionUrl,rule.usernameField,rule.passwordField]);if(targets.has(target))throw Error(label+' duplicates an approved target/field signature');targets.add(target);return {...rule}; }); } if(typeof module==='object'&&module.exports)module.exports={validateCredentialRuleDraft}; root.validateCredentialRuleDraft=validateCredentialRuleDraft;if(typeof document==='undefined')return; let rules=[],editable=false,invalidImport=false; const status=(message,error=false)=>{const box=$('vault-rule-status');box.textContent=message;box.className=error?'error':'muted';}; const changed=()=>{$('vault-submit-forms').value=JSON.stringify(rules,null,2);try{validateCredentialRuleDraft(rules);status('Login rule changes remain in this reviewed policy draft.');}catch(error){status(error.message,true);}try{updateDraftStatus();}catch{}}; function render(){const host=$('vault-rule-editor');host.replaceChildren();$('vault-rule-add').disabled=!editable||invalidImport||rules.length>=32;for(const [index,rule]of rules.entries()){const box=document.createElement('fieldset'),legend=document.createElement('legend');legend.textContent='Login rule '+(index+1);box.append(legend);const grid=document.createElement('div');grid.className='grid';for(const [key,label,max]of [['id','Rule identifier',80],['actionUrl','Exact HTTPS POST action URL',4096],['usernameField','Username field name',128],['passwordField','Password field name',128]]){const item=document.createElement('label');item.textContent=label;const field=document.createElement('input');field.value=rule[key]||'';field.maxLength=max;field.disabled=!editable||invalidImport;field.autocomplete='off';field.spellcheck=false;field.dataset.credentialRuleField=key;field.addEventListener('input',()=>{if(!editable||invalidImport)return;rule[key]=field.value;changed();});item.append(field);grid.append(item);}box.append(grid);const remove=document.createElement('button');remove.type='button';remove.className='secondary';remove.textContent='Remove login rule';remove.disabled=!editable||invalidImport;remove.addEventListener('click',()=>{if(!editable||invalidImport)return;rules.splice(index,1);render();changed();});box.append(remove);host.append(box);}} function importRules(){try{const value=JSON.parse($('vault-submit-forms').value||'[]');if(!Array.isArray(value)||value.length>32||value.some(rule=>!rule||typeof rule!=='object'||Array.isArray(rule)||Object.keys(rule).sort().join(',')!=='actionUrl,id,passwordField,usernameField'))throw Error('Advanced login rules must be an array of at most 32 metadata-only rules');invalidImport=false;rules=structuredClone(value);render();try{validateCredentialRuleDraft(rules);status('Only login target metadata is edited here; never enter credential values.');}catch(error){status(error.message,true);}}catch{invalidImport=true;render();status('Advanced login rule JSON is invalid; it has not been replaced or discarded. Fix it before using typed rows.',true);}} const priorFill=fill;fill=function(p){priorFill(p);importRules();}; const priorDraft=draft;draft=function(){const p=priorDraft();p.passwordManager.controlledSubmission.forms=validateCredentialRuleDraft(p.passwordManager.controlledSubmission.forms);if(p.passwordManager.controlledSubmission.enabled&&!p.passwordManager.controlledSubmission.forms.length)throw Error('Enablement requires at least one approved login POST target');return p;}; const priorPermissions=applyOperatorPermissions;applyOperatorPermissions=function(principal){priorPermissions(principal);editable=principal?.permissions?.includes(independentApprovalRequired?'policy.prepare':'policy.write')===true;render();}; $('vault-rule-add').addEventListener('click',()=>{if(!editable||invalidImport||rules.length>=32)return;rules.push({id:'',actionUrl:'',usernameField:'username',passwordField:'password'});render();changed();});$('vault-submit-forms').addEventListener('change',()=>{importRules();try{updateDraftStatus();}catch{}}); if(policy)importRules();if(currentPrincipal)applyOperatorPermissions(currentPrincipal); })(typeof globalThis==='object'?globalThis:this);