@mostajs/recette 0.1.0 → 0.2.1
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.
- package/CHANGELOG.md +41 -0
- package/package.json +1 -1
- package/src/recette.js +27 -2
- package/src/sceau.js +21 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
4
|
|
|
5
|
+
## 0.2.1 — 2026-09-10
|
|
6
|
+
|
|
7
|
+
Correction née d'une panne **en production**, invisible depuis les essais.
|
|
8
|
+
|
|
9
|
+
### Corrigé
|
|
10
|
+
|
|
11
|
+
- **Le contenu du dossier revenait en CHAÎNE** de plusieurs dialectes SQL (sqljs le premier) là
|
|
12
|
+
où un dépôt en mémoire rend l'objet. L'enveloppe exportée transportait donc une chaîne, le
|
|
13
|
+
destinataire recalculait l'empreinte sur autre chose, et **refusait tout dossier** — avec un
|
|
14
|
+
message parlant de falsification. Le moteur normalise désormais à la lecture : `dossiers.of()`
|
|
15
|
+
et l'export rendent toujours un objet.
|
|
16
|
+
- **`dossiers.exporter()` vérifie l'empreinte avant de signer.** Un document dont l'empreinte ne
|
|
17
|
+
recouvre plus le contenu relu est refusé ici, en nommant la cause, plutôt que d'aller se faire
|
|
18
|
+
rejeter chez le destinataire qui, lui, n'aura que « signature ou empreinte invalide » à offrir.
|
|
19
|
+
|
|
20
|
+
### Ajouté aux essais
|
|
21
|
+
|
|
22
|
+
- **T-REC-14** monte un dépôt qui imite sqljs — écriture d'objet, lecture de texte. C'est le cas
|
|
23
|
+
qui manquait : quatorze essais verts et rien ne fonctionnait en ligne.
|
|
24
|
+
|
|
25
|
+
## 0.2.0 — 2026-09-10
|
|
26
|
+
|
|
27
|
+
Le sceau savait fermer un pli ; il ne savait pas l'ouvrir. C'est ce qui manquait pour que QaTrax
|
|
28
|
+
puisse recevoir ce que ValiTrax lui scelle — la moitié retour de la chaîne.
|
|
29
|
+
|
|
30
|
+
### Ajouté
|
|
31
|
+
|
|
32
|
+
- **`sceau.ouvrir(scelle)`**, rendu quand `destinataire.privee` est fourni. Pendant exact de
|
|
33
|
+
`sceller`, et pour la même raison logé ici : le format ne s'écrit qu'à un seul endroit. Ouvrir
|
|
34
|
+
n'authentifie pas — c'est la signature retrouvée à l'intérieur qui dit l'origine. **Ouvrir
|
|
35
|
+
d'abord, vérifier ensuite.**
|
|
36
|
+
- **`verification: { publique }`**, alias neutre de `certificateur: { publique }`. La contrepartie
|
|
37
|
+
est *le certificateur* vue de ValiTrax et *l'émetteur* vue de QaTrax : même verbe, vantage
|
|
38
|
+
inverse. L'ancien nom reste servi, aucune application n'est à modifier.
|
|
39
|
+
|
|
40
|
+
### Compatibilité
|
|
41
|
+
|
|
42
|
+
Aucune rupture. Une instance qui ne fournit pas `destinataire.privee` n'obtient pas `ouvrir` — la
|
|
43
|
+
règle inchangée du module : **un verbe n'existe que si sa clé existe**, pour que le manque se
|
|
44
|
+
découvre à la mise en service et non le jour de la remise.
|
|
45
|
+
|
|
5
46
|
## 0.1.0 — 2026-09-10
|
|
6
47
|
|
|
7
48
|
Première version. Le moteur de la recette applicative, extrait du besoin de **déléguer la recette
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/recette",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Validation & recette applicative : trame `mostajs-devtest/1` reçue signée du certificateur, points enrichis POUR LE CLIENT (l'import n'écrase jamais l'enrichissement), feuillets composés à l'image du manifeste PDF (ok/ko/er), anomalies déclarées puis qualifiées, dossier figé et EXPORTÉ SIGNÉ — aucun appel réseau —, certificat reçu et vérifié, feu d'exploitation DÉRIVÉ du certificat. Powers ValiTrax (valitrax.mostajs.dev).",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
package/src/recette.js
CHANGED
|
@@ -31,6 +31,17 @@ const rang = (e) => PARCOURS.indexOf(e);
|
|
|
31
31
|
const need = (v, msg) => { if (v === undefined || v === null || v === '') throw new Error(msg); return v; };
|
|
32
32
|
const asDate = (d) => (d ? new Date(d) : null);
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Le CONTENU d'un dossier, quel que soit ce que le stockage a bien voulu en faire.
|
|
36
|
+
*
|
|
37
|
+
* Un champ `json` revient en OBJET d'un dépôt en mémoire, et en CHAÎNE de plusieurs dialectes
|
|
38
|
+
* SQL — sqljs le premier. Sans cette normalisation, l'enveloppe exportée transportait une
|
|
39
|
+
* chaîne là où le destinataire attend un objet, et l'empreinte recalculée à l'arrivée ne
|
|
40
|
+
* pouvait plus correspondre : tout dossier était refusé, pour une raison invisible depuis les
|
|
41
|
+
* essais, qui n'utilisent que des dépôts en mémoire.
|
|
42
|
+
*/
|
|
43
|
+
const contenuDe = (d) => (typeof d?.contenu === 'string' ? JSON.parse(d.contenu) : d?.contenu);
|
|
44
|
+
|
|
34
45
|
export function createRecette({ repositories, ports = {}, clock = () => new Date(), numeroteur } = {}) {
|
|
35
46
|
const repo = need(repositories, 'repositories requis');
|
|
36
47
|
for (const c of ['campagnes', 'points', 'feuillets', 'releves', 'anomalies', 'dossiers', 'certificats']) {
|
|
@@ -392,12 +403,21 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
392
403
|
const sceau = ports.sceau;
|
|
393
404
|
if (!sceau?.signer) throw new Error("aucun sceau configuré : un export non signé ne prouve rien, il est refusé");
|
|
394
405
|
|
|
406
|
+
// ON VÉRIFIE AVANT DE SIGNER. Exporter un document dont l'empreinte ne recouvre plus le
|
|
407
|
+
// contenu, c'est fabriquer un refus chez le destinataire — et un refus qu'il mettra sur le
|
|
408
|
+
// compte d'une falsification. Mieux vaut refuser ici, en nommant la cause.
|
|
409
|
+
const contenu = contenuDe(d);
|
|
410
|
+
const verif = createHash('sha256').update(JSON.stringify(contenu)).digest('hex');
|
|
411
|
+
if (verif !== d.empreinte) {
|
|
412
|
+
throw new Error(`empreinte du dossier ${d.no} incohérente avec son contenu relu (${verif.slice(0, 12)}… ≠ ${String(d.empreinte).slice(0, 12)}…) : le stockage n'a pas rendu ce qui lui a été confié`);
|
|
413
|
+
}
|
|
414
|
+
|
|
395
415
|
const enveloppe = {
|
|
396
416
|
type: 'mostajs-recette-dossier/1',
|
|
397
417
|
emisLe: clock().toISOString(),
|
|
398
418
|
campagne: { no: c.no, releaseRef: c.releaseRef },
|
|
399
419
|
dossier: { no: d.no, empreinte: d.empreinte, figeLe: d.figeLe },
|
|
400
|
-
contenu
|
|
420
|
+
contenu,
|
|
401
421
|
};
|
|
402
422
|
const charge = JSON.stringify(enveloppe);
|
|
403
423
|
const signature = await sceau.signer(charge);
|
|
@@ -416,7 +436,12 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
416
436
|
const d = need(await dossiers.of(campagneId), 'aucun dossier pour cette campagne');
|
|
417
437
|
return repo.dossiers.update(d.id, { depotRef: need(depotRef, 'depotRef requis') });
|
|
418
438
|
},
|
|
419
|
-
of: async (campagneId) =>
|
|
439
|
+
of: async (campagneId) => {
|
|
440
|
+
const d = (await repo.dossiers.find((x) => x.campagneId === campagneId))[0] || null;
|
|
441
|
+
// On rend TOUJOURS un objet : l'appelant n'a pas à connaître le dialecte SQL sous-jacent
|
|
442
|
+
// pour relire le contenu du dossier qu'il vient de figer.
|
|
443
|
+
return d ? { ...d, contenu: contenuDe(d) } : null;
|
|
444
|
+
},
|
|
420
445
|
};
|
|
421
446
|
|
|
422
447
|
const certificats = {
|
package/src/sceau.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// La formule « signé avec la clé publique fournie par le certificateur » décrit en réalité le
|
|
21
21
|
// SCELLEMENT : cette clé sert à fermer le pli à son intention, pas à prouver notre identité.
|
|
22
22
|
// C'est `destinataire.publique` ci-dessous. Notre identité, elle, vient de `signature.privee`.
|
|
23
|
-
import { sign, verifySignature, seal, generateSigningKeyPair, generateRecipientKeyPair } from '@mostajs/crypto-box';
|
|
23
|
+
import { sign, verifySignature, seal, open, generateSigningKeyPair, generateRecipientKeyPair } from '@mostajs/crypto-box';
|
|
24
24
|
|
|
25
25
|
const ALG_SIGNATURE = 'Ed25519';
|
|
26
26
|
|
|
@@ -43,10 +43,16 @@ function octets(k, quoi) {
|
|
|
43
43
|
*
|
|
44
44
|
* @param {object} opt
|
|
45
45
|
* @param {object} [opt.signature] `{ privee, nom }` — NOTRE identité (Ed25519). Sans elle, pas d'export.
|
|
46
|
-
* @param {object} [opt.destinataire] `{ publique }` —
|
|
47
|
-
*
|
|
46
|
+
* @param {object} [opt.destinataire] `{ publique, privee }` — X25519. La PUBLIQUE du destinataire
|
|
47
|
+
* permet de SCELLER pour lui ; notre PRIVÉE permet d'OUVRIR
|
|
48
|
+
* ce qui nous a été scellé. Les deux vantages du même pli.
|
|
49
|
+
* @param {object} [opt.certificateur] `{ publique }` — Ed25519 de la contrepartie. Sans elle,
|
|
50
|
+
* aucun document reçu n'est vérifiable. Alias neutre :
|
|
51
|
+
* `verification`, car la contrepartie est le certificateur
|
|
52
|
+
* vu de ValiTrax, et l'émetteur vu de QaTrax — même verbe,
|
|
53
|
+
* vantage inverse.
|
|
48
54
|
*/
|
|
49
|
-
export function creerSceau({ signature, destinataire, certificateur } = {}) {
|
|
55
|
+
export function creerSceau({ signature, destinataire, certificateur, verification } = {}) {
|
|
50
56
|
const port = {};
|
|
51
57
|
|
|
52
58
|
if (signature?.privee) {
|
|
@@ -67,8 +73,17 @@ export function creerSceau({ signature, destinataire, certificateur } = {}) {
|
|
|
67
73
|
port.sceller = async (charge) => Buffer.from(seal([pub], charge)).toString('base64');
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
if (
|
|
71
|
-
const
|
|
76
|
+
if (destinataire?.privee) {
|
|
77
|
+
const priv = octets(destinataire.privee, 'destinataire.privee');
|
|
78
|
+
// OUVRIR est le pendant exact de SCELLER, et il vit ici pour la même raison : le format ne
|
|
79
|
+
// s'écrit qu'à un seul endroit. Le pli ouvert n'est PAS authentifié pour autant — c'est la
|
|
80
|
+
// signature qu'il contient qui dira d'où il vient. Ouvrir d'abord, vérifier ensuite.
|
|
81
|
+
port.ouvrir = async (scelle) => open(priv, Buffer.from(String(scelle), 'base64')).toString('utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const contrepartie = certificateur?.publique ? certificateur : verification;
|
|
85
|
+
if (contrepartie?.publique) {
|
|
86
|
+
const pub = octets(contrepartie.publique, 'clé publique de la contrepartie');
|
|
72
87
|
port.verifier = async (charge, sig) => {
|
|
73
88
|
// Un algorithme inattendu n'est pas une signature invalide : c'est un document qu'on ne
|
|
74
89
|
// sait pas lire. On refuse dans les deux cas, mais on ne prétend pas avoir vérifié.
|