> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Clerk

> Authenticate requests and manage users with Clerk behind the framework's IAuth interface.

`ClerkAuth` is the [Auth](/components/auth) component's Clerk-backed strategy. It wraps the official `@clerk/backend` SDK to verify session tokens, resolve the current user, and manage accounts — banning, locking, metadata, sessions, and password sign-in — all behind the framework's `IAuth` interface. A companion `ClerkAuthMiddleware` plugs Clerk into the request pipeline and maps a Clerk user onto the framework `IUser`, so your controllers read `context.user` and never depend on Clerk directly.

## Why Clerk

* **Token verification built in.** `getCurrentUser(token)` verifies the Clerk session token with your secret key and resolves the user in one call.
* **Drop-in middleware.** `ClerkAuthMiddleware` extracts the bearer token, enforces route roles, and maps the Clerk user onto `IUser`.
* **Full account management.** Ban, lock, update profiles, manage metadata, and revoke sessions through the Clerk Backend API.
* **Password sign-in.** `signIn()` verifies credentials and mints a Clerk sign-in token.
* **Container-managed.** Registered with `@decorator.auth()` and resolved from the container — no manual wiring.

## Installation

`ClerkAuth` ships with `@ooneex/auth` and depends on the Clerk Backend SDK.

```bash theme={null}
bun add @ooneex/auth @clerk/backend
```

## Environment variables

| Variable           | Required | Purpose                                                                            |
| ------------------ | -------- | ---------------------------------------------------------------------------------- |
| `CLERK_SECRET_KEY` | Yes      | Clerk Backend API secret key. Missing throws `AuthException` (`API_KEY_REQUIRED`). |

```bash theme={null}
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
```

The secret key is validated when `ClerkAuth` is constructed, so a missing key fails fast at startup rather than on the first request.

## How it works

`ClerkAuthMiddleware` runs in the request pipeline. For each request it:

1. Reads the token from the `Authorization: Bearer <token>` header (or a `bearerToken` query).
2. Checks the route's `roles` — if the route is guest-only (no roles or `ROLE_GUEST`), it lets the request through without a token.
3. Otherwise calls `ClerkAuth.getCurrentUser(token)`, which verifies the token and loads the Clerk user.
4. Maps the Clerk user onto the framework `IUser` and sets it on `context.user`.

```
Request → Bearer token
  → ClerkAuthMiddleware: route requires roles?
      → guest-only  → continue, no token needed
      → protected   → verifyToken → getUser → context.user
  → Controller (context.user is available)
```

When mapping, the middleware pulls the user's primary email, derives roles and the internal id from Clerk's `privateMetadata` (falling back to `ROLE_USER`), and copies across name, phone, avatar, activity timestamps, and ban/lock flags.

## Usage

Register the middleware (and `ClerkAuth`) with the container, then declare route roles — the middleware enforces them automatically.

```typescript theme={null}
import { container } from "@ooneex/container";
import { ClerkAuth } from "@ooneex/auth";

const auth = container.get(ClerkAuth);

// Resolve the current user from a session token
const user = await auth.getCurrentUser(token); // User | null
```

Protect a route by declaring the roles it requires. The middleware verifies the Clerk token only when roles are present:

```typescript theme={null}
import { Route } from "@ooneex/routing";

@Route.get("/api/profile", {
  name: "profile",
  version: 1,
  description: "Get the current user profile",
  roles: ["ROLE_USER"], // requires a valid Clerk token
})
export class ProfileController {
  public async index(context: ContextType<ProfileRouteType>) {
    const { user } = context; // mapped from Clerk by the middleware
    return context.response.json({
      id: user.id,
      email: user.email,
      roles: user.roles,
    });
  }
}
```

### Password sign-in

`signIn()` looks up the user by email, verifies the password through Clerk, and returns the user plus a sign-in token (default TTL 30 days):

```typescript theme={null}
const { user, token } = await auth.signIn({
  email: "ada@example.com",
  password: "s3cret",
  ttl: 2_592_000, // optional, seconds
});

await auth.signOut(sessionId); // revokes the Clerk session
```

### Account management

`ClerkAuth` exposes the Clerk user and session APIs directly:

