Skip to content

Conversation

@jeffycyang
Copy link

@jeffycyang jeffycyang commented Apr 23, 2025

By submitting a PR to this repository, you agree to the terms within the Auth0 Code of Conduct. Please see the contributing guidelines for how to create and submit a high-quality PR for this repo.

Description

This change adds backwards compatible TypeScript support so this can be used in TS project without having to manually craft a types file.

I tried to make minimal changes to tests and examples to avoid regressions. Changes were only in how they are imported/required due to the transpiled JS being in dist.

References

Closed Issue that the community should do this so I'm tried.

Testing

Automated:

  • npm test should pass
  • node examples/basic.js && node examples/custom-rules.js && node examples/default-rules.js should all run without fail

Manual:

Create a new node project and pack this package, ie

# clone, checkout branch, build, & pack
git clone https://github.com/jeffycyang/password-sheriff.git
cd password-sheriff
git checkout typescript-support
npm install
npm pack

# create a mock project & install tarball
cd ..
mkdir password-sheriff-test
cd password-sheriff-test
npm init -y
npm install <path_to_tarball>

# if using typescript in mock project
npm install typescript @types/node --save-dev
npx tsc --init
npm install <path_to_tarball>

Create some mocks, copying the examples

CommonJS
  • basic
var assert = require('assert');

var { PasswordPolicy, format } = require('password-sheriff');

// Create a length password policy
var lengthPolicy = new PasswordPolicy({length: {minLength: 6}});

// throws if password does not meet criteria
try {
  lengthPolicy.assert('hello');
  assert.ok(false);
} catch (e) {
  assert.ok(e);
}

// returns false if password does not meet rules
assert.equal(false, lengthPolicy.check('hello'));

// explains the policy
var explained = lengthPolicy.explain();

assert.equal(1, explained.length);

// Rule code for i18n
assert.equal('lengthAtLeast', explained[0].code);

assert.equal(
  'At least 6 characters in length',
  format(explained[0].message, explained[0].format)
);
  • custom rules
var assert = require('assert');

// Custom rules

// Let's create a custom rule named Foo. The rule will enforce that
// "foo" is present at least `count` times.
function FooRule() {
}

FooRule.prototype = {};

FooRule.prototype.validate = function (options) {
  if (!options) { throw new Error('options should be an object'); }
  if (typeof options.count !== 'number') { throw new Error('count should be Number'); }
  if (options.count !== (options.count | 0)) { throw new Error('count should be Integer'); }
};

FooRule.prototype.assert = function (options, password) {
  if (!password) { return false; }
  if (typeof password !== 'string') { throw new Error('password should be string'); }

  var count = options.count;
  var lastIndex = 0;

  while (count > 0 && lastIndex !== -1) {
    lastIndex = password.indexOf('foo', lastIndex + 1);
    count--;
  }

  if (lastIndex === -1) {
    return false;
  }

  return true;
};

FooRule.prototype.explain = function (options) {
  return {
    // identifier rule (to make i18n easier)
    code: 'foo',
    message: 'Foo should be present at least %d times.',
    format: [options.count]
  };
};

FooRule.prototype.missing = function (options, password) {
  var explain = this.explain();
  explain.verified = this.assert(options, password);
  return explain;
};

var { PasswordPolicy } = require('password-sheriff');

var fooOnlyPolicy = new PasswordPolicy({noFoo: {count: 3}}, {noFoo: new FooRule()});

assert.equal(true, fooOnlyPolicy.check('lalafooasdasdfooasddafooadsasd'));
assert.equal(false, fooOnlyPolicy.check('asd'));
  • default rules
var assert = require('assert');

var { PasswordPolicy } = require('password-sheriff');

/* The default Password Sheriff rules are:
 *  * length
 *  * contains
 *  * containsAtLeast
 *  * identicalChars
*/

/*
 * length
 *
 * Parameters:  minLength :: Integer
 *
 * Specify the minimum amount of characters a password must have using the
 * `minLength` argument.
 */
var lengthPolicy = new PasswordPolicy({length: {minLength: 3}});

