auth-test-tokens.ts108 lines · main
1/**
2 * Testing tokens — Phase 7.4.
3 *
4 * Special tokens for E2E test suites that bypass bot protection,
5 * rate limiting, and MFA requirements. Created via the admin API;
6 * exchanged for a real session via the customer API.
7 *
8 * Tokens are SHA-256 hashed at rest (same pattern as sign-in tokens).
9 * The raw token is returned exactly once — on creation.
10 */
11
12import { createHash, randomBytes } from 'node:crypto';
13import { runInProjectDatabase } from '../db/data-plane.js';
14
15const TOKEN_BYTES = 32;
16
17function hashToken(token: string): string {
18 return createHash('sha256').update(token).digest('hex');
19}
20
21export function generateRawTestToken(): string {
22 return `briven_test_${randomBytes(TOKEN_BYTES).toString('base64url')}`;
23}
24
25export interface TestTokenResult {
26 id: string;
27 token: string;
28 name: string | null;
29 expiresAt: Date;
30}
31
32export async function createTestToken(
33 projectId: string,
34 userId: string,
35 name?: string,
36): Promise<TestTokenResult> {
37 const raw = generateRawTestToken();
38 const tokenHash = hashToken(raw);
39 const id = crypto.randomUUID();
40 const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
41
42 await runInProjectDatabase(projectId, async (tx) => {
43 await tx.unsafe(
44 `INSERT INTO "_briven_auth_test_tokens" (id, user_id, token_hash, name, expires_at) VALUES ($1, $2, $3, $4, $5)`,
45 [id, userId, tokenHash, name ?? null, expiresAt] as never,
46 );
47 });
48
49 return { id, token: raw, name: name ?? null, expiresAt };
50}
51
52export async function listTestTokens(
53 projectId: string,
54): Promise<Array<{ id: string; userId: string; name: string | null; expiresAt: Date; createdAt: Date }>> {
55 const rows = await runInProjectDatabase(projectId, async (tx) => {
56 return (await tx.unsafe(
57 `SELECT id, user_id, name, expires_at, created_at FROM "_briven_auth_test_tokens" ORDER BY created_at DESC`,
58 )) as Array<{ id: string; user_id: string; name: string | null; expires_at: Date; created_at: Date }>;
59 });
60 return rows.map((r) => ({
61 id: r.id,
62 userId: r.user_id,
63 name: r.name,
64 expiresAt: r.expires_at,
65 createdAt: r.created_at,
66 }));
67}
68
69export async function revokeTestToken(projectId: string, tokenId: string): Promise<void> {
70 await runInProjectDatabase(projectId, async (tx) => {
71 await tx.unsafe(
72 `DELETE FROM "_briven_auth_test_tokens" WHERE id = $1`,
73 [tokenId] as never,
74 );
75 });
76}
77
78export async function exchangeTestToken(
79 projectId: string,
80 token: string,
81): Promise<{ userId: string; sessionToken: string; expiresAt: Date } | null> {
82 const tokenHash = hashToken(token);
83
84 const row = await runInProjectDatabase(projectId, async (tx) => {
85 const rows = (await tx.unsafe(
86 `SELECT user_id, expires_at FROM "_briven_auth_test_tokens" WHERE token_hash = $1 LIMIT 1`,
87 [tokenHash] as never,
88 )) as Array<{ user_id: string; expires_at: Date }>;
89 return rows[0] ?? null;
90 });
91
92 if (!row) return null;
93 if (row.expires_at < new Date()) return null;
94
95 // Create a session directly — same shape as Better Auth sessions.
96 const sessionToken = randomBytes(32).toString('hex');
97 const sessionId = crypto.randomUUID();
98 const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
99
100 await runInProjectDatabase(projectId, async (tx) => {
101 await tx.unsafe(
102 `INSERT INTO "_briven_auth_sessions" (id, user_id, token, expires_at, created_at, updated_at) VALUES ($1, $2, $3, $4, now(), now())`,
103 [sessionId, row.user_id, sessionToken, expiresAt] as never,
104 );
105 });
106
107 return { userId: row.user_id, sessionToken, expiresAt };
108}