البداية

نظام النطاقات (Scopes)

كل قطعة بيانات في أوكتا محمية بـ scope بصيغة:

<feature>.<resource>.<action>

أمثلة:

education.students.read       → قراءة الطلاب
education.students.write      → إضافة/تعديل الطلاب (لا حذف)
education.face_embeddings.read → قوالب التعرّف على الوجه (خطِر، قراءة فقط)
employees.directory.read      → قراءة الموظفين
reports.builder.read          → قراءة التقارير
reports.builder.write         → اقتراح قوالب (ترويسات) — لا تعريف تقارير

المبادئ الأساسية

  1. lowercase + dot-separated بدون wildcards.
  2. شريكاً يحصل فقط على read و write. الحذف لا يُمنح أبداً للشركاء.
  3. الكتالوج هو المصدر الوحيد. لا تخترع scopes جديدة.
  4. أوكتا يضيف نطاقات جديدة عبر تحديثات المنصة.

كتالوج النطاقات

اطلب الكتالوج الكامل من:

GET /api/partners/permissions/catalog

يعيد جميع الـ scopes المتاحة مجمَّعة حسب feature → resource. هذا هو ما تختار منه عند إنشاء تطبيقك.

مرجع النطاقات التفصيلي

كل نطاق يفتح وصولاً لمجموعة محدّدة من نقاط نهاية الـPartner-app runtime API، وكل نقطة نهاية تتحقق من الـscope تلقائياً عبر middleware app.scope:<feature>.<resource>.<action>. الـURL الأساس: /api/apps.

ملاحظة عامة على المعرّفات: كل المعرّفات في الاستجابات هي ULIDs (سلسلة مكوّنة من 26 حرفاً، Crockford base32). لا تكسِتها إلى integer ولا تقارنها بأرقام — boilerplate scanner يرفض الـPR لو حاول (ulid-cast-to-int و ulid-numeric-comparison). خزّنها كـchar(26) في جداولك، ووسِم العمود في الميغريشن بـcore_reference:<الجدول> وإلا لن يحميه الحارس — راجع المعرّفات والـcore_reference.


education.students — الطلاب

النطاق الصلاحيات
education.students.read عرض قائمة الطلاب، عرض طالب واحد
education.students.write + إضافة طالب، تعديل طالب (الحذف غير ممنوح)

نقاط النهاية:

Method Path Scope الوصف
GET /api/apps/education/students read قائمة (مع pagination + فلترة بـgrade_id/section_id)
GET /api/apps/education/students/{student} read طالب واحد بـULID
POST /api/apps/education/students write إضافة طالب
PATCH /api/apps/education/students/{student} write تعديل طالب

مثال Embedded (PHP):

use App\Services\PartnerApi\Education\Students\ListStudents;
use App\Services\PartnerApi\Education\Students\GetStudent;

$page = app(ListStudents::class)(page: 1, perPage: 50);
foreach ($page->data as $student) {
    // $student هو StudentDto
    echo $student->id;          // ULID مثل "01H..."
    echo $student->name;
    echo $student->gradeId;     // ULID للصف
    echo $student->sectionId;   // ULID للشعبة
}

$one = app(GetStudent::class)('01HXXXXXXXXXXXXXXXXXXXXXXX');

مثال External (HTTP):

curl -H "Authorization: Bearer $INSTALLATION_TOKEN" \
     -H "Accept: application/json" \
     "https://app.okta.platform/api/apps/education/students?page=1&perPage=50"

حالات استخدام شائعة: قوائم الفصول، إنشاء جداول الاختبارات، إصدار شهادات، استيراد طلاب جدد من نظام الشريك.


education.face_embeddings — قوالب التعرّف على الوجه

نطاق خطِر (is_dangerous). شاشة التثبيت تعرضه بألوان الخطر، وأدمن الجهة سيتوقّف عنده ويقرأه. اطلبه فقط إن كان تطبيقك يطابق الوجوه فعلاً، واشرح السبب في reason بلغة يفهمها مدير مدرسة.

النطاق الصلاحيات
education.face_embeddings.read قراءة قوالب الوجه (متجهات رقمية) للطلاب والموظفين المسجَّلين