| Method                                     | Purpose                                         |
| ------------------------------------------ | ----------------------------------------------- |
| `getCurrentUser(token)`                    | Verify a token and resolve the user, or `null`. |
| `getUser(userId)`                          | Fetch a user by id.                             |
| `banUser(userId)` / `unbanUser(userId)`    | Ban or unban a user.                            |
| `isBanned(userId)`                         | Whether the user is banned.                     |
| `lockUser(userId)` / `unlockUser(userId)`  | Lock or unlock a user.                          |
| `isLocked(userId)`                         | Whether the user is locked.                     |
| `updateUser(userId, params)`               | Update user attributes.                         |
| `updateUserProfileImage(userId, { file })` | Replace the profile image.                      |
| `updateUserMetadata(userId, params)`       | Update public/private/unsafe metadata.          |
| `getUserMetadata(userId)`                  | Read public/private/unsafe metadata.            |
| `deleteUser(userId)`                       | Delete a user.                                  |
| `deleteUserProfileImage(userId)`           | Remove the profile image.                       |
| `getSession(sessionId)`                    | Fetch a session.                                |
| `getCurrentUserSession(token)`             | Resolve the session for a token, or `null`.     |
| `signIn({ email, password, ttl? })`        | Verify credentials and mint a sign-in token.    |
| `signOut(sessionId)`                       | Revoke a session.                               |

## Use in the app

In an `@ooneex/app` application, Clerk plugs in as a request middleware. Add `ClerkAuthMiddleware` to the `middlewares` slot of your `App` config — importing it registers both `ClerkAuthMiddleware` and `ClerkAuth` with the container through `@decorator.auth()`, so there is nothing else to wire.

```bash theme={null}
bun add @ooneex/app @ooneex/auth @clerk/backend
```

```typescript theme={null}
import { App } from "@ooneex/app";
import { ClerkAuthMiddleware } from "@ooneex/auth";
import { TerminalLogger } from "@ooneex/logger";

const app = new App({
  routing: { prefix: "/api" },
  loggers: [TerminalLogger],
  middlewares: [ClerkAuthMiddleware], // runs on every matched route
});

await app.run();
```

Once registered, the middleware runs on each matched route: it reads the bearer token, enforces the route's `roles`, and maps the Clerk user onto `context.user`. Controllers then read `context.user` and declare the roles they require — see [Protecting routes](#usage) above. Set `CLERK_SECRET_KEY` in your `.env.yml` (or environment) so the strategy can verify tokens.

## Exceptions

`ClerkAuth` and `ClerkAuthMiddleware` throw `AuthException` with a machine-readable `key`, so callers can branch on it.

| Key                    | When                                                      |
| ---------------------- | --------------------------------------------------------- |
| `API_KEY_REQUIRED`     | `ClerkAuth` is constructed without `CLERK_SECRET_KEY`.    |
| `INVALID_CREDENTIALS`  | `signIn()` is given an unknown email or a wrong password. |
| `MISSING_BEARER_TOKEN` | A protected route is requested without a token.           |
| `INVALID_TOKEN`        | The token verifies to no user.                            |
| `NO_PRIMARY_EMAIL`     | The Clerk user has no primary email address.              |

```typescript theme={null}
import { AuthException } from "@ooneex/auth";

try {
  const { user, token } = await auth.signIn({ email, password });
  return context.response.json({ user, token });
} catch (error) {
  if (error instanceof AuthException && error.key === "INVALID_CREDENTIALS") {
    return context.response
      .status(401)
      .json({ error: "Invalid email or password" });
  }
  throw error;
}
```

## Best practices

* **Keep `CLERK_SECRET_KEY` in the environment.** Never hard-code it; load it from `.env` so it stays out of source control.
* **Store roles and the internal id in Clerk metadata.** The middleware reads `privateMetadata.roles` and `privateMetadata.externalId` — set them so users map onto `IUser` correctly.
* **Read `context.user`, not Clerk.** Keep controllers provider-agnostic so the auth strategy can change without touching handlers.
* **Drive access from route roles.** Declare `roles` on routes and let `ClerkAuthMiddleware` enforce them instead of re-checking tokens in controllers.
* **Branch on `AuthException.key`.** Return `401` for `INVALID_CREDENTIALS`, `MISSING_BEARER_TOKEN`, and `INVALID_TOKEN`.

See the [Auth component](/components/auth) for the strategy interface and route-protection model.
