الانتقال إلى المحتوى الرئيسي

نظرة عامة

الاتصالات هي بيانات اعتماد يحتفظ بها المستخدم لخدمة خارجية (Linear وGitHub وSlack، …). يحدّد تطبيقك كيف يتم الحصول على تلك بيانات الاعتماد — موفّر اتصال — ويستخدمها وقت التشغيل لإجراء استدعاءات مُصادَقة إلى واجهة برمجة تطبيقات الطرف الثالث. OAuth 2.0 مدمج. تستخدم الخدمات التي لها تدفق اتصال خاص بها — صفحة تسجيل دخول مستضافة، ومعرّف حساب يُسلَّم عبر webhook — موفّر 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 على مستوى الخادم، ويوجد تطبيق OAuth واحد لكل خادم Twenty.
  • إلى أن يتم ملء كلا 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
تعيد دالة الاستدعاء:تعمل كلتا الدالتين بينما ينتظر العضو إعادة التوجيه، لذا يقتصر مهلة انتهائهما على 10 ثوانٍ. يؤدي الفشل أو الإرجاع غير الصالح إلى إلغاء الاتصال: لا يُحفظ شيء ولا تعمل onConnectLogicFunction. تعمل onConnectLogicFunction عند إنشاء اتصال أو إعادة اتصاله أو استعادته، ولا تعمل مجدداً عندما تُبلغ الخدمة عن حساب متصل بالفعل. تنتهي صلاحية الحالة بعد ساعة، لذا يُرفض الإشعار الذي يصل لاحقاً. تعيد الخدمات عادةً محاولة الإشعارات: يؤدي تكرار لحساب متصل بالفعل إلى تحديث ذلك الاتصال، لذا من الآمن إعادة الحساب نفسه مرة أخرى.لا يقوم Twenty بتحديث الاتصالات المخصصة أو إلغائها. حرّر ما تحتفظ به الخدمة للحساب في onDisconnectLogicFunction.لا يمكن تغيير type لموفر، مثل scope الخاص به، بعد مزامنته: لنقل خدمة بين oauth وcustom، عرّف موفراً جديداً باستخدام universalIdentifier جديد.تتوفر الموفرات المخصصة مع Twenty 2.52.0.
عندما ينقر مستخدم على Reconnect، لا شيء يمنعه من تفويض حساب مصدر مختلف عن الحساب الذي أُنشئ الاتصال به: مساحة عمل Slack B بدلًا من مساحة العمل A، أو مؤسسة GitHub أخرى، أو حساب Google آخر. من دون طريقة للتمييز بينهما، سيستبدل Twenty رموز A المميزة برموز B، وسيبدأ كل ما أنشأه تطبيقك اعتمادًا على ذلك الاتصال بالتواصل مع B بصمت.عيّن identifyAccountLogicFunction إلى دالة منطقية في التطبيق نفسه تُرجع معرّفًا ثابتًا للحساب المرتبط بالرمز المميز الصادر حديثًا:
src/connection-providers/slack-connection.ts
متى يتم تشغيلها. بشكل متزامن داخل معاودة نداء OAuth، مباشرةً بعد استبدال رمز التفويض برموز مميزة وقبل حفظ أي شيء. ينتظر المستخدم إعادة التوجيه، لذا عند تثبيت التطبيق أو مزامنته، يقيّد Twenty قيمة timeoutSeconds للدالة بـ 10، بغض النظر عما يعلنه البيان؛ اجعلها طلبًا واحدًا إلى نقطة نهاية “من أنا” الخاصة بالموفر. يستمر تشغيل خطاف on-connect، إن وُجد، بعد ذلك، بمجرد حفظ الاتصال.الحمولة. يتلقى المعالج رمز الوصول المؤقت. لم يُحفَظ بعد، لذا لا يمكن لـ getConnection إرجاعه؛ استخدمه مباشرةً ولا تسجّله.
النتيجة. أعد معرّف الحساب، ومعالجًا اختياريًا لعرضه:
  • يجب أن يكون externalAccountId ثابتًا طوال عمر الحساب المصدر (معرّف فريق أو معرّف مؤسسة أو معرّف مستخدم)، وألا يكون أبدًا اسمًا يمكن للمستخدم تغييره. يقوم Twenty بتشذيبه وتخزينه في الاتصال عند أول اتصال.
  • يستبدل handle، عند إرجاعه، المعالج الذي كان Twenty سيستنتجه بخلاف ذلك (بريد OIDC الإلكتروني في id_token، أو بريد المستخدم الذي يجري الاتصال). أعد شيئًا يتعرّف عليه المستخدم، مثل اسم مساحة عمل Slack.
  • أعد { externalAccountId: null } عندما لا يملك الموفر معرّفًا ثابتًا لتقديمه. يتصرف Twenty حينها كما لو لم يتم الإعلان عن دالة تعريف: لا يُخزَّن أي معرّف، وتؤدي إعادة الاتصال إلى مسح أي معرّف مخزن سابقًا، ولا يتم التحقق من عمليات إعادة الاتصال. يُعامل null المجرد أو عدم وجود قيمة معادة على أنه خطأ، لذلك لا يمكن لمعالج ينسى الإرجاع تعطيل التحقق بصمت.
  • إذا أطلقت الدالة استثناءً، أو انتهت مهلتها، أو أعادت أي شيء آخر، فلن يُحفظ الاتصال وسيعود المستخدم إلى صفحة الإعدادات مع ظهور خطأ. أطلق استثناءً فقط عندما يتعذر عليك فعلًا تحديد الحساب.