لا يوجد .write ولن يوجد. التسجيل يتمّ في المنصة بحضور الشخص أمام مكتب الاستقبال؛ تطبيقٌ يستطيع كتابة قالب يستطيع أن يجعل أي كاميرا تتعرّف على أي شخص بأنه شخص آخر.

لماذا هو منفصل عن education.student_photos.read: يبدوان الموافقة نفسها وليسا كذلك. الصورة ما تطبعه المدرسة على البطاقة وتعرضه عند البوابة؛ القالب مفتاحٌ يطابق الطفل أمام أي كاميرا في أي مكان — وخلافاً لكلمة المرور لا يمكن إصدار بديل عنه بعد التسريب. جهةٌ وافقت على عرض الوجوه لم تقل شيئاً عن تسليم الرياضيات التي تتعرّف عليها.

نقاط النهاية:

Method Path Scope الوصف
GET /api/apps/education/face-embeddings read قوالب الجهة لنموذج واحد (مُرقَّم الصفحات)

المعاملات: model (مطلوب)، person_type (student/employee، اختياري)، page، per_page (افتراضي 50، بحد 100).

model مطلوب بلا افتراضي عمداً: AdaFace1 وArcFace_50r1 ينتجان متجهات غير قابلة للمقارنة ببعضها. لو اختارت المنصة واحداً نيابةً عنك لتسلّمت قوالب يفشل محرّكك في مطابقتها بصمت — بلا خطأ في أي مكان.

شكل الناتج:

{
  "model": "AdaFace1",
  "count": 2,
  "total": 431,
  "people": [
    {
      "person_type": "student",
      "person_id": "01HXXXXXXXXXXXXXXXXXXXXXXX",
      "model": "AdaFace1",
      "dimensions": 512,
      "captures_count": 3,
      "registered_at": "2026-08-25T09:12:44+00:00",
      "embedding": [0.0134, -0.0891, "…"]
    }
  ]
}

count ≠ total: الأول ما تحمله هذه الصفحة بعد إسقاط الطلاب المُخلى سبيلهم والصفوف غير القابلة لفكّ التشفير، والثاني ما تملكه الجهة. رقّم الصفحات بـtotal — من يشتقّه من count يتوقّف مبكراً عند أول صفحة تصادف طالباً مُخلى سبيله.

لا أسماء في الناتج. تحصل على متجهات مفهرسة بـ ULID فقط. إن أردت معرفة صاحب الوجه الذي طابقته، اطلب education.students.read أيضاً وحلّ الـ ULID هناك — منحة واحدة لا تُسلّمك اثنتين.

مثال Embedded (PHP):

use App\Services\PartnerApi\Education\FaceEmbeddings\ListFaceEmbeddings;

$page = app(ListFaceEmbeddings::class)(model: 'AdaFace1', page: 1, perPage: 50);

foreach ($page['people'] as $person) {
    $person['person_id'];   // ULID
    $person['embedding'];   // list<float>
}

مثال من تطبيق مصغّر (Dart) — لا يحتاج رمزاً خاصاً في okta_host:

final res = await Okta.get('/api/apps/education/face-embeddings?model=AdaFace1');
if (res.status == 200) {
  final people = res.body['people'];
  // people[i]['embedding'] متجه من الأعداد
}

حالات استخدام شائعة: بوّابة تتعرّف على الطلاب عند الدخول والخروج، تحضير آليّ في الفصل، مطابقة هوية قبل تسليم طالب لوليّ أمره.

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


education.subjects — المواد الدراسية

النطاق الصلاحيات
education.subjects.read عرض قائمة المواد، عرض مادة
education.subjects.write + إضافة مادة، تعديل مادة

نقاط النهاية:

Method Path Scope
GET /api/apps/education/subjects read
GET /api/apps/education/subjects/{subject} read
POST /api/apps/education/subjects write (+ idempotent)
PATCH /api/apps/education/subjects/{subject} write

مثال:

use App\Services\PartnerApi\Education\Subjects\ListSubjects;

$page = app(ListSubjects::class)(page: 1, perPage: 100, gradeId: $gradeUlid);
// كل subject له ulid في .id و grade_id في .gradeId (كلاهما ULID)

حالات استخدام شائعة: ربط المواد بالمعلمين، تنظيم الجداول الزمنية، إعداد الدرجات.


education.curriculum — توزيع المنهج

النطاق الصلاحيات
education.curriculum.read قراءة توزيع المنهج المنشور لمادة، وتحديد درس حصة بعينها

