Перейти к основному содержанию

Обзор

Подключения — это учетные данные, которыми пользователь располагает для внешнего сервиса (Linear, GitHub, Slack, …). Ваше приложение определяет, как получают эти учетные данные — через провайдера подключения — и использует их во время выполнения для выполнения аутентифицированных вызовов к стороннему API. OAuth 2.0 встроен. Сервисы с собственным потоком подключения — размещенная страница входа, идентификатор аккаунта, передаваемый через вебхук, — используют провайдер custom, в котором ваше приложение управляет потоком, а Twenty сохраняет пользователя, рабочее пространство и подключение. Будущие типы учетных данных (персональные токены доступа, ключи API, базовая аутентификация) будут подключаться к тому же интерфейсу — приложения, уже использующие defineConnectionProvider({ type: 'oauth', ... }) не потребуют миграции.
Провайдер подключения описывает процедуру OAuth-обмена, которая требуется вашему приложению. Для провайдера WORKSPACE (по умолчанию) участник нажимает «Добавить подключение» в настройках вашего приложения, завершает процесс на экране согласия провайдера, и в его рабочем пространстве создается запись ConnectedAccount. См. раздел «Подключения рабочего пространства или личные подключения» для провайдеров USER.Рабочей конфигурации нужны два файла — провайдер подключения и соответствующее объявление serverVariables в defineApplication, которое содержит учетные данные клиента OAuth.
src/connection-providers/linear-connection.ts
src/application.config.ts
Основные моменты:
  • name — это уникальная строка-идентификатор, используемая в listConnections({ providerName }) (kebab-case, должна соответствовать ^[a-z][a-z0-9-]*$).
  • displayName отображается на вкладке «Общие» приложения и в списке инструментов ИИ.
  • scope определяет, кто подключается: 'WORKSPACE' (по умолчанию) для подключений, общих для всего рабочего пространства, и 'USER' для подключений, которые каждый участник создает для себя. См. ниже раздел «Подключения рабочего пространства или личные подключения».
  • logo — это путь к ресурсу, включенному в папку public/ вашего приложения и обслуживаемому через публичные ресурсы приложения. Если параметр не указан, на вкладке «Общие» используется заполнитель, созданный на основе displayName.
  • clientIdVariable / clientSecretVariable — это имена, а не значения — они должны совпадать с ключами, объявленными в defineApplication.serverVariables. Фактические client_id и client_secret вводятся администратором сервера через интерфейс регистрации приложения и никогда не коммитятся в ваш репозиторий.
  • Используйте serverVariables (не applicationVariables) — учетные данные OAuth являются общими для сервера, и на каждом сервере Twenty используется одно приложение OAuth.
  • Пока оба serverVariables не заполнены, на вкладке «Общие» приложения отображается подсказка «нужен администратор сервера», а кнопка «Добавить подключение» отключена.
  • type имеет значение 'oauth' (с блоком oauth) или 'custom' (с блоком custom, см. «Пользовательские потоки подключения»). Дискриминатор совместим с будущими версиями: будущие типы ('pat', 'api-key', …) добавит новые блоки подконфигурации наряду с этими.
