@mostajs/backup 0.1.0 → 0.2.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/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { readManifest } from '@mostajs/archive-box';
|
|
32
32
|
import type { ArchiveManifest, IntegrityVerdict, SignatureVerdict } from '@mostajs/archive-box';
|
|
33
33
|
import type { StorageDriver, ObjectRef } from '@mostajs/blob-store';
|
|
34
|
+
import type { Schedule, Scheduler } from '@mostajs/scheduler';
|
|
34
35
|
export declare const moduleInfo: {
|
|
35
36
|
readonly name: "@mostajs/backup";
|
|
36
37
|
readonly version: "0.1.0";
|
|
@@ -90,7 +91,20 @@ export interface BackupRef {
|
|
|
90
91
|
sealed: boolean;
|
|
91
92
|
}
|
|
92
93
|
export interface CreatedBackup extends BackupRef {
|
|
94
|
+
/** Archive telle qu'elle sera DÉPOSÉE (scellée si des destinataires sont fournis). */
|
|
93
95
|
buffer: Buffer;
|
|
96
|
+
/**
|
|
97
|
+
* Archive AVANT scellement — **en mémoire uniquement, jamais déposée**.
|
|
98
|
+
*
|
|
99
|
+
* Elle existe pour une seule raison : permettre de TESTER la restauration sans posséder la
|
|
100
|
+
* clé privée du client. Sans elle, on serait devant une contradiction : une archive scellée
|
|
101
|
+
* pour le client ne peut pas être ouverte par la console — c'est tout l'intérêt du modèle —
|
|
102
|
+
* donc la console ne pourrait jamais vérifier qu'elle est restaurable. On testerait à
|
|
103
|
+
* l'aveugle, ou pas du tout.
|
|
104
|
+
*
|
|
105
|
+
* Le processus de sauvegarde a le clair en mémoire de toute façon : on teste AVANT de sceller.
|
|
106
|
+
*/
|
|
107
|
+
plainBuffer: Buffer;
|
|
94
108
|
manifest: ArchiveManifest;
|
|
95
109
|
/** Compteurs par entité, au moment du dump — la RÉFÉRENCE du test de restauration. */
|
|
96
110
|
counts: Record<string, number>;
|
|
@@ -174,4 +188,53 @@ export declare function pruneBackups(driver: StorageDriver, bucket: string, site
|
|
|
174
188
|
supprimees: string[];
|
|
175
189
|
conservees: string[];
|
|
176
190
|
}>;
|
|
191
|
+
export interface ScheduledBackupOptions extends Omit<CreateBackupOptions, 'source'> {
|
|
192
|
+
source: DbSource;
|
|
193
|
+
driver: StorageDriver;
|
|
194
|
+
bucket: string;
|
|
195
|
+
/** Planification — RELUE à chaque tick (changement d'heure sans redémarrage). */
|
|
196
|
+
getSchedule: () => Schedule | Promise<Schedule>;
|
|
197
|
+
/** Rétention. Appliquée SEULEMENT après une sauvegarde réussie (voir ci-dessous). */
|
|
198
|
+
retention?: RetentionPolicy;
|
|
199
|
+
/**
|
|
200
|
+
* Tester la restauration après chaque sauvegarde. DÉFAUT : **true**.
|
|
201
|
+
*
|
|
202
|
+
* Une sauvegarde planifiée qui n'est jamais restaurée est un rituel, pas une protection : on
|
|
203
|
+
* accumule deux ans d'archives, et le jour J elles sont inutilisables. Le test restaure dans
|
|
204
|
+
* une base JETABLE et RECOMPTE — il coûte du temps machine, il vaut ce qu'il coûte.
|
|
205
|
+
*/
|
|
206
|
+
verifyRestore?: boolean;
|
|
207
|
+
/** Notifié à chaque cycle — c'est LE point de supervision. */
|
|
208
|
+
onBackup?: (info: {
|
|
209
|
+
ok: boolean;
|
|
210
|
+
backup?: CreatedBackup;
|
|
211
|
+
restoreTest?: RestoreResult;
|
|
212
|
+
pruned?: string[];
|
|
213
|
+
error?: unknown;
|
|
214
|
+
}) => void;
|
|
215
|
+
/** Cadence d'évaluation (ms). Défaut 60 000. */
|
|
216
|
+
intervalMs?: number;
|
|
217
|
+
/** Horloge injectable (tests). */
|
|
218
|
+
now?: () => Date;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Planifie les sauvegardes. Compose `@mostajs/scheduler` — on ne réinvente ni la cadence, ni
|
|
222
|
+
* l'idempotence, ni l'adoption de la période au démarrage.
|
|
223
|
+
*
|
|
224
|
+
* ─── DEUX PIÈGES MORTELS, TRAITÉS ICI ────────────────────────────────────────
|
|
225
|
+
*
|
|
226
|
+
* 1. **La rétention qui purge après un ÉCHEC.** C'est la façon classique de tout perdre : la
|
|
227
|
+
* sauvegarde de ce soir échoue (base verrouillée, disque plein), la rotation s'exécute quand
|
|
228
|
+
* même, et elle supprime la plus ancienne des N archives valides. Répétez l'opération N nuits,
|
|
229
|
+
* et il ne reste RIEN — alors que chaque nuit « la sauvegarde a tourné ».
|
|
230
|
+
* → La purge n'a lieu **QUE** si la nouvelle sauvegarde est créée ET vérifiée.
|
|
231
|
+
*
|
|
232
|
+
* 2. **L'échec silencieux.** Un planificateur qui n'a personne pour écouter ses erreurs a l'air
|
|
233
|
+
* de fonctionner. On ne s'aperçoit de rien jusqu'au jour où l'on cherche une archive qui
|
|
234
|
+
* n'existe pas.
|
|
235
|
+
* → `onBackup` est notifié à CHAQUE cycle, succès **comme** échec. C'est le point de
|
|
236
|
+
* supervision : branchez-y une alerte, sinon vous ne saurez jamais.
|
|
237
|
+
*/
|
|
238
|
+
export declare function scheduleBackups(opts: ScheduledBackupOptions): Scheduler;
|
|
177
239
|
export { readManifest };
|
|
240
|
+
export type { Schedule, Scheduler } from '@mostajs/scheduler';
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { buildArchive, openArchive, verifyArchive, readManifest } from '@mostajs/archive-box';
|
|
32
32
|
import { copyData } from '@mostajs/orm-copy-data';
|
|
33
33
|
import { seal, open as openSealed, sha256 } from '@mostajs/crypto-box';
|
|
34
|
+
import { createScheduler } from '@mostajs/scheduler';
|
|
34
35
|
export const moduleInfo = { name: '@mostajs/backup', version: '0.1.0', level: 'N1' };
|
|
35
36
|
/** Ce que le module NE met JAMAIS dans une archive. */
|
|
36
37
|
export const SECRETS_EXCLUS = ['JWT_SECRET', 'SGBD_URI', 'DB_PASSWORD', 'token', 'password'];
|
|
@@ -96,6 +97,7 @@ export async function createBackup(opts) {
|
|
|
96
97
|
const buffer = sealed ? seal(opts.recipients, built.buffer) : built.buffer;
|
|
97
98
|
return {
|
|
98
99
|
buffer,
|
|
100
|
+
plainBuffer: built.buffer, // jamais déposé — sert au test de restauration
|
|
99
101
|
manifest: built.manifest,
|
|
100
102
|
counts,
|
|
101
103
|
sealed,
|
|
@@ -283,4 +285,67 @@ export async function pruneBackups(driver, bucket, siteId, policy) {
|
|
|
283
285
|
await driver.delete({ bucket, path: b.path });
|
|
284
286
|
return { supprimees: aSupprimer.map((b) => b.path), conservees: conservees.map((b) => b.path) };
|
|
285
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Planifie les sauvegardes. Compose `@mostajs/scheduler` — on ne réinvente ni la cadence, ni
|
|
290
|
+
* l'idempotence, ni l'adoption de la période au démarrage.
|
|
291
|
+
*
|
|
292
|
+
* ─── DEUX PIÈGES MORTELS, TRAITÉS ICI ────────────────────────────────────────
|
|
293
|
+
*
|
|
294
|
+
* 1. **La rétention qui purge après un ÉCHEC.** C'est la façon classique de tout perdre : la
|
|
295
|
+
* sauvegarde de ce soir échoue (base verrouillée, disque plein), la rotation s'exécute quand
|
|
296
|
+
* même, et elle supprime la plus ancienne des N archives valides. Répétez l'opération N nuits,
|
|
297
|
+
* et il ne reste RIEN — alors que chaque nuit « la sauvegarde a tourné ».
|
|
298
|
+
* → La purge n'a lieu **QUE** si la nouvelle sauvegarde est créée ET vérifiée.
|
|
299
|
+
*
|
|
300
|
+
* 2. **L'échec silencieux.** Un planificateur qui n'a personne pour écouter ses erreurs a l'air
|
|
301
|
+
* de fonctionner. On ne s'aperçoit de rien jusqu'au jour où l'on cherche une archive qui
|
|
302
|
+
* n'existe pas.
|
|
303
|
+
* → `onBackup` est notifié à CHAQUE cycle, succès **comme** échec. C'est le point de
|
|
304
|
+
* supervision : branchez-y une alerte, sinon vous ne saurez jamais.
|
|
305
|
+
*/
|
|
306
|
+
export function scheduleBackups(opts) {
|
|
307
|
+
const verifyRestore = opts.verifyRestore !== false;
|
|
308
|
+
return createScheduler({
|
|
309
|
+
getSchedule: opts.getSchedule,
|
|
310
|
+
intervalMs: opts.intervalMs,
|
|
311
|
+
now: opts.now,
|
|
312
|
+
onTrigger: async () => {
|
|
313
|
+
let backup;
|
|
314
|
+
try {
|
|
315
|
+
backup = await createBackup(opts);
|
|
316
|
+
await storeBackup(opts.driver, opts.bucket, backup);
|
|
317
|
+
// Le test de restauration fait partie de la sauvegarde, pas d'une corvée séparée :
|
|
318
|
+
// une archive non testée n'est pas une archive vérifiée.
|
|
319
|
+
let restoreTest;
|
|
320
|
+
if (verifyRestore) {
|
|
321
|
+
// On teste l'archive AVANT scellement. Une archive scellée pour le client ne peut pas
|
|
322
|
+
// être ouverte par la console — c'est le point du modèle. Tester sur `buffer` exigerait
|
|
323
|
+
// donc la clé PRIVÉE du client, exactement ce qu'on refuse de détenir.
|
|
324
|
+
restoreTest = await testRestore(backup.plainBuffer, {
|
|
325
|
+
schemas: opts.source.schemas,
|
|
326
|
+
expectedPublicKey: opts.signingPublicKey,
|
|
327
|
+
});
|
|
328
|
+
if (!restoreTest.verifie) {
|
|
329
|
+
// On NE PURGE PAS : les anciennes archives sont peut-être tout ce qui reste de bon.
|
|
330
|
+
throw new Error(`[backup] sauvegarde créée mais NON RESTAURABLE — rétention suspendue. ${restoreTest.ecarts.join(' · ')}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Purge — uniquement maintenant, la nouvelle sauvegarde étant créée ET vérifiée.
|
|
334
|
+
let pruned;
|
|
335
|
+
if (opts.retention) {
|
|
336
|
+
const r = await pruneBackups(opts.driver, opts.bucket, opts.siteId, opts.retention);
|
|
337
|
+
pruned = r.supprimees;
|
|
338
|
+
}
|
|
339
|
+
opts.onBackup?.({ ok: true, backup, restoreTest, pruned });
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
// La rétention n'a PAS tourné : c'est le point. Un échec ne doit jamais faire disparaître
|
|
343
|
+
// une archive valide.
|
|
344
|
+
opts.onBackup?.({ ok: false, backup, error });
|
|
345
|
+
throw error; // remonte aussi à `onError` du planificateur
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
onError: (e) => { void e; /* déjà remonté par onBackup */ },
|
|
349
|
+
});
|
|
350
|
+
}
|
|
286
351
|
export { readManifest };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/backup",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sauvegarder, vérifier, restaurer — pour de vrai.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Sauvegarder, vérifier, restaurer — pour de vrai. Artefact daté, signé, chiffrable par enveloppe. Sauvegardes PLANIFIÉES avec test de restauration automatique et rétention qui ne purge JAMAIS après un échec. Compose archive-box, orm-copy-data, crypto-box, blob-store et scheduler.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
7
|
"type": "module",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"@mostajs/archive-box": "^0.1.0",
|
|
37
37
|
"@mostajs/blob-store": "^0.3.0",
|
|
38
38
|
"@mostajs/crypto-box": "^0.2.0",
|
|
39
|
-
"@mostajs/orm-copy-data": "^0.4.0"
|
|
39
|
+
"@mostajs/orm-copy-data": "^0.4.0",
|
|
40
|
+
"@mostajs/scheduler": "^0.2.0"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"@mostajs/orm": ">=2.0.0"
|
|
@@ -53,4 +54,4 @@
|
|
|
53
54
|
"sql.js": "^1.14.1",
|
|
54
55
|
"typescript": "^5.6.0"
|
|
55
56
|
}
|
|
56
|
-
}
|
|
57
|
+
}
|