@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 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.1.0",
4
- "description": "Sauvegarder, vérifier, restaurer — pour de vrai. Une copie n'est pas une sauvegarde : artefact daté, signé, chiffrable par enveloppe, avec TEST DE RESTAURATION automatique et recomptage. Compose archive-box, orm-copy-data, crypto-box et blob-store.",
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
+ }