nessie

API

واجهة HTTP التي تقوم عليها كل تطبيقات Nessie، وكيفية المصادقة عليها.

ما هي واجهة API

لدى Nessie واجهة API واحدة تعتمد على HTTP. تطبيق الويب الإداري وتطبيقات macOS وiOS والخدمة الخلفية للمنفّذ عن بُعد كلها عملاء عاديون لهذه الواجهة، ولا يمكن الوصول إلى أي شيء يستطيعون فعله بطريقة أخرى. وهي واجهة JSON تعمل على خادم Fastify، وليست GraphQL، ولا تملك مجموعة “عامة” منفصلة: فالواجهة التي تستدعيها نصوصك البرمجية هي الواجهة نفسها التي يستدعيها المنتج.

لا يوجد جزء للإصدار في المسار. تقع كل المسارات تحت /api، باستثناء نقطة MCP الواردة POST /mcp وعدد قليل من مستندات الاكتشاف تحت /.well-known/. ويُحافَظ على التوافق بإضافة الحقول فقط، فيستمر العميل الذي يتجاهل ما لا يعرفه في العمل.

عنوان URL الأساسي

تُستضاف Nessie ذاتيًا، لذا فإن عنوان URL الأساسي هو أصل الخادم الذي تقدّم عليه منصتك واجهة API. يضبطه المشغِّل في NESSIE_API_PUBLIC_URL، وهي القيمة نفسها التي تستخدمها واجهة API حين تحتاج إلى إنشاء عنوان URL مطلق خاص بها. وفي النسخة التي نشغّلها، هو https://api.nessie.works؛ أما نسخة التطوير التي تُشغَّل بالأمر pnpm dev فتستجيب على http://localhost:5454.

تطبيق الويب الإداري أصل مختلف عن واجهة API، لذلك تخضع عملاء المتصفح لقائمة سماح CORS (NESSIE_CORS_ORIGINS). أما الطلبات الصادرة من نص برمجي أو خادم فلا أصل لها يُفحص، ولا تتأثر بذلك.

المصادقة

يحمل كل طلب خارج مجموعة صغيرة من المسارات العامة المقصودة رمز وصول في الترويسة القياسية:

Authorization: Bearer <token>

الرمز هو JWT موقَّع تصدره واجهة API. وهو قصير العمر: ثلاثون دقيقة افتراضيًا (NESSIE_AUTH_TOKEN_TTL)، وهو بيانات الاعتماد الوحيدة التي يقدّمها أي مستدعٍ عادي. لا تملك Nessie مفاتيح API شخصية طويلة الأمد. وإذا أردت أن يستدعي نص برمجي واجهة API، فإنه يسجّل الدخول بالطريقة نفسها التي يسجّل بها متصفح الشخص، ويجدّد رمزه كلما تقدّم.

الحصول على رمز

POST /api/auth/session ينشئ جلسة. ويُعيد رمز الوصول وهوية المستدعي، ويضع رمز التجديد في ملف تعريف ارتباط منفصل مُقيَّد بـ HTTP فقط.

تعتمد طريقة إثبات هويتك على كيفية نشر النسخة. تقبل النسخة التي تعمل بوضع local البريد الإلكتروني وكلمة المرور. أما في نسخة hosted أو selfHosted فيُرفض تسجيل الدخول بكلمة المرور صراحةً برمز 403 PASSWORD_AUTH_DISABLED، ولا منفذ للدخول إلا عبر مزوّد الهوية المُعدّ، فلا يمكن أن ينشأ للحساب مسار هوية ثانٍ وأضعف إلى جانبه.

تسجيل الدخول عبر مزوّد هوية

تسجيل الدخول الموحّد هو تبادل رمز التفويض في OAuth مع PKCE، على ثلاث خطوات. ومزوّد الهوية في Nessie هو UnlikeOtherAI، وهو الذي يملك أيضًا بنية المؤسسات والفرق التي تعكسها النسخة. والتبادل أدناه بالشكل نفسه مع أي مزوّد مُعدّ.

  1. GET /api/auth/providers يسرد ما تقبله هذه النسخة، ولكل مزوّد providerId وتسمية وبيان عما إذا كان ينبغي لصفحة تسجيل الدخول أن تحوّل إليه تلقائيًا.
  2. GET /api/auth/providers/:providerId/authorize، مع codeChallenge وredirectUri وstate معاملات في الاستعلام، يُعيد { data: { authorizeUrl } }. وجِّه الشخص إليه.
  3. يعيد المزوّد الشخص إلى redirectUri لديك مع رمز. أرسِل هذا الرمز إلى POST /api/auth/session مع { providerId, code, codeVerifier, redirectUri }؛ فالحقول الأربعة مطلوبة معًا، والمجموعة الناقصة تُقابَل بالرمز 400 EXTERNAL_AUTH_INCOMPLETE.

