📄 mdseal

Aide — comment utiliser mdseal

1. Fournir le document

2. Choisir le format de sortie

FormatRésultat
PDF (A4)Document paginé A4, thémé — Markdown ou HTML.
Diaporama PDF (16:9)Une diapo par page ; séparez les diapos par une ligne ---.
PowerPoint (PPTX)Présentation éditable ; attend du Markdown (les titres ## ouvrent les diapos).
Aperçu HTMLAffiche le document thémé dans le navigateur (sans téléchargement).

3. Choisir un thème — palette.amia.fr

Les thèmes proposés (hadhinat, carbon, dark…) sont dérivés de palette.amia.fr — le Palette Studio, la source unique des palettes de l'écosystème @mostajs/*. Chaque palette y est composée à partir de quelques couleurs de marque, puis toutes les couleurs utiles sont dérivées et validées pour le contraste (WCAG) : aucune couleur illisible n'est produite (l'accent ne porte jamais de texte).

Pour ajouter ou ajuster un thème : composez-le sur palette.amia.fr (aperçus PC/mobile, table de contrastes), récupérez ses couleurs de marque, puis ajoutez son entrée au magasin @mostajs/palette (GALLERY). mdseal le proposera automatiquement.

Le thème s'applique au PDF et à l'aperçu HTML. Le PPTX suit un gabarit PowerPoint (pas le CSS).

4. Signer un document, et le protéger

Deux chemins, le même cœur : la case Signer le PDF produit en bas du convertisseur, et l'écran Signer qui prend un PDF déjà fait et le scelle tel quel.

Le certificat

Un certificat PKCS#12 (.p12 / .pfx) et sa phrase secrète. Celui que vous téléversez prime ; à défaut, mdseal emploie celui du serveur s'il est configuré. La clé ne vit qu'en mémoire le temps de la signature : elle n'est ni stockée, ni journalisée, et la phrase secrète ne passe jamais par la ligne de commande du moteur.

La protection — ce qu'elle empêche, et ce qu'elle n'empêche pas

⚠ À lire avant de cocher. Les permissions d'un PDF sont des consignes, pas des verrous. Elles lient les lecteurs qui les respectent — Acrobat et tous les sérieux — et rien d'autre. Un document scellé « ni impression ni copie » s'imprime et se copie sans difficulté avec un outil qui choisit de les ignorer, et le format PDF n'offre aucun moyen de l'empêcher. Ce qui suit décrit ce que mdseal inscrit dans le document, pas ce qu'il garantit.

Cocher Chiffrer (AES-256) et restreindre ajoute au document des permissions, que vous réglez case par case. Le tableau ci-dessous donne l'état par défaut :

PermissionPar défaut
Impression (haute définition)refusée
Impression (basse définition)refusée
Extraction du texte (copier-coller)refusée
Modification du contenurefusée
Annotations et commentairesrefusées
Assemblage (pages)refusé
Lecture par un lecteur d'écranautorisée
Remplissage des champs de formulaireautorisé — et non refusable

Une case cochée veut dire autorisé, jamais « interdit » : c'est le sens du format PDF, et c'est ce qu'affichera un vérificateur — décocher « Impression » fait lire print_highres : False. L'accessibilité reste ouverte à dessein : protéger un texte contre la copie n'est pas une raison de le fermer à qui ne voit pas.

Autoriser l'impression haute définition autorise aussi l'impression simple : le format ignore le bit « haute qualité » quand l'impression est refusée.

⚠ Le remplissage des champs de formulaire ne peut pas être refusé, et ce n'est pas un oubli : en PDF, un champ de signature est un champ de formulaire. Sceller exige donc cette permission. Conséquence à connaître : un PDF qui possédait déjà des champs remplissables les garde remplissables. Le sceau, lui, détecte toute modification — la garantie est là, pas dans la permission.

La DATE — ce qui est attesté, et par qui

Un sceau prouve que le document n'a pas changé. Il ne prouve pas quand il a été scellé : l'heure est, par défaut, un attribut déclaré par le signataire — donc antidatable. C'est pourquoi mdseal fait attester l'instant par un horodateur indépendant (RFC 3161), ce qui fait passer la signature de PAdES-B à PAdES-T.

⚠ Deux niveaux d'horodatage, et l'écart n'est pas de degré. Le règlement européen eIDAS (article 41) distingue :

niveauce que la date vautqui doit prouver
qualifiéprésomption légale d'exactitudecelui qui conteste
simplerecevable, sans présomptioncelui qui produit la pièce
aucundéclarée par le signataire— antidatable

Un horodatage qualifié ne s'obtient qu'auprès d'un prestataire inscrit sur la liste de confiance de l'Union. GET /api/v1/moi rend signature.horodatage.niveau : demandez-le avant de vous engager auprès de vos propres clients, et n'annoncez jamais plus que ce qu'il répond.

⚠ Si l'horodateur ne répond pas, le scellement échoue — il n'y a pas de repli. Un document que vous croiriez horodaté et qui ne l'est pas ne se découvrirait qu'au moment de vous en prévaloir.

Le mot de passe à l'ouverture — le seul verrou dur, et il ne verrouille que l'ouverture

La zone Mot de passe à l'ouverture change la nature de la protection. Renseignée, le document ne s'ouvre dans aucun lecteur sans ce mot de passe : ce n'est plus une consigne qu'on respecte, c'est un chiffrement qu'on ne contourne pas. La laisser vide garde un document librement lisible, dont les restrictions reposent sur la bonne volonté du lecteur.

Saisir un mot de passe active la protection : les permissions affichées s'appliquent. mdseal ne le conserve pas — perdu, le document l'est avec lui. Il n'y a pas de recours, et c'est précisément ce qui fait sa valeur.

⚠ Ce que le mot de passe change, et ce qu'il ne change pas. Sans lui, le document s'ouvre chez tout le monde, et les restrictions reposent sur la bonne volonté du lecteur. Avec lui, personne ne l'ouvre sans le connaître : cette part-là est du vrai chiffrement, et elle ne se contourne pas. Mais une fois le document ouvert, les permissions redeviennent des consignes — qui détient le mot de passe peut extraire le texte et enregistrer une copie sans chiffrement et tout autorisé, en une seule commande. Le mot de passe protège l'accès, jamais l'usage.

La seule question qui vaille est donc « à qui je donne le mot de passe ». Pour empêcher un destinataire légitime d'imprimer ou de copier, le format PDF n'offre rien — ni ici, ni ailleurs, et se méfier de qui l'affirme. Ce qui est réellement opposable dans un document scellé par mdseal, c'est la signature : elle ne cache rien, mais elle prouve que le document n'a pas changé depuis le sceau, et à quelle date.

⚠ La protection exige le moteur pyhanko sur le serveur. Absent, mdseal signe encore, mais refuse la protection et le dit — il ne rendra jamais un document imprimable à qui l'a demandé protégé. La signature, elle, ne demande rien de plus que Node.

Deux façons de sceller — et elles ne prouvent pas la même chose

mdseal scelle de deux manières. La différence n’est pas technique : elle porte sur ce que le document prouve.

Avec le certificat du service — rien à téléverser. Le document est scellé, toute altération le casse, et il porte la date attestée par un horodateur indépendant. Ce qu’il ne prouve pas : votre identité. Le certificat du service n’a été vérifié par aucun tiers, et un lecteur PDF affichera « validité inconnue ».

Avec VOTRE certificat — vous déposez votre fichier .p12 et sa phrase secrète, à l’écran ou par l’API. Le cachet vous est alors attribué, et vous en gardez le contrôle. C’est la voie qui a du poids juridique : c’est votre organisation qui cachette, pas la nôtre.

Quand choisir laquelle ? Pour un document interne, un brouillon, un envoi de convenance : le certificat du service suffit. Pour une attestation, un diplôme, un rapport qu’on opposera à quelqu’un : le vôtre.

Où l’obtenir ? Auprès d’une autorité de certification, pour un certificat de cachet d’organisation — ce n’est pas le même usage qu’un certificat de site web. Comptez quelques centaines d’euros par an et une vérification de l’identité de votre structure.

⚠️ Votre phrase secrète ne sert qu’à ouvrir le fichier le temps du scellement. Ni elle ni le certificat ne sont conservés.

5. Sans navigateur — la même conversion en ligne de commande

Ce site est une interface. Le convertisseur, lui, est un module qui s'installe :

npm i -g @mostajs/doc-export

doc-export rapport.md -o rapport.pdf            # PDF A4 thémé
doc-export cours.md -f slides-pdf -t carbon     # diaporama 16:9
cat doc.md | doc-export -f html -o -            # dans un tuyau
doc-export --capacites                          # ce qui est disponible ici

Même cœur, donc même document : cette page et cette commande appellent le même @mostajs/doc-export. La commande sert ce qu'un navigateur ne sait pas faire — un dossier entier d'un coup, une intégration continue, un poste sans réseau.

⚠ Une seule différence, et elle est voulue. Ce site interroge le Studio en direct ; la commande lit la galerie embarquée dans @mostajs/palette. Un thème retouché ce matin paraît ici tout de suite, et en ligne de commande après mise à jour du module. Le texte est le même ; les couleurs peuvent être d'un jour d'écart.

⚠ PDF ⇒ Chrome/Chromium, PPTX ⇒ pandoc. Absents, la commande le dit et s'arrête : elle ne rend jamais un document dégradé en silence.

La signature suit la même règle et a sa propre commande, pour la même raison — ce site n'a aucun privilège sur elle :

npm i -g @mostajs/pdf-sign

mosta-pdf-sign sign rapport.pdf signe.pdf --p12 cert.p12 --pass ••• --restrict \
                                    --open-password •••     # verrou dur
mosta-pdf-sign verify signe.pdf --trust ca.pem      # intact ? valide ? de confiance ?
mosta-pdf-sign engines                              # ce serveur sait-il protéger ?

6. Depuis un logiciel — l'API HTTP

Les écrans servent une personne. L'API sert un système : une facture à la commande, un rapport chaque nuit, un relevé à chaque signature de contrat. Elle passe par le même moteur que cette page — même thème, même document.

Tout appel présente une clé dans l'en-tête Authorization. Il n'y a pas de route publique, pas de clé dans l'URL : un secret qui voyage dans une adresse se retrouve dans les journaux du serveur, dans l'historique du navigateur et dans le Referer envoyé à des tiers.

AppelCe qu'il fait
GET /api/v1/moiVérifie la clé et dit ce qu'elle porte, ce que ce serveur sait faire, et sa limite de taille. Ne consomme pas de conversion.
POST /api/v1/convertConvertit — et scelle dans le même passage si le bloc sign est fourni.
POST /api/v1/signScelle un PDF déjà fait.

Convertir

curl -X POST https://mdseal.dev/api/v1/convert \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"source":"# Rapport\n\nTexte.","format":"pdf","themeId":"hadhinat"}' \
  -o rapport.pdf
ChampValeursDéfaut
sourcele Markdown ou le HTML — obligatoire—
inputTypemd · htmlmd
formatpdf · slides-pdf · pptx · htmlpdf
themeIdun thème de la liste rendue par /api/v1/moihadhinat
filenamele nom du fichier rendudocument
signbloc de signature (ci-dessous) — exige une clé portant signabsent

Signer, et protéger

Deux entrées, selon d'où l'on appelle — le PDF brut avec les options en paramètres d'URL, ou du JSON avec le PDF en 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

Le bloc sign (dans /convert) et les paramètres (dans /sign) portent les mêmes noms : proteger, motdepasse, permissions, p12 (base64) et passphrase, reason, name, location.

⚠ Deux règles reprises telles quelles des écrans, pour que l'API et le navigateur rendent le même document. Un mot de passe vaut demande de protection : le fournir suffit, sans proteger. Et un bloc permissions fourni fait liste close — ce qui n'y figure pas est refusé, jamais laissé au moteur. Bloc absent, ce sont les valeurs par défaut du tableau du §4 qui s'appliquent.

Les quotas

Deux ressources se comptent, sur deux périodes différentes — et ce n'est pas un caprice. Une conversion est fréquente et bon marché : elle se borne au jour, ce qui protège le serveur sans gêner personne. Une signature est rare, coûteuse à exécuter, et c'est elle qu'on achète : elle se borne au mois, comme le forfait — pour qu'un jour de forte activité ne bloque pas.

PlanConversions / jourSignatures / moisClés
Gratuit20aucune1
Pro5002003
Entreprisesans limite2 00010

GET /api/v1/moi rend quotas.conversion et quotas.signature avec courant, limite et restant : de quoi s'arrêter avant le refus plutôt que de le découvrir au milieu d'une chaîne de traitement. Cet appel ne consomme rien.

Un appel qui échoue n'est pas compté : il n'a rien rendu. Et le quota est vérifié avant le rendu et avant le certificat — ce qui sera refusé à la fin est refusé tout de suite, sans dépenser trois secondes de moteur.

Ce que l'API répond

Un succès rend le fichier, avec son type et son nom. Une erreur rend du JSON — jamais une page HTML : {"error":{"code":"…","message":"…"}}.

CodeCe qu'il veut dire
401 UNAUTHORIZEDClé absente, invalide, révoquée ou expirée. Les quatre se répondent du même mot, à dessein.
403 FORBIDDENLa clé est bonne mais ne porte pas cette opération.
403 PLAN_REQUIREDVotre plan ne comprend pas cette opération. Aucune attente n'y changera rien — c'est pourquoi cette réponse ne porte pas de Retry-After.
429 QUOTA_EXCEEDEDForfait épuisé. La réponse porte Retry-After (secondes) et dit courant, limite et periode.
400 BAD_REQUESTDemande mal formée — le message dit quoi.
413 PAYLOAD_TOO_LARGEDocument au-delà de la limite (voir limiteCorpsMo dans /api/v1/moi). La réponse ferme la connexion : réduisez l'envoi, ne réessayez pas à l'identique.
415Type de contenu non servi par cet appel.
422 SIGNATURE_FAILEDLa conversion a réussi, la signature a échoué — certificat ou phrase secrète.

La signature n'est servie que si ce serveur la porte. /api/v1/moi le dit avant tout appel : signature.disponible, signature.motDePasseOuverture, signature.certificatServeur.

7. Brancher mdseal dans votre logiciel

Trois façons d’appeler la même porte. Les exemples visent ce serveur — copiez-les tels quels.

La clé se crée depuis votre espace et ne s’affiche qu’une fois. Gardez-la dans une variable d’environnement, jamais dans le code : une clé commise dans un dépôt est une clé à révoquer.

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());
}

Deux refus, deux traitements — c’est le point que la plupart des intégrations ratent. 403 veut dire « votre offre ne le permet pas » : réessayer n’y changera rien. 429 veut dire « votre quota est atteint, il revient » : l’en-tête Retry-After dit quand. Confondre les deux fait boucler un client sur un refus définitif, ou abandonner un document qu’il suffisait d’attendre.

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") + '"';
  }
}

Et voici ce que ça donne dans une application qui facture déjà :

// 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' });
});

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.

Astuces

Confidentialité

Les documents sont convertis sur ce serveur souverain (PDF par Chrome/Chromium, PPTX par pandoc) : rien n'est envoyé à un service tiers.