# Usage Reference

## Package exports

| Import | Description |
|--------|-------------|
| `@appdirect/auth-bff` | Core SDK — `createAppDirectAuth()` |
| `@appdirect/auth-bff/next` | Next.js App Router handlers |
| `@appdirect/auth-bff/handlers` | Node `(req, res)` route handlers + cookie helpers |

### Exports from `@appdirect/auth-bff/handlers`

| Export | Description |
|--------|-------------|
| `createAuthRouteHandlers` | Built-in login/callback/me/jwt/refresh/logout/importSession handlers |
| `readAuthCookies` | Parse `Cookie` header → `{ session, refreshToken, oauthState, store }` |
| `applyCookieMutations` | Write `CookieMutation[]` to response (`appendHeader` when available) |
| `createHandlerCookieWriter` | Build `CookieHeaderWriter` from Node `HandlerResponse` |
| `formatSetCookieHeader` | Format one `CookieMutation` as a `Set-Cookie` string |
| `CookieHeaderWriter` | Type for response cookie writers |
| `HandlerRequest`, `HandlerResponse` | Types for Node handler adapters |
| `assertSameSiteRequest` | CSRF guard for `importSession` (Sec-Fetch-Site / Origin) |
| `readBodySessionTokens` | Parse `{ sessionToken, refreshToken }` from JSON body |

---

## `createAppDirectAuth(config, options?)`

Factory for framework-agnostic auth operations.

```typescript
import { createAppDirectAuth } from '@appdirect/auth-bff';

const auth = createAppDirectAuth({
  issuerBaseUrl: 'https://marketplace.example.com',
  clientId: 'client-id',
  clientSecret: 'client-secret',
  appBaseUrl: 'http://localhost:3000',
});

// Optional: inject fetch for tests
const authWithMock = createAppDirectAuth(config, { fetch: mockFetch });
```

### Config options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `issuerBaseUrl` | `string` | **required** | AppDirect marketplace base URL |
| `clientId` | `string` | **required** | OAuth client ID |
| `clientSecret` | `string` | **required** | OAuth client secret |
| `appBaseUrl` | `string` | **required** | Application origin (no trailing slash) |
| `callbackPath` | `string` | `/api/auth/callback` | OAuth callback path |
| `scopes` | `string[]` | `company`, `openid`, `profile`, `email` | OAuth scopes |
| `expiryBufferSec` | `number` | `60` | Treat JWT as expired this many seconds early |
| `secureCookies` | `boolean` | `NODE_ENV === 'production'` | Set `Secure` flag on cookies |
| `cookieOptions` | `object` | see below | Cookie max ages and flags |
| `routes` | `object` | `/api/auth/*` | Route paths (for error responses) |
| `onErrorRedirect` | `string` | `/?error=` | Callback error redirect prefix |

#### `cookieOptions`

| Field | Default | Description |
|-------|---------|-------------|
| `sessionMaxAge` | 604800 (7 days) | `session` cookie max age (seconds) |
| `refreshMaxAge` | 2592000 (30 days) | `refresh_token` cookie max age |
| `stateMaxAge` | 600 (10 min) | `oauth_state` cookie max age |
| `sameSite` | `'lax'` | Cookie SameSite attribute |
| `path` | `'/'` | Cookie path |
| `partitioned` | `false` | Emit `Partitioned` for CHIPS (cross-site iframe) cookies |

When `partitioned` is `true`, the SDK forces `secure: true` and defaults `sameSite` to `'none'` (required for third-party iframe contexts). Explicit `sameSite` values other than `'none'` are rejected.

#### Cookie semantics

| Cookie | Source | Purpose |
|--------|--------|---------|
| `session` | `POST /auth/token` response (`access_token` / FEJWT) | User identity + Global Header token; short TTL (~5 min) |
| `refresh_token` | `POST /auth/token` response (`refresh_token`) | **Auth-layer** refresh token for `POST /auth/refresh` only |
| `oauth_state` | Generated at login | CSRF protection during OAuth redirect |

The `refresh_token` cookie stores the **auth-layer** refresh token from `/auth/token`. It is **not** the OAuth2 `refresh_token` returned by `POST /oauth2/token`. Session renewal always uses `POST /auth/refresh` with the auth-layer token as Bearer.

---

## Core flow methods

### `auth.buildLoginRedirect()`

Starts the OAuth flow.

