📄 mdseal

المساعدة — كيف يُستعمل mdseal

1. تقديم الوثيقة

2. اختيار صيغة المُخرَج

الصيغةالنتيجة
PDF (مقاس A4)وثيقة مُصفّحة بمقاس A4، بسمة — من Markdown أو HTML.
عرض شرائح PDF (16:9)شريحة في كلّ صفحة؛ افصل بين الشرائح بسطر ---.
PowerPoint (PPTX)عرضٌ قابل للتحرير؛ ينتظر Markdown (عناوين ## تفتح الشرائح).
معاينة HTMLتعرض الوثيقة بسمتها في المتصفّح، دون تنزيل.

3. اختيار سمة — palette.amia.fr

السمات المعروضة (mdseal، carbon، dark…) مشتقّة من palette.amia.fr — أي Palette Studio، وهو المصدر الوحيد لألواح ألوان منظومة @mostajs/*. تُؤلَّف كلّ لوحة هناك انطلاقًا من بضعة ألوان للعلامة، ثمّ تُشتقّ كلّ الألوان المفيدة ويُتحقَّق من تباينها وفق WCAG: فلا يُنتَج لون غير مقروء أبدًا (واللون المميّز لا يحمل نصًّا قطّ).

ولإضافة سمة أو تعديلها: ألّفها على palette.amia.fr (معاينات للحاسوب والهاتف، وجدول تباين)، ثمّ خذ ألوان علامتها وأضف مدخلها إلى مخزن @mostajs/palette (GALLERY). وسيعرضها mdseal تلقائيًّا.

تنطبق السمة على PDF وعلى معاينة HTML. أمّا PPTX فيتبع قالب PowerPoint لا ورقة الأنماط.

4. توقيع وثيقة وحمايتها

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

الشهادة

شهادة PKCS#12 (.p12 أو .pfx) وعبارة سرّها. الشهادة التي تحمّلها أنت هي المقدَّمة؛ وإن لم توجد استعمل mdseal شهادة الخادم إن كانت مهيّأة. ولا يعيش المفتاح إلّا في الذاكرة ريثما يتمّ التوقيع: لا يُخزَّن ولا يُدوَّن، ولا تمرّ عبارة السرّ قطّ في سطر أوامر المحرّك.

الحماية — ما تمنعه وما لا تمنعه

⚠ اقرأ هذا قبل التأشير. أذونُ PDF تعليماتٌ لا أقفال. تُلزِم القرّاء الذين يحترمونها — Acrobat وكلّ جادّ منها — ولا تُلزِم سواهم. فوثيقةٌ مختومة بـ«لا طبع ولا نسخ» تُطبَع وتُنسَخ بلا عناء بأداةٍ تختار تجاهلها، وليس في صيغة PDF ما يمنع ذلك. وما يلي يصف ما يكتبه mdseal في الوثيقة، لا ما يضمنه.

تأشير التشفير (AES-256) وتقييد الأذون يُلحق بالوثيقة أذونًا تضبطها خانةً خانة. ويعطي الجدول التالي الحالة الافتراضية:

الإذنافتراضيًّا
الطبع (دقّة عالية)مرفوض
الطبع (دقّة منخفضة)مرفوض
استخراج النصّ (النسخ واللصق)مرفوض
تعديل المحتوىمرفوض
التعليقات والحواشيمرفوضة
التجميع (الصفحات)مرفوض
القراءة بقارئ الشاشةمأذونة
ملء حقول النموذجمأذون — ولا يمكن رفضه

الخانة المؤشَّرة تعني مأذون، لا «ممنوع» أبدًا: هذا هو معنى صيغة PDF، وهو ما سيعرضه كلّ مدقّق — فإلغاء تأشير «الطبع» يُقرأ print_highres : False. وتبقى إتاحة الوصول مفتوحة عن قصد: حماية نصّ من النسخ ليست سببًا لإغلاقه في وجه من لا يبصر.

الإذن بالطبع عالي الدقّة يأذن كذلك بالطبع العادي: فالصيغة تتجاهل بتّ «الجودة العالية» متى رُفض الطبع.

⚠ ملء حقول النموذج لا يمكن رفضه، وليس ذلك سهوًا: ففي صيغة PDF حقلُ التوقيع حقلُ نموذج، فختمُ الوثيقة يقتضي هذا الإذن. ونتيجةٌ جديرةٌ بالعلم: وثيقةٌ كانت لها أصلًا حقول قابلة للملء تبقى قابلة للملء. أمّا الختم فيكشف كلّ تعديل — وهناك تكمن الضمانة، لا في الإذن.

التاريخ — ما الذي يُشهَد به، ومن يشهد

يُثبت الختمُ أنّ الوثيقة لم تتغيّر، ولا يُثبت متى خُتمت: فالساعة، افتراضًا، صفةٌ يصرّح بها المُوقِّع — أي قابلة للتقديم إلى الوراء. لذلك يجعل mdseal سلطةَ تأريخ مستقلّة تشهد باللحظة (RFC 3161)، فينتقل التوقيع من PAdES-B إلى PAdES-T.

⚠ مستويان للتأريخ، والفرق بينهما ليس في الدرجة. يميّز نظام eIDAS الأوروبيّ (المادة 41):

المستوىما يساويه التاريخمن يقع عليه الإثبات
مؤهَّلقرينة قانونيّة بصحّتهمن يعترض
بسيطمقبول، بلا قرينةمن يقدّم الوثيقة
لا شيءمصرَّح به من المُوقِّع— قابل للتقديم إلى الوراء

ولا يُحصَّل التأريخ المؤهَّل إلّا من مزوّدٍ مُدرَجٍ في قائمة الثقة الأوروبيّة. يردّ GET /api/v1/moi الحقلَ signature.horodatage.niveau: اطلبه قبل أن تلتزم أمام زبائنك، ولا تُعلن أكثر ممّا يردّه.

⚠ وإن لم تُجب سلطةُ التأريخ فشل الختم — ولا بديل. فوثيقةٌ تحسبها مؤرَّخة وهي ليست كذلك لا ينكشف أمرها إلّا لحظة الاحتجاج بها.

كلمة السرّ عند الفتح — القفل الصلب الوحيد، وهو لا يقفل غير الفتح

حقل كلمة السرّ عند الفتح يغيّر طبيعة الحماية. فمتى مُلئ، لم تُفتَح الوثيقة في أيّ قارئ بدونه: لم يعد الأمر تعليمةً تُحترَم، بل تشفيرًا لا يُلتَفّ عليه. وإن تُرك فارغًا بقيت الوثيقة مقروءة بحرّيّة، وبقيت قيودها معلّقة على حسن نيّة القارئ.

وإدخال كلمة سرّ يفعّل الحماية: فتنطبق الأذون المعروضة. ولا يحتفظ mdseal بها — فإن ضاعت ضاعت الوثيقة معها. لا رجوع في ذلك، وهو بالضبط ما يمنحها قيمتها.

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

فالسؤال الوحيد الجدير بالطرح هو: «لمن أُعطي كلمة السرّ؟». أمّا منعُ مُرسَلٍ إليه شرعيّ من الطبع أو النسخ فلا تقدّم فيه صيغة PDF شيئًا — لا هنا ولا في غيرها، ولْيُحذَر من يزعم خلاف ذلك. والذي يُحتجّ به حقًّا في وثيقةٍ ختمها mdseal هو التوقيع: لا يُخفي شيئًا، لكنّه يُثبت أنّ الوثيقة لم تتغيّر منذ الختم، وفي أيّ تاريخ.

طريقتان للختم — ولا تُثبتان الشيء نفسه

يختم mdseal بطريقتين. والفرق ليس تقنيًّا: إنّه في ما تُثبته الوثيقة.

بشهادة الخدمة — لا شيء تُرسله. تُختَم الوثيقة، وأيّ تعديل يكسر الختم، وتحمل تاريخًا يشهد به مُؤرِّخ مستقلّ. وما لا تُثبته: هويّتك. فشهادة الخدمة لم يتحقّق منها أيّ طرف ثالث، وسيعرض قارئ PDF «صلاحية غير معروفة».

بشهادتك أنت — تُودع ملفّ .p12 وعبارته السرّية، من الشاشة أو عبر الواجهة البرمجية. عندئذٍ يُنسب الختم إليك، وتبقى لك السيطرة عليه. وهذه هي الطريق ذات الوزن القانونيّ: مؤسّستك أنت هي التي تختم، لا نحن.

متى تختار أيًّا منهما؟ لوثيقة داخليّة أو مسوّدة: شهادة الخدمة تكفي. أمّا لشهادة أو دبلوم أو تقرير سيُحتجّ به: فشهادتك أنت.

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

⚠️ عبارتك السرّية لا تُستعمل إلّا لفتح الملفّ أثناء الختم. ولا تُحفظ هي ولا الشهادة.

5. من دون متصفّح — التحويل نفسه في سطر الأوامر

هذا الموقع واجهة. أمّا المحوّل نفسه فوحدة تُثبَّت:

npm i -g @mostajs/doc-export

doc-export rapport.md -o rapport.pdf            # PDF بمقاس A4 وبسمة
doc-export cours.md -f slides-pdf -t carbon     # عرض شرائح 16:9
cat doc.md | doc-export -f html -o -            # داخل أنبوب
doc-export --capacites                          # ما هو متاح هنا

القلب نفسه، فالوثيقة نفسها: هذه الصفحة وذلك الأمر يستدعيان كلاهما @mostajs/doc-export عينه. ويخدم الأمرُ ما لا يقدر عليه متصفّح — مجلّدًا كاملًا دفعةً واحدة، أو تكاملًا مستمرًّا، أو جهازًا بلا شبكة.

⚠ فارقٌ واحد لا غير، وهو مقصود. هذا الموقع يستعلم الاستوديو مباشرةً؛ أمّا الأمر فيقرأ المعرض المضمَّن في @mostajs/palette. فسمةٌ عُدّلت هذا الصباح تظهر هنا فورًا، وفي سطر الأوامر بعد تحديث الوحدة. النصّ واحد؛ وقد تفترق الألوان بيوم.

⚠ صيغة PDF تمرّ بـ Chrome/Chromium، وصيغة PPTX بـ pandoc. فإن غابا قال الأمر ذلك وتوقّف: ولا يسلّم أبدًا وثيقةً منقوصة في صمت.

ويتبع التوقيع القاعدة نفسها وله أمره الخاصّ، للسبب نفسه — فلا امتياز لهذا الموقع عليه:

npm i -g @mostajs/pdf-sign

mosta-pdf-sign sign rapport.pdf signe.pdf --p12 cert.p12 --pass ••• --restrict \
                                    --open-password •••     # القفل الصلب
mosta-pdf-sign verify signe.pdf --trust ca.pem      # سليمة؟ صحيحة؟ موثوقة؟
mosta-pdf-sign engines                              # أيقدر هذا الخادم على الحماية؟

6. من برنامج — واجهة HTTP

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

كلّ نداء يقدّم مفتاحًا في ترويسة Authorization. لا مسار عموميّ، ولا مفتاح في العنوان: فالسرّ الذي يسافر في عنوان ينتهي في سجلّات الخادم، وفي تاريخ المتصفّح، وفي ترويسة Referer المرسلة إلى الغير.

النداءوظيفته
GET /api/v1/moiيتحقّق من المفتاح ويبيّن ما يحمله، وما يقدر عليه هذا الخادم، وحدّ الحجم عنده. ولا يستهلك تحويلًا.
POST /api/v1/convertيحوّل — ويختم في المرور نفسه إن قُدِّمت كتلة sign.
POST /api/v1/signيختم ملفّ PDF جاهزًا.

التحويل

curl -X POST https://mdseal.dev/api/v1/convert \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"source":"# تقرير\n\nنصّ.","format":"pdf","themeId":"mdseal"}' \
  -o rapport.pdf
الحقلالقيمافتراضيًّا
sourceنصّ Markdown أو HTML — مطلوب—
inputTypemd · htmlmd
formatpdf · slides-pdf · pptx · htmlpdf
themeIdسمة من القائمة التي يردّها /api/v1/moihadhinat
filenameاسم الملفّ المُعادdocument
signكتلة التوقيع (أدناه) — تقتضي مفتاحًا يحمل signغائبة

التوقيع والحماية

مدخلان، بحسب الجهة التي تنادي منها — ملفّ PDF خامًا مع الخيارات في معاملات العنوان، أو JSON مع الملفّ مرمّزًا بـ base64:

curl -X POST "https://mdseal.dev/api/v1/sign?proteger=1&motdepasse=•••&reason=Original" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/pdf" \
  --data-binary @rapport.pdf -o rapport-signe.pdf

تحمل كتلة sign (في /convert) والمعاملات (في /sign) الأسماء نفسها: proteger، motdepasse، permissions، p12 (بـ base64) وpassphrase، reason، name، location.

⚠ قاعدتان منقولتان حرفيًّا عن الشاشات، حتّى تُرجع الواجهة والمتصفّح الوثيقة نفسها. كلمة السرّ تعدل طلبَ حماية: يكفي تقديمها دون proteger. وكتلة permissions متى قُدِّمت صارت قائمة مغلقة — فما لم يرد فيها مرفوض، ولا يُترَك للمحرّك قطّ. وإن غابت الكتلة انطبقت القيم الافتراضية في الفقرة 4.

الحصص

يُحتسَب موردان على مدّتين مختلفتين، وليس ذلك اعتباطًا. التحويل متواتر وزهيد: فحدّه باليوم يحمي الخادم دون أن يضايق أحدًا. أمّا التوقيع فنادر، وتنفيذه مكلف، وهو المبيع: فحدّه بالشهر، على مثال الاشتراك — حتّى لا يوقفك يومٌ كثير العمل.

الخطّةتحويلات/يومتوقيعات/شهرمفاتيح
مجّانية20لا شيء1
Pro5002003
مؤسّساتبلا حدّ200010

يردّ GET /api/v1/moi الحقلين quotas.conversion و quotas.signature مع courant وlimite وrestant: وذلك يكفي للتوقّف قبل الرفض بدل اكتشافه في وسط سلسلة معالجة. ولا يستهلك هذا النداء شيئًا.

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

ما تردّه الواجهة

عند النجاح: الملفّ بنوعه واسمه. وعند الخطأ: JSON لا صفحة HTML أبدًا — {"error":{"code":"…","message":"…"}}.

الرمزمعناه
401 UNAUTHORIZEDالمفتاح غائب أو باطل أو ملغًى أو موقوف. وتُجاب الحالات الأربع بالعبارة نفسها، عن قصد.
403 FORBIDDENالمفتاح سليم لكنّه لا يحمل هذه العمليّة.
403 PLAN_REQUIREDخطّتك لا تشمل هذه العمليّة. ولا ينفع الانتظار في شيء — ولذلك لا تحمل هذه الإجابة ترويسة Retry-After.
429 QUOTA_EXCEEDEDنفدت الحصّة. تحمل الإجابة Retry-After بالثواني، وتذكر courant وlimite وperiode.
400 BAD_REQUESTطلب سيّئ التكوين — والرسالة تبيّن موضع الخلل.
413 PAYLOAD_TOO_LARGEالوثيقة تتجاوز الحدّ (انظر limiteCorpsMo في /api/v1/moi). وتُغلق الإجابة الاتّصال: صغّر الإرسال ولا تُعده كما هو.
415نوع محتوى لا يخدمه هذا النداء.
422 SIGNATURE_FAILEDنجح التحويل وفشل التوقيع — الشهادة أو عبارة السرّ.

لا يُخدَم التوقيع إلّا إذا كان هذا الخادم يحمله. ويقول /api/v1/moi ذلك قبل أيّ نداء: signature.disponible وsignature.motDePasseOuverture و signature.certificatServeur.

٧. ربط mdseal ببرنامجك

ثلاث طرائق لطرق الباب نفسه. الأمثلة تستهدف هذا الخادم — انسخها كما هي.

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

curl

# Convertir un Markdown en PDF
curl -X POST https://mdseal.dev/api/v1/convert \
  -H "Authorization: Bearer $MDSEAL_CLE" \
  -H "Content-Type: application/json" \
  -d '{"source":"# Devis n°42\n\nMontant : 1 200 €","format":"pdf"}' \
  -o devis.pdf

# Convertir ET sceller en un seul appel
curl -X POST https://mdseal.dev/api/v1/convert \
  -H "Authorization: Bearer $MDSEAL_CLE" \
  -H "Content-Type: application/json" \
  -d '{"source":"# Attestation","format":"pdf","sign":{"reason":"Attestation","name":"Service RH"}}' \
  -o attestation-signee.pdf

# Sceller un PDF qu'on a déjà
curl -X POST https://mdseal.dev/api/v1/sign \
  -H "Authorization: Bearer $MDSEAL_CLE" \
  -H "Content-Type: application/json" \
  -d "{\"pdf\":\"$(base64 -w0 facture.pdf)\",\"filename\":\"facture\"}" \
  -o facture-signee.pdf

# Ce qu'il reste de quota
curl -H "Authorization: Bearer $MDSEAL_CLE" https://mdseal.dev/api/v1/moi

TypeScript

const CLE = process.env.MDSEAL_CLE!;

export async function sceller(markdown: string, motif: string): Promise<Buffer> {
  const r = await fetch('https://mdseal.dev/api/v1/convert', {
    method: 'POST',
    headers: { authorization: `Bearer ${CLE}`, 'content-type': 'application/json' },
    body: JSON.stringify({ source: markdown, format: 'pdf', sign: { reason: motif } }),
  });

  // Les deux refus ne se traitent PAS pareil, et c'est tout l'intérêt de les distinguer :
  //   403 — l'offre ne comprend pas l'opération. Réessayer n'y changera rien.
  //   429 — le quota est atteint, mais il revient : Retry-After dit quand.
  if (r.status === 403) throw new Error('Offre insuffisante — changez de plan.');
  if (r.status === 429) {
    const secondes = Number(r.headers.get('retry-after') ?? 3600);
    throw Object.assign(new Error('Quota atteint.'), { reessayerDans: secondes });
  }
  if (!r.ok) {
    const { error } = await r.json() as { error: { code: string; message: string } };
    throw new Error(`${error.code} — ${error.message}`);
  }
  return Buffer.from(await r.arrayBuffer());
}

رفضان، ومعالجتان — وهي النقطة التي تُغفلها أغلب عمليّات الدمج. 403 تعني «خطّتك لا تسمح بذلك»: إعادة المحاولة لا تغيّر شيئًا. 429 تعني «نفدت حصّتك، وهي تعود»: وترويسة Retry-After تقول متى. الخلط بينهما يجعل العميل يدور على رفض نهائيّ، أو يُسقط وثيقة كان يكفي انتظارها.

Java

import java.net.URI;
import java.net.http.*;
import java.nio.file.*;

public final class Mdseal {
  private static final HttpClient CLIENT = HttpClient.newHttpClient();
  private final String cle;

  public Mdseal(String cle) { this.cle = cle; }

  public byte[] sceller(String markdown, String motif) throws Exception {
    String corps = """
        {"source": %s, "format": "pdf", "sign": {"reason": %s}}
        """.formatted(json(markdown), json(motif));

    HttpRequest requete = HttpRequest.newBuilder()
        .uri(URI.create("https://mdseal.dev/api/v1/convert"))
        .header("Authorization", "Bearer " + cle)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(corps))
        .build();

    HttpResponse<byte[]> r = CLIENT.send(requete, HttpResponse.BodyHandlers.ofByteArray());

    // 403 : l'offre ne le permet pas — définitif. 429 : le quota revient — Retry-After dit quand.
    if (r.statusCode() == 403) throw new IllegalStateException("Offre insuffisante.");
    if (r.statusCode() == 429) {
      String delai = r.headers().firstValue("retry-after").orElse("3600");
      throw new IllegalStateException("Quota atteint, réessayer dans " + delai + " s.");
    }
    if (r.statusCode() != 200) throw new IllegalStateException("mdseal " + r.statusCode());
    return r.body();
  }

  /** Échappement JSON minimal — une bibliothèque JSON fait mieux, ceci évite d'en imposer une. */
  private static String json(String s) {
    return '"' + s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + '"';
  }
}