للقراءة فقط. التطبيق الذي يشغّل الجدول اليومي يحتاج أن يسمّي درس الحصة، ولا شأن له بإعادة كتابة توزيع المدرسة — فلا يوجد .write هنا، لأن نطاقاً تمنحه المدرسة ولا يفعل شيئاً أسوأ من غيابه: شاشة الموافقة توحي بأنه يفعل.

نقاط النهاية:

Method Path Scope
GET /api/apps/education/subjects/{subject}/curriculum read
GET /api/apps/education/subjects/{subject}/curriculum/session read

القاعدة التي يقوم عليها العقد: أنت تملك الجدول، والمنصّة تملك التوزيع. الحصص تعيش في تطبيقك لا في okta-web، فالمنصّة لا تعرف أن الحصة التي بدأت الآن هي حصة العربي الثانية في هذا الأسبوع. تخمينها من التاريخ وحده يعني اختراع جدول وتسمية الدرس الخطأ بثقة. لذلك أنت تمرّر الرقم الذي تحسبه أصلاً في session_index، والمنصّة تمشي على أسابيع التوزيع حتى الدرس الذي يقع فيه.

GET /api/apps/education/subjects/{subject}/curriculum/session
      ?date=2026-08-25          # اختياري، الافتراضي اليوم
      &session_index=3          # ترتيب حصة هذه المادة داخل الأسبوع (1-based)
{
  "subject_id": "01K3…",
  "date": "2026-08-25",
  "session_index": 3,
  "resolved": true,
  "reason": null,
  "week": { "number": 1, "position": 1,
            "starts_on": "2026-08-23", "ends_on": "2026-08-27",
            "kind": "study", "label": null, "lessons": [ … ] },
  "lesson": {
    "position": 2, "unit": "أسرتي", "title": "الدرس الأول: حرف (م)",
    "kind": "lesson", "sessions": 3, "content": "الأهداف… الصفحات 22-27",
    "session_of_lesson": 2, "sessions_in_lesson": 3
  }
}

session_of_lesson تقول إنها الحصة الثانية من ثلاث لهذا الدرس — والمعلّم في الحصة الثالثة من درس يحتاج أن يعرف أنها الثالثة.

مفتاحان للأسبوع، والثابت منهما position. number هو الرقم الذي تقرأه المدرسة على الورقة، وهو عدّ أسابيع الدراسة وحدها: صندوق الإجازة يرسل number: null — كما تطبع وزارة التعليم تماماً: «الأسبوع ١٣»، «إجازة الخريف»، «الأسبوع ١٤» — وتعليمُ أسبوعٍ كإجازة يُنقص رقم كل أسبوع بعده واحداً. أما position فهو موضع الصندوق في الفصل بما فيه الإجازات، ولا يتحرّك. إن خزّنت إشارة إلى أسبوع فخزّن position، واعرض number ولا تبنِ عليه مفتاحاً.

لا تعامل «لا يوجد درس» كخطأ. ثلاث حالات ترجع 200 مع resolved: false وreason قابلة للفرع عليها برمجياً، ولكل واحدة بطاقة مختلفة في واجهتك:

reason معناه ما تعرضه
week_holiday / week_exam الأسبوع إجازة أو اختبارات اسم الإجازة من week.label
week_not_planned المدرسة لم تُدخل دروس هذا الأسبوع «لم يُخطَّط بعد»
beyond_planned_sessions الحصص تجاوزت ما خُطِّط للأسبوع «الصف متقدّم على التوزيع»
session_index_missing لم تمرّر رقم الحصة دروس الأسبوع بترتيبها من week.lessons
no_week_for_date التاريخ خارج أسابيع التوزيع لا شيء

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

الكتاب: textbook_url فقط يعبر الواجهة. الملف المرفوع يبقى خلف دخول المدرسة — تقديمه عبر توكن صادر لتطبيقٍ لا لشخص قرار آخر لم يُتَّخذ.

حالات استخدام شائعة: عرض درس الحصة داخل الجدول اليومي، تحضير المعلّم، إشعار وليّ الأمر بما دُرِّس اليوم.


education.grades — الصفوف

النطاق الصلاحيات
education.grades.read عرض القائمة، عرض صف واحد
education.grades.write + إضافة صف، تعديل صف

