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)
endWas jeder Schritt garantiert
- 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. - 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, wennexpin der Vergangenheit liegt. Das ist Hard Invariant #4. - Parent-Key-Verifikation. Der Parent-
ss_search_*(oderss_scoped_*-Parent) wird sha256-gehasht und inSearchApiKey.hashnachgeschlagen. Klartext-Schlüssel werden niemals gespeichert — Hard Invariant #3. - Origin Allow-List. Wenn
allowedOriginsnicht leer ist, wird derOrigin-Header strikt verglichen. Fehlschläge liefern 403, bevor Typesense angefasst wird. - Rate Limit. Pro-Schlüssel-Sliding-Window; 429 bei Überschreitung.
- Mandanten-Filter-Kombination. Das Typesense-
filter_bykombiniert immer per AND den kundenseitigen Filter, den Scoped-Token-Filter (falls vorhanden) und den serverseitig eingefügtentenantId:<org>-Term. Der Mandantenfilter wird auf der SQL/Typesense-WHERE-Ebene angehängt, niemals als Header — Hard Invariant #5. - multi_search-Dispatch. Ein einziger Round-Trip nach Typesense fächert Such-Einträge auf. Jeder Eintrag MUSS denselben Mandantenfilter tragen.
- Antwort-Sanitisierung.
buildSearchResponseentfernt interne Felder, wendet die konfigurierten Highlight-Tags an und gibt die öffentliche Response-Form aus. - Analyse.
recordSearchUsageAsyncreiht eineSearchUsageEvent-Zeile vom Typsearch_queryein. 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
- Schlüsseltypen & Sicherheitsmodell — vollständiger Vergleich der vier Schlüsselkategorien.
- Schreibpfad — der spiegelnde Ingest-Flow.
Schreibpfad
Wie ein Dokument-Write in AACsearch ankommt — von POST /v1/indexes/.../documents über SearchIngestBuffer / SearchSyncOutbox und den Worker bis in den Typesense-Alias.
Schlüsseltypen & Sicherheitsmodell
Die vier AACsearch-Schlüsselkategorien — search, connector, scoped und admin — ihre ss_*-Präfixe, ausschließlich gehashte Speicherung, Scoped-Token HMAC + TTL + Filter und wie sie zur Request-Zeit verifiziert werden.