# OAuth PKCE: API-ключ для вашего приложения

OAuth PKCE позволяет приложению получить API-ключ пользователя без ручного копирования. Пользователь входит в RouterAI, выбирает аккаунт, название ключа и лимит расходов. Приложение получает обычный ключ `sk-…`.

Регистрировать приложение не нужно. Пути и поля совпадают с OpenRouter, поэтому приложению с «Login with OpenRouter» достаточно заменить хост на `https://routerai.ru`.

## Как это работает

1. Приложение генерирует `code_verifier` и `code_challenge`.
2. Перенаправляет пользователя на `https://routerai.ru/auth`.
3. Пользователь подтверждает выдачу ключа.
4. RouterAI возвращает пользователя на `callback_url` с параметром `code`.
5. Приложение отправляет `code` и `code_verifier` на `POST https://routerai.ru/api/v1/auth/keys` и получает ключ.
6. Ключ работает как обычный: `https://routerai.ru/api/v1`, заголовок `Authorization: Bearer sk-…`.

## Шаг 1. code_verifier и code_challenge

`code_verifier` — случайная строка из 43–128 символов `[A-Za-z0-9-._~]`. `code_challenge` — `BASE64URL(SHA256(code_verifier))` без `=` на конце. `code_verifier` храните у себя и не передавайте в URL.


```typescript
import { randomBytes, createHash } from 'node:crypto';

const codeVerifier = randomBytes(32).toString('base64url');
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
```

```python
import base64, hashlib, secrets

code_verifier = secrets.token_urlsafe(32)
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
```

```javascript
const b64url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes)))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

const codeVerifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const codeChallenge = b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)));
```

```bash
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')
CODE_CHALLENGE=$(printf %s "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_')
```


## Шаг 2. Перенаправление на /auth

```
https://routerai.ru/auth?callback_url=https://example.com/callback&code_challenge=…&code_challenge_method=S256&state=…&app_name=My%20App
```

| Параметр | Обязательный | Описание |
|---|---|---|
| `callback_url` | нет | Адрес возврата. Только `https`, `http` допускается лишь для `localhost`/`127.0.0.1` (любой порт). Можно указать собственную схему приложения (`myapp://callback`). Фрагмент `#` запрещён. Без параметра включается [headless-режим](#headless-режим). |
| `code_challenge` | да | `BASE64URL(SHA256(code_verifier))`, 43 символа |
| `code_challenge_method` | да | Только `S256`. `plain` не поддерживается |
| `state` | нет | До 256 символов, возвращается без изменений. Защищает от CSRF: сверьте его перед обменом кода |
| `app_name` | нет | Название приложения на экране согласия |
| `key_label` | нет | Название ключа по умолчанию, пользователь может изменить |
| `limit` | нет | Лимит расходов по умолчанию, ₽ (0 — без лимита). Если не задан — 100 ₽ |
| `limit_period` | нет | `day`, `week` или `month`. Если не задан — `day` |

Если в запросе ошибка (например, нет `code_challenge` или указан недопустимый `callback_url`), RouterAI показывает её на своей странице и не возвращает пользователя в приложение.

## Шаг 3. Callback

- Успех: `callback_url?code=…&state=…&iss=https://routerai.ru`
- Отказ: `callback_url?error=access_denied&state=…`

Сверьте `state` с сохранённым значением. Код действует **5 минут** и используется **один раз**.

## Шаг 4. Обмен кода на ключ


```bash
curl -X POST https://routerai.ru/api/v1/auth/keys \
  -H "Content-Type: application/json" \
  -d '{
    "code": "…",
    "code_verifier": "…",
    "code_challenge_method": "S256",
    "callback_url": "https://example.com/callback"
  }'
```

```typescript
const response = await fetch('https://routerai.ru/api/v1/auth/keys', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    code,
    code_verifier: codeVerifier,
    code_challenge_method: 'S256',
    callback_url: 'https://example.com/callback',
  }),
});
const { key } = await response.json();
```

```python
import requests

response = requests.post(
    "https://routerai.ru/api/v1/auth/keys",
    json={
        "code": code,
        "code_verifier": code_verifier,
        "code_challenge_method": "S256",
        "callback_url": "https://example.com/callback",
    },
)
response.raise_for_status()
key = response.json()["key"]
```


Ответ:

```json
{ "key": "sk-…" }
```

`callback_url` передавайте тот же, что и в `/auth`, символ в символ. `code_challenge_method` необязателен, но если он указан, допустимо только `S256`. Эндпоинт поддерживает CORS, поэтому обмен можно выполнить прямо из браузера.

**Ошибки** (`{"error": "…", "error_description": "…"}`):

| HTTP | `error` | Причина |
|---|---|---|
| 400 | `invalid_grant` | Код не найден, истёк или уже использован; `callback_url` не совпадает; неверный `code_verifier`; у пользователя больше нет прав на выбранный аккаунт |
| 400 | `invalid_request` | `code_challenge_method` отличается от `S256` |
| 429 | `slow_down` | Больше 30 запросов в минуту с одного IP |

Если уже использованный код предъявить повторно вместе с верным `code_verifier`, выданный по нему ключ выключается: значит, утекла вся пара. Повтор одного кода без верного `code_verifier` ключ не трогает.

## Headless-режим

Для CLI и приложений без браузерного callback откройте `/auth` **без** `callback_url`. После подтверждения RouterAI покажет код на экране. Пользователь вставляет его в приложение, и приложение обменивает код через `POST /api/v1/auth/keys` без `callback_url`. Правила те же: код действует 5 минут и используется один раз.

## Повторная авторизация

Если тот же пользователь снова авторизует то же приложение для того же аккаунта, ключ **перевыпускается**. Старый секрет сразу перестаёт работать, а название и лимит обновляются. Дубликат не создаётся, статистика ключа сохраняется.

Приложение определяется так:
- веб-приложение — по адресу (`https://example.com`) из `callback_url`;
- `localhost` и headless — по `app_name`.

Ключи `localhost`- и headless-приложений без `app_name` не перевыпускаются: каждый раз создаётся новый ключ.

## Лимиты и управление ключом

Лимит расходов действует как у обычного ключа: при его достижении или нехватке баланса запросы отклоняются. Пользователь видит ключ в [Настройках → API-ключи](https://routerai.ru/settings/keys) с пометкой «OAuth». Там можно изменить лимит, выключить или удалить ключ.