وهذا ما يعطيه ذلك داخل تطبيق يُصدر الفواتير أصلًا:

// Dans une application qui facture déjà : la facture existe, on la scelle avant de l'envoyer.
//
// Le point important n'est pas l'appel — c'est ce qu'on fait du refus. Un 429 n'est pas une
// panne : c'est une file d'attente. Le traiter comme une erreur ferait perdre la facture.

import { sceller } from './mdseal.js';

app.post('/factures/:id/envoyer', async (req, res) => {
  const facture = await depot.lire(req.params.id);

  let pdf;
  try {
    pdf = await sceller(facture.markdown, `Facture ${facture.numero}`);
  } catch (e) {
    if (e.reessayerDans) {
      // Le quota revient : on remet la facture en file plutôt que de la perdre.
      await file.reprendre(facture.id, { dans: e.reessayerDans });
      return res.status(202).json({ etat: 'en attente de quota' });
    }
    throw e;   // 403 ou panne réelle : ça remonte, et quelqu'un doit le voir.
  }

  await courriel.envoyer(facture.client, { piece: pdf, nom: `facture-${facture.numero}.pdf` });
  await depot.marquerEnvoyee(facture.id);
  res.json({ etat: 'envoyée' });
});

المهمّ ليس النداء — بل ما يُفعل بالرفض. الـ429 ليست عطلًا، بل صفّ انتظار.

إشارات نافعة

الخصوصيّة

تُحوَّل الوثائق على هذا الخادم السيّد (صيغة PDF بـ Chrome/Chromium، وصيغة PPTX بـ pandoc): ولا يُرسَل شيء إلى أيّ خدمة خارجيّة.