@redwoodjs/sdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
136 lines (135 loc) • 5.3 kB
JavaScript
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,
});
};