# Login and sessions

Mount login routes, link local users, and run model calls in the personal user session.

Every standard or premium call needs a personal Enlitt session. Your model
key authenticates your application; it does not sign in a user. If the user is
signed out or their session is expired, the call fails with
`EnlittLoginRequiredError` before a premium provider runs.

## How login works

1. Your browser navigates to your server's login-start route.
2. The handler redirects to Enlitt, where the user signs in and authorizes your application.
3. Enlitt redirects to your registered callback URL with a one-time code.
4. Your callback handler validates the saved state and exchanges the code using your App key.
5. The handler sets an `HttpOnly`, `Secure`, `SameSite=Lax` cookie on your application's domain.
6. On later requests, session middleware reads that cookie and makes the session available to model calls.

## Mount the three handlers

The handlers use standard Web `Request` and `Response` objects. Route the
returned functions through your framework's server routing system:

```ts
import {
  createLoginStartHandler,
  createCallbackHandler,
  createLogoutHandler,
  type CallbackConfig,
  type StateStore
} from "@enlitt/sdk/server";

export function createAuthRoutes(stateStore: StateStore) {
  // The public client ID; `npx enlitt setup` writes it to .env.local.
  const clientId = process.env.ENLITT_CLIENT_ID;
  if (!clientId) throw new Error("Set ENLITT_CLIENT_ID on the server");

  const config: CallbackConfig = {
    clientId,
    appOrigin: "https://your-app.example",
    redirectUri: "https://your-app.example/auth/enlitt/callback",
    defaultReturnTo: "/",
    stateStore
    // secretKey defaults to the server's ENLITT_SECRET_KEY.
    // Against Enlitt staging only: enlittOrigin: "https://staging.enlitt.com"
  };

  return {
    login: createLoginStartHandler(config),
    callback: createCallbackHandler(config),
    logout: createLogoutHandler(config)
  };
}
```

Mount `login` at `/auth/enlitt/login`, `callback` at the exact registered
`/auth/enlitt/callback`, and `logout` at `/auth/enlitt/logout` on your server.
These paths are example choices for your application.

* **Methods.** Login-start and the callback are browser navigations, so mount
  them for `GET`. Logout accepts any method: a link (`GET`) or a form (`POST`).
  To keep other sites from signing your users out with a link, mount logout for
  `POST` only.
* **Responses.** Every response is a `303` redirect with `Cache-Control: no-store`.
* **Where the user lands.** Pass `?return_to=/some/path` to login-start or
  logout. It must be a path on `appOrigin`; anything else falls back to
  `defaultReturnTo`, so a login link cannot become an open redirect.
* **Failures.** The callback refuses a missing, unknown, reused, or expired
  `state` before it exchanges anything, and sets no cookie.

### `CallbackConfig`

| Field             | Required | Meaning                                                                                                      |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `clientId`        | Yes      | The application's public client ID (`ENLITT_CLIENT_ID`).                                                     |
| `secretKey`       | No       | The App key. Defaults to `process.env.ENLITT_SECRET_KEY`.                                                    |
| `redirectUri`     | Yes      | Your callback URL. Must equal a registered callback URL exactly.                                             |
| `appOrigin`       | Yes      | Your application's origin, such as `https://your-app.example`. Bounds every redirect, and scopes the cookie. |
| `defaultReturnTo` | Yes      | Path on `appOrigin` to land on when no valid `return_to` was given.                                          |
| `stateStore`      | Yes      | Server-side login-state storage; see below.                                                                  |
| `enlittOrigin`    | No       | Where users sign in. Defaults to `https://enlitt.com`. Set `https://staging.enlitt.com` against staging.     |
| `enlittApiOrigin` | No       | Origin for the server-to-server code exchange and revocation. Defaults to `enlittOrigin`.                    |
| `cookieName`      | No       | Session cookie name. Defaults to `enlitt_session`. Use the same name in the session middleware.              |
| `onSignIn`        | No       | Hook that links the signed-in user to your own users; see below.                                             |

