AACsearch
Architecture

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)
    end

Ce que garantit chaque étape

  1. 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.
  2. 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 si exp est dépassé. C'est la Hard Invariant #4.
  3. Vérification de la clé parente. La clé ss_search_* parente (ou la parente du ss_scoped_*) est hachée en sha256 puis recherchée dans SearchApiKey.hash. Les clés en clair ne sont jamais stockées — Hard Invariant #3.
  4. Allow-list d'origine. Quand allowedOrigins est non vide, l'en-tête Origin est comparé strictement. Les échecs renvoient 403 avant tout contact avec Typesense.
  5. Rate limit. Fenêtre glissante par clé ; 429 en cas de dépassement.
  6. Combinaison du filtre tenant. Le filter_by de Typesense combine toujours en AND le filtre fourni par le client, le filtre du scoped-token (s'il existe) et la clause tenantId:<org> injectée par le serveur. Le filtre tenant est ajouté à la couche SQL/Typesense WHERE, jamais en en-tête — Hard Invariant #5.
  7. 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.
  8. Assainissement de la réponse. buildSearchResponse supprime les champs internes, applique les balises de highlight configurées et émet la forme publique de la réponse.
  9. Analytics. recordSearchUsageAsync met en file une ligne SearchUsageEvent de type search_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_SECRET uniquement côté serveur),
  • une contrainte filterBy que l'API combinera en AND à chaque requête (par ex. user_id:42 && shop_id:abc),
  • un exp court (typiquement 15 minutes), pour qu'un token fuité ait un rayon d'impact réduit.

Liens associés

On this page