استبدال الـ token
الخلفية
تُصدر Shukah token معتمًا (opaque). وتُصدر خدمة المصادقة JWT من Locate وترفض أي شيء آخر على
routes الـ /api/v1/clients/* التي تتطلب تسجيل دخول: فtoken من Shukah يحصل على 401. ويمثّل استبدال الـ token الجسر
بين النظامين. وهو موجود في خدمة المصادقة فقط. وليس لدى Shukah ما يقابله، فلا توجد واجهة من Shukah تُقابله.
وأثناء الترحيل قد يحمل التطبيق token لأحد النظامين ويحتاج إلى استدعاء الآخر. يحوّل هذا الـ endpoint الـ token الذي بحوزته إلى token يقبله النظام المستهدف.
الميزة
أرسل الـ token الذي بحوزتك والنظام الذي توشك على استدعائه (target_platform: shukah أو
locate). لا تحتاج إلى معرفة نوع الـ token الذي بحوزتك: فالخدمة تقرّر من شكله.
ثلاثة أجزاء تفصلها نقاط تعني JWT من Locate، وأي شيء آخر هو token من Shukah.
| الـ token الذي ترسله | target_platform | ما تحصل عليه |
|---|---|---|
| JWT من Locate | locate | الـ JWT نفسه دون تغيير. ولا يُفحص. |
| token من Shukah | shukah | الـ token نفسه دون تغيير. ولا يُفحص. |
| JWT من Locate | shukah | token من Shukah مخزَّن لذلك العميل. ويُفحص الـ JWT أولًا. |
| token من Shukah | locate | JWT جديد من Locate للعميل الذي يملك token Shukah. |
الصف الأخير هو الوحيد الذي يكتب. فكل استدعاء ينشئ JWT جديدًا من Locate ويخزّنه ويدفعه إلى Shukah عندما يكون للعميل معرّف في Shukah ويكون الدفع الصادر (outbound push) مفعّلًا. أما tokens Locate التي يخزّنها فلا تُحذف أبدًا.
يوضح المخطط التدفقي المسودة لخدمة المصادقة (2026-07-08، انظر Sources تحت «التفاصيل») كيف ينبغي للتطبيق
استخدامه. عند البدء يقرأ التطبيق إعداد العميل auth_source. فإذا كان لديه token محفوظ، يستدعي
استبدال الـ token بذلك الـ token مع ضبط target_platform على auth_source. وإن لم يكن لديه، يسجّل الدخول
برمز OTP على النظام المطابق. وهذا المخطط مسودة، ولا يزال يعرض المسار القديم
/maeda/api/exchange-token.
لا يفعل التطبيق ذلك في نسخته اللحظية بتاريخ 2026-02-26. فلا توجد شيفرة في تطبيق الجوال تستدعي استبدال
الـ token، وليس للتطبيق base URL لخدمة المصادقة ولا إعداد auth_source. لذا فالمستخدم الذي يحمل
token من Shukah ويستدعي التحقق من الـ token على Locate يحصل على 401، ثم
يحذف التطبيق الـ token المخزَّن، مما يسجّل خروج المستخدم. وهل يستدعي إصدار التطبيق الحالي، وهو
أحدث من النسخة اللحظية، هذا الـ endpoint: غير موثّق بعد.
POST /api/v1/clients/exchange-token
الطلب (Request)
لا حاجة إلى header باسم Authorization ولا يُقرأ: فالـ token المراد تحويله يوضع في JSON body.
| الحقل | الموضع | النوع | مطلوب | القيود | الوصف |
|---|---|---|---|---|---|
token | body | string | نعم | يجب ألا يكون فارغًا. ثلاثة أجزاء تفصلها نقاط تُعدّ JWT من Locate. ولا يُفحص أي شيء آخر عن الـ token في هذه الخطوة. والقيمة غير الفارغة التي ليست سلسلة تفشل بـ 401. | الـ token الذي بحوزة التطبيق. |
target_platform | body | string | نعم | تمامًا "shukah" أو "locate"، بأحرف صغيرة. وأي شيء آخر يعطي 400. | النظام الذي يوشك التطبيق على استدعائه. |
Content-Type | header | string | نعم | application/json. وبدونه لا يُقرأ الـ body، ويعطي الاستدعاء 400. | — |
الاستجابة (Response)
200 مع الـ token الخاص بالنظام المستهدف:
| الحقل | النوع | يقبل null | الصيغة / القيود | الوصف |
|---|---|---|---|---|
status | integer | لا | دائمًا 200. | — |
data.token | string | لا | JWT من Locate عندما يكون target_platform هو locate. وtoken من Shukah عندما يكون shukah. | الـ token الخاص بالنظام المستهدف. |
{
"status": 200,
"data": {
"token": "<locate-jwt>"
}
}
التُقط على dev، 2026-10-01 (العميل 356259؛ الشكل فقط): token من Shukah أُرسل مع target_platform: "locate". وقيمة الـ token نائبة (placeholder)، لأن الـ tokens لا تُخزَّن أبدًا في هذا المستودع.
{
"status": 200,
"data": {
"token": "<shukah-token>"
}
}
التُقط على dev، 2026-10-01 (العميل 356259؛ الشكل فقط): ذلك الـ JWT من Locate أُعيد إرساله مع target_platform: "shukah". أما token من Shukah أُرسل مع target_platform: "shukah" فعاد دون تغيير.
قد يعود JWT من Locate أُرسل مع target_platform: "shukah" بالشكل {"status":200,"data":{}}، عندما
لا تكون لـ token Shukah المخزَّن نسخة خام. وأي البيئات تتأثر بذلك: غير موثّق بعد (انظر «التفاصيل»).
الأخطاء
| HTTP | Body | متى |
|---|---|---|
400 | {"status":400,"message":"Invalid token or target_platform"} | token مفقود أو فارغ، أو target_platform ليس shukah ولا locate، أو ليس في الطلب Content-Type: application/json. |
400 | {"status":400,"message":"Internal server error"} | الـ body ليس JSON صالحًا. وخارج production تكون الرسالة نص المحلّل (parser)، مثل Unexpected end of JSON input، ويُضاف حقل stack. |
401 | {"status":401,"message":"Unauthorization"} | JWT من Locate إلى shukah: التوقيع غير صالح، أو الـ token مشوَّه، أو ليس للعميل token نشط من Shukah. token من Shukah إلى locate: لا يوجد token مخزَّن مطابق، أو العميل الذي يعود إليه غير موجود. وأي فشل غير متوقع داخل الاستدعاء يعطي 401 هذه أيضًا، مثل خطأ في قاعدة البيانات، أو token ليس سلسلة، أو token مكرر. |
التُقط أول body بـ 400 على dev في 2026-10-01، مع JSON body فارغ ومرة أخرى مع
target_platform بالقيمة nope. أما body الخاص بـ 401 فهو ما تُرجعه الشيفرة هنا؛ ولم تُثِره
جولة الالتقاط على هذا الـ route. وأما 400 الخاص بـ JSON المشوَّه فمصدره اختبار دون اتصال لشيفرة الخدمة،
لا dev.
جرّبه
هذا الاستدعاء يكتب: ففي كل مرة يحوّل فيها token من Shukah إلى JWT من Locate يخزّن token جديدًا ويدفعه إلى Shukah. كما تكتب الخدمة الـ request body، متضمنًا الـ token، في log الخاص بها. لا ترسل عمليتي استبدال للعميل نفسه خلال ثانية واحدة.
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # see "Getting a test token" on the module overview
curl -s -X POST "$AUTH_BASE_URL/api/v1/clients/exchange-token" -H "Content-Type: application/json" -d "{\"token\":\"$SHUKAH_TOKEN\",\"target_platform\":\"locate\"}"
المتوقع: 200 وJWT من Locate في data.token. احتفظ به في $TOKEN للصفحات الأخرى.
تنبيه
- الحالات التي يُعاد فيها الـ token دون تغيير لا تفحصه: فأي سلسلة من ثلاثة أجزاء تُرسل مع
target_platform: "locate"تعيد200. لاختبار token، استدعِ التحقق من الـ token. - token من Shukah يُرسل مع
target_platform: "locate"يخزّن token جديدًا ويدفعه إلى Shukah في كل مرة. وقد يجعل استدعاءان للعميل نفسه في الثانية نفسها الثاني منهما يفشل بـ401(قراءة شيفرة، لم يُعاد إنتاجه). - في وضع production، وحدها الـ tokens التي دفعتها Shukah مباشرة لها نسخة خام مخزَّنة، والاستبدال يحتاج إليها. السلوك في production: غير موثّق بعد.
التفاصيل (للـ backend)
فُحص dev فقط: الـ host https://dev.authentication.locate.sa والبادئة /api/v1. أما الـ host والبادئة في UAT
وproduction: غير موثّق بعد. لا يعمل dev بـ NODE_ENV=production: فأجسام أخطائه
تحمل حقل stack (التُقط في 2026-10-01 على أخطاء address-book وprofile وsetting)، والـ handler لا يضيف stack إلا خارج production (src/middlewares/error-handler.js:12-35).
لذا يُكتب token_raw على dev، لأنه لا يُخزَّن إلا خارج production (انظر «مجموعة token_bridge» أدناه).
الـ Route
- ليس للـ route أي middleware: لا حارس
client، والـ handler غير ملفوف بـasyncHandler(src/routes/client.js:22). وللـ handler try/catch خاص به، وكل استثناء بداخله يصبح401(src/controllers/client/exchange-token/index.js:10-96). - يقرأ
express.json()الـ body (src/index.js:42). والطلب بدونContent-Type: application/jsonيتركreq.bodyعلى{}، فينتج400المذكور أعلاه (الافتراضي في المكتبة، لم يُلتقط). ويسري حد الـ body الافتراضي في المكتبة وهو 100 kb. - يُكتب request body، متضمنًا الـ token، في log الخادم
(
src/controllers/client/exchange-token/index.js:32).
ما تفعله كل توليفة
| الـ token الداخل | target_platform | ما يحدث | المصدر |
|---|---|---|---|
| JWT (ثلاثة أجزاء) | locate | يُعاد دون تغيير ودون تحقق. وسلسلة مزوَّرة من ثلاثة أجزاء تعيد 200 أيضًا. | src/controllers/client/exchange-token/index.js:35-41 |
| معتم (Opaque) | shukah | يُعاد دون تغيير ودون تحقق. | src/controllers/client/exchange-token/index.js:35-41 |
| JWT | shukah | يعمل verifyAccessToken، وأي استثناء يعطي 401. ثم يبحث عن صف token_bridge للعميل بقيمة source_system ضمن ['shukah','locate'] وis_active: true، دون ترتيب: فأي token نشط من Shukah مقبول (تقرّر في 2026-09-09). وعدم العثور على شيء يعطي 401. وإلا يُرجع الـ token الخام لذلك الصف. ولا يمكن أن تطابق 'locate' في القائمة أبدًا، لأن القيم الممكنة للـ enum هي shukah أو merchant. | src/controllers/client/exchange-token/index.js:44-67؛ src/models/token-bridge.js:5-23؛ docs/plans/2026-07-08-token-bridge-generic-and-image-urls.md:200-224 |
| معتم (Opaque) | locate | يجد الصف بحسب الـ token الخام، دون فلتر is_active. وعدم العثور على شيء يعطي 401. ثم يحمّل العميل دون فحص للحالة أو is_deleted. وعدم العثور عليه يعطي 401. وينشئ JWT بالقيمتين {client_id, phone}، ويُدرج صف token_bridge (source_system: 'merchant')، ويدفع الـ JWT إلى Shukah (بذل أقصى جهد)، ويجيب بـ 200. | src/controllers/client/exchange-token/index.js:70-89 |
- العميل غير النشط ما زال يحصل على JWT في الصف الأخير، لكن حارس
clientفي الـ routes الأخرى يرفضه (src/middlewares/client.js:27-33،src/shared/constant/user.js:14). - إذا لم يكن للصف المطابق token خام، يُحذف المفتاح ويصبح الـ body هو
{"status":200,"data":{}}(src/controllers/client/exchange-token/index.js:66؛ قراءة شيفرة). - لا يستطيع الـ endpoint التحقق من token: فهو لا يفحصه إلا عندما يضطر إلى البحث عن شيء.
- استدعاءان من نوع معتم-إلى-
locateللعميل نفسه في الثانية نفسها ينشئان JWTs متطابقين، لأن الـ token لا يحتوي جزءًا عشوائيًا وiatدقته ثانية واحدة. فيصطدم الإدراج الثاني بفهرسtoken_hashالفريد ويُرجع الـ catch-all قيمة401. قراءة شيفرة وفحص حتمية (determinism) دون اتصال، لم يُعاد إنتاجه: هل يحدث على dev: غير موثّق بعد.
مجموعة token_bridge والدفع إلى Shukah
- يُخزَّن JWT من Locate في
token_bridgeبالقيمةsource_system: 'merchant':client_id، وتجزئة SHA-256 للـ token بوصفهاtoken_hash، وis_active: true، وcreated_at، وtoken_rawفقط عندما لا يكونNODE_ENVهوproduction(src/services/merchant-token-bridge.js:6-16،src/shared/helpers/jwt.js:12). - تأتي tokens Shukah (
source_system: 'shukah') من ETL لجدول tokens في Shukah، معtoken_rawفقط عندما لا يكونNODE_ENVهوproduction(src/sync/mappers/token-bridge-mapper.js:7-25)، ومن الدفع الوارد (inbound push)POST {PREFIX_API_V1}/admin/clients/tokens، الذي يخزّن دائمًا الـ token الخام (src/routes/admin.js:27,47،src/controllers/admin/create-token/handler.js:12-29،src/services/shukah-token-bridge.js:8-33). - يقرأ الاستبدال
token_rawفي الاتجاهين (src/controllers/client/exchange-token/index.js:66,72). ولا تحذف خدمة المصادقة الصفوف أو تعطّلها أبدًا فيما يخص tokens Locate: فكل استدعاء معتم-إلى-locateيضيف صفًا واحدًا. ولا شيء يُبطل JWT من Locate. - الدفع هو
POST {SHUKAH_HOST}/maeda/client/add-tokenمع{access_token, client_id: <shukah_id>}والـ headersx-from-application: LOCATE_CLIENTوAuthorization: Bearer <SHUKAH_ADMIN_TOKEN>. وهو بذل أقصى جهد ويُنتظر إتمامه داخل الاستدعاء، بلا مهلة. ولا يفعل شيئًا عندما يكونshukah_idقيمة null، أو المزامنة معطّلة، أو الـ host أو token المسؤول غير مضبوط (src/services/shukah-sync.js:13-75،src/config/envConfig.js:19-33). وبهذه الطريقة تتعرف Shukah على JWTs من Locate (docs/plans/2026-07-16-shukah-token-outbound-push.md:10-28).
حيث تختلف الوثائق عن الشيفرة (القول للشيفرة)
- تسرد وثيقة الخدمة
401لحالة "JWT expired" (docs/api/client-auth-and-address-book.md:404). وJWTs من Locate لا انتهاء لها (src/shared/helpers/jwt.js:4-6)، لذا لا يمكن أن تحدث هذه الحالة. - يكتب المخطط التدفقي المسودة ووثيقة تصميم 2026-07-06 هذا الـ route بالشكل
POST /maeda/api/exchange-token. وفي commitc2d1c9f(2026-07-08) كانت الخدمة تركّبه هناك فعلًا، دون البادئة. ثم نقل commitd837492(2026-07-10) كل routes العميل تحتPREFIX_API_V1/clients، ويجيب host المصادقة الآن بـ404على/maeda/*. - تقول وثيقتان إن الـ token الخام "never populated in prod"
(
docs/guides/shukah-migration-schema-reference.md:113،docs/plans/2026-07-08-token-bridge-generic-and-image-urls.md:106-108). لكن كاتب الدفع الوارد يخزّنه دون شرط (src/services/shukah-token-bridge.js:16). وحدهما ETL وكاتب JWT من Locate مقيَّدان بغير production (src/sync/mappers/token-bridge-mapper.js:22،src/services/merchant-token-bridge.js:14).
غير موثّق بعد
- في وضع production، أي tokens Shukah المخزَّنة تحمل نسخة خام، وما الذي يُرجعه استدعاء JWT-إلى-
shukahعند غيابها. وفق الشيفرة هو{"status":200,"data":{}}. - هل الدفع الصادر إلى Shukah مفعّل على dev (
SHUKAH_SYNC_ENABLEDوSHUKAH_HOSTوSHUKAH_ADMIN_TOKEN). - هل يفشل فعلًا استبدالان للعميل نفسه في الثانية نفسها.
- هل يستدعي إصدار التطبيق الحالي (الأحدث من النسخة اللحظية 2026-02-26) هذا الـ endpoint.
Sources
locate-authentication-service@6757aa4:src/routes/client.js:22،src/controllers/client/exchange-token/index.js:10-96,src/index.js:42،src/shared/helpers/jwt.js:4-12,src/services/merchant-token-bridge.js:6-16،src/services/shukah-token-bridge.js:8-33,src/sync/mappers/token-bridge-mapper.js:7-25،src/models/token-bridge.js:5-23,src/routes/admin.js:27,47،src/controllers/admin/create-token/handler.js:12-29،src/services/shukah-sync.js:13-75,src/config/envConfig.js:19-33،src/middlewares/client.js:27-33,src/shared/constant/user.js:14،src/middlewares/error-handler.js:12-35,src/shared/constant/error-message.js:59- وثائق
locate-authentication-service@6757aa4:docs/api/client-auth-and-address-book.md§9 (الأسطر 372-408؛ آخر تغيير 2026-07-27)،docs/draft/flow-chart-shukah2locate.md:15-36,54,69-112(2026-07-08)،docs/plans/2026-07-06-shukah-auth-api-local.md:8-11,26،docs/plans/2026-07-08-token-bridge-generic-and-image-urls.md:106-108,196-224(2026-09-09)،docs/plans/2026-07-16-shukah-token-outbound-push.md:10-28،docs/guides/shukah-migration-schema-reference.md:113 - نسخة الجوال اللحظية
mobile/locate-client@e960c3a(الفرعdevelopment، 2026-02-26؛ شيفرة التطبيق نفسه، وليست من Locate): البحث في الشجرة كاملة عنexchange-tokenوapi/v1/clientsوauthentication.locateوauth_sourceلا يجد شيئًا؛src/entities/user/model/api/services/auth.service.ts:85-112(يُحذف الـ token المخزَّن عند401في:101-103)؛src/entities/settings/model/api/dto/settings.dto.ts:112-126(نوع client-settings لا يحتوي حقلauth_source) - التقاطات dev، 2026-10-01:
probe-locate-exchange-tokenوprobe-auth-host-maeda-prefix، واستدعاءات exchange-token المسجَّلة في ملخص الالتقاط المباشر (الشكل فقط، ولم تُخزَّن الـ tokens)