Путь чтения
Как поисковый запрос проходит через AACsearch — от клиентского SDK к /search/public/multi, через public-auth и сборку tenant-фильтра, в multi_search Typesense и обратно в виде очищенного ответа.
Путь чтения — stateless и горячий: на синхронном поисковом запросе в PostgreSQL не пишется ни одной строки (события использования записываются fire-and-forget). Вся цепочка — auth, объединение scoped-фильтра, объединение tenant-фильтра, вызов multi_search — спроектирована так, чтобы добавлять минимальные накладные расходы поверх базового вызова Typesense.
Поток
Описание
Диаграмма прослеживает публичный поисковый запрос от клиентского SDK через проверку токена (HMAC-проверка scoped-токена либо поиск parent-ключа по хешу), гейты origin и rate-limit, а затем через combineTenantFilter, который AND-объединяет пользовательский фильтр, фильтр scoped-токена и инжектируемую сервером клаузу tenantId, прежде чем multi_search уйдёт в Typesense. Очищенный ответ возвращается синхронно, тогда как строка SearchUsageEvent асинхронно ставится в очередь для аналитики.
sequenceDiagram
autonumber
participant SDK as Customer SDK / Widget
participant API as packages/api/modules/search/public-handler.ts
participant Gate as gatePublicSearchRequest
participant ST as verifyScopedSearchToken (HMAC + exp)
participant Key as verifySearchApiKey (hash-only)
participant RL as SearchRateLimitBucket
participant Filt as combineTenantFilter
participant TS as Typesense multi_search
participant Ev as SearchUsageEvent (async)
SDK->>API: POST /api/search/public/multi
API->>Gate: Bearer header (ss_search_* OR ss_scoped_*)
alt ss_scoped_* prefix
Gate->>ST: decode + HMAC verify + exp check
ST-->>Gate: { parentRawKey, filterBy (scoped), keyId }
end
Gate->>Key: verifySearchApiKey(parentRawKey, scope=search)
Key-->>Gate: VerifiedSearchKey { organizationId, indexSlug }
Gate->>Gate: origin allow-list check
Gate->>RL: increment + compare rateLimitPerMinute
Gate-->>API: { verified, scopedFilter }
API->>Filt: combine(userFilter && tenantFilter && scopedFilter)
Filt-->>API: final filter_by expression
API->>TS: multi_search(searches[], common_params)
TS-->>API: hits + facets per search entry
API->>API: buildSearchResponse (PII strip, highlight tags)
API-->>SDK: 200 sanitized JSON
par fire-and-forget
API->>Ev: recordSearchUsageAsync(search_query)
endЧто гарантирует каждый шаг
- Форма токена. Принимаются только три префикса:
ss_search_*,ss_scoped_*и (для записи коннектором)ss_connector_*. Проверка формы выполняется до любого обращения к БД. - Проверка scoped-токена. Токены
ss_scoped_*несут подпись HMAC-SHA256 над base64url-кодированным payload-ом{ keyId, parentRawKey, filterBy, exp }. Сервер сравнивает подпись за константное время, декодирует родительский ключ изнутри и отказывает, еслиexpуже истёк. Это Hard Invariant #4. - Проверка родительского ключа. Родительский
ss_search_*(или родительss_scoped_*) хешируется в sha256 и ищется вSearchApiKey.hash. Открытые ключи никогда не сохраняются — Hard Invariant #3. - Origin allow-list. Если
allowedOriginsнепуст, заголовокOriginсравнивается строго. На несовпадении возвращается 403 до обращения к Typesense. - Rate limit. Скользящее окно по ключу; 429 при превышении.
- Объединение tenant-фильтра.
filter_byдля Typesense всегда объединяет через AND фильтр от клиента, фильтр scoped-токена (если есть) и серверныйtenantId:<org>. Tenant-фильтр добавляется на уровне SQL/TypesenseWHERE, а не в заголовке — Hard Invariant #5. - Диспатч multi_search. Один round-trip в Typesense раздаёт записи поиска. Каждая запись ОБЯЗАНА нести один и тот же tenant-фильтр.
- Очистка ответа.
buildSearchResponseвырезает внутренние поля, применяет настроенные highlight-теги и выдаёт публичную форму ответа. - Аналитика.
recordSearchUsageAsyncдобавляет в очередь строкуSearchUsageEventтипаsearch_query. Promise намеренно не ожидается.
Почему scoped-токены
Scoped-токен — единственный безопасный способ встроить поисковую строку под управлением AACsearch на клиентскую страницу. Он несёт:
- стабильный HMAC, который клиент не может подделать (серверный
BETTER_AUTH_SECRET), - ограничение
filterBy, которое API будет AND-объединять с каждым запросом (например,user_id:42 && shop_id:abc), - короткий
exp(обычно 15 минут), чтобы утечка токена имела малый радиус воздействия.
См. также
- Типы ключей и модель безопасности — полное сравнение четырёх категорий ключей.
- Путь записи — зеркальный поток приёма.
Путь записи
Как запись документа попадает в AACsearch — от POST /v1/indexes/.../documents через SearchIngestBuffer / SearchSyncOutbox и воркер в alias Typesense.
Типы ключей и модель безопасности
Четыре категории ключей AACsearch — search, connector, scoped и admin — их префиксы ss_*, хранение только хешей, HMAC + TTL + фильтр scoped-токена и порядок проверки каждой категории во время запроса.