**Returns:** `LoginRedirectResult`

```typescript
{
  authorizeUrl: string;   // Redirect user here
  state: string;          // CSRF state value
  cookiesToSet: CookieMutation[];  // Includes oauth_state cookie
}
```

### `auth.handleCallback({ code, state, cookies })`

Completes OAuth after user returns from AppDirect.

**Parameters:**
- `code` — authorization code from query string
- `state` — state from query string
- `cookies` — `CookieStore` with `get(name)` method

**Returns:** `CallbackResult`

```typescript
{
  fejwt: string;
  refreshToken: string | null;
  user: { id, email, name, picture };
  cookiesToSet: CookieMutation[];   // session + refresh_token
  cookiesToClear: string[];         // oauth_state
}
```

**Throws:** `AuthError` with codes `no_code`, `invalid_state`, `token_exchange`, `fejwt`, `userinfo`

### `auth.refreshSession({ refreshToken })`

Renews the application session via `POST /auth/refresh` (Bearer auth refresh token).

**Returns:** `RefreshResult`

```typescript
{
  fejwt: string;  // session access_token JWT
  refreshToken: string | null;  // auth-layer refresh token
  cookiesToSet: CookieMutation[];
}
```

**Throws:** `AuthError` with code `refresh_failed`

### `auth.getMe({ sessionToken, refreshToken? })`

Returns current user, auto-refreshing expired FEJWT when a `refresh_token` is available.

**Returns:** `MeResult`

```typescript
{
  user: { id, email, name, picture } | null;
  hasSession: boolean;
  cookiesToSet?: CookieMutation[];  // Present if refresh occurred (apply in custom routes)
}
```

**`hasSession`:** `true` only when a valid FEJWT exists after refresh. `false` when unauthenticated or refresh failed.

### `auth.getUserFromSession({ sessionToken })`

Sync decode-only helper (no expiry check, no refresh). Prefer `getMe` for HTTP handlers.

**Returns:** `MeResult` without `cookiesToSet`

### `auth.ensureValidFeJwt({ sessionToken, refreshToken? })`

Central session helper. Returns a valid FEJWT, refreshing via `refresh_token` only when expired.

**Returns:** `JwtResult` (same as `getFeJwt`)

**Throws:** `AuthError` with codes `not_authenticated`, `session_expired`

### `auth.getFeJwt({ sessionToken, refreshToken? })`

Alias for `ensureValidFeJwt`. Returns valid FEJWT, refreshing if expired.

**Returns:** `JwtResult`

```typescript
{
  token: string;
  cookiesToSet?: CookieMutation[];  // Present if refresh occurred
}
```

**Throws:** `AuthError` with codes `not_authenticated`, `session_expired`

### `auth.buildLogout()`

**Returns:** `LogoutResult`

```typescript
{
  cookiesToClear: string[];  // session, refresh_token, oauth_state
}
```

---

## Adapter: `createNextAuthHandlers(config)`

```typescript
import { createNextAuthHandlers } from '@appdirect/auth-bff/next';

const handlers = createNextAuthHandlers(config);

handlers.login(req: Request): NextResponse
handlers.callback(req: Request): Promise<NextResponse>
handlers.me(req: Request): Promise<NextResponse>
handlers.jwt(req: Request): Promise<NextResponse>
handlers.refresh(req: Request): Promise<NextResponse>
handlers.logout(req: Request): NextResponse
handlers.importSession(req: Request): Promise<NextResponse>

handlers.auth  // underlying AppDirectAuth instance
```

---

## Adapter: `createAuthRouteHandlers(config)`

```typescript
import { createAuthRouteHandlers } from '@appdirect/auth-bff/handlers';

const handlers = createAuthRouteHandlers(config);

handlers.login(req, res)
handlers.callback(req, res)
handlers.me(req, res)
handlers.jwt(req, res)
handlers.refresh(req, res)
handlers.logout(req, res)
handlers.importSession(req, res)

handlers.auth
```

### Custom API routes

For routes outside the built-in handlers (e.g. proxying marketplace APIs), use the same cookie helpers the handlers use:

