@mostajs/pdf-sign 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +101 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +78 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config.d.ts +14 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +44 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/engines.d.ts +11 -0
  12. package/dist/engines.d.ts.map +1 -0
  13. package/dist/engines.js +39 -0
  14. package/dist/engines.js.map +1 -0
  15. package/dist/index.d.ts +15 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +14 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/node-signer.d.ts +4 -0
  20. package/dist/node-signer.d.ts.map +1 -0
  21. package/dist/node-signer.js +35 -0
  22. package/dist/node-signer.js.map +1 -0
  23. package/dist/pyhanko-signer.d.ts +4 -0
  24. package/dist/pyhanko-signer.d.ts.map +1 -0
  25. package/dist/pyhanko-signer.js +59 -0
  26. package/dist/pyhanko-signer.js.map +1 -0
  27. package/dist/sign.d.ts +13 -0
  28. package/dist/sign.d.ts.map +1 -0
  29. package/dist/sign.js +35 -0
  30. package/dist/sign.js.map +1 -0
  31. package/dist/types.d.ts +79 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +8 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/verify.d.ts +3 -0
  36. package/dist/verify.d.ts.map +1 -0
  37. package/dist/verify.js +36 -0
  38. package/dist/verify.js.map +1 -0
  39. package/examples/sign-basic.mjs +28 -0
  40. package/examples/sign-restricted.mjs +33 -0
  41. package/examples/sign-stamped.mjs +28 -0
  42. package/llms.txt +57 -0
  43. package/package.json +60 -0
  44. package/python/requirements.txt +3 -0
  45. package/python/setup-venv.sh +9 -0
  46. package/python/sign_with_perms.py +173 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog — @mostajs/pdf-sign
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
+
5
+ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/) ; versionnage [SemVer](https://semver.org/lang/fr/).
6
+
7
+ ## [0.1.0] — 2026-07-01
8
+
9
+ ### Ajouté
10
+
11
+ - Moteur **hybride** de signature PDF : cœur Node (PKCS#7 détaché via `@signpdf`) + bascule pyhanko (PAdES visible + permissions AES-256).
12
+ - API : `signPdf`, `verifyPdf`, `detectEngines`, `resolvePython`, `pyhankoAvailable`, `loadPdfSignConfig`.
13
+ - CLI `mosta-pdf-sign` : `sign`, `verify`, `engines`.
14
+ - Configuration 100 % `.env` via `@mostajs/config` (préfixe `PDF_SIGN_*`).
15
+ - Script Python embarqué `python/sign_with_perms.py` (portage générique et paramétré du pipeline `pyhanko + pikepdf`), + `setup-venv.sh` et `requirements.txt`.
16
+ - Cachet visible « image de fond + texte » (logo/tampon) via `field.stampImage` / `PDF_SIGN_STAMP_IMAGE` (moteur pyhanko, `stamp.TextStampStyle`).
17
+ - Vérification : validation cryptographique pyhanko si disponible (authentification automatique du mot de passe utilisateur vide sur PDF chiffré, nom du signataire lisible), sinon détection Node (`/ByteRange`).
18
+ - Tests `@mostajs/mjs-unit` et exemples `examples/*.mjs`.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # @mostajs/pdf-sign
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
+ **Licence** : AGPL-3.0-or-later
5
+
6
+ Signature numérique de PDF pour l'écosystème `@mostajs/*`. Moteur **hybride** :
7
+
8
+ - **cœur Node** — signature PKCS#7 détachée via [`@signpdf`](https://github.com/vbuch/node-signpdf), cachet invisible, **aucune dépendance Python** ;
9
+ - **bascule pyhanko** — signature **PAdES** visible + **permissions restrictives** (chiffrement AES-256, R=6) quand un venv Python est disponible.
10
+
11
+ Configuration **100 % `.env`** (via `@mostajs/config`), API + CLI. Portage générique et paramétré du pipeline `pyhanko + pikepdf` éprouvé.
12
+
13
+ ## Pourquoi — valeur probante (SIH & documents officiels)
14
+
15
+ La signature électronique de PDF apporte la **preuve médico-légale** et la **valeur probante** exigées par l'interopérabilité et la sécurité d'un Système d'Information Hospitalier :
16
+
17
+ | Domaine | Documents |
18
+ |---|---|
19
+ | Clinique | CRH, CRO, comptes rendus d'imagerie / biologie signés par le praticien |
20
+ | Administratif | certificats de séjour, ordonnances, dossiers de sortie |
21
+ | Financier / CNAS-CASNOS | factures, bordereaux de remboursement, prises en charge |
22
+
23
+ Le module reste **générique** : il ne connaît aucun métier. La sémantique (quel document, quel signataire) est portée par l'appelant.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pnpm add @mostajs/pdf-sign
29
+ # Moteur pyhanko (optionnel : PAdES + permissions) :
30
+ bash node_modules/@mostajs/pdf-sign/python/setup-venv.sh
31
+ export PDF_SIGN_PYTHON="$(pwd)/node_modules/@mostajs/pdf-sign/python/.venv/bin/python"
32
+ ```
33
+
34
+ ## API
35
+
36
+ ```js
37
+ import { signPdf, verifyPdf, detectEngines } from "@mostajs/pdf-sign";
38
+
39
+ // Signature (auto : pyhanko si dispo, sinon Node)
40
+ await signPdf({
41
+ input: "crh.pdf",
42
+ output: "crh.signe.pdf",
43
+ p12: "praticien.p12",
44
+ passphrase: process.env.P12_PASS,
45
+ meta: { reason: "Compte rendu d'hospitalisation", name: "Dr H. Madani", location: "Alger, DZ" },
46
+ });
47
+
48
+ // Signature restreinte (PAdES + chiffrement AES-256 — exige pyhanko)
49
+ await signPdf({
50
+ input: "facture-cnas.pdf",
51
+ output: "facture-cnas.signe.pdf",
52
+ p12: "etablissement.p12",
53
+ passphrase: process.env.P12_PASS,
54
+ permissions: { extract: false, modifyOther: false, printHighres: true },
55
+ field: { page: 1, box: [320, 40, 575, 140], stampImage: "logo-chu.png" }, // cachet visible image + texte
56
+ engine: "pyhanko",
57
+ });
58
+
59
+ // Vérification
60
+ const r = verifyPdf({ input: "crh.signe.pdf", trustPem: "ac-racine.pem" });
61
+ console.log(r.signed, r.signatures);
62
+
63
+ // État des moteurs
64
+ console.log(detectEngines());
65
+ ```
66
+
67
+ ## CLI
68
+
69
+ ```bash
70
+ mosta-pdf-sign sign in.pdf out.pdf --p12 cert.p12 --pass secret \
71
+ --reason "Ordonnance" --name "Dr H. Madani" --location "Alger, DZ"
72
+ mosta-pdf-sign sign in.pdf out.pdf --p12 cert.p12 --pass secret --restrict --engine pyhanko
73
+ mosta-pdf-sign verify out.pdf --trust ac-racine.pem
74
+ mosta-pdf-sign engines
75
+ ```
76
+
77
+ ## Configuration (`.env`)
78
+
79
+ | Clé | Rôle |
80
+ |---|---|
81
+ | `PDF_SIGN_P12` / `PDF_SIGN_PASSPHRASE` / `PDF_SIGN_PASSPHRASE_FILE` | certificat PKCS#12 et secret |
82
+ | `PDF_SIGN_ENGINE` | `auto` (défaut) · `node` · `pyhanko` |
83
+ | `PDF_SIGN_PYTHON` | binaire Python du venv pyhanko |
84
+ | `PDF_SIGN_REASON` / `_LOCATION` / `_NAME` / `_CONTACT` | métadonnées du cachet |
85
+ | `PDF_SIGN_FIELD_PAGE` / `PDF_SIGN_FIELD_BOX` | page (0-based) et boîte `x1,y1,x2,y2` (points, origine bas-gauche) |
86
+ | `PDF_SIGN_STAMP_IMAGE` | image de fond du cachet visible (logo/tampon derrière le texte ; moteur pyhanko) |
87
+ | `PDF_SIGN_RESTRICT` | `true` → permissions restrictives (pyhanko) |
88
+ | `PDF_SIGN_TRUST_PEM` | certificat de confiance pour la vérification |
89
+
90
+ ## Moteurs
91
+
92
+ | Moteur | PKCS#7 | PAdES visible | Permissions AES-256 | Dépendance |
93
+ |---|:---:|:---:|:---:|---|
94
+ | `node` | ✅ | ❌ | ❌ | aucune (JS pur) |
95
+ | `pyhanko` | ✅ | ✅ | ✅ | venv `pyhanko>=0.25`, `pikepdf>=8` |
96
+
97
+ Le cœur Node **lève une erreur** si des permissions sont demandées : le PAdES et le chiffrement exigent pyhanko.
98
+
99
+ ## Voir aussi
100
+
101
+ - `@mostajs/pdf-sign-ui` — couche de présentation (formulaire de signature, vérification, headless + composants).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @mostajs/pdf-sign — CLI. `mosta-pdf-sign sign|verify|engines`.
4
+ *
5
+ * @author Dr Hamid MADANI <drmdh@msn.com>
6
+ * @license AGPL-3.0-or-later
7
+ */
8
+ import { signPdf } from "./sign.js";
9
+ import { verifyPdf } from "./verify.js";
10
+ import { detectEngines } from "./engines.js";
11
+ function opt(argv, name) {
12
+ const i = argv.indexOf(`--${name}`);
13
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined;
14
+ }
15
+ function flag(argv, name) {
16
+ return argv.includes(`--${name}`);
17
+ }
18
+ const HELP = `mosta-pdf-sign — signature numérique de PDF (moteur hybride Node + pyhanko)
19
+
20
+ mosta-pdf-sign sign <in.pdf> [out.pdf] [options]
21
+ --p12 <fichier> certificat PKCS#12 (ou PDF_SIGN_P12)
22
+ --pass <str> | --pass-file <fichier>
23
+ --engine auto|node|pyhanko (défaut auto)
24
+ --restrict permissions restrictives (PAdES pyhanko requis)
25
+ --reason --name --location --contact
26
+ mosta-pdf-sign verify <pdf> [--trust cert.pem] [--engine ...]
27
+ mosta-pdf-sign engines état des moteurs disponibles
28
+ `;
29
+ async function main() {
30
+ const argv = process.argv.slice(2);
31
+ const cmd = argv[0];
32
+ const rest = argv.slice(1).filter((a) => !a.startsWith("--"));
33
+ if (!cmd || cmd === "--help" || cmd === "-h") {
34
+ process.stdout.write(HELP);
35
+ return;
36
+ }
37
+ if (cmd === "engines") {
38
+ console.log(JSON.stringify(detectEngines(), null, 2));
39
+ return;
40
+ }
41
+ if (cmd === "sign") {
42
+ const input = rest[0];
43
+ const output = rest[1];
44
+ if (!input) {
45
+ process.stderr.write("Entrée PDF manquante.\n" + HELP);
46
+ process.exit(1);
47
+ }
48
+ const res = await signPdf({
49
+ input,
50
+ output,
51
+ p12: opt(argv, "p12"),
52
+ passphrase: opt(argv, "pass"),
53
+ passphraseFile: opt(argv, "pass-file"),
54
+ engine: opt(argv, "engine"),
55
+ permissions: flag(argv, "restrict") ? {} : undefined,
56
+ meta: {
57
+ reason: opt(argv, "reason"), name: opt(argv, "name"),
58
+ location: opt(argv, "location"), contact: opt(argv, "contact"),
59
+ },
60
+ });
61
+ console.log(`OK — signé (${res.engine}) : ${res.output ?? "<buffer>"}`);
62
+ return;
63
+ }
64
+ if (cmd === "verify") {
65
+ const input = rest[0];
66
+ if (!input) {
67
+ process.stderr.write("PDF à vérifier manquant.\n");
68
+ process.exit(1);
69
+ }
70
+ const res = verifyPdf({ input, trustPem: opt(argv, "trust"), engine: opt(argv, "engine") });
71
+ console.log(JSON.stringify(res, null, 2));
72
+ return;
73
+ }
74
+ process.stderr.write(`Commande inconnue : ${cmd}\n` + HELP);
75
+ process.exit(1);
76
+ }
77
+ main().catch((e) => { process.stderr.write(String(e?.message ?? e) + "\n"); process.exit(1); });
78
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { SignEngine, SignField, SignMeta } from "./types.js";
2
+ export interface PdfSignConfig {
3
+ p12?: string;
4
+ passphrase?: string;
5
+ passphraseFile?: string;
6
+ engine: SignEngine;
7
+ python?: string;
8
+ meta: SignMeta;
9
+ field: SignField;
10
+ restrict: boolean;
11
+ trustPem?: string;
12
+ }
13
+ export declare function loadPdfSignConfig(): PdfSignConfig;
14
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAOD,wBAAgB,iBAAiB,IAAI,aAAa,CAwBjD"}
package/dist/config.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @mostajs/pdf-sign — configuration via @mostajs/config (.env). Rien en dur.
3
+ *
4
+ * Clés :
5
+ * PDF_SIGN_P12 / PDF_SIGN_PASSPHRASE / PDF_SIGN_PASSPHRASE_FILE
6
+ * PDF_SIGN_ENGINE (auto|node|pyhanko) · PDF_SIGN_PYTHON (bin du venv pyhanko)
7
+ * PDF_SIGN_REASON / PDF_SIGN_LOCATION / PDF_SIGN_NAME / PDF_SIGN_CONTACT
8
+ * PDF_SIGN_FIELD_PAGE / PDF_SIGN_FIELD_BOX ("x1,y1,x2,y2") · PDF_SIGN_STAMP_IMAGE (logo du cachet) · PDF_SIGN_RESTRICT (bool)
9
+ * PDF_SIGN_TRUST_PEM (validation)
10
+ *
11
+ * @author Dr Hamid MADANI <drmdh@msn.com>
12
+ * @license AGPL-3.0-or-later
13
+ */
14
+ import { getEnv, getEnvBool } from "@mostajs/config";
15
+ function parseBox(s) {
16
+ const n = s.split(",").map((x) => Number(x.trim()));
17
+ return n.length === 4 && n.every((v) => !Number.isNaN(v)) ? n : undefined;
18
+ }
19
+ export function loadPdfSignConfig() {
20
+ const engine = getEnv("PDF_SIGN_ENGINE", "auto");
21
+ const box = getEnv("PDF_SIGN_FIELD_BOX", "");
22
+ const page = getEnv("PDF_SIGN_FIELD_PAGE", "");
23
+ return {
24
+ p12: getEnv("PDF_SIGN_P12", "") || undefined,
25
+ passphrase: getEnv("PDF_SIGN_PASSPHRASE", "") || undefined,
26
+ passphraseFile: getEnv("PDF_SIGN_PASSPHRASE_FILE", "") || undefined,
27
+ engine: (["auto", "node", "pyhanko"].includes(engine) ? engine : "auto"),
28
+ python: getEnv("PDF_SIGN_PYTHON", "") || undefined,
29
+ meta: {
30
+ reason: getEnv("PDF_SIGN_REASON", "") || undefined,
31
+ location: getEnv("PDF_SIGN_LOCATION", "") || undefined,
32
+ name: getEnv("PDF_SIGN_NAME", "") || undefined,
33
+ contact: getEnv("PDF_SIGN_CONTACT", "") || undefined,
34
+ },
35
+ field: {
36
+ page: page ? Number(page) : undefined,
37
+ box: box ? parseBox(box) : undefined,
38
+ stampImage: getEnv("PDF_SIGN_STAMP_IMAGE", "") || undefined,
39
+ },
40
+ restrict: getEnvBool("PDF_SIGN_RESTRICT", false),
41
+ trustPem: getEnv("PDF_SIGN_TRUST_PEM", "") || undefined,
42
+ };
43
+ }
44
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAerD,SAAS,QAAQ,CAAC,CAAS;IACzB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAsC,CAAC,CAAC,CAAC,SAAS,CAAC;AAClH,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;IAC/C,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,cAAc,EAAE,EAAE,CAAC,IAAI,SAAS;QAC5C,UAAU,EAAE,MAAM,CAAC,qBAAqB,EAAE,EAAE,CAAC,IAAI,SAAS;QAC1D,cAAc,EAAE,MAAM,CAAC,0BAA0B,EAAE,EAAE,CAAC,IAAI,SAAS;QACnE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAe;QACtF,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,EAAE,CAAC,IAAI,SAAS;QAClD,IAAI,EAAE;YACJ,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,EAAE,CAAC,IAAI,SAAS;YAClD,QAAQ,EAAE,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,IAAI,SAAS;YACtD,IAAI,EAAE,MAAM,CAAC,eAAe,EAAE,EAAE,CAAC,IAAI,SAAS;YAC9C,OAAO,EAAE,MAAM,CAAC,kBAAkB,EAAE,EAAE,CAAC,IAAI,SAAS;SACrD;QACD,KAAK,EAAE;YACL,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;YACrC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;YACpC,UAAU,EAAE,MAAM,CAAC,sBAAsB,EAAE,EAAE,CAAC,IAAI,SAAS;SAC5D;QACD,QAAQ,EAAE,UAAU,CAAC,mBAAmB,EAAE,KAAK,CAAC;QAChD,QAAQ,EAAE,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,IAAI,SAAS;KACxD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { EngineAvailability } from "./types.js";
2
+ /** Répertoire des scripts Python embarqués (proven pipeline). */
3
+ export declare const PYTHON_DIR: string;
4
+ export declare const SIGN_PY: string;
5
+ /** Résout le binaire Python : explicite > venv local du module > python3. */
6
+ export declare function resolvePython(explicit?: string): string;
7
+ /** Vrai si ce Python importe pyhanko + pikepdf (moteur PAdES + permissions). */
8
+ export declare function pyhankoAvailable(python: string): boolean;
9
+ /** État des moteurs disponibles. */
10
+ export declare function detectEngines(explicitPython?: string): EngineAvailability;
11
+ //# sourceMappingURL=engines.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engines.d.ts","sourceRoot":"","sources":["../src/engines.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGrD,iEAAiE;AACjE,eAAO,MAAM,UAAU,QAAgC,CAAC;AACxD,eAAO,MAAM,OAAO,QAA4C,CAAC;AAGjE,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAOxD;AAED,oCAAoC;AACpC,wBAAgB,aAAa,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAGzE"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @mostajs/pdf-sign — détection des moteurs (Node toujours, pyhanko si venv/deps).
3
+ *
4
+ * @author Dr Hamid MADANI <drmdh@msn.com>
5
+ * @license AGPL-3.0-or-later
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+ import { dirname, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ const HERE = dirname(fileURLToPath(import.meta.url)); // dist/
12
+ /** Répertoire des scripts Python embarqués (proven pipeline). */
13
+ export const PYTHON_DIR = resolve(HERE, "..", "python");
14
+ export const SIGN_PY = resolve(PYTHON_DIR, "sign_with_perms.py");
15
+ const VENV_PY = resolve(PYTHON_DIR, ".venv", "bin", "python");
16
+ /** Résout le binaire Python : explicite > venv local du module > python3. */
17
+ export function resolvePython(explicit) {
18
+ if (explicit && existsSync(explicit))
19
+ return explicit;
20
+ if (existsSync(VENV_PY))
21
+ return VENV_PY;
22
+ return "python3";
23
+ }
24
+ /** Vrai si ce Python importe pyhanko + pikepdf (moteur PAdES + permissions). */
25
+ export function pyhankoAvailable(python) {
26
+ try {
27
+ execFileSync(python, ["-c", "import pyhanko, pikepdf"], { stdio: "ignore" });
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /** État des moteurs disponibles. */
35
+ export function detectEngines(explicitPython) {
36
+ const python = resolvePython(explicitPython);
37
+ return { node: true, pyhanko: pyhankoAvailable(python), python };
38
+ }
39
+ //# sourceMappingURL=engines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engines.js","sourceRoot":"","sources":["../src/engines.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ;AAC9D,iEAAiE;AACjE,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC;AACjE,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAE9D,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,QAAiB;IAC7C,IAAI,QAAQ,IAAI,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IACtD,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACxC,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,IAAI,CAAC;QACH,YAAY,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,yBAAyB,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7E,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,oCAAoC;AACpC,MAAM,UAAU,aAAa,CAAC,cAAuB;IACnD,MAAM,MAAM,GAAG,aAAa,CAAC,cAAc,CAAC,CAAC;IAC7C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;AACnE,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @mostajs/pdf-sign — signature numérique de PDF, moteur hybride (Node + pyhanko).
3
+ *
4
+ * Cœur Node (PKCS#7 via @signpdf) + bascule pyhanko (PAdES + permissions AES-256) si
5
+ * disponible. Réutilise le pipeline éprouvé pyhanko+pikepdf. Config 100% `.env`.
6
+ *
7
+ * @author Dr Hamid MADANI <drmdh@msn.com>
8
+ * @license AGPL-3.0-or-later
9
+ */
10
+ export { signPdf } from "./sign.js";
11
+ export { verifyPdf } from "./verify.js";
12
+ export { detectEngines, resolvePython, pyhankoAvailable } from "./engines.js";
13
+ export { loadPdfSignConfig, type PdfSignConfig } from "./config.js";
14
+ export type { SignOptions, SignResult, SignMeta, SignField, SignPermissions, SignEngine, VerifyOptions, VerifyResult, SignatureInfo, EngineAvailability, } from "./types.js";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EACV,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EACzE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,kBAAkB,GAC/D,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @mostajs/pdf-sign — signature numérique de PDF, moteur hybride (Node + pyhanko).
3
+ *
4
+ * Cœur Node (PKCS#7 via @signpdf) + bascule pyhanko (PAdES + permissions AES-256) si
5
+ * disponible. Réutilise le pipeline éprouvé pyhanko+pikepdf. Config 100% `.env`.
6
+ *
7
+ * @author Dr Hamid MADANI <drmdh@msn.com>
8
+ * @license AGPL-3.0-or-later
9
+ */
10
+ export { signPdf } from "./sign.js";
11
+ export { verifyPdf } from "./verify.js";
12
+ export { detectEngines, resolvePython, pyhankoAvailable } from "./engines.js";
13
+ export { loadPdfSignConfig } from "./config.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAsB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { SignOptions, SignResult } from "./types.js";
2
+ /** Signe un PDF côté Node (PKCS#7 détaché, cachet invisible). */
3
+ export declare function signNode(opts: SignOptions): Promise<SignResult>;
4
+ //# sourceMappingURL=node-signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-signer.d.ts","sourceRoot":"","sources":["../src/node-signer.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1D,iEAAiE;AACjE,wBAAsB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuBrE"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @mostajs/pdf-sign — cœur Node (signature PKCS#7 détachée via @signpdf).
3
+ *
4
+ * Ne gère PAS les permissions/chiffrement ni le PAdES/LTV : pour cela, moteur pyhanko.
5
+ *
6
+ * @author Dr Hamid MADANI <drmdh@msn.com>
7
+ * @license AGPL-3.0-or-later
8
+ */
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { SignPdf } from "@signpdf/signpdf";
11
+ import { P12Signer } from "@signpdf/signer-p12";
12
+ import { plainAddPlaceholder } from "@signpdf/placeholder-plain";
13
+ /** Signe un PDF côté Node (PKCS#7 détaché, cachet invisible). */
14
+ export async function signNode(opts) {
15
+ if (opts.permissions) {
16
+ throw new Error("Le cœur Node ne gère pas les permissions/PAdES — utilisez engine:'pyhanko' (python/setup-venv.sh).");
17
+ }
18
+ const pdfBuffer = typeof opts.input === "string" ? readFileSync(opts.input) : Buffer.from(opts.input);
19
+ const p12Buffer = typeof opts.p12 === "string" ? readFileSync(opts.p12) : Buffer.from(opts.p12);
20
+ const withPlaceholder = plainAddPlaceholder({
21
+ pdfBuffer,
22
+ reason: opts.meta?.reason ?? "Signature",
23
+ contactInfo: opts.meta?.contact ?? "",
24
+ name: opts.meta?.name ?? "",
25
+ location: opts.meta?.location ?? "",
26
+ });
27
+ const signer = new P12Signer(p12Buffer, { passphrase: opts.passphrase ?? "" });
28
+ const signed = await new SignPdf().sign(withPlaceholder, signer);
29
+ if (opts.output) {
30
+ writeFileSync(opts.output, signed);
31
+ return { output: opts.output, engine: "node" };
32
+ }
33
+ return { bytes: new Uint8Array(signed), engine: "node" };
34
+ }
35
+ //# sourceMappingURL=node-signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-signer.js","sourceRoot":"","sources":["../src/node-signer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGjE,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAiB;IAC9C,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,oGAAoG,CAAC,CAAC;IACxH,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtG,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEhG,MAAM,eAAe,GAAG,mBAAmB,CAAC;QAC1C,SAAS;QACT,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,WAAW;QACxC,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE;QACrC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE;QAC3B,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE;KACpC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,SAAS,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAEjE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACnC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { SignOptions, SignResult } from "./types.js";
2
+ /** Signe via pyhanko (PAdES visible + éventuelles permissions restrictives). */
3
+ export declare function signPyhanko(opts: SignOptions, python: string): SignResult;
4
+ //# sourceMappingURL=pyhanko-signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pyhanko-signer.d.ts","sourceRoot":"","sources":["../src/pyhanko-signer.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAmB,UAAU,EAAE,MAAM,YAAY,CAAC;AAqB3E,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CA8BzE"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @mostajs/pdf-sign — moteur pyhanko (PAdES + permissions AES-256), via le script Python embarqué.
3
+ *
4
+ * @author Dr Hamid MADANI <drmdh@msn.com>
5
+ * @license AGPL-3.0-or-later
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { SIGN_PY } from "./engines.js";
12
+ function toFile(dir, name, data) {
13
+ const p = join(dir, name);
14
+ writeFileSync(p, data);
15
+ return p;
16
+ }
17
+ function mapPermissions(p) {
18
+ return {
19
+ accessibility: p.accessibility ?? true,
20
+ extract: p.extract ?? false,
21
+ modify_annotation: p.modifyAnnotation ?? false,
22
+ modify_assembly: p.modifyAssembly ?? false,
23
+ modify_other: p.modifyOther ?? false,
24
+ print_lowres: p.printLowres ?? true,
25
+ print_highres: p.printHighres ?? true,
26
+ };
27
+ }
28
+ /** Signe via pyhanko (PAdES visible + éventuelles permissions restrictives). */
29
+ export function signPyhanko(opts, python) {
30
+ const dir = mkdtempSync(join(tmpdir(), "mosta-pdf-sign-"));
31
+ try {
32
+ const inPath = typeof opts.input === "string" ? opts.input : toFile(dir, "in.pdf", opts.input);
33
+ const p12Path = typeof opts.p12 === "string" ? opts.p12 : toFile(dir, "cert.p12", opts.p12);
34
+ const outPath = opts.output ?? join(dir, "out.pdf");
35
+ let passFile = opts.passphraseFile;
36
+ if (!passFile)
37
+ passFile = toFile(dir, ".pass", opts.passphrase ?? "");
38
+ const perms = opts.permissions ? mapPermissions(opts.permissions) : null;
39
+ const cfg = {
40
+ meta: opts.meta ?? {},
41
+ field: {
42
+ name: opts.field?.name,
43
+ page: opts.field?.page ?? 0,
44
+ box: opts.field?.box ?? [320, 40, 575, 140],
45
+ stamp_image: opts.field?.stampImage,
46
+ },
47
+ permissions: perms,
48
+ };
49
+ const cfgPath = toFile(dir, "config.json", JSON.stringify(cfg));
50
+ execFileSync(python, [SIGN_PY, "sign", inPath, outPath, p12Path, passFile, cfgPath], { stdio: "inherit" });
51
+ if (opts.output)
52
+ return { output: outPath, engine: "pyhanko" };
53
+ return { bytes: readFileSync(outPath), engine: "pyhanko" };
54
+ }
55
+ finally {
56
+ rmSync(dir, { recursive: true, force: true });
57
+ }
58
+ }
59
+ //# sourceMappingURL=pyhanko-signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pyhanko-signer.js","sourceRoot":"","sources":["../src/pyhanko-signer.ts"],"names":[],"mappings":"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"}
package/dist/sign.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @mostajs/pdf-sign — dispatcher HYBRIDE.
3
+ *
4
+ * `auto` (défaut) : pyhanko si disponible (PAdES + permissions), sinon cœur Node.
5
+ * `node` : force PKCS#7 Node. `pyhanko` : force PAdES (erreur si venv absent).
6
+ * Fusionne les options avec la config `.env`.
7
+ *
8
+ * @author Dr Hamid MADANI <drmdh@msn.com>
9
+ * @license AGPL-3.0-or-later
10
+ */
11
+ import type { SignOptions, SignResult } from "./types.js";
12
+ export declare function signPdf(opts: SignOptions): Promise<SignResult>;
13
+ //# sourceMappingURL=sign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM1D,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CA8BpE"}
package/dist/sign.js ADDED
@@ -0,0 +1,35 @@
1
+ import { detectEngines } from "./engines.js";
2
+ import { signNode } from "./node-signer.js";
3
+ import { signPyhanko } from "./pyhanko-signer.js";
4
+ import { loadPdfSignConfig } from "./config.js";
5
+ export async function signPdf(opts) {
6
+ const cfg = loadPdfSignConfig();
7
+ const merged = {
8
+ ...opts,
9
+ p12: opts.p12 ?? cfg.p12,
10
+ passphrase: opts.passphrase ?? cfg.passphrase,
11
+ passphraseFile: opts.passphraseFile ?? cfg.passphraseFile,
12
+ meta: { ...cfg.meta, ...opts.meta },
13
+ field: { ...cfg.field, ...opts.field },
14
+ permissions: opts.permissions ?? (cfg.restrict ? {} : undefined),
15
+ engine: opts.engine ?? cfg.engine,
16
+ };
17
+ if (!merged.p12)
18
+ throw new Error("Certificat PKCS#12 manquant (opts.p12 ou PDF_SIGN_P12).");
19
+ const avail = detectEngines(cfg.python);
20
+ const want = merged.engine ?? "auto";
21
+ let engine;
22
+ if (want === "pyhanko") {
23
+ if (!avail.pyhanko)
24
+ throw new Error("Moteur pyhanko indisponible — lancer python/setup-venv.sh (ou PDF_SIGN_PYTHON).");
25
+ engine = "pyhanko";
26
+ }
27
+ else if (want === "node") {
28
+ engine = "node";
29
+ }
30
+ else {
31
+ engine = avail.pyhanko ? "pyhanko" : "node"; // auto : préfère pyhanko
32
+ }
33
+ return engine === "pyhanko" ? signPyhanko(merged, avail.python) : signNode(merged);
34
+ }
35
+ //# sourceMappingURL=sign.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign.js","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAiB;IAC7C,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAEhC,MAAM,MAAM,GAAgB;QAC1B,GAAG,IAAI;QACP,GAAG,EAAE,IAAI,CAAC,GAAG,IAAK,GAAG,CAAC,GAAc;QACpC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,UAAU;QAC7C,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,GAAG,CAAC,cAAc;QACzD,IAAI,EAAE,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE;QACnC,KAAK,EAAE,EAAE,GAAG,GAAG,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE;QACtC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QAChE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM;KAClC,CAAC;IAEF,IAAI,CAAC,MAAM,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAE5F,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC;IAErC,IAAI,MAA0B,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,iFAAiF,CAAC,CAAC;QACvH,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;SAAM,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QAC3B,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,yBAAyB;IACxE,CAAC;IAED,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,MAAgB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC/F,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @mostajs/pdf-sign — types publics.
3
+ *
4
+ * @author Dr Hamid MADANI <drmdh@msn.com>
5
+ * @license AGPL-3.0-or-later
6
+ */
7
+ /** Métadonnées de la signature (motif, lieu, signataire). */
8
+ export interface SignMeta {
9
+ reason?: string;
10
+ location?: string;
11
+ name?: string;
12
+ contact?: string;
13
+ }
14
+ /** Position du cachet de signature visible. Boîte en points PDF (origine bas-gauche). */
15
+ export interface SignField {
16
+ name?: string;
17
+ /** Index de page 0-based (0 = page 1). */
18
+ page?: number;
19
+ /** [x1, y1, x2, y2] en points (A4 = 595×842). */
20
+ box?: [number, number, number, number];
21
+ /** Image de fond du cachet visible (chemin ; logo/tampon derrière le texte). Moteur pyhanko. */
22
+ stampImage?: string;
23
+ }
24
+ /** Permissions restrictives (chiffrement AES-256). Requiert le moteur pyhanko. */
25
+ export interface SignPermissions {
26
+ extract?: boolean;
27
+ modifyAnnotation?: boolean;
28
+ modifyAssembly?: boolean;
29
+ modifyOther?: boolean;
30
+ printLowres?: boolean;
31
+ printHighres?: boolean;
32
+ accessibility?: boolean;
33
+ }
34
+ export type SignEngine = "auto" | "node" | "pyhanko";
35
+ export interface SignOptions {
36
+ /** PDF source : chemin ou octets. */
37
+ input: string | Uint8Array;
38
+ /** Chemin de sortie ; si absent, les octets signés sont renvoyés. */
39
+ output?: string;
40
+ /** Certificat PKCS#12 : chemin ou octets. */
41
+ p12: string | Uint8Array;
42
+ passphrase?: string;
43
+ passphraseFile?: string;
44
+ meta?: SignMeta;
45
+ field?: SignField;
46
+ /** Restrictions d'édition. `false`/absent = aucune (le cœur Node suffit) ; objet = PAdES pyhanko. */
47
+ permissions?: SignPermissions | false;
48
+ /** `auto` (défaut) = pyhanko si dispo sinon Node ; `node` = PKCS#7 ; `pyhanko` = PAdES. */
49
+ engine?: SignEngine;
50
+ }
51
+ export interface SignResult {
52
+ output?: string;
53
+ bytes?: Uint8Array;
54
+ engine: "node" | "pyhanko";
55
+ }
56
+ export interface VerifyOptions {
57
+ input: string;
58
+ /** Certificat de confiance (PEM) pour la validation pyhanko. */
59
+ trustPem?: string;
60
+ engine?: SignEngine;
61
+ }
62
+ export interface SignatureInfo {
63
+ field?: string;
64
+ intact?: boolean;
65
+ valid?: boolean;
66
+ trusted?: boolean;
67
+ signer?: string;
68
+ }
69
+ export interface VerifyResult {
70
+ signed: boolean;
71
+ signatures: SignatureInfo[];
72
+ engine: "pyhanko" | "node";
73
+ }
74
+ export interface EngineAvailability {
75
+ node: boolean;
76
+ pyhanko: boolean;
77
+ python?: string;
78
+ }
79
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,yFAAyF;AACzF,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,qCAAqC;IACrC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3B,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,qGAAqG;IACrG,WAAW,CAAC,EAAE,eAAe,GAAG,KAAK,CAAC;IACtC,2FAA2F;IAC3F,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @mostajs/pdf-sign — types publics.
3
+ *
4
+ * @author Dr Hamid MADANI <drmdh@msn.com>
5
+ * @license AGPL-3.0-or-later
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,3 @@
1
+ import type { VerifyOptions, VerifyResult } from "./types.js";
2
+ export declare function verifyPdf(opts: VerifyOptions): VerifyResult;
3
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI9D,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAsB3D"}
package/dist/verify.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @mostajs/pdf-sign — vérification de signature.
3
+ *
4
+ * pyhanko (validation cryptographique complète) si disponible ; sinon détection Node
5
+ * basique (présence d'un dictionnaire de signature).
6
+ *
7
+ * @author Dr Hamid MADANI <drmdh@msn.com>
8
+ * @license AGPL-3.0-or-later
9
+ */
10
+ import { execFileSync } from "node:child_process";
11
+ import { readFileSync } from "node:fs";
12
+ import { detectEngines, SIGN_PY } from "./engines.js";
13
+ import { loadPdfSignConfig } from "./config.js";
14
+ export function verifyPdf(opts) {
15
+ const cfg = loadPdfSignConfig();
16
+ const avail = detectEngines(cfg.python);
17
+ const trust = opts.trustPem ?? cfg.trustPem;
18
+ const want = opts.engine ?? cfg.engine;
19
+ if (avail.pyhanko && want !== "node") {
20
+ try {
21
+ const args = trust ? [SIGN_PY, "verify", opts.input, trust] : [SIGN_PY, "verify", opts.input];
22
+ const out = execFileSync(avail.python, args, { encoding: "utf-8" });
23
+ const line = out.trim().split("\n").pop() ?? "{}";
24
+ const parsed = JSON.parse(line);
25
+ return { signed: !!parsed.signed, signatures: parsed.signatures ?? [], engine: "pyhanko" };
26
+ }
27
+ catch {
28
+ /* repli sur la détection Node */
29
+ }
30
+ }
31
+ // Détection Node basique : présence d'un /ByteRange + dictionnaire de signature.
32
+ const s = readFileSync(opts.input).toString("latin1");
33
+ const signed = s.includes("/ByteRange") && /\/Type\s*\/Sig|adbe\.pkcs7|ETSI\.CAdES/.test(s);
34
+ return { signed, signatures: signed ? [{ intact: undefined, valid: undefined }] : [], engine: "node" };
35
+ }
36
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,UAAU,SAAS,CAAC,IAAmB;IAC3C,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC;IAEvC,IAAI,KAAK,CAAC,OAAO,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YAC9F,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,MAAgB,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;YAC9E,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;YAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAChC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC7F,CAAC;QAAC,MAAM,CAAC;YACP,iCAAiC;QACnC,CAAC;IACH,CAAC;IAED,iFAAiF;IACjF,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,wCAAwC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5F,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACzG,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Exemple — signature de base (moteur auto) + vérification.
3
+ *
4
+ * Usage : node examples/sign-basic.mjs <in.pdf> <cert.p12> <passphrase> [out.pdf]
5
+ *
6
+ * @author Dr Hamid MADANI <drmdh@msn.com>
7
+ * @license AGPL-3.0-or-later
8
+ */
9
+ import { signPdf, verifyPdf, detectEngines } from "../dist/index.js";
10
+
11
+ const [input, p12, passphrase, output = "signe.pdf"] = process.argv.slice(2);
12
+ if (!input || !p12) {
13
+ console.error("Usage : node examples/sign-basic.mjs <in.pdf> <cert.p12> <passphrase> [out.pdf]");
14
+ process.exit(1);
15
+ }
16
+
17
+ console.log("Moteurs :", detectEngines());
18
+
19
+ const res = await signPdf({
20
+ input,
21
+ output,
22
+ p12,
23
+ passphrase,
24
+ meta: { reason: "Compte rendu d'hospitalisation", name: "Dr H. Madani", location: "Alger, DZ" },
25
+ });
26
+ console.log(`Signé (${res.engine}) → ${res.output}`);
27
+
28
+ console.log("Vérification :", JSON.stringify(verifyPdf({ input: output }), null, 2));
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Exemple — signature PAdES + permissions restrictives (chiffrement AES-256).
3
+ * Exige le moteur pyhanko : bash python/setup-venv.sh puis exporter PDF_SIGN_PYTHON.
4
+ *
5
+ * Usage : node examples/sign-restricted.mjs <in.pdf> <cert.p12> <passphrase> [out.pdf]
6
+ *
7
+ * @author Dr Hamid MADANI <drmdh@msn.com>
8
+ * @license AGPL-3.0-or-later
9
+ */
10
+ import { signPdf, detectEngines } from "../dist/index.js";
11
+
12
+ const [input, p12, passphrase, output = "signe-restreint.pdf"] = process.argv.slice(2);
13
+ if (!input || !p12) {
14
+ console.error("Usage : node examples/sign-restricted.mjs <in.pdf> <cert.p12> <passphrase> [out.pdf]");
15
+ process.exit(1);
16
+ }
17
+
18
+ const avail = detectEngines();
19
+ if (!avail.pyhanko) {
20
+ console.error("Moteur pyhanko indisponible — lancer python/setup-venv.sh et exporter PDF_SIGN_PYTHON.");
21
+ process.exit(1);
22
+ }
23
+
24
+ const res = await signPdf({
25
+ input,
26
+ output,
27
+ p12,
28
+ passphrase,
29
+ engine: "pyhanko",
30
+ permissions: { extract: false, modifyAnnotation: false, modifyOther: false, printHighres: true },
31
+ meta: { reason: "Facture CNAS — valeur probante", name: "Établissement", location: "Alger, DZ" },
32
+ });
33
+ console.log(`Signé + restreint (${res.engine}) → ${res.output}`);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Exemple — cachet visible « image de fond + texte » (logo/tampon) + PAdES.
3
+ * Exige le moteur pyhanko (bash python/setup-venv.sh, PDF_SIGN_PYTHON).
4
+ *
5
+ * Usage : node examples/sign-stamped.mjs <in.pdf> <cert.p12> <passphrase> <logo.png> [out.pdf]
6
+ *
7
+ * @author Dr Hamid MADANI <drmdh@msn.com>
8
+ * @license AGPL-3.0-or-later
9
+ */
10
+ import { signPdf, verifyPdf, detectEngines } from "../dist/index.js";
11
+
12
+ const [input, p12, passphrase, logo, output = "signe-cachet.pdf"] = process.argv.slice(2);
13
+ if (!input || !p12 || !logo) {
14
+ console.error("Usage : node examples/sign-stamped.mjs <in.pdf> <cert.p12> <passphrase> <logo.png> [out.pdf]");
15
+ process.exit(1);
16
+ }
17
+ if (!detectEngines().pyhanko) {
18
+ console.error("Moteur pyhanko indisponible — lancer python/setup-venv.sh et exporter PDF_SIGN_PYTHON.");
19
+ process.exit(1);
20
+ }
21
+
22
+ const res = await signPdf({
23
+ input, output, p12, passphrase, engine: "pyhanko", permissions: {},
24
+ meta: { reason: "Compte rendu d'hospitalisation", name: "Dr Hamid MADANI", location: "Alger, DZ" },
25
+ field: { page: 1, box: [320, 40, 575, 140], stampImage: logo }, // cachet image + texte, bas-droite p.2
26
+ });
27
+ console.log(`Signé avec cachet image (${res.engine}) → ${res.output}`);
28
+ console.log("Vérification :", JSON.stringify(verifyPdf({ input: output }), null, 2));
package/llms.txt ADDED
@@ -0,0 +1,57 @@
1
+ # @mostajs/pdf-sign — fiche LLM
2
+ > Signature numérique de PDF, moteur HYBRIDE : cœur Node (PKCS#7 détaché via @signpdf, zéro Python) + bascule pyhanko (PAdES visible + permissions AES-256) quand un venv est disponible. Config 100 % `.env`, CLI + API. Portage générique du pipeline pyhanko+pikepdf éprouvé. IMPLÉMENTÉ 0.1.0.
3
+
4
+ - Version: 0.1.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
5
+ - Chemin: mostajs/mosta-pdf-sign-stack/mosta-pdf-sign · UI: ../mosta-pdf-sign-ui
6
+
7
+ ## RÔLE
8
+ Le mécanisme de scellement probant d'UN PDF (signer + vérifier), pas l'orchestration métier.
9
+ GÉNÉRIQUE : ne connaît aucun domaine. La sémantique (quel document, quel signataire, quelle
10
+ valeur légale) est portée par l'appelant via options/`.env`.
11
+
12
+ ## POURQUOI (SIH & documents officiels)
13
+ Valeur probante médico-légale exigée par l'interopérabilité (chap. 11) et la sécurité (chap. 12) :
14
+ - cliniques : CRH, CRO, comptes rendus d'imagerie/biologie signés par le praticien ;
15
+ - administratifs : certificats de séjour, ordonnances, dossiers de sortie ;
16
+ - financiers/CNAS-CASNOS : factures, bordereaux de remboursement, prises en charge.
17
+ La signature PAdES + permissions AES-256 garantit intégrité, authenticité et non-répudiation.
18
+
19
+ ## DEUX MOTEURS
20
+ - **node** (défaut si pas de venv) : PKCS#7 détaché, cachet invisible, aucune dépendance Python.
21
+ NE gère PAS permissions/chiffrement/PAdES.
22
+ - **pyhanko** (si venv) : PAdES visible + permissions restrictives (chiffrement AES-256, R=6) via le
23
+ script Python embarqué `python/sign_with_perms.py`. Pipeline : pikepdf chiffre → IncrementalPdfFileWriter
24
+ (writer.encrypt(owner)) → append_signature_field → PdfSigner PAdES. Préserve chiffrement ET signature.
25
+ - **auto** : pyhanko si dispo, sinon node.
26
+
27
+ ## EXPORTS ("@mostajs/pdf-sign")
28
+ - signPdf(opts): Promise<SignResult> — fusionne opts + `.env`, dispatch node/pyhanko
29
+ - verifyPdf(opts): VerifyResult — validation pyhanko complète si dispo, sinon détection Node (/ByteRange)
30
+ - detectEngines(python?): EngineAvailability — { node:true, pyhanko:bool, python }
31
+ - resolvePython / pyhankoAvailable
32
+ - loadPdfSignConfig(): PdfSignConfig — lecture `.env` via @mostajs/config
33
+ - types SignOptions/SignResult/SignMeta/SignField/SignPermissions/SignEngine/Verify*
34
+
35
+ ## CONFIG (.env — via @mostajs/config, rien en dur)
36
+ PDF_SIGN_P12 / PDF_SIGN_PASSPHRASE / PDF_SIGN_PASSPHRASE_FILE
37
+ PDF_SIGN_ENGINE (auto|node|pyhanko) · PDF_SIGN_PYTHON (bin du venv)
38
+ PDF_SIGN_REASON / _LOCATION / _NAME / _CONTACT (métadonnées du cachet)
39
+ PDF_SIGN_FIELD_PAGE / PDF_SIGN_FIELD_BOX ("x1,y1,x2,y2") · PDF_SIGN_STAMP_IMAGE (logo du cachet visible, pyhanko) · PDF_SIGN_RESTRICT (bool → permissions)
40
+ PDF_SIGN_TRUST_PEM (certificat de confiance pour la vérification)
41
+
42
+ ## CLI
43
+ mosta-pdf-sign sign <in.pdf> [out.pdf] --p12 c.p12 --pass s [--engine …] [--restrict] [--reason …]
44
+ mosta-pdf-sign verify <pdf> [--trust cert.pem] [--engine …]
45
+ mosta-pdf-sign engines
46
+
47
+ ## MOTEUR pyhanko (optionnel)
48
+ bash python/setup-venv.sh → .venv local → exporter PDF_SIGN_PYTHON=…/.venv/bin/python
49
+ requiert pyhanko>=0.25, pikepdf>=8.
50
+
51
+ ## PIÈGES
52
+ - Le cœur Node lève si `permissions` est demandé : le PAdES/chiffrement EXIGE pyhanko.
53
+ - `engine:'pyhanko'` sans venv → erreur explicite (lancer setup-venv.sh).
54
+ - p12 obligatoire (opts.p12 ou PDF_SIGN_P12) sinon throw.
55
+ - box en points PDF, origine bas-gauche (A4 = 595×842) ; défaut cachet bas-droite [320,40,575,140].
56
+ - field.stampImage / PDF_SIGN_STAMP_IMAGE : cachet visible « image de fond + texte » (pyhanko stamp.TextStampStyle, opacité 0.35). Ignoré par le cœur Node.
57
+ - La passphrase peut passer par fichier (PASSPHRASE_FILE) pour ne jamais transiter en argv (pyhanko).
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@mostajs/pdf-sign",
3
+ "version": "0.1.0",
4
+ "description": "Signature numérique de PDF pour @mostajs — moteur HYBRIDE : cœur Node (PKCS#7 via @signpdf) + bascule pyhanko (PAdES + permissions AES-256) si disponible. Config .env, CLI, API. Réutilise le pipeline éprouvé pyhanko+pikepdf.",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "bin": {
18
+ "mosta-pdf-sign": "dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "python/*.py",
23
+ "python/*.txt",
24
+ "python/*.sh",
25
+ "examples/*.mjs",
26
+ "README.md",
27
+ "llms.txt",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json",
32
+ "test": "bash test-scripts/run-tests.sh"
33
+ },
34
+ "keywords": [
35
+ "pdf",
36
+ "signature",
37
+ "pades",
38
+ "pkcs7",
39
+ "pkcs12",
40
+ "pyhanko",
41
+ "signpdf",
42
+ "sign",
43
+ "mostajs"
44
+ ],
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "dependencies": {
49
+ "@mostajs/config": "^1.1.0",
50
+ "@signpdf/signpdf": "^3.2.4",
51
+ "@signpdf/signer-p12": "^3.2.4",
52
+ "@signpdf/placeholder-plain": "^3.2.4",
53
+ "pdf-lib": "^1.17.1"
54
+ },
55
+ "devDependencies": {
56
+ "@mostajs/mjs-unit": "^0.4.1",
57
+ "@types/node": "^22.0.0",
58
+ "typescript": "^5.6.0"
59
+ }
60
+ }
@@ -0,0 +1,3 @@
1
+ # @mostajs/pdf-sign — dépendances du moteur pyhanko (PAdES + permissions).
2
+ pyhanko>=0.25
3
+ pikepdf>=8
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # @mostajs/pdf-sign — crée le venv Python du moteur pyhanko (optionnel : PAdES + permissions).
3
+ # @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
4
+ set -euo pipefail
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ python3 -m venv "$HERE/.venv"
7
+ "$HERE/.venv/bin/pip" install --upgrade pip >/dev/null
8
+ "$HERE/.venv/bin/pip" install -r "$HERE/requirements.txt"
9
+ echo "OK — venv pyhanko prêt : $HERE/.venv (exporter PDF_SIGN_PYTHON=$HERE/.venv/bin/python)"
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ """sign_with_perms.py — signature PAdES + permissions restrictives (pyhanko + pikepdf).
3
+
4
+ Version paramétrée (config JSON) du pipeline éprouvé : applique éventuellement des
5
+ permissions PDF restrictives (chiffrement AES-256) PUIS signe en PAdES visible, en
6
+ incremental update sur le PDF chiffré — préservant l'authentification du chiffrement
7
+ ET la validité cryptographique de la signature.
8
+
9
+ Usage :
10
+ sign_with_perms.py sign INPUT.pdf OUTPUT.pdf CERT.p12 PASSFILE CONFIG.json
11
+ sign_with_perms.py verify PDF CERT.pem
12
+
13
+ Le moteur Node (@mostajs/pdf-sign) écrit CONFIG.json puis appelle ce script.
14
+
15
+ @author Dr Hamid MADANI <drmdh@msn.com>
16
+ @license AGPL-3.0-or-later
17
+ """
18
+ import sys
19
+ import io
20
+ import json
21
+ import secrets
22
+ import string
23
+
24
+
25
+ def do_sign(inp, outp, p12_path, p12_pass_file, config_path):
26
+ import pikepdf
27
+ from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
28
+ from pyhanko.sign import signers, fields, PdfSignatureMetadata
29
+ from pyhanko.sign.fields import SigFieldSpec
30
+
31
+ with open(p12_pass_file, "rb") as f:
32
+ p12_pass = f.read().strip()
33
+ with open(config_path, "r", encoding="utf-8") as f:
34
+ cfg = json.load(f)
35
+
36
+ meta_cfg = cfg.get("meta", {})
37
+ field_cfg = cfg.get("field", {})
38
+ perms_cfg = cfg.get("permissions") # None => pas de chiffrement/restriction
39
+
40
+ src_buf = inp
41
+ # --- 1. permissions + chiffrement (optionnel) via pikepdf, en mémoire ---
42
+ owner_pwd = None
43
+ if perms_cfg is not None:
44
+ print("-- 1/3 permissions (chiffrement AES-256) --", flush=True)
45
+ owner_pwd = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(40))
46
+ perms = pikepdf.Permissions(
47
+ accessibility=perms_cfg.get("accessibility", True),
48
+ extract=perms_cfg.get("extract", False),
49
+ modify_annotation=perms_cfg.get("modify_annotation", False),
50
+ modify_assembly=perms_cfg.get("modify_assembly", False),
51
+ modify_form=True, # requis pour signer après chiffrement
52
+ modify_other=perms_cfg.get("modify_other", False),
53
+ print_lowres=perms_cfg.get("print_lowres", True),
54
+ print_highres=perms_cfg.get("print_highres", True),
55
+ )
56
+ encryption = pikepdf.Encryption(user="", owner=owner_pwd, R=6, allow=perms)
57
+ buf = io.BytesIO()
58
+ with pikepdf.open(inp) as pdf:
59
+ pdf.save(buf, encryption=encryption)
60
+ buf.seek(0)
61
+ src_buf = buf
62
+
63
+ # --- 2. champ de signature ---
64
+ print("-- 2/3 champ de signature --", flush=True)
65
+ writer = IncrementalPdfFileWriter(src_buf if isinstance(src_buf, io.BytesIO) else open(src_buf, "rb"))
66
+ if owner_pwd is not None:
67
+ writer.encrypt(owner_pwd)
68
+
69
+ field_name = field_cfg.get("name", "Signature1")
70
+ box = tuple(field_cfg.get("box", [320, 40, 575, 140]))
71
+ on_page = field_cfg.get("page", 0)
72
+ fields.append_signature_field(
73
+ writer,
74
+ sig_field_spec=SigFieldSpec(sig_field_name=field_name, on_page=on_page, box=box),
75
+ )
76
+
77
+ # --- 3. signature PAdES ---
78
+ print("-- 3/3 signature PAdES --", flush=True)
79
+ signer = signers.SimpleSigner.load_pkcs12(pfx_file=p12_path, passphrase=p12_pass)
80
+ meta = PdfSignatureMetadata(
81
+ field_name=field_name,
82
+ reason=meta_cfg.get("reason", "Authentification du document"),
83
+ location=meta_cfg.get("location", ""),
84
+ name=meta_cfg.get("name", ""),
85
+ contact_info=meta_cfg.get("contact", ""),
86
+ subfilter=fields.SigSeedSubFilter.PADES,
87
+ )
88
+
89
+ # Apparence du cachet visible : image de fond (logo/tampon) + texte, si une image est fournie.
90
+ stamp_style = _build_stamp_style(field_cfg.get("stamp_image"), meta_cfg)
91
+
92
+ with open(outp, "wb") as out_f:
93
+ signers.PdfSigner(signature_meta=meta, signer=signer, stamp_style=stamp_style).sign_pdf(writer, output=out_f)
94
+ print(f"OK -- PDF signé : {outp}", flush=True)
95
+
96
+
97
+ def _build_stamp_style(stamp_image, meta_cfg):
98
+ """Style de cachet visible « image de fond + texte » (ou None → texte par défaut de pyhanko)."""
99
+ if not stamp_image:
100
+ return None
101
+ from pyhanko import stamp
102
+ from pyhanko.pdf_utils import images
103
+
104
+ lines = [x for x in (meta_cfg.get("name"), meta_cfg.get("reason"), meta_cfg.get("location")) if x]
105
+ # %(ts)s est substitué par pyhanko (horodatage) ; le reste est du texte littéral.
106
+ text = ("\n".join(lines) + "\n" if lines else "") + "Signé le %(ts)s"
107
+ return stamp.TextStampStyle(
108
+ stamp_text=text,
109
+ background=images.PdfImage(stamp_image),
110
+ background_opacity=0.35,
111
+ )
112
+
113
+
114
+ def _cert_name(cert):
115
+ """Nom lisible du signataire (sujet du certificat) plutôt que le repr brut."""
116
+ try:
117
+ return cert.subject.human_friendly
118
+ except Exception:
119
+ return str(cert) if cert else ""
120
+
121
+
122
+ def do_verify(pdf_path, cert_pem):
123
+ import logging
124
+ # Silence les logs internes (tracebacks de chaîne de confiance) : on renvoie le verdict en JSON.
125
+ for name in ("pyhanko", "pyhanko_certvalidator"):
126
+ logging.getLogger(name).setLevel(logging.CRITICAL)
127
+
128
+ from pyhanko.pdf_utils.reader import PdfFileReader
129
+ from pyhanko.sign.validation import validate_pdf_signature
130
+ from pyhanko_certvalidator import ValidationContext
131
+ from pyhanko.keys import load_cert_from_pemder
132
+
133
+ vc = None
134
+ try:
135
+ trust = load_cert_from_pemder(cert_pem) if cert_pem else None
136
+ vc = ValidationContext(trust_roots=[trust]) if trust else ValidationContext(allow_fetching=False)
137
+ except Exception:
138
+ vc = ValidationContext(allow_fetching=False)
139
+
140
+ with open(pdf_path, "rb") as f:
141
+ r = PdfFileReader(f)
142
+ # PDF chiffré (permissions restrictives) : s'authentifier avec le mot de passe
143
+ # utilisateur vide (lecture libre) avant d'énumérer les signatures.
144
+ if getattr(r, "encrypted", False):
145
+ try:
146
+ r.decrypt("")
147
+ except Exception:
148
+ pass
149
+ sigs = r.embedded_signatures
150
+ if not sigs:
151
+ print(json.dumps({"signed": False, "signatures": []}))
152
+ return
153
+ out = []
154
+ for s in sigs:
155
+ status = validate_pdf_signature(s, vc)
156
+ out.append({
157
+ "field": s.field_name,
158
+ "intact": bool(status.intact),
159
+ "valid": bool(status.valid),
160
+ "trusted": bool(getattr(status, "trusted", False)),
161
+ "signer": _cert_name(getattr(status, "signing_cert", None)),
162
+ })
163
+ print(json.dumps({"signed": True, "signatures": out}))
164
+
165
+
166
+ if __name__ == "__main__":
167
+ if len(sys.argv) >= 2 and sys.argv[1] == "sign" and len(sys.argv) == 7:
168
+ do_sign(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6])
169
+ elif len(sys.argv) >= 2 and sys.argv[1] == "verify" and len(sys.argv) >= 3:
170
+ do_verify(sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else None)
171
+ else:
172
+ print(__doc__)
173
+ sys.exit(1)