تجديد الجلسة

لا يظهر رمز التجديد أبدًا في متن الاستجابة. بل يُضبط كملف تعريف ارتباط HTTP-only اسمه nessie_refresh، ونطاقه المسار /api/auth، ومدة صلاحيته ثلاثون يومًا افتراضيًا (NESSIE_AUTH_REFRESH_TOKEN_TTL). ويؤدي استدعاء POST /api/auth/refresh مع ملف تعريف الارتباط هذا إلى استهلاكه، وإصدار رمز وصول جديد، وضبط ملف تعريف ارتباط جديد مكانه. وتتدوّر الرموز، لذلك لا يصلح الرمز المسروق إلا إلى أن يجدّد العميل الحقيقي جلسته في المرة التالية.

يحتفظ المتصفح المسجَّل الدخول إلى عدة حسابات بملف تعريف ارتباط من هذا النوع لكل تسجيل دخول، باسم nessie_refresh_<slot>، ويحدد الحساب المقصود بالطلب بترويسة x-nessie-account-slot. والعميل الذي لا يرسل هذه الترويسة يقصد nessie_refresh، وهذا كل ما يحتاجه عميل سطر الأوامر.

يحتاج عميل سطر الأوامر إذًا إلى مخزن لملفات تعريف الارتباط. ومع curl يكون ذلك بالخيار --cookie-jar في استدعاء تسجيل الدخول، والخيار --cookie في التجديد، كما في المثال التطبيقي أدناه.

ما الذي يُتحقَّق منه في كل طلب

المصادقة ليست مجرد فحص للتوقيع. قبل تشغيل أي معالج، تتحقق واجهة API من توقيع الرمز وانتهاء صلاحيته، وتؤكد أن الجلسة لم تُلغَ (سواء عبر إصدار الرمز على مستوى المستخدم أو عبر معرّف الجلسة الفردي)، وتحمّل عضوية المستدعي في المؤسسة التي يسمّيها الرمز. والعضوية المعطّلة تعني 403. وعندما تكون المؤسسة مرتبطة بمزوّد هوية خارجي، تُعاد مراجعة وضع المستدعي الحالي لدى ذلك المزوّد عند الطلب، ولا يُقرأ من الرمز، فالوصول الذي سُحب في المصدر يتوقف فورًا لا عند انتهاء الصلاحية التالي.

بيانات اعتماد ليست رموز جلسات

توجد بيانات اعتماد حاملة أخرى من نوعين، وليس أيٌّ منهما مفتاحًا عامًا. تُقبل بيانات اعتماد جهاز الصوت في مسارات المكالمات الصوتية فقط، وتُقبل بيانات اعتماد وصول الوكيل في نقطة MCP فقط. وتقديم أيٍّ منهما في أي مكان آخر يُرفض برمز 403 يسمّي بيانات الاعتماد، ولا يُعاد تجربته كجلسة أضعف. راجع MCP والأدوات المتصلة لمعرفة كيفية إصدار بيانات اعتماد الوكيل.

غلاف الاستجابة

كل استجابة ناجحة كائن يحتوي على المفتاح data. لا توجد مصفوفة مجرّدة ولا حمولة على المستوى الجذري، فلا يحتاج العميل أبدًا إلى التمييز بين الأشكال:

{
  "data": { "id": "3f1c…", "label": "engineering" }
}

تضيف نقاط نهاية القوائم كائن meta بجوار البيانات. ولا يُضاف أي شيء آخر على المستوى الأعلى أبدًا.

تستخدم الإخفاقات شكلًا واحدًا أيضًا، أيًّا كان مصدرها:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "A message needs content or at least one attachment",
    "field": "body",
    "details": { "formErrors": [], "fieldErrors": {} }
  }
}

code هو الجزء القابل للقراءة آليًا، وهو ما ينبغي أن تتفرّع عليه الشيفرة؛ وmessage موجّهة إلى الإنسان. ويَرِد field وdetails عندما يكون الإخفاق محددًا بما يكفي لتحديد موقعه. أما خطأ الخادم غير المعالَج فهو دائمًا 500 INTERNAL_ERROR برسالة ثابتة، إذ يُسجَّل نص الخطأ الأصلي ولا يُعاد أبدًا، لأنه ربما التقط بيانات الطلب في طريقه إلى الخارج.

