@cleocode/core 2026.4.12 → 2026.4.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/core",
3
- "version": "2026.4.12",
3
+ "version": "2026.4.13",
4
4
  "description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,15 +32,16 @@
32
32
  "pino": "^10.3.1",
33
33
  "pino-roll": "^4.0.0",
34
34
  "proper-lockfile": "^4.1.2",
35
+ "tar": "^7.4.3",
35
36
  "write-file-atomic": "^6.0.0",
36
37
  "yaml": "^2.8.2",
37
38
  "zod": "^3.25.76",
38
- "@cleocode/adapters": "2026.4.12",
39
- "@cleocode/agents": "2026.4.12",
40
- "@cleocode/contracts": "2026.4.12",
41
- "@cleocode/caamp": "2026.4.12",
42
- "@cleocode/lafs": "2026.4.12",
43
- "@cleocode/skills": "2026.4.12"
39
+ "@cleocode/adapters": "2026.4.13",
40
+ "@cleocode/agents": "2026.4.13",
41
+ "@cleocode/contracts": "2026.4.13",
42
+ "@cleocode/caamp": "2026.4.13",
43
+ "@cleocode/lafs": "2026.4.13",
44
+ "@cleocode/skills": "2026.4.13"
44
45
  },
45
46
  "optionalDependencies": {
46
47
  "tree-sitter-c": "^0.24.1",
@@ -70,6 +71,7 @@
70
71
  ],
71
72
  "devDependencies": {
72
73
  "@types/proper-lockfile": "^4.1.4",
74
+ "@types/tar": "^6.1.13",
73
75
  "@types/write-file-atomic": "^4.0.3"
74
76
  },
