hono-sess
Version:
A Simple Session Middleware for Hono
275 lines (274 loc) • 10.2 kB
JavaScript
;
import { getSignedCookie, setSignedCookie } from "hono/cookie";
import crypto from "crypto";
import { Cookie } from "./cookie.js";
import { MemoryStore } from "./memory.js";
import { Session } from "./session.js";
import { Store } from "./store.js";
import { issecure, warning, debug, deprecate, hash, expressCookieOptionsToHonoCookieOptions, } from "./utils.js";
let env = process.env.NODE_ENV;
export { Store, Cookie, Session, MemoryStore };
export * from "./types.js";
const session = ({ cookie = {}, genid = crypto.randomUUID, name = "connect.sid", store = new MemoryStore(), proxy = false, resave = false, rolling = false, saveUninitialized = true, secret = "dev-secret", unset = "keep", }) => {
if (typeof genid !== "function")
throw new TypeError("genid option must be a function");
if (resave === undefined) {
deprecate("undefined resave option; provide resave option");
resave = true;
}
if (saveUninitialized === undefined) {
deprecate("undefined saveUninitialized option; provide saveUninitialized option");
saveUninitialized = true;
}
if (unset !== "destroy" && unset !== "keep")
throw new TypeError("unset option must be \"destroy\" or \"keep\"");
const unsetDestroy = unset === "destroy";
if (Array.isArray(secret) && secret.length === 0) {
throw new TypeError("secret option array must contain one or more strings");
}
if (secret && !Array.isArray(secret))
secret = [secret];
if (!secret)
deprecate("req.secret; provide secret option");
if (env === "production" && store instanceof MemoryStore)
console.warn(warning);
store.generate = function (req) {
req.sessionID = genid();
req.session = new Session(req, null);
req.session.cookie = new Cookie(cookie);
req.raw.sessionID = req.sessionID;
if (cookie.secure === "auto") {
req.session.cookie.secure = issecure(req, proxy);
}
};
const storeImplementsTouch = typeof store.touch === "function";
let storeReady = true;
store.on("disconnect", function ondisconnect() {
storeReady = false;
});
store.on("connect", function onconnect() {
storeReady = true;
});
return async function session(context, next) {
const c = context;
if (c.req.session) {
return await next();
}
if (!storeReady) {
debug("store is disconnected");
return await next();
}
if (c.req.path.indexOf(cookie.path || "/") !== 0) {
debug("pathname mismatch");
return await next();
}
if (!secret) {
console.error("secret option required for sessions");
return await next();
}
let secrets = secret;
let cookieId = null;
let originalHash = null;
let originalId = null;
let savedHash = null;
let touched = false;
c.req.sessionStore = store;
const signedCookie = (((await getSignedCookie(c, secrets.join(" "), name)) || undefined));
cookieId = signedCookie;
if (cookieId) {
c.req.sessionID = cookieId;
}
const getNext = () => next().then(async (nextResult) => {
if (shouldDestroy(c.req)) {
debug("destroying");
await new Promise((resolve, reject) => {
store.destroy(c.req.sessionID, (err) => {
if (err)
reject(err);
debug("destroyed");
resolve();
});
});
}
if (!c.req.session) {
debug("no session at post next");
return nextResult;
}
if (!touched) {
c.req.session.touch();
touched = true;
}
if (shouldSave(c.req)) {
await new Promise((resolve, reject) => {
c.req.session.save((err) => {
if (err) {
reject(err);
}
resolve();
});
});
}
else if (storeImplementsTouch && shouldTouch(c.req)) {
debug("touching");
await new Promise((resolve, reject) => {
store.touch?.(c.req.sessionID, c.req.session, (err) => {
if (err) {
reject(err);
}
debug("touched");
resolve();
});
});
}
await handleCookies();
return nextResult;
});
const handleCookies = async () => {
if (!c.req.session) {
debug("no session");
return;
}
if (!shouldSetCookie(c.req)) {
debug("should not set cookie");
return;
}
if (c.req.session.cookie.secure && !issecure(c.req, proxy)) {
debug("not secured");
return;
}
if (!touched) {
c.req.session.touch();
touched = true;
debug("touched");
}
try {
debug("setting cookie");
const cookieData = c.req.session.cookie.data;
await setSignedCookie(c, name, c.req.sessionID, secrets.join(" "), expressCookieOptionsToHonoCookieOptions(cookieData, c.req, proxy));
return;
}
catch (err) {
console.error(err);
return;
}
};
function generate() {
debug("generating");
store.generate(c.req);
originalId = c.req.sessionID;
originalHash = hash(c.req.session);
wrapmethods(c.req.session);
}
function inflate(req, session) {
debug("inflating");
store.createSession(req, session);
originalId = req.sessionID;
originalHash = hash(session);
if (!resave) {
savedHash = originalHash;
}
wrapmethods(session);
}
function rewrapmethods(session, callback) {
debug("rewrapmethods");
return function () {
if (c.req.session !== session) {
wrapmethods(c.req.session);
}
callback.apply(this, arguments);
};
}
function wrapmethods(session) {
const _reload = session.reload;
const _save = session.save;
function reload(callback) {
debug("reloading %s", session.id);
_reload.call(session, rewrapmethods(session, callback));
}
function save() {
debug("saving %s", session.id);
savedHash = hash(session);
_save.apply(session, arguments);
}
Object.defineProperty(session, "reload", {
configurable: true,
enumerable: false,
value: reload,
writable: true,
});
Object.defineProperty(session, "save", {
configurable: true,
enumerable: false,
value: save,
writable: true,
});
}
function isModified(session) {
return originalId !== session.id || originalHash !== hash(session);
}
function isSaved(session) {
return originalId === session.id && savedHash === hash(session);
}
function shouldDestroy(req) {
return req.sessionID && unsetDestroy && req.session == null;
}
function shouldSave(req) {
if (typeof req.sessionID !== "string") {
debug("session ignored because of bogus req.sessionID %o", req.sessionID);
return false;
}
return !saveUninitialized && !savedHash && cookieId !== req.sessionID
? isModified(req.session)
: !isSaved(req.session);
}
function shouldTouch(req) {
if (typeof req.sessionID !== "string") {
debug("session ignored because of bogus req.sessionID %o", req.sessionID);
return false;
}
return cookieId === req.sessionID && !shouldSave(req);
}
function shouldSetCookie(req) {
if (typeof req.sessionID !== "string") {
debug("session ignored because of bogus req.sessionID %o");
return false;
}
return cookieId !== req.sessionID
? saveUninitialized || isModified(req.session)
: rolling ||
(req.session.cookie.expires != null && isModified(req.session));
}
if (!c.req.sessionID) {
debug("no SID sent, generating session");
generate();
return getNext();
}
debug("fetching %s", c.req.sessionID);
return new Promise((resolve) => {
store.get(c.req.sessionID, (err, session) => {
if (err && err.code !== "ENOENT") {
debug("error %j", err);
resolve(getNext());
return;
}
try {
if (err || !session) {
debug("no session found");
generate();
}
else {
debug("session found");
inflate(c.req, session);
}
}
catch (e) {
console.error(e);
resolve(getNext());
return;
}
resolve(getNext());
});
});
};
};
export default session;