svelte-kit-cookie-session-patch
Version:
⚒️ Encrypted 'stateless' cookie sessions for SvelteKit
177 lines (176 loc) • 7.38 kB
JavaScript
import { parse, serialize, daysToMaxage } from "./utils/cookie.js";
import { decrypt, encrypt } from "./utils/crypto/index.js";
let initialSecret;
let encoder;
let decoder;
export function initializeSession(headers, options) {
var _a;
const key = options.key || "kit.session";
const expires = daysToMaxage((_a = options.expires) !== null && _a !== void 0 ? _a : 7);
// Null or Undefined
if (options.secret == null) {
throw new Error("Please provide at least one secret");
}
const secrets = Array.isArray(options.secret)
? options.secret
: [{ id: 1, secret: options.secret }];
/** This is mainly for testing purposes */
let changedSecrets = false;
if (!initialSecret || initialSecret !== secrets[0].secret) {
initialSecret = secrets[0].secret;
changedSecrets = true;
}
// Setup de/encoding
if (!encoder || changedSecrets) {
encoder = encrypt(secrets[0].secret);
}
if (!decoder || changedSecrets) {
decoder = decrypt(secrets[0].secret);
}
const sessionOptions = {
key,
expires,
cookie: options.cookie || {},
};
//@ts-ignore That's okay
sessionOptions.cookie.maxAge = expires;
// Set sane default cookie optioons
if (!sessionOptions.cookie.httpOnly == "undefined") {
sessionOptions.cookie.httpOnly = true;
}
if (typeof sessionOptions.cookie.sameSite) {
sessionOptions.cookie.sameSite = true;
}
if (!sessionOptions.cookie.path) {
sessionOptions.cookie.path = "/";
}
// Parse the cookie header
const cookies = parse(headers.cookie || headers.Cookie || "", {});
// Grab the session cookie from the parsed cookies
let sessionCookie = cookies[sessionOptions.key] || "";
let isInvalidDate = false;
let shouldReEncrypt = false;
let shouldDestroy = false;
let sessionData;
// If we have a session cookie we try to get the id from the cookie value and use it to decode the cookie.
// If the decodeID is not the first secret in the secrets array we should re encrypt to the newest secret.
if (sessionCookie.length > 0) {
// Split the sessionCookie on the &id= field to get the id we used to encrypt the session.
const [_sessionCookie, id] = sessionCookie.split("&id=");
const decodeID = id ? Number(id) : 1;
// Use the id from the cookie or the initial one which is always 1.
let secret = secrets.find((sec) => sec.id === decodeID);
// If there is no secret found try the first in the secrets array.
if (!secret)
secret = secrets[0];
// Set the session cookie without &id=
sessionCookie = _sessionCookie;
// If the decodeID unequals the newest secret id in the array, re initialize the decoder.
if (secrets[0].id !== decodeID) {
decoder = decrypt(secret.secret);
}
// Try to decode with the given sessionCookie and secret
try {
const decrypted = decoder(sessionCookie);
if (decrypted && decrypted.length > 0) {
sessionData = JSON.parse(decrypted);
// If the decodeID unequals the newest secret id in the array, we should re-encrypt the session with the newest secret.
if (secrets[0].id !== decodeID) {
shouldReEncrypt = true;
}
}
else {
shouldDestroy = true;
}
}
catch (error) {
shouldDestroy = true;
}
}
// Check if the session is already expired
if (sessionData &&
sessionData.expires &&
new Date(sessionData.expires).getTime() < new Date().getTime()) {
isInvalidDate = true;
}
const session = {};
// Initialize the session proxy
const sessionProxy = new Proxy(session, {
set: function (obj, prop, value) {
if (prop === "data") {
if (sessionData && sessionData.expires) {
const currentDate = new Date();
if (typeof sessionData.expires === "string") {
sessionData.expires = new Date(sessionData.expires);
}
const exp = sessionData.expires.getTime() / 1000 - currentDate.getTime() / 1000;
sessionOptions.cookie.maxAge = exp;
}
sessionData = {
...value,
expires: (sessionData === null || sessionData === void 0 ? void 0 : sessionData.expires) || new Date(Date.now() + expires * 1000),
};
sessionCookie = serialize(sessionOptions.key, (encoder(JSON.stringify(sessionData)) || "") + "&id=" + secrets[0].id, sessionOptions.cookie);
obj["set-cookie"] = sessionCookie;
}
return true;
},
get: function (obj, prop) {
if (prop === "data") {
return sessionData && !isInvalidDate ? sessionData : {};
}
else if (prop === "refresh") {
return (expires_in_days) => {
var _a;
if (!sessionData || isInvalidDate) {
return false;
}
let new_expires = daysToMaxage((_a = options.expires) !== null && _a !== void 0 ? _a : 7);
if (expires_in_days) {
new_expires = daysToMaxage(expires_in_days);
}
sessionData = {
...sessionData,
expires: new Date(Date.now() + new_expires * 1000),
};
sessionCookie = serialize(sessionOptions.key, (encoder(JSON.stringify(sessionData)) || "") +
"&id=" +
secrets[0].id, {
...sessionOptions.cookie,
maxAge: new_expires,
});
obj["set-cookie"] = sessionCookie;
return true;
};
}
else if (prop === "destroy") {
return () => {
if (sessionCookie.length === 0)
return false;
sessionData = undefined;
sessionCookie = serialize(sessionOptions.key, "0", {
...sessionOptions.cookie,
maxAge: undefined,
expires: new Date(Date.now() - 360000000),
});
obj["set-cookie"] = sessionCookie;
return true;
};
}
return obj[prop];
},
});
// If we have an invalid date or shouldDestroy is set to true we destroy the session.
if (isInvalidDate || shouldDestroy) {
sessionProxy.destroy();
}
// If rolling is activated and the session exists we refresh the session on every request.
if ((options === null || options === void 0 ? void 0 : options.rolling) && !isInvalidDate && sessionData) {
sessionProxy.refresh();
}
// Check if we have to re encrypt the data
if (shouldReEncrypt && sessionData) {
sessionProxy.data = { ...sessionData };
}
return sessionProxy;
}