نقاط النهاية:

Method Path Scope
GET /api/apps/education/grades read
GET /api/apps/education/grades/{grade} read
POST /api/apps/education/grades write (+ idempotent)
PATCH /api/apps/education/grades/{grade} write

مثال:

use App\Services\PartnerApi\Education\Grades\ListGrades;

$page = app(ListGrades::class)(page: 1, perPage: 50, onlyActive: true);
foreach ($page->data as $grade) {
    echo $grade->id;        // ULID
    echo $grade->nameAr;
    echo $grade->order;
}

حالات استخدام شائعة: dropdowns لاختيار الصف، تصفية الطلاب، تجميع التقارير حسب المرحلة.


education.sections — الشُعب

النطاق الصلاحيات
education.sections.read عرض القائمة، عرض شعبة
education.sections.write + إضافة شعبة، تعديل شعبة

نقاط النهاية:

Method Path Scope
GET /api/apps/education/sections read
GET /api/apps/education/sections/{section} read
POST /api/apps/education/sections write (+ idempotent)
PATCH /api/apps/education/sections/{section} write

مثال:

use App\Services\PartnerApi\Education\Sections\ListSections;

$page = app(ListSections::class)(page: 1, perPage: 50, gradeId: $gradeUlid);

حالات استخدام شائعة: تنظيم الجلوس في الاختبارات، توزيع المعلمين، طباعة كشوف الحضور.


education.academic_years — السنوات الدراسية

النطاق الصلاحيات
education.academic_years.read قراءة السنوات
education.academic_years.write + إضافة وتعديل

حالة المسارات: مُسجَّل في الكتالوج للاستخدام المستقبلي. نقاط نهاية الـrun-time لم تُكشف بعد على /api/apps/education/academic-years — ستُضاف في إصدار قادم. اطلب هذا الـscope إذا كنت تخطط لاستخدامه قريباً.


education.terms — الفصول الدراسية

النطاق الصلاحيات
education.terms.read قراءة الفصول
education.terms.write + إضافة وتعديل

حالة المسارات: مماثل للسنوات الدراسية أعلاه — مُسجَّل في الكتالوج، نقاط النهاية ستُكشف لاحقاً.


employees.directory — الموظفون

النطاق الصلاحيات
employees.directory.read عرض القائمة، عرض موظف واحد
employees.directory.write + إضافة وتعديل

نقاط النهاية:

Method Path Scope
GET /api/apps/employees/directory read
GET /api/apps/employees/directory/{employee} read
POST /api/apps/employees/directory write (+ idempotent)
PATCH /api/apps/employees/directory/{employee} write

مثال:

use App\Services\PartnerApi\Employees\Directory\ListEmployees;

$page = app(ListEmployees::class)(
    page: 1,
    perPage: 100,
    type: 'teacher',     // أو 'administrator', null للجميع
    onlyActive: true,
);

حالات استخدام شائعة: تعيين المعلمين على المواد، إصدار جداول المراقبين، تطبيقات HR التعليمية.


reports.builder — التقارير

النطاق الصلاحيات
reports.builder.read عرض كتالوج التقارير وتشغيل تقرير، وسرد قوالب المدرسة والطباعة عليها
reports.builder.write اقتراح قالب (ترويسة) من تأليف تطبيقك، وتعديله وحذفه — ما ألّفه هو وحده

التقارير نفسها بلا write: الكتالوج تُنسّقه أوكتا ولا يستطيع شريك تعريف تقرير جديد — أنت تشغّل الموجود وتأخذ مخرجاته. النطاق الثاني يخصّ القوالب وحدها.

وwrite لا يشتري الطباعة. قالبٌ ألّفه تطبيقك يصل مسودةً ولا يُطبع حتى تعتمده المدرسة، وتطبيقك لا يمسّ إلا ما ألّفه هو. التفصيل في أن يقترح تطبيقك ترويسة.

النطاق يفتح سطحين مختلفين: كتالوج التقارير (بيانات تُعيدها أوكتا)، وقوالب المدرسة (ترويسة تطبع عليها أنت محتواك). الثاني موصوف كاملاً في الطباعة على ترويسة المدرسة.

نقاط النهاية:

