Типы ключей и модель безопасности
Четыре категории ключей AACsearch — search, connector, scoped и admin — их префиксы ss_*, хранение только хешей, HMAC + TTL + фильтр scoped-токена и порядок проверки каждой категории во время запроса.
AACsearch выдаёт по одной физической строке на учётную запись в SearchApiKey,
различая их колонкой scopes[]. Исходная строка ключа никогда не хранится — только
sha256-хеш. Scoped-токены — четвёртая, производная категория учётных данных,
живущая целиком в bearer-строке (без строки в БД) и проверяемая через HMAC.
Четыре категории ключей
Описание
Диаграмма группирует учётные данные AACsearch в два бакета: три персистируемых типа строк в SearchApiKey (admin, ingest/connector, search) — каждый хранится только как sha256-хеш со своим префиксом и набором scopes — и производный тип scoped-токена, который никогда не персистируется, а подписан HMAC-SHA256 поверх BETTER_AUTH_SECRET. Стрелки помечают предполагаемую поверхность развёртывания (только сервер, CMS, доверенный backend, браузерный виджет) и показывают, что scoped-токены наследуются от родительского search-ключа.
graph TD
classDef admin fill:#fde68a,stroke:#b45309,color:#78350f
classDef ingest fill:#bae6fd,stroke:#0369a1,color:#075985
classDef search fill:#bbf7d0,stroke:#15803d,color:#14532d
classDef scoped fill:#ddd6fe,stroke:#6d28d9,color:#4c1d95
subgraph Persisted["SearchApiKey rows — sha256(raw) only"]
Admin["Admin key<br/>prefix: ss_search_<br/>scopes: [admin]<br/>full index control"]:::admin
Ingest["Ingest / Connector key<br/>prefix: ss_connector_ (or ss_search_)<br/>scopes: [connector_write] / [ingest]<br/>writes to SearchSyncOutbox"]:::ingest
Search["Search key<br/>prefix: ss_search_<br/>scopes: [search]<br/>read-only, origin-restricted"]:::search
end
subgraph Derived["Not persisted — verified by HMAC"]
Scoped["Scoped token<br/>prefix: ss_scoped_<br/>payload: { keyId, parentRawKey, filterBy, exp }<br/>HMAC-SHA256 over BETTER_AUTH_SECRET"]:::scoped
end
Search -. parent of .-> Scoped
UseAdmin["Server-side only<br/>NEVER in browser/CMS"] --- Admin
UseIngest["CMS connector / sync job"] --- Ingest
UseSearch["Server-rendered search<br/>(trusted backend)"] --- Search
UseScoped["Browser widget / per-user filter<br/>(short-lived, embeddable)"] --- ScopedПоток проверки на запрос
Описание
Диаграмма — это поток проверки на каждый запрос: по префиксу выбирается либо «HMAC-проверить-и-затем-хешировать» (для ss_scoped_*), либо прямой «хеш-и-lookup» (для ss_search_* / ss_connector_*), затем — проверка scope, allow-list origin, бакет rate-limit и, наконец, AND-объединение tenant-фильтра до запуска обработчика. Любая ошибка коротит на 401, 403 или 429.
flowchart TD
Req["Request: Authorization: Bearer <token>"]
Shape{"Prefix?"}
Scoped["ss_scoped_*"]
Search["ss_search_*"]
Connector["ss_connector_*"]
Reject["401 missing_bearer_token"]
HMAC["1. base64url decode payload<br/>2. HMAC-SHA256(payload, BETTER_AUTH_SECRET)<br/>3. timingSafeEqual(sig, expected)<br/>4. exp > now()"]
Hash["sha256(normalizeKeyForHash(raw))"]
Lookup["SELECT * FROM search_api_key<br/>WHERE hash = ?<br/>AND revokedAt IS NULL<br/>AND deletedAt IS NULL"]
ScopeCheck{"Required scope<br/>in scopes[]?"}
OriginCheck{"Origin in<br/>allowedOrigins?"}
RateLimit{"RateLimitBucket<br/>≤ rateLimitPerMinute?"}
Filter["Combine: userFilter && tenantId:<org> && (scopedFilter)"]
Allow["Proceed to handler"]
Req --> Shape
Shape --> Scoped --> HMAC --> Hash
Shape --> Search --> Hash
Shape --> Connector --> Hash
Shape -. unknown .-> Reject
Hash --> Lookup --> ScopeCheck
ScopeCheck -- no --> Reject
ScopeCheck -- yes --> OriginCheck
OriginCheck -- no --> Reject
OriginCheck -- yes --> RateLimit
RateLimit -- 429 --> Reject
RateLimit -- ok --> Filter --> AllowПравила хранения
| Свойство | Admin | Ingest / Connector | Search | Scoped |
|---|---|---|---|---|
| Префикс | ss_search_* | ss_connector_* (или ss_search_*) | ss_search_* | ss_scoped_* |
Сохранён в SearchApiKey? | Да | Да | Да | Нет — производный |
| Хранится как | sha256-хеш | sha256-хеш (общее пространство) | sha256-хеш | Самодостаточный подписанный payload |
| Требуемый scope | admin | connector_write / ingest | search | Наследует от родителя (должен быть search) |
| Истечение | Необязательный expiresAt | Необязательный expiresAt | Необязательный expiresAt | Обязательный exp (≤ 24 ч, обычно 15 мин) |
| Tenant-фильтр применяется? | n/a (admin) | Авто-инжекция при записи | Авто-инжекция при поиске | Авто-инжекция плюс scoped-фильтр |
| Допустим в браузере? | Никогда | Никогда | Только при origin-lock | Да (целевое использование) |
| Отзыв | revokedAt | revokedAt | revokedAt | Дождаться exp (или ротировать родителя) |
Какие Hard invariants это обеспечивает
- #3 Хранение только хешей. Ни один путь кода не сохраняет исходный ключ.
hashSearchApiKey()— единственная функция, касающаяся секрета, и она возвращает hex-дайджест. - #4 Scoped-токены. Любая клиентская поисковая строка использует
ss_scoped_*-токен, а не долгоживущийss_search_*. HMAC + TTL + фильтр не обсуждается. - #5 Изоляция тенантов.
tenantId:<org>AND-объединяется вfilter_byна уровне поискового адаптера, а не отправляется заголовком — это защищено от подделки.
См. также
- Путь чтения — где цепочка проверки сидит в request-конвейере.
- Путь записи — как ingest-ключи питают outbox.
Путь чтения
Как поисковый запрос проходит через AACsearch — от клиентского SDK к /search/public/multi, через public-auth и сборку tenant-фильтра, в multi_search Typesense и обратно в виде очищенного ответа.
Жизненный цикл коннектора
Шесть операций коннектора — handshake, heartbeat, full-sync, delta-sync, delete, диагностика — и их соответствие поверхности Connector API.