Skip to content

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

ValorSignificadoSlugs actuales
credentialsLa conexión se crea enviando un payload JSON al endpoint POST /connections con el shape definido por form-schema.smtp, telegram
oauthLa 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/min por userId.
  • El handler (StartGoogleOAuthCommandHandler) resuelve la entrada del catálogo por id y verifica authType === 'oauth'. Si no lo es, devuelve 400 CONNECTION.OAUTH_NOT_SUPPORTED con metadata: { slug }.
  • Los scopes se sacan del registry (oauthScopes del entry correspondiente) — ver Registry Contract.
  • La respuesta contiene authUrl (URL de consentimiento de Google) y state (token firmado que round-trip-ea por el callback).

Errores específicos del start

CódigoHTTPCuándo
CONNECTION.CATALOG_ENTRY_NOT_FOUND404El availableConnectionId no existe.
CONNECTION.OAUTH_NOT_SUPPORTED400El slug tiene authType = 'credentials' (e.g. intentar OAuth contra smtp).
CONNECTION.OAUTH_PROVIDER_NOT_CONFIGURED400Falta 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):
    1. Resuelve el catálogo por id — si no existe, 404 CATALOG_ENTRY_NOT_FOUND.
    2. Usa ConnectionConfigFactory.create(catalog.slug, config) para validar y construir el value-object.
    3. Si el shape del config no coincide con el esperado por el slug, devuelve 400 CONNECTION.INVALID_CONFIGURATION con reason descriptivo.
    4. Persiste la conexión cifrando los campos sensibles.

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.

Páginas relacionadas