Files
safetynet/packages/core/credential-policy.mjs
T

15 lines
1.7 KiB
JavaScript
Raw Normal View History

import {z} from 'zod';
export function credentialActionUrlAllowed(value){
try {const u=new URL(value);return u.protocol==='https:'&&!u.username&&!u.password&&!u.search&&!u.hash&&!value.includes('?')&&!value.includes('#')&&u.href===value&&!/[\u0000-\u0020\u007f]/.test(value);}catch{return false;}
}
const field=z.string().min(1).max(128).regex(/^[\x21-\x7e]+$/,'Credential field names require bounded printable ASCII');
export const CredentialSubmissionFormSchema=z.object({
id:z.string().min(1).max(80).regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/),
actionUrl:z.string().max(4096).refine(credentialActionUrlAllowed,'An exact canonical HTTPS action URL without credentials, query or fragment is required'),
usernameField:field,passwordField:field
}).strict().superRefine((rule,context)=>{if(rule.usernameField===rule.passwordField)context.addIssue({code:'custom',path:['passwordField'],message:'Username and password fields must differ'});});
export const ControlledSubmissionSchema=z.object({enabled:z.boolean().default(false),forms:z.array(CredentialSubmissionFormSchema).max(32).default([])}).strict().superRefine((value,context)=>{
const ids=new Set(),targets=new Set();value.forms.forEach((rule,index)=>{if(ids.has(rule.id))context.addIssue({code:'custom',path:['forms',index,'id'],message:'Credential rule identifiers must be unique'});ids.add(rule.id);const signature=JSON.stringify([rule.actionUrl,rule.usernameField,rule.passwordField]);if(targets.has(signature))context.addIssue({code:'custom',path:['forms',index],message:'Duplicate credential target and field signature'});targets.add(signature);});
if(value.enabled&&!value.forms.length)context.addIssue({code:'custom',path:['forms'],message:'Enablement requires at least one approved login POST target'});
}).prefault({});