assert.equal(false, lengthPolicy.check('f'));
assert.equal(false, lengthPolicy.check('fo'));
assert.equal(true, lengthPolicy.check('foo'));
assert.equal(true, lengthPolicy.check('foobar'));

/*
 * contains
 *
 * Parameters: expressions :: [Charset]
 *
 * Password should contain all of the charsets specified. There are
 * 4 predefined charsets: `upperCase`, `lowerCase`, `numbers` and
 * `specialCharacters` (`specialCharacters`are the ones defined in
 * OWASP Password Policy recommendation document).
 */

var { charsets } = require('password-sheriff');

// var lowerCase         = charsets.lowerCase;
// var specialCharacters = charsets.specialCharacters;
var upperCase         = charsets.upperCase;
var numbers           = charsets.numbers;


var containsPolicy = new PasswordPolicy({contains: {
  expressions: [upperCase, numbers]
}});

assert.equal(false, containsPolicy.check('foo'));
assert.equal(false, containsPolicy.check('Bar'));
assert.equal(true, containsPolicy.check('Bar9'));
assert.equal(true, containsPolicy.check('B9'));

/*
 * containsAtLeast
 *
 * Parameters: expressions :: [Charset], atLeast :: Integer
 *
 * Passwords should contain at least `atLeast` of a total of `expressions.length`
 * groups.
 */

var { charsets } = require('password-sheriff');

var lowerCase         = charsets.lowerCase;
// var specialCharacters = charsets.specialCharacters;
upperCase         = charsets.upperCase;
numbers           = charsets.numbers;

var containsAtLeastPolicy = new PasswordPolicy({
  containsAtLeast: {
    atLeast: 2,
    expressions: [ lowerCase, upperCase, numbers ]
  }
});

assert.equal(false, containsAtLeastPolicy.check('hello'));
assert.equal(false, containsAtLeastPolicy.check('387'));
assert.equal(true,  containsAtLeastPolicy.check('387hello'));
assert.equal(true,  containsAtLeastPolicy.check('HELLOhello'));
assert.equal(true,  containsAtLeastPolicy.check('HELLOhello123'));

/*
 * identicalChars
 *
 * Parameters: max :: Integer
 *
 * Passwords should not contain any character repeated continuously `max + 1` times.
 */
var identitcalCharsPolicy = new PasswordPolicy({
  identicalChars: {
    max: 3
  }
});

assert.equal(true, identitcalCharsPolicy.check('hello'));
assert.equal(true, identitcalCharsPolicy.check('hellol'));
assert.equal(true, identitcalCharsPolicy.check('helllo'));
assert.equal(false, identitcalCharsPolicy.check('hellllo'));
assert.equal(false, identitcalCharsPolicy.check('123333334'));
TypeScript
  • basic
import * as assert from 'assert';
import { PasswordPolicy, format, RuleDescription } from 'password-sheriff';

// Create a length password policy
const lengthPolicy = new PasswordPolicy({ length: { minLength: 6 } });

// throws if password does not meet criteria
try {
  lengthPolicy.assert('hello');
  assert.ok(false);
} catch (e: unknown) {
  assert.ok(e);
}

// returns false if password does not meet rules
assert.equal(false, lengthPolicy.check('hello'));

// explains the policy
const explained: RuleDescription[] = lengthPolicy.explain();

assert.equal(1, explained.length);

// Rule code for i18n
assert.equal('lengthAtLeast', explained[0].code);

assert.equal(
  'At least 6 characters in length',
  format(explained[0].message, explained[0].format)
);
  • custom rules
import * as assert from 'assert';
import { PasswordPolicy, RuleDescription } from 'password-sheriff';

interface FooRuleOptions {
  count: number;
}

class FooRule {
  validate(options: FooRuleOptions): void {
    if (!options) { throw new Error('options should be an object'); }
    if (typeof options.count !== 'number') { throw new Error('count should be Number'); }
    if (options.count !== (options.count | 0)) { throw new Error('count should be Integer'); }
  }

