# `resolveUserId` — wiring your auth

The proxy injects an end-user id (server-side) so each of your users gets an
isolated agent context. `resolveUserId(request)` maps the incoming request to
**your** authenticated user id. It's **required** (fail-closed) — pass
`{ singleTenant: true }` only if you deliberately want one shared scope.

Drop one of these into `app/api/m8tes/[...path]/route.ts`.

### Clerk

```ts
import { auth } from "@clerk/nextjs/server";
import { createM8tesHandler } from "@m8tes/react/server";

export const { GET, POST } = createM8tesHandler({
  apiKey: process.env.M8TES_API_KEY!,
  resolveUserId: async () => {
    const { userId } = await auth();
    return userId; // null when signed out -> the proxy returns 401
  },
});
```

### Auth.js / NextAuth (v5)

```ts
import { auth } from "@/auth";
import { createM8tesHandler } from "@m8tes/react/server";

export const { GET, POST } = createM8tesHandler({
  apiKey: process.env.M8TES_API_KEY!,
  resolveUserId: async () => (await auth())?.user?.id ?? null,
});
```

### Supabase (SSR)

```ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { createM8tesHandler } from "@m8tes/react/server";

export const { GET, POST } = createM8tesHandler({
  apiKey: process.env.M8TES_API_KEY!,
  resolveUserId: async () => {
    const store = await cookies();
    const supabase = createServerClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
      { cookies: { getAll: () => store.getAll(), setAll: () => {} } },
    );
    const { data } = await supabase.auth.getUser();
    return data.user?.id ?? null;
  },
});
```

### Custom session cookie / JWT

`resolveUserId` receives the Web `Request`, so read your own cookie/header and
verify it:

```ts
import { jwtVerify } from "jose";
import { createM8tesHandler } from "@m8tes/react/server";

const secret = new TextEncoder().encode(process.env.SESSION_SECRET!);

export const { GET, POST } = createM8tesHandler({
  apiKey: process.env.M8TES_API_KEY!,
  resolveUserId: async (request) => {
    const token = request.headers.get("cookie")?.match(/session=([^;]+)/)?.[1];
    if (!token) return null;
    try {
      const { payload } = await jwtVerify(token, secret);
      return String(payload.sub);
    } catch {
      return null; // invalid token -> 401
    }
  },
});
```

### Single-tenant (no per-user isolation)

Only when every end-user should share one scope (e.g. an internal tool):

```ts
export const { GET, POST } = createM8tesHandler({
  apiKey: process.env.M8TES_API_KEY!,
  singleTenant: true,
});
```

### Express (or any Node framework)

Outside Next, use `createM8tesProxy` — it returns a framework-agnostic
`(Request) => Promise<Response>`. Bridge Express's `req`/`res` to the Web Fetch
types and pipe the response through **unbuffered** so SSE streaming works:

```ts
import express from "express";
import { Readable } from "node:stream";
import { createM8tesProxy } from "@m8tes/react/server";

const proxy = createM8tesProxy({
  apiKey: process.env.M8TES_API_KEY!,
  // resolveUserId receives the Web Request — read your own cookie/header off it.
  resolveUserId: (request) => verifySession(request.headers.get("cookie"))?.userId ?? null,
});

const app = express();

// Mount as a catch-all. Do NOT add express.json() on this route — the proxy
// reads the raw body itself (and enforces a 1 MiB cap). Disable compression here.
app.all("/api/m8tes/*", async (req, res) => {
  const headers = new Headers();
  for (const [k, v] of Object.entries(req.headers)) {
    if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));
    else if (v != null) headers.set(k, v);
  }
  // Abort the upstream stream when the browser disconnects (else SSE runs leak).
  const ac = new AbortController();
  req.on("close", () => ac.abort());

  const hasBody = req.method !== "GET" && req.method !== "HEAD";
  const request = new Request(`${req.protocol}://${req.get("host")}${req.originalUrl}`, {
    method: req.method,
    headers,
    body: hasBody ? (Readable.toWeb(req) as ReadableStream) : undefined,
    duplex: "half",
    signal: ac.signal,
  });

  const response = await proxy(request);
  res.status(response.status);
  response.headers.forEach((value, key) => res.setHeader(key, value));
  if (response.body) Readable.fromWeb(response.body).pipe(res);
  else res.end();
});
```

Point the browser client at this origin's `/api/m8tes` (the provider's default).
The same shape adapts to Hono, Remix, or any server that exposes a raw request
stream; first-class adapters for those land in a later release.

> **Never** derive the user id from a client-supplied body/query field — the
> proxy strips `user_id`/`end_user_id` from the request precisely so a browser
> can't spoof another end-user. Always resolve it from your server session.

## `resolveAgentId` — one mate per end-user

`resolveUserId` decides *whose* data a run sees. `resolveAgentId` decides *which
mate* runs it. You need the second one as soon as the agent acts on your own API
as the signed-in user, because a custom tool is scoped to one end-user and a mate
carrying it is unusable by anyone else — the API 404s the scope mismatch. A single
static `agentId` cannot serve every end-user; pin per request instead:

```ts
export const { GET, POST } = createM8tesHandler({
  apiKey: process.env.M8TES_API_KEY!,
  resolveUserId: async (req) => (await auth(req)).userId,
  resolveAgentId: async ({ userId }) => db.agents.findByUser(userId),
});
```

It receives **only** the resolved `userId` — deliberately not the `Request`. The
browser controls the request, so keying mate selection off a query param or a
non-HttpOnly cookie would hand that choice straight back to it. Look the mate up
from the trusted id.

Behaviour worth knowing:

- Fail-closed. If it throws, or returns anything that isn't a positive integer id
  (null, `""`, `0`, `"0"`, `NaN`, a negative), the request 401s. It never falls
  back to an unpinned run, because an unpinned run lets the browser choose the
  mate — and upstream those falsy values would silently auto-select one.
- Mutually exclusive with the static `agentId` — passing both throws at setup.
- Needs `resolveUserId`, so it can't be combined with `{ singleTenant: true }`.
