@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 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**.
@@ -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
+ }