رموز الأخطاء التي ستواجهها أولًا

الحالةالرمزالمعنى
400VALIDATION_ERRORلم يطابق الجسم أو الاستعلام المخطط المحدد.
401AUTH_REQUIREDلا يوجد رمز حامل، أو أن الترويسة مشوّهة.
401TOKEN_EXPIREDانتهت صلاحية رمز الوصول. جدّده ثم أعد المحاولة.
401TOKEN_REVOKEDسُجّل الخروج من الجلسة، أو أُجبرت كل الجلسات على الخروج.
403ACCOUNT_DEACTIVATEDعضوية هذه المؤسسة عُطّلت.
404NOT_FOUNDلا يوجد مسار بهذا الاسم، أو لا يوجد سجل بهذا الاسم يراه هذا المستدعي.
429RATE_LIMITEDامتلأ حاجز الطلبات. اقرأ Retry-After.
500INTERNAL_ERRORخطأ خادم غير معالَج، وهو مُبهم عمدًا.

ترقيم الصفحات

تُقسَّم نقاط نهاية القوائم إلى صفحات بمؤشر keyset مُبهَم، لا باستخدام offset، لأن الصفوف تُضاف بينما يقرأ أحدهم، ومع الإزاحة تكرّر الصفحة الثانية بصمت سجلًا عرضته الصفحة الأولى بالفعل.

تُقبل ثلاثة معاملات في الاستعلام: cursor وlimit وdirection (forward أو backward). الصفحة الافتراضية 25 عنصرًا، والحد الأقصى 100؛ والقيمة الأكبر تُقصّ إلى الحد ولا تُرفض. وتضيف نقاط النهاية التي ترتّب أو تُصفّي أيضًا sort وorder وq للبحث النصي الحر.

يخبرك meta الصفحة بمكانك:

{
  "data": [ … ],
  "meta": {
    "hasMore": true,
    "nextCursor": "2026-09-12T08:31:44.005Z|8b2f…",
    "prevCursor": null,
    "total": 134
  }
}

nextCursor وprevCursor حقيقيان معًا، فـ“السابق” و“التالي” زرّان يتوقعهما الشخص. وهما مُبهمان: فالخادم يضع مفتاح ترتيبه داخلهما، والعميل الذي يحلّلهما أو ينشئهما سينكسر في اللحظة التي تغيّر فيها القائمة ترتيبها. وtotal موجودة في معظم القوائم، وتُحذف حيث لا يكون العدّ ذا معنى، كنتائج البحث المرتبة.

حدود الطلبات

تُطبَّق الحدود على نوافذ زمنية ثابتة، وتُفرض قبل قراءة جسم الطلب أصلًا. والطلب المرفوض هو 429 برمز RATE_LIMITED وترويسة Retry-After بالثواني.

تُربط معظم الحدود بعنوان IP للعميل، أما الحدود الحساسة لبيانات الاعتماد (تسجيل الدخول والتجديد وإعادة إثبات كلمة المرور وكتابة بيانات اعتماد MCP) فتحتفظ بعدّاد لكل حساب إضافةً إلى ذلك، وترفض الطلب عندما يتجاوز أيٌّ منهما الحد. فعنوان واحد يجرّب كلمات مرور على كثير من الحسابات يصطدم بجدار، وحساب واحد يُهاجَم من عناوين كثيرة يصطدم بجدار أيضًا. ويبلغ الحد الافتراضي لتسجيل الدخول 10 محاولات لكل IP و5 لكل بريد إلكتروني كل عشر دقائق؛ وإرسال رسالة 60 في الدقيقة؛ والكتابة تحت /api/agents 60 في الدقيقة. وكل عتبة قابلة للضبط لكل نشر، فاعتبر هذه القيم الافتراضية المشحونة مع المنتج، لا عقدًا ملزمًا. وتُستثنى فحوص الصحة وبثوث الوقت الفعلي.

الوقت الفعلي

