index.ts117 lines · main
1/**
2 * @briven/auth/server — Next.js App Router server helpers.
3 *
4 * import { getServerSession } from '@briven/auth/server';
5 *
6 * export default async function Page() {
7 * const session = await getServerSession(auth, {
8 * cookieHeader: (await cookies()).toString(),
9 * });
10 * if (!session?.authenticated) redirect('/sign-in');
11 * ...
12 * }
13 *
14 * Reads the `cookie` header off the incoming RSC request and forwards
15 * it to the api. The api validates the session token (stored in the
16 * cookie) and returns the user + session. Cookie names match Better
17 * Auth defaults — the SDK never hard-codes them.
18 *
19 * Why not include `next` as a peer dep? `cookies()` from next/headers
20 * is the cleanest way to read the cookie in App Router, but pulling
21 * `next` into a published SDK risks version mismatches. Instead we
22 * accept the cookie header as an opt-in argument so server-side
23 * runtimes that expose `cookies()` (Next.js, Remix) can hand it over
24 * without us depending on a specific framework.
25 */
26
27import type { BrivenAuthClient, SessionResponse, User } from '../index.js';
28
29export interface ServerSessionInput {
30 /** Raw `cookie` header from the incoming request. Required. */
31 cookieHeader: string | null;
32}
33
34const BRIDGE_PREFIX = '/v1/auth-tenant';
35
36/**
37 * Validate the session cookie server-side. Returns `null` when there is
38 * no cookie or the api is unreachable; returns `{ authenticated: false }`
39 * when the cookie is present but the session is expired / revoked.
40 */
41export async function getServerSession(
42 client: BrivenAuthClient,
43 input: ServerSessionInput,
44): Promise<SessionResponse | null> {
45 if (!input.cookieHeader) return null;
46 try {
47 const res = await fetch(`${client.apiOrigin}${BRIDGE_PREFIX}/get-session`, {
48 method: 'GET',
49 headers: {
50 cookie: input.cookieHeader,
51 'x-briven-project-id': client.projectId,
52 },
53 });
54 if (!res.ok) return null;
55 const body = (await res.json()) as {
56 user?: { id?: string };
57 session?: { expiresAt?: string };
58 } | null;
59 if (body && body.user?.id && body.session?.expiresAt) {
60 return {
61 authenticated: true,
62 userId: body.user.id,
63 expiresAt: body.session.expiresAt,
64 };
65 }
66 return { authenticated: false };
67 } catch {
68 return null;
69 }
70}
71
72/**
73 * Get the full user record server-side. Same lifecycle as
74 * `getServerSession` — returns `null` on any failure path. Email + name
75 * are present in the response; callers must honour the privacy boundary
76 * (CLAUDE.md §5.1: email only visible to the account holder).
77 */
78export async function getServerUser(
79 client: BrivenAuthClient,
80 input: ServerSessionInput,
81): Promise<User | null> {
82 if (!input.cookieHeader) return null;
83 try {
84 const res = await fetch(`${client.apiOrigin}${BRIDGE_PREFIX}/get-session`, {
85 method: 'GET',
86 headers: {
87 cookie: input.cookieHeader,
88 'x-briven-project-id': client.projectId,
89 },
90 });
91 if (!res.ok) return null;
92 const body = (await res.json()) as { user?: User } | null;
93 return body?.user ?? null;
94 } catch {
95 return null;
96 }
97}
98
99/**
100 * Convenience wrapper. Throws when there is no session — callers redirect
101 * inside the catch / fall-through path.
102 *
103 * const session = await requireServerSession(auth, { cookieHeader });
104 * // guaranteed authenticated past this line
105 */
106export async function requireServerSession(
107 client: BrivenAuthClient,
108 input: ServerSessionInput,
109): Promise<{ userId: string; expiresAt: string }> {
110 const session = await getServerSession(client, input);
111 if (!session || !session.authenticated) {
112 throw new Error('briven-auth: unauthenticated');
113 }
114 return { userId: session.userId, expiresAt: session.expiresAt };
115}
116
117export type { BrivenAuthClient, SessionResponse, User };