import { HaveIBeenPwnedConfig } from './types' const textEncode = (text: string) => { try { return new TextEncoder().encode(text) } catch (error: any) { throw new Error(`No encoder found, ${error}`) } } const digestMessage = async (message: string) => { const data = textEncode(message) const hashBuffer = await crypto.subtle.digest('SHA-1', data) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray .map((b) => b.toString(16).padStart(2, '0')) .join('') .toUpperCase() } const pwnedUrl = 'https://api.pwnedpasswords.com/range/' const defaultNetworkErrorHandler = (_error: Error | Response): false => { return false } export default async ( password: string, { universalFetch, url = pwnedUrl, networkErrorHandler = defaultNetworkErrorHandler, }: HaveIBeenPwnedConfig, ) => { // we don't need to check passwords with a length smaller 2 which can happen for repeat matcher or if we don't have a fetch function if (!universalFetch || password.length < 2) { return null } const passwordHash = (await digestMessage(password)).toUpperCase() const range = passwordHash.slice(0, 5) const suffix = passwordHash.slice(5) const response = await universalFetch(`${url}${range}`, { method: 'GET', headers: { 'Add-Padding': 'true', }, }).catch((error: unknown) => { return networkErrorHandler(error as Error) }) if (typeof response === 'boolean') { return false } if (response.status >= 400) { return networkErrorHandler(response) } const result = await response.text() const resultArray = result.split('\r\n') return resultArray.find((entry: string) => { const passwordHasPart = entry.split(':')[0] return passwordHasPart === suffix }) }