Add generateSecureBase58Token to shared utils with login_ prefix

- Create packages/utils/src/generate-base58-token.ts with typed prefix support
- Function returns `${TPrefix}${string}` for type-safe prefixed tokens
- Add isBase58() validator and parseBase58Token() helper
- Add comprehensive tests (13 test cases)

- Update login request tokens to use "login_" prefix
- Fix login-password.ts to not replace token (cookie/DB mismatch bug)
- Migrate all token generation from generateSecureToken (hex) to
  generateSecureBase58Token (base58)
- Remove duplicate token generation from api-server/utils/crypto.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 19:31:30 +08:00
parent 68fc67ba4a
commit ddd7c0c03b
10 changed files with 218 additions and 80 deletions

View File

@@ -1,5 +1,8 @@
import { base58 } from "@scure/base";
// Re-export generateSecureBase58Token from shared utils
export { generateSecureBase58Token } from "@reviq/utils";
/**
* Token prefix for all RevIQ API tokens
*/
@@ -62,58 +65,6 @@ export const generateDeviceFingerprint = (): string => {
return crypto.randomUUID();
};
/**
* Generate a secure random token for email verification, password reset, etc.
* Uses 32 bytes (256 bits) of entropy
* Uses Web Crypto API for Cloudflare Workers compatibility
*/
export const generateSecureToken = (): string => {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.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
*/