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 | |
| 27 | import type { BrivenAuthClient, SessionResponse, User } from '../index.js'; |
| 28 | |
| 29 | export interface ServerSessionInput { |
| 30 | /** Raw `cookie` header from the incoming request. Required. */ |
| 31 | cookieHeader: string | null; |
| 32 | } |
| 33 | |
| 34 | const 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 | */ |
| 41 | export 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 | */ |
| 78 | export 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 | */ |
| 106 | export 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 | |
| 117 | export type { BrivenAuthClient, SessionResponse, User }; |