@otterhttp/session
Version:
Simple promise-based session utility for otterhttp
145 lines (140 loc) • 4.38 kB
JavaScript
// src/session.ts
import { nanoid } from "nanoid";
// src/memory-store.ts
var MemoryStore = class {
store;
constructor() {
this.store = /* @__PURE__ */ new Map();
}
async get(sid) {
const sess = this.store.get(sid);
if (sess) {
const session2 = JSON.parse(sess, (key, value) => {
if (key === "expires") return new Date(value);
return value;
});
if (session2.cookie.expires && session2.cookie.expires.getTime() <= Date.now()) {
await this.destroy(sid);
return null;
}
return session2;
}
return null;
}
async set(sid, sess) {
this.store.set(sid, JSON.stringify(sess));
}
async destroy(sid) {
this.store.delete(sid);
}
async touch(sid, sess) {
this.store.set(sid, JSON.stringify(sess));
}
};
// src/symbol.ts
var isTouched = Symbol("session.isTouched");
var isDestroyed = Symbol("session.isDestroyed");
var isNew = Symbol("session.isNew");
var lateHeaderAction = Symbol("session.lateHeaderAction");
// src/utils.ts
function appendSessionCookieHeader(res, name, { cookie, id }, { encode, sign }) {
if (res.headersSent) return;
res.cookie(name, id, {
path: cookie.path,
httpOnly: cookie.httpOnly,
expires: cookie.expires,
domain: cookie.domain,
sameSite: cookie.sameSite,
secure: cookie.secure,
encode,
sign
});
}
// src/session.ts
function session(options = {}) {
const store = options.store || new MemoryStore();
const genId = options.genid || nanoid;
const touchAfter = options.touchAfter ?? -1;
const { name: maybeName, unsign, ...cookieOpts } = options.cookie ?? {};
const name = maybeName ?? "sid";
function decorateSession(req, res, session2, id, _now) {
Object.defineProperties(session2, {
commit: {
value: async function commit() {
await store.set(this.id, this);
}
},
touch: {
value: async function touch() {
if (this.cookie.maxAge != null) {
this.cookie.expires = new Date(_now + this.cookie.maxAge * 1e3);
}
await store.touch?.(this.id, this);
this[isTouched] = true;
}
},
destroy: {
value: async function destroy() {
this[isDestroyed] = true;
this.cookie.expires = /* @__PURE__ */ new Date(1);
await store.destroy(this.id);
req.session = void 0;
}
},
id: { value: id }
});
}
return async function sessionHandle(req, res) {
if (req.session != null) return req.session;
const _now = Date.now();
const sessionCookie = req.cookies[name];
if (unsign != null && sessionCookie != null && !sessionCookie.signed) sessionCookie.unsign(unsign);
let sessionId = null;
try {
sessionId = sessionCookie?.value ?? null;
} catch (err) {
}
const _session = sessionId ? await store.get(sessionId) : null;
let session2;
if (_session) {
session2 = _session;
const expires = session2.cookie.expires;
if (typeof expires === "string") {
session2.cookie.expires = new Date(expires);
}
decorateSession(req, res, session2, sessionId, _now);
if (touchAfter >= 0 && session2.cookie.expires) {
const lastTouchedTime = session2.cookie.expires.getTime() - session2.cookie.maxAge * 1e3;
if (_now - lastTouchedTime >= touchAfter * 1e3) {
await session2.touch();
}
}
} else {
sessionId = genId();
session2 = {
[isNew]: true,
cookie: {
path: cookieOpts.path || "/",
httpOnly: cookieOpts.httpOnly ?? true,
domain: cookieOpts.domain || null,
sameSite: cookieOpts.sameSite || null,
secure: cookieOpts.secure || false
}
};
if (cookieOpts.maxAge) {
session2.cookie.maxAge = cookieOpts.maxAge;
session2.cookie.expires = new Date(_now + cookieOpts.maxAge * 1e3);
}
decorateSession(req, res, session2, sessionId, _now);
}
req.session = session2;
res.registerLateHeaderAction(lateHeaderAction, (res2) => {
if (!(session2[isNew] && Object.keys(session2).length > 1) && !session2[isTouched] && !session2[isDestroyed]) return;
appendSessionCookieHeader(res2, name, session2, cookieOpts);
});
return session2;
};
}
export {
session as default
};