UNPKG

@otterhttp/session

Version:

Simple promise-based session utility for otterhttp

145 lines (140 loc) 4.38 kB
// 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 };