رفض إعادة الاتصال. عند إعادة الاتصال، يقارن Twenty قيمة externalAccountId المعادة بتلك المخزنة في الاتصال. إذا اختلفا، يُرفض رد الاتصال قبل كتابة أي شيء: تبقى الرموز المميزة والمقبض والنطاق الحالية كما كانت، ولا يُشغَّل خطاف عند الاتصال، ويرى المستخدم الحساب الذي منح له التفويض والحساب الذي ينتمي إليه الاتصال. لا تحتوي الاتصالات التي أُنشئت قبل أن يعلن تطبيقك عن الدالة على معرّف مخزن بعد؛ وتخزنه عند إعادة اتصالها التالية.لا يزيل Twenty التكرارات استنادًا إلى externalAccountId: يؤدي ربط الحساب المصدر نفسه مرتين عبر إضافة اتصال إلى إنشاء اتصالين. يُكشف عن المعرّف في listConnections وgetConnection، لذا يمكن للتطبيق الذي يريد اتصالًا واحدًا لكل حساب مصدر اكتشاف التكرارات بنفسه (على سبيل المثال في خطاف on-connect الخاص به).كتابة الدالة. يكشف معظم الموفرين عن نقطة نهاية واحدة تصف الرمز الحالي. بالنسبة إلى Slack، تكون هذه auth.test، التي تعيد مساحة العمل (team_id، team) والمستخدم المرتبط بالرمز:
src/logic-functions/slack-identify-account.ts
يتبع الموفرون الآخرون النمط نفسه، سواء كانت نقطة نهاية “من أنا” الخاصة بهم REST أو GraphQL. أطلق استثناءً عندما يرفض الموفر الرمز، وأعد المعرّف كسلسلة نصية: حوّل المعرّفات الرقمية، مثل معرّفات مستخدمي 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
يعمل الـ hook بشكل غير متزامن في مساحة العمل التي تجري الاتصال (يتم وضعه في قائمة الانتظار، ولا يُنتظر انتهاؤه)، لذلك لن يؤدي hook بطيء أو فاشل إلى حظر أو تعطيل رد نداء OAuth — اجعله قابلاً للتكرار دون آثار جانبية (idempotent) ويتولى إعادة المحاولات بنفسه. يتلقى المعالج:
من هناك استخدم getConnection(connectedAccountId) لقراءة رمز الوصول الجديد واستدعاء واجهة برمجة تطبيقات المزوّد (على سبيل المثال، Slack auth.test) أو حفظ تعيين باستخدام مخزن المفاتيح والقيم.
أي شيء يطالب به تطبيق ما وقت الاتصال يجب تحريره عندما يزول الاتصال. على سبيل المثال، يجب على تكامل Slack الذي يطالب بـ team_id عند الاتصال تحرير تلك المطالبة حتى تتمكن مساحة عمل أخرى من ربط فريق Slack نفسه. عيّن onDisconnectLogicFunction للإشارة إلى دالة منطقية في التطبيق نفسه، ويتم تشغيلها عند فصل الاتصال ومرة أخرى عند حذفه.
src/connection-providers/slack-connection.ts
بخلاف خطاف الاتصال، يتم تشغيله ضمنيًا في مساحة العمل التي يجري فصل اتصالها، من دون إعادات محاولة: يتم تسجيل الإخفاق ويستمر إكمال الفصل أو الحذف. يتلقى المعالج نفس شكل الحمولة:
عند فصل الاتصال، يظل ConnectedAccount ورمزه المميز موجودين، لذا تُرجع getConnection(connectedAccountId) نتيجة ويمكن للمعالج تحرير أي شيء سجّله عن بُعد. عند الحذف، قد تكون بيانات الاعتماد قد اختفت بالفعل (يمكن للعضو حذف اتصال فصله سابقًا)، لذا اجعل المعالج آمنًا لإعادة التنفيذ واحتفظ بالحالة التي يحتاجها (مثل team_id أو معرّف اشتراك خارجي) في مخزن المفاتيح والقيم، وتُكتب عند وقت الاتصال وتُفهرس بواسطة connectedAccountId. اجعله قصيرًا: فهو يعمل ضمن الطلب، لذا فإن timeoutSeconds يمثل الحد الأقصى لمدة استغراق هذا الطلب.يتم تشغيل الخطاف عند قطع اتصال أو إزالته من تلقاء نفسه. يؤدي إلغاء تثبيت التطبيق إلى حذف اتصالاته عبر حذف متسلسل في قاعدة البيانات، لذلك لا يتم تشغيل الخطاف في هذه الحالة. صرّح عن 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' — يتصل كل عضو بحسابه الخاص، مما ينشئ اتصالات شخصية. يضيف الأعضاء التطبيقات ويعيدون ربطها ويزيلونها من صفحة التطبيق ضمن الإعدادات > المستخدم > تفضيلات التطبيقات، الأمر الذي يتطلب إذن تطبيقات المستخدم. لا تحتوي إعدادات مساحة عمل التطبيق على زر إضافة اتصال لموفّر USER.
لم يتم تمكين تفضيلات التطبيقات في كل مساحة عمل بعد: ففي الأماكن التي لا تكون فيها مُمكّنة، لا يتوفر للأعضاء مكان لإضافة اتصال USER، لذا لا تعتمد على اتصال واحد من أجل ميزة تحتاجها كل مساحة عمل الآن.
للموفّر نطاق واحد فقط. إذا كان تطبيقك يحتاج إلى بيانات اعتماد مشتركة وأخرى لكل عضو للخدمة نفسها (مثل رمز بوت وحساب كل عضو الخاص)، فأعلن عن موفّرين.يكون للاتصال دائمًا نطاق موفره، ويُعد نطاق الموفر جزءًا من هويته: لا يمكن لإصدار لاحق تغيير scope لموفر موجود، ويُرفض التحديث إذا حاول ذلك. لنقل خدمة من نطاق إلى آخر، عرّف موفرًا جديدًا باستخدام universalIdentifier جديد وأزل الموفر القديم. يؤدي التحديث بعد ذلك إلى حذف الموفر القديم وكل اتصال أُنشئ به عبر مسار قطع الاتصال المعتاد (onDisconnectLogicFunction، وإلغاء الرمز المميز، وتنظيف القناة)، ثم يعيد الأعضاء الاتصال عبر الموفر الجديد.تتطلب إضافة اتصال لي فقط أو إعادة ربطه إذن تطبيقات المستخدم، بينما تتطلب إضافة اتصال مشترك لمساحة العمل أو إعادة ربطه إذن تطبيقات مساحة العمل.استخدم الاتصال المناسب لكل معالج:
يُسمح بوجود اتصالات متعددة لكل (مستخدم، مزوّد)، لذا يمكن للمستخدم نفسه امتلاك “Linear شخصي” و”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 من قسم الاتصالات الخاص بكل تطبيق.