Skip to main content
يحتاج الوكيل الذي يُقدَّم عبر HTTP إلى معرفة أمرين عن كل طلب: هل يحق له الاستدعاء أصلًا، ومن هو صاحبه. تجيب الوحدة @lousho/build-ai-agent/auth عن السؤالين. تعطي المسار قائمة مرتبة من مدخلات المصادقة (jwt() أو oidc() أو basic() أو apiToken() أو anonymous() أو دالة من كتابتك)؛ فيُعيد أول مدخل يقبل الطلب هوية (principal)، وتدخل هذه الهوية في التشغيل، حيث تستطيع دوال model وinstructions وtools ونطاقات الذاكرة قراءتها.

قائمة المصادقة

تُنفَّذ المدخلات بالترتيب، ويفعل كل مدخل واحدًا من ثلاثة أشياء: إذا تجاوزت كل المدخلات الطلب، يحصل على 401. والقائمة الفارغة ترفض كل الطلبات. أما المدخل الذي يرمي أي خطأ آخر فهو خلل برمجي: يحصل الطلب على 500 بجسم عام، ويُسجَّل الخطأ بـ console.error (ولا يُرسَل أبدًا). لا ترمي الأدوات المساعدة المضمّنة أي خطأ عند بيانات اعتماد سيئة: كلمة مرور خاطئة أو رمز منتهي الصلاحية أو رمز مُصدَر لجمهور (audience) آخر، كلها تتجاوز الطلب، فتُتاح الفرصة للمدخل التالي (يعمل مدخلا jwt() لمُصدِرَين مختلفين)، وينتهي الطلب الذي لا يقبله أحد بالـ 401 نفسه. الـ Principal هو:

الأدوات المساعدة

تُنشأ كل الأدوات المساعدة مرة واحدة عند بدء التشغيل، وتتحقق من خياراتها حينها: الأداة التي لا يمكنها العمل ترمي LOUSHO_AUTH_CONFIG_INVALID بدلًا من أن ترفض كل الطلبات لاحقًا.

jwt()

تقبل Authorization: Bearer <JWT> الموقّع بالمفتاح الذي تضبطه. أعطِ مصدر مفتاح واحدًا بالضبط:
ما الذي يُفحَص:
  • يجب أن تكون قيمة alg في الترويسة إحدى قيم algorithms؛ ويُرفض none دائمًا، وكذلك الرمز الذي تعلن ترويسته امتدادًا بـ crit.
  • يجب أن تناسب الخوارزمية المفتاح: لا يُستخدم المفتاح العام أبدًا كسر HMAC مهما قالت ترويسة الرمز، ولا توفّر مجموعة المفاتيح مفتاح HMAC أبدًا.
  • الحقل exp مطلوب؛ ويُعمل بـ nbf وiat. وتسمح الثلاثة بفارق ساعة قدره clockToleranceSec (الافتراضي 60، والحد الأقصى 300).
  • يجب أن يساوي iss إحدى قيم issuer تمامًا (دون تسامح مع الشرطة المائلة الأخيرة)، عندما تضبط issuer.
  • يجب أن يحتوي aud (نصًّا أو قائمة) على إحدى قيم audience. والخيار audience مطلوب؛ مرّر allowAnyAudience: true لتخطّي هذا الفحص عن قصد.
  • تُجلب مجموعة المفاتيح بمهلة 5 ثوانٍ، وتُخزَّن مؤقتًا 10 دقائق، وتُعاد جلبها مرة واحدة كل 30 ثانية على الأكثر حين يذكر رمز kid لا تعرفه (وتُعاد محاولة الجلب الفاشل بالحد الزمني نفسه البالغ 30 ثانية). وعنوانها إعداد فقط: تُتجاهل ترويسات jku وx5u وjwk في الرمز.
الهوية الافتراضية هي { id: sub, type: 'user', authenticator: 'jwt', issuer: iss, claims }؛ والرمز الذي لا يحوي sub يُتجاوز.

oidc()

هي jwt() تأتي مفاتيحها من مزوّد OpenID Connect. تجلب <issuer>/.well-known/openid-configuration مرة واحدة (أو discoveryUrl)، وتشترط أن يساوي issuer في الوثيقة قيمتك تمامًا، وتستخدم jwks_uri منها (ويجب أن يكون https). الخوارزميات الافتراضية RS256 وES256؛ ولا يُسمح بـ HMAC.
تحمل الهوية authenticator: 'oidc'، وiss الخاص بالمزوّد في issuer.

basic()

بيانات اعتماد HTTP Basic. تُطبَّع أسماء المستخدمين وكلمات المرور بصيغة NFC وتُقارَن في زمن ثابت، ويُقارَن كل مستخدم مضبوط في كل طلب، فيستغرق المستخدم المجهول الوقت نفسه الذي تستغرقه كلمة المرور الخاطئة. ويحمل الرد 401 الترويسة WWW-Authenticate: Basic realm="<realm>", charset="UTF-8" لتعرض المتصفحات نافذة تسجيل الدخول.
لا تستخدم basic() إلا عبر HTTPS: فكلمة المرور تنتقل مع كل طلب.

apiToken() وanonymous()

