# جولة في URnetwork SDK

هذه هي الغوصة العميقة خلف [البدء](/docs/getting-started-sdk): كيف تُركَّب
كل طبقة ربط، وما تشحنه من افتراضيات، والحوافُّ الحادّة التي نعرفها. وكل شيء
يعمل مقابل منصة URnetwork المستضافة. أنت تجلب الاعتمادات، لا البنية
التحتية. ونموذج الكائنات (الجهاز، وAPI، ومتحكّمات العرض) في
[/docs/overview](/docs/overview) و[/docs/api](/docs/api).

نواة Go واحدة تنفّذ العميل كله: النقل، والعقود، ومسار الحزم. وكل مخرَج
(AAR، وxcframework، وwasm، ومكتبة c-shared) ربطٌ لتلك النواة نفسها، ولهذا
يتطابق السلوك عبر المنصات، ولهذا أيضاً تكبر المخرجات (فكل واحد يحمل وقت
تشغيل Go). والنواة تحمل قواعد المنتج أيضاً، لا الحزم وحدها. وSolana Pay
يسكن هنا (`CreatePaymentReference` و`BuildSolanaPaymentUrl`) لأنه لم يكن
كذلك يوماً: فقد سكّ تطبيقُ الويب مرجعَ الدفع بصيغة uuid ست عشرية حيث يشترط
Solana Pay مفتاحاً عاماً بصيغة base58 من 32 بايت، فكان يمكن لعميل أن يدفع
ولا يُطابَق أبداً بحسابه، وثبّت Android المبلغَ وعنوانَ التاجر برمجياً.
وإعادةُ تنفيذ قاعدة تتبعها التطبيقات أصلاً هي صنف الخلل الذي وُجدت هذه
الطبقة لمنعه.

## المنصات المحمولة

### DeviceLocal، وقسمة عملية التطبيق على Apple

`DeviceLocal` هو الشيء الحقيقي، أي محرك العميل العامل: فهو يملك النقل،
وحالةَ العقود (وهي محاسبة URnetwork للبايتات التي يجوز لحساب أن يحرّكها)،
ومسارَ الحزم. أما `DeviceRemote` فعميلٌ لكائن `DeviceLocal` يسكن في عملية
أخرى أو على المنصة، بسطح الواجهة نفسه: أي جهاز تحكم عن بُعد بالجهاز
الحقيقي. وعلى Android تستضيف عمليةٌ واحدة الواجهةَ و`VpnService` معاً،
فيمسك التطبيق بـ`DeviceLocal` مباشرة.

وتشغّل Apple معالجة حزم VPN في عملية NetworkExtension معزولة خاصة بها (وكل
شبكة VPN على iOS وmacOS مقسومة هكذا)، فينقسم SDK معها:

- امتداد `NEPacketTunnelProvider` يملك `SdkDeviceLocal` وينادي عليه
  `setRpcServer(serverPem, clientCertPem, hostPort)` لبدء المستمع.
- وعملية التطبيق تُنشئ `SdkDeviceRemote` وترتبط عبر اتصال **RPC للجهاز
  بـmTLS على الاسترجاع المحلي**: أي الصورة المرآتية
  `setRpcServer(clientPem, serverCertPem, hostPort)`، وكل جانب يسمّي هويته
  هو أولاً وشهادة النِّد ثانياً. وRPC الجهاز هو بروتوكول SDK الخاص:
  استدعاءات دوال إضافة إلى تدفقات اشتراك فوق net/rpc في Go، لا gRPC. وTLS
  المتبادل على الاسترجاع المحلي ليس مراسم شكلية. فـlocalhost يستطيع بلوغه
  كل عملية محلية وكل مستخدم، ولا يستطيع الارتباط إلا من يملك ملفات PEM
  المطابقة.
- **والتطبيق هو الذي يسكّ مادة المفاتيح، لا الامتداد.**
  فـ`SdkGenerateDeviceRpcKeyMaterial()` يعيد زوجَي مفاتيح جديدين موقَّعين
  ذاتياً للخادم وللعميل في كل جلسة VPN؛ ويضع التطبيق ملفَّي PEM وقيمة
  host:port في **إعداد الموفّر** في `NETunnelProviderProtocol`، وهكذا
  يحصل عليها الامتداد حين يبدأ النفق. ولا تدخل في الأمر أي حاوية مجموعة
  تطبيقات.
