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

@@ -3,9 +3,9 @@
* Public procedure - no authentication required
*
* Flow:
* 1. Read rev.login_request_token cookie (could be real ID or fake UUID)
* 2. If fake token (not found in DB): return { status: 'pending' }
* 3. If valid login request ID:
* 1. Read rev.login_request_token cookie
* 2. If token not found in DB (fake or expired): return { status: 'pending' }
* 3. If valid login request:
* - Check if expired: return { status: 'expired' }
* - Check if not completed: return { status: 'pending' }
* - If completed:
@@ -31,15 +31,6 @@ import {
} from "../../utils/session.js";
import { os } from "../base.js";
/**
* Check if a string looks like a UUID (fake token)
*/
const isUUID = (str: string): boolean => {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
};
/**
* Login if request is completed handler
* Polls for login completion and creates session when ready
@@ -57,21 +48,7 @@ export const loginIfRequestIsCompleted =
return { status: "pending" as const };
}
// Check if it's a fake token (UUID)
if (isUUID(loginRequestToken)) {
// Fake token - user doesn't exist
// The cookie will expire naturally after 15 minutes
return { status: "pending" as const };
}
// Try to parse as login request ID
const loginRequestId = Number.parseInt(loginRequestToken, 10);
if (Number.isNaN(loginRequestId)) {
// Invalid format - treat as pending
return { status: "pending" as const };
}
// Fetch login request from database
// Fetch login request from database by token
const loginRequest = await context.db
.selectFrom("login_requests")
.select([
@@ -81,7 +58,7 @@ export const loginIfRequestIsCompleted =
"completed_at",
"expires_at",
])
.where("id", "=", String(loginRequestId))
.where("token", "=", loginRequestToken)
.executeTakeFirst();
// Login request not found - might have been deleted or invalid ID
@@ -140,7 +117,7 @@ export const loginIfRequestIsCompleted =
// Delete the login request (it's been consumed)
await context.db
.deleteFrom("login_requests")
.where("id", "=", String(loginRequestId))
.where("id", "=", loginRequest.id)
.execute();
// Set session cookie