Skip to content

tsforge/ts-password-validator

Repository files navigation

ts-password-validator

npm version npm downloads total downloads bundle size types license CI

Русская версия

Entropy-based password validator for TypeScript. Pluggable character sets and pattern collapsers, no character-type mandates, no dictionaries, no network calls. Ships dual ESM + CJS with type declarations and runtime input validation via Zod.

The algorithm is a TypeScript port of wagslane/go-password-validator

  • same character pools, same sequence/repeat penalties, same log2(base^length) formula.

Install

npm install @tsforge7/ts-password-validator

Quick start

import { validatePassword } from '@tsforge7/ts-password-validator';

const result = validatePassword('Ololo_123!');
// {
//   valid: true,
//   entropy: 55.5,
//   base: 72,
//   length: 10,
//   effectiveLength: 9,
//   strength: 'fair',
//   minEntropy: 50,
//   message: 'Password entropy 55.5 bits meets the required 50 bits (strength: fair).'
// }

validatePassword always returns a full IValidationResult - boolean, entropy, strength label, and a ready-to-show message.

Custom threshold

import { validatePassword } from '@tsforge7/ts-password-validator';

validatePassword('Ololo_123!', { minEntropy: 70 });
// { valid: false, strength: 'fair', message: '... below the required 70 bits ...' }

Reuse a validator (hot path)

import { createPasswordValidator } from '@tsforge7/ts-password-validator';

const validator = createPasswordValidator({ minEntropy: 70 });

validator.check('Ololo_123!'); //    -> IValidationResult
validator.validate('Ololo_123!'); // -> boolean
validator.getEntropy('Ololo_123!'); // -> number
validator.details('Ololo_123!'); //   -> IEntropyDetails

Strength only

import {
  classifyStrength,
  getEntropyDetails,
  PASSWORD_STRENGTH,
} from '@tsforge7/ts-password-validator';

classifyStrength(42); // 'weak'
getEntropyDetails('xK#9!mLp_2');
// { entropy: 65.5, base: 94, length: 10, effectiveLength: 10 }

// Compare against the const object, not magic strings
if (classifyStrength(42) === PASSWORD_STRENGTH.weak) {
  // ...
}

PASSWORD_STRENGTH is an as const object - its values are exactly the strings in IValidationResult.strength, and TPasswordStrength is the derived type.

Backwards-compatible primitives

import {
  getPasswordEntropy,
  validatePasswordEntropy,
} from '@tsforge7/ts-password-validator';

getPasswordEntropy('Ololo_123!'); // 55.5 (bits)
validatePasswordEntropy('Ololo_123!'); // true

Customisation

createPasswordValidator accepts a partial options bag - pass only what you want to override.

import {
  createPasswordValidator,
  CharacterSet,
  RepeatCollapser,
  SequenceCollapser,
  PASSWORD_STRENGTH,
} from '@tsforge7/ts-password-validator';

const validator = createPasswordValidator({
  minEntropy: 60,
  characterSets: [
    new CharacterSet('абвгдеёжзийклмнопрстуфхцчшщъыьэюя'),
    new CharacterSet('АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'),
  ],
  sequences: ['абвгдеёжзийклмнопрстуфхцчшщъыьэюя'],
  strengthThresholds: [
    { entropy: 80, strength: PASSWORD_STRENGTH.veryStrong },
    { entropy: 60, strength: PASSWORD_STRENGTH.strong },
    { entropy: 40, strength: PASSWORD_STRENGTH.fair },
    { entropy: 20, strength: PASSWORD_STRENGTH.weak },
    { entropy: 0, strength: PASSWORD_STRENGTH.veryWeak },
  ],
});

validator.check('Прекрасный_Пароль42');
Option What it overrides Default
minEntropy Threshold for valid: true 50 bits
characterSets Character categories for the base DEFAULT_CHARACTER_SETS
sequences Sequences for the default collapser pipeline DEFAULT_SEQUENCES
collapsers Full collapser pipeline (wins over sequences) DEFAULT_COLLAPSERS
strengthThresholds Mapping from entropy to label DEFAULT_STRENGTH_THRESHOLDS

Runtime input validation

Every public function and every constructor of a publicly-instantiable class validates its inputs with Zod and throws a friendly TypeError if the contract is broken. You get the same safety nets when calling from JavaScript or with any-typed data.

validatePassword(123 as any);
// -> TypeError: Invalid password: password must be a string (got number).

validatePassword('x', { minEntropy: -1 });
// -> TypeError: Invalid options: minEntropy: Number must be greater than or equal to 0

validatePassword('x', { foo: 'bar' } as any);
// -> TypeError: Invalid options: Unrecognized key(s) in object: 'foo'

new CharacterSet('');
// -> TypeError: Invalid CharacterSet chars: CharacterSet chars must be a non-empty string

new RepeatCollapser(-1);
// -> TypeError: Invalid maxStreak: maxStreak must be a positive integer

The validatorOptionsSchema is .strict(), so typos in option keys are caught immediately rather than silently ignored.

Schemas live in src/commands/ as TypeScript namespaces that bundle the Zod schema with its derived TS type:

import { z } from 'zod';

export namespace PasswordCommand {
  export const schema = z.string({ ... });
  export type Type = z.infer<typeof schema>;
}

Available commands: PasswordCommand, CharacterSetCommand, CollapserCommand, StrengthCommand, ValidatorOptionsCommand. The assertSchema(schema, value, label) helper does the safeParse + TypeError wrap if you want to validate something yourself.

Result shape

interface IValidationResult {
  valid: boolean;
  entropy: number; // bits
  base: number; // pool size
  length: number; // raw code points
  effectiveLength: number; // after collapsing repeats/sequences
  strength: TPasswordStrength;
  minEntropy: number;
  message: string;
}