- وراقب `RemoteChangeListener`: ففي أول مرة تصير فيها `remoteConnected`
  صحيحة تكون تلك المادة قد أُثبتت، فاحفظها بوصفها آخر نسخة معروفة السلامة
  وأعد تطبيقها في الإطلاق التالي. وهذا هو ما يتيح للتطبيق أن يرتبط بامتداد
  يعمل أصلاً.

وإعادات البناء أمر روتيني: فالامتداد يعيد بناء جهازه كلما أُعيد تشغيل
النفق، إما لأن المستخدم بدّل VPN، أو لأن نظام التشغيل أعاد تشغيل الامتداد،
أو لأن iOS قتله لتجاوزه حدَّ ذاكرة الامتدادات. فوصّل إعادة الارتباط تلك
(السكّ، وبدء النفق، و`setRpcServer`، وإعادة تطبيق حالة واجهتك) من اليوم
الأول. وكل ما تستطيع فعله على `DeviceLocal` تستطيع فعله على `DeviceRemote`؛
فالبعيد يوكّل الاستدعاءات ويعيد تشغيل الاشتراكات عبر عمليات إعادة الاتصال.
ومستويان: **إعادة اتصال** بسيطة (الجهاز نفسه، وقد سقطت وصلة RPC ثم عادت)
تعيد إقامة الاشتراكات عنك؛ و**إعادة إنشاء** (نسخة جهاز جديدة تحتك) تعني
تشغيل ذلك التسلسل من جديد. وفخّ واحد: `DeviceRecreatedListener` يُطلق عند
تغيّر *جيل الجهاز*، ولا يختم جيلاً إلا مسارُ RPC المستضاف على المنصة،
فعلى الاسترجاع المحلي قُد إعادة الارتباط من دورة حياة نفقك أنت ومن
`RemoteChangeListener`.

### IoLoop ‏(Android وحده)

`Sdk.newIoLoop(device, detachedFd)` هي مضخّة الحزم:

- مرّر واصف ملف **مفصولاً وغير حاجب** ‏(`ParcelFileDescriptor.detachFd()`).
  وبعد ذلك الاستدعاء، **تملك Go واصف الملف وستغلقه**؛ فلا تغلّفه ولا تغلقه
  من Java مرة أخرى أبداً. فالمالكان يعنيان إغلاقاً مزدوجاً: وأرقام واصفات
  الملفات يُعاد تدويرها فوراً، فإغلاقٌ شارد من Java قد يدوس أي واصف لا صلة
  له أخذ الرقم بعده، وفسادٌ بعيد عن سببه.
- والحلقة تضخّ في الاتجاهين (tun←الجهاز والجهاز←tun) داخل Go، وهذا يتجنب
  عبور JNI ونسخ المخازن المؤقتة لكل حزمة. ولهذا تأخذ الواجهة واصف ملف خاماً
  بدل كشف دوال قراءة وكتابة.
- أغلق IoLoop (لا واصف الملف) لتتوقف؛ ويُطلق ردُّ نداء الإتمام حين تخرج
  الحلقة. وإن أُطلق من دون أن تطلب، لأن واصف tun بلغ EOF أو خطأً أو لأن
  الجهاز أُغلق، فعامله بوصفه «النفق ذهب»: أنهِ جلسة `VpnService`، ثم أعد
  الإقامة أو أظهر انقطاع الاتصال. ورد النداء يصل على خيط من خيوط SDK، وGo
  تغلق واصف الملف وهي خارجة؛ فلا تلمسه من المعالج أبداً.

ولا تستخدم Apple كائن IoLoop: فموفّر النفق يحرّك الحزم عبر `packetFlow`،
وهي الواجهة الوحيدة التي يعرضها NetworkExtension. وكل شبكة VPN على iOS تدفع
ثمن ذلك العبور، وقراءاتها الدفعية توزّع الكلفة.

### أوضاع المشاركة، ولماذا تهمّ مادة المفاتيح

