Add utils package with Web Crypto password hashing

- Create @reviq/utils package with PBKDF2-SHA256 password hashing
  compatible with Cloudflare Workers (uses crypto.subtle)
- Update api-server and CLI to use new utils package for consistent
  password hashing format across the codebase
- Add pino logging to api-server for better request debugging
- Make login request tokens cryptographically secure base58 strings
  instead of database IDs
- Add migration to make login_requests.token non-nullable with unique
  constraint
- Fix RPCLink URL construction for client-side API calls
- Add db:codegen script to root package.json

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 18:12:33 +08:00
parent cee700f063
commit c1afc39062
25 changed files with 512 additions and 142 deletions

View File

@@ -40,6 +40,45 @@ export const generateSecureToken = (): string => {
.join("");
};
/**
* Base58 alphabet (Bitcoin-style, no 0, O, I, l)
*/
const BASE58_ALPHABET =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
/**
* Generate a cryptographically secure base58 token
* Uses 24 bytes (192 bits) of entropy, producing ~33 character output
*/
export const generateBase58Token = (byteLength = 24): string => {
const bytes = new Uint8Array(byteLength);
crypto.getRandomValues(bytes);
// Convert bytes to base58
let result = "";
let num = BigInt(0);
for (const byte of bytes) {
num = num * 256n + BigInt(byte);
}
while (num > 0n) {
const remainder = Number(num % 58n);
result = BASE58_ALPHABET.charAt(remainder) + result;
num /= 58n;
}
// Handle leading zeros
for (const byte of bytes) {
if (byte === 0) {
result = BASE58_ALPHABET.charAt(0) + result;
} else {
break;
}
}
return result;
};
/**
* Generate expiration date
*/

View File

@@ -1,3 +1,7 @@
import {
hashPassword as hashPasswordUtil,
verifyPassword as verifyPasswordUtil,
} from "@reviq/utils";
import zxcvbn from "zxcvbn";
export interface PasswordValidationResult {
@@ -41,27 +45,11 @@ export const validatePassword = (
};
/**
* Hash a password using Bun's built-in argon2id
* @param password - The plaintext password to hash
* @returns The hashed password
* Hash a password using PBKDF2-SHA256 (Cloudflare Workers compatible)
*/
export const hashPassword = async (password: string): Promise<string> => {
return Bun.password.hash(password, {
algorithm: "argon2id",
memoryCost: 65536, // 64 MiB
timeCost: 3,
});
};
export const hashPassword = hashPasswordUtil;
/**
* 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);
};
export const verifyPassword = verifyPasswordUtil;