SessionKit is a framework-agnostic, class-based cookie session engine for Node.js.
This README is the primary documentation for the open-source project. TypeDoc is kept as a backup API index.
SessionKit separates session runtime logic from framework adapter logic:
@sessionkit/core: session lifecycle, auth state, error model, pluggable store/lock contracts@sessionkit/express: Express adapter@sessionkit/hono: Hono adapter@sessionkit/redis: Redis session store and distributed lock provider
- Framework-neutral core API (no Express/Hono coupling in core)
- Type-safe
payload -> principalprojection - Complete auth flow:
signIn,signOut,optionalAuth,requireAuth - Rolling session support with TTL renewal controls
- Token refresh support with distributed locking
- Unified typed error model via
SessionKitError
- Clear boundaries between domain auth logic and HTTP runtime integration
- A single request auth context read path via
getAuth - Replaceable persistence layer (in-memory, Redis, custom implementation)
- Consistent adapter error-to-HTTP behavior
Install only the packages you need:
# core only
npm install @sessionkit/core
# express integration
npm install @sessionkit/core @sessionkit/express express
# hono integration
npm install @sessionkit/core @sessionkit/hono hono
# redis persistence
npm install @sessionkit/core @sessionkit/redis redisThis example demonstrates:
- global session middleware
- login via
signIn - protected route via
requireAuth - logout via
signOut
import express from "express";
import { MapSessionStore, SessionKit } from "@sessionkit/core";
import { createExpressSessionKit } from "@sessionkit/express";
type SessionPayload = {
userId: string;
role: "user" | "admin";
};
type Principal = {
id: string;
role: "user" | "admin";
};
const coreKit = new SessionKit<SessionPayload, Principal>({
store: new MapSessionStore<SessionPayload>({
cleanupIntervalSeconds: 60,
maxSize: 10000,
}),
cookie: {
name: "sid",
path: "/",
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
},
session: {
ttlSeconds: 60 * 60 * 24,
rolling: true,
renewBeforeSeconds: 60,
},
principalFactory(payload) {
return { id: payload.userId, role: payload.role };
},
hooks: {
onUnauthorized(ctx) {
ctx.status(401);
ctx.json({ error: { code: "UNAUTHORIZED", message: "Authentication required" } });
},
},
});
const sessionKit = createExpressSessionKit(coreKit);
const app = express();
app.use(express.json());
// 1) Hydrate auth context for every request.
app.use(sessionKit.middleware());
// 2) Login endpoint.
app.post("/login", async (req, res, next) => {
try {
const result = await sessionKit.signIn(
req,
res,
{ userId: "u_001", role: "user" },
{ ttlSeconds: 3600, hydrateContext: true },
);
res.json({ ok: true, sessionId: result.sessionId, expiresAt: result.expiresAt });
} catch (error) {
next(error);
}
});
// 3) Protected endpoint.
app.get(
"/me",
sessionKit.requireAuth(),
(req, res) => {
res.json({ auth: req.auth });
},
);
// 4) Logout endpoint.
app.post("/logout", async (req, res, next) => {
try {
await sessionKit.signOut(req, res, { alwaysClearCookie: true });
res.json({ ok: true });
} catch (error) {
next(error);
}
});
app.listen(3000);This section documents all public exports from package entry points. Each subsection ends with a runnable-style example.
Creates a SessionKit runtime with store, session policy, cookie policy, principal projection, and optional hooks.
const kit = new SessionKit<Payload, Principal>({
// required: session persistence implementation
store,
// required: baseline session TTL policy
session: { ttlSeconds: 3600 },
// required: map payload into principal exposed to app code
principalFactory(payload) {
return { id: payload.userId };
},
});Creates middleware that resolves session state from cookie + store and hydrates auth context for the current request.
// core instance + adapter conversion
app.use(toExpressMiddleware(kit.middleware()));
// adapter-bound instance (recommended)
app.use(sessionKit.middleware());Creates middleware equivalent to middleware(). This is an intent-oriented alias when authentication is optional.
// core instance + adapter conversion
app.use(toExpressMiddleware(kit.optionalAuth()));
// adapter-bound instance (recommended)
app.use(sessionKit.optionalAuth());Creates middleware that enforces authenticated access. If the request is unauthenticated, handling priority is options.onFail, then configured hooks.onUnauthorized, then throwing SessionKitError("UNAUTHORIZED", ...).
options is optional and contains:
onFail: custom unauthenticated handler(ctx) => void | Promise<void>
app.get("/private", toExpressMiddleware(kit.requireAuth()), handler);
app.get("/private", sessionKit.requireAuth(), handler);
app.get(
"/private-custom",
toExpressMiddleware(
kit.requireAuth({
// option: override unauthenticated behavior for this route
onFail(ctx) {
ctx.status(401);
ctx.json({ error: "login required" });
},
}),
),
handler,
);Creates a new session, stores it, sets cookie, and returns SignInResult<TPrincipal>.
options is optional and contains:
ttlSeconds: per-call TTL override (default issession.ttlSeconds)hydrateContext: whether to set auth context immediately in current request (default istrue)
const result = await kit.signIn(
ctx,
{
// payload: your stored session data
userId: "u_001",
role: "admin",
},
{
// option: override TTL for this sign-in only
ttlSeconds: 900,
// option: immediately mark current request as authenticated
hydrateContext: true,
},
);
console.log(result.sessionId, result.principal, result.expiresAt);Deletes session from store, clears cookie, and resets auth context to unauthenticated.
options is optional and contains:
alwaysClearCookie: iftrue, cookie is cleared even when store delete fails; iffalse, delete failure throws
await kit.signOut(ctx, {
// option: fail hard if store deletion fails
alwaysClearCookie: false,
});Reads auth context from request context and returns an unauthenticated default object when none is present.
const auth = kit.getAuth(ctx);
// shape includes: sessionId, session, principal, isAuthenticated
if (!auth.isAuthenticated) {
// handle guest flow
}Defines runtime configuration for new SessionKit(...), including required store/session/principal settings and optional cookie, token-refresh, lock, hook, and logger settings.
session options are:
rollingtouchEverySecondsrenewBeforeSeconds
token options are:
onRefreshFail
const kit = new SessionKit<Payload, Principal>({
store,
cookie: {
// option: cookie name (default: "sid")
name: "sid",
// option: cookie path (default: "/")
path: "/",
// option: cookie domain
domain: "example.com",
// option: client-side JS access (default: true means HttpOnly enabled)
httpOnly: true,
// option: secure cookie for HTTPS
secure: true,
// option: lax | strict | none (default: "lax")
sameSite: "lax",
// option: explicit max-age override in seconds
maxAgeSeconds: 3600,
},
session: {
ttlSeconds: 3600,
// option: enable rolling renewal
rolling: true,
// option: fallback renewal threshold in seconds
touchEverySeconds: 60,
// option: preferred renewal threshold in seconds
renewBeforeSeconds: 30,
},
principalFactory(payload) {
return { id: payload.userId, role: payload.role };
},
payloadTransformer(raw) {
// option: migrate/validate legacy payload shape
return raw as Payload;
},
token: {
shouldRefresh(payload, nowMs) {
return payload.accessTokenExpMs - nowMs < 60_000;
},
async refresh(payload) {
const refreshed = await refreshToken(payload.refreshToken);
return {
payload: {
...payload,
accessToken: refreshed.accessToken,
accessTokenExpMs: refreshed.expMs,
},
// option: override TTL after refresh
ttlSeconds: 3600,
};
},
// option: unauth | revoke
onRefreshFail: "revoke",
},
lockProvider,
hooks: {
onUnauthorized(ctx) {
ctx.status(401);
ctx.json({ error: "unauthorized" });
},
onInvalidSession(ctx, reason) {
console.warn("invalid session", reason);
},
},
logger: console,
});Defines cookie-level behavior used by core and adapters.
const cookieOptions = {
// option: default is "sid"
name: "sid",
// option: default is "/"
path: "/",
// option: cookie scope domain
domain: "example.com",
// option: default is true
httpOnly: true,
// option: set true for HTTPS deployment
secure: true,
// option: default is "lax"
sameSite: "strict",
// option: max-age in seconds
maxAgeSeconds: 1800,
};Defines the framework-neutral contract SessionKit uses to read cookies, write cookies, store auth context, and emit response status/body.
const ctx: HttpContext = {
getCookie(name) {
return null;
},
setCookie(name, value, options) {
// options includes cookie flags and optional maxAgeSeconds
},
clearCookie(name, options) {
// clears cookie using provided cookie scope
},
setAuth(value) {
// attach auth context for current request
},
getAuth() {
return null;
},
status(code) {
// set response status
},
json(body) {
// write JSON response
},
};Defines middleware signature accepted by adapters.
const middleware: HttpMiddleware = async (ctx, next) => {
// perform work before downstream
await next();
// perform work after downstream
};Defines storage contracts and the bundled in-memory implementation.
SessionStore<TPayload> optional options are:
touchclose
MapSessionStore constructor options are:
cleanupIntervalSecondsmaxSize
const memoryStore = new MapSessionStore<Payload>({
// option: cleanup interval in seconds
cleanupIntervalSeconds: 60,
// option: max entries before naive eviction
maxSize: 10_000,
});
await memoryStore.set("sid-1", { payload: { userId: "u1" }, createdAt: Date.now(), expiresAt: Date.now() + 3600_000 }, 3600);
const session = await memoryStore.get("sid-1");
await memoryStore.touch?.("sid-1", 3600);
await memoryStore.del("sid-1");
await memoryStore.close?.();Defines distributed lock contract and the bundled no-op implementation.
const lock = new NoopLockProvider();
const result = await lock.withLock("sessionkit:refresh:sid-1", 10, async () => {
// critical section
return "ok";
});Defines error code model, canonical error type, and helper utilities used by adapters.
try {
throw new SessionKitError("UNAUTHORIZED", "Authentication required.");
} catch (error) {
if (isSessionKitError(error)) {
const status = statusFromErrorCode(error.code);
const body = defaultErrorBody(error.code, error.message);
console.log(status, body);
}
}Provides parser/serializer helpers used by adapters and custom integrations.
const parsed = parseCookieHeader("sid=abc123; theme=dark");
const setHeader = serializeSetCookie("sid", "abc123", {
// option: cookie path
path: "/",
// option: secure + httponly flags
secure: true,
httpOnly: true,
// option: same-site policy
sameSite: "lax",
// option: explicit max-age
maxAgeSeconds: 3600,
});
const clearHeader = serializeClearCookie("sid", { path: "/" });Binds a core SessionKit instance to Express so you can use sessionKit.middleware() directly and avoid creating context manually in each handler.
const coreKit = new SessionKit<Payload, Principal>({ store, session: { ttlSeconds: 3600 }, principalFactory });
const sessionKit = createExpressSessionKit(coreKit);
app.use(sessionKit.middleware());
app.get("/private", sessionKit.requireAuth(), (req, res) => {
const auth = sessionKit.getAuth(req, res);
res.json({ me: auth.principal });
});Defines minimal request shape required by the Express adapter.
const req: SessionKitExpressRequest = {
headers: {
cookie: "sid=abc123",
},
auth: undefined,
};Defines minimal response shape required by the Express adapter.
const res: SessionKitExpressResponse = {
status(code) {
return code;
},
json(body) {
return body;
},
getHeader(name) {
return undefined;
},
setHeader(name, value) {
return value;
},
};Converts Express request/response objects into core HttpContext.
const ctx = createExpressHttpContext(req, res);
await kit.signIn(ctx, { userId: "u_001", role: "user" });Converts core middleware to Express middleware and maps SessionKitError to HTTP responses.
options is optional and contains:
onError: custom SessionKitError handler
app.use(
toExpressMiddleware(kit.middleware(), {
// option: customize adapter-level error output
onError(error, req, res) {
res.status(500);
res.json({ code: error.code, message: error.message });
},
}),
);Defines the context key used by the Hono adapter to store auth context.
console.log(SESSIONKIT_HONO_AUTH_KEY); // "auth"Defines adapter-level error customization for Hono integration.
options is optional and contains:
onError: custom SessionKitError handler returningResponse | void
app.use(
"*",
toHonoMiddleware(kit.middleware(), {
// option: customize error mapping
onError(error, c) {
return c.json({ code: error.code, message: error.message }, 500);
},
}),
);Binds a core SessionKit instance to Hono so handlers can use sessionKit.middleware() and sessionKit.signIn(c, ...) directly.
const coreKit = new SessionKit<Payload, Principal>({ store, session: { ttlSeconds: 3600 }, principalFactory });
const sessionKit = createHonoSessionKit(coreKit);
app.use("*", sessionKit.middleware());
app.get("/private", sessionKit.requireAuth(), (c) => {
const auth = sessionKit.getAuth(c);
return c.json({ me: auth.principal });
});Converts Hono Context into core HttpContext.
const ctx = createHonoHttpContext(c);
const auth = kit.getAuth(ctx);Converts core middleware to Hono middleware and handles SessionKit error mapping.
options is optional and contains:
onError: custom SessionKitError handler returningResponse | void
app.use("/private/*", toHonoMiddleware(kit.requireAuth()));Defines custom serialization/deserialization strategy for persisted sessions.
const codec: SessionCodec<Payload> = {
serialize(value) {
return JSON.stringify(value);
},
deserialize(raw) {
return JSON.parse(raw) as StoredSession<Payload>;
},
};Defines optional Redis store behavior.
options is optional and contains:
keyPrefixcodec
const store = new RedisSessionStore<Payload>(
{ url: "redis://localhost:6379" },
{
// option: namespacing key prefix
keyPrefix: "sessionkit:sess:",
// option: custom codec
codec,
},
);Defines optional lock acquisition behavior.
options is optional and contains:
keyPrefixacquireTimeoutMsretryDelayMs
const lockProvider = new RedisLockProvider(
{ url: "redis://localhost:6379" },
{
// option: lock key namespace
keyPrefix: "sessionkit:lock:",
// option: max wait to acquire lock
acquireTimeoutMs: 5000,
// option: polling delay while waiting lock
retryDelayMs: 50,
},
);Defines parameterized Redis connection input.
options is optional and contains:
urlhostportusernamepassworddatabasetlslazyConnectredisOptions
const connection: RedisConnectionParams = {
// option: full URL
url: "redis://localhost:6379",
// option: enable lazy connect
lazyConnect: false,
};Represents accepted constructor input for Redis store and lock provider.
const byClient: RedisConnectionInput = redisClient;
const byWrapper: RedisConnectionInput = { client: redisClient, manageClient: false, lazyConnect: true };
const byParams: RedisConnectionInput = { url: "redis://localhost:6379" };Represents accepted constructor input for RedisLockProvider.
const lockInputByParams: RedisLockProviderInput = { url: "redis://localhost:6379" };
const lockInputByStore: RedisLockProviderInput = { store };Redis-backed session store implementation for SessionKit.
const store = new RedisSessionStore<Payload>({ url: "redis://localhost:6379" });
await store.set("sid-1", { payload: { userId: "u1" }, createdAt: Date.now(), expiresAt: Date.now() + 3600_000 }, 3600);
const session = await store.get("sid-1");
await store.touch("sid-1", 3600);
await store.del("sid-1");
await store.close();Redis-backed distributed lock provider, commonly used for token refresh race control.
const lock = new RedisLockProvider({ url: "redis://localhost:6379" });
await lock.withLock("sessionkit:refresh:sid-1", 10, async () => {
// critical section work
});
await lock.close();Recommended rollout sequence:
- Define
SessionPayloadandPrincipaltypes. - Choose store strategy (
MapSessionStorefor local development,RedisSessionStorefor shared/runtime environments). - Configure cookie policy (
httpOnly/sameSite/secure/domain/path). - Configure
session.ttlSecondsand whether rolling renewal is required. - Add
middleware()andrequireAuth()in routes. - If token rotation is needed, configure
token.shouldRefresh,token.refresh, andtoken.onRefreshFail. - For multi-instance deployment, add
RedisLockProviderto avoid refresh races. - Add integration tests for login, expiry, invalid session handling, and logout.
- Primary docs (this README): https://github.com/harutostudio/sessionkit
- TypeDoc backup: https://harutostudio.github.io/sessionkit/
- Contributing: ./CONTRIBUTING.md
- Security policy: ./SECURITY.md
pnpm install
pnpm typecheck
pnpm build
pnpm test
pnpm docs:buildpnpm changeset
pnpm version-packages
pnpm releaseApache-2.0