`SetProvideMode` يتحكم في ما إذا كان الجهاز يعرض سعةً على الشبكة.
والافتراضي هو الإيقاف: فالجهاز المُنشأ حديثاً وضعُ مشاركته «none» ولا يعرض
شيئاً حتى تضبط وضعاً، فتضمين SDK لا يشارك أبداً نطاق مستخدميك بصمت. ووضعان
يفعلان شيئاً اليوم: **public**، و**network**، وهو يقصر المشاركة على الأجهزة
الأخرى في حساب URnetwork نفسه. وتحمل قائمة البروتوكول قيماً أكثر، لكن
المنصة تحسم كل نِدّ خارج شبكتك إلى public، فالخيار الحقيقي هو: إيقاف، أو
أجهزتي أنا، أو أي أحد.

والمشاركة هي حيث تهمّ **مثابرة مادة المفاتيح**: فكائن
`DeviceLocalKeyMaterial` الذي تمرّره عند الإنشاء *هو* هوية الجهاز بوصفه
مزوّداً، أي بذرةُ مفتاح عميل إضافة إلى شهادة TLS المشاركة ومفتاحها.
فاحفظها في التخزين الآمن للمنصة (Keystore وKeychain) ومرّر المادة نفسها كل
إطلاق، وإلا رأت الشبكةُ مزوّداً جديداً تماماً في كل مرة وفقدت تاريخ
الموثوقية الذي يفضّله الاختيار. والمادة العابرة (`null`) لا بأس بها للعملاء
المحض.

