compareLocalHash, OTP_ONLY_PASSWORD_HASH and a new isPasswordHash now live in password-hash.ts, which depends on bcryptjs only. The data-import script needs them, and importing auth.service would load NestJS and the ESM-only @nestjs/jwt. auth.service re-exports the first two, so callers and tests are unchanged. Its own bcrypt import, now unused, is dropped. No behaviour change: 21/21 auth tests, 178/178 overall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 lines
1.5 KiB
TypeScript
34 lines
1.5 KiB
TypeScript
import * as bcrypt from 'bcryptjs';
|
|
|
|
/**
|
|
* Value written into `passwordHash` for accounts that must never log in with a
|
|
* password — the ~1,590 legacy customers who only ever used OTP
|
|
* (docs/DATA-MIGRATION-MAP.md §10.4). The `!` prefix is the Django/Rails
|
|
* "unusable password" convention: readable by anyone looking at the row, and
|
|
* matched by no password.
|
|
*
|
|
* Kept in this dependency-free module (bcryptjs only) so the data-import
|
|
* script can use it without loading NestJS.
|
|
*/
|
|
export const OTP_ONLY_PASSWORD_HASH = '!OTP_ONLY_NO_PASSWORD_LOGIN';
|
|
|
|
/**
|
|
* A real bcrypt hash always starts with `$2` ($2a$, $2b$, or PHP's $2y$ from
|
|
* the legacy server — all three verify with bcryptjs). Anything else is not a
|
|
* password: '' on accounts created without one, or OTP_ONLY_PASSWORD_HASH.
|
|
*
|
|
* Such hashes are refused here, BEFORE bcrypt is called, so the outcome never
|
|
* depends on how a particular bcrypt build treats a malformed hash. bcryptjs 3
|
|
* happens to return false today; a future version or a native binding might
|
|
* throw and turn a wrong password into a 500 (owner ruling 2026-10-07).
|
|
*/
|
|
export async function compareLocalHash(password: string, passwordHash: string | null | undefined) {
|
|
if (typeof passwordHash !== 'string' || !passwordHash.startsWith('$2')) return false;
|
|
return bcrypt.compare(password, passwordHash);
|
|
}
|
|
|
|
/** True when a stored legacy hash can be carried over as a working password. */
|
|
export function isPasswordHash(hash: unknown): hash is string {
|
|
return typeof hash === 'string' && hash.startsWith('$2');
|
|
}
|