AACsearch
Architektur

Lesepfad

Wie eine Suchanfrage durch AACsearch fließt — vom Kunden-SDK über /search/public/multi, durch public-auth und die Mandanten-Filter-Kombination, in Typesense multi_search und zurück als sanitisierte Antwort.

Der Lesepfad ist zustandslos und heiß: Auf dem synchronen Such-Request wird keine Zeile nach PostgreSQL geschrieben (Usage-Events werden fire-and-forget aufgezeichnet). Die gesamte Kette — Auth, Scoped-Filter-Kombination, Mandanten-Filter-Kombination, multi_search-Dispatch — ist darauf ausgelegt, möglichst geringen Overhead über dem zugrundeliegenden Typesense-Aufruf hinzuzufügen.

Ablauf

Beschreibung

Das Diagramm verfolgt eine öffentliche Suchanfrage vom Customer-SDK durch die Token-Verifikation (HMAC-geprüftes Scoped-Token oder per Hash nachgeschlagener Parent-Key), die Origin- und Rate-Limit-Gates und schließlich durch combineTenantFilter, der den Nutzerfilter, den Scoped-Token-Filter und die serverseitig injizierte tenantId-Klausel AND-kombiniert, bevor multi_search an Typesense ausgelöst wird. Die bereinigte Antwort geht synchron zurück, während eine SearchUsageEvent-Zeile asynchron für die Analytik enqueued wird.

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

Was jeder Schritt garantiert

  1. Token-Form. Nur drei Präfixe werden akzeptiert: ss_search_*, ss_scoped_* und (für Connector-Writes) ss_connector_*. Die Form-Prüfung erfolgt vor jeder DB-Abfrage.
  2. Scoped-Token-Verifikation. ss_scoped_*-Tokens tragen eine HMAC-SHA256-Signatur über einer base64url-kodierten Payload { keyId, parentRawKey, filterBy, exp }. Der Server vergleicht die Signatur in konstanter Zeit, dekodiert den Parent-Schlüssel und verweigert, wenn exp in der Vergangenheit liegt. Das ist Hard Invariant #4.
  3. Parent-Key-Verifikation. Der Parent-ss_search_* (oder ss_scoped_*-Parent) wird sha256-gehasht und in SearchApiKey.hash nachgeschlagen. Klartext-Schlüssel werden niemals gespeichert — Hard Invariant #3.
  4. Origin Allow-List. Wenn allowedOrigins nicht leer ist, wird der Origin-Header strikt verglichen. Fehlschläge liefern 403, bevor Typesense angefasst wird.
  5. Rate Limit. Pro-Schlüssel-Sliding-Window; 429 bei Überschreitung.
  6. Mandanten-Filter-Kombination. Das Typesense-filter_by kombiniert immer per AND den kundenseitigen Filter, den Scoped-Token-Filter (falls vorhanden) und den serverseitig eingefügten tenantId:<org>-Term. Der Mandantenfilter wird auf der SQL/Typesense-WHERE-Ebene angehängt, niemals als Header — Hard Invariant #5.
  7. multi_search-Dispatch. Ein einziger Round-Trip nach Typesense fächert Such-Einträge auf. Jeder Eintrag MUSS denselben Mandantenfilter tragen.
  8. Antwort-Sanitisierung. buildSearchResponse entfernt interne Felder, wendet die konfigurierten Highlight-Tags an und gibt die öffentliche Response-Form aus.
  9. Analyse. recordSearchUsageAsync reiht eine SearchUsageEvent-Zeile vom Typ search_query ein. Auf das Promise wird absichtlich nicht gewartet.

Warum Scoped-Tokens

Ein Scoped-Token ist der einzige sichere Weg, eine von AACsearch betriebene Suchbox in eine kundenseitige Seite einzubetten. Es trägt:

  • eine stabile HMAC, die der Kunde nicht fälschen kann (server-only BETTER_AUTH_SECRET),
  • eine filterBy-Bedingung, die die API mit jedem Request per AND verbindet (z. B. user_id:42 && shop_id:abc),
  • ein kurzes exp (typischerweise 15 Minuten), sodass ein geleaktes Token einen kleinen Blast-Radius hat.

Verwandt

On this page