الاستطلاع الدوري لقائمة هو الطريقة الخاطئة لمتابعة محادثة. لذلك توجد ثلاث نقاط نهاية للبث بدلًا منه:

  • GET /api/events/stream — أحداث يرسلها الخادم (server-sent events) لكل ما يستطيع الشخص المسجّل الدخول رؤيته.
  • GET /api/threads/:threadId/stream — أحداث بث لخيط واحد. ويحترم Last-Event-ID، فإعادة الاتصال تستأنف ولا تبدأ من جديد.
  • GET /api/activity — اتصال WebSocket يحمل خلاصة النشاط.

لا تستطيع المتصفحات ضبط الترويسات عند مصافحة WebSocket، لذلك يقبل مسار WebSocket رمز الوصول أيضًا كمعامل في الاستعلام ?token=. وهذا هو المكان الوحيد الذي ينبغي أن يظهر فيه الرمز في عنوان URL؛ وكل مسار آخر يأخذه من الترويسة.

عائلات الموارد

توجد أكثر من ألف نقطة نهاية، وهي أكثر بكثير من أن تُسرد هنا. وهي مُجمّعة في عائلات، وما إن تعرف بادئة العائلة حتى تصبح المسارات الفردية متوقعة: مجموعة عند البادئة، وسجل تحتها، وإجراءات مسمّاة كمسارات فرعية بطريقة POST.

العائلةالبادئةما تغطيه
المصادقة/api/authتسجيل الدخول والتجديد وتسجيل الخروج والهوية الحالية، وجلسات الشخص الخاصة به.
القنوات والخيوط/api/channels, /api/threadsنموذج المحادثة: القنوات تحتوي على خيوط، والخيوط تحتوي على رسائل، والردود تتفرّع تحت رسالتها الأصلية.
الوكلاء/api/agentsتعريفات الوكلاء، وارتباطات قنواتهم، ومحادثاتهم، ومهامهم، وحالتهم.
المشاريع واللوحات/api/projects, /api/tasks, /api/iterationsتتبّع العمل: لوحات وأعمدة تحت مشروع، ومهام عليها، وتكرارات فوقها.
المعرفة/api/knowledge-baseمساحات من الصفحات والملفات المُنقّحة، مع التعليقات والروابط الخلفية والبحث.
البريد/api/mail, /api/mailbox-connectionsصناديق البريد المتصلة، وصناديق بريد الوكلاء، والمسودات التي كتبها وكيل ليوافق عليها شخص.
الموافقات/api/approvalsقائمة الإجراءات التي لا يحق للوكيل تنفيذها من تلقاء نفسه، والاستدعاء الواحد الذي يحسمها.
التدقيق/api/audit-logسجل التدقيق المربوط بسلسلة تجزئة للمؤسسة، مع نقطة تحقق. للمالكين والمسؤولين.
التكلفة والاستخدام/api/ledger, /api/billingمحاسبة الرموز والتكلفة، والميزانيات وتوقيتات التشغيل؛ وبشكل منفصل، الأرصدة والشحنات. للمالكين والمسؤولين.
المؤسسة والفرق/api/organizations, /api/teams, /api/usersإدارة المؤسسة والفرق والعضوية، بما فيها الدعوات.
المحفّزات/api/triggersالجداول الزمنية وخطافات الويب التي تبدأ عمل الوكلاء، مع سجل مرات تشغيلها.
المكالمات والصوت/api/calls, /api/voiceالاتصال داخل القناة، وجلسة مساعد الصوت ونصّها وسجلها، ورموز الأجهزة الصوتية.
المنفّذون/api/executors, /api/execution-environment-templatesالأجهزة التي يعمل عليها الوكلاء، وبيئاتها، وقناة إقران الخدمة الخلفية.
التشغيلات/api/runsالتشغيلات الحية للوكلاء: الإلغاء والمتابعة وإعادة التشغيل والربط بمنفّذ.
الرفع/api/uploads, /api/attachmentsارفع الملف أولًا، ثم اربط المعرّف المُعاد بالرسالة.
البحث/api/messages/searchالبحث في الرسائل على مستوى المؤسسة كلها، أو مقصورًا على قناة واحدة.
الأسرار والإعدادات/api/secrets, /api/settings/scopedتخزين بيانات الاعتماد للكتابة فقط مع منح صريحة، وقيم الإعدادات المتدرجة.
الأدوات المتصلة/api/mcpخوادم MCP التي تتصل بها هذه النسخة لاستدعائها. راجع MCP والأدوات المتصلة.
الصحة/api/healthفحوص الحيوية والجاهزية، وكلاهما عام. ويتحقق /api/health/ready أيضًا من قاعدة البيانات.