URL обратного вызова OAuth, который вашему провайдеру нужно добавить в список разрешенных:
Некоторые сервисы подключают аккаунты через собственный поток вместо OAuth: Unipile, Plaid или Merge отправляют пользователя на размещенную страницу, а затем сообщают о подключенном аккаунте по указанному вами URL-адресу, иногда со своих серверов, а не из браузера. Объявите их с помощью type: 'custom' и двух логических функций:
src/connection-providers/linkedin-connection.ts
Добавление подключения и Повторное подключение работают так же, как для OAuth. Twenty проверяет, может ли участник подключиться, подписывает краткосрочное состояние для попытки и запускает authorizeLogicFunction с callbackUrl, который его содержит. Верните, куда направить пользователя:
src/logic-functions/handlers/linkedin-authorize-handler.ts
Передайте callbackUrl сервису без изменений. Twenty принимает его как перенаправление браузера (GET) и как уведомление сервера (POST) и запускает callbackLogicFunction для каждого запроса с его методом, параметрами запроса (без state), заголовками, обработанным телом и rawBody (точными байтами запроса, null для GET). Верните аккаунт, который подтверждает запрос, или null, если он не содержит аккаунта — обычно это браузер, возвращающийся в то время, как идентификатор аккаунта передается уведомлением:
src/logic-functions/handlers/linkedin-callback-handler.ts
Подписанное состояние в callbackUrl доказывает только то, что запрос содержит этот URL-адрес, но не то, что его отправил сервис. Когда сервис подписывает свои уведомления, проверяйте подпись для rawBody, прежде чем доверять телу. Заголовки приводятся к нижнему регистру, а cookie, authorization и proxy-authorization никогда не передаются:
src/logic-functions/handlers/hosted-callback-handler.ts
Callback возвращает:Обе функции выполняются, пока участник ожидает перенаправления, поэтому их время ожидания ограничено 10 секундами. Ошибка или недопустимое возвращаемое значение прерывает подключение: ничего не сохраняется и onConnectLogicFunction не запускается. onConnectLogicFunction запускается при создании, повторном подключении или восстановлении подключения, но не запускается повторно, когда сервис сообщает об уже подключенном аккаунте. Срок действия состояния истекает через час, поэтому уведомление, поступившее позже, отклоняется. Сервисы обычно повторно отправляют уведомления: повтор для уже подключенного аккаунта обновляет это подключение, поэтому повторно вернуть тот же аккаунт безопасно.Twenty не обновляет и не отзывает пользовательские подключения. Освободите то, что сервис хранит для аккаунта, в onDisconnectLogicFunction.type провайдера, как и его scope, нельзя изменить после синхронизации: чтобы перевести сервис между oauth и custom, объявите нового провайдера с новым universalIdentifier.Пользовательские провайдеры доступны в Twenty 2.52.0.
Когда пользователь нажимает Повторное подключение, ничто не мешает ему авторизовать другую вышестоящую учетную запись вместо той, с которой было установлено подключение: рабочую область Slack B вместо рабочей области A, другую организацию GitHub или другую учетную запись Google. Не имея способа отличить их друг от друга, Twenty перезапишет токены A токенами B, и все, что ваше приложение создало на основе этого подключения, незаметно начнет взаимодействовать с B.Укажите для identifyAccountLogicFunction функцию логики в том же приложении, которая возвращает стабильный идентификатор учетной записи, связанной с только что выданным токеном:
src/connection-providers/slack-connection.ts
Когда выполняется. Синхронно в обработчике обратного вызова OAuth, сразу после обмена кода авторизации на токены и до сохранения каких-либо данных. Пользователь ожидает перенаправления, поэтому при установке или синхронизации приложения Twenty ограничивает timeoutSeconds функции значением 10 независимо от того, что указано в манифесте; выполняйте только один запрос к конечной точке провайдера “who am I”. Хук on-connect, если он есть, всё равно запускается позже, после сохранения подключения.Полезная нагрузка. Обработчик получает предварительный токен доступа. Он ещё не сохранён, поэтому getConnection не может его вернуть; используйте его напрямую и не записывайте в журнал.
Результат. Верните id учётной записи и, при необходимости, отображаемый идентификатор:
  • externalAccountId должен быть неизменным на протяжении всего существования вышестоящей учётной записи (id команды, id организации, id пользователя), а не именем, которое пользователь может изменить. Twenty обрезает его и сохраняет в подключении при первом подключении.
  • handle, если он возвращён, заменяет идентификатор, который иначе определил бы Twenty (электронный адрес из OIDC id_token или электронный адрес подключающегося пользователя). Верните то, что пользователь узнает, например имя рабочего пространства Slack.
  • Верните { externalAccountId: null }, когда провайдер не может предоставить неизменный id. Тогда Twenty ведёт себя так, как если бы функция идентификации не была объявлена: id не сохраняется, повторное подключение очищает любой ранее сохранённый id, а повторные подключения не проверяются. Простой null или отсутствие возвращаемого значения считается ошибкой, поэтому обработчик, который забыл вернуть значение, не сможет незаметно отключить проверку.
  • Если функция генерирует исключение, превышает время ожидания или возвращает что-либо иное, подключение не сохраняется, а пользователь возвращается на страницу настроек с ошибкой. Генерируйте исключение только тогда, когда действительно не можете идентифицировать учётную запись.
