Saltar al contenido principal

Resumen

Las conexiones son credenciales que un usuario posee para un servicio externo (Linear, GitHub, Slack, …). Tu aplicación declara cómo se obtienen esas credenciales — un proveedor de conexión — y las consume en tiempo de ejecución para realizar llamadas autenticadas a la API de terceros. OAuth 2.0 está integrado. Los servicios con su propio flujo de conexión — una página de inicio de sesión alojada, un identificador de cuenta entregado mediante webhook — utilizan un proveedor custom, donde tu aplicación controla el flujo y Twenty conserva el usuario, el espacio de trabajo y la conexión guardada. Los futuros tipos de credenciales (tokens de acceso personal, claves de API, autenticación básica) se integrarán en la misma interfaz — las aplicaciones que ya usan defineConnectionProvider({ type: 'oauth', ... }) no necesitarán migrar.
Un proveedor de conexión describe el flujo de OAuth que tu aplicación necesita. Para un proveedor WORKSPACE (el predeterminado), un miembro hace clic en “Agregar conexión” en la configuración de tu aplicación, completa la pantalla de consentimiento del proveedor y se crea una fila ConnectedAccount en su espacio de trabajo. Consulta “Conexiones de espacio de trabajo o personales” para los proveedores USER.Una configuración funcional necesita dos archivos — el proveedor de conexión y una declaración serverVariables correspondiente en defineApplication que contiene las credenciales del cliente OAuth.
src/connection-providers/linear-connection.ts
src/application.config.ts
Puntos clave:
  • name es la cadena de identificador única utilizada en listConnections({ providerName }) (kebab-case, debe coincidir con ^[a-z][a-z0-9-]*$).
  • displayName se muestra en la pestaña General de la aplicación y en la lista de herramientas de IA.
  • scope determina quién se conecta: 'WORKSPACE' (el predeterminado) para las conexiones compartidas con todo el espacio de trabajo, 'USER' para las conexiones que cada miembro realiza por sí mismo. Consulta “Conexiones de espacio de trabajo o personales” a continuación.
  • logo es una ruta a un recurso incluido en la carpeta public/ de tu aplicación, que se sirve a través de los recursos públicos de la aplicación. Cuando se omite, la pestaña General usa un marcador de posición creado a partir de displayName.
  • clientIdVariable / clientSecretVariable son nombres, no valores — deben coincidir con las claves declaradas en defineApplication.serverVariables. Los client_id y client_secret reales los introduce el administrador del servidor a través de la interfaz de registro de la aplicación; nunca se incluyen en tu repositorio.
  • Usa serverVariables (no applicationVariables) — las credenciales de OAuth son a nivel de servidor y hay una aplicación OAuth por servidor de Twenty.
  • Hasta que ambas serverVariables estén completas, la pestaña General de la aplicación muestra un aviso de “requiere administrador del servidor” y el botón “Agregar conexión” está deshabilitado.
  • type es 'oauth' (con un bloque oauth) o 'custom' (con un bloque custom, consulta “Flujos de conexión personalizados”). El discriminador es compatible hacia adelante: tipos futuros ('pat', 'api-key', …) agregará nuevos bloques de subconfiguración junto a estos.
