@mostajs/archive-box 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.
- package/LICENSE +29 -0
- package/README.md +79 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +189 -0
- package/llms.txt +33 -0
- package/package.json +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
COMMERCIAL LICENSE
|
|
20
|
+
|
|
21
|
+
For organizations that cannot comply with the AGPL open-source requirements,
|
|
22
|
+
a commercial license is available. Contact: drmdh@msn.com
|
|
23
|
+
|
|
24
|
+
The commercial license allows you to:
|
|
25
|
+
- Use the software in proprietary/closed-source projects
|
|
26
|
+
- Modify without publishing your source code
|
|
27
|
+
- Get priority support and SLA
|
|
28
|
+
|
|
29
|
+
Contact: Dr Hamid MADANI <drmdh@msn.com>
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @mostajs/archive-box
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
|
|
4
|
+
**Niveau** : N0 · **Dépendances** : `jszip` + `@mostajs/crypto-box`
|
|
5
|
+
|
|
6
|
+
Une archive : **un zip, un manifeste, des empreintes** — et, si vous voulez, **une signature**.
|
|
7
|
+
Rien de métier.
|
|
8
|
+
|
|
9
|
+
## Pourquoi ce module existe
|
|
10
|
+
|
|
11
|
+
`@mostajs/ixarchive` avait les **bonnes mécaniques** (manifeste versionné, SHA-256, zip, fichiers
|
|
12
|
+
hors base, import validé) — mais **prisonnières d'un domaine** : arbres de projets
|
|
13
|
+
(`collectProjectTree`, `rootProjectSlug`), pierres tombales, pseudonymisation, compteurs de
|
|
14
|
+
répondants et de réponses **gravés dans le format lui-même**. Impossible d'archiver autre chose
|
|
15
|
+
qu'un projet d'enquête.
|
|
16
|
+
|
|
17
|
+
Extrait ici, le noyau redevient réutilisable — `ixarchive` retrouve son domaine, `@mostajs/backup`
|
|
18
|
+
obtient son artefact.
|
|
19
|
+
|
|
20
|
+
## Une archive n'est pas « des fichiers zippés »
|
|
21
|
+
|
|
22
|
+
C'est un artefact **daté, autodescriptif et vérifiable** :
|
|
23
|
+
|
|
24
|
+
| | Ce que ça prouve |
|
|
25
|
+
|---|---|
|
|
26
|
+
| **SHA-256 par fichier** | l'archive n'a pas été **corrompue par accident** |
|
|
27
|
+
| **Signature Ed25519** (facultative) | l'archive a été **produite par nous** |
|
|
28
|
+
|
|
29
|
+
**La différence n'est pas rhétorique.** Un attaquant qui modifie un fichier **et recalcule son
|
|
30
|
+
empreinte dans le manifeste** passe le contrôle d'intégrité : tout est « cohérent ». Seule la
|
|
31
|
+
signature du manifeste le trahit.
|
|
32
|
+
|
|
33
|
+
Et il y a plus subtil : une archive **parfaitement valide, signée par quelqu'un d'autre**.
|
|
34
|
+
Empreintes bonnes, signature authentique — et pourtant ce n'est pas la vôtre. D'où
|
|
35
|
+
`expectedPublicKey` : sans elle, on restaure une base fabriquée par un tiers.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { buildArchive, openArchive } from '@mostajs/archive-box';
|
|
41
|
+
import { generateSigningKeyPair } from '@mostajs/crypto-box';
|
|
42
|
+
|
|
43
|
+
const cle = generateSigningKeyPair();
|
|
44
|
+
|
|
45
|
+
const { buffer } = await buildArchive({
|
|
46
|
+
name: 'sauvegarde-yalidine',
|
|
47
|
+
kind: 'backup', // étiquette libre — le module ne l'interprète pas
|
|
48
|
+
files: [
|
|
49
|
+
{ path: 'entities/Ticket.json', content: json },
|
|
50
|
+
{ path: 'config.json', content: config },
|
|
51
|
+
{ path: 'files/logo.png', content: octets },
|
|
52
|
+
],
|
|
53
|
+
extra: { siteId: 'yalidine-01', entites: 12480 }, // le DOMAINE range ce qu'il veut ici
|
|
54
|
+
signingKey: cle.privateKey,
|
|
55
|
+
signingPublicKey: cle.publicKey,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Restauration — strict par défaut : LÈVE si l'intégrité ou la signature est en défaut.
|
|
59
|
+
const { manifest, files, integrity, signature } = await openArchive(buffer, {
|
|
60
|
+
expectedPublicKey: cle.publicKey,
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`strict: false` permet d'**inspecter** une archive suspecte sans la refuser d'emblée.
|
|
65
|
+
|
|
66
|
+
## Ce que le module ignore
|
|
67
|
+
|
|
68
|
+
**Ce qu'il y a dans les fichiers.** Des entités ORM, une configuration, une licence, des images :
|
|
69
|
+
il ne le sait pas et n'a pas à le savoir. C'est ce qui le rend réutilisable — et c'est exactement
|
|
70
|
+
ce qui manquait à `ixarchive`.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm test # 14 tests mjs-unit
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Une archive se juge sur ce qu'elle **refuse** : corruption d'un fichier, fichier manquant,
|
|
79
|
+
manifeste falsifié (intégrité bernée, signature non), et **signataire inattendu**.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/archive-box — Une archive : un zip, un manifeste, des empreintes. Rien de métier.
|
|
3
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
* Licence : AGPL-3.0-or-later
|
|
5
|
+
* Niveau : N0 (primitif) · Dépendances : `jszip` + `@mostajs/crypto-box`
|
|
6
|
+
*
|
|
7
|
+
* ─── POURQUOI CE MODULE EXISTE ───────────────────────────────────────────────
|
|
8
|
+
* `@mostajs/ixarchive` avait les BONNES mécaniques — manifeste versionné, SHA-256 par fichier,
|
|
9
|
+
* zip, fichiers hors base, import avec validation — mais elles étaient **prisonnières d'un
|
|
10
|
+
* domaine** : arbres de projets (`collectProjectTree`, `rootProjectSlug`), pierres tombales,
|
|
11
|
+
* pseudonymisation, comptes de répondants et de réponses. Impossible d'archiver autre chose
|
|
12
|
+
* qu'un projet d'enquête.
|
|
13
|
+
*
|
|
14
|
+
* Extrait ici, le noyau devient réutilisable :
|
|
15
|
+
* - `@mostajs/ixarchive` → archive-box + son domaine « projets » (sa généralisation, enfin)
|
|
16
|
+
* - `@mostajs/backup` → archive-box + orm-copy-data + crypto-box
|
|
17
|
+
*
|
|
18
|
+
* ─── CE QU'UNE ARCHIVE EST, ET N'EST PAS ─────────────────────────────────────
|
|
19
|
+
* Une archive n'est pas « des fichiers zippés ». C'est un artefact **daté, autodescriptif et
|
|
20
|
+
* VÉRIFIABLE** :
|
|
21
|
+
* - chaque fichier porte son **SHA-256** dans le manifeste → la corruption est détectée ;
|
|
22
|
+
* - le manifeste peut être **SIGNÉ** (Ed25519) → l'archive prouve **qui l'a produite**.
|
|
23
|
+
*
|
|
24
|
+
* La différence n'est pas rhétorique. L'empreinte dit « pas corrompue par accident ». La
|
|
25
|
+
* signature dit « produite par nous ». Pour une archive qu'on restaure chez un client — et
|
|
26
|
+
* pour une garantie de déploiement — c'est une différence de nature.
|
|
27
|
+
*
|
|
28
|
+
* ─── CE QUE CE MODULE IGNORE ─────────────────────────────────────────────────
|
|
29
|
+
* Ce qu'il y a DANS les fichiers. Des entités ORM, une configuration, une licence, des
|
|
30
|
+
* images : il ne le sait pas et n'a pas à le savoir. C'est ce qui le rend réutilisable —
|
|
31
|
+
* et c'est exactement ce qui manquait à `ixarchive`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const moduleInfo: {
|
|
34
|
+
readonly name: "@mostajs/archive-box";
|
|
35
|
+
readonly version: "0.1.0";
|
|
36
|
+
readonly level: "N0";
|
|
37
|
+
};
|
|
38
|
+
/** Version du FORMAT d'archive (pas du module) : ce qui garantit la lisibilité future. */
|
|
39
|
+
export declare const ARCHIVE_VERSION = "1.0.0";
|
|
40
|
+
/** Un fichier de l'archive, avec son empreinte. */
|
|
41
|
+
export interface ManifestFile {
|
|
42
|
+
path: string;
|
|
43
|
+
size: number;
|
|
44
|
+
sha256: string;
|
|
45
|
+
}
|
|
46
|
+
/** Signature détachée du manifeste (Ed25519). */
|
|
47
|
+
export interface ArchiveSignature {
|
|
48
|
+
algo: 'ed25519';
|
|
49
|
+
/** Clé publique du signataire, en hex — pour identifier QUI a signé. */
|
|
50
|
+
publicKey: string;
|
|
51
|
+
/** Signature de la charge canonique (manifeste sans son bloc `signature`), en hex. */
|
|
52
|
+
value: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ArchiveMeta {
|
|
55
|
+
/** Version du FORMAT. */
|
|
56
|
+
version: string;
|
|
57
|
+
/** Étiquette libre du producteur : 'backup', 'project', 'seed'… Le module ne l'interprète pas. */
|
|
58
|
+
kind: string;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
createdBy?: string;
|
|
61
|
+
sourceApp?: string;
|
|
62
|
+
sourceUrl?: string;
|
|
63
|
+
files: ManifestFile[];
|
|
64
|
+
signature?: ArchiveSignature;
|
|
65
|
+
/**
|
|
66
|
+
* Le domaine met ici CE QU'IL VEUT (compteurs, identifiants, versions de schémas…).
|
|
67
|
+
* C'est cette poche qui a manqué à `ixarchive` : faute d'elle, ses compteurs de répondants
|
|
68
|
+
* et son `rootProjectSlug` étaient gravés dans le format lui-même.
|
|
69
|
+
*/
|
|
70
|
+
extra?: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
export interface ArchiveManifest {
|
|
73
|
+
$schema: string;
|
|
74
|
+
'@context': string;
|
|
75
|
+
'@type': string;
|
|
76
|
+
archiveBox: ArchiveMeta;
|
|
77
|
+
name: string;
|
|
78
|
+
description: string;
|
|
79
|
+
}
|
|
80
|
+
export interface ArchiveFile {
|
|
81
|
+
path: string;
|
|
82
|
+
content: Buffer | Uint8Array | string;
|
|
83
|
+
}
|
|
84
|
+
export interface BuildArchiveOptions {
|
|
85
|
+
files: ArchiveFile[];
|
|
86
|
+
name: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
kind?: string;
|
|
89
|
+
createdBy?: string;
|
|
90
|
+
sourceApp?: string;
|
|
91
|
+
sourceUrl?: string;
|
|
92
|
+
extra?: Record<string, unknown>;
|
|
93
|
+
/** Clé privée Ed25519 (32 o). Fournie → le manifeste est SIGNÉ. */
|
|
94
|
+
signingKey?: Uint8Array | Buffer;
|
|
95
|
+
/** Clé publique correspondante (32 o) — inscrite au manifeste pour identifier le signataire. */
|
|
96
|
+
signingPublicKey?: Uint8Array | Buffer;
|
|
97
|
+
}
|
|
98
|
+
export interface BuiltArchive {
|
|
99
|
+
buffer: Buffer;
|
|
100
|
+
manifest: ArchiveManifest;
|
|
101
|
+
/** Empreinte de l'archive ENTIÈRE (le zip) — à conserver hors de l'archive. */
|
|
102
|
+
sha256: string;
|
|
103
|
+
}
|
|
104
|
+
/** Construit une archive : zip + manifeste + empreinte par fichier (+ signature facultative). */
|
|
105
|
+
export declare function buildArchive(opts: BuildArchiveOptions): Promise<BuiltArchive>;
|
|
106
|
+
export type IntegrityVerdict = {
|
|
107
|
+
ok: true;
|
|
108
|
+
} | {
|
|
109
|
+
ok: false;
|
|
110
|
+
reason: 'manifest_absent' | 'manifest_illisible' | 'fichier_absent' | 'empreinte_invalide';
|
|
111
|
+
detail?: string;
|
|
112
|
+
};
|
|
113
|
+
export type SignatureVerdict = 'absente' | 'valide' | 'invalide' | 'signataire_inattendu';
|
|
114
|
+
export interface OpenedArchive {
|
|
115
|
+
manifest: ArchiveManifest;
|
|
116
|
+
/** Contenu des fichiers, par chemin. */
|
|
117
|
+
files: Map<string, Buffer>;
|
|
118
|
+
integrity: IntegrityVerdict;
|
|
119
|
+
signature: SignatureVerdict;
|
|
120
|
+
}
|
|
121
|
+
/** Lit le manifeste sans extraire l'archive (peu coûteux, utile pour lister). */
|
|
122
|
+
export declare function readManifest(zipBuffer: Buffer | Uint8Array): Promise<ArchiveManifest | null>;
|
|
123
|
+
/**
|
|
124
|
+
* Ouvre une archive en VÉRIFIANT chaque empreinte, et la signature si une clé est fournie.
|
|
125
|
+
*
|
|
126
|
+
* `strict` (défaut **true**) : lève si l'intégrité échoue. C'est le bon défaut — restaurer
|
|
127
|
+
* une archive corrompue est pire que ne pas restaurer du tout. Passer `strict: false` pour
|
|
128
|
+
* inspecter une archive suspecte sans la refuser d'emblée.
|
|
129
|
+
*/
|
|
130
|
+
export declare function openArchive(zipBuffer: Buffer | Uint8Array, opts?: {
|
|
131
|
+
expectedPublicKey?: Uint8Array | Buffer;
|
|
132
|
+
strict?: boolean;
|
|
133
|
+
}): Promise<OpenedArchive>;
|
|
134
|
+
/** Vérifie une archive sans en extraire le contenu (verdict seul). */
|
|
135
|
+
export declare function verifyArchive(zipBuffer: Buffer | Uint8Array, opts?: {
|
|
136
|
+
expectedPublicKey?: Uint8Array | Buffer;
|
|
137
|
+
}): Promise<{
|
|
138
|
+
integrity: IntegrityVerdict;
|
|
139
|
+
signature: SignatureVerdict;
|
|
140
|
+
manifest: ArchiveManifest | null;
|
|
141
|
+
}>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/archive-box — Une archive : un zip, un manifeste, des empreintes. Rien de métier.
|
|
3
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
* Licence : AGPL-3.0-or-later
|
|
5
|
+
* Niveau : N0 (primitif) · Dépendances : `jszip` + `@mostajs/crypto-box`
|
|
6
|
+
*
|
|
7
|
+
* ─── POURQUOI CE MODULE EXISTE ───────────────────────────────────────────────
|
|
8
|
+
* `@mostajs/ixarchive` avait les BONNES mécaniques — manifeste versionné, SHA-256 par fichier,
|
|
9
|
+
* zip, fichiers hors base, import avec validation — mais elles étaient **prisonnières d'un
|
|
10
|
+
* domaine** : arbres de projets (`collectProjectTree`, `rootProjectSlug`), pierres tombales,
|
|
11
|
+
* pseudonymisation, comptes de répondants et de réponses. Impossible d'archiver autre chose
|
|
12
|
+
* qu'un projet d'enquête.
|
|
13
|
+
*
|
|
14
|
+
* Extrait ici, le noyau devient réutilisable :
|
|
15
|
+
* - `@mostajs/ixarchive` → archive-box + son domaine « projets » (sa généralisation, enfin)
|
|
16
|
+
* - `@mostajs/backup` → archive-box + orm-copy-data + crypto-box
|
|
17
|
+
*
|
|
18
|
+
* ─── CE QU'UNE ARCHIVE EST, ET N'EST PAS ─────────────────────────────────────
|
|
19
|
+
* Une archive n'est pas « des fichiers zippés ». C'est un artefact **daté, autodescriptif et
|
|
20
|
+
* VÉRIFIABLE** :
|
|
21
|
+
* - chaque fichier porte son **SHA-256** dans le manifeste → la corruption est détectée ;
|
|
22
|
+
* - le manifeste peut être **SIGNÉ** (Ed25519) → l'archive prouve **qui l'a produite**.
|
|
23
|
+
*
|
|
24
|
+
* La différence n'est pas rhétorique. L'empreinte dit « pas corrompue par accident ». La
|
|
25
|
+
* signature dit « produite par nous ». Pour une archive qu'on restaure chez un client — et
|
|
26
|
+
* pour une garantie de déploiement — c'est une différence de nature.
|
|
27
|
+
*
|
|
28
|
+
* ─── CE QUE CE MODULE IGNORE ─────────────────────────────────────────────────
|
|
29
|
+
* Ce qu'il y a DANS les fichiers. Des entités ORM, une configuration, une licence, des
|
|
30
|
+
* images : il ne le sait pas et n'a pas à le savoir. C'est ce qui le rend réutilisable —
|
|
31
|
+
* et c'est exactement ce qui manquait à `ixarchive`.
|
|
32
|
+
*/
|
|
33
|
+
import JSZip from 'jszip';
|
|
34
|
+
import { sha256, sign, verifySignature, equalsConstantTime } from '@mostajs/crypto-box';
|
|
35
|
+
export const moduleInfo = { name: '@mostajs/archive-box', version: '0.1.0', level: 'N0' };
|
|
36
|
+
/** Version du FORMAT d'archive (pas du module) : ce qui garantit la lisibilité future. */
|
|
37
|
+
export const ARCHIVE_VERSION = '1.0.0';
|
|
38
|
+
const MANIFEST_PATH = 'manifest.json';
|
|
39
|
+
const VERSION_PATH = 'archive-version.txt';
|
|
40
|
+
/**
|
|
41
|
+
* Charge canonique signée : le manifeste **sans son bloc `signature`**, sérialisé de façon
|
|
42
|
+
* stable. Sans cette exclusion, on signerait un objet contenant sa propre signature —
|
|
43
|
+
* impossible à vérifier.
|
|
44
|
+
*/
|
|
45
|
+
function signingPayload(manifest) {
|
|
46
|
+
const { signature, ...meta } = manifest.archiveBox;
|
|
47
|
+
void signature;
|
|
48
|
+
return JSON.stringify({ ...manifest, archiveBox: meta });
|
|
49
|
+
}
|
|
50
|
+
const toBuffer = (c) => Buffer.isBuffer(c) ? c : typeof c === 'string' ? Buffer.from(c, 'utf8') : Buffer.from(c);
|
|
51
|
+
/** Construit une archive : zip + manifeste + empreinte par fichier (+ signature facultative). */
|
|
52
|
+
export async function buildArchive(opts) {
|
|
53
|
+
const entries = opts.files.map((f) => ({ path: f.path, content: toBuffer(f.content) }));
|
|
54
|
+
const manifestFiles = entries.map((f) => ({
|
|
55
|
+
path: f.path,
|
|
56
|
+
size: f.content.length,
|
|
57
|
+
sha256: sha256(f.content),
|
|
58
|
+
}));
|
|
59
|
+
const manifest = {
|
|
60
|
+
$schema: 'https://mostajs.dev/schemas/archive-box/1.0.0.json',
|
|
61
|
+
'@context': 'https://mostajs.dev/context/archive-box',
|
|
62
|
+
'@type': 'Archive',
|
|
63
|
+
name: opts.name,
|
|
64
|
+
description: opts.description ?? `Archive « ${opts.name} »`,
|
|
65
|
+
archiveBox: {
|
|
66
|
+
version: ARCHIVE_VERSION,
|
|
67
|
+
kind: opts.kind ?? 'generic',
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
createdBy: opts.createdBy,
|
|
70
|
+
sourceApp: opts.sourceApp,
|
|
71
|
+
sourceUrl: opts.sourceUrl,
|
|
72
|
+
files: manifestFiles,
|
|
73
|
+
extra: opts.extra,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
if (opts.signingKey) {
|
|
77
|
+
if (!opts.signingPublicKey) {
|
|
78
|
+
throw new Error('[archive-box] signingPublicKey requise : sans elle, la signature est invérifiable');
|
|
79
|
+
}
|
|
80
|
+
manifest.archiveBox.signature = {
|
|
81
|
+
algo: 'ed25519',
|
|
82
|
+
publicKey: Buffer.from(opts.signingPublicKey).toString('hex'),
|
|
83
|
+
value: sign(opts.signingKey, signingPayload(manifest)).toString('hex'),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const zip = new JSZip();
|
|
87
|
+
zip.file(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
88
|
+
zip.file(VERSION_PATH, ARCHIVE_VERSION);
|
|
89
|
+
for (const f of entries)
|
|
90
|
+
zip.file(f.path, f.content);
|
|
91
|
+
const buffer = await zip.generateAsync({
|
|
92
|
+
type: 'nodebuffer',
|
|
93
|
+
compression: 'DEFLATE',
|
|
94
|
+
compressionOptions: { level: 9 },
|
|
95
|
+
});
|
|
96
|
+
return { buffer, manifest, sha256: sha256(buffer) };
|
|
97
|
+
}
|
|
98
|
+
/** Lit le manifeste sans extraire l'archive (peu coûteux, utile pour lister). */
|
|
99
|
+
export async function readManifest(zipBuffer) {
|
|
100
|
+
try {
|
|
101
|
+
const zip = await JSZip.loadAsync(Buffer.from(zipBuffer));
|
|
102
|
+
const f = zip.file(MANIFEST_PATH);
|
|
103
|
+
if (!f)
|
|
104
|
+
return null;
|
|
105
|
+
return JSON.parse(await f.async('string'));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Ouvre une archive en VÉRIFIANT chaque empreinte, et la signature si une clé est fournie.
|
|
113
|
+
*
|
|
114
|
+
* `strict` (défaut **true**) : lève si l'intégrité échoue. C'est le bon défaut — restaurer
|
|
115
|
+
* une archive corrompue est pire que ne pas restaurer du tout. Passer `strict: false` pour
|
|
116
|
+
* inspecter une archive suspecte sans la refuser d'emblée.
|
|
117
|
+
*/
|
|
118
|
+
export async function openArchive(zipBuffer, opts = {}) {
|
|
119
|
+
const strict = opts.strict !== false;
|
|
120
|
+
const buf = Buffer.from(zipBuffer);
|
|
121
|
+
const zip = await JSZip.loadAsync(buf);
|
|
122
|
+
const mf = zip.file(MANIFEST_PATH);
|
|
123
|
+
if (!mf) {
|
|
124
|
+
const verdict = { ok: false, reason: 'manifest_absent' };
|
|
125
|
+
if (strict)
|
|
126
|
+
throw new Error('[archive-box] manifeste absent — ce n’est pas une archive');
|
|
127
|
+
return { manifest: null, files: new Map(), integrity: verdict, signature: 'absente' };
|
|
128
|
+
}
|
|
129
|
+
let manifest;
|
|
130
|
+
try {
|
|
131
|
+
manifest = JSON.parse(await mf.async('string'));
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
if (strict)
|
|
135
|
+
throw new Error(`[archive-box] manifeste illisible : ${e.message}`);
|
|
136
|
+
return { manifest: null, files: new Map(), integrity: { ok: false, reason: 'manifest_illisible' }, signature: 'absente' };
|
|
137
|
+
}
|
|
138
|
+
// ── Intégrité : chaque fichier annoncé doit être présent ET conforme à son empreinte.
|
|
139
|
+
const files = new Map();
|
|
140
|
+
let integrity = { ok: true };
|
|
141
|
+
for (const decl of manifest.archiveBox?.files ?? []) {
|
|
142
|
+
const entry = zip.file(decl.path);
|
|
143
|
+
if (!entry) {
|
|
144
|
+
integrity = { ok: false, reason: 'fichier_absent', detail: decl.path };
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
const content = await entry.async('nodebuffer');
|
|
148
|
+
// Comparaison à temps constant : une empreinte est une valeur d'intégrité, on ne
|
|
149
|
+
// laisse pas fuir d'information par le temps de comparaison.
|
|
150
|
+
if (!equalsConstantTime(sha256(content), decl.sha256)) {
|
|
151
|
+
integrity = { ok: false, reason: 'empreinte_invalide', detail: decl.path };
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
files.set(decl.path, content);
|
|
155
|
+
}
|
|
156
|
+
if (!integrity.ok && strict) {
|
|
157
|
+
throw new Error(`[archive-box] intégrité en défaut (${integrity.reason}${integrity.detail ? ' : ' + integrity.detail : ''})`);
|
|
158
|
+
}
|
|
159
|
+
// ── Signature : l'intégrité dit « pas corrompue » ; la signature dit « produite par X ».
|
|
160
|
+
let signature = 'absente';
|
|
161
|
+
const sig = manifest.archiveBox?.signature;
|
|
162
|
+
if (sig) {
|
|
163
|
+
const payload = signingPayload(manifest);
|
|
164
|
+
const signerPub = Buffer.from(sig.publicKey, 'hex');
|
|
165
|
+
const valide = verifySignature(signerPub, payload, Buffer.from(sig.value, 'hex'));
|
|
166
|
+
if (!valide)
|
|
167
|
+
signature = 'invalide';
|
|
168
|
+
else if (opts.expectedPublicKey && !equalsConstantTime(signerPub, Buffer.from(opts.expectedPublicKey))) {
|
|
169
|
+
// Signature authentique… mais de QUELQU'UN D'AUTRE. C'est le piège : une archive
|
|
170
|
+
// correctement signée par un tiers n'est PAS une archive de confiance.
|
|
171
|
+
signature = 'signataire_inattendu';
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
signature = 'valide';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (strict && signature === 'invalide') {
|
|
178
|
+
throw new Error('[archive-box] signature INVALIDE — le manifeste a été altéré');
|
|
179
|
+
}
|
|
180
|
+
if (strict && signature === 'signataire_inattendu') {
|
|
181
|
+
throw new Error('[archive-box] signature valide mais d’un signataire INATTENDU');
|
|
182
|
+
}
|
|
183
|
+
return { manifest, files, integrity, signature };
|
|
184
|
+
}
|
|
185
|
+
/** Vérifie une archive sans en extraire le contenu (verdict seul). */
|
|
186
|
+
export async function verifyArchive(zipBuffer, opts = {}) {
|
|
187
|
+
const res = await openArchive(zipBuffer, { ...opts, strict: false });
|
|
188
|
+
return { integrity: res.integrity, signature: res.signature, manifest: res.manifest ?? null };
|
|
189
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @mostajs/archive-box — 0.1.0
|
|
2
|
+
Auteur : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later · N0 · deps : jszip + @mostajs/crypto-box
|
|
3
|
+
|
|
4
|
+
RÔLE
|
|
5
|
+
Une archive = zip + manifeste + empreinte SHA-256 par fichier + signature Ed25519 facultative.
|
|
6
|
+
IGNORE ce qu'il y a dans les fichiers — c'est ce qui le rend réutilisable.
|
|
7
|
+
EXTRAIT du noyau d'@mostajs/ixarchive, débarrassé de son domaine « projets » (collectProjectTree,
|
|
8
|
+
rootProjectSlug, pierres tombales, compteurs de répondants — qui étaient GRAVÉS dans le format).
|
|
9
|
+
|
|
10
|
+
API
|
|
11
|
+
buildArchive({ files:[{path,content}], name, description?, kind?, createdBy?, sourceApp?,
|
|
12
|
+
extra?, signingKey?, signingPublicKey? }) -> { buffer, manifest, sha256 }
|
|
13
|
+
openArchive(zip, { expectedPublicKey?, strict=true }) -> { manifest, files:Map, integrity, signature }
|
|
14
|
+
verifyArchive(zip, { expectedPublicKey? }) -> { integrity, signature, manifest }
|
|
15
|
+
readManifest(zip) -> manifest | null (sans extraire)
|
|
16
|
+
ARCHIVE_VERSION = '1.0.0'
|
|
17
|
+
|
|
18
|
+
VERDICTS
|
|
19
|
+
integrity : {ok:true} | {ok:false, reason: manifest_absent|manifest_illisible|fichier_absent|empreinte_invalide}
|
|
20
|
+
signature : 'absente' | 'valide' | 'invalide' | 'signataire_inattendu'
|
|
21
|
+
|
|
22
|
+
INTÉGRITÉ ≠ AUTHENTICITÉ (le point du module)
|
|
23
|
+
- Empreinte : l'archive n'a pas été corrompue PAR ACCIDENT.
|
|
24
|
+
- Signature : l'archive a été PRODUITE PAR NOUS.
|
|
25
|
+
Un attaquant qui modifie un fichier ET recalcule son empreinte au manifeste PASSE l'intégrité.
|
|
26
|
+
Seule la signature le trahit. Et une archive VALIDE signée par un TIERS reste dangereuse :
|
|
27
|
+
d'où `expectedPublicKey` — sans elle on restaure une base fabriquée par quelqu'un d'autre.
|
|
28
|
+
|
|
29
|
+
DOMAINE
|
|
30
|
+
`extra: Record<string,unknown>` — le producteur y range ses métadonnées. Le FORMAT ne les grave pas.
|
|
31
|
+
|
|
32
|
+
TESTS
|
|
33
|
+
14 tests mjs-unit : corruption, fichier absent, manifeste falsifié (intégrité bernée), signataire inattendu.
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/archive-box",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Une archive : un zip, un manifeste, des empreintes SHA-256 par fichier, une signature Ed25519 facultative. Générique — le module ignore ce qu'il y a DANS les fichiers. Extrait du noyau d'@mostajs/ixarchive, débarrassé de son domaine « projets ».",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" } },
|
|
9
|
+
"files": ["dist", "README.md", "llms.txt", "LICENSE"],
|
|
10
|
+
"scripts": { "build": "tsc", "test": "bash test-scripts/run-tests.sh", "prepublishOnly": "npm run build" },
|
|
11
|
+
"keywords": ["archive", "zip", "manifest", "sha256", "ed25519", "backup", "integrity", "mostajs"],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@mostajs/crypto-box": "^0.2.0",
|
|
14
|
+
"jszip": "^3.10.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@mostajs/mjs-unit": "^0.4.0",
|
|
18
|
+
"@types/node": "^20",
|
|
19
|
+
"typescript": "^5.6.0"
|
|
20
|
+
}
|
|
21
|
+
}
|