
# The definition: token(), issue(), open()

> What a session definition does besides `use()`: reading the raw token, minting a token without a request, and opening a token by hand. The same four members on every entry.

```ts
import { defineJWSSession, generateJWK } from "unjwt/session";

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

// in a handler
const session = await useSession.use(event); // the framework's event, see Frameworks
await session.update({ user: "alice" });
// session.id: "a1d9390e-…", session.token: "eyJraWQiOiJ1UklV…"

// in the next request
useSession.token(event); // "eyJraWQiOiJ1UklV…"

// anywhere, no request needed
const token = await useSession.issue({ user: "bob" });
const opened = await useSession.open(token);
// { id: "7246385f-…", createdAt: 1792068521000, expiresAt: 1792069421000, data: { user: "bob" } }
```

`unjwt/session` and the Elysia entry export `defineJWSSession()` and `defineJWESession()`, and each returns the same four members; `use()` and `token()` take the framework's event, a `context` on Elysia. The signatures are on [Web standard sessions](/frameworks/session#signature).

## What each one does

| Member | Reads request | Writes cookie | Fires hooks | Returns |
| --- | --- | --- | --- | --- |
| `use(event)` | once | on `update()`, `clear()`, `rolling`, or to drop an expired or unreadable token, when the token came from the cookie | `onRead`, `onExpire`, `onError`, then `onUpdate` and `onClear` | The [session manager](/frameworks#the-session-manager). |
| `token(event)` | yes | no | none | The raw token, or `undefined`. |
| `issue(data, options?)` | no | no | none | A compact token for `data`. |
| `open(token)` | no | no | key lookup only | `{ id, createdAt, expiresAt, data }`. |

The manager's `update()` and `clear()` are the writes. `update()` fires `onUpdate` (or `onError` when issuing fails), and `clear()` fires `onClear`.

## token()

Returns the token the session would read: the token of this request's session once `use()` has read, updated or cleared it, else the session header's when the request carries the header, else a cookie this response already set, then the request cookie (chunks joined). A session header without a token returns `undefined`, never the cookie. After a `clear()` in the same request it returns `undefined`, header included. It checks nothing. Use it to forward the token to another service that verifies it on its own.

## issue()

Mints a compact token for `data`, with a fresh `jti` (from `generateId`), `iat` now and `exp` from `maxAge`. `options.maxAge` sets the lifetime of this token alone. It reads no request, writes no cookie and fires no hooks, so it runs outside a handler as well.

`jti`, `iat`, `exp` and `nbf` in `data` are dropped. A `__proto__`, `constructor` or `prototype` key throws `ERR_INVALID_OPTION`.

A session with `cookie: false` and `sessionHeader: false` has no token source, and `issue()` is what it is for: a token handed out by some other channel, such as a magic link or an email verification link.

```ts
const magicLink = defineJWSSession({
  key,
  maxAge: "15m",
  cookie: false,
  sessionHeader: false,
});

const token = await magicLink.issue({ email: "alice@example.com" });
// send `https://app.example/login?token=${token}`

// in the login handler
const { data } = await magicLink.open(getQuery(event).token);
// { email: "alice@example.com" }
```

To hand the client the token of the request's own session, return `session.token` from the manager after `update()`.

## open()

Opens a token that did not come from this request's cookie or header, such as one posted in a body, passed in a link or received over a WebSocket. The checks are those of a session read: the session `typ`, the pinned `alg` (and `enc`), `jti` and `iat`, and, when `maxAge` is set, `exp` and `iat + maxAge`. The request's session is left alone, and only the key lookup hook runs.

```ts
const { id, createdAt, expiresAt, data } = await useSession.open(token);
// id: "7246385f-…", createdAt: 1792068521000, expiresAt: 1792069421000, data: { user: "bob" }
```

It resolves to `{ id, createdAt, expiresAt, data }`, typed `OpenedSessionState<T, MaxAge>`. `id` and `createdAt` are always set, `data` is typed by `T`, and `expiresAt` is a `number` when `maxAge` is in the config.

[`onVerifyKeyLookup`](/frameworks/hooks#key-lookup-hooks) and `onUnsealKeyLookup` receive the event only when the token came through `use()`. `open()` passes none, so they get the header and the config alone and the event is typed as optional there: `open()` works anywhere, and a request's own token goes through `use()`.

Failures throw, since there is no `onError` to receive them:

| Input | Throws |
| --- | --- |
| An expired session token, or one past `iat + maxAge` | `ERR_JWT_EXPIRED` |
| A malformed token | `ERR_SESSION_TOKEN_INVALID`, `cause` `ERR_JWS_INVALID` |
| A tampered JWE | `ERR_SESSION_TOKEN_INVALID`, `cause` `ERR_JWE_DECRYPTION_FAILED` |
| A throw from key lookup | `ERR_SESSION_TOKEN_INVALID`, `cause` `ERR_JWK_KEY_NOT_FOUND` |

## update() and clear()

The manager `use()` returns does the writing. `session.update(data?, { maxAge? })` issues a new token and writes the cookie, or, on a session read from the session header, leaves the new token on `session.token` alone; inside a hook, `session.update()` on the manager the hook receives does the same thing. A hook that only has the event, such as `onExpire`, opens the manager with `use()`:

```ts
const refreshSession = defineJWESession({ key: refreshKey, name: "refresh_token" });

const accessSession = defineJWSSession({
  key,
  name: "access_token",
  maxAge: "15m",
  hooks: {
    async onExpire({ event }) {
      const refresh = await refreshSession.use(event);
      if (refresh.data.sub) {
        await (await accessSession.use(event)).update({ sub: refresh.data.sub });
      }
    },
  },
});
```

`session.clear()` expires the cookie (none on a session read from the header) and leaves an empty session for the rest of the request. `onClear` receives the session as it stood in this request, so a request that carried no session passes `oldSession: undefined`.

## Errors

These members throw the same codes as [`use()`](/frameworks/session#errors). `use()` and `token()` also throw `ERR_SESSION_CONFIG_INVALID` when `cookie` and `sessionHeader` are both `false`; `issue()` and `open()` work on such a session. `open()` throws where `use()` reports to hooks, as listed above.