Method Path Scope
GET /api/apps/reports/builder read
POST /api/apps/reports/builder/{key}/run read (+ idempotent)
GET /api/apps/reports/templates read
POST /api/apps/reports/templates/{id}/render read
POST /api/apps/reports/templates write (+ idempotent)
PATCH /api/apps/reports/templates/{id} write
DELETE /api/apps/reports/templates/{id} write

مثال:

use App\Services\PartnerApi\Reports\Builder\ListReports;
use App\Services\PartnerApi\Reports\Builder\RunReport;

$catalog = app(ListReports::class)();
$result = app(RunReport::class)('students.attendance.monthly', [
    'gradeId' => $gradeUlid,
    'month'   => '2026-04',
]);

حالات استخدام شائعة: لوحات إحصائية، تصدير شهري للأهالي، تقارير المعلمين الدورية.


ساعة الجهة — /api/apps/tenant/clock

بلا نطاق. كل تطبيق مثبَّت يصل إليها بتوكن التثبيت وحده، ولا شيء يُطلب من الجهة.

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

Method Path Scope الوصف
GET /api/apps/tenant/clock — المنطقة الزمنية التي تقرأ بها الجهة يومها

شكل الناتج:

{
  "timezone": "Asia/Riyadh",
  "offset": "+03:00",
  "now": "2026-09-02T14:31:07+03:00"
}

خزّن timezone، ولا تخزّن offset أبداً

offset وnow يصفان هذه اللحظة لا هذا المكان. الاسم وحده يحمل القواعد، فيبقى صحيحاً حين تتغيّر القواعد تحته.

وهذا ليس افتراضاً نظرياً — إحدى الدول المدعومة تعمل بالتوقيت الصيفي:

المنطقة يناير يوليو
Asia/Riyadh +03:00 +03:00
Asia/Dubai +04:00 +04:00
Africa/Cairo +02:00 +03:00

فتطبيق خزّن +02:00 لمدرسة قاهريّة في يناير يصير ساعةً كاملة خطأً في أبريل — على كل شاشة، بلا خطأ في أي سجلّ، والتاريخ صحيح والصيغة صحيحة والساعة وحدها مغلوطة. ولو خزّن "Africa/Cairo" لتحرّك معها وحده.

استعمل offset للعرض بجانب وقت، أو لتسليمه لشيء لا يقبل اسم منطقة. لا لشيء آخر.

التخزين يبقى UTC

كل طابع زمني تُسلّمه المنصّة لك، وكل طابع تُعيده أنت، هو UTC — وهذا لا يتغيّر. هذه النقطة تجيب سؤالاً آخر: ماذا يُعرَض. حوِّل عند العرض، لا عند الكتابة؛ قيمةٌ حُوِّلت وقت الدخول قيمةٌ لا يستطيع أحد مقارنتها بين مدرستين بعد ذلك، والضرر لا يظهر حتى تجتمع مدرستان في تقرير واحد.

من أين تأتي القيمة

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

مثال Embedded (PHP):

use App\Services\PartnerApi\Tenants\GetTenantClock;

$clock = app(GetTenantClock::class)();

$clock->timezone;   // «Asia/Riyadh» — هذا ما يُخزَّن
$clock->offset;     // «+03:00» — للعرض فقط
$clock->now;        // ISO8601 على ساعة المدرسة

// وموقتٌ مخزَّن UTC مقروءاً على ساعتها:
app(GetTenantClock::class)->at($row->created_at)?->format('Y-m-d H:i');

مثال من تطبيق مصغّر (Dart) — لا يحتاج رمزاً جديداً في okta_host ولا رفع minContract:

final res = await Okta.get('/api/apps/tenant/clock');
if (res.status == 200) {
  final zone = res.body['timezone'];   // خزّن هذا
}

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

لا تسألها مرّة وتخزّن now. «كم الساعة» سؤالٌ يُسأل حين يُحتاج جوابه؛ والاسم وحده هو ما يستحقّ أن يُحفَظ.


المعرّفات والـcore_reference

كل response يخرج من المنصة يستبدل numeric IDs بـULIDs. الـcore_reference هو عمود في جدولك أنت يحمل واحداً من هذه المعرّفات: سلسلة 26 حرفاً تشير إلى صف في جدول منصّة على okta-web (students, subjects, grades, sections, tenants, tenant_employees, users).

