Tipos de claves y modelo de seguridad
Las cuatro categorías de claves de AACsearch — search, connector, scoped y admin — sus prefijos ss_*, almacenamiento solo-hash, HMAC + TTL + filtro del scoped-token y cómo se verifica cada una en tiempo de petición.
AACsearch emite una fila física por credencial en SearchApiKey, diferenciada por
la columna scopes[]. La cadena cruda de la clave nunca se almacena — solo un hash
sha256. Los scoped-tokens son una cuarta categoría derivada que vive enteramente en la
cadena bearer (sin fila) y se verifica por HMAC.
Las cuatro categorías de claves
Descripción
El diagrama agrupa las credenciales de AACsearch en dos buckets: tres tipos de fila persistidos en SearchApiKey (admin, ingest/connector, search) — cada uno guardado solo como hash sha256 con su propio prefijo y conjunto de scopes — y un tipo derivado de scoped-token que nunca se persiste, firmado con HMAC-SHA256 sobre BETTER_AUTH_SECRET. Las flechas anotan la superficie de despliegue prevista (solo servidor, CMS, backend de confianza, widget de navegador) y muestran que los scoped-tokens heredan de una clave search padre.
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)"] --- ScopedFlujo de verificación por petición
Descripción
El diagrama es el flujo de verificación por petición: el prefijo despacha o bien a verificar-HMAC-y-luego-hashear (para ss_scoped_*) o bien a hash-y-lookup directo (para ss_search_* / ss_connector_*), seguido del chequeo de scope, allow-list de origen, bucket de rate-limit y, finalmente, la combinación AND del filtro de tenant antes de que se ejecute el handler. Cualquier fallo corta el flujo con 401, 403 o 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 --> AllowReglas de almacenamiento
| Propiedad | Admin | Ingest / Connector | Search | Scoped |
|---|---|---|---|---|
| Prefijo | ss_search_* | ss_connector_* (o ss_search_*) | ss_search_* | ss_scoped_* |
¿Persistido en SearchApiKey? | Sí | Sí | Sí | No — derivado |
| Almacenado como | hash sha256 | hash sha256 (espacio compartido) | hash sha256 | Carga útil firmada autocontenida |
| Scope requerido | admin | connector_write / ingest | search | Hereda del padre (debe ser search) |
| Expiración | expiresAt opcional | expiresAt opcional | expiresAt opcional | exp obligatorio (≤ 24 h, típ. 15 min) |
| ¿Filtro de tenant aplicado? | n/a (admin) | Auto-inyectado al escribir | Auto-inyectado al buscar | Auto-inyectado más filtro scoped |
| ¿Permitido en el navegador? | Nunca | Nunca | Solo si está origin-locked | Sí (uso previsto) |
| Revocación | revokedAt | revokedAt | revokedAt | Esperar exp (o rotar el padre) |
Hard invariants que esto impone
- #3 Almacenamiento solo-hash. Ningún code path persiste la clave cruda.
hashSearchApiKey()es la única función que toca el secreto, y devuelve un digest hex. - #4 Scoped-tokens. Cada caja de búsqueda orientada al cliente usa un token
ss_scoped_*, no una clavess_search_*de larga vida. HMAC + TTL + filtro es innegociable. - #5 Aislamiento de tenant.
tenantId:<org>se combina por AND dentro defilter_byen el adaptador de búsqueda, nunca como cabecera — es a prueba de falsificación.
Relacionado
- Ruta de lectura — cómo encaja la cadena de verificación en el pipeline de peticiones.
- Ruta de escritura — cómo las claves de ingesta alimentan la outbox.
Ruta de lectura
Cómo una consulta de búsqueda fluye por AACsearch — desde el SDK del cliente hasta /search/public/multi, pasando por public-auth y la combinación de filtro de tenant, hacia el multi_search de Typesense y de vuelta como respuesta saneada.
Ciclo de vida del conector
Las seis operaciones del conector — handshake, heartbeat, full-sync, delta-sync, delete, diagnóstico — y cómo se mapean a la superficie de la Connector API.