Ошибки авторизации
Ответы 401 / 403 от API AACsearch — диагностика отсутствующих заголовков, неверного префикса ключа, отозванных ключей, истечения scoped-токенов и ограничений по origin.
| Симптом | Вероятная причина |
|---|---|
401 missing_bearer_token | Заголовок Authorization отсутствует или искажён |
401 unauthorized | Пустой или неверный формат токена |
401 token_expired | TTL scoped-токена истёк |
403 forbidden | Неверный префикс ключа для операции, или ключ отозван |
403 invalid_or_revoked_key | Connector-токен отозван или не совпал |
403 origin_not_allowed | Origin запроса из браузера не входит в allowedOrigins |
403 key_does_not_match_index | Ключ был выпущен для другого indexSlug |
Дерево решений
401 missing_bearer_token
→ проверьте, что Authorization задан и начинается с `Bearer `
401 unauthorized
→ формат токена неправильный; убедитесь что он начинается с
ss_search_, ss_connector_, ss_scoped_ или aa_admin_ и без пробелов
401 token_expired (только scoped-токены)
→ истёк exp scoped-токена; выпустите новый на сервере
403 forbidden / invalid_or_revoked_key
→ проверьте Search → API Keys; не отозван ли?
→ если не отозван: префикс не совпадает — см. таблицу ниже
403 origin_not_allowed
→ заголовок Origin запроса не входит в allowedOrigins[] ключа
→ в dev оставьте allowedOrigins пустымПодберите правильный ключ под операцию
| Операция | Требуемый префикс |
|---|---|
POST /api/search/... | ss_search_* или ss_scoped_* |
POST /api/multi-search | ss_search_* или ss_scoped_* |
POST /api/connectors/handshake и прочие /api/connectors/... | ss_connector_* |
POST /api/projects/{projectId}/sync/... | ss_connector_* |
POST /api/webhooks/sync/{indexSlug} | HMAC-SHA256 подпись секретом вебхука индекса (без Bearer) |
POST /api/v1/... (управление) | aa_admin_* (только сервер) |
ss_search_* ключ на connector-эндпоинте или ss_connector_* ключ на /api/search — оба возвращают 403 forbidden. Маппинг проверяется функциями gatePublicSearchRequest() и gateConnectorRequest() в packages/api/modules/search/lib/.
Проверки
-
Заголовок Authorization корректен.
curl -i -X POST https://app.aacsearch.com/api/search/products \ -H "Authorization: Bearer ss_search_..." \ -H "Content-Type: application/json" \ -d '{ "q": "test" }'Ожидайте
200. Если401— скопируйте полеerrorиз ответа в таблицу выше. -
Ключ не отозван. Откройте Search → API Keys. Отозванные ключи скрыты в списке по умолчанию — переключите "Show revoked" чтобы увидеть.
-
Origin разрешён (только браузер). Dev tools → Network → кликните на падающий запрос → проверьте заголовок
Origin. Он должен совпадать с одной из записей вallowedOrigins[]ключа.// Получить список разрешённых origin программно: const keys = await admin.listKeys(); const yours = keys.find((k) => k.id === "key_..."); console.log(yours.allowedOrigins); -
Scoped-токен не истёк. Декодируйте payload (он base64url, не зашифрован) и проверьте
expiresAt:const [, payloadB64] = scopedToken.replace("ss_scoped_", "").split("."); const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString()); console.log(payload.expiresAt, "vs now", Math.floor(Date.now() / 1000)); -
Slug индекса совпадает. Ключ для
indexSlug: "products"не может искать вindexSlug: "categories". Пересоздайте ключ под нужный индекс или не указывайте slug при выпуске чтобы открыть все индексы организации.
Исправление
| Диагноз | Что сделать |
|---|---|
| Нет заголовка | Добавьте Authorization: Bearer <ключ> |
| Неверный префикс | Выпустите ключ нужного типа — см. таблицу выше |
| Ключ отозван | Создайте замену: Search → API Keys → Create key |
| Origin запрещён | Добавьте origin сторфронта в allowedOrigins[] (или оставьте пустым в dev) |
| Scoped-токен истёк | Выпустите свежий scoped-токен на сервере; клиент должен запросить новый при 401 token_expired |
| Slug не совпадает | Пересоздайте ключ без привязки к индексу или с правильным slug |
Восстановление на стороне браузера
Для интеграций виджетного типа — обрабатывайте 401 token_expired как сигнал перезапросить:
import { AacSearchClient, AacSearchError } from "@repo/search-client";
async function searchWithTokenRefresh(query: string) {
try {
return await client.search({ q: query });
} catch (err) {
if (err instanceof AacSearchError && err.code === "token_expired") {
const fresh = await fetch("/api/search-token").then((r) => r.text());
client.setApiKey(fresh);
return await client.search({ q: query });
}
throw err;
}
}На сервере — подключите Server Action (или роут /api/search-token), вызывающий orpc.search.createScopedToken.call({ ... }) со свежим expiresInSeconds.
Пакет диагностики
| Поле | Заметки |
|---|---|
| ID организации | обязательно |
| Время (UTC) | обязательно |
| Request ID | из заголовка ответа X-Request-Id |
| Префикс упавшего ключа | только первые 12 символов — никогда не весь ключ |
| Origin (если браузер) | из заголовков запроса |
| Эндпоинт | полный путь, например POST /api/search/products |
| Тело ответа | { "error": "...", "message": "..." } |
Никогда не вставляйте полный API-ключ в тикет. Префикса и последних 4 символов хватает поддержке для идентификации в audit log.