The login handlers do **not** read `ENLITT_BASE_URL`; that variable steers only
model calls. See [Test and live applications](/docs/environments.md#target-enlitt-staging).

### The `StateStore`

Login-start creates a one-time `state` value and saves it; the callback must find
and consume it. You supply where it is saved:

```ts
interface StateRecord {
  returnTo?: string;
  createdAt: number; // epoch milliseconds
  expiresAt: number; // epoch milliseconds
}

interface StateStore {
  set(state: string, record: StateRecord, ttlMs: number): void | Promise<void>;
  get(state: string): StateRecord | null | Promise<StateRecord | null>;
  // Read and delete in one step, so a state can be used only once.
  consume(state: string): StateRecord | null | Promise<StateRecord | null>;
}
```

Each method may return its value directly or as a promise. Return `null` for a
state that is unknown or older than its `ttlMs` (milliseconds). `consume` must be
atomic: two callbacks racing on one state must not both receive the record.

For one process in local development, a `Map` is enough:

```ts
import type { StateRecord, StateStore } from "@enlitt/sdk/server";

// Development only: every server process has its own Map, and a restart loses it.
export function memoryStateStore(): StateStore {
  const records = new Map<string, { record: StateRecord; deadline: number }>();
  const live = (state: string): StateRecord | null => {
    const entry = records.get(state);
    if (!entry) return null;
    if (entry.deadline <= Date.now()) {
      records.delete(state);
      return null;
    }
    return entry.record;
  };
  return {
    set(state, record, ttlMs) {
      records.set(state, { record, deadline: Date.now() + ttlMs });
    },
    get: live,
    consume(state) {
      const record = live(state);
      records.delete(state);
      return record;
    }
  };
}
```

In production, and in any deployment with more than one process or instance
(serverless functions included), the login request and the callback can reach
different instances, so use shared storage. With Redis (`redis` package, Redis
6.2 or later):

```ts
import type { StateRecord, StateStore } from "@enlitt/sdk/server";
import type { RedisClientType } from "redis";

export function redisStateStore(redis: RedisClientType): StateStore {
  const key = (state: string) => `enlitt:login-state:${state}`;
  const parse = (value: string | null): StateRecord | null =>
    value === null ? null : (JSON.parse(value) as StateRecord);
  return {
    async set(state, record, ttlMs) {
      await redis.set(key(state), JSON.stringify(record), { PX: ttlMs });
    },
    async get(state) {
      return parse(await redis.get(key(state)));
    },
    async consume(state) {
      return parse(await redis.getDel(key(state))); // atomic read and delete
    }
  };
}
```

A database table with an expiry column works the same way, provided `consume`
deletes and returns the row in one statement.

## Next.js App Router

Each handler is a Route Handler. Build the handlers once in a server-only
module, then export them from the route files:

```ts
// lib/enlitt/auth.ts
import "server-only";
import { createAuthRoutes } from "./create-auth-routes"; // the function above
import { redisStateStore } from "./state-store";
import { redis } from "../redis";

export const enlittAuth = createAuthRoutes(redisStateStore(redis));
```

```ts
// app/auth/enlitt/login/route.ts
import { enlittAuth } from "@/lib/enlitt/auth";
export const GET = enlittAuth.login;
```

```ts
// app/auth/enlitt/callback/route.ts
import { enlittAuth } from "@/lib/enlitt/auth";
export const GET = enlittAuth.callback;
```

```ts
// app/auth/enlitt/logout/route.ts
import { enlittAuth } from "@/lib/enlitt/auth";
export const POST = enlittAuth.logout;
```

Sign in with a plain link, `<a href="/auth/enlitt/login?return_to=/chat">`, and
sign out with `<form method="post" action="/auth/enlitt/logout">`. Do not use a
client-side `fetch` for either: they are full-page redirects.

Wrap each Route Handler that makes model calls, as in
[Wrap the model request](#wrap-the-model-request). Next.js `middleware.ts` (or
`proxy.ts`) cannot do this: the session context must surround the code that
calls the model, in the same request.

## Link your own users and history

The successful server-side code exchange returns an opaque `session_token`
and a verified `subject`. The subject identifies this personal user **within
this application**. It stays the same across devices, new login sessions, key
rotation, and revocation followed by reauthorization. Other applications receive
a different subject for the same person. Treat it as an opaque string, not an
email, internal Enlitt user ID, or credential.

Add the optional `onSignIn` hook to your `CallbackConfig` to find or create a
user in your own database and establish your own application session:

```ts
import type { CallbackConfig } from "@enlitt/sdk/server";

type LocalUserStore = {
  // Enforce a unique constraint on the Enlitt subject for concurrent logins.
  findOrCreateByEnlittSubject(subject: string): Promise<{ id: string }>;
};
type LocalSessions = {
  // Return your application's signed/opaque HttpOnly, Secure, SameSite=Lax cookie.
  createCookie(userId: string): Promise<string>;
};

export function withLocalUsers(
  config: CallbackConfig,
  users: LocalUserStore,
  sessions: LocalSessions
): CallbackConfig {
  return {
    ...config,
    async onSignIn({ subject }, response) {
      const user = await users.findOrCreateByEnlittSubject(subject);
      const cookie = await sessions.createCookie(user.id);
      response.headers.append("set-cookie", cookie);
    }
  };
}
```

Pass the resulting config to `createCallbackHandler`. The SDK awaits the hook
before appending its own Enlitt cookie; both cookies reach the browser on the
same redirect. The hook runs once per consumed login state and receives only
the verified subject and the redirect response. A subject in the callback URL
is ignored. Failed login or malformed exchange data never invokes the hook.
If your database/session setup fails, its error propagates to your server and
the SDK returns no successful login response. Handle that error in your server
without logging credentials.

Store your app's conversations against its local user ID (or the unique Enlitt
subject), and authorize history requests through your own authenticated session.
An unsigned cookie containing the subject is not proof of identity. The SDK
neither stores the subject nor adds it to `getEnlittSession()`; that function
still returns only an opaque model-call credential. Your local session lifecycle
is yours: clear its cookie in your logout route as well as calling the SDK's
logout handler. Enlitt revocation immediately prevents further model calls, but
it does not automatically revoke your independent application session.

