native-session.ts175 lines · main
1/**
2 * briven-engine sessions on Doltgres.
3 */
4
5import { createHash, randomBytes } from 'node:crypto';
6
7import { getEnginePool } from './db.js';
8
9export type EngineSession = {
10 sessionHandle: string;
11 userId: string;
12 tenantId: string;
13 accessToken: string;
14 refreshToken: string;
15 expiresAt: Date;
16};
17
18function newToken(): string {
19 return randomBytes(32).toString('base64url');
20}
21
22function hash(t: string): string {
23 return createHash('sha256').update(t).digest('hex');
24}
25
26export async function createEngineSession(input: {
27 userId: string;
28 tenantId: string;
29 ttlDays?: number;
30}): Promise<EngineSession> {
31 const sessionHandle = `sh_${randomBytes(16).toString('hex')}`;
32 // Phase 2: access cookie value IS the session handle (Doltgres PK lookup).
33 // Keep field name accessToken for FDI/cookie compatibility.
34 const accessToken = sessionHandle;
35 const refreshToken = newToken();
36 const days = input.ttlDays ?? 30;
37 const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
38 const pool = getEnginePool();
39
40 await pool.query(
41 `INSERT INTO be_sessions
42 (session_handle, user_id, tenant_id, refresh_token_hash, access_payload_json, expires_at)
43 VALUES ($1, $2, $3, $4, $5, $6)`,
44 [
45 sessionHandle,
46 input.userId,
47 input.tenantId,
48 hash(refreshToken),
49 JSON.stringify({ sub: input.userId, tenantId: input.tenantId }),
50 expiresAt.toISOString(),
51 ],
52 );
53
54 return {
55 sessionHandle,
56 userId: input.userId,
57 tenantId: input.tenantId,
58 accessToken,
59 refreshToken,
60 expiresAt,
61 };
62}
63
64export async function getSessionByHandle(
65 sessionHandle: string,
66): Promise<{ userId: string; tenantId: string; expiresAt: Date } | null> {
67 const pool = getEnginePool();
68 const res = await pool.query(
69 `SELECT user_id, tenant_id, expires_at FROM be_sessions
70 WHERE session_handle = $1 LIMIT 1`,
71 [sessionHandle],
72 );
73 const row = res.rows[0] as
74 | { user_id: string; tenant_id: string; expires_at: Date | string }
75 | undefined;
76 if (!row) return null;
77 const expiresAt = new Date(row.expires_at);
78 if (expiresAt.getTime() < Date.now()) return null;
79 return { userId: row.user_id, tenantId: row.tenant_id, expiresAt };
80}
81
82export async function revokeEngineSession(sessionHandle: string): Promise<boolean> {
83 const pool = getEnginePool();
84 // Capture user/tenant before delete for audit
85 let userId: string | null = null;
86 let tenantId: string | null = null;
87 try {
88 const prev = await pool.query(
89 `SELECT user_id, tenant_id FROM be_sessions WHERE session_handle = $1 LIMIT 1`,
90 [sessionHandle],
91 );
92 const row = prev.rows[0] as { user_id?: string; tenant_id?: string } | undefined;
93 userId = row?.user_id ?? null;
94 tenantId = row?.tenant_id ?? null;
95 } catch {
96 /* ignore */
97 }
98 const res = await pool.query(`DELETE FROM be_sessions WHERE session_handle = $1`, [
99 sessionHandle,
100 ]);
101 const ok = (res.rowCount ?? 0) > 0;
102 if (ok) {
103 const { recordBrivenEngineAudit } = await import('./audit.js');
104 void recordBrivenEngineAudit({
105 action: 'session.revoked',
106 tenantId,
107 userId,
108 metadata: { sessionHandle },
109 });
110 }
111 return ok;
112}
113
114export async function revokeAllForUser(userId: string): Promise<number> {
115 const pool = getEnginePool();
116 const res = await pool.query(`DELETE FROM be_sessions WHERE user_id = $1`, [userId]);
117 return res.rowCount ?? 0;
118}
119
120export async function listSessionHandles(userId: string): Promise<string[]> {
121 const pool = getEnginePool();
122 const res = await pool.query(
123 `SELECT session_handle FROM be_sessions WHERE user_id = $1 AND expires_at > NOW()`,
124 [userId],
125 );
126 return res.rows.map((r: { session_handle: string }) => r.session_handle);
127}
128
129/** Recent active sessions for yellow dashboard (optionally one tenant). */
130export async function listRecentEngineSessions(
131 limit = 50,
132 opts?: { tenantId?: string },
133): Promise<
134 Array<{
135 handle: string;
136 userId: string;
137 tenantId: string;
138 expiresAt: string;
139 createdAt: string;
140 }>
141> {
142 const pool = getEnginePool();
143 const res = opts?.tenantId
144 ? await pool.query(
145 `SELECT session_handle, user_id, tenant_id, expires_at, created_at
146 FROM be_sessions
147 WHERE expires_at > NOW() AND tenant_id = $1
148 ORDER BY created_at DESC
149 LIMIT $2`,
150 [opts.tenantId, limit],
151 )
152 : await pool.query(
153 `SELECT session_handle, user_id, tenant_id, expires_at, created_at
154 FROM be_sessions
155 WHERE expires_at > NOW()
156 ORDER BY created_at DESC
157 LIMIT $1`,
158 [limit],
159 );
160 return (
161 res.rows as Array<{
162 session_handle: string;
163 user_id: string;
164 tenant_id: string;
165 expires_at: Date | string;
166 created_at: Date | string;
167 }>
168 ).map((r) => ({
169 handle: r.session_handle,
170 userId: r.user_id,
171 tenantId: r.tenant_id,
172 expiresAt: new Date(r.expires_at).toISOString(),
173 createdAt: new Date(r.created_at).toISOString(),
174 }));
175}