snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
# @rocket.chat/password-policies
## 0.1.1
### Patch Changes
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
## 0.1.1-rc.0
### Patch Changes
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
## 0.1.0
### Minor Changes
- ([#37327](https://github.com/RocketChat/Rocket.Chat/pull/37327)) Adds complexity requirements to end-to-end encryption passphrase
## 0.1.0-rc.0
### Minor Changes
- ([#37327](https://github.com/RocketChat/Rocket.Chat/pull/37327)) Adds complexity requirements to end-to-end encryption passphrase
## 0.0.2
### Patch Changes
- ([#31138](https://github.com/RocketChat/Rocket.Chat/pull/31138)) feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo
## 0.0.2-rc.0
### Patch Changes
- b223cbde14: feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo

View File

@@ -0,0 +1,6 @@
import server from '@rocket.chat/jest-presets/server';
import type { Config } from 'jest';
export default {
preset: server.preset,
} satisfies Config;

View File

@@ -0,0 +1,30 @@
{
"name": "@rocket.chat/password-policies",
"version": "0.1.1",
"private": true,
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"build": "rm -rf dist && tsc",
"dev": "tsc --watch --preserveWatchOutput",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "jest",
"testunit": "jest",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/tsconfig": "workspace:*",
"@types/jest": "~30.0.0",
"eslint": "~9.39.5",
"jest": "~30.2.0",
"typescript": "~5.9.3"
},
"volta": {
"extends": "../../package.json"
}
}

View File

@@ -0,0 +1,219 @@
import { PasswordPolicy } from './PasswordPolicy';
describe('Password tests with default options', () => {
it('should allow all passwords', () => {
const passwordPolicy = new PasswordPolicy({ throwError: false });
expect(passwordPolicy.validate(null as any)).toBe(false);
expect(passwordPolicy.validate(undefined as any)).toBe(false);
expect(passwordPolicy.validate('')).toBe(false);
expect(passwordPolicy.validate(' ')).toBe(false);
expect(passwordPolicy.validate('a')).toBe(true);
expect(passwordPolicy.validate('aaaaaaaaa')).toBe(true);
});
});
describe('Password tests with options', () => {
it('should not allow non string or empty', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
throwError: false,
});
expect(passwordPolicy.validate(null as any)).toBe(false);
expect(passwordPolicy.validate(undefined as any)).toBe(false);
expect(passwordPolicy.validate(1 as any)).toBe(false);
expect(passwordPolicy.validate(true as any)).toBe(false);
expect(passwordPolicy.validate(new Date() as any)).toBe(false);
expect(passwordPolicy.validate(new Function() as any)).toBe(false);
expect(passwordPolicy.validate('')).toBe(false);
});
it('should restrict by minLength', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
minLength: 5,
throwError: false,
});
expect(passwordPolicy.validate('1')).toBe(false);
expect(passwordPolicy.validate('1234')).toBe(false);
expect(passwordPolicy.validate('12345')).toBe(true);
expect(passwordPolicy.validate(' ')).toBe(false);
});
it('should restrict by maxLength', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
maxLength: 5,
throwError: false,
});
expect(passwordPolicy.validate('1')).toBe(true);
expect(passwordPolicy.validate('12345')).toBe(true);
expect(passwordPolicy.validate('123456')).toBe(false);
expect(passwordPolicy.validate(' ')).toBe(false);
});
it('should allow repeated characters', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
forbidRepeatingCharacters: false,
throwError: false,
});
expect(passwordPolicy.validate('1')).toBe(true);
expect(passwordPolicy.validate('12345')).toBe(true);
expect(passwordPolicy.validate('123456')).toBe(true);
expect(passwordPolicy.validate(' ')).toBe(false);
expect(passwordPolicy.validate('11111111111111')).toBe(true);
});
it('should restrict repeated characters', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
forbidRepeatingCharacters: true,
forbidRepeatingCharactersCount: 3,
throwError: false,
});
expect(passwordPolicy.validate('1')).toBe(true);
expect(passwordPolicy.validate('11')).toBe(true);
expect(passwordPolicy.validate('111')).toBe(true);
expect(passwordPolicy.validate('1111')).toBe(false);
expect(passwordPolicy.validate(' ')).toBe(false);
expect(passwordPolicy.validate('123456')).toBe(true);
});
it('should restrict repeated characters customized', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
forbidRepeatingCharacters: true,
forbidRepeatingCharactersCount: 5,
throwError: false,
});
expect(passwordPolicy.validate('1')).toBe(true);
expect(passwordPolicy.validate('11')).toBe(true);
expect(passwordPolicy.validate('111')).toBe(true);
expect(passwordPolicy.validate('1111')).toBe(true);
expect(passwordPolicy.validate('11111')).toBe(true);
expect(passwordPolicy.validate('111111')).toBe(false);
expect(passwordPolicy.validate(' ')).toBe(false);
expect(passwordPolicy.validate('123456')).toBe(true);
});
it('should contain one lowercase', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
mustContainAtLeastOneLowercase: true,
throwError: false,
});
expect(passwordPolicy.validate('a')).toBe(true);
expect(passwordPolicy.validate('aa')).toBe(true);
expect(passwordPolicy.validate('A')).toBe(false);
expect(passwordPolicy.validate(' ')).toBe(false);
expect(passwordPolicy.validate('123456')).toBe(false);
expect(passwordPolicy.validate('AAAAA')).toBe(false);
expect(passwordPolicy.validate('AAAaAAA')).toBe(true);
});
it('should contain one uppercase', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
mustContainAtLeastOneUppercase: true,
throwError: false,
});
expect(passwordPolicy.validate('a')).toBe(false);
expect(passwordPolicy.validate('aa')).toBe(false);
expect(passwordPolicy.validate('A')).toBe(true);
expect(passwordPolicy.validate(' ')).toBe(false);
expect(passwordPolicy.validate('123456')).toBe(false);
expect(passwordPolicy.validate('AAAAA')).toBe(true);
expect(passwordPolicy.validate('AAAaAAA')).toBe(true);
});
it('should contain one number', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
mustContainAtLeastOneNumber: true,
throwError: false,
});
expect(passwordPolicy.validate('a')).toBe(false);
expect(passwordPolicy.validate('aa')).toBe(false);
expect(passwordPolicy.validate('A')).toBe(false);
expect(passwordPolicy.validate(' ')).toBe(false);
expect(passwordPolicy.validate('123456')).toBe(true);
expect(passwordPolicy.validate('AAAAA')).toBe(false);
expect(passwordPolicy.validate('AAAaAAA')).toBe(false);
expect(passwordPolicy.validate('AAAa1AAA')).toBe(true);
});
it('should contain one special character', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
mustContainAtLeastOneSpecialCharacter: true,
throwError: false,
});
expect(passwordPolicy.validate('a')).toBe(false);
expect(passwordPolicy.validate('aa')).toBe(false);
expect(passwordPolicy.validate('A')).toBe(false);
expect(passwordPolicy.validate(' ')).toBe(false);
expect(passwordPolicy.validate('123456')).toBe(false);
expect(passwordPolicy.validate('AAAAA')).toBe(false);
expect(passwordPolicy.validate('AAAaAAA')).toBe(false);
expect(passwordPolicy.validate('AAAa1AAA')).toBe(false);
expect(passwordPolicy.validate('AAAa@AAA')).toBe(true);
});
});
describe('Password Policy', () => {
it('should return a correct password policy', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
throwError: false,
minLength: 10,
maxLength: 20,
forbidRepeatingCharacters: true,
forbidRepeatingCharactersCount: 4,
mustContainAtLeastOneLowercase: true,
mustContainAtLeastOneUppercase: true,
mustContainAtLeastOneNumber: true,
mustContainAtLeastOneSpecialCharacter: true,
});
const policy = passwordPolicy.getPasswordPolicy();
expect(policy).not.toBe(undefined);
expect(policy.enabled).toBe(true);
expect(policy.policy.length).toBe(8);
expect(policy.policy[0][0]).toBe('get-password-policy-minLength');
expect(policy.policy[0][1]?.minLength).toBe(10);
});
it('should return correct values if policy is disabled', () => {
const passwordPolicy = new PasswordPolicy({
enabled: false,
});
const policy = passwordPolicy.getPasswordPolicy();
expect(policy.enabled).toBe(false);
expect(policy.policy.length).toBe(0);
});
it('should return correct values if policy is enabled but no specifiers exists', () => {
const passwordPolicy = new PasswordPolicy({
enabled: true,
});
const policy = passwordPolicy.getPasswordPolicy();
expect(policy.enabled).toBe(true);
// even when no policy is specified, forbidRepeatingCharactersCount is still configured
// since its default value is 3
expect(policy.policy.length).toBe(1);
});
});