تقبل apiToken(token, { id }) الترويسة Authorization: Bearer <token> (تُقارَن في زمن ثابت) بوصفها الهوية الخدمية { id: id ?? 'api-token', type: 'service', authenticator: 'api-token' }. وهي ما كان يعنيه النص الرمزي دائمًا: createRouteHandler({ auth: '<token>' }) وLOUSHO_API_TOKEN هما apiToken(). تقبل anonymous() الجميع بالهوية { id: 'anonymous', type: 'user', authenticator: 'anonymous' }: ضعها أخيرًا في المسارات التي تخدم المستخدمين المسجّلين والمجهولين معًا، واقرأ principal.authenticator أثناء التشغيل.

مدخل من كتابتك

أي دالة تأخذ Request تصلح مدخلًا. أعلِن التحديات (challenges) التي تردّ بها بالحالة 401، وإلا أعلنت Bearer:

الحالتان 401 و403 والترويسة WWW-Authenticate

  • لم يقبل أحد الطلب (أو رمى مدخل AuthError(401)): تُعاد 401 مع { "error": "Unauthorized" } وترويسة WWW-Authenticate واحدة لكل تحدٍّ مميّز في القائمة، بالترتيب (Bearer لـ jwt وoidc وapiToken؛ وBasic realm="..." لـ basic).
  • رمى مدخل AuthError(403): تُعاد 403 مع { "error": "Forbidden" }.
  • فشل مدخل بطريقة أخرى: تُعاد 500 مع { "error": "Internal Server Error" }.
لا يذكر الجسم أبدًا أي فحص فشل: فالرمز المنتهي والتوقيع السيئ والجمهور الخاطئ كلها تحصل على 401 نفسها. ويحمل كل ردّ مصادقة الترويسة Cache-Control: no-store. ولا تُسجَّل الرموز وكلمات المرور أبدًا.

أين يعمل

تحتاج oidc() وjwt({ jwksUrl }) إلى fetch صادر إلى مجموعة المفاتيح، وهو متاح في كل بيئات التشغيل المذكورة. وفي Worker مكتوب يدويًا، استدعِ createRouteHandler أو routeAuth() بنفسك مع أي أداة مساعدة.

createRouteHandler

يقبل auth القائمة، أو مدخلًا واحدًا، أو نص رمز، أو (كما في السابق) دالة تُعيد قيمة منطقية. تقبل true الطلب بالهوية { id: 'anonymous', type: 'user', authenticator: 'custom' }، وتعني false الحالة 401. وبلا auth يكون المسار مفتوحًا، وفي بيئة الإنتاج (NODE_ENV=production) يُسجَّل تحذير واحد. والمسار GET <basePath>/health ليس خلف auth أبدًا. وتصل الهوية المقبولة إلى كل المسارات: POST <basePath> و/chat والموافقات ونقطة useChat. راجع Next.js.

خادم Node وauth.ts

يأخذ createDeployedServer(agent, { auth }) القائمة نفسها (أو مدخلًا واحدًا). وعند ضبط LOUSHO_API_TOKEN، تُلحَق apiToken(LOUSHO_API_TOKEN) بعد مدخلاتك، فيظل رمز المشغّل صالحًا. أما القنوات تحت /channels/<name> فليست خلف القائمة: فهي تتحقق من طلباتها بنفسها. في مجلد الوكيل، ضع القائمة في auth.ts (أو auth.js) بجوار agent.ts:
يجمّعها lousho build --target=node-server (أو docker) ويستخدمها الخادم المبني (تُعيدها resolveAgentDir() باسم auth؛ ويذكر البيان auth: true). ومع وجود auth.ts، لا يُستخدم الرمز المضمَّن عبر adapter.scaffold(..., { auth: { token } })؛ أما LOUSHO_API_TOKEN فيبقى مستخدمًا. ويقدّم الهدف cloudflare-worker ملفات المواصفات فقط ويُبقي رمز LOUSHO_API_TOKEN؛ راجع النشر.

قراءة الهوية أثناء التشغيل

تكون الهوية في سياق التشغيل بجوار sessionId وmetadata:
الحقل principal منفصل عن metadata عن قصد: فـ metadata هو ما أرسله المستدعي، وprincipal هو ما تحقّقت منه المصادقة. والتشغيل الديناميكي المُستأنف من نقطة الحفظ يحتفظ بالهوية التي بدأ بها. وتضبط قناتا Slack وDiscord المُرسِل هويةً (authenticator: 'slack' / 'discord'). أما الأدوات وسياسات الموافقة والتشغيلات الساكنة الموقوفة فلا ترى الهوية بعد.

ملاحظات أمنية

  • لا تتحقق مصادقة المسار ممن يملك الجلسة. يستطيع أي مستدعٍ يجتاز المصادقة ويعرف معرّف جلسة أن يقرأ نصّها (GET /chat/:id) ويتابعها ويبتّ في موافقاتها المعلّقة. استخدم معرّفات جلسات لا يمكن تخمينها، أو اشتقّها من الهوية في جانبك، أو تحقق من الملكية في مسارك قبل استدعاء المعالج.
  • لا تستخدم basic() إلا عبر HTTPS؛ والرمز الحامل (bearer) كلمة مرور أيضًا، لذا أنهِ TLS أمام أي خادم ليس على localhost.
  • أبقِ issuer وaudience مضبوطين في jwt(): وإلا قُبل هنا رمز صُدر لخدمة أخرى من خدماتك.