Existing integrations may omit the hook. Older SDKs ignore the additive
`subject` response field. Deploy the updated Enlitt backend before using this
SDK's updated callback handler, which requires a valid subject in the exchange.

## Wrap the model request

Run the model operation inside the middleware's `next` callback. Creating a
middleware function without invoking it around the operation does not install session context.

```ts
import { enlittSessionMiddleware } from "@enlitt/sdk/core";
import { enlitt, ENLITT_STANDARD_MODEL } from "@enlitt/sdk/vercel";
import { generateText } from "ai";

const withSession = enlittSessionMiddleware();

export async function POST(request: Request): Promise<Response> {
  return withSession(request, async () => {
    const result = await generateText({
      model: enlitt(ENLITT_STANDARD_MODEL),
      prompt: "Write a short welcome message."
    });
    return Response.json({ text: result.text });
  });
}
```

Add [error handling](/docs/errors.md) to send signed-out users through login.
The middleware itself allows requests with no cookie to continue; it does not
choose which routes your application protects.

To check whether the visitor is signed in, call `getEnlittSession()`, also from
`@enlitt/sdk/core`, inside the same wrapper:

```ts
import { enlittSessionMiddleware, getEnlittSession } from "@enlitt/sdk/core";

const withSession = enlittSessionMiddleware();

// app/api/me/route.ts: lets your UI decide between "Sign in" and "Sign out".
export async function GET(request: Request): Promise<Response> {
  return withSession(request, async () => Response.json({ signedIn: getEnlittSession() !== null }));
}
```

It returns `null` for a signed-out request inside the wrapper, and throws
`EnlittConfigurationError` when called outside one. A non-null session may still
be expired or revoked; Enlitt checks validity when you make a call, which then
fails with `EnlittLoginRequiredError`.

If you customize `cookieName`, give the same name to the login handlers and
session middleware. If your framework uses a different middleware shape,
`runWithEnlittSession(session, callback)` provides the same request context.

## Background jobs and linked accounts

For a server job outside an HTTP request, construct a session from an actual
Enlitt token your server securely stored, then pass it explicitly:

```ts
import { enlittSession } from "@enlitt/sdk/core";
import { enlitt, ENLITT_STANDARD_MODEL } from "@enlitt/sdk/vercel";

export function modelForLinkedAccount(storedToken: string) {
  const session = enlittSession(storedToken);
  return enlitt(ENLITT_STANDARD_MODEL, { session });
}
```

A session token is opaque. Do not derive one from an email, user ID, or client
input. Never expose it or your business key in logs or browser JavaScript.
Keep all SDK model, callback, and reporting imports in server-only modules.
The browser carries the HttpOnly cookie automatically.
