UNPKG

@redwoodjs/sdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

136 lines (135 loc) 5.3 kB
import { ErrorResponse } from "../../error"; export const MAX_SESSION_DURATION = 14 * 24 * 60 * 60 * 1000; // 14 days const packSessionId = (parts) => { return btoa([parts.unsignedSessionId, parts.signature].join(":")); }; const unpackSessionId = (packed) => { const [unsignedSessionId, signature] = atob(packed).split(":"); return { unsignedSessionId, signature }; }; const arrayBufferToHex = (buffer) => { const array = new Uint8Array(buffer); return Array.from(array) .map((b) => b.toString(16).padStart(2, "0")) .join(""); }; export const createSessionCookie = ({ name, sessionId, maxAge, }) => { const isViteDev = typeof import.meta.env !== "undefined" && import.meta.env.DEV; return `${name}=${sessionId}; Path=/; HttpOnly; ${isViteDev ? "" : "Secure; "}SameSite=Lax${maxAge != null ? `; Max-Age=${maxAge === true ? MAX_SESSION_DURATION / 1000 : maxAge}` : ""}`; }; export const signSessionId = async ({ unsignedSessionId, secretKey, }) => { const encoder = new TextEncoder(); const key = await crypto.subtle.importKey("raw", encoder.encode(secretKey), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const signatureArrayBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(unsignedSessionId)); return arrayBufferToHex(signatureArrayBuffer); }; export const generateSessionId = async ({ secretKey, }) => { const unsignedSessionId = crypto.randomUUID(); const signature = await signSessionId({ unsignedSessionId, secretKey }); return packSessionId({ unsignedSessionId, signature }); }; export const isValidSessionId = async ({ sessionId, secretKey, }) => { try { const { unsignedSessionId, signature } = unpackSessionId(sessionId); const computedSignature = await signSessionId({ unsignedSessionId, secretKey, }); return computedSignature === signature; } catch { return false; } }; export const defineSessionStore = ({ cookieName = "session_id", createCookie = createSessionCookie, secretKey, get, set, unset, }) => { const getSessionIdFromCookie = (request) => { const cookieHeader = request.headers.get("Cookie"); if (!cookieHeader) return undefined; for (const cookie of cookieHeader.split(";")) { const trimmedCookie = cookie.trim(); const separatorIndex = trimmedCookie.indexOf("="); if (separatorIndex === -1) continue; const key = trimmedCookie.slice(0, separatorIndex); const value = trimmedCookie.slice(separatorIndex + 1); if (key === cookieName) { return value; } } }; const load = async (request) => { const sessionId = getSessionIdFromCookie(request); if (!sessionId) { return null; } if (!(await isValidSessionId({ sessionId, secretKey }))) { throw new ErrorResponse(401, "Invalid session id"); } try { return await get(sessionId); } catch (error) { throw new ErrorResponse(401, "Invalid session id"); } }; const save = async (headers, sessionInputData, { maxAge } = {}) => { const sessionId = await generateSessionId({ secretKey }); await set(sessionId, sessionInputData); headers.set("Set-Cookie", createCookie({ name: cookieName, sessionId, maxAge })); }; const remove = async (request, headers) => { const sessionId = getSessionIdFromCookie(request); if (sessionId) { await unset(sessionId); } headers.set("Set-Cookie", createCookie({ name: cookieName, sessionId: "", maxAge: 0 })); }; return { load, save, remove, }; }; export const defineDurableSession = ({ cookieName, createCookie, secretKey, sessionDurableObject, }) => { const get = async (sessionId) => { const { unsignedSessionId } = unpackSessionId(sessionId); const doId = sessionDurableObject.idFromName(unsignedSessionId); const sessionStub = sessionDurableObject.get(doId); const result = (await sessionStub.getSession()); if ("error" in result) { throw new Error(result.error); } return result.value; }; const set = async (sessionId, sessionInputData) => { const { unsignedSessionId } = unpackSessionId(sessionId); const doId = sessionDurableObject.idFromName(unsignedSessionId); const sessionStub = sessionDurableObject.get(doId); // todo(justinvdm, 2025-02-20): Fix this // @ts-ignore await sessionStub.saveSession(sessionInputData); }; const unset = async (sessionId) => { let unsignedSessionId; try { unsignedSessionId = unpackSessionId(sessionId).unsignedSessionId; } catch { return; } const doId = sessionDurableObject.idFromName(unsignedSessionId); const sessionStub = sessionDurableObject.get(doId); await sessionStub.revokeSession(); }; return defineSessionStore({ cookieName, createCookie, secretKey, get, set, unset, }); };