La URL de callback de OAuth que tu proveedor debe autorizar es:
Algunos servicios conectan cuentas mediante su propio flujo en lugar de OAuth: Unipile, Plaid o Merge envían al usuario a una página alojada y luego informan de la cuenta conectada a una URL que les proporcionaste, a veces desde sus servidores en lugar de desde el navegador. Decláralos con type: 'custom' y dos funciones lógicas:
src/connection-providers/linkedin-connection.ts
Añadir conexión y Reconectar funcionan igual que para OAuth. Twenty comprueba que el miembro pueda conectarse, firma un estado de corta duración para el intento y ejecuta authorizeLogicFunction con una callbackUrl que lo contiene. Devuelve a dónde enviar al usuario:
src/logic-functions/handlers/linkedin-authorize-handler.ts
Pasa callbackUrl al servicio sin cambios. Twenty la acepta como una redirección del navegador (GET) y como una notificación del servidor (POST), y ejecuta callbackLogicFunction para cada solicitud con su método, parámetros de consulta (sin state), encabezados, cuerpo analizado y rawBody (los bytes exactos de la solicitud, null en GET). Devuelve la cuenta que demuestra la solicitud o null cuando no contiene ninguna; normalmente, el navegador regresa mientras el id de la cuenta se envía mediante una notificación:
src/logic-functions/handlers/linkedin-callback-handler.ts
El estado firmado en callbackUrl solo demuestra que la solicitud contiene esa URL, no que el servicio la envió. Cuando el servicio firma sus notificaciones, verifica la firma sobre rawBody antes de confiar en el cuerpo. Los encabezados están en minúsculas, y cookie, authorization y proxy-authorization nunca se transmiten:
src/logic-functions/handlers/hosted-callback-handler.ts
La devolución de llamada devuelve:Ambas funciones se ejecutan mientras el miembro espera una redirección, por lo que su tiempo de espera está limitado a 10 segundos. Un fallo o una devolución no válida cancela la conexión: no se guarda nada y onConnectLogicFunction no se ejecuta. onConnectLogicFunction se ejecuta cuando se crea, reconecta o restaura una conexión, pero no de nuevo cuando el servicio informa de una cuenta que ya está conectada. El estado caduca después de una hora, por lo que se rechaza una notificación que llegue más tarde. Los servicios suelen reintentar las notificaciones: una repetición para una cuenta ya conectada actualiza esa conexión, por lo que es seguro devolver de nuevo la misma cuenta.Twenty no actualiza ni revoca las conexiones personalizadas. Libera lo que el servicio retiene para la cuenta en onDisconnectLogicFunction.El type de un proveedor, al igual que su scope, no puede cambiar una vez sincronizado: para mover un servicio entre oauth y custom, declara un nuevo proveedor con un nuevo universalIdentifier.Los proveedores personalizados se incluyen con Twenty 2.52.0.
Cuando un usuario hace clic en Reconnect, nada le impide autorizar una cuenta de origen distinta de aquella con la que se estableció la conexión: el espacio de trabajo B de Slack en lugar del espacio de trabajo A, otra organización de GitHub u otra cuenta de Google. Sin una forma de distinguirlas, Twenty sobrescribiría los tokens de A con los de B y todo lo que tu aplicación hubiera creado sobre esa conexión empezaría silenciosamente a comunicarse con B.Establece identifyAccountLogicFunction en una función de lógica de la misma aplicación que devuelva un identificador estable para la cuenta asociada al token recién emitido:
src/connection-providers/slack-connection.ts
Cuándo se ejecuta. De forma sincrónica dentro de la devolución de llamada de OAuth, inmediatamente después de intercambiar el código de autorización por tokens y antes de guardar nada. El usuario está esperando la redirección, por lo que, cuando se instala o sincroniza la aplicación, Twenty limita el timeoutSeconds de la función a 10, independientemente de lo que declare el manifiesto; limítalo a una única solicitud al endpoint «quién soy» del proveedor. El hook de conexión, si lo hay, se sigue ejecutando después, una vez guardada la conexión.Carga útil. El controlador recibe el token de acceso provisional. Aún no se ha guardado, por lo que getConnection no puede devolverlo; úsalo directamente y no lo registres.
Resultado. Devuelve el id de la cuenta y, opcionalmente, un identificador para mostrar:
  • externalAccountId debe ser estable durante toda la vida de la cuenta ascendente (un id de equipo, un id de organización o un id de usuario), nunca un nombre que el usuario pueda cambiar. Twenty lo recorta y lo almacena en la conexión durante la primera conexión.
  • handle, cuando se devuelve, sustituye al handle que Twenty determinaría de otro modo (el correo electrónico del id_token de OIDC o el correo electrónico del usuario que se conecta). Devuelve algo que un usuario reconozca, como el nombre del espacio de trabajo de Slack.
  • Devuelve { externalAccountId: null } cuando el proveedor no tiene un id estable que ofrecer. Twenty se comporta entonces como si no se hubiera declarado ninguna función de identificación: no se almacena ningún id, una reconexión borra cualquier id almacenado anteriormente y no se comprueban las reconexiones. Un null sin más o la ausencia de un retorno se tratan como un error, por lo que un controlador que olvide devolver algo no puede desactivar silenciosamente la comprobación.
  • Si la función lanza una excepción, agota el tiempo de espera o devuelve cualquier otra cosa, la conexión no se guarda y el usuario vuelve a la página de configuración con un error. Lanza una excepción solo cuando realmente no puedas identificar la cuenta.
