espace-paie-odentas/SIGNATURE_MULTI_PARTIES.md

7.9 KiB

Signatures Multiples avec /Name et /Reason - Odentas Sign

📋 Concept

Quand plusieurs personnes signent un document via Odentas Sign (employeur puis salarié), chaque signature est techniquement signée par Odentas (avec le certificat KMS), mais avec des métadonnées personnalisées indiquant pour qui la signature a été faite.

C'est exactement ce que fait DocuSeal : quand vous ouvrez un PDF dans Adobe, vous voyez "DocuSeal" comme signataire technique, mais les champs /Name et /Reason indiquent le vrai signataire.

🔑 Architecture

Tables Supabase utilisées

Pas besoin de nouvelles tables ! Votre système existant est parfait :

  1. sign_requests : La demande de signature
  2. signers : Les signataires (Employeur + Salarié)
    • NOUVELLES colonnes :
      • signature_name : Nom affiché dans la signature (/Name)
      • signature_reason : Raison de la signature (/Reason)
      • signature_location : Lieu (/Location)
      • signature_contact_info : Contact (/ContactInfo)
  3. sign_positions : Où placer les signatures
  4. sign_events : Audit trail
  5. sign_assets : Document final scellé

Workflow de signature

1. Création de la demande
   ├─ sign_requests (status: pending)
   ├─ signers[0] (Employeur, signature_name: "Entreprise SARL", signature_reason: "Signature employeur")
   └─ signers[1] (Salarié, signature_name: "Jean Dupont", signature_reason: "Signature salarié")

2. Employeur signe (interface graphique)
   ├─ signers[0].signed_at = now()
   └─ signers[0].signature_image_s3 = "signatures/req-123/employeur.png"

3. Salarié signe (interface graphique)
   ├─ signers[1].signed_at = now()
   └─ signers[1].signature_image_s3 = "signatures/req-123/salarie.png"

4. Scellement PAdES (API /seal-document)
   ├─ Lambda sign avec signers[0] → /Name="Entreprise SARL" /Reason="Signature employeur"
   │  └─ PDF devient: signed-pades/contract-123-step1.pdf (1 signature PAdES)
   │
   └─ Lambda sign avec signers[1] → /Name="Jean Dupont" /Reason="Signature salarié"
      └─ PDF final: signed-pades/contract-123-final.pdf (2 signatures PAdES)

5. Résultat
   └─ sign_assets.signed_pdf_s3_key = "signed-pades/contract-123-final.pdf"

🔧 Modifications nécessaires

1. Migration Supabase

Fichier : supabase/migrations/20251029_add_signature_metadata_to_signers.sql

ALTER TABLE signers
ADD COLUMN signature_name TEXT NOT NULL,
ADD COLUMN signature_reason TEXT NOT NULL,
ADD COLUMN signature_location TEXT DEFAULT 'France',
ADD COLUMN signature_contact_info TEXT;

2. Lambda odentas-pades-sign

Input modifié :

{
  "s3Key": "source/contract-123.pdf",
  "signatureMetadata": {
    "name": "Entreprise SARL",           // → /Name dans le dictionnaire de signature
    "reason": "Signature employeur",     // → /Reason
    "location": "Paris, France",         // → /Location
    "contactInfo": "contact@entreprise.com" // → /ContactInfo
  },
  "signerOrder": 1  // 1ère signature ou 2ème
}

Dans la Lambda, lors de la création du dictionnaire de signature :

const signatureDict = {
  Type: 'Sig',
  Filter: 'Adobe.PPKLite',
  SubFilter: 'ETSI.CAdES.detached',
  Name: signatureMetadata.name,        // ← ICI
  Reason: signatureMetadata.reason,    // ← ICI
  Location: signatureMetadata.location, // ← ICI
  ContactInfo: signatureMetadata.contactInfo, // ← ICI
  M: `D:${timestamp}`,
  ByteRange: [0, 0, 0, 0],
  Contents: '<signature_hex>'
};

3. API Route /api/odentas-sign/seal-document

Fichier : app/api/odentas-sign/seal-document/route.ts

