99 */
1010
1111import { zxcvbn , zxcvbnOptions } from '@zxcvbn-ts/core' ;
12- import * as zxcvbnCommon from '@zxcvbn-ts/language-common' ;
13- import * as zxcvbnEn from '@zxcvbn-ts/language-en' ;
1412
1513const SCORE_CONFIG = [
1614 { label : 'Very weak' , barClass : 'bg-danger' , width : 20 } ,
@@ -21,13 +19,22 @@ const SCORE_CONFIG = [
2119] ;
2220
2321let zxcvbnReady = false ;
22+ let zxcvbnLoadPromise = null ;
2423
2524async function ensureZxcvbn ( ) {
2625 if ( zxcvbnReady ) return ;
26+ if ( ! zxcvbnLoadPromise ) {
27+ zxcvbnLoadPromise = Promise . all ( [
28+ import ( '@zxcvbn-ts/language-common' ) ,
29+ import ( '@zxcvbn-ts/language-en' ) ,
30+ ] ) ;
31+ }
32+ const [ zxcvbnCommon , zxcvbnEn ] = await zxcvbnLoadPromise ;
2733 zxcvbnOptions . setOptions ( {
2834 translations : zxcvbnEn . translations ,
2935 graphs : zxcvbnCommon . adjacencyGraphs ,
3036 dictionary : { ...zxcvbnCommon . dictionary , ...zxcvbnEn . dictionary } ,
37+ useLevenshteinDistance : true ,
3138 } ) ;
3239 zxcvbnReady = true ;
3340}
@@ -42,17 +49,36 @@ function debounce(fn, ms) {
4249
4350function getUserInputValues ( userInputFieldIds ) {
4451 if ( ! userInputFieldIds ) return [ ] ;
45- return userInputFieldIds
52+ const raw = userInputFieldIds
4653 . split ( ',' )
4754 . map ( sel => document . querySelector ( sel . trim ( ) ) ?. value )
4855 . filter ( Boolean ) ;
56+
57+ const parts = [ ] ;
58+ for ( const val of raw ) {
59+ parts . push ( val ) ;
60+ // Split email addresses: "john.doe@gmail.com" → "john.doe", "john", "doe", "gmail"
61+ if ( val . includes ( '@' ) ) {
62+ const [ local , domain ] = val . split ( '@' ) ;
63+ parts . push ( local ) ;
64+ parts . push ( ...local . split ( / [ . _ + \- ] / ) ) ;
65+ const domainBase = domain ?. split ( '.' ) [ 0 ] ;
66+ if ( domainBase ) parts . push ( domainBase ) ;
67+ }
68+ // Split on common word-separators: spaces, dots, underscores, hyphens
69+ parts . push ( ...val . split ( / [ \s . _ @ + \- ] + / ) ) ;
70+ }
71+ // Deduplicate and discard single-character fragments (too noisy)
72+ return [ ...new Set ( parts . filter ( s => s . length >= 2 ) ) ] ;
4973}
5074
51- function updateMeter ( container , score , feedback ) {
75+ function updateMeter ( container , score , feedback , crackTimesDisplay ) {
5276 const config = SCORE_CONFIG [ score ] ;
5377 const bar = container . querySelector ( '.password-strength-bar' ) ;
5478 const label = container . querySelector ( '.password-strength-label' ) ;
55- const feedbackEl = container . querySelector ( '.password-strength-feedback' ) ;
79+ const warningEl = container . querySelector ( '.password-strength-warning' ) ;
80+ const suggestionsEl = container . querySelector ( '.password-strength-suggestions' ) ;
81+ const crackTimeEl = container . querySelector ( '.password-strength-cracktime' ) ;
5682
5783 // Reset bar classes
5884 bar . className = 'progress-bar password-strength-bar ' + config . barClass ;
@@ -64,10 +90,25 @@ function updateMeter(container, score, feedback) {
6490
6591 label . textContent = config . label ;
6692
67- const suggestions = [ feedback . warning , ...( feedback . suggestions ?? [ ] ) ]
68- . filter ( Boolean )
69- . join ( ' ' ) ;
70- feedbackEl . textContent = suggestions ;
93+ // Warning: what's wrong (null for score >= 3)
94+ if ( warningEl ) {
95+ warningEl . textContent = feedback . warning ?? '' ;
96+ warningEl . classList . toggle ( 'd-none' , ! feedback . warning ) ;
97+ }
98+
99+ // Suggestions: how to improve
100+ if ( suggestionsEl ) {
101+ const tips = ( feedback . suggestions ?? [ ] ) . filter ( Boolean ) ;
102+ suggestionsEl . textContent = tips . join ( ' ' ) ;
103+ suggestionsEl . classList . toggle ( 'd-none' , tips . length === 0 ) ;
104+ }
105+
106+ // Crack time estimate (online throttled — most relevant for web app context)
107+ if ( crackTimeEl ) {
108+ const display = crackTimesDisplay ?. onlineThrottling100PerHour ;
109+ crackTimeEl . textContent = display ? `Time to crack: ${ display } ` : '' ;
110+ crackTimeEl . classList . toggle ( 'd-none' , ! display ) ;
111+ }
71112}
72113
73114function clearMeter ( container ) {
@@ -76,7 +117,12 @@ function clearMeter(container) {
76117 bar . style . width = '0' ;
77118 container . querySelector ( '.progress' ) . setAttribute ( 'aria-valuenow' , '0' ) ;
78119 container . querySelector ( '.password-strength-label' ) . textContent = '' ;
79- container . querySelector ( '.password-strength-feedback' ) . textContent = '' ;
120+ const warningEl = container . querySelector ( '.password-strength-warning' ) ;
121+ if ( warningEl ) { warningEl . textContent = '' ; warningEl . classList . add ( 'd-none' ) ; }
122+ const suggestionsEl = container . querySelector ( '.password-strength-suggestions' ) ;
123+ if ( suggestionsEl ) { suggestionsEl . textContent = '' ; suggestionsEl . classList . add ( 'd-none' ) ; }
124+ const crackTimeEl = container . querySelector ( '.password-strength-cracktime' ) ;
125+ if ( crackTimeEl ) { crackTimeEl . textContent = '' ; crackTimeEl . classList . add ( 'd-none' ) ; }
80126 container . querySelector ( '.password-hibp-warning' ) . classList . add ( 'd-none' ) ;
81127}
82128
@@ -132,7 +178,7 @@ function initMeter(container) {
132178 await ensureZxcvbn ( ) ;
133179 const userInputs = getUserInputValues ( userInputFieldIds ) ;
134180 const result = zxcvbn ( password , userInputs ) ;
135- updateMeter ( container , result . score , result . feedback ) ;
181+ updateMeter ( container , result . score , result . feedback , result . crackTimesDisplay ) ;
136182
137183 // Re-show HIBP warning if previously triggered and password unchanged
138184 const hibpEl = container . querySelector ( '.password-hibp-warning' ) ;
0 commit comments