Rechazo de reconexión. Durante una reconexión, Twenty compara el externalAccountId devuelto con el almacenado en la conexión. Si difieren, se rechaza la devolución de llamada antes de que se escriba nada: los tokens, el identificador y el ámbito existentes permanecen como estaban, el hook de conexión no se ejecuta y el usuario ve qué cuenta autorizó y a cuál pertenece la conexión. Las conexiones creadas antes de que tu aplicación declarara la función aún no tienen un id almacenado; su próxima reconexión lo almacenará.Twenty no elimina duplicados según externalAccountId: conectar dos veces la misma cuenta upstream mediante Agregar conexión crea dos conexiones. El id se expone en listConnections y getConnection, por lo que una aplicación que quiera una conexión por cuenta ascendente puede detectar los duplicados por sí misma (por ejemplo, en su hook al conectar).Cómo escribir la función. La mayoría de los proveedores exponen un único endpoint que describe el token actual. Para Slack es auth.test, que devuelve el espacio de trabajo (team_id, team) y el usuario detrás del token:
src/logic-functions/slack-identify-account.ts
Otros proveedores siguen el mismo patrón, tanto si su endpoint «quién soy» es REST como GraphQL. Lanza un error cuando el proveedor rechace el token y devuelve el id como una cadena: convierte los id numéricos, como los id de usuario de GitHub, con String(id).La identificación está disponible a partir de Twenty 2.48.0. Si tu aplicación depende de ella, declara "engines": { "twenty": ">=2.48.0" } en su package.json.
Algunos proveedores te entregan datos en el momento de la conexión que necesitas conservar antes de que la conexión sea utilizable: el ejemplo clásico es Slack, donde la respuesta de OAuth identifica el team_id del espacio de trabajo con el que se asociarán los eventos entrantes. Establece onConnectLogicFunction para que haga referencia a una función de lógica en la misma aplicación (por su universalIdentifier), y se ejecutará justo después de que se cree el ConnectedAccount.
src/connection-providers/slack-connection.ts
El hook se ejecuta de forma asíncrona en el espacio de trabajo que se está conectando (se pone en cola, no se espera su finalización), por lo que un hook lento o con fallos nunca bloquea ni interrumpe el callback de OAuth; hazlo idempotente y haz que gestione sus propios reintentos. El controlador recibe:
A partir de ahí, usa getConnection(connectedAccountId) para leer el token de acceso reciente y llamar a la API del proveedor (por ejemplo, Slack auth.test) o para conservar un mapeo con el almacenamiento de pares clave-valor.
Todo lo que una aplicación reclame en el momento de la conexión debe liberarse cuando la conexión se termina. Una integración de Slack que reclame un team_id al conectarse, por ejemplo, debe liberar esa reserva para que otro espacio de trabajo pueda conectar ese mismo espacio de trabajo de Slack. Configura onDisconnectLogicFunction para que haga referencia a una función de lógica en la misma aplicación; se ejecuta cuando se desconecta la conexión y de nuevo cuando se elimina.
src/connection-providers/slack-connection.ts
A diferencia del gancho de conexión, se ejecuta en línea en el espacio de trabajo que se está desconectando, sin reintentos: se captura un error y la desconexión o eliminación se completa de todos modos. El controlador recibe la misma estructura de payload:
Al desconectarse, ConnectedAccount y su token siguen presentes, por lo que getConnection(connectedAccountId) se resuelve y el controlador puede liberar todo lo que registró de forma remota. Al eliminar, es posible que las credenciales ya no estén disponibles (un miembro puede eliminar una conexión que desconectó anteriormente), así que haz que el controlador sea idempotente y conserva el estado que necesita (un team_id, un ID de suscripción externa) en el almacén de clave-valor, escrito en el momento de la conexión y con la clave connectedAccountId. Que sea breve: se ejecuta dentro de la solicitud, por lo que su timeoutSeconds es el límite máximo de duración de esa solicitud.El hook se dispara cuando una conexión se desconecta o se elimina por sí sola. Desinstalar la app elimina sus conexiones mediante una cascada de base de datos, por lo que el hook no se ejecuta en ese caso. Declara una uninstallLogicFunction en defineApplication para esa ruta: se ejecuta antes de que se eliminen los metadatos de la app, por lo que todavía puede llamar a listConnections y limpiar lo que quede.
Dentro de un controlador de función de lógica, listConnections({ providerName }) devuelve las filas ConnectedAccount de esta aplicación para el proveedor indicado, con tokens de acceso actualizados.
src/logic-functions/handlers/create-linear-issue-handler.ts
Cada conexión tiene:Puntos clave:
  • Pasa { providerName } para filtrar por proveedor; omítelo para obtener todas las conexiones que posee esta aplicación en todos los proveedores.
  • El servidor actualiza de forma transparente el token de acceso antes de devolver la respuesta. Tu controlador siempre ve un token utilizable (o authFailedAt establecido).
  • getConnection(id) es el equivalente de una sola fila.