Отклонение повторного подключения. При повторном подключении Twenty сравнивает возвращённый externalAccountId с сохранённым в подключении. Если они различаются, обратный вызов отклоняется до внесения каких-либо записей: существующие токены, дескриптор и область действия остаются без изменений, хук подключения не запускается, а пользователь видит, какую учетную запись он авторизовал и какой учетной записи принадлежит подключение. Подключения, созданные до того, как ваше приложение объявило функцию, ещё не имеют сохранённого id; при следующем повторном подключении он будет сохранён.Twenty не устраняет дубликаты по externalAccountId: повторное подключение той же вышестоящей учётной записи через Добавить подключение создаёт два подключения. Id доступен в listConnections и getConnection, поэтому приложение, которому нужно одно подключение для каждой вышестоящей учётной записи, может самостоятельно обнаруживать дубликаты (например, в своём хуке on-connect).Написание функции. Большинство провайдеров предоставляют одну конечную точку, описывающую текущий токен. Для Slack это auth.test, который возвращает рабочее пространство (team_id, team) и пользователя, которому принадлежит токен:
src/logic-functions/slack-identify-account.ts
Другие провайдеры используют тот же шаблон независимо от того, является ли их конечная точка “who am I” REST или GraphQL. Генерируйте исключение, когда провайдер отклоняет токен, и возвращайте id как строку: преобразуйте числовые id, например id пользователей GitHub, с помощью String(id).Идентификация доступна в Twenty 2.48.0. Если ваше приложение зависит от неё, объявите "engines": { "twenty": ">=2.48.0" } в его package.json.
Некоторые провайдеры передают вам данные в момент подключения, которые нужно сохранить до того, как соединение станет пригодным к использованию — классический пример — Slack, где OAuth-ответ указывает team_id рабочей области, по которому будут определяться входящие события. Установите onConnectLogicFunction, чтобы сослаться на логическую функцию в том же приложении (по её universalIdentifier), и она будет выполнена сразу после создания ConnectedAccount.
src/connection-providers/slack-connection.ts
Хук выполняется асинхронно в подключающейся рабочей области (он ставится в очередь, а не ожидается), поэтому медленный или падающий хук никогда не блокирует и не ломает OAuth-callback — сделайте его идемпотентным и обеспечьте собственную обработку повторных попыток. Обработчик получает:
Оттуда используйте getConnection(connectedAccountId), чтобы прочитать новый access token и вызвать API провайдера (например, Slack auth.test) или сохранить отображение с помощью key-value store.
Всё, на что приложение заявляет права при подключении, должно быть освобождено, когда подключение прекращается. Интеграция со Slack, которая, например, заявляет team_id при подключении, должна освободить это право, чтобы другое рабочее пространство могло подключить ту же Slack-команду. Установите onDisconnectLogicFunction, чтобы сослаться на логическую функцию в том же приложении; она выполняется при отключении подключения и повторно при его удалении.
src/connection-providers/slack-connection.ts
В отличие от хука подключения, она выполняется непосредственно в отключаемом рабочем пространстве, без повторных попыток: сбой фиксируется, а отключение или удаление всё равно завершается. Обработчик получает тот же формат полезной нагрузки:
При отключении ConnectedAccount и его токен всё ещё существуют, поэтому getConnection(connectedAccountId) успешно возвращает результат, и обработчик может отменить всё, что он зарегистрировал удалённо. При удалении учётные данные могут уже отсутствовать (участник может удалить подключение, которое он отключил ранее), поэтому сделайте обработчик идемпотентным и сохраняйте необходимое ему состояние (team_id, идентификатор внешней подписки) в хранилище ключей и значений, записывая его при подключении и используя connectedAccountId в качестве ключа. Не задавайте слишком большое значение: он выполняется в рамках запроса, поэтому его timeoutSeconds задаёт максимальную продолжительность этого запроса.Hook срабатывает, когда соединение отключается или удаляется само по себе. При удалении приложения его соединения удаляются каскадом на уровне базы данных, поэтому hook в этом случае не запускается. Объявите uninstallLogicFunction в defineApplication для этого сценария: она запускается до удаления метаданных приложения, поэтому всё ещё может вызвать listConnections и выполнить оставшуюся очистку.
Внутри обработчика логической функции listConnections({ providerName }) возвращает записи ConnectedAccount этого приложения для указанного провайдера с обновленными токенами доступа.
src/logic-functions/handlers/create-linear-issue-handler.ts
Каждое подключение имеет:Основные моменты:
  • Передайте { providerName }, чтобы отфильтровать по провайдеру; опустите, чтобы получить все подключения этого приложения у всех провайдеров.
  • Сервер прозрачно обновляет токен доступа перед возвратом. Ваш обработчик всегда получает рабочий токен (или установлено authFailedAt).
  • getConnection(id) — эквивалент для одной записи.