Cette route :

  1. Récupère tous les signers ayant signé
  2. Les trie par ordre chronologique (signed_at)
  3. Appelle la Lambda séquentiellement pour chaque signataire
  4. Chaque appel Lambda utilise les métadonnées du signataire (signature_name, signature_reason)
  5. Le PDF de sortie d'une signature devient l'entrée de la suivante
  6. Enregistre le PDF final dans sign_assets

4. Quand créer les signataires

Dans : app/api/odentas-sign/requests/create/route.ts

const signersData = body.signers.map((s) => ({
  request_id: signRequest.id,
  role: s.role,
  name: s.name,
  email: s.email,
  // Nouvelles colonnes
  signature_name: s.name, // ou entreprise.name pour l'employeur
  signature_reason: `Signature du contrat ${signRequest.ref} en tant que ${s.role.toLowerCase()}`,
  signature_location: 'France',
  signature_contact_info: s.email,
}));

📊 Résultat dans Adobe Acrobat

Quand vous ouvrez le PDF final dans Adobe :

┌─────────────────────────────────────────┐
│ Signatures du document (2)              │
├─────────────────────────────────────────┤
│ ✓ Odentas Media SAS                     │ ← Certificat technique
│   Nom: Entreprise SARL                  │ ← /Name
│   Raison: Signature employeur           │ ← /Reason
│   Lieu: Paris, France                   │ ← /Location
│   Contact: contact@entreprise.com       │ ← /ContactInfo
│   Date: 29/10/2025 14:30:00            │
├─────────────────────────────────────────┤
│ ✓ Odentas Media SAS                     │ ← Même certificat
│   Nom: Jean Dupont                      │ ← /Name différent
│   Raison: Signature salarié             │ ← /Reason différent
│   Lieu: France                          │
│   Contact: jean.dupont@example.com      │
│   Date: 29/10/2025 15:45:00            │
└─────────────────────────────────────────┘

🔄 Flux complet

sequenceDiagram
    participant UI as Interface Web
    participant API as API Odentas Sign
    participant DB as Supabase
    participant Lambda as Lambda PAdES Sign
    participant S3 as AWS S3

    UI->>API: POST /requests/create
    API->>DB: INSERT sign_requests
    API->>DB: INSERT signers (avec signature_name/reason)
    API->>S3: Upload PDF source
    
    UI->>API: Employeur signe (interface)
    API->>DB: UPDATE signers[0].signed_at
    API->>S3: Upload signature image
    
    UI->>API: Salarié signe (interface)
    API->>DB: UPDATE signers[1].signed_at
    API->>S3: Upload signature image
    
    UI->>API: POST /seal-document
    API->>DB: SELECT signers ORDER BY signed_at
    
    loop Pour chaque signataire
        API->>Lambda: Invoke avec signature_metadata
        Lambda->>S3: GET PDF (source ou précédent)
        Lambda->>Lambda: Ajouter signature PAdES
        Lambda->>S3: PUT PDF signé
        Lambda-->>API: signedS3Key
    end
    
    API->>DB: INSERT sign_assets (PDF final)
    API->>DB: UPDATE sign_requests (status: completed)
    API-->>UI: Document scellé

Avantages

  1. Pas de nouvelles tables : Réutilise signers existant
  2. Flexible : Peut gérer 2, 3 ou N signatures
  3. Conforme PAdES : Chaque signature est indépendante et valide
  4. Audit trail : sign_events trace tout le processus
  5. Compatible Adobe : Affiche correctement les noms dans Adobe Acrobat
  6. Identique DocuSeal : Même principe technique

🎯 Prochaines étapes

  1. Appliquer la migration Supabase
  2. Modifier la Lambda pour accepter signatureMetadata
  3. Tester avec un contrat CDDU
  4. Vérifier dans Adobe que /Name et /Reason apparaissent correctement
  5. Intégrer l'appel à /seal-document dans votre workflow existant

📝 Notes

  • Le certificat est toujours Odentas (émis par AWS KMS)
  • Les métadonnées changent pour chaque signataire
  • Les signatures sont appliquées séquentiellement (importante pour la validité)
  • Chaque signature englobe la précédente (signature incrémentale PAdES)