75
77
  "repository": {
package/src/internal.ts CHANGED
@@ -410,6 +410,9 @@ export { listStickies, purgeSticky } from './sticky/index.js';
410
410
  export type { CreateStickyParams, ListStickiesParams, StickyNote } from './sticky/types.js';
411
411
  // Store
412
412
  export { createBackup, listBackups, restoreFromBackup } from './store/backup.js';
413
+ // Backup portability — bundle packer (T311 / T347)
414
+ export type { PackBundleInput, PackBundleResult } from './store/backup-pack.js';
415
+ export { packBundle } from './store/backup-pack.js';
413
416
  export { getBrainDb, getBrainNativeDb } from './store/brain-sqlite.js';
414
417
  export type { LegacyCleanupResult, StrayNexusCleanupResult } from './store/cleanup-legacy.js';
415
418
  export {
@@ -442,7 +445,11 @@ export {
442
445
  SIGNALDOCK_SCHEMA_VERSION,
443
446
  } from './store/signaldock-sqlite.js';
444
447
  export { getDb, getNativeDb } from './store/sqlite.js';
445
- export type { BackupScope, GlobalBackupEntry, GlobalSaltBackupEntry } from './store/sqlite-backup.js';
448
+ export type {
449
+ BackupScope,
450
+ GlobalBackupEntry,
451
+ GlobalSaltBackupEntry,
452
+ } from './store/sqlite-backup.js';
446
453
  export {
447
454
  backupGlobalSalt,
448
455
  listBrainBackups,
@@ -696,11 +703,51 @@ export { readSnapshot } from './snapshot/index.js';
696
703
  export { addSticky } from './sticky/create.js';
697
704
  export { getSticky } from './sticky/index.js';
698
705
 
706
+ // Store — backup crypto (T363)
707
+ export { decryptBundle, encryptBundle, isEncryptedBundle } from './store/backup-crypto.js';
708
+
699
709
  // Store (additional)
700
710
  export { resolveProjectRoot } from './store/file-utils.js';
701
711
  export { TASK_PRIORITIES } from './store/tasks-schema.js';
702
712
  // System (additional)
703
713
  export type { MigrateResult } from './system/index.js';
714
+
715
+ // ---------------------------------------------------------------------------
716
+ // T311 Backup portability — unpack + verify + A/B restore (T350, T352, T354, T357)
717
+ // ---------------------------------------------------------------------------
718
+
719
+ // Unpack + verify (T350)
720
+ export type {
721
+ SchemaCompatWarning as BundleSchemaCompatWarning,
722
+ UnpackBundleInput,
723
+ UnpackBundleResult,
724
+ } from './store/backup-unpack.js';
725
+ export { BundleError, cleanupStaging, unpackBundle } from './store/backup-unpack.js';
726
+ // Dry-run JSON file generators (T352)
727
+ export type { RegeneratedFile } from './store/regenerators.js';
728
+ export {
729
+ regenerateAllJson,
730
+ regenerateConfigJson,
731
+ regenerateProjectContextJson,
732
+ regenerateProjectInfoJson,
733
+ } from './store/regenerators.js';
734
+ // Conflict report formatter (T357)
735
+ export type {
736
+ BuildConflictReportInput,
737
+ ReauthWarning,
738
+ SchemaCompatWarning as RestoreSchemaCompatWarning,
739
+ } from './store/restore-conflict-report.js';
740
+ export { buildConflictReport, writeConflictReport } from './store/restore-conflict-report.js';
741
+ // A/B regenerate-and-compare engine (T354)
742
+ export type {
743
+ FieldCategory,
744
+ FieldClassification,
745
+ FilenameForRestore,
746
+ JsonRestoreReport,
747
+ RegenerateAndCompareInput,
748
+ Resolution,
749
+ } from './store/restore-json-merge.js';
750
+ export { regenerateAndCompare, regenerateAndCompareAll } from './store/restore-json-merge.js';
704
751
  // Tasks (additional — stats)
705
752
  export { coreTaskStats } from './tasks/task-ops.js';
706
753
 
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Unit tests for backup-crypto.ts (T345).
3
+ *
4
+ * Covers: round-trip correctness, magic/version header, ciphertext
5
+ * non-determinism (random salt + nonce), error paths (wrong passphrase,
6
+ * truncated payload, bit-flip tamper, magic mismatch, too-short), and
7
+ * isEncryptedBundle detection.
8
+ *
9
+ * @task T345
10
+ * @epic T311
11
+ */
12
+
13
+ import crypto from 'node:crypto';
14
+ import { describe, expect, it } from 'vitest';
15
+ import { decryptBundle, encryptBundle, isEncryptedBundle } from '../backup-crypto.js';
16
+
17
+ describe('backup-crypto', () => {
18
+ const plaintext = Buffer.from('Hello, CLEO backup world! 🌍 Non-ASCII: ñoño café');
19
+ const passphrase = 'correct horse battery staple';
20
+
21
+ it('round-trip: decrypt(encrypt(x)) == x', () => {
22
+ const encrypted = encryptBundle(plaintext, passphrase);
23
+ const decrypted = decryptBundle(encrypted, passphrase);
24
+ expect(decrypted.equals(plaintext)).toBe(true);
25
+ });
26
+
27
+ it('encryptBundle output starts with CLEOENC1 magic', () => {
28
+ const enc = encryptBundle(plaintext, passphrase);
29
+ expect(enc.subarray(0, 8).toString('utf8')).toBe('CLEOENC1');
30
+ });
31
+
32
+ it('encryptBundle output has version byte 0x01 at offset 8', () => {
33
+ const enc = encryptBundle(plaintext, passphrase);
34
+ expect(enc[8]).toBe(0x01);
35
+ });
36
+
37
+ it('two encryptions of same input differ (random salt + nonce)', () => {
38
+ const e1 = encryptBundle(plaintext, passphrase);
39
+ const e2 = encryptBundle(plaintext, passphrase);
40
+ expect(e1.equals(e2)).toBe(false);
41
+ });
42
+
43
+ it('decryptBundle throws on wrong passphrase', () => {
44
+ const enc = encryptBundle(plaintext, passphrase);
45
+ expect(() => decryptBundle(enc, 'wrong passphrase')).toThrow(/authentication failed/);
46
+ });
47
+
48
+ it('decryptBundle throws on truncated ciphertext (auth tag missing)', () => {
49
+ const enc = encryptBundle(plaintext, passphrase);
50
+ const truncated = enc.subarray(0, enc.length - 1);
51
+ expect(() => decryptBundle(truncated, passphrase)).toThrow();
52
+ });
53
+
54
+ it('decryptBundle throws on flipped bit (auth tag fails)', () => {
55
+ const enc = encryptBundle(plaintext, passphrase);
56
+ // Flip a byte in the middle of the ciphertext region
57
+ const tampered = Buffer.from(enc);
58
+ const idx = 16 + 32 + 12 + 5; // well inside ciphertext region
59
+ tampered[idx] ^= 0x01;
60
+ expect(() => decryptBundle(tampered, passphrase)).toThrow(/authentication failed/);
61
+ });
62
+
63
+ it('decryptBundle throws on magic mismatch', () => {
64
+ const bogus = Buffer.alloc(128);
65
+ expect(() => decryptBundle(bogus, passphrase)).toThrow(/magic mismatch/);
66
+ });
67
+
68
+ it('decryptBundle throws on payload too short', () => {
69
+ const tiny = Buffer.alloc(32);
70
+ expect(() => decryptBundle(tiny, passphrase)).toThrow(/too short/);
71
+ });
72
+
73
+ it('encryptBundle throws on empty passphrase', () => {
74
+ expect(() => encryptBundle(plaintext, '')).toThrow(/passphrase/);
75
+ });
76
+
77
+ it('isEncryptedBundle returns true for magic header', () => {
78
+ const enc = encryptBundle(plaintext, passphrase);
79
+ expect(isEncryptedBundle(enc.subarray(0, 8))).toBe(true);
80
+ });
81
+
82
+ it('isEncryptedBundle returns false for random data', () => {
83
+ const random = crypto.randomBytes(32);
84
+ expect(isEncryptedBundle(random)).toBe(false);
85
+ });
86
+
87
+ it('handles large payloads (1 MB)', () => {
88
+ const large = crypto.randomBytes(1024 * 1024);
89
+ const enc = encryptBundle(large, passphrase);
90
+ const dec = decryptBundle(enc, passphrase);
91
+ expect(dec.equals(large)).toBe(true);
92
+ });
93
+
94
+ it('scrypt derivation is deterministic for same passphrase + salt', () => {
95
+ // Indirectly verified: two independent round-trips both recover the plaintext.
96
+ const e1 = encryptBundle(plaintext, passphrase);
97
+ const e2 = encryptBundle(plaintext, passphrase);
98
+ expect(decryptBundle(e1, passphrase).equals(plaintext)).toBe(true);
99
+ expect(decryptBundle(e2, passphrase).equals(plaintext)).toBe(true);
100
+ });
101
+ });