```typescript
import {
  createAuthRouteHandlers,
  readAuthCookies,
  applyCookieMutations,
  createHandlerCookieWriter,
} from '@appdirect/auth-bff/handlers';

const { auth } = createAuthRouteHandlers(config);

async function myProtectedRoute(req, res) {
  const { session, refreshToken } = readAuthCookies(req.headers.cookie);
  if (!session && !refreshToken) {
    res.status(401).json({ error: 'unauthorized' });
    return;
  }

  const result = await auth.getFeJwt({ sessionToken: session, refreshToken });
  if (result.cookiesToSet?.length) {
    applyCookieMutations(createHandlerCookieWriter(res), result.cookiesToSet);
  }

  // Use result.token as Bearer for upstream API calls
}
```

`auth.getMe` follows the same pattern when you need user info instead of a raw token.

---

## HTTP endpoint responses

### `GET /api/auth/me`

```json
{
  "user": {
    "id": "user-sub",
    "email": "user@example.com",
    "name": "Jane Doe",
    "picture": "https://..."
  },
  "hasSession": true
}
```

When not logged in:

```json
{ "user": null, "hasSession": false }
```

When session expired and refresh failed:

```json
{ "user": null, "hasSession": false }
```

When session exists but user claims cannot be decoded (valid FEJWT):

```json
{ "user": null, "hasSession": true }
```

### `GET /api/auth/jwt`

Success:

```json
{ "token": "<FEJWT>" }
```

Failure:

```json
{
  "error": "session_expired",
  "token": null,
  "refresh_url": "/api/auth/refresh"
}
```

### `GET /api/auth/refresh`

Success:

```json
{ "ok": true, "token": "<FEJWT>" }
```

Failure:

```json
{ "error": "refresh_failed", "ok": false }
```

### `POST /api/auth/import-session`

Iframe preview handoff: accepts session tokens from a same-origin `POST` (typically after a parent `postMessage`) and writes HttpOnly cookies.

Request body:

```json
{
  "sessionToken": "<FEJWT>",
  "refreshToken": "<auth-layer refresh token>"
}
```

Success:

```json
{ "ok": true }
```

Failures:

```json
{ "error": "Cross-site request blocked" }
```

```json
{ "error": "invalid_body" }
```

```json
{ "error": "unauthorized" }
```

When `applicationId` is configured, the handler verifies MyApps assignment (same as callback/refresh) before setting cookies. When `applicationId` is unset, the check is skipped.

---

## Debugging

Set `AUTH_BFF_DEBUG=true` on the BFF host (local shell, Vercel project env, etc.) to enable opt-in SDK flow logs for JWT resolution and session refresh.

Logs use the `[auth-bff:jwt]` and `[auth-bff:refresh]` prefixes and include only safe metadata (token presence/length, JWT `sub`/`exp`, decision path, error codes). Raw session tokens, refresh tokens, and cookie values are never logged.

---

## Error codes

| Code | HTTP | Description |
|------|------|-------------|
| `no_code` | 302 redirect | User cancelled or no authorization code |
| `invalid_state` | 302 redirect | OAuth state CSRF mismatch |
| `server_config` | 500 | Missing configuration |
| `token_exchange` | 302 redirect | Authorization code exchange failed |
| `fejwt` | 302 redirect | Initial `/auth/token` exchange failed during login callback |
| `userinfo` | 302 redirect | Could not resolve user identity |
| `not_authenticated` | 401 | No session or refresh token |
| `session_expired` | 401 | FEJWT expired and lazy renewal failed, or no refresh token available |
| `refresh_failed` | 502 | Explicit `/api/auth/refresh` exchange failed |
| `method_not_allowed` | 405 | Wrong HTTP method |

Callback errors redirect to `{appBaseUrl}{onErrorRedirect}{code}` (default `{appBaseUrl}/?error=token_exchange`). Post-login and logout redirects also use `appBaseUrl` in both the Next.js and handlers adapters.

---

## Types

Exported from `@appdirect/auth-bff`:

- `AppDirectAuthConfig`
- `AppDirectAuth`
- `AuthUser`
- `CookieStore`
- `CookieMutation`
- `LoginRedirectResult`
- `CallbackResult`
- `RefreshResult`
- `MeResult`
- `JwtResult`
- `LogoutResult`
- `AuthError`
- `COOKIE_SESSION`, `COOKIE_REFRESH_TOKEN`, `COOKIE_OAUTH_STATE`

Handler-only types (`HandlerRequest`, `HandlerResponse`, `CookieHeaderWriter`) and cookie helpers are exported from `@appdirect/auth-bff/handlers` — see [Package exports](#package-exports) above.
