Skip to content

Secrets & Connections

Две разные сущности:

  • Secret — произвольная зашифрованная строка (API-key, webhook URL, токен, пароль). Управляется вручную через UI.
  • Connection — OAuth-привязка к конкретному провайдеру с автоматической ротацией access/refresh-токенов. Создаётся через OAuth-flow.

Каждый secret — это:

  • id (uuid)
  • name (уникален в пределах space’а)
  • description (опционально)
  • ciphertext (AES-256-GCM, encrypted master key’ем space’а)
  • createdAt / updatedAt / archivedAt

Plain-text никогда не хранится. Master key — SECRET_ENCRYPTION_ KEY в .env (32 байта base64), с поддержкой ротации через SECRET_ENCRYPTION_KEY_VERSION.

В workflow’е secret подтягивается через template:

external.http
url = "https://api.vendor.com/data"
headers = {
Authorization: "Bearer {{secrets.vendor-api-key}}"
}

Resolver вызывает decryption на runtime’е, plain-text держится в RAM только на время исполнения step’а. Trace’у secret-значение не записывается (подменяется [SECRET]).

OpUIAPI
СоздатьSettings → Secrets → + NewPOST /spaces/<id>/secrets
Посмотреть metadataсписок в UIGET /spaces/<id>/secrets
Увидеть plain-text❌ никогда❌ нет API
РотироватьSettings → Secrets → → RotatePUT /secrets/<id>/value
АрхивироватьSettings → Secrets → → DeleteDELETE /secrets/<id>

Архивированный secret нельзя использовать в новых workflow’ах, но running-run’ы с него дочитывают значение. Нельзя расшифровать ciphertext без master key’а — потеря ключа = потеря секретов (одна из причин нужен key rotation runbook; backlog’ом).

Почему UI не показывает plain-text

Section titled “Почему UI не показывает plain-text”

В UX некоторые платформы прячут key за «eye» иконкой → клик раскрывает. Мы это принципиально не делаем:

  • Shoulder-surfing (в опене-офисе за вами кто-то смотрит)
  • Screenshots / screen recordings (копирование случайное)
  • Audit-логи браузера (DevTools ловит секреты в network-tab)

Если вам нужно plain-text (например, экспорт в другой инструмент) — пересоздайте в провайдере + сохраните в новый secret.

Многие вендоры не дают long-lived tokens, только OAuth2 с refresh- циклом (Google / Yandex / Bitrix24 app / Slack / GitHub). Для них OPORA использует отдельную сущность — Connection:

  • id (uuid)
  • providerKey (google / github / slack / yandex-id / …)
  • name (человекочитаемое, для UI)
  • externalAccountId (id пользователя у провайдера)
  • accessToken + refreshToken (оба encrypted)
  • expiresAt (когда access expired)
  • scopes (что дал провайдер)
  • revokedAt (nullable — если отозван)

OPORA внутри connection-resolver’а проверяет expiresAt перед каждым handler-call’ом. Если expired — делает refresh через /token endpoint провайдера, обновляет access/refresh/expiresAt в БД, потом идёт handler’у.

Пользователь этого не видит — для workflow’а connection выглядит «всегда свежей».

Connections+ Connect → выбираете провайдера → OPORA ре-директит на authorize-URL провайдера → вы авторизуетесь → провайдер ре-директит обратно на /auth/oauth/complete → OPORA сохраняет tokens.

Connections → connection → Disconnect:

  • OPORA вызывает revoke endpoint провайдера (best-effort; если провайдер не поддерживает — skip)
  • Ставит revokedAt = now()
  • Connection остаётся в БД (для audit’а) но в workflow’ах помечена как UNAVAILABLE — run с таким connection’ом упадёт с OAUTH_CONNECTION_REVOKED

Connections → connection → Force-refresh. Полезно когда провайдер поменял scope’ы и нужно проверить что refresh работает.

Secret vs connection — когда что

Section titled “Secret vs connection — когда что”
ПровайдерSecretConnection
Bitrix24 (webhook-URL)✅ — URL как secret(OAuth2 отложен)
amoCRM (long-lived)✅ — access_token(OAuth2 отложен)
ЮKassa✅ — shop_id + secret_key
Telegram✅ — bot token
Google (Sheets, Calendar)✅ OAuth2
GitHub✅ OAuth2
Slack✅ OAuth2
Yandex ID✅ OAuth2

Правило: если у провайдера есть только OAuth2 — Connection. Если есть long-lived API-key — Secret (проще, не надо refresh-flow перезапускать).

Secret / connection в пределах одного space’а доступны всем workflow’ам этого space’а. Между space’ами — никогда (даже если вы owner обоих space’ов — это tenant-изоляция).

Secret’ы хранятся локально в Postgres на вашей VM’е. Ничего в Sentry / Resend / внешние SaaS’ы не улетает (PII scrubber проверяет). Это упрощает compliance — всё в РФ-периметре.

  • How it works — как connection-resolver живёт на runtime’е
  • OAuth providers list — какие connection’ы поддерживаем сейчас