Implement Workstream F1: me.get and me.setupProfile procedures

- Add me.get procedure returning user profile with needsSetup flag
- Add me.setupProfile procedure for initial profile setup after signup
- Add nonEmptyString/optionalString schema helpers with tests
- Use Web Crypto API (SubtleCrypto) for Cloudflare Workers compatibility
- Use @formatjs/intl-durationformat for duration formatting
- Remove node:crypto dependency from crypto utilities

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 16:29:41 +08:00
parent 93851afe38
commit 860d791125
16 changed files with 205 additions and 61 deletions

View File

@@ -1,11 +1,16 @@
import { createHash, randomBytes } from "node:crypto";
/**
* Hash a token with SHA-256 for storage in database
* Never store raw tokens - always hash first
* Uses Web Crypto API for Cloudflare Workers compatibility
*/
export const hashToken = (token: string): string => {
return createHash("sha256").update(token).digest("hex");
export const hashToken = async (token: string): Promise<string> => {
const encoder = new TextEncoder();
const data = encoder.encode(token);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = new Uint8Array(hashBuffer);
return Array.from(hashArray)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/**
@@ -25,9 +30,14 @@ export const generateDeviceFingerprint = (): string => {
/**
* 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 => {
return randomBytes(32).toString("hex");
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/**