Платформа самостоятельно устанавливает authFailedAt только при сбое обновления OAuth. Провайдеры, чьи токены никогда не обновляются (например, токен бота Slack), дают сбой только во время вызова внутри ваших обработчиков, поэтому строка подключения в настройках продолжит показывать Подключено, хотя каждый вызов будет завершаться ошибкой. Когда ваш код получает однозначный отказ в аутентификации, сообщите о нём:
Это устанавливает authFailedAt и необязательную причину для подключения. Строка в настройках изменится на Требуется повторное подключение с кнопкой «Повторно подключить», страница сведений о подключении покажет причину, а getConnection начнёт выбрасывать AppConnectionAuthFailedError для этой строки. Флаг и причина автоматически очищаются, когда пользователь повторно подключается. reason длиной более 1000 символов отклоняется, поэтому сокращайте полезные нагрузки провайдера перед их передачей.Сообщайте только о сбоях, которые действительно исправит повторное подключение (отказе класса invalid_auth от провайдера), и никогда — о временных сетевых ошибках. Приложение может сообщать только о собственных подключениях, а пользователь запроса — только о собственных личных учетных данных.Мутация, лежащая в основе этого помощника, поставляется вместе с Twenty 2.40.0, поэтому её вызов на более старом сервере завершится ошибкой. Если ваше приложение использует её, укажите зависимость в package.json: "engines": { "twenty": ">=2.40.0" }.
Каждое подключение имеет одну из двух областей действия:
  • Общее для рабочего пространства ('WORKSPACE') — любой участник рабочего пространства может использовать эти учетные данные. Триггеры cron/базы данных также видят их, поскольку у них нет пользователя запроса.
  • Только для меня ('USER') — учетные данные принадлежат подключающемуся участнику. При запусках от имени участника (триггер HTTP-маршрута с isAuthRequired: true) listConnections возвращает общие подключения и собственные подключения этого участника, а findConnectionForRequest выбирает личное подключение участника, переходя к общему подключению, если личного нет. При запусках без пользователя запроса (например, триггеры cron) listConnections возвращает все подключения, включая личные подключения каждого участника.
scope провайдера определяет, какую область действия получат его новые подключения:
  • scope: 'WORKSPACE' (по умолчанию) — приложение работает с подключениями, общими для рабочего пространства. Добавить подключение в настройках рабочего пространства приложения создает общее для рабочего пространства подключение.
  • scope: 'USER' — каждый участник подключает свою учетную запись, создавая личные подключения. Участники добавляют, повторно подключают и удаляют их на странице приложения в разделе Settings > User > Apps preferences, для чего требуется разрешение User Apps. В настройках рабочего пространства приложения нет кнопки Добавить подключение для поставщика USER.
Настройки приложений пока включены не во всех рабочих пространствах: там, где они не включены, участникам негде добавить подключение USER, поэтому не полагайтесь на него для функции, которая сейчас нужна каждому рабочему пространству.
У провайдера может быть только одна область действия. Если вашему приложению для одного сервиса нужны как общие учетные данные, так и отдельные для каждого участника (например, токен бота и собственная учетная запись каждого участника), объявите два провайдера.Подключение всегда имеет область действия своего поставщика, и область действия поставщика является частью его идентичности: более поздняя версия не может изменить scope существующего поставщика, и обновление отклоняется, если оно пытается это сделать. Чтобы переместить службу из одной области действия в другую, объявите нового поставщика с новым universalIdentifier и удалите старого. При обновлении старый поставщик и все подключения, созданные с его помощью, удаляются через обычный путь отключения (onDisconnectLogicFunction, отзыв токена, очистка канала), а участники снова подключаются через нового поставщика.Для добавления или повторного подключения соединения Just for me требуется разрешение User Apps, а для добавления или повторного подключения соединения Workspace shared — разрешение Workspace Apps.Используйте подходящее подключение для каждого обработчика:
Допускается несколько подключений на пару (пользователь, провайдер), поэтому один и тот же пользователь может иметь “Personal Linear” и “Work Linear” одновременно.Область действия провайдера доступна в Twenty 2.49.0. Если ваше приложение объявляет scope, укажите "engines": { "twenty": ">=2.49.0" } в его package.json. В той же версии scope подключения заменяет его visibility, который остается доступным как устаревший псевдоним в нижнем регистре. listConnections и getConnection из twenty-sdk 2.49.0 считывают scope, поэтому приложениям, созданным с его помощью, требуется сервер версии 2.49.0.
Для каждого провайдера подключения администратору сервера сначала нужно зарегистрировать у стороннего сервиса приложение OAuth.
  1. Перейдите в настройки разработчика провайдера (например, https://linear.app/settings/api/applications/new).
  2. Установите Redirect URI в значение \<SERVER_URL>/auth/apps/callback.
  3. Скопируйте сгенерированные Client ID и Client Secret.
  4. Откройте установленное приложение в Twenty под учетной записью администратора сервера → задайте значения в соответствующих serverVariables.
  5. Затем участники рабочего пространства смогут добавлять подключения к провайдерам WORKSPACE в разделе Подключения конкретного приложения.