SessionKit - v0.1.0

SessionKit

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 -> principal projection
  • 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 redis

This 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 is session.ttlSeconds)
  • hydrateContext: whether to set auth context immediately in current request (default is true)
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: if true, cookie is cleared even when store delete fails; if false, 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:

  • rolling
  • touchEverySeconds
  • renewBeforeSeconds

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:

  • touch
  • close

MapSessionStore constructor options are:

  • cleanupIntervalSeconds
  • maxSize
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 returning Response | 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 returning Response | 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:

  • keyPrefix
  • codec
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:

  • keyPrefix
  • acquireTimeoutMs
  • retryDelayMs
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:

  • url
  • host
  • port
  • username
  • password
  • database
  • tls
  • lazyConnect
  • redisOptions
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:

  1. Define SessionPayload and Principal types.
  2. Choose store strategy (MapSessionStore for local development, RedisSessionStore for shared/runtime environments).
  3. Configure cookie policy (httpOnly/sameSite/secure/domain/path).
  4. Configure session.ttlSeconds and whether rolling renewal is required.
  5. Add middleware() and requireAuth() in routes.
  6. If token rotation is needed, configure token.shouldRefresh, token.refresh, and token.onRefreshFail.
  7. For multi-instance deployment, add RedisLockProvider to avoid refresh races.
  8. Add integration tests for login, expiry, invalid session handling, and logout.
pnpm install
pnpm typecheck
pnpm build
pnpm test
pnpm docs:build
pnpm changeset
pnpm version-packages
pnpm release

Apache-2.0