session.ts92 lines · main
1import type { MiddlewareHandler } from 'hono';
2
3import { auth, type Session, type User } from '../lib/auth.js';
4import { log } from '../lib/logger.js';
5
6/**
7 * Populate every request context with the current user + session, or nulls.
8 * Protected routes then use `requireAuth()` below.
9 */
10export const attachSession = (): MiddlewareHandler => async (c, next) => {
11 const session = await auth.api.getSession({ headers: c.req.raw.headers });
12 if (session) {
13 c.set('user', session.user);
14 c.set('session', session.session);
15 } else {
16 c.set('user', null);
17 c.set('session', null);
18 }
19 await next();
20};
21
22/**
23 * Guard for any route that must not run anonymously. Returns 401 with a
24 * structured error so the CLI/dashboard can redirect to sign-in.
25 */
26export const requireAuth = (): MiddlewareHandler => async (c, next) => {
27 // If an upstream project-scoped guard (requireProjectAuth) already
28 // authenticated this request via a project API key (Bearer brk_…), accept
29 // it: the key is not a CLI JWT and the Bearer check below would otherwise
30 // reject it as "invalid cli token". Sessions and CLI JWTs still verify
31 // through the logic that follows. This is what lets `briven deploy` /
32 // SDK calls (which carry brk_ keys) reach project routes that sit behind
33 // both this broad guard and a route-level requireProjectAuth.
34 if (c.get('apiKeyId')) {
35 await next();
36 return;
37 }
38
39 // SCIM 2.0 directory-sync (enterprise): protocol lives on
40 // `/v1/projects/:id/scim/v2/*` and authenticates with `scim_briven_…`
41 // bearer tokens only — not dashboard sessions or CLI JWTs. The broad
42 // `/v1/projects/*` requireAuth on projectsRouter must NOT reject those
43 // requests; auth-scim router enforces the token itself.
44 const path = c.req.path;
45 if (path.includes('/scim/v2')) {
46 await next();
47 return;
48 }
49
50 const authHeader = c.req.header('authorization');
51 if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
52 const token = authHeader.slice(7).trim();
53 // SCIM tokens are never CLI JWTs — pass through even off /scim paths
54 // (defensive; primary carve-out is path-based above).
55 if (token.startsWith('scim_briven_')) {
56 await next();
57 return;
58 }
59 try {
60 const { verifyCliToken } = await import('../lib/cli-jwt.js');
61 const payload = await verifyCliToken(token);
62 const { getDb } = await import('../db/client.js');
63 const { users: userTable } = await import('../db/schema.js');
64 const { eq } = await import('drizzle-orm');
65 const [row] = await getDb()
66 .select({ id: userTable.id, email: userTable.email, name: userTable.name })
67 .from(userTable)
68 .where(eq(userTable.id, payload.sub))
69 .limit(1);
70 if (!row) {
71 return c.json({ code: 'unauthorized', message: 'cli token user not found' }, 401);
72 }
73 c.set('user', row as unknown as User);
74 await next();
75 return;
76 } catch (err) {
77 log.warn('cli_bearer_rejected', {
78 err: err instanceof Error ? err.message : String(err),
79 });
80 return c.json({ code: 'unauthorized', message: 'invalid cli token' }, 401);
81 }
82 }
83
84 const user = c.get('user') as User | null;
85 if (!user) {
86 return c.json({ code: 'unauthorized', message: 'authentication required' }, 401);
87 }
88 await next();
89 return;
90};
91
92export type { Session, User };