Validators
A complete guide to the seven built-in validators. All seven run inside a single call to validatePassword(password, options) — you don't pick them individually; you tune their behavior through the flat options object.
Each validator is also exported standalone if you want to call it directly (handy for testing or for tree-shaking when you only need one).
How Validators Combine
validatePassword runs all seven checks unconditionally and reports the result in result.checks. To make a check effectively a no-op:
length,repetition: relax the threshold (minLength: 0— note0, not1, because the check is strictlength < minLength;maxRepeatedChars: 9999)characterTypes: leave the fourrequire*options off (the default)sequential,keyboardPattern,commonPassword: setcheckSequential,checkKeyboardPatterns, orcheckCommonPasswordstofalsepersonalInfo: omit thepersonalInfoarray (default), or pass an empty array
import { validatePassword } from '@sentinel-password/core'
const result = validatePassword('MyP@ssw0rd!', {
minLength: 12,
requireUppercase: true,
requireDigit: true,
requireSymbol: true,
personalInfo: ['user@example.com', 'Alex'],
})
console.log(result.checks)
// { length: false, characterTypes: true, repetition: true, sequential: true,
// keyboardPattern: true, commonPassword: true, personalInfo: true }Available Validators
Length
Caps the password length on both sides.
Options:
| Option | Default | Effect |
|---|---|---|
minLength | 8 | Reject passwords shorter than this |
maxLength | 128 | Reject passwords longer than this |
Standalone:
import { validateLength } from '@sentinel-password/core'
validateLength('abc', { minLength: 8 })
// { passed: false, message: 'Password must be at least 8 characters' }Character Types
Enforces required character classes.
Options:
| Option | Default | Effect |
|---|---|---|
requireUppercase | false | Require ≥1 uppercase letter |
requireLowercase | false | Require ≥1 lowercase letter |
requireDigit | false | Require ≥1 digit |
requireSymbol | false | Require ≥1 symbol |
Standalone:
import { validateCharacterTypes, hasUppercase, hasDigit } from '@sentinel-password/core'
validateCharacterTypes('alllower1', { requireUppercase: true })
// { passed: false, message: 'Password must contain at least one uppercase letter' }
hasUppercase('Hi') // true
hasDigit('hi') // falseRepetition
Rejects long runs of the same character (aaaa, 1111).
Options:
| Option | Default | Effect |
|---|---|---|
maxRepeatedChars | 3 | Max allowed identical consecutive characters |
Standalone:
import { validateRepetition } from '@sentinel-password/core'
validateRepetition('Paaaaass1!', { maxRepeatedChars: 3 })
// { passed: false, message: 'Password contains too many repeated characters' }Sequential
Detects three or more characters whose charCodeAt values are consecutive (ascending or descending) — not just within a single character class. Source uses String.prototype.charCodeAt deltas (sequential.ts), which return UTF-16 code units. For the Basic Multilingual Plane (U+0000–U+FFFF) — every character you'd see in a typical password, including ASCII, Latin extensions, Cyrillic, Greek, and CJK — code units and Unicode code points are identical, so the practical effect is "three consecutive code points." Supplementary-plane characters (emoji, etc.) are encoded as surrogate pairs, so a code-point-level run like 😀😁😂 does not trigger the check.
The detector matches more than the obvious cases:
- Within letters:
abc,xyz,ABC(and reverse:cba,zyx) - Within digits:
123,987 - Within symbols:
!"#(!is 33,"is 34,#is 35),,-.(44-46),/01(47-49) - Across character classes:
9:;(digit → punctuation, codes 57-59),Z[\(letter → symbol, codes 90-92)
If a password is rejected for "sequential characters" and you don't see an obvious abc/123, scan for any three adjacent characters whose charCodeAt values increment or decrement by 1 — they're often inside punctuation runs (-., ./, !"#).
Overlap with keyboard-pattern
The numeric runs 123, 456, 789 (and their reverses 321, 654, 987) are also matched by the Keyboard Pattern validator's numeric-keypad list. Setting checkSequential: false alone will not allow a password like password123 through — checkKeyboardPatterns (default true) still catches the 123 substring. To accept those numeric runs you must disable both flags: { checkSequential: false, checkKeyboardPatterns: false }. The two validators were designed as independent defences (one catches arbitrary code-point runs, the other catches keyboard-locality runs), so this overlap is by design — but it can be surprising when you're trying to isolate behaviour.
Options:
| Option | Default | Effect |
|---|---|---|
checkSequential | true | Disable with false to allow sequences |
Standalone:
import { validateSequential } from '@sentinel-password/core'
validateSequential('abc1xyz!')
// { passed: false, message: 'Password contains sequential characters (e.g., abc, 123)' }Keyboard Pattern
Catches runs along common keyboard layouts (qwerty, asdfgh, zxcvbn) plus the numeric row (1234567890) and numeric-keypad rows/columns (789, 456, 123, 741, 852, 963). Supports QWERTY, AZERTY, QWERTZ, Dvorak, Colemak, and Cyrillic layouts. The shifted symbol row (!@#$%…) is not in the pattern set today — only unshifted runs are detected.
Overlap with sequential
The numeric-keypad rows 123, 456, 789 (and their reverses) are also caught by the Sequential validator's charCodeAt-consecutive check — Sequential catches them as code-point runs, Keyboard Pattern catches them as numeric-keypad substrings. The two validators are independent gates: checkKeyboardPatterns: false alone won't allow password123 through because checkSequential (default true) still rejects it. Disable both (checkSequential: false, checkKeyboardPatterns: false) to accept simple numeric runs.
Options:
| Option | Default | Effect |
|---|---|---|
checkKeyboardPatterns | true | Disable with false to allow keyboard patterns |
Standalone:
import { validateKeyboardPattern } from '@sentinel-password/core'
validateKeyboardPattern('qwerty123!')
// { passed: false, message: 'Password contains common keyboard patterns' }Common Password
Looks the password up in a precomputed Bloom filter of the top 1,000 common passwords. O(1) lookup, no network calls.
Bloom filter tradeoff
A Bloom filter is space-efficient (~1.5 KB here, vs ~8 KB for the raw list) but probabilistic in one direction: no false negatives (every password in the top-1,000 list is rejected) and a small false-positive rate of ~0.84% — about 1 in 119 passwords not in the list may still be flagged as "common." This is by design and documented in common-password.ts. If you have a use case that needs exact-match rejection (e.g., a curated wordlist with no near-collisions), do that lookup yourself outside the validator.
Options:
| Option | Default | Effect |
|---|---|---|
checkCommonPasswords | true | Disable with false to skip the lookup |
Standalone:
import { validateCommonPassword } from '@sentinel-password/core'
validateCommonPassword('password')
// { passed: false, message: 'Password is too common. Please choose a more unique password.' }Personal Info
Rejects passwords that contain any of the supplied identifiers as a case-insensitive substring. Pass user-identifying strings — name, username, email — that the password should not include.
Emails are reduced to the local part
Any value containing @ is treated as an email and only the part before @ is matched. personalInfo: ['john.doe@example.com'] is effectively personalInfo: ['john.doe'] — the domain (example.com) is not checked. If you want to reject passwords containing your company domain, pass it as a separate string (e.g. ['john.doe@example.com', 'example']).
Identifiers shorter than 3 characters are also ignored to avoid false positives.
Options:
| Option | Default | Effect |
|---|---|---|
personalInfo | undefined | Array of strings the password must not contain (substring match, case-insensitive; emails reduced to local part) |
Standalone:
import { validatePersonalInfo } from '@sentinel-password/core'
validatePersonalInfo('john1234!', { personalInfo: ['john@example.com', 'John', 'Doe'] })
// { passed: false, message: 'Password contains personal information' }TIP
Pass personalInfo whenever you have user context (signup form, profile update). It's a cheap, high-signal check — substring match catches JohnDoe2024! for [ "John", "Doe" ].
Strict Policy Example
A common "high security" preset:
import { validatePassword } from '@sentinel-password/core'
const result = validatePassword(password, {
minLength: 12,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireDigit: true,
requireSymbol: true,
maxRepeatedChars: 2,
// checkSequential, checkKeyboardPatterns, checkCommonPasswords default to true
personalInfo: [user.email, user.firstName, user.lastName].filter(Boolean),
})