تُفرض الصلاحيات لكل مسار، لا لكل عائلة. وبوجه عام: سجل التدقيق وسجل التكاليف وقواعد السياسات ومستوى التحكم بالاستدلال أسطح خاصة بمالكي المؤسسة ومسؤوليها، بينما المحادثات والمشاريع والمعرفة والموافقات مفتوحة لأي عضو، ويُضيَّق نطاقها بما يستطيع ذلك العضو رؤيته فعلًا.

أمثلة تطبيقية

سجّل الدخول واحتفظ بملف تعريف ارتباط التجديد

على نسخة تعمل بوضع local، وبكلمة مرور. يعود رمز الوصول في متن الاستجابة، ويستقر رمز التجديد في مخزن ملفات تعريف الارتباط.

curl -sS https://api.example.com/api/auth/session \
  --cookie-jar nessie.cookies \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com","password":"…"}'
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "me": {
      "user": {
        "id": "1d4e…",
        "email": "ada@example.com",
        "displayName": "Ada Lovelace",
        "roleIds": ["owner"],
        "superAdmin": false
      },
      "session": {
        "sessionId": "9c07…",
        "issuedAt": "2026-09-16T09:00:00.000Z",
        "expiresAt": "2026-09-16T09:30:00.000Z"
      },
      "context": {
        "organizationId": "5a81…",
        "projectId": "7b12…",
        "teamId": "2e60…",
        "channelId": null,
        "bootstrapMode": false
      },
      "auth": {
        "providerId": "local",
        "providerType": "local-bootstrap",
        "autoRedirectToSso": false
      }
    }
  }
}

عندما ينتهي الرمز، استبدل ملف تعريف الارتباط برمز جديد:

curl -sS -X POST https://api.example.com/api/auth/refresh \
  --cookie nessie.cookies --cookie-jar nessie.cookies

تأتي الاستجابة بالشكل نفسه { token, me }، ويحمل مخزن ملفات تعريف الارتباط الآن رمز التجديد المُدوَّر.

اعرض القنوات التي تراها

curl -sS https://api.example.com/api/channels \
  -H "Authorization: Bearer $TOKEN"
{
  "data": [
    {
      "id": "8b2f…",
      "label": "engineering",
      "type": "standard",
      "visibility": "public",
      "projectId": "…",
      "projectName": "Platform",
      "teamId": "…",
      "defaultThreadId": "c41a…",
      "unreadCount": 3,
      "lastMessageAt": "2026-09-15T18:22:07.441Z",
      "viewerCanManage": true
    }
  ]
}

أضف ?teamId=… لتقصرها على فريق واحد، أو ?includeArchived=true لتضمين القنوات المؤرشفة. وdefaultThreadId هو الخيط الذي تنشر فيه بعد ذلك.

انشر رسالة

تحتاج الرسالة إلى بعض content أو مرفق واحد مرفوع على الأقل. أرسل Idempotency-Key، فالطلب المُعاد يُحسم إلى الرسالة التي أنشأتها المحاولة الأولى، بدلًا من نشر نسخة ثانية.

curl -sS https://api.example.com/api/threads/c41a…/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 0f9c2d16-8f4a-4c1e-9a77-6b0d4f2e1a53' \
  -d '{"content":"Deploying the migration now."}'

الرسالة الجديدة تُعاد بالرمز 201:

{
  "data": {
    "message": {
      "id": "d70e…",
      "threadId": "c41a…",
      "role": "user",
      "content": "Deploying the migration now.",
      "createdAt": "2026-09-16T09:04:11.882Z"
    },
    "pendingAgentInvites": []
  }
}

إعادة المحاولة بمفتاح Idempotency نفسه تُجيب بالرمز 200 مع الرسالة الأصلية ومصفوفة pendingAgentInvites فارغة، لأن المحاولة الأولى قامت بالتنبيه والإرسال اللذين تصفهما المصفوفة.

يستحق رفضان على هذا المسار معالجة صريحة. المحتوى الذي يتجاوز حد طول الدردشة يُقابَل بالرمز 413 MESSAGE_TOO_LARGE، والاستجابة الصحيحة هي رفع النص كملف. والمحتوى الذي يبدو بنيويًا كبيانات اعتماد يُقابَل بالرمز 422 SECRET_INTERCEPTED، إذ يرفض الخادم الرسالة قبل تخزينها أو إرسالها أو تسجيلها، وينبغي أن توضع بيانات الاعتماد في /api/secrets.