// PASSWORD_STRENGTH is the source of truth - TPasswordStrength is derived
const PASSWORD_STRENGTH = {
  veryWeak: 'very-weak', //   0 - 34 bits
  weak: 'weak', //  35 - 49 bits
  fair: 'fair', //  50 - 69 bits
  strong: 'strong', //  70 - 99 bits
  veryStrong: 'very-strong', // 100+ bits
} as const;

type TPasswordStrength =
  (typeof PASSWORD_STRENGTH)[keyof typeof PASSWORD_STRENGTH];

Algorithm

entropy(password) = effectiveLength · log2(base)

Base

Sum of the sizes of every character category present in the password, plus 1 for each unique character outside those categories (e.g. unicode letters).

Category Default characters Size
Replace !@$&* 5
Separator _-., (incl. space) 5
Other special ", #, %, ', (, ), +, /, :, ;, <, =, >, ?, [, \, ], ^, {, |, }, ~ 22
Lowercase a-z 26
Uppercase A-Z 26
Digits 0-9 10
Unknown (each) anything else (per unique character) +1

Maximum base with all six categories is 94.

Effective length

The length is reduced before the entropy calculation to avoid overestimating predictable patterns:

  • Repeats - runs of three or more identical characters count as two. aaaa -> effective aa.
  • Sequences - three or more consecutive characters from a known sequence count as two, checked case-insensitively in both directions. Default sequences: 0123456789, qwertyuiop, asdfghjkl, zxcvbnm, abcdefghijklmnopqrstuvwxyz. Example: qwerty -> qw, 9876 -> 98.

Default threshold

DEFAULT_MIN_ENTROPY = 50 bits. The upstream Go project recommends 50-70 depending on threat model.

Public API

Functions

Export Returns
validatePassword(password, options?) IValidationResult
createPasswordValidator(options?) PasswordValidator
getEntropyDetails(password) IEntropyDetails
classifyStrength(entropy, thresholds?) TPasswordStrength
getPasswordEntropy(password) number (bits)
validatePasswordEntropy(password) boolean
assertSchema(schema, value, label) parsed value

Classes

Export Purpose
PasswordValidator validate, check, getEntropy, details
PasswordEntropyCalculator calculate, details
BaseCalculator Character-pool size from a list of CharacterSets
EffectiveLengthCalculator Reduces length through a pipeline of IPatternCollapsers
CharacterSet A character pool with contains / size
RepeatCollapser aaa… -> aa
SequenceCollapser Forward/reverse sequence collapsing

Commands (Zod schemas + inferred types)

Namespace Members
PasswordCommand schema, Type
CharacterSetCommand charsSchema, Chars
CollapserCommand sequenceSchema, maxStreakSchema, patternSchema, Sequence, MaxStreak, Pattern
StrengthCommand passwordStrengthSchema, entropySchema, thresholdSchema, thresholdsSchema, Entropy, Threshold, Thresholds
ValidatorOptionsCommand schema, Type

Interfaces, types and constants

Export Kind
IValidationResult interface
IEntropyDetails interface
IPatternCollapser interface
TPasswordStrength type
PASSWORD_STRENGTH const
DEFAULT_CHARACTER_SETS const
DEFAULT_SEQUENCES const
DEFAULT_COLLAPSERS const
DEFAULT_MIN_ENTROPY const
DEFAULT_STRENGTH_THRESHOLDS const
defaultEntropyCalculator instance
defaultPasswordValidator instance

Output-only shapes (IValidationResult, IEntropyDetails, IPatternCollapser) live in src/interfaces/ with an I prefix. Input shapes that come with a runtime schema live in src/commands/ as namespaces. Top-level type aliases (e.g. TPasswordStrength) get a T prefix.

Project layout

src/
├── base-calculator.ts
├── character-set.ts
├── defaults.ts
├── effective-length-calculator.ts
├── factory.ts
├── index.ts                       # public entry
├── password-entropy-calculator.ts
├── password-entropy.ts
├── password-validator.ts
├── strength.ts
├── validate.ts
├── collapsers/
│   ├── index.ts
│   ├── repeat-collapser.ts
│   └── sequence-collapser.ts
├── commands/                      # Zod schemas + inferred types
│   ├── index.ts
│   ├── assert.ts
│   ├── character-set.command.ts
│   ├── collapser.command.ts
│   ├── password.command.ts
│   ├── strength.command.ts
│   └── validator-options.command.ts
├── constants/
│   ├── index.ts
│   ├── character-sets.constant.ts
│   ├── collapsers.constant.ts
│   ├── min-entropy.constant.ts
│   ├── password-strength.constant.ts
│   ├── sequences.constant.ts
│   └── strength-thresholds.constant.ts
└── interfaces/
    ├── index.ts
    ├── entropy-details.interface.ts
    ├── pattern-collapser.interface.ts
    └── validation-result.interface.ts

Imports across the codebase go through folder barrels - from './constants', from './interfaces', from './collapsers', from './commands' - never through a specific *.constant.ts / *.interface.ts / *.command.ts file from outside its folder.

Scripts

npm test          # vitest (86 tests covering logic + input validation)
npm run typecheck # tsc --noEmit
npm run lint      # eslint
npm run format    # prettier --write
npm run build     # tsup -> dist/index.js (ESM) + dist/index.cjs + types

The build (tsup + esbuild + rollup-plugin-dts) emits a flat dist/:

dist/
├── index.js       # ESM bundle
├── index.cjs      # CJS bundle
├── index.d.ts     # ESM types
├── index.d.cts    # CJS types
└── *.map          # sourcemaps

Stats

daily weekly monthly yearly total

For interactive download graphs, package comparisons and bundle-size analysis:

License

MIT © 2026 tsforge7

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors