defineJWSSession() / defineJWESession()

Define a session once and open its manager from Elysia's cookie and request yourself, for a resolve, derive or guard you write by hand, or issue and open its tokens outside a request.

import { Elysia } from "elysia";
import { defineJWSSession, generateJWK } from "unjwt/adapters/elysia";

const key = await generateJWK("HS256");
const useSession = defineJWSSession({ key, maxAge: "1h" });

const app = new Elysia()
  .resolve(async ({ cookie, request }) => ({
    session: await useSession.use({ cookie, request }),
  }))
  .get("/login", async ({ session }) => {
    await session.update({ user: "alice" });
    return { id: session.id }; // { id: "fee86981-…" }
  })
  .get("/me", ({ session }) => ({ id: session.id ?? null, data: session.data }));
// GET /me with the cookie: { id: "fee86981-…", data: { user: "alice" } }

#Signature

function defineJWSSession<
  T extends Record<string, any> = SessionClaims,
  MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined,
  K extends string = "session",
>(config: SessionConfigJWS<T, MaxAge> & { contextKey?: K }): SessionPluginDefinition<T, MaxAge, K>;

function defineJWESession<
  T extends Record<string, any> = SessionClaims,
  MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined,
  K extends string = "session",
>(config: SessionConfigJWE<T, MaxAge> & { contextKey?: K }): SessionPluginDefinition<T, MaxAge, K>;

interface SessionPluginDefinition<T, MaxAge, K> extends SessionDefinition<
  T,
  MaxAge,
  SessionContext
> {
  plugin(): SessionPlugin<{ [P in K]: SessionManager<T, MaxAge> }, `require${Capitalize<K>}`>;
}

interface SessionDefinition<T, MaxAge, TContext> {
  use(context: TContext): Promise<SessionManager<T, MaxAge>>;
  token(context: TContext): string | undefined;
  issue(data?: Partial<SessionData<T>>, options?: { maxAge?: ExpiresIn }): Promise<string>;
  open(token: string): Promise<OpenedSessionState<T, MaxAge>>; // { id, createdAt, expiresAt, data }
}

interface SessionContext {
  cookie: ChunkableCookieJar; // Elysia's reactive `cookie` context property
  request: { headers: { get(name: string): string | null } };
}

Defining a session validates and reads nothing. Each method checks the config and the key when it runs.

#Options

config is the same object jwsSession() and jweSession() take. contextKey only names the property and the guard of plugin(); with use() you choose where the manager goes.

#Methods

MethodWhat it does
use(context)Reads the request's session and returns its manager, the one described under the session manager.
token(context)The raw token the request carries, unopened: the session's own once use() read or wrote it, else the session header's when the request carries the header, else the cookie. undefined without one.
issue(data?, options?)Mints a token for data with a fresh jti and iat, and exp from maxAge (options.maxAge overrides it). Reads no request and writes no cookie.
open(token)Opens a token into { id, createdAt, expiresAt, data } with the same checks as a cookie read: typ, pinned algorithms, jti/iat, and, when maxAge is set, exp and iat + maxAge. Throws ERR_SESSION_TOKEN_INVALID, or ERR_JWT_EXPIRED for an expired session token.
plugin()The Elysia plugin, as jwsSession() returns it.

issue() and open() need no request, so they also serve a session that is never a cookie, such as a magic link or an email verification token. Disable both token sources for such a session; use() and token() then throw ERR_SESSION_CONFIG_INVALID, while issue() and open() work:

const magicLink = defineJWESession({
  key,
  name: "magic",
  maxAge: "15m",
  cookie: false,
  sessionHeader: false,
});

const token = await magicLink.issue({ email: "alice@example.com" });
// later, from the link
const { data } = await magicLink.open(token); // { email: "alice@example.com" }

issue() strips jti, iat, exp and nbf from data, and a __proto__, constructor or prototype key throws ERR_INVALID_OPTION. onVerifyKeyLookup and onUnsealKeyLookup receive context only when the token came through use(). open() passes none, so they get the header and the config alone and context is typed as optional there. A throw from the lookup makes open() throw ERR_SESSION_TOKEN_INVALID.

#When to use it

  • You want the session under a key, or with a typing, that the plugin does not give you.
  • You open the session only on some routes, or only after another check, instead of on every request.
  • You need the manager inside a derive or beforeHandle of an app that does not mount the plugin.
  • You write a guard with its own rule, for example a role check, in place of requireSession.

The plugin is a thin wrapper over use(): a scoped resolve that calls use({ cookie, request }) and a macro that answers 401 while session.id is unset.

#The context argument

Destructure { cookie, request } in the hook signature, as above. Elysia decides which context fields to build by reading the hook's source, and naming cookie there keeps the jar in place.

The adapter reads cookies from the jar and writes back through each reactive cookie's set(). A cookie object that is not Elysia's jar fails on the first write:

const session = await defineJWSSession({ key }).use({ cookie: {}, request });
await session.update({ a: 1 });
// JWTError ERR_SESSION_CONFIG_INVALID: [unjwt/elysia] Session cookie jar has no reactive cookie at "elysia-jws", pass Elysia's `cookie`.

#Shared state within a request

The session state lives on the request object. Every use() of the same family with the same name during one request returns a manager over the same state, whether it comes from a plugin, a handler or a hook. A clear() in a hook therefore empties ctx.session too, and an update() in a hook hands the handler the new session. A hook that calls use() again gets the same session back, and onRead does not fire a second time. For sliding expiry, set rolling.

Hooks receive the context you passed as context:

hooks: {
  onUpdate: ({ session, context }) =>
    console.log("onUpdate", session.id.slice(0, 8), "has request:", "request" in context, "has cookie:", "cookie" in context),
}
// onUpdate fee86981 has request: true has cookie: true

#Errors

The same codes as the plugins. Key and config errors throw from the method that runs: use(), token(), issue() or open(). A jar without reactive cookies throws ERR_SESSION_CONFIG_INVALID from the first update() or clear(). open() throws ERR_SESSION_TOKEN_INVALID with the failure as cause, or ERR_JWT_EXPIRED.