tokens من AgentStore، مفهرسةً بالمزوّد وبمالك بيانات الاعتماد. أما تسجيل الدخول (إعادة التوجيه، ومسار رد النداء، وإيقاف التشغيل مؤقتًا إلى أن يتصل المستخدم) فيأتي في إصدار لاحق؛ والمخزن اليوم هو اللبنة الأساسية، ولا شيء يسجّل الدخول من تلقاء نفسه.
ملّاك بيانات الاعتماد
لكل بيانات اعتماد مخزَّنة مزوّد (اسم مثلgithub، من 1 إلى 64 حرفًا من A-Z وa-z و0-9 و_ و-) ومالك:
- التطبيق (
{ owner: 'app' }): بيانات اعتماد واحدة يستخدمها الوكيل للجميع، مثل حساب خدمة أو تثبيت بوت. - مستخدم (
{ owner: 'user', principalId, issuer? }): بيانات اعتماد واحدة لكل شخص سجّل الدخول.principalIdهو معرّف المستخدم، وissuerيسمّي مزوّد الهوية الذي أصدره، فيكونaliceمن جهة إصدار وaliceمن جهة أخرى مالكَين مختلفَين. مع مصادقة المسارات يُطابَق مبدأ الطلب (principal) عليه هكذا:{ owner: 'user', principalId: principal.id, issuer: principal.issuer }؛ وبيانات اعتماد المستخدم تحتاج إلى ذلك المبدأ، لأنه بدونه لا يوجد مستخدم يملكها.
tokenStoreKey(provider, owner) هو مفتاح سجل بيانات الاعتماد: <provider>|app، أو <provider>|user|<issuer>|<principalId> مع ترميز النسبة المئوية لجهة الإصدار والمعرّف (جهة الإصدار الغائبة جزء فارغ). يجعل الترميز المفتاح غير ملتبس: فمعرّف مبدأ يحتوي | لا يمكن أن يتعارض مع مالك آخر.
تخزين الرموز
AgentStore.tokens هو OAuthTokenStore:
getوsetوdeleteتقرأ بيانات اعتماد واحدة وتكتبها (بالاستبدال) وتزيلها.list({ provider?, owner? })تُرجع بيانات وصفية فقط: المزوّد والمالك ونوع الرمز وانتهاء الصلاحية والنطاق (scope) وهل يوجد رمز تحديث مخزَّن ووقت كتابة السجل. ولا تُرجع أبدًا رمز وصول أو رمز تحديث.putPending(state, value, ttlMs)وtakePending(state)تحفظان عملية تسجيل دخول جارية (مُتحقِّق PKCE ومكان العودة) بين إعادة التوجيه ورد النداء. تُرجعtakePendingالقيمة مرة واحدة وتحذفها؛ وبعدttlMsتزول. وstateمن 16 إلى 128 حرفًا منA-Zوa-zو0-9و_و-؛ وأيstateمشوَّه يُمرَّر إلىtakePendingببساطة لا يُعثر عليه.setClient(provider, client)وgetClient(provider)تحفظان عميلًا مسجَّلًا لدى خادم التفويض (التسجيل الديناميكي للعميل). وقد يحتوي السجل سرًّا للعميل، لذلك يُشفَّر كما يُشفَّر الرمز.
tokens:
المفتاح. تشفّر المخازن الدائمة كل سجل بخوارزمية AES-256-GCM بمفتاح من 32 بايت تقدّمه بصيغة base64: الخيار
tokenKey، وإلا متغير البيئة LOUSHO_TOKEN_KEY. لا يوجد مفتاح افتراضي ولا يُشتق شيء من كلمة مرور. أنشئ واحدًا بـgenerateTokenKey() أو في الصدفة (shell):
process.env في Cloudflare Workers: أضف المفتاح سرًّا (wrangler secret put LOUSHO_TOKEN_KEY) ومرّر env.LOUSHO_TOKEN_KEY بوصفه tokenKey. ويفعل الـWorker الذي يولّده lousho build --target=cloudflare-worker ذلك عنك.
- المفتاح الذي ليس 32 بايت بالضبط بصيغة base64 يسبب
ConfigurationErrorعند إنشاء المخزن. - بلا مفتاح، يعمل الوكيل الذي لا يخزّن رمزًا قط كما كان: القراءات لا تجد شيئًا. أما أول
setأوsetClientأوputPendingفيرميLOUSHO_TOKEN_KEY_MISSING، وكذلك قراءة سجل موجود. - كل كتابة تستخدم متجه تهيئة (IV) عشوائيًا جديدًا من 12 بايت، فالرمز نفسه المكتوب مرتين يعطي نصَّين مشفَّرين مختلفَين. ويُوثَّق مفتاح السجل معه: فالنص المشفَّر المنسوخ إلى صف مالك آخر لا يُفكّ تشفيره. والصيغة المخزَّنة هي
v1.<base64 iv>.<base64 ciphertext>. - السجل المكتوب بمفتاح آخر، أو الذي تغيّر على القرص، يفشل بالخطأ
LOUSHO_TOKEN_DECRYPT_FAILED، الذي يسمّي المزوّد ولا يذكر الرمز أبدًا.
tokenKey: [newKey, oldKey]، أو LOUSHO_TOKEN_KEY="<new>,<old>". تستخدم الكتابة المفتاح الأول وتجرّب القراءة كل مفتاح بالتتابع، فتنتقل الرموز إلى المفتاح الجديد كلما جُدِّدت. لا يوجد أمر يعيد تشفير كل سجل؛ فبعد أن تُسقط المفتاح القديم، لا يمكن قراءة الرموز التي ما زالت مكتوبة به، ويسجّل مستخدموها الدخول من جديد (احذف تلك السجلات).
لا شيء يسجّل الرمز. لا تضع المخازن أبدًا رمز وصول أو رمز تحديث أو مُتحقِّق PKCE أو سر عميل في رسالة خطأ أو سطر سجل أو cause لخطأ، ولا تكون قيم الرموز جزءًا من أحداث الوكيل أو السجلات الحوارية أو نقاط الحفظ أو التتبّعات أو أشرطة التسجيل ما لم تُرجع أداة واحدًا منها بنفسها. والأداة التي تستخدم رمزًا ينبغي أن تُرجع جواب الـAPI لا الرمز.
الاتساق. يأخذ SQLite عملية تسجيل دخول معلّقة في معاملة واحدة، ويحجزها مخزن الملفات بإنشاء ملف حصري، فيحصل عليها مستدعٍ واحد بالضبط. أما Workers KV فلا معاملات فيه: takePending تقرأ ثم تحذف، فقد يقرأ ردّا نداء في موقعَي حافة مختلفَين، ضمن تأخّر انتشار KV (حتى دقيقة تقريبًا)، الـstate نفسه. وstate قيمة عشوائية من 128 بتًا لا يحملها إلا متصفح المستخدم، فإعادة استخدامها لا تمنح شيئًا لمن لا يملكها أصلًا. وتحتاج tokens.list() في KVStore إلى list() الخاصة بالربط (تملكها مساحة أسماء KV الحقيقية، وقد لا تملكها نسخة مزيفة مصنوعة يدويًا). أما prune() في SqliteStore فتحذف كذلك عمليات تسجيل الدخول المعلّقة المنتهية؛ ويسقطها مخزن الملفات عند بدء تسجيل الدخول التالي، وينهيها KV بنفسه.