View File

@@ -0,0 +1,287 @@
import { PasswordPolicyError } from './PasswordPolicyError';
type PasswordPolicyMap = {
minLength: number;
maxLength: number;
forbidRepeatingCharacters: boolean;
forbidRepeatingCharactersCount: number;
mustContainAtLeastOneLowercase: boolean;
mustContainAtLeastOneUppercase: boolean;
mustContainAtLeastOneNumber: boolean;
mustContainAtLeastOneSpecialCharacter: boolean;
};
type PasswordPolicyKey = keyof PasswordPolicyMap;
type PasswordPolicyName<K extends PasswordPolicyKey> = `get-password-policy-${K}`;
type PasswordPolicyParametersEntry = {
[K in PasswordPolicyKey]: PasswordPolicyMap[K] extends number
? [PasswordPolicyName<K>, Record<K, PasswordPolicyMap[K]>]
: [PasswordPolicyName<K>];
}[PasswordPolicyKey];
type PasswordPolicyType<Entry = PasswordPolicyParametersEntry> = {
enabled: boolean;
policy: Entry[];
};
export type PasswordPolicyOptions = Partial<
PasswordPolicyMap & {
enabled: boolean;
throwError: boolean;
}
>;
export type PasswordPolicyValidation = {
[K in PasswordPolicyKey]: PasswordPolicyMap[K] extends number
? { name: PasswordPolicyName<K>; limit: number }
: { name: PasswordPolicyName<K> };
}[PasswordPolicyKey] & { isValid: boolean };
export class PasswordPolicy {
private regex: {
forbiddingRepeatingCharacters: RegExp;
mustContainAtLeastOneLowercase: RegExp;
mustContainAtLeastOneUppercase: RegExp;
mustContainAtLeastOneNumber: RegExp;
mustContainAtLeastOneSpecialCharacter: RegExp;
};
private enabled: boolean;
private minLength: number;
private maxLength: number;
private forbidRepeatingCharacters: boolean;
private mustContainAtLeastOneLowercase: boolean;
private mustContainAtLeastOneUppercase: boolean;
private mustContainAtLeastOneNumber: boolean;
private mustContainAtLeastOneSpecialCharacter: boolean;
private throwError: boolean;
private forbidRepeatingCharactersCount: number;
constructor({
enabled = false,
minLength = -1,
maxLength = -1,
forbidRepeatingCharacters = false,
forbidRepeatingCharactersCount = 3, // the regex is this number minus one
mustContainAtLeastOneLowercase = false, // /[A-Z]{3,}/ could do this instead of at least one
mustContainAtLeastOneUppercase = false,
mustContainAtLeastOneNumber = false,
mustContainAtLeastOneSpecialCharacter = false,
throwError = true,
}: PasswordPolicyOptions) {
this.enabled = enabled;
this.minLength = minLength;
this.maxLength = maxLength;
this.forbidRepeatingCharacters = forbidRepeatingCharacters;
this.forbidRepeatingCharactersCount = forbidRepeatingCharactersCount;
this.mustContainAtLeastOneLowercase = mustContainAtLeastOneLowercase;
this.mustContainAtLeastOneUppercase = mustContainAtLeastOneUppercase;
this.mustContainAtLeastOneNumber = mustContainAtLeastOneNumber;
this.mustContainAtLeastOneSpecialCharacter = mustContainAtLeastOneSpecialCharacter;
this.throwError = throwError;
this.regex = {
forbiddingRepeatingCharacters: new RegExp(`(.)\\1{${forbidRepeatingCharactersCount},}`),
mustContainAtLeastOneLowercase: new RegExp('[a-z]'),
mustContainAtLeastOneUppercase: new RegExp('[A-Z]'),
mustContainAtLeastOneNumber: new RegExp('[0-9]'),
mustContainAtLeastOneSpecialCharacter: new RegExp('[^A-Za-z0-9 ]'),
};
}
error(
error: string,
message: string,
reasons?: {
error: string;
message: string;
}[],
) {
if (this.throwError) {
throw new PasswordPolicyError(message, error, reasons);
}
return false;
}
sendValidationMessage(password: string): PasswordPolicyValidation[] {
const validationReturn: PasswordPolicyValidation[] = [];
if (!this.enabled) {
return [];
}
if (this.minLength >= 1) {
validationReturn.push({
name: 'get-password-policy-minLength',
isValid: !(password.length < this.minLength),
limit: this.minLength,
});
}
if (this.maxLength >= 1) {
validationReturn.push({
name: 'get-password-policy-maxLength',
isValid: !(password.length > this.maxLength),
limit: this.maxLength,
});
}
if (this.forbidRepeatingCharacters) {
validationReturn.push({
name: 'get-password-policy-forbidRepeatingCharactersCount',
isValid: !this.regex.forbiddingRepeatingCharacters.test(password),
limit: this.forbidRepeatingCharactersCount,
});
}
if (this.mustContainAtLeastOneLowercase) {
validationReturn.push({
name: 'get-password-policy-mustContainAtLeastOneLowercase',
isValid: this.regex.mustContainAtLeastOneLowercase.test(password),
});
}
if (this.mustContainAtLeastOneUppercase) {
validationReturn.push({
name: 'get-password-policy-mustContainAtLeastOneUppercase',
isValid: this.regex.mustContainAtLeastOneUppercase.test(password),
});
}
if (this.mustContainAtLeastOneNumber) {
validationReturn.push({
name: 'get-password-policy-mustContainAtLeastOneNumber',
isValid: this.regex.mustContainAtLeastOneNumber.test(password),
});
}
if (this.mustContainAtLeastOneSpecialCharacter) {
validationReturn.push({
name: 'get-password-policy-mustContainAtLeastOneSpecialCharacter',
isValid: this.regex.mustContainAtLeastOneSpecialCharacter.test(password),
});
}
return validationReturn;
}
validate(password: string) {
const reasons: {
error: string;
message: string;
}[] = [];
if (typeof password !== 'string' || !password.trim().length) {
return this.error('error-password-policy-not-met', "The password provided does not meet the server's password policy.");
}
if (!this.enabled) {
return true;
}
if (this.minLength >= 1 && password.length < this.minLength) {
reasons.push({
error: 'error-password-policy-not-met-minLength',
message: 'The password does not meet the minimum length password policy.',
});
}
if (this.maxLength >= 1 && password.length > this.maxLength) {
reasons.push({
error: 'error-password-policy-not-met-maxLength',
message: 'The password does not meet the maximum length password policy.',
});
}
if (this.forbidRepeatingCharacters && this.regex.forbiddingRepeatingCharacters.test(password)) {
reasons.push({
error: 'error-password-policy-not-met-repeatingCharacters',
message: 'The password contains repeating characters which is against the password policy.',
});
}
if (this.mustContainAtLeastOneLowercase && !this.regex.mustContainAtLeastOneLowercase.test(password)) {
reasons.push({
error: 'error-password-policy-not-met-oneLowercase',
message: 'The password does not contain at least one lowercase character which is against the password policy.',
});
}
if (this.mustContainAtLeastOneUppercase && !this.regex.mustContainAtLeastOneUppercase.test(password)) {
reasons.push({
error: 'error-password-policy-not-met-oneUppercase',
message: 'The password does not contain at least one uppercase character which is against the password policy.',
});
}
if (this.mustContainAtLeastOneNumber && !this.regex.mustContainAtLeastOneNumber.test(password)) {
reasons.push({
error: 'error-password-policy-not-met-oneNumber',
message: 'The password does not contain at least one numerical character which is against the password policy.',
});
}
if (this.mustContainAtLeastOneSpecialCharacter && !this.regex.mustContainAtLeastOneSpecialCharacter.test(password)) {
reasons.push({
error: 'error-password-policy-not-met-oneSpecial',
message: 'The password does not contain at least one special character which is against the password policy.',
});
}
if (reasons.length) {
return this.error('error-password-policy-not-met', `The password provided does not meet the server's password policy.`, reasons);
}
return true;
}
getPasswordPolicy(): PasswordPolicyType<[name: string, params?: Record<string, number | boolean>]> {
const data: PasswordPolicyType = {
enabled: false,
policy: [],
};
if (this.enabled) {
data.enabled = true;
if (this.minLength >= 1) {
data.policy.push(['get-password-policy-minLength', { minLength: this.minLength }]);
}
if (this.maxLength >= 1) {
data.policy.push(['get-password-policy-maxLength', { maxLength: this.maxLength }]);
}
if (this.forbidRepeatingCharacters) {
data.policy.push(['get-password-policy-forbidRepeatingCharacters']);
}
if (this.forbidRepeatingCharactersCount) {
data.policy.push([
'get-password-policy-forbidRepeatingCharactersCount',
{ forbidRepeatingCharactersCount: this.forbidRepeatingCharactersCount },
]);
}
if (this.mustContainAtLeastOneLowercase) {
data.policy.push(['get-password-policy-mustContainAtLeastOneLowercase']);
}
if (this.mustContainAtLeastOneUppercase) {
data.policy.push(['get-password-policy-mustContainAtLeastOneUppercase']);
}
if (this.mustContainAtLeastOneNumber) {
data.policy.push(['get-password-policy-mustContainAtLeastOneNumber']);
}
if (this.mustContainAtLeastOneSpecialCharacter) {
data.policy.push(['get-password-policy-mustContainAtLeastOneSpecialCharacter']);
}
}
return data;
}
}

View File

@@ -0,0 +1,11 @@
export class PasswordPolicyError extends Error {
public error: string;
public details?: { error: string; message: string }[] | undefined;
constructor(message: string, error: string, details?: { error: string; message: string }[]) {
super(message);
this.error = error;
this.details = details;
}
}

View File

@@ -0,0 +1 @@
export { PasswordPolicy, type PasswordPolicyOptions, type PasswordPolicyValidation } from './PasswordPolicy';

View File

@@ -0,0 +1,8 @@
{
"extends": "@rocket.chat/tsconfig/client.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src/**/*"]
}