لماذا لا يوجد قيد FK حقيقي: الصف المُشار إليه يعيش في قاعدة بيانات أخرى على okta-web، وPostgres لا يعرف قيود مفاتيح أجنبية عابرة لقواعد البيانات. فما تخزّنه هو char(26)/varchar(26) عادي بلا أي قيد — ومن هنا تأتي المشكلة: char(26) عرض قد تستخدمه لأي شيء آخر (كود قسيمة، رقم فاتورة خارجية، بادئة hash). العرض وحده ليس دليلاً، ولو عاملت المنصة كل char(26) كمرجع منصّة لرفضت كتاباتك المشروعة.

ولهذا الحارس لا يخمّن: عليك أن توسم العمود صراحةً في الميغريشن.

الوسم — صيغتان

الصيغة (أ) — تعليق على العمود في كتالوج Postgres (المفضّلة)، لأنها تبقى في قاعدة البيانات الحيّة فتقرأها okta-web وأدوات الفحص لاحقاً:

CREATE TABLE "students" (
    "id"           BIGSERIAL PRIMARY KEY,
    "owner_ulid"   CHAR(26) NOT NULL,
    "teacher_ulid" CHAR(26)
);

COMMENT ON COLUMN "students"."owner_ulid"   IS 'core_reference:tenants';
COMMENT ON COLUMN "students"."teacher_ulid" IS 'core_reference:users';

في ميغريشن Laravel هذه هي ->comment():

$table->string('owner_ulid', 26)->index()->comment('core_reference:tenants');

الصيغة (ب) — تعليق SQL على سطر العمود نفسه، داخل CREATE TABLE أو على ALTER TABLE … ADD COLUMN من سطر واحد:

CREATE TABLE "students" (
    "id"         BIGSERIAL PRIMARY KEY,
    "owner_ulid" CHAR(26) NOT NULL, -- core_reference: tenants
);

ALTER TABLE "students" ADD COLUMN "teacher_ulid" CHAR(26); -- core_reference: users

الفاصل بين الكلمة والهدف مرن: core_reference: tenants و core_reference=tenants و core_reference(tenants) كلها مقبولة. و core_reference بلا هدف تُقبل أيضاً كوسم (قاعدة صيغة الـULID تنطبق مهما كان الجدول الهدف) — لكن سمِّ الهدف دائماً، فهو ما يظهر في الـ manifest.

ماذا يحدث بلا وسم

لا شيء يُرفَض — وهذه هي النقطة الخطرة. العمود ببساطة لا يراه الحارس، فلا يحميه:

  • App\Services\Partners\Sandbox\ValidateCoreReferenceWrites لا يعرف أن العمود مرجع منصّة، فيمرّر أي قيمة تكتبها فيه بلا فحص صيغة ULID ولا فحص أن الجدول الهدف مسموح.
  • كتلة core_references في الـ manifest تخرج فارغة لأعمدتك غير الموسومة.

الأعمدة بعرض 26 بلا وسم تُصنَّف «مرشّحة» (candidates) وتُعرَض لك في اللوحة — تنبيهاً لا رفضاً. فالفرق بين «هذا الجدول لا يحوي مراجع منصّة» و«قد يحويها ولا نراها» فرق يجب أن تحسمه أنت بالوسم.

ميغريشنات بمسار ملف داخل مستودعك (.php) لا يمكن قراءة SQL منها في وقت الفحص، فتُحسب ضمن unreadable وتُعرَض على أنها غير مفحوصة — لا تفترض تغطية كاملة معها.

ممنوع (بوسم أو بدونه):

  • (int) $row->student_id_ulid — كسته إلى integer.
  • where('student_id_ulid', 12345) — مقارنته بـliteral رقمي.

كلاهما يُكشَف من boilerplate scanner ويفشل CI. خزّنه ومرّره دائماً كـstring.

تحديث الكتالوج محلياً (okta-partners)

الكتالوج يُحدَّث تلقائياً من okta-web عبر:

  1. Cron كل ساعة (افتراضي).
  2. Webhook فوري على partner_scopes.catalog.changed.
  3. يدوي: php artisan partners:sync-scope-catalog --force.

تحديث محلي عبر hash drift detection — لو الـhash لم يتغيّر يتجاوز المزامنة بالكامل.

عرض الدليل كاملاً في صفحة واحدة · ↧ Markdown خام