
# h3

> An h3 v2 `H3Event` is `{ req, res, context }`, so it goes to `use()` as it is. No adapter, no `h3` peer dependency.

```ts
import { H3, HTTPError, serve } from "h3";
import { type WebSessionEvent, defineJWESession, deriveJWKFromPassword } from "unjwt/session";

const key = await deriveJWKFromPassword(process.env.SESSION_PASSWORD!, {
  alg: "PBES2-HS256+A128KW",
  salt: "my-app-session",
  iterations: 600_000,
});

const session = defineJWESession<{ user: string }, "1h">({ key, maxAge: "1h", rolling: "15m" });
const useAppSession = (event: WebSessionEvent) => session.use(event);

const app = new H3()
  .post("/login", async (event) => {
    const body: { user?: unknown } = await event.req.json();
    if (typeof body.user !== "string")
      throw new HTTPError({ status: 400, message: "user required" });
    const current = await useAppSession(event);
    await current.update({ user: body.user });
    return { id: current.id };
  })
  .get("/me", async (event) => {
    const current = await useAppSession(event);
    if (!current.id) throw new HTTPError({ status: 401 });
    return current.data;
  })
  .post("/logout", async (event) => {
    await (await useAppSession(event)).clear();
    return { ok: true };
  });

serve(app);
```

The session state lives on `event.context.sessions`, so a middleware and a handler on the same event share one read.

## A handler that throws after a write

h3 sends a thrown handler's error with `res.errHeaders` only and discards the prepared `res.headers`. The session writes each cookie into both: when the event's `res` carries an `errHeaders`, every `Set-Cookie` line of the session (a write, a chunk, a drop) replaces that cookie's lines there too, and the other lines in `errHeaders` stay. A `clear()` followed by `throw new HTTPError({ status: 401 })` therefore signs the client out, and an `update()` before a throw still sets the cookie:

```ts
app.post("/logout", async (event) => {
  await (await useAppSession(event)).clear();
  throw new HTTPError({ status: 401 }); // the 401 carries the cookie's Max-Age=0
});
```

This holds for `update()`, `rolling`, `clear()` and the drop of an expired or unreadable cookie during the read. It is the one h3-specific step `unjwt/session` takes, and it only reads `res.errHeaders`, so any event shaped `{ req, res: { headers, errHeaders } }` gets it.

## WebSockets

h3's `defineWebSocketHandler` runs on crossws 0.4: the `upgrade` hook receives a `Request` with an optional `context`, a thrown `Response` refuses, and the object it returns carries `headers` for the `101` handshake and `context` for every later hook's `peer.context`. The session is read once there; nothing can be written to the client after the upgrade.

```ts
import { defineWebSocketHandler } from "h3";

export default defineWebSocketHandler({
  async upgrade(request) {
    const headers = new Headers();
    const session = await useAppSession({
      headers: request.headers,
      context: request.context,
      res: { headers },
    });
    if (!session.id) throw new Response("Sign in first", { status: 401 });
    return { headers, context: { ...session } }; // a roll or a drop made during the read rides on the 101
  },
  open(peer) {
    peer.send(`hello ${(peer.context as { data: { user: string } }).data.user}`);
  },
});
```

The fields are named rather than spread because a spread drops a `Request`'s `headers` getter. crossws 0.4 merges the returned headers with `set`, which keeps one `Set-Cookie` line: a session that fits one cookie is what rides the upgrade.