وإن عرضت المشاركة، فاذكر المقايضة: حركة الغرباء تخرج من عنوان IP للمستخدم،
وموضع المزوّد يرى عناوين IP للوجهات وأسماء SNI في TLS (كما ترى شركة
الإنترنت)، وإن كان لا يرى افتراضياً عنوان IP الحقيقي للمستخدم الأصلي.
وسلامة المزوّد مهندَسة في المحرك. فطبقة ip_security المفتوحة المصدر تفحص
منفذ خروج المزوّد نفسه وتُسقط الحركة من فئة DMCA ومن فئة CFAA قبل أن تغادر.
والحكم حزمةٌ مُسقطة، بلا تسجيل وجهة ولا نطاق ولا محتويات في أي مكان.
وتطابقُ توقيع BitTorrent يُصدر أيضاً علامة إساءة إلى المشغّل لا تحمل إلا
معرّف جهاز النِّد وقيمة نعم/لا، ولا يشحن المشغّل لها أي معالج اليوم؛ أما
إسقاطات المشفَّر المبهم فصامتة. ويشارك المزوّدون في بروتوكول UR؛ ويوثّق
[ur.xyz](https://ur.xyz) المكافآت.

### ملاحظات وقت التشغيل

- يُضبَط إيقاع جمع المهملات لكل نظام تشغيل تلقائياً: معامل إيقاع مقداره 10
  على iOS (فحدود ذاكرة الامتدادات وحشية) مقابل 50 على Android و100 في ما
  عداهما. ولستَ أنت من يضبطه.
- و`memoryTargetByteCount` مقبض مختلف: ميزانية بايتات *يقسمها* الجهاز
  ليحدّد أحجام مخازنه المؤقتة، بنسبة dns 2 : client 14 : provider 4، مع
  إسناد حصة المزوّد لزوج العميل ما دامت المشاركة متوقفة (والافتراضي 20 MB).
  وهي ليست سقفاً ولا إعداداً لجمع المهملات: فحدّ البصمة الليّن على مستوى
  العملية هو `SetMemoryLimit` المنفصل، والقتل القاطع هو حدّ الامتدادات في
  نظام التشغيل. اضبط الهدف تحته؛ وأبقِ الامتداد خالياً من المنطق تقريباً.
- وربطُ gomobile محروس في البناء، والآلية مهمة: فـgobind يحذف بصمت كل ما لا
  يستطيع ربطه، ولا يترك إلا تعليق `// skipped` في المصادر المولَّدة، ولذلك
  يبحث البناء في تلك المصادر و**يخفق عند أي حذف ليس على قائمة سماح صريحة**.
  والتخطيات المسموحة داخلية (حمولات gob في RPC، وسطح البروكسي/المنصة،
  وأشكال `uint64`/`[][]byte` التي لا يستطيع gomobile التعبير عنها): مقصودة،
  لا انحرافاً.

## JavaScript

### لماذا لا يوجد DeviceLocal في wasm

لا تستطيع صفحةُ متصفح أن تملك واجهة tun، فكائن `DeviceLocal` في wasm لن
يكون لديه ما يضخّه. ولذلك تشحن طبقة JS نصفَي جانب العميل وحدهما: سطح
الواجهة، ومتحكّمات العرض، و`DeviceRemote`، وهو *عميل* جهاز كامل يسكن جهازه
في مكان آخر. فالصفحة تمسك بلوحة التحكم (حالة الاتصال، والمواقع،
والإحصاءات، والحساب) بينما يستهلك مسارَ الحركة أيُّ شيء يستطيع استخدام
البروكسي المستضاف. ومن هنا المصنعان: `createProxyDevice` هو النموذج الرفيع،
يحلّ عناوين البروكسي المستضافة ويترك الحركة لأي شيء يستهلكها (إعداد بروكسي
في امتداد، أو وكيل fetch)؛ و`createPlatformDeviceRemote` هو النموذج
السميك، أي `DeviceRemote` حقيقي بسطح المستمعين ومتحكّمات العرض كاملاً فوق
جهاز تستضيفه المنصة نيابة عنك.

### المصادقة بـsignedProxyId

يفتح `createPlatformDeviceRemote` مقبس websocket لـRPC الجهاز على
`wss://<proxy>/device-rpc`. وذلك الـwebsocket **لا** يصادِق برمز JWT للحساب
(`byJwt`، وهو الرمز الحامل من تسجيل الدخول في URnetwork؛ و«by» بقيّة من
BringYour). بل يصادِق بـ`signedProxyId`، وهو `auth_token` الذي تعيده نقطة
النهاية `/network/auth-client` في المنصة إلى جانب عنوان البروكسي. وذلك فصلُ
نطاق مقصود: فمعرّف البروكسي الموقَّع يفوّض مقبس websocket واحداً بالضبط ولا
يحمل أي سلطة على الحساب، فلا يركب الرمزُ الواسع مقبسَ مستوى البيانات أبداً.
عامِل الزوج (`proxyUrl` و`signedProxyId`) اعتماداً واحداً. اطلبهما ومرّرهما
وجدّدهما معاً، وأعد طلب الزوج حين يرفض البروكسي المقبس بدل تخزين قطعة منه
مؤقتاً.

### نمط المستمعين ومتحكّمات العرض

كل اشتراك يتبع الشكل نفسه: `add*ChangeListener(fn)` يعيد **دالة إلغاء
اشتراك**. أمسكها ونادِها عند الهدم. وفي React، أعِدها:

```js
useEffect(() => {
  const unsub = device.addConnectChangeListener(setConnectEnabled);
  return unsub;
}, [device]);
```

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

### كيف يستخدمه ur.io

سطح `/app` على ur.io هو المستهلك المرجعي: فملف wasm **محمَّل بتكاسل** ولا
يُحمَّل إلا حين يبلغ المستخدم سطحَ اتصال، فلا تدفع صفحاتُ الهبوط كلفة الـ43
MB تقريباً أبداً، وهناك `DeviceRemote` واحد لكل تبويب. والصادرات عامة على
مستوى الصفحة، فـ`init` يقبع خلف مفرد.

ويحزم امتدادُ المتصفح ملف wasm نفسه لكنه لا يُنشئه اليوم: فهو يقود
الاتصالية عبر بروكسي المنصة إضافة إلى واجهات البروكسي في المتصفح نفسه،
ويستهلك المدخل **الآخر** لحزمة npm، وهو `@urnetwork/sdk-js/react`، أي خطاطيف
واجهة قائمة على `fetch` المجرَّد وأنواع مولَّدة لا تحتاج إلى wasm. وإن كنت
لا تريد إلا سطح REST، فاستورد ذلك ولا تنادِ `init` أبداً: فالمُحمِّل يحلّ
عنوان wasm وقت التشغيل بدل حلّه عبر `new URL(..., import.meta.url)` ساكن،
فلا تُصدره أدوات التجميع لمن لا يريده.

## cgo

### قسمة الخدمة الخفية

تطبيقا URnetwork المشحونان على Linux وWindows مبنيان على هذه الطبقة،
وكلاهما يستخدم نمط العمليتين نفسه، وهو الشكل الذي تنسخه لأي تكامل cgo يمسّ
جهاز tun. فـSDK يشحن المحرك وواجهة C؛ أما الخدمة الخفية فهي لك، ومعك ذانك
التطبيقان المُصدَران مرجعَين مفتوحَي المصدر:

- **عملية جذر/خدمة** (وحدة systemd، أو خدمة Windows) تشغّل `DeviceLocal`
  وتملك واجهة tun.
- و**عملية الواجهة بلا امتيازات** تشغّل `DeviceRemote` وترتبط عبر RPC
  الجهاز بـmTLS على الاسترجاع المحلي؛ وعنوان SDK الافتراضي هو
  `127.0.0.1:12025`.
- وملفات PEM الخاصة بـmTLS تُسلَّم إلى الواجهة عبر قناة يفوّضها نظام
  التشغيل نفسه: **مقبس unix مفحوص بـ`SO_PEERCRED`** على Linux (يُفحص عند
  القبول، قبل قراءة أي إطار)، و**أنبوب مُسمّى** على Windows. وتلك المصافحة،
  لا منفذ TCP، هي حدّ التفويض الحقيقي؛ وmTLS على الاسترجاع المحلي لا يفعل
  إلا إبعاد المستخدمين المحليين الآخرين عن المنفذ.

### دلالات غلاف RAII ‏(`urnetwork_sdk.hpp`)

- تُغلَّف المقابض في أنواع مالكة يستدعي مدمّرها `urnet_release`. لكن
  **التحرير ليس إغلاقاً ولا إيقافاً**: فإسقاط آخر غلاف لا يوقف جهازاً ولا
  يغلق اتصالاً. نادِ `*_close` / `*_stop` (أو `.close()` / `.stop()` في
  الغلاف) أولاً، ثم دع الغلاف يحرّر. فعدة مقابض قد تشير إلى كائن حيّ واحد،
  ولذلك يجب ألا يقتل خروجٌ من نطاق جلسةً حيّة بصمت أبداً.
- وتعود الاشتراكات بصيغة `urnet::Sub`؛ ومدمّرها يلغي الاشتراك.
- وتظهر الأخطاء بصيغة استثناءات `urnet::Error` تحمل نصّ `out_error`.

وقاعدة صارمة واحدة، وهي عكس ما يتوقعه الناس:

```cpp
sub = device.addConnectChangeListener([&](bool enabled) { /* ... */ });
sub.close();   // returns immediately; a callback may still be running
// do NOT free what that lambda captured here
```

قوائم المستمعين تعمل بالنسخ عند الكتابة، فإلغاء الاشتراك يزيل المدخل ويعود.
وذلك مقصود (فمن *المسموح* لرد نداء أن يزيل نفسه من داخل نفسه)، لكنه يعني أن
رد نداء قد يظل يعمل على خيط آخر بعد اختفاء `Sub`. وتدميرُ الحالة التي التقطها
ذلك المستمع هو الانهيار الحقيقي؛ فأبقِها حيّةً بعد الهدم بـ`shared_ptr` أو
بعَلَم يفحصه رد النداء.

ولانضباط التسريب، خذ لقطة من `urnet_live_handle_count()` قبل السيناريو،
ونفّذ إنشاءً/استخداماً/إغلاقاً/تحريراً، وتحقّق من أنه يعود إلى خط الأساس.
وهذا بالضبط ما يفعله `cgo/smoke`، وهو النمط الذي تنسخه.

### ما المربوط فعلاً

واجهة C ABI *مولَّدة* من سطح Go، والمولِّد يكتب `cgo/coverage_report.txt`:
كل رمز مُصدَّر، ولكل رمز لم يعبر، السبب (سياقات Go، ودواخل `net.Conn`،
ومعاملات الدوال، واستدعاءات ملكية المجمّعات، وأنواع gob في RPC). اقرأ ذلك
الملف، لا مصدر Go، للإجابة عن «هل هذا قابل للاستدعاء من C؟». والتصدير
الجديد من Go لا يبلغ الترويسة إلا حين يعمل المولِّد، ولذلك تُشحن الترويسة
بجانب المكتبة التي قُطعت معها. ثبّتهما معاً.

### توافق البروتوكول على السلك

بروتوكول RPC الجهاز على السلك **مثبَّت الإصدار (`DeviceRpcVersion`، وهو 1
حالياً) ومُنفَّذ** من قِبل المحلي في كل مزامنة. وهو عمداً *ليس* إصدار
الإصدارة: فالنصفان المستضافان يُنشران مستقلَّين، فربطهما سيرفض كل متصفح بعد
نشر الخادم. وعدم التطابق لا يرمي استثناءً. فالبعيد يبقى غير متزامن ويعيد
المحاولة، وهذا يبدو مطابقاً لـ«الخدمة الخفية لا تعمل». ميّز بينهما
بـ`GetRemoteConnected()` إضافة إلى `GetSyncError()`: فخطأ مزامنة فارغ يعني
أنه غير قابل للوصول بعد، بينما `"device rpc version mismatch: ..."` أو
`"device instance mismatch: ..."` رفضٌ لن تصلحه إعادة الاتصال أبداً. اشحن
الخدمة الخفية والواجهة من إصدارة SDK نفسها.

## أمور عابرة للطبقات

### إعدادات تحمل سياسة

هذه الأعلام تحمل قرارات منتج، لا ضبطاً، وافتراضياتها هي وضعية الخصوصية التي
يحصل عليها مستخدموك. وما يشحنه جهاز خارج الصندوق:

| الإعداد | الافتراضي | الأثر | الكلفة الرئيسة |
|---|---|---|---|
| `SetPerformanceProfile` | nil (تلقائي) | نافذتا الجودة والسرعة تعملان جنباً إلى جنب؛ والحركة تخرج عبر عدة مزوّدين في آن واحد (عادةً 3–8)، مع انتماء لكل موقع | تثبيت `WindowType` يضيّق إلى نافذة واحدة |
| `AllowDirect` | متوقف | يُبقي قفزة الإخفاء، فلا يرى أي مزوّد عنوان IP الحقيقي للمستخدم | تفعيله: معدل نقل أعلى، والمزوّد يرى عنوان IP الحقيقي للمستخدم |
| `PostQuantumEncryption` | مفعّل | يختم جلسة العميل↔المزوّد، فيُرحّل المشغّل بايتات لا يستطيع قراءتها | يُتخطّى المزوّد الذي يتعذّر ختم جلسة معه، ولا يُستخدم بلا ختم |
| `SetRouteLocal` | مسموح | ترتدّ الحركة إلى المسار المحلي حين يسقط النفق | المنع هو مفتاح الإيقاف: تتوقف الحركة بدلاً من ذلك |
| `SetProvideMode` | none | الجهاز لا يعرض أي سعة على الشبكة | المشاركة العامة أو الشبكية تُخرج حركة الآخرين من عنوان IP للمستخدم |

تحفّظات ترافق الجدول:

- `AllowDirect` هو إعداد السرعة، والعنوان الذي يكشفه هو بالضبط القفزة التي
  يزيلها. وتعرضه التطبيقات مقلوباً باسم «الإخفاء القوي للهوية»، مفعّلاً
  افتراضياً، وهو مُجبَر على الإيقاف في ملفات الأجهزة المستضافة مهما ضبط
  المستدعي. فلا تقدّمه لمستخدميك أبداً بوصفه سرعة مجانية؛ قدّمه بوصفه
  المقايضة التي هو عليها.
- و`PostQuantumEncryption` هو جلسة العميل↔المزوّد من طرف إلى طرف التي تشحنها
  التطبيقات باسم «تشفير ما بعد الكم»، مفعّلة افتراضياً منذ الإطلاق وجاهزة
  من جانب المزوّد: فكل إصدارات المزوّد الحالية تفعّل الجانب المجيب. وما دام
  مفعّلاً، يعمل العميل بالإغلاق عند الفشل: فهو لا يحمل بيانات التطبيقات
  في العلن، ويتخطّى المزوّد الذي يتعذّر ختم جلسة معه بدل أن يستخدمه من دون
  ختم. فالكلفة في التوفّر، لا في السرّية. أوقف العَلَم، وتعود الحركة قادرةً
  على سلوك المسار القياسي.
- و`SetRouteLocal` هو العنصر الأولي لمفتاح الإيقاف («السماح بالحركة
  المحلية»). أوقفه، وأعطِ مستخدميك المفتاح الموجود في كل تطبيق URnetwork،
  فتتوقف الحركة بدل أن ترتد حين يسقط النفق.

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

والأوضاع الثلاثة التي يختارها هذان العَلَمان، مذكورةً من أجل المستخدمين
الذين توجّههم:

| الوضع | ما يراه المشغّل | ما يراه المزوّد | كيف تحصل عليه |
|---|---|---|---|
| **مُرحَّل مختوم** | اتصال الحساب/المصدر، والاقتران بالمزوّد، ونص مشفّر مع التوقيت/الحجم | حركة الوجهات ومعرّف جهاز/عقد، **وليس** عنوان IP الحقيقي للمستخدم | الافتراضي: `PostQuantumEncryption` مفعّل، و`AllowDirect` متوقف |
| **مُرحَّل قياسي** | اتصال الحساب/المصدر، والاقتران بالمزوّد، والوجهات الداخلية وبايتات الحزم | حركة الوجهات ومعرّف جهاز/عقد، **وليس** عنوان IP الحقيقي للمستخدم | بإيقاف `PostQuantumEncryption` فقط |
| **مباشر** | دور أقل في الترحيل | **عنوان IP الحقيقي للمستخدم** وحركة الوجهات | يُفعَّل يدوياً: `AllowDirect` مفعّل (مُجبَر على الإيقاف في الملفات المستضافة) |

والافتراضي يشحن الصفَّ الأول أصلاً بلا ضبط شيء. والثاني يقتضي إيقاف
`PostQuantumEncryption`: فما دام مفعّلاً، يتخطّى العميل المزوّد الذي يتعذّر
ختم جلسة معه بدل أن يهبط إلى ذلك الصف. ولا توجد واجهة لكل اتصال تُبلغ
عن الصف الذي انتهت إليه جلسةٌ بعينها. ويُخضِع
[نموذج التهديد](/docs/threat-model) هذه الصفوف لخصوم مسمّين، ويصرّح بموضع
إخفاق كل صف.

### قواعد الخيوط

- الكائنات غير العرضية (`DeviceLocal` و`DeviceRemote` و`Api` وفضاءات
  الشبكة) آمنة للتزامن. نادِها من أي خيط.
- **ومتحكّمات العرض أحادية الخيط** ما لم يوثّق أحدها غير ذلك. قُد كلاً منها
  من خيط واحد (عادةً خيط واجهتك). وJavaScript في المتصفح يفي بذلك مجاناً؛
  أما على Android وApple وcgo فهو يعضّ، لأن ردود النداء تصل على خيوط تديرها
  Go.
- وردود النداء تُطلق على خيوط عشوائية يديرها SDK. فانقلها إلى خيط واجهتك
  قبل أن تلمس الواجهة، ولا تحجب داخل واحد منها أبداً: فـ`DeviceRemote`
  يُسلسل ردود ندائه عبر قناة واحدة ذات مخزن مؤقت، فالمستمع البطيء يضغط
  عكسياً على كل اشتراك عليها.

### أحجام المخرجات

خطّط ميزانيات التنزيل والتعبئة حول هذه:

| المخرَج | الحجم (مضغوطاً حيث يُذكر) |
| --- | --- |
| Android AAR | نحو 37 MB |
| Apple xcframework ‏(zip) | نحو 116 MB |
| JS wasm | نحو 43.5 MB |
| Linux c-shared ‏(zip) | نحو 25 MB |
| Windows c-shared ‏(zip) | نحو 21 MB |

### استقرار واجهة API

الإصدارات مؤرَّخة بالتاريخ (`vYYYY.M.D-<code>`)؛ ولا يوجد عقد SemVer بعد،
وحزمة npm في مرحلة بيتا وتُنشر ليلياً. عامِل الأسطح الثلاثة كلها بوصفها
متحركة حتى إصدار 1.0: ثبّت إصدارات بعينها، واقرأ ملاحظات الإصدار عند كل
قفزة، وأبقِ المخرجات المتزاوجة (الخدمة الخفية والواجهة، والامتداد
والتطبيق) على إصدارة واحدة.
