Chemin de lecture
Comment une requête de recherche transite dans AACsearch — du SDK client à /search/public/multi, en passant par public-auth et la combinaison du filtre tenant, jusqu'au multi_search de Typesense, puis retour en réponse assainie.
Le chemin de lecture est sans état et chaud : aucune ligne n'est écrite dans PostgreSQL sur la requête de recherche synchrone (les événements d'usage sont enregistrés fire-and-forget). Toute la chaîne — auth, combinaison du filtre scoped, combinaison du filtre tenant, dispatch multi_search — est conçue pour ajouter le moins de surcoût possible par-dessus l'appel sous-jacent à Typesense.
Flux
Description
Le diagramme suit une requête publique de recherche depuis le SDK client à travers la vérification du token (scoped token vérifié par HMAC ou clé parente recherchée par hash), les gates d'origine et de rate-limit, puis par combineTenantFilter, qui combine en AND le filtre utilisateur, le filtre du scoped token et la clause tenantId injectée par le serveur avant de dispatcher multi_search vers Typesense. La réponse assainie est renvoyée de manière synchrone tandis qu'une ligne SearchUsageEvent est mise en file de manière asynchrone pour l'analytique.
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)
endCe que garantit chaque étape
- Forme du token. Seuls trois préfixes sont acceptés :
ss_search_*,ss_scoped_*et (pour les écritures connecteur)ss_connector_*. La vérification de forme intervient avant toute lecture en base. - Vérification du scoped-token. Les tokens
ss_scoped_*portent une signature HMAC-SHA256 sur un payload encodé en base64url{ keyId, parentRawKey, filterBy, exp }. Le serveur compare la signature en temps constant, décode la clé parente à l'intérieur et refuse siexpest dépassé. C'est la Hard Invariant #4. - Vérification de la clé parente. La clé
ss_search_*parente (ou la parente duss_scoped_*) est hachée en sha256 puis recherchée dansSearchApiKey.hash. Les clés en clair ne sont jamais stockées — Hard Invariant #3. - Allow-list d'origine. Quand
allowedOriginsest non vide, l'en-têteOriginest comparé strictement. Les échecs renvoient 403 avant tout contact avec Typesense. - Rate limit. Fenêtre glissante par clé ; 429 en cas de dépassement.
- Combinaison du filtre tenant. Le
filter_byde Typesense combine toujours en AND le filtre fourni par le client, le filtre du scoped-token (s'il existe) et la clausetenantId:<org>injectée par le serveur. Le filtre tenant est ajouté à la couche SQL/TypesenseWHERE, jamais en en-tête — Hard Invariant #5. - Dispatch multi_search. Un seul aller-retour vers Typesense fan-out les entrées de recherche. Chaque entrée DOIT porter le même filtre tenant.
- Assainissement de la réponse.
buildSearchResponsesupprime les champs internes, applique les balises de highlight configurées et émet la forme publique de la réponse. - Analytics.
recordSearchUsageAsyncmet en file une ligneSearchUsageEventde typesearch_query. On n'attend volontairement pas la promesse.
Pourquoi des scoped tokens
Un scoped token est la seule manière sûre d'embarquer une boîte de recherche pilotée par AACsearch dans une page exposée au client. Il porte :
- un HMAC stable que le client ne peut pas forger (
BETTER_AUTH_SECRETuniquement côté serveur), - une contrainte
filterByque l'API combinera en AND à chaque requête (par ex.user_id:42 && shop_id:abc), - un
expcourt (typiquement 15 minutes), pour qu'un token fuité ait un rayon d'impact réduit.
Liens associés
- Types de clés et modèle de sécurité — comparaison complète des quatre catégories de clés.
- Chemin d'écriture — le flux miroir pour l'ingest.
Chemin d'écriture
Comment une écriture de document entre dans AACsearch — depuis POST /v1/indexes/.../documents via SearchIngestBuffer / SearchSyncOutbox et le worker, jusqu'à l'alias Typesense.
Types de clés et modèle de sécurité
Les quatre catégories de clés AACsearch — search, connector, scoped et admin — leurs préfixes ss_*, leur stockage en hash uniquement, le HMAC + TTL + filtre des scoped tokens et la manière dont chacune est vérifiée à la requête.