@wristband/nextjs-auth
Version:
SDK for integrating your Next.js application with Wristband. Handles user authentication, session management, and token management.
275 lines (274 loc) • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getReadOnlySessionFromCookies = getReadOnlySessionFromCookies;
exports.getMutableSessionFromCookies = getMutableSessionFromCookies;
exports.saveSessionWithCookies = saveSessionWithCookies;
exports.destroySessionWithCookies = destroySessionWithCookies;
const typescript_session_1 = require("@wristband/typescript-session");
const error_1 = require("../error");
const types_1 = require("../types");
const DEFAULT_SESSION_COOKIE_NAME = 'session';
const REQUEST_PLACEHOLDER_URL = 'https://placeholder.url';
const SESSION_READ_ONLY_ERROR = 'SESSION_READ_ONLY'; // For Server Component session errors
// ======================================
// INTERNAL FUNCTIONS
// ======================================
/**
* Internal helper to get session from Next.js cookies() API.
*
* Extracts the session cookie and creates a Web Request to pass to the
* underlying typescript-session SDK. Not exported - users should use
* getReadOnlySessionFromCookies() or getMutableSessionFromCookies().
*
* For App Router Server Actions and Server Components only.
*
* @internal
* @template T - Session data type extending SessionData
* @param cookieStore - The result of `await cookies()` from 'next/headers'
* @param options - Session configuration options
* @returns Promise resolving to a full session instance
*/
async function getSessionFromCookies(cookieStore, options) {
const cookieName = options.cookieName || DEFAULT_SESSION_COOKIE_NAME;
const cookieValue = cookieStore.get(cookieName)?.value;
const request = new Request(REQUEST_PLACEHOLDER_URL, {
headers: { cookie: cookieValue ? `${cookieName}=${cookieValue}` : '' },
});
return (0, typescript_session_1.getSession)(request, options);
}
/**
* Type guard to check if a session is from a Server Component.
*
* Checks for the invisible SERVER_COMPONENT_SESSION symbol marker
* that is added by getReadOnlySessionFromCookies().
*
* @internal
* @param session - The session object to check
* @returns True if the session is from a Server Component
*/
function isServerComponentSession(session) {
return Boolean(session && typeof session === 'object' && session[types_1.SERVER_COMPONENT_SESSION]);
}
// ======================================
// PUBLIC API
// ======================================
/**
* Get a read-only session from Next.js cookies() API.
*
* **For App Router Server Components only** - when you need to READ session data.
* Cannot modify or save. For modifying sessions in Server Actions, use getMutableSessionFromCookies().
*
* For route handlers or middleware/proxy, use getSession() instead.
* For Pages Router, use getPagesRouterSession() instead.
*
* **IMPORTANT:** Using `cookies()` automatically makes your Server Component dynamic (no static caching).
* The route will execute fresh on every request, ensuring session data is always current.
*
* @template T - Session data type extending SessionData
* @param cookieStore - The result of `await cookies()` from 'next/headers'
* @param options - Session configuration options
* @returns Promise resolving to a read-only session with runtime protection against mutations
*
* @example
* ```typescript
* // ✅ Server Component - reading session data
* import { cookies } from 'next/headers';
* import { getReadOnlySessionFromCookies } from '@wristband/nextjs-auth';
*
* const sessionOptions = {
* secrets: process.env.SESSION_SECRET!,
* cookieName: 'my-session'
* };
*
* export default async function ProfilePage() {
* const cookieStore = await cookies();
* const session = await getReadOnlySessionFromCookies(cookieStore, sessionOptions);
* return <div>User: {session.userId}</div>;
* }
*
* // ❌ These will throw TypeScript errors:
* // session.set('theme', 'dark');
* // await session.save();
* ```
* @see {@link getMutableSessionFromCookies} For Server Actions that need to modify sessions
* @see {@link getSessionFromRequest} For App Router route handlers and middleware/proxy
* @see {@link getPagesRouterSession} For Pages Router
*/
async function getReadOnlySessionFromCookies(cookieStore, options) {
const session = await getSessionFromCookies(cookieStore, options);
// Add invisible marker for runtime detection
Object.defineProperty(session, types_1.SERVER_COMPONENT_SESSION, { value: true, enumerable: false, writable: false });
return session;
}
/**
* Get a mutable session from Next.js cookies() API.
*
* **For App Router Server Actions only** - when you need to MODIFY session data.
* Can call set()/delete(), but must use saveSessionWithCookies() to persist changes
* or destroySessionWithCookies() to destroy the session.
*
* For route handlers or middleware/proxy, use getSession() instead.
* For Pages Router, use getPagesRouterSession() instead.
*
* **Cache Behavior:** Server Actions automatically trigger a router refresh when cookies are modified,
* so the UI will update to reflect session changes without manual revalidation. If you have cached
* data that depends on session values, you can optionally call `revalidatePath()` or `revalidateTag()`.
*
* @template T - Session data type extending SessionData
* @param cookieStore - The result of `await cookies()` from 'next/headers'
* @param options - Session configuration options
* @returns Promise resolving to a mutable session that can be modified and saved
* @throws {WristbandError} If an invalid session method is invoked.
*
* @example
* ```typescript
* // Server Action - modifying session data
* 'use server'
* import { cookies } from 'next/headers';
* import {
* getMutableSessionFromCookies,
* saveSessionWithCookies
* } from '@wristband/nextjs-auth';
*
* const sessionOptions = {
* secrets: process.env.SESSION_SECRET!,
* cookieName: 'my-session'
* };
*
* export async function updateUserPreferences(theme: string, language: string) {
* const cookieStore = await cookies();
* const session = await getMutableSessionFromCookies(cookieStore, sessionOptions);
*
* // ✅ Modify session data
* session.set('theme', theme);
* session.set('language', language);
*
* // ✅ Persist changes
* await saveSessionWithCookies(cookieStore, session);
*
* // Optional: revalidate cached data if you have OTHER data that depends on session
* // revalidatePath('/settings'); <- Next.js function
*
* // ❌ These will throw TypeScript errors:
* // await session.save(); -> Use saveSessionWithCookies(cookieStore, session) instead
* // session.destroy(); -> Use destroySessionWithCookies(cookieStore, session) instead
* }
* ```
*
* @see {@link saveSessionWithCookies} To persist session changes
* @see {@link destroySessionWithCookies} To destroy the session
* @see {@link getReadOnlySessionFromCookies} For Server Components (read-only)
*/
async function getMutableSessionFromCookies(cookieStore, options) {
const session = await getSessionFromCookies(cookieStore, options);
return session;
}
/**
* Save session changes to Next.js cookies API.
*
* **For App Router Server Actions only.**
* Encrypts and saves session data to cookies using Next.js `cookies()` API.
*
* For App Router route handlers and middleware/proxy, use session.saveToResponse() instead.
* For Pages Router route handlers, use session.save().
*
* **Cache Behavior:** Modifying cookies automatically invalidates Next.js Router Cache
* and triggers a re-render of the current route, so UI updates reflect session changes
* without manual revalidation.
*
* @template T - Session data type extending SessionData
* @param cookieStore - The result of await cookies() from 'next/headers'
* @param session - The session instance to save
* @returns Promise that resolves when cookies are set
* @throws {WristbandError} If session is from a Server Component (read-only)
*
* @example
* ```typescript
* // app/actions.ts
* 'use server'
* import { cookies } from 'next/headers';
* import {
* getMutableSessionFromCookies,
* saveSessionWithCookies
* } from '@wristband/nextjs-auth';
*
* const sessionOptions = {
* secrets: process.env.SESSION_SECRET!,
* cookieName: 'my-session'
* };
*
* export async function updateTheme(theme: string) {
* const cookieStore = await cookies();
* const session = await getMutableSessionFromCookies(cookieStore, sessionOptions);
*
* session.set('theme', theme);
* await saveSessionWithCookies(cookieStore, session);
*
* // UI automatically updates - no manual revalidation needed.
* }
* ```
*
* @see {@link getMutableSessionFromCookies} To get a mutable session
* @see {@link destroySessionWithCookies} To destroy the session instead
*/
async function saveSessionWithCookies(cookieStore, session) {
if (isServerComponentSession(session)) {
throw new error_1.WristbandError(SESSION_READ_ONLY_ERROR, 'Cannot save a Server Component session. Server Components are read-only. ' +
'Use getMutableSessionFromCookies() in Server Actions instead.');
}
const cookieData = await session.getCookieDataForSave();
cookieData.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
}
/**
* Destroy session using Next.js cookies API.
*
* **For App Router Server Actions only.**
* Clears session cookies using Next.js `cookies()` API.
*
* For App Router route handlers and middleware/proxy, use session.destroyToResponse() instead.
* For Pages Router route handlers, use session.destroy().
*
* **Cache Behavior:** Deleting cookies automatically invalidates Next.js Router Cache
* and triggers a re-render of the current route.
*
* @template T - Session data type extending SessionData
* @param cookieStore - The result of `await cookies()` from 'next/headers'
* @param session - The session instance to destroy
* @throws {WristbandError} If session is from a Server Component (read-only)
*
* @example
* ```typescript
* 'use server'
* import { cookies } from 'next/headers';
* import { getMutableSessionFromCookies, destroySessionWithCookies } from '@wristband/nextjs-auth';
* import { redirect } from 'next/navigation';
*
* const sessionOptions = {
* secrets: process.env.SESSION_SECRET!,
* cookieName: 'my-session'
* };
*
* export async function logout() {
* const cookieStore = await cookies();
* const session = await getMutableSessionFromCookies(cookieStore, sessionOptions);
*
* destroySessionWithCookies(cookieStore, session);
* redirect('/login');
* }
* ```
*
* @see {@link getMutableSessionFromCookies} To get a mutable session
* @see {@link saveSessionWithCookies} To save session changes instead
*/
function destroySessionWithCookies(cookieStore, session) {
if (isServerComponentSession(session)) {
throw new error_1.WristbandError(SESSION_READ_ONLY_ERROR, 'Cannot destroy a Server Component session. Server Components are read-only. ' +
'Use getMutableSessionFromCookies() in Server Actions instead.');
}
const cookieData = session.getCookieDataForDestroy();
cookieData.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
}