auth-core-session.ts189 lines · main
1/**
2 * Briven Auth Core — session admin/verify endpoints (Phase 2).
3 *
4 * GET /v1/auth-core/session/me — verify current session (cookie/header)
5 * POST /v1/auth-core/session/revoke — revoke by handle or all for user
6 * GET /v1/auth-core/session/list — list handles for userId (query)
7 */
8
9import { Hono } from 'hono';
10
11import { requireAuthCoreDashboard } from '../middleware/auth-core-guard.js';
12import { requireDashboardProjectAdmin } from '../services/auth-core/dashboard-project-auth.js';
13import {
14 listSessionsForUser,
15 revokeAllSessionsForUser,
16 revokeSession,
17 verifyAuthCoreSession,
18} from '../services/auth-core/session.js';
19import { listRecentEngineSessions } from '../services/auth-core/native-session.js';
20import {
21 BRIVEN_ENGINE_ID,
22 isAuthCoreInitialized,
23} from '../services/auth-core/engine.js';
24import type { AppEnv } from '../types/app-env.js';
25
26export const authCoreSessionRouter = new Hono<AppEnv>();
27
28// me = self-check via cookie (public to holders of session cookie)
29// list/revoke/recent = dashboard only
30authCoreSessionRouter.use('/v1/auth-core/session/list', requireAuthCoreDashboard());
31authCoreSessionRouter.use('/v1/auth-core/session/revoke', requireAuthCoreDashboard());
32authCoreSessionRouter.use('/v1/auth-core/session/recent', requireAuthCoreDashboard());
33
34authCoreSessionRouter.get('/v1/auth-core/session/me', async (c) => {
35 if (!isAuthCoreInitialized()) {
36 return c.json({ code: 'auth_core_sdk_not_ready' }, 503);
37 }
38
39 const result = await verifyAuthCoreSession({
40 url: c.req.url,
41 method: c.req.method,
42 headers: c.req.raw.headers,
43 cookieHeader: c.req.header('cookie'),
44 });
45
46 if (!result.ok) {
47 return c.json(
48 { authenticated: false, reason: result.reason },
49 (result.status as 401) ?? 401,
50 );
51 }
52
53 const session = result.session;
54 const userId = session.getUserId();
55 // Load email for app session mint (Konnos etc.) — apps expect user.email.
56 let email: string | null = null;
57 let name: string | null = null;
58 try {
59 const { getEnginePool } = await import('../services/auth-core/db.js');
60 const pool = getEnginePool();
61 const u = await pool.query(
62 `SELECT email, metadata_json FROM be_users WHERE id = $1 LIMIT 1`,
63 [userId],
64 );
65 const row = u.rows[0] as
66 | { email?: string | null; metadata_json?: string | null }
67 | undefined;
68 if (row?.email) email = row.email;
69 if (row?.metadata_json) {
70 try {
71 const meta = JSON.parse(row.metadata_json) as { name?: string };
72 if (meta.name) name = meta.name;
73 } catch {
74 /* ignore */
75 }
76 }
77 } catch {
78 /* email optional */
79 }
80 return c.json({
81 authenticated: true,
82 userId,
83 sessionHandle: session.getHandle(),
84 accessTokenPayload: session.getAccessTokenPayload(),
85 // Better Auth–shaped fields for app mint routes
86 user: { id: userId, email, name },
87 });
88});
89
90authCoreSessionRouter.get('/v1/auth-core/session/list', async (c) => {
91 if (!isAuthCoreInitialized()) {
92 return c.json({ code: 'auth_core_sdk_not_ready' }, 503);
93 }
94 const projectGate = await requireDashboardProjectAdmin(
95 c,
96 c.req.query('projectId'),
97 );
98 if (projectGate instanceof Response) return projectGate;
99 const userId = c.req.query('userId');
100 if (!userId) {
101 return c.json({ code: 'userId_required' }, 400);
102 }
103 const handles = await listSessionsForUser(userId);
104 return c.json({
105 userId,
106 handles,
107 count: handles.length,
108 projectId: projectGate.projectId,
109 });
110});
111
112/** Yellow dashboard: recent active sessions across tenants. */
113authCoreSessionRouter.get('/v1/auth-core/session/recent', async (c) => {
114 if (!isAuthCoreInitialized()) {
115 return c.json(
116 {
117 engine: BRIVEN_ENGINE_ID,
118 code: 'auth_core_sdk_not_ready',
119 sessions: [],
120 },
121 503,
122 );
123 }
124 const projectGate = await requireDashboardProjectAdmin(
125 c,
126 c.req.query('projectId'),
127 );
128 if (projectGate instanceof Response) return projectGate;
129 const limit = Number(c.req.query('limit') ?? '50');
130 const projectId = projectGate.projectId;
131 let tenantId = c.req.query('tenantId') ?? undefined;
132 if (!tenantId && projectId) {
133 try {
134 const { projectIdToTenantId } = await import(
135 '../services/auth-core/project-map.js'
136 );
137 tenantId = projectIdToTenantId(projectId);
138 } catch {
139 tenantId = undefined;
140 }
141 }
142 const sessions = await listRecentEngineSessions(
143 Number.isFinite(limit) ? limit : 50,
144 tenantId ? { tenantId } : undefined,
145 );
146 return c.json({
147 engine: BRIVEN_ENGINE_ID,
148 storage: 'doltgres',
149 projectId: projectId ?? null,
150 tenantId: tenantId ?? null,
151 sessions,
152 count: sessions.length,
153 });
154});
155
156authCoreSessionRouter.post('/v1/auth-core/session/revoke', async (c) => {
157 if (!isAuthCoreInitialized()) {
158 return c.json({ code: 'auth_core_sdk_not_ready' }, 503);
159 }
160 let body: {
161 sessionHandle?: string;
162 userId?: string;
163 all?: boolean;
164 projectId?: string;
165 } = {};
166 try {
167 body = await c.req.json();
168 } catch {
169 body = {};
170 }
171 const projectGate = await requireDashboardProjectAdmin(
172 c,
173 body.projectId ?? c.req.query('projectId'),
174 );
175 if (projectGate instanceof Response) return projectGate;
176
177 if (body.all && body.userId) {
178 const n = await revokeAllSessionsForUser(body.userId);
179 return c.json({ revoked: n, userId: body.userId });
180 }
181 if (body.sessionHandle) {
182 const ok = await revokeSession(body.sessionHandle);
183 return c.json({ revoked: ok ? 1 : 0, sessionHandle: body.sessionHandle });
184 }
185 return c.json(
186 { code: 'bad_request', message: 'Provide sessionHandle, or userId+all' },
187 400,
188 );
189});