La plataforma establece authFailedAt por sí sola únicamente cuando falla una actualización de OAuth. Los proveedores cuyos tokens nunca se actualizan (por ejemplo, un token de bot de Slack) fallan únicamente en el momento de la llamada dentro de tus controladores, por lo que la fila de conexión en la configuración seguiría mostrando Conectada mientras falla cada llamada. Cuando tu código recibe un rechazo de autenticación definitivo, infórmalo:
Esto establece authFailedAt y el motivo opcional en la conexión. La fila de configuración cambia a Se necesita reconectar con un botón Reconectar, la página de detalles de la conexión muestra el motivo y getConnection comienza a lanzar AppConnectionAuthFailedError para esa fila. La marca y el motivo se borran automáticamente cuando el usuario se reconecta. Se rechaza un reason de más de 1000 caracteres, así que trunca las cargas útiles del proveedor antes de pasarlas.Informa únicamente los fallos que una reconexión realmente corregiría (un rechazo de la clase invalid_auth del proveedor), nunca los errores transitorios de red. Una aplicación solo puede informar de sus propias conexiones, y un usuario de solicitud solo puede informar de sus propias credenciales personales.La mutación detrás de este asistente se incluye con Twenty 2.40.0, por lo que llamarla en un servidor anterior falla. Si tu aplicación la usa, declara la dependencia en tu package.json: "engines": { "twenty": ">=2.40.0" }.
Cada conexión tiene uno de dos ámbitos:
  • Compartida en el espacio de trabajo ('WORKSPACE') — cualquier miembro del espacio de trabajo puede usar la credencial. Los activadores de cron y de base de datos también la ven, ya que no tienen usuario de la solicitud.
  • Solo para mí ('USER') — la credencial pertenece al miembro que se conecta. En las ejecuciones realizadas en nombre de un miembro (activador de ruta HTTP con isAuthRequired: true), listConnections devuelve las conexiones compartidas más las propias de ese miembro, y findConnectionForRequest selecciona la conexión personal del miembro, recurriendo a una compartida cuando no tiene ninguna. En las ejecuciones sin un usuario de solicitud (por ejemplo, activadores cron), listConnections devuelve todas las conexiones, incluidas las personales de cada miembro.
El scope del proveedor determina cuál reciben sus nuevas conexiones:
  • scope: 'WORKSPACE' (el predeterminado) — la aplicación funciona con conexiones compartidas con el espacio de trabajo. Agregar conexión en la configuración del espacio de trabajo de la aplicación crea una conexión compartida con el espacio de trabajo.
  • scope: 'USER' — cada miembro conecta su propia cuenta, creando conexiones personales. Los miembros las añaden, reconectan y eliminan desde la página de la aplicación en Configuración > Usuario > Preferencias de aplicaciones, lo que requiere el permiso Aplicaciones de usuario. La configuración del espacio de trabajo de la aplicación no tiene un botón Agregar conexión para un proveedor USER.
Preferencias de apps aún no está habilitado en todos los espacios de trabajo: donde no lo está, los miembros no tienen dónde agregar una conexión USER, así que no dependas de una para una función que todos los espacios de trabajo necesitan ahora.
Un proveedor tiene exactamente un ámbito. Si tu aplicación necesita tanto una credencial compartida como una por miembro para el mismo servicio (por ejemplo, un token de bot y la cuenta propia de cada miembro), declara dos proveedores.Una conexión siempre tiene el ámbito de su proveedor, y el ámbito de un proveedor forma parte de su identidad: una versión posterior no puede cambiar el scope de un proveedor existente, y la actualización se rechaza si lo intenta. Para mover un servicio de un ámbito a otro, declare un proveedor nuevo con un universalIdentifier nuevo y elimine el anterior. La actualización elimina entonces el proveedor anterior y todas las conexiones realizadas con él mediante la ruta de desconexión habitual (onDisconnectLogicFunction, revocación de tokens, limpieza de canales), y los miembros se conectan de nuevo mediante el proveedor nuevo.Para añadir o reconectar una conexión Solo para mí se requiere el permiso Aplicaciones de usuario, y para añadir o reconectar una conexión Compartida en el espacio de trabajo se requiere el permiso Aplicaciones del espacio de trabajo.Usa la conexión adecuada para cada controlador:
Se permiten múltiples conexiones por (usuario, proveedor), por lo que el mismo usuario puede tener “Linear personal” y “Linear de trabajo” a la vez.El ámbito del proveedor se incluye en Twenty 2.49.0. Si tu aplicación declara scope, declara "engines": { "twenty": ">=2.49.0" } en su package.json. En la misma versión, el scope de una conexión reemplaza su visibility, que sigue disponible como un alias en minúsculas en desuso. listConnections y getConnection de twenty-sdk 2.49.0 leen scope, por lo que las aplicaciones creadas con él necesitan un servidor 2.49.0.
Para cada proveedor de conexión, el administrador del servidor debe registrar primero una aplicación OAuth en el servicio de terceros.
  1. Ve a la configuración de desarrollador del proveedor (p. ej., https://linear.app/settings/api/applications/new).
  2. Configura el URI de redirección en \<SERVER_URL>/auth/apps/callback.
  3. Copia el Client ID y el Client Secret generados.
  4. Abre la aplicación instalada en Twenty como administrador del servidor → establece los valores en los serverVariables correspondientes.
  5. Luego, los miembros del espacio de trabajo pueden agregar conexiones a proveedores WORKSPACE desde la sección Conexiones de cada aplicación.