Appearance
Auth Discovery Flow
Esta página documenta la decisión de diseño introducida por connections-redesign: un solo campo authType en el catálogo guía a la UI hacia OAuth o hacia el formulario de credenciales. La UI no hardcodea qué slug es OAuth ni cuáles piden token vs correo+password.
Contrato: authType en available_connections
| Valor | Significado | Slugs actuales |
|---|---|---|
credentials | La conexión se crea enviando un payload JSON al endpoint POST /connections con el shape definido por form-schema. | smtp, telegram |
oauth | La conexión requiere un viaje al proveedor (Google, Microsoft, etc.). El cliente llama a POST /connections/oauth/start, redirige al usuario a authUrl, y el callback GET /connections/oauth/callback persiste la conexión. | gmail, sheets, calendar |
El campo vive en:
- DB:
connections.available_connections.auth_type varchar(32) NOT NULL. - Registry:
IAvailableConnectionRegistryEntry.authType(TS literal union'oauth' | 'credentials'). - API:
AvailableConnectionSchema.authType(@repo/schemas).
Flujo de UI (panel)
sequenceDiagram
participant U as Usuario
participant UI as Panel
participant API as API
participant G as Google
U->>UI: Elige "Conectar Gmail"
UI->>API: GET /connections/available/:id
API-->>UI: { slug: 'gmail', authType: 'oauth', ... }
alt authType === 'oauth'
UI->>API: POST /connections/oauth/start { availableConnectionId, label? }
API-->>UI: { authUrl, state }
UI->>U: Redirige a authUrl
U->>G: Consiente scopes
G->>API: GET /connections/oauth/callback?code&state
API-->>UI: Redirige a successUrl?connectionId=...
else authType === 'credentials'
UI->>API: GET /connections/available/:id/form-schema
API-->>UI: { fields: [...] }
UI->>U: Renderiza formulario dinámico
U->>UI: Submit
UI->>API: POST /connections { availableConnectionId, config, label? }
API-->>UI: 201 Created { id, slug, ... }
end
Rama OAuth — POST /connections/oauth/start
- Body:
{ availableConnectionId: uuid, label?: string, visibility?: 'private' | 'restricted' }. - Rate-limit:
10/minporuserId. - El handler (
StartGoogleOAuthCommandHandler) resuelve la entrada del catálogo por id y verificaauthType === 'oauth'. Si no lo es, devuelve400 CONNECTION.OAUTH_NOT_SUPPORTEDconmetadata: { slug }. - Los scopes se sacan del
registry(oauthScopesdel entry correspondiente) — ver Registry Contract. - La respuesta contiene
authUrl(URL de consentimiento de Google) ystate(token firmado que round-trip-ea por el callback).
Errores específicos del start
| Código | HTTP | Cuándo |
|---|---|---|
CONNECTION.CATALOG_ENTRY_NOT_FOUND | 404 | El availableConnectionId no existe. |
CONNECTION.OAUTH_NOT_SUPPORTED | 400 | El slug tiene authType = 'credentials' (e.g. intentar OAuth contra smtp). |
CONNECTION.OAUTH_PROVIDER_NOT_CONFIGURED | 400 | Falta GOOGLE_GMAIL_* o GOOGLE_WORKSPACE_* en env vars del API. |
Rama credentials — POST /connections
- Body:
{ availableConnectionId: uuid, label?: string, config: object, visibility?: 'private' | 'restricted' }. - El handler (
CreateConnectionHandler):- Resuelve el catálogo por id — si no existe,
404 CATALOG_ENTRY_NOT_FOUND. - Usa
ConnectionConfigFactory.create(catalog.slug, config)para validar y construir el value-object. - Si el shape del
configno coincide con el esperado por el slug, devuelve400 CONNECTION.INVALID_CONFIGURATIONconreasondescriptivo. - Persiste la conexión cifrando los campos sensibles.
- Resuelve el catálogo por id — si no existe,
Form schema dinámico
GET /connections/available/:id/form-schema devuelve el contrato del formulario:
json
{
"fields": [
{ "name": "host", "type": "string", "required": true }
]
}Tipos soportados: string, number, boolean, password, labeledList. El panel renderiza un formulario genérico a partir de esta descripción.
labeledList modela colecciones opcionales de pares {label, value} — por ejemplo las allow/deny lists de Telegram y SMTP. Ver Recipient Allowlists.
Para entradas
authType = 'oauth'este endpoint devuelve{ fields: [] }— el formulario no aplica.
Por qué un solo campo
Alternativa descartada: mantener dos endpoints separados (GET /oauth/providers vs GET /credentials/providers).
Trade-offs evaluados:
- Un campo (elegido): el cliente hace una sola query para descubrir el catálogo completo, luego ramifica localmente. Simplifica el screen "elegir integración". Costo: añade un campo al payload de catálogo (+12 bytes por entrada).
- Dos endpoints: API más "semántica" pero obliga al cliente a hacer dos requests y a sincronizar estados separados. Peor DX.
La decisión vive en el Phase 2 spec del change connections-redesign — ver engram://sdd/connections-redesign/spec si necesitas el contexto histórico.