Add complete auth backend (Workstream D): - Auth middleware for session/API key authentication - Signup with password or passkey (WebAuthn) - Login flow with device trust and email confirmation - Password reset and email verification - Session management and logout Utilities created: - cookies.ts: Cookie helpers and configuration - crypto.ts: Token generation and hashing - password.ts: zxcvbn validation, argon2id hashing - geo.ts: IP/location extraction from headers - email.ts: Stubbed email sending - session.ts: Session creation and device trust Code review improvements applied: - Use ORPCError instead of Error in procedures - Add ast-grep rule to enforce ORPCError usage - Remove error info leakage (generic messages) - Optimize N+1 query with JOIN in login-password - Extract signupWithPassword/signupWithPasskey for testability - Add 15-minute WebAuthn challenge expiry check - Strengthen CookieOptions type definitions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
68 lines
1.6 KiB
TypeScript
68 lines
1.6 KiB
TypeScript
import zxcvbn from "zxcvbn";
|
|
|
|
export interface PasswordValidationResult {
|
|
valid: boolean;
|
|
feedback: string[];
|
|
score: number;
|
|
}
|
|
|
|
/**
|
|
* Validate password strength using zxcvbn
|
|
* @param password - The password to validate
|
|
* @param userInputs - User-specific inputs to penalize (email, display name)
|
|
* @returns Validation result with feedback if invalid
|
|
*/
|
|
export const validatePassword = (
|
|
password: string,
|
|
userInputs: string[] = [],
|
|
): PasswordValidationResult => {
|
|
const result = zxcvbn(password, userInputs);
|
|
|
|
if (result.score < 3) {
|
|
const feedback =
|
|
result.feedback.suggestions.length > 0
|
|
? result.feedback.suggestions
|
|
: [
|
|
"Password is too weak. Try a longer phrase or add numbers and symbols.",
|
|
];
|
|
|
|
return {
|
|
valid: false,
|
|
feedback,
|
|
score: result.score,
|
|
};
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
feedback: [],
|
|
score: result.score,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Hash a password using Bun's built-in argon2id
|
|
* @param password - The plaintext password to hash
|
|
* @returns The hashed password
|
|
*/
|
|
export const hashPassword = async (password: string): Promise<string> => {
|
|
return Bun.password.hash(password, {
|
|
algorithm: "argon2id",
|
|
memoryCost: 65536, // 64 MiB
|
|
timeCost: 3,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Verify a password against a stored hash
|
|
* @param password - The plaintext password to verify
|
|
* @param hash - The stored password hash
|
|
* @returns True if the password matches the hash
|
|
*/
|
|
export const verifyPassword = async (
|
|
password: string,
|
|
hash: string,
|
|
): Promise<boolean> => {
|
|
return Bun.password.verify(password, hash);
|
|
};
|