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.
AACsearch gibt eine physische Zeile pro Credential in SearchApiKey aus,
unterschieden durch die Spalte scopes[]. Der Roh-Schlüssel-String wird nie
gespeichert — nur ein sha256-Hash. Scoped-Tokens sind eine vierte, abgeleitete
Credential-Art, die vollständig im Bearer-String lebt (keine Zeile) und per HMAC
verifiziert wird.
Die vier Schlüsselkategorien
Beschreibung
Das Diagramm gruppiert AACsearch-Credentials in zwei Buckets: drei persistierte Zeilentypen in SearchApiKey (admin, ingest/connector, search) — jeder nur als sha256-Hash mit eigenem Prefix und Scope-Set gespeichert — sowie einen abgeleiteten Scoped-Token-Typ, der niemals persistiert, aber per HMAC-SHA256 über BETTER_AUTH_SECRET signiert wird. Pfeile annotieren die vorgesehene Einsatzoberfläche (nur Server, CMS, vertrauenswürdiges Backend, Browser-Widget) und zeigen, dass Scoped-Tokens von einem Parent-Search-Key erben.
graph TD
classDef admin fill:#fde68a,stroke:#b45309,color:#78350f
classDef ingest fill:#bae6fd,stroke:#0369a1,color:#075985
classDef search fill:#bbf7d0,stroke:#15803d,color:#14532d
classDef scoped fill:#ddd6fe,stroke:#6d28d9,color:#4c1d95
subgraph Persisted["SearchApiKey rows — sha256(raw) only"]
Admin["Admin key<br/>prefix: ss_search_<br/>scopes: [admin]<br/>full index control"]:::admin
Ingest["Ingest / Connector key<br/>prefix: ss_connector_ (or ss_search_)<br/>scopes: [connector_write] / [ingest]<br/>writes to SearchSyncOutbox"]:::ingest
Search["Search key<br/>prefix: ss_search_<br/>scopes: [search]<br/>read-only, origin-restricted"]:::search
end
subgraph Derived["Not persisted — verified by HMAC"]
Scoped["Scoped token<br/>prefix: ss_scoped_<br/>payload: { keyId, parentRawKey, filterBy, exp }<br/>HMAC-SHA256 over BETTER_AUTH_SECRET"]:::scoped
end
Search -. parent of .-> Scoped
UseAdmin["Server-side only<br/>NEVER in browser/CMS"] --- Admin
UseIngest["CMS connector / sync job"] --- Ingest
UseSearch["Server-rendered search<br/>(trusted backend)"] --- Search
UseScoped["Browser widget / per-user filter<br/>(short-lived, embeddable)"] --- ScopedVerifikationsfluss pro Request
Beschreibung
Das Diagramm zeigt den Verifikationsfluss pro Request: Das Prefix entscheidet zwischen HMAC-prüfen-und-dann-hashen (für ss_scoped_*) oder direktem Hash-und-Lookup (für ss_search_* / ss_connector_*), gefolgt von Scope-Check, Origin-Allow-List, Rate-Limit-Bucket und schließlich AND-Kombination des Tenant-Filters, bevor der Handler läuft. Jeder Fehler springt sofort auf 401, 403 oder 429.
flowchart TD
Req["Request: Authorization: Bearer <token>"]
Shape{"Prefix?"}
Scoped["ss_scoped_*"]
Search["ss_search_*"]
Connector["ss_connector_*"]
Reject["401 missing_bearer_token"]
HMAC["1. base64url decode payload<br/>2. HMAC-SHA256(payload, BETTER_AUTH_SECRET)<br/>3. timingSafeEqual(sig, expected)<br/>4. exp > now()"]
Hash["sha256(normalizeKeyForHash(raw))"]
Lookup["SELECT * FROM search_api_key<br/>WHERE hash = ?<br/>AND revokedAt IS NULL<br/>AND deletedAt IS NULL"]
ScopeCheck{"Required scope<br/>in scopes[]?"}
OriginCheck{"Origin in<br/>allowedOrigins?"}
RateLimit{"RateLimitBucket<br/>≤ rateLimitPerMinute?"}
Filter["Combine: userFilter && tenantId:<org> && (scopedFilter)"]
Allow["Proceed to handler"]
Req --> Shape
Shape --> Scoped --> HMAC --> Hash
Shape --> Search --> Hash
Shape --> Connector --> Hash
Shape -. unknown .-> Reject
Hash --> Lookup --> ScopeCheck
ScopeCheck -- no --> Reject
ScopeCheck -- yes --> OriginCheck
OriginCheck -- no --> Reject
OriginCheck -- yes --> RateLimit
RateLimit -- 429 --> Reject
RateLimit -- ok --> Filter --> AllowSpeicherregeln
| Eigenschaft | Admin | Ingest / Connector | Search | Scoped |
|---|---|---|---|---|
| Präfix | ss_search_* | ss_connector_* (oder ss_search_*) | ss_search_* | ss_scoped_* |
In SearchApiKey persistiert? | Ja | Ja | Ja | Nein — abgeleitet |
| Gespeichert als | sha256-Hash | sha256-Hash (geteilter Raum) | sha256-Hash | Selbst-enthaltene signierte Payload |
| Erforderlicher Scope | admin | connector_write / ingest | search | Erbt vom Parent (muss search sein) |
| Ablauf | Optional expiresAt | Optional expiresAt | Optional expiresAt | Pflicht-exp (≤ 24 h, typ. 15 min) |
| Mandantenfilter angewendet? | n/a (admin) | Beim Write automatisch | Bei der Suche automatisch | Automatisch plus Scoped-Filter |
| Im Browser erlaubt? | Niemals | Niemals | Nur origin-gebunden | Ja (Verwendungszweck) |
| Widerruf | revokedAt | revokedAt | revokedAt | Auf exp warten (oder Parent rotieren) |
Hard Invariants, die das durchsetzt
- #3 Hash-Only-Speicherung. Kein Codepfad persistiert den Roh-Schlüssel.
hashSearchApiKey()ist die einzige Funktion, die das Secret berührt, und sie gibt einen Hex-Digest zurück. - #4 Scoped-Tokens. Jede kundenseitige Suchbox verwendet ein
ss_scoped_*-Token, keinen langlebigenss_search_*-Schlüssel. HMAC + TTL + Filter ist nicht verhandelbar. - #5 Mandanten-Isolation.
tenantId:<org>wird auf Adapter-Ebene per AND infilter_bykombiniert, niemals als Header gesendet — fälschungssicher.
Verwandt
- Lesepfad — wie die Verifikationskette in der Request-Pipeline sitzt.
- Schreibpfad — wie Ingest-Schlüssel die Outbox speisen.
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.
Konnektor-Lebenszyklus
Die sechs Konnektor-Operationen — Handshake, Heartbeat, Full-Sync, Delta-Sync, Delete, Diagnose — und wie sie auf die Connector-API-Oberfläche abgebildet werden.