  assert(options: FooRuleOptions, password: string): boolean {
    if (!password) { return false; }
    if (typeof password !== 'string') { throw new Error('password should be string'); }

    let count = options.count;
    let lastIndex = 0;

    while (count > 0 && lastIndex !== -1) {
      lastIndex = password.indexOf('foo', lastIndex + 1);
      count--;
    }

    if (lastIndex === -1) {
      return false;
    }

    return true;
  }

  explain(options: FooRuleOptions): RuleDescription {
    return {
      code: 'foo',
      message: 'Foo should be present at least %d times.',
      format: [options.count]
    };
  }

  missing(options: FooRuleOptions, password: string): RuleDescription {
    const explain = this.explain(options);
    explain.verified = this.assert(options, password);
    return explain;
  }
}

const fooOnlyPolicy = new PasswordPolicy(
  { noFoo: { count: 3 } }, 
  { noFoo: new FooRule() }
);

assert.equal(true, fooOnlyPolicy.check('lalafooasdasdfooasddafooadsasd'));
assert.equal(false, fooOnlyPolicy.check('asd')); 
  • default rules
import * as assert from 'assert';
import { PasswordPolicy, charsets } from 'password-sheriff';

/* The default Password Sheriff rules are:
 *  * length
 *  * contains
 *  * containsAtLeast
 *  * identicalChars
*/

/*
 * length
 *
 * Parameters:  minLength :: Integer
 *
 * Specify the minimum amount of characters a password must have using the
 * `minLength` argument.
 */
const lengthPolicy = new PasswordPolicy({ length: { minLength: 3 } });

assert.equal(false, lengthPolicy.check('f'));
assert.equal(false, lengthPolicy.check('fo'));
assert.equal(true, lengthPolicy.check('foo'));
assert.equal(true, lengthPolicy.check('foobar'));

/*
 * contains
 *
 * Parameters: expressions :: [Charset]
 *
 * Password should contain all of the charsets specified. There are
 * 4 predefined charsets: `upperCase`, `lowerCase`, `numbers` and
 * `specialCharacters` (`specialCharacters`are the ones defined in
 * OWASP Password Policy recommendation document).
 */

const upperCase = charsets.upperCase;
const numbers = charsets.numbers;

const containsPolicy = new PasswordPolicy({
  contains: {
    expressions: [upperCase, numbers]
  }
});

assert.equal(false, containsPolicy.check('foo'));
assert.equal(false, containsPolicy.check('Bar'));
assert.equal(true, containsPolicy.check('Bar9'));
assert.equal(true, containsPolicy.check('B9'));

/*
 * containsAtLeast
 *
 * Parameters: expressions :: [Charset], atLeast :: Integer
 *
 * Passwords should contain at least `atLeast` of a total of `expressions.length`
 * groups.
 */

const lowerCase = charsets.lowerCase;

const containsAtLeastPolicy = new PasswordPolicy({
  containsAtLeast: {
    atLeast: 2,
    expressions: [lowerCase, upperCase, numbers]
  }
});

assert.equal(false, containsAtLeastPolicy.check('hello'));
assert.equal(false, containsAtLeastPolicy.check('387'));
assert.equal(true, containsAtLeastPolicy.check('387hello'));
assert.equal(true, containsAtLeastPolicy.check('HELLOhello'));
assert.equal(true, containsAtLeastPolicy.check('HELLOhello123'));

/*
 * identicalChars
 *
 * Parameters: max :: Integer
 *
 * Passwords should not contain any character repeated continuously `max + 1` times.
 */
const identitcalCharsPolicy = new PasswordPolicy({
  identicalChars: {
    max: 3
  }
});

assert.equal(true, identitcalCharsPolicy.check('hello'));
assert.equal(true, identitcalCharsPolicy.check('hellol'));
assert.equal(true, identitcalCharsPolicy.check('helllo'));
assert.equal(false, identitcalCharsPolicy.check('hellllo'));
assert.equal(false, identitcalCharsPolicy.check('123333334')); 
Details

Checklist

  • I have added documentation for new/changed functionality in this PR or in auth0.com/docs
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not the default branch

@jeffycyang
Copy link
Author

Alternatively, we could create a https://github.com/DefinitelyTyped/DefinitelyTyped for this, albeit less maintainable due to the risk of drift.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant