ruvnet/ruflo · error · PasswordHashError

VALIDATION_FAILED

VALIDATION_FAILED

Error message

validation.errors.join('; ')

What it means

PasswordHasher.hash() runs its configured password policy through validate() before ever calling bcrypt. When the plaintext violates that policy, it throws a PasswordHashError with code VALIDATION_FAILED whose message is the policy failures joined by '; '. Defaults require 8-128 chars, at least one uppercase, one lowercase, and one digit.

Source

Thrown at v3/@claude-flow/security/src/password-hasher.ts:183

    return {
      isValid: errors.length === 0,
      errors,
    };
  }

  /**
   * Hashes a password using bcrypt.
   *
   * @param password - The plaintext password to hash
   * @returns The bcrypt hash
   * @throws PasswordHashError if password is invalid
   */
  async hash(password: string): Promise<string> {
    const validation = this.validate(password);

    if (!validation.isValid) {
      throw new PasswordHashError(
        validation.errors.join('; '),
        'VALIDATION_FAILED'
      );
    }

    try {
      // bcrypt automatically generates a random salt per hash
      return await bcrypt.hash(password, this.config.rounds);
    } catch (error) {
      throw new PasswordHashError(
        'Failed to hash password',
        'HASH_FAILED'
      );
    }
  }

  /**
   * Verifies a password against a bcrypt hash.

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Read the thrown message — it lists every failed rule (e.g. 'Password must contain at least one digit'), so fix the password to satisfy each named rule.
  2. Call hasher.validate(password) before hash() and return its errors array to the UI instead of relying on the throw.
  3. If your product policy intentionally differs, construct PasswordHasher with matching config (e.g. { requireDigit: false, minLength: 10 }) instead of mutating defaults after the fact.
  4. Keep client-side validation derived from the same PasswordHasherConfig so the two never diverge.

Example fix

// before
const hash = await hasher.hash('weak');
// throws PasswordHashError: Password must contain at least one uppercase letter; ...

// after
const check = hasher.validate('WeakPass1');
if (!check.isValid) throw new UserInputError(check.errors);
const hash = await hasher.hash('WeakPass1');
Defensive patterns

Strategy: validation

Validate before calling

const check = hasher.validate(password);
if (!check.isValid) {
  return badRequest(check.errors); // surface policy errors to the caller
}
const hash = await hasher.hash(password);

Type guard

function isPolicyCompliant(hasher: PasswordHasher, pw: string): boolean {
  return hasher.validate(pw).isValid;
}

Try / catch

try {
  const hash = await hasher.hash(password);
} catch (err) {
  if (err instanceof PasswordHashError && err.code === 'VALIDATION_FAILED') {
    return res.status(400).json({ errors: err.message.split('; ') });
  }
  throw err;
}

Prevention

When it happens

Trigger: await hasher.hash('') (empty), hash('short1A' is fine but hash('short') is not), hash('alllowercase123'), hash('NOLOWERCASE123') — any input failing the minLength/maxLength/requireUppercase/requireLowercase/requireDigit/requireSpecial checks. Typical when requireSpecial was enabled in config but the caller's form does not enforce it.

Common situations: Signup/change-password flows where UI validation is looser than the hasher config; importing legacy users whose passwords predate the policy; tests using passwords like 'test' or 'password'; enabling requireSpecial: true without updating the client-side checker.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/51abdff840ad0190. Report an issue: GitHub.