Enterprise الإصدار
Webhook — مرجع متعمّق
لمحة سريعة
قسم بعنوان «لمحة سريعة»يوفّر مجال الأسماء NextPDF\Enterprise\Webhook تسليم webhook محدّدًا بنطاق المستأجر لأحداث المهام. يتكوّن السطح العام من ستة رموز: WebhookManager وWebhookRegistration وWebhookPayload وWebhookDelivery وWebhookRetryPolicy وDeadLetterEntry. يسجّل المدير نقاط النهاية لكل مستأجر ويُرسِل أحداث المهام إلى التسجيلات المشتركة. يرسل محرك التسليم بطلب POST حمولة JSON موقَّعة بخوارزمية HMAC-SHA256، ويتحقق من كل وجهة عبر بوّابة خروج SSRF في Core، ويعيد المحاولة بتراجع أُسّي، ويسجّل الإخفاقات الدائمة في طابور رسائل ميتة في الذاكرة. اعتبارًا من 3.1.0، يربط التوقيع ترويسة X-NextPDF-Timestamp داخل السلسلة الأساس للـ MAC، فيتحقق المستقبِلون من الحداثة والسلامة معًا. للاطّلاع على الدليل على مستوى سير العمل، راجع Webhook.
التوفّر والترخيص
قسم بعنوان «التوفّر والترخيص»تُشحن هذه الإمكانية في NextPDF Enterprise (nextpdf/enterprise) وتُفعَّل بمظروف ترخيص من فئة Enterprise. أي نشر بلا ذلك الاستحقاق لا يُحمِّل أصناف الإمكانية. قارن الإصدارات واحصل على ترخيص.
سطح webhook إمكانية Enterprise أساسية، متاحة بمجرد تثبيت حزمة Enterprise؛ ولا توجد راية منفصلة لكل ميزة. لا يملك NextPDF Core (Apache-2.0) ولا NextPDF Pro أي سطح لتسجيل webhook أو تسليمه؛ إذ يُشحن المدير والتسجيل والحمولة ومحرك التسليم وسياسة إعادة المحاولة وإدخال الرسائل الميتة في nextpdf/enterprise فقط.
سطح واجهة API العامة
قسم بعنوان «سطح واجهة API العامة»| الرمز | المعاملات | السلوك الافتراضي | القيمة المُعادة | يرمي أو يفشل بـ | ملاحظات |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | يُنشئ مديرًا بفهرس تسجيل فارغ في الذاكرة | كائن WebhookManager جديد | لا يرمي | تُفهرَس التسجيلات لكل مستأجر |
WebhookManager::register | TenantContext $tenant, WebhookRegistration $registration | يُلحق التسجيل بفهرس المستأجر المُستدعِي | void | InvalidArgumentException عندما لا يطابق مستأجرُ التسجيل مستأجرَ السياق | يُرفض التسجيل عبر المستأجرين قبل التخزين |
WebhookManager::unregister | TenantContext $tenant, string $registrationId | يستبدل التسجيل المطابق بنسخة مُعطَّلة | bool | لا يرمي؛ يُعيد false عند عدم العثور على المعرّف | تعطيل ناعم؛ يُحفظ السجل |
WebhookManager::activeRegistrations | TenantContext $tenant | يُرشّح تسجيلات المستأجر إلى النشطة منها | list<WebhookRegistration> | لا يرمي | تظهر تسجيلات المستأجر المُستدعِي فقط |
WebhookManager::dispatch | TenantContext $tenant, JobEvent $event | يُسلّم الحدث إلى كل تسجيل نشط مشترك في نوع الحدث | int (عمليات التسليم الناجحة) | يُمرّر JsonException عندما تكون بيانات الحدث غير قابلة للترميز بصيغة JSON؛ إخفاقات التسليم لا ترمي | يُولَّد معرّف تسليم جديد من 32 خانة سُداسية عشرية لكل عملية تسليم لتسجيل |
WebhookRegistration::__construct | string $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = null | يخزّن القيم المُمرَّرة حرفيًا | كائن WebhookRegistration جديد | لا يوجد @throws مُعلَن؛ يرفع PHP النوع TypeError عند عدم تطابق أنواع الوسائط تحت strict_types | final readonly؛ $events الفارغ يعني الاشتراك في الكل |
WebhookRegistration::subscribesTo | JobEventType $eventType | true عندما يكون $events فارغًا أو يحتوي على النوع | bool | لا يرمي | مقارنة هوية صارمة |
WebhookRegistration::deactivate | — | يُعيد نسخة غير نشطة | self | لا يرمي | الكائن الأصلي دون تغيير |
WebhookPayload::fromJobEvent | JobEvent $event, string $tenantId, string $deliveryId | ينسخ معرّف المهمة ونوع الحدث والبيانات والطابع الزمني من الحدث | self | لا يرمي | مصنع ساكن يستخدمه dispatch |
WebhookPayload::toJson | — | يُسلسِل جسم الرسالة ذا الحقول الستة مع شرطات مائلة غير مهروبة | non-empty-string | JsonException عندما تكون بيانات الحدث غير قابلة للترميز بصيغة JSON | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | يُعيد جسم الرسالة كمصفوفة ترابطية | array<string, mixed> | لا يرمي | الطابع الزمني منسَّق بصيغة RFC 3339 الموسّعة |
WebhookPayload::signedTimestamp | — | زمن الحدث بثوانٍ Unix، مقيَّد إلى صفر أو أكبر | int<0, max> | لا يرمي | يُصدَر بوصفه X-NextPDF-Timestamp ويُربط داخل الـ MAC |
WebhookPayload::sign | string $secret | HMAC-SHA256 على السلسلة الأساس {signedTimestamp}.{jsonBody} | non-empty-string (سُداسي عشري) | JsonException عبر toJson() عندما يكون الجسم غير قابل للترميز | يربط ترويسة الطابع الزمني بالجسم تشفيريًا |
WebhookDelivery::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null | محرك تسليم PSR-18/PSR-17 بطابور رسائل ميتة فارغ | كائن WebhookDelivery جديد | لا يرمي | السياسة الافتراضية: 5 محاولات، أساس 1 ثانية، سقف 300 ثانية |
WebhookDelivery::deliver | WebhookRegistration $registration, WebhookPayload $payload | يرسل الحمولة الموقَّعة بطلب POST مع التحقق من خروج SSRF لكل محاولة وتراجع أُسّي | bool | JsonException قبل المحاولة الأولى عندما يكون الجسم غير قابل للترميز؛ خلاف ذلك لا يرمي — false يعني أن الحمولة وُجِّهت إلى طابور الرسائل الميتة | true فقط عند استجابة 2xx |
WebhookDelivery::deadLetters | — | يُعيد كل الإدخالات المُسجَّلة | list<DeadLetterEntry> | لا يرمي | في الذاكرة، بنطاق العملية |
WebhookDelivery::clearDeadLetters | — | يُفرّغ طابور الرسائل الميتة | void | لا يرمي | لا رجعة فيه؛ صدّر الإدخالات أولًا إذا لزمت إعادة التشغيل |
WebhookRetryPolicy::__construct | int $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300 | يخزّن قيم السياسة | كائن WebhookRetryPolicy جديد | لا يوجد @throws مُعلَن؛ المعاملات موثّقة على أنها positive-int | $maxRetries يعُدّ إجمالي المحاولات |
WebhookRetryPolicy::delayForAttempt | int $attempt | baseDelaySeconds × 2^(attempt − 1)، بحد أقصى maxDelaySeconds | positive-int | لا يرمي | أرقام المحاولات تبدأ من 1 |
WebhookRetryPolicy::shouldRetry | int $currentAttempt | true طالما أن المحاولة الحالية دون الحد الأقصى | bool | لا يرمي | يُتخطّى الانتظار بعد المحاولة الأخيرة |
WebhookRetryPolicy::default | — | 5 محاولات، أساس 1 ثانية، سقف 300 ثانية | self | لا يرمي | مصنع ساكن؛ الافتراضي للإنتاج |
WebhookRetryPolicy::aggressive | — | 10 محاولات، أساس 2 ثانية، سقف 600 ثانية | self | لا يرمي | مصنع ساكن لنقاط النهاية الحرجة |
DeadLetterEntry::__construct | string $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false | يخزّن سجل الإخفاق حرفيًا | كائن DeadLetterEntry جديد | لا يوجد @throws مُعلَن؛ TypeError تحت strict_types | final readonly؛ $lastHttpStatus بقيمة null يعني إخفاق النقل |
DeadLetterEntry::markReplayed | — | يُعيد نسخة بـ replayed = true | self | لا يرمي | المعرّف نفسه؛ الإدخال الأصلي دون تغيير |
public function __construct( private readonly WebhookDelivery $delivery, private readonly ?LoggerInterface $logger = null,) {}
public function register(TenantContext $tenant, WebhookRegistration $registration): void
public function unregister(TenantContext $tenant, string $registrationId): bool
public function activeRegistrations(TenantContext $tenant): array
public function dispatch(TenantContext $tenant, JobEvent $event): intpublic function __construct( public string $id, public string $tenantId, public string $url, public array $events, public string $secret, public bool $active = true, public ?string $description = null,) {}
public function subscribesTo(JobEventType $eventType): bool
public function deactivate(): selfpublic static function fromJobEvent( JobEvent $event, string $tenantId, string $deliveryId,): self
public function toJson(): string
public function toArray(): array
public function signedTimestamp(): int
public function sign(string $secret): stringpublic function __construct( private readonly ClientInterface $httpClient, private readonly RequestFactoryInterface $requestFactory, private readonly StreamFactoryInterface $streamFactory, private readonly WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), private readonly ?LoggerInterface $logger = null,) {}
public function deliver(WebhookRegistration $registration, WebhookPayload $payload): bool
public function deadLetters(): array
public function clearDeadLetters(): voidpublic function __construct( public int $maxRetries = 5, public int $baseDelaySeconds = 1, public int $maxDelaySeconds = 300,) {}
public function delayForAttempt(int $attempt): int
public function shouldRetry(int $currentAttempt): bool
public static function default(): self
public static function aggressive(): selfpublic function __construct( public string $id, public string $registrationId, public WebhookPayload $payload, public int $attempts, public string $lastError, public ?int $lastHttpStatus, public DateTimeImmutable $failedAt, public bool $replayed = false,) {}
public function markReplayed(): selfعقد السلوك
قسم بعنوان «عقد السلوك»- تُفهرَس التسجيلات لكل مستأجر. يرفض
register()أي تسجيل لا يطابق معرّفُ مستأجره السياقَ المُستدعِي. وunregister()تعطيل ناعم: يُستبدَل التسجيل بنسخة غير نشطة، مع الحفاظ على السجل واستبعاده من أي إرسال مستقبلي. - يمرّ
dispatch()فقط على تسجيلات المستأجر المُستدعِي النشطة المشتركة في نوع الحدث المُرسَل. وقائمة الأحداث المشتركة الفارغة تعني الاشتراك في الكل. وتَعُدّ القيمة المُعادة عمليات التسليم الناجحة. - كل عملية تسليم هي طلب HTTP POST بجسم JSON وخمس ترويسات:
Content-Type: application/jsonوX-NextPDF-Signature(sha256=<hex>) وX-NextPDF-Timestamp(ثوانٍ unix) وX-NextPDF-Delivery-IdوX-NextPDF-Event. - حقول جسم JSON هي
delivery_idوjob_idوevent_typeوdataوtimestamp(RFC 3339 الموسّعة) وtenant_id، مُسلسَلة بشرطات مائلة غير مهروبة. وتأتي قيم نوع الحدث منJobEventTypeفيnextpdf/core:progressوcompletedوfailedوcancelled. - مخطط التوقيع (تغيّر في 3.1.0، تغيير كاسر). السلسلة الأساس لـ HMAC-SHA256 هي
{signedTimestamp}.{jsonBody}، مُفتاحها سرّ التسجيل — لا الجسم وحده. وقيمةX-NextPDF-Timestampهي مكوّن الطابع الزمني في الـ MAC، لذا فإن أي ترويسة طابع زمني مُتلاعَب بها أو مُعاد تشغيلها تُبطِل التوقيع. - تحقُّق المستقبِل: اقرأ ترويسة
X-NextPDF-TimestampبالقيمةT؛ وارفُض عندما تقعTخارج نافذة حداثة مقبولة (مثلًا 300 ثانية)؛ وأعِد حسابhash_hmac('sha256', T . '.' . rawBody, secret)على البايتات الخام المُستلَمة؛ وقارِن في زمن ثابت مع قيمة الترويسة بعد إزالة البادئةsha256=. - يُحسَب الجسم والتوقيع ومعرّف التسليم مرة واحدة لكل عملية تسليم ويبقون ثابتين عبر محاولات إعادة المحاولة.
- بوّابة خروج SSRF. قبل كل محاولة، تمرّ وجهة URL عبر بوّابة
UrlValidator::validateExternalUrl()في Core: مخطط HTTPS فقط؛ وتُحجَب نطاقات الاسترجاع (loopback) والخاصة والمحجوزة وcarrier-grade-NAT وبيانات السحابة الوصفية ونطاقات انتقال IPv6 المضمّنة لـ IPv4؛ وتُحلّ أسماء المضيفين عبر DNS (السجلّان A وAAAA) وتُرفَض المضيفات غير القابلة للحل برفض مُغلَق فشلًا. ولا تُرسَل وجهة URL محجوبة أبدًا: تُجهَض حلقة المحاولات وتُوجَّه الحمولة مباشرة إلى طابور الرسائل الميتة مع خطأ أخيرBlocked SSRF destination:وحالة HTTP بقيمة null. - تصنيف النتيجة لكل محاولة: 2xx نجاح ويعود فورًا؛ و4xx غير 429 نهائي ويذهب مباشرة إلى الرسائل الميتة؛ وكل نتيجة أخرى — 3xx أو 429 أو 5xx أو استثناء نقل — قابلة لإعادة المحاولة حتى إجمالي عدد محاولات السياسة.
- التراجع أُسّي: الانتظار قبل المحاولة التالية هو
baseDelaySeconds × 2^(attempt − 1)، بحد أقصىmaxDelaySeconds. ويُتخطّى الانتظار بعد المحاولة الأخيرة. - عندما لا تنجح أي محاولة، يسجّل
DeadLetterEntryمعرّفًا فريدًا، ومعرّف التسجيل، والحمولة الأصلية، وعدد المحاولات (المُقيَّد إلى الحد الأقصى للسياسة)، ورسالة الخطأ الأخيرة، وحالة HTTP الأخيرة (null عند إخفاق النقل أو حجب SSRF)، والطابع الزمني للإخفاق. - طابور الرسائل الميتة في الذاكرة ونطاقه عمر العملية. ويُنتِج
markReplayed()نسخة مُعلَّمة؛ لكنه لا يُعيد الإرسال، ويحتفظ الطابور بالإدخال الأصلي.
الحالات الحديّة وأنماط الإخفاق
قسم بعنوان «الحالات الحديّة وأنماط الإخفاق»- قائمة أحداث فارغة. يستقبل التسجيل كل نوع حدث. حدِّد نطاق القائمة صراحةً عندما يجب ألّا يرى المستقبِل كل الأحداث.
- 4xx النهائي مقابل إخفاق النقل. يسجّل رفض 4xx قيمة معبّأة في
lastHttpStatus؛ بينما يسجّل إخفاق الاتصال null. استخدم قيمة null للتمييز بين رفض المستقبِل وإخفاق النقل. - وجهة محجوبة بـ SSRF. أي تسجيل يشير إلى عنوان HTTP أو خاص أو استرجاع (loopback) أو بيانات وصفية يُرحَّل إلى الرسائل الميتة في المحاولة الأولى مع خطأ
Blocked SSRF destination:وحالة null. ولا يُرسَل أي طلب صادر. صحّح عنوان URL وسجِّل من جديد. - المستقبِلات القديمة بعد الترقية. أي مستقبِل ما زال يتحقق من HMAC القديم القائم على الجسم وحده (قبل 3.1.0) يفشل مُغلَقًا أمام عمليات تسليم 3.1.0. رحِّل المستقبِل إلى السلسلة الأساس
{timestamp}.{body}واستهلكX-NextPDF-Timestamp. - بيانات حدث غير قابلة للترميز. ترمي
toJson()وsign()النوعJsonException، الذي يُمرَّر خارجdeliver()وdispatch()قبل إجراء أي محاولة. - حجب متزامن. تنام
deliver()ضمنيًا بين المحاولات. ويبلغ التراجع التراكمي 15 ثانية تحت السياسة الافتراضية ونحو 17 دقيقة تحت السياسة العدوانية. أرسِل من عامل طابور عندما يكون زمن استجابة المستقبِل غير موثوق. - تقييد عدد المحاولات. لا يتجاوز عدد المحاولات المُسجَّل الحد الأقصى للسياسة أبدًا، رغم أن عدّاد الحلقة الداخلي يتقدّم متجاوزًا إياه عند الاستنفاد.
- نمو الطابور ومتانته. ينمو طابور الرسائل الميتة بلا حدّ داخل العملية ويتلاشى عند إعادة التشغيل. صدّر الإدخالات عبر
deadLetters()واحفظها خارجيًا قبل استدعاءclearDeadLetters()عندما تلزم إعادة تشغيل مُعمَّرة. - إعادة التشغيل مدفوعة بالمُشغِّل. إعادة التسليم تعني استدعاء
deliver()مجددًا بحمولة الإدخال؛ أماmarkReplayed()فتسجّل الأمر على نسخة فقط. - بقية إعادة ربط DNS. يُعاد التحقق من URL في كل محاولة، ما يضيّق نافذة إعادة الربط دون أن يغلقها: إذ لا يستطيع تجريد PSR-18 تثبيت الاتصال على IP المُتحقَّق منه. أضِف ضوابط خروج على مستوى الشبكة حيثما كانت هذه البقية مهمّة.
- التعامل مع السرّ. سرّ التسجيل بيانات اعتماد. ويوثّق HMAC السلامة والمصدر فقط — وليس السرّية. لا تضع في حمولة الحدث بيانات يجب ألّا يراها المستقبِل.
سلوك وضع FIPS
قسم بعنوان «سلوك وضع FIPS»توقيع الحمولة هو HMAC-SHA256 عبر دالة hash_hmac() في PHP، لذا يعتمد على مزوّد التشفير المضيف. وفي بناء مُقيَّد بـ FIPS، يفشل أي بدائي غير مُعتمَد عند الحدّ التشفيري بدلًا من التنازل إلى مستوى أدنى. ولا تضيف طبقة webhook أي سياسة تشفير خاصة بها.
المطابقة
قسم بعنوان «المطابقة»- يُنفِّذ توثيق الحمولة HMAC، وهو رمز توثيق الرسائل بالتجزئة المُفتاحية وفق FIPS PUB 198-1 §1، مُهيّأً بـ SHA-256.
- تتبع الحماية من إعادة التشغيل إرشادات أمن webhook في OWASP Cheat Sheet Series: يسافر الطابع الزمني للحدث في ترويسة مخصّصة ويُبذَر في حساب التوقيع، لذا يفشل أي طابع زمني مُتلاعَب به في التحقق.
- تستخدم الطوابع الزمنية للجسم صيغة التاريخ والوقت الموسّعة RFC 3339. مُعلَن في الشيفرة: لم يُسترجَع RFC 3339 من مجموعة RAG لهذه الصفحة.
- هذه بيانات إمكانية مؤصَّلة في مصدر المنتج والبنود المُستشهَد بها. لا يقدّم NextPDF أي ادّعاء مطابقة أو اعتماد لهذا السطح.
ملاحظات التطوير
قسم بعنوان «ملاحظات التطوير»- تُعلن كل الأصناف
strict_types=1وهيfinal؛ وWebhookRegistrationوWebhookPayloadوWebhookRetryPolicyوDeadLetterEntryهيfinal readonlyبخصائص عامة مُرقّاة. - تحمل الوحدة تعليق
@sinceبقيمة2.2.0؛ ومخطط التوقيع المرتبط بالطابع الزمني تغيير كاسر موثَّق في 3.1.0. - يأخذ محرك التسليم تجريدات PSR-18/PSR-17، لذا يستطيع عميل HTTP وهمي تمرين مسار الإرسال وإعادة المحاولة والرسائل الميتة كاملًا دون اتصال. ويتخذ المسجِّل القيمة الافتراضية null؛ احقِن مسجِّل PSR-3 في الإنتاج وإلا فلن تظهر الإخفاقات إلا عبر القيم المُعادة.
- ينبغي لتطبيقات المستقبِل استخدام
hash_equals()لمقارنة التوقيع وفرض نافذة حداثة علىX-NextPDF-Timestamp. - اختبارات حدّية موصى بها: تسجيل بعدم تطابق المستأجر، وتوزيع بقائمة أحداث فارغة، و4xx نهائي، واستنفاد إعادة المحاولة، وعنوان URL محجوب بـ SSRF، ورفض توقيع مُتلاعَب بطابعه الزمني مقابل متجه ثابت، وتقييد عدد محاولات الرسائل الميتة.
حدود النشر
قسم بعنوان «حدود النشر»توثّق هذه الصفحة السلوك القابل للملاحظة خارجيًا وسطح واجهة API العامة المدعوم فقط. أما مسارات مجالات الأسماء الداخلية والأصناف المساعِدة وجداول الآليات وأسماء ملفات كتيّبات التشغيل وبادئات التذاكر فخارج النطاق.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- Webhook — NextPDF Enterprise — صفحة الإمكانية: سير العمل والإعداد وأمثلة تسجيل مشروحة.
- SaaS — مرجع متعمّق — هوية المستأجر ومفاتيح API والحصص؛ مصدر
TenantContext. - Metering — مرجع متعمّق — توزيع قياس الاستخدام بانضباط التسليم نفسه لـ PSR-18.