@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 +9 -7
- package/src/internal.ts +48 -1
- package/src/store/__tests__/backup-crypto.test.ts +101 -0
- package/src/store/__tests__/backup-pack.test.ts +491 -0
- package/src/store/__tests__/backup-unpack.test.ts +298 -0
- package/src/store/__tests__/regenerators.test.ts +234 -0
- package/src/store/__tests__/restore-conflict-report.test.ts +274 -0
- package/src/store/__tests__/restore-json-merge.test.ts +521 -0
- package/src/store/__tests__/t310-readiness.test.ts +111 -0
- package/src/store/__tests__/t311-integration.test.ts +661 -0
- package/src/store/backup-crypto.ts +209 -0
- package/src/store/backup-pack.ts +739 -0
- package/src/store/backup-unpack.ts +583 -0
- package/src/store/regenerators.ts +243 -0
- package/src/store/restore-conflict-report.ts +317 -0
- package/src/store/restore-json-merge.ts +653 -0
- package/src/store/t310-readiness.ts +119 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/core",
|
|
3
|
-
"version": "2026.4.
|
|
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.
|
|
39
|
-
"@cleocode/agents": "2026.4.
|
|
40
|
-
"@cleocode/contracts": "2026.4.
|
|
41
|
-
"@cleocode/caamp": "2026.4.
|
|
42
|
-
"@cleocode/lafs": "2026.4.
|
|
43
|
-
"@cleocode/skills": "2026.4.
|
|
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 {
|
|
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
|
+
});
|