
# 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.

```ts
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

```ts
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()`](/frameworks/elysia#options) and `jweSession()` take. `contextKey` only names the property and the guard of `plugin()`; with `use()` you choose where the manager goes.

## Methods

| Method | What it does |
| --- | --- |
| `use(context)` | Reads the request's session and returns its manager, the one described under [the session manager](/frameworks#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()`](/frameworks/elysia) 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:

```ts
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`](/frameworks/hooks#key-lookup-hooks) 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:

```ts
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`](/frameworks/hooks#rolling-expiry).

Hooks receive the context you passed as `context`:

```ts
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](/frameworks/elysia#errors). 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`.
