@remnic/core 9.3.543 → 9.3.545

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.
Files changed (81) hide show
  1. package/dist/access-cli.js +15 -15
  2. package/dist/access-http.js +9 -9
  3. package/dist/access-mcp.js +8 -8
  4. package/dist/access-schema.js +3 -3
  5. package/dist/access-service.js +6 -6
  6. package/dist/briefing.js +3 -3
  7. package/dist/causal-consolidation.js +4 -4
  8. package/dist/{chunk-FWYUJY4N.js → chunk-25BY3HHZ.js} +2 -2
  9. package/dist/{chunk-K6OABSBA.js → chunk-5GX5MUQ2.js} +2 -2
  10. package/dist/{chunk-NHZHAZCJ.js → chunk-5RIRL3XL.js} +2 -2
  11. package/dist/{chunk-QKV4LVLA.js → chunk-5WLYNZPC.js} +2 -2
  12. package/dist/{chunk-PR577DSG.js → chunk-6PDPQLSV.js} +12 -12
  13. package/dist/{chunk-NJBIGWAI.js → chunk-ACCAZOBX.js} +2 -2
  14. package/dist/{chunk-BRCYNT4I.js → chunk-CC2ESOOG.js} +2 -2
  15. package/dist/{chunk-QWQX7YK5.js → chunk-E5OECWZ5.js} +2 -2
  16. package/dist/{chunk-J62VXZR2.js → chunk-FADZBOR4.js} +2 -2
  17. package/dist/{chunk-GWUUEPOR.js → chunk-FVCZINOF.js} +2 -2
  18. package/dist/{chunk-73JGZ5VA.js → chunk-ILXTATKK.js} +47 -20
  19. package/dist/chunk-ILXTATKK.js.map +1 -0
  20. package/dist/{chunk-FXE46BJ5.js → chunk-JFN6K74Q.js} +2 -2
  21. package/dist/{chunk-ENIIJ3MZ.js → chunk-M3BAMVAE.js} +2 -2
  22. package/dist/{chunk-QCJLDMY5.js → chunk-OF46AKZC.js} +8 -8
  23. package/dist/{chunk-7NEW7PTS.js → chunk-QFGWYIB6.js} +5 -5
  24. package/dist/{chunk-LKUNOD7B.js → chunk-S53PAX2V.js} +2 -2
  25. package/dist/{chunk-INMWM3UZ.js → chunk-SCKIZM6L.js} +4 -4
  26. package/dist/{chunk-3M7OTJ5H.js → chunk-SI3QCHWF.js} +4 -4
  27. package/dist/{chunk-UMXWQL3P.js → chunk-SOTR74FK.js} +2 -2
  28. package/dist/{chunk-A5TLPLUO.js → chunk-TVZ6LKKS.js} +2 -2
  29. package/dist/{chunk-25XNFWT3.js → chunk-VKVKH6XO.js} +2 -2
  30. package/dist/{chunk-GAMKRZRP.js → chunk-XOSQ4MRE.js} +5 -5
  31. package/dist/{chunk-RGLJNOQN.js → chunk-Y4YATXHL.js} +3 -3
  32. package/dist/{chunk-XE23FSDQ.js → chunk-Z56KAZQL.js} +2 -2
  33. package/dist/cli.js +18 -18
  34. package/dist/compounding/engine.js +3 -3
  35. package/dist/connectors/codex-materialize-runner.js +3 -3
  36. package/dist/connectors/index.js +3 -3
  37. package/dist/entity-retrieval.js +3 -3
  38. package/dist/index.js +24 -24
  39. package/dist/maintenance/memory-governance.js +3 -3
  40. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  41. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  42. package/dist/namespaces/migrate.js +4 -4
  43. package/dist/namespaces/storage.js +3 -3
  44. package/dist/offline-sync.js +2 -2
  45. package/dist/operator-toolkit.js +6 -6
  46. package/dist/orchestrator.js +11 -11
  47. package/dist/schemas.d.ts +22 -22
  48. package/dist/secure-store/index.js +2 -2
  49. package/dist/semantic-consolidation.js +4 -4
  50. package/dist/semantic-rule-promotion.js +3 -3
  51. package/dist/semantic-rule-verifier.js +3 -3
  52. package/dist/storage.js +2 -2
  53. package/dist/transfer/types.d.ts +12 -12
  54. package/dist/verified-recall.js +3 -3
  55. package/package.json +1 -1
  56. package/src/secure-store/secure-fs.ts +63 -38
  57. package/src/secure-store/secure-store.test.ts +155 -106
  58. package/dist/chunk-73JGZ5VA.js.map +0 -1
  59. /package/dist/{chunk-FWYUJY4N.js.map → chunk-25BY3HHZ.js.map} +0 -0
  60. /package/dist/{chunk-K6OABSBA.js.map → chunk-5GX5MUQ2.js.map} +0 -0
  61. /package/dist/{chunk-NHZHAZCJ.js.map → chunk-5RIRL3XL.js.map} +0 -0
  62. /package/dist/{chunk-QKV4LVLA.js.map → chunk-5WLYNZPC.js.map} +0 -0
  63. /package/dist/{chunk-PR577DSG.js.map → chunk-6PDPQLSV.js.map} +0 -0
  64. /package/dist/{chunk-NJBIGWAI.js.map → chunk-ACCAZOBX.js.map} +0 -0
  65. /package/dist/{chunk-BRCYNT4I.js.map → chunk-CC2ESOOG.js.map} +0 -0
  66. /package/dist/{chunk-QWQX7YK5.js.map → chunk-E5OECWZ5.js.map} +0 -0
  67. /package/dist/{chunk-J62VXZR2.js.map → chunk-FADZBOR4.js.map} +0 -0
  68. /package/dist/{chunk-GWUUEPOR.js.map → chunk-FVCZINOF.js.map} +0 -0
  69. /package/dist/{chunk-FXE46BJ5.js.map → chunk-JFN6K74Q.js.map} +0 -0
  70. /package/dist/{chunk-ENIIJ3MZ.js.map → chunk-M3BAMVAE.js.map} +0 -0
  71. /package/dist/{chunk-QCJLDMY5.js.map → chunk-OF46AKZC.js.map} +0 -0
  72. /package/dist/{chunk-7NEW7PTS.js.map → chunk-QFGWYIB6.js.map} +0 -0
  73. /package/dist/{chunk-LKUNOD7B.js.map → chunk-S53PAX2V.js.map} +0 -0
  74. /package/dist/{chunk-INMWM3UZ.js.map → chunk-SCKIZM6L.js.map} +0 -0
  75. /package/dist/{chunk-3M7OTJ5H.js.map → chunk-SI3QCHWF.js.map} +0 -0
  76. /package/dist/{chunk-UMXWQL3P.js.map → chunk-SOTR74FK.js.map} +0 -0
  77. /package/dist/{chunk-A5TLPLUO.js.map → chunk-TVZ6LKKS.js.map} +0 -0
  78. /package/dist/{chunk-25XNFWT3.js.map → chunk-VKVKH6XO.js.map} +0 -0
  79. /package/dist/{chunk-GAMKRZRP.js.map → chunk-XOSQ4MRE.js.map} +0 -0
  80. /package/dist/{chunk-RGLJNOQN.js.map → chunk-Y4YATXHL.js.map} +0 -0
  81. /package/dist/{chunk-XE23FSDQ.js.map → chunk-Z56KAZQL.js.map} +0 -0
@@ -14,11 +14,11 @@
14
14
  * silently regress them.
15
15
  */
16
16
 
17
- import { PassThrough } from "node:stream";
18
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
17
+ import assert from "node:assert/strict";
18
+ import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
19
19
  import { tmpdir } from "node:os";
20
20
  import path from "node:path";
21
- import assert from "node:assert/strict";
21
+ import { PassThrough } from "node:stream";
22
22
  import test from "node:test";
23
23
 
24
24
  import {
@@ -32,6 +32,7 @@ import {
32
32
  parseEnvelope,
33
33
  seal,
34
34
  } from "./cipher.js";
35
+ import { renderInitReport, renderLockReport, renderStatusReport, renderUnlockReport } from "./cli-renderer.js";
35
36
  import {
36
37
  HEADER_FORMAT,
37
38
  HEADER_FORMAT_VERSION,
@@ -40,30 +41,18 @@ import {
40
41
  validateHeader,
41
42
  } from "./header.js";
42
43
  import {
43
- FILE_FORMAT_FLAGS,
44
- FILE_FORMAT_VERSION,
45
- MAGIC_BYTES,
46
- SecureStoreDecryptError,
47
- decryptFileBody,
48
- decryptMemoryDirToPlaintext,
49
- encryptFileBody,
50
- filePathAad,
51
- migrateMemoryDirToEncrypted,
52
- readMaybeEncryptedFile,
53
- } from "./secure-fs.js";
54
- import {
44
+ type Argon2idParams,
55
45
  DEFAULT_ARGON2ID_PARAMS,
56
46
  DEFAULT_SCRYPT_PARAMS,
57
47
  KDF_KEY_LENGTH,
58
48
  KDF_SALT_LENGTH,
49
+ type ScryptParams,
59
50
  constantTimeEqual,
60
51
  deriveKey,
61
52
  deriveKeyArgon2id,
62
53
  deriveKeyScrypt,
63
54
  validateArgon2idParams,
64
55
  validateScryptParams,
65
- type Argon2idParams,
66
- type ScryptParams,
67
56
  } from "./kdf.js";
68
57
  import {
69
58
  METADATA_FORMAT,
@@ -76,11 +65,18 @@ import {
76
65
  } from "./metadata.js";
77
66
  import { createPassphraseReader } from "./passphrase-reader.js";
78
67
  import {
79
- renderInitReport,
80
- renderLockReport,
81
- renderStatusReport,
82
- renderUnlockReport,
83
- } from "./cli-renderer.js";
68
+ FILE_FORMAT_FLAGS,
69
+ FILE_FORMAT_VERSION,
70
+ MAGIC_BYTES,
71
+ SecureStoreDecryptError,
72
+ decryptFileBody,
73
+ decryptMemoryDirToPlaintext,
74
+ encryptFileBody,
75
+ filePathAad,
76
+ isEncryptedFile,
77
+ migrateMemoryDirToEncrypted,
78
+ readMaybeEncryptedFile,
79
+ } from "./secure-fs.js";
84
80
 
85
81
  /** Cheap scrypt params for tests — still hex-correct but ~milliseconds. */
86
82
  const FAST_SCRYPT: ScryptParams = {
@@ -153,7 +149,7 @@ test("secure-store CLI renderers describe process-local unlock scope, not daemon
153
149
  unlockedAt: "2026-05-21T00:00:01.000Z",
154
150
  algorithm: "scrypt",
155
151
  }),
156
- /unlocked in this process/,
152
+ /unlocked in this process/
157
153
  );
158
154
  assert.match(renderLockReport({ ok: true, cleared: true }), /this process's in-memory keyring/);
159
155
  assert.match(
@@ -169,7 +165,7 @@ test("secure-store CLI renderers describe process-local unlock scope, not daemon
169
165
  salt: Buffer.alloc(KDF_SALT_LENGTH, 0x22).toString("hex"),
170
166
  },
171
167
  }),
172
- /lockedInThisProcess: no/,
168
+ /lockedInThisProcess: no/
173
169
  );
174
170
  });
175
171
 
@@ -179,10 +175,7 @@ test("deriveKeyScrypt rejects empty passphrase", () => {
179
175
  });
180
176
 
181
177
  test("deriveKeyScrypt rejects too-short salt", () => {
182
- assert.throws(
183
- () => deriveKeyScrypt("pw", Buffer.alloc(4, 0), FAST_SCRYPT),
184
- /salt/,
185
- );
178
+ assert.throws(() => deriveKeyScrypt("pw", Buffer.alloc(4, 0), FAST_SCRYPT), /salt/);
186
179
  });
187
180
 
188
181
  test("deriveKey('scrypt') dispatches to scryptSync", () => {
@@ -219,29 +212,14 @@ test("deriveKey('argon2id') dispatches to Argon2id", () => {
219
212
  });
220
213
 
221
214
  test("validateArgon2idParams rejects invalid values", () => {
222
- assert.throws(
223
- () => validateArgon2idParams({ ...FAST_ARGON2ID, memoryKiB: 0 }),
224
- /memoryKiB/,
225
- );
226
- assert.throws(
227
- () => validateArgon2idParams({ ...FAST_ARGON2ID, iterations: 0 }),
228
- /iterations/,
229
- );
230
- assert.throws(
231
- () => validateArgon2idParams({ ...FAST_ARGON2ID, parallelism: 0 }),
232
- /parallelism/,
233
- );
234
- assert.throws(
235
- () => validateArgon2idParams({ ...FAST_ARGON2ID, keyLength: 16 }),
236
- /keyLength/,
237
- );
215
+ assert.throws(() => validateArgon2idParams({ ...FAST_ARGON2ID, memoryKiB: 0 }), /memoryKiB/);
216
+ assert.throws(() => validateArgon2idParams({ ...FAST_ARGON2ID, iterations: 0 }), /iterations/);
217
+ assert.throws(() => validateArgon2idParams({ ...FAST_ARGON2ID, parallelism: 0 }), /parallelism/);
218
+ assert.throws(() => validateArgon2idParams({ ...FAST_ARGON2ID, keyLength: 16 }), /keyLength/);
238
219
  });
239
220
 
240
221
  test("validateScryptParams rejects non-power-of-2 N", () => {
241
- assert.throws(
242
- () => validateScryptParams({ ...FAST_SCRYPT, N: 1000 }),
243
- /power of 2/,
244
- );
222
+ assert.throws(() => validateScryptParams({ ...FAST_SCRYPT, N: 1000 }), /power of 2/);
245
223
  });
246
224
 
247
225
  test("validateScryptParams rejects N < 2", () => {
@@ -312,7 +290,7 @@ test("decryptFileBody reports truncated envelopes as structural errors", () => {
312
290
  assert.equal(error instanceof SecureStoreDecryptError, false);
313
291
  assert.match(error.message, /envelope too short/);
314
292
  return true;
315
- },
293
+ }
316
294
  );
317
295
  });
318
296
 
@@ -335,10 +313,7 @@ test("seal envelope layout matches the documented format", () => {
335
313
  const sealed = seal(key, salt, Buffer.from("payload"));
336
314
  assert.equal(sealed[ENVELOPE_LAYOUT.version], ENVELOPE_VERSION);
337
315
  // Salt bytes round-trip exactly.
338
- const saltSlice = sealed.subarray(
339
- ENVELOPE_LAYOUT.salt,
340
- ENVELOPE_LAYOUT.salt + ENVELOPE_SALT_LENGTH,
341
- );
316
+ const saltSlice = sealed.subarray(ENVELOPE_LAYOUT.salt, ENVELOPE_LAYOUT.salt + ENVELOPE_SALT_LENGTH);
342
317
  assert.ok(saltSlice.equals(salt));
343
318
  // Total length = header + ciphertext("payload" is 7 bytes).
344
319
  assert.equal(sealed.length, ENVELOPE_HEADER_SIZE + 7);
@@ -419,10 +394,7 @@ test("open fails when AAD is mismatched", () => {
419
394
 
420
395
  test("seal rejects key of wrong length", () => {
421
396
  const salt = generateSalt();
422
- assert.throws(
423
- () => seal(Buffer.alloc(16), salt, Buffer.from("x")),
424
- /AES-256-GCM/,
425
- );
397
+ assert.throws(() => seal(Buffer.alloc(16), salt, Buffer.from("x")), /AES-256-GCM/);
426
398
  });
427
399
 
428
400
  test("seal rejects salt of wrong length", () => {
@@ -494,10 +466,7 @@ test("buildMetadata defaults createdAt to a parseable ISO string when omitted",
494
466
  });
495
467
 
496
468
  test("buildMetadata rejects salt of wrong length", () => {
497
- assert.throws(
498
- () => buildMetadata({ algorithm: "scrypt", salt: Buffer.alloc(8) }),
499
- /salt/,
500
- );
469
+ assert.throws(() => buildMetadata({ algorithm: "scrypt", salt: Buffer.alloc(8) }), /salt/);
501
470
  });
502
471
 
503
472
  test("parseMetadata rejects non-JSON input", () => {
@@ -600,11 +569,8 @@ test("serializeMetadata produces stable top-level key order", () => {
600
569
  const kdfIdx = json.indexOf('"kdf"');
601
570
  const createdAtIdx = json.indexOf('"createdAt"');
602
571
  assert.ok(
603
- formatIdx >= 0 &&
604
- formatIdx < formatVersionIdx &&
605
- formatVersionIdx < kdfIdx &&
606
- kdfIdx < createdAtIdx,
607
- `unexpected top-level key order in: ${json}`,
572
+ formatIdx >= 0 && formatIdx < formatVersionIdx && formatVersionIdx < kdfIdx && kdfIdx < createdAtIdx,
573
+ `unexpected top-level key order in: ${json}`
608
574
  );
609
575
  });
610
576
 
@@ -641,10 +607,7 @@ test("secure-store migration keeps namespaced file AAD readable through memory r
641
607
 
642
608
  assert.equal(migrated.encrypted, 1);
643
609
  assert.deepEqual(migrated.errors, []);
644
- assert.equal(
645
- await readMaybeEncryptedFile(filePath, key, memoryDir),
646
- "namespaced fact",
647
- );
610
+ assert.equal(await readMaybeEncryptedFile(filePath, key, memoryDir), "namespaced fact");
648
611
 
649
612
  const decrypted = await decryptMemoryDirToPlaintext(memoryDir, key);
650
613
  assert.equal(decrypted.decrypted, 1);
@@ -655,6 +618,117 @@ test("secure-store migration keeps namespaced file AAD readable through memory r
655
618
  }
656
619
  });
657
620
 
621
+ test("secure-store migration treats missing memory roots as empty", async () => {
622
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-missing-root-"));
623
+ try {
624
+ const missingMemoryDir = path.join(tempRoot, "missing-memory");
625
+ const key = deriveKeyScrypt("missing-root", Buffer.alloc(KDF_SALT_LENGTH, 0x55), FAST_SCRYPT);
626
+
627
+ const migrated = await migrateMemoryDirToEncrypted(missingMemoryDir, key);
628
+ assert.deepEqual(migrated, { encrypted: 0, skipped: 0, errors: [] });
629
+
630
+ const decrypted = await decryptMemoryDirToPlaintext(missingMemoryDir, key);
631
+ assert.deepEqual(decrypted, { decrypted: 0, skipped: 0, errors: [] });
632
+ } finally {
633
+ await rm(tempRoot, { recursive: true, force: true });
634
+ }
635
+ });
636
+
637
+ test("secure-store migration scans the normalized root for symlink dot-dot paths", async () => {
638
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-root-dotdot-"));
639
+ try {
640
+ const baseDir = path.join(tempRoot, "base");
641
+ const outsideParent = path.join(tempRoot, "outside-parent");
642
+ const outsideTarget = path.join(outsideParent, "target");
643
+ const memoryLink = path.join(baseDir, "memory-link");
644
+ const traversalRoot = `${memoryLink}${path.sep}..`;
645
+ const insideFilePath = path.join(baseDir, "facts", "note.md");
646
+ const outsideFilePath = path.join(outsideParent, "facts", "note.md");
647
+ await mkdir(path.dirname(insideFilePath), { recursive: true });
648
+ await mkdir(path.dirname(outsideFilePath), { recursive: true });
649
+ await mkdir(outsideTarget, { recursive: true });
650
+ await writeFile(insideFilePath, "inside fact", "utf8");
651
+ await writeFile(outsideFilePath, "outside fact", "utf8");
652
+ await symlink(outsideTarget, memoryLink, "dir");
653
+
654
+ const key = deriveKeyScrypt("dotdot-root-symlink", Buffer.alloc(KDF_SALT_LENGTH, 0x56), FAST_SCRYPT);
655
+ const migrated = await migrateMemoryDirToEncrypted(traversalRoot, key);
656
+
657
+ assert.equal(migrated.encrypted, 1);
658
+ assert.equal(isEncryptedFile(await readFile(insideFilePath)), true);
659
+ assert.equal(await readFile(outsideFilePath, "utf8"), "outside fact");
660
+
661
+ const decrypted = await decryptMemoryDirToPlaintext(traversalRoot, key);
662
+ assert.equal(decrypted.decrypted, 1);
663
+ assert.equal(await readFile(insideFilePath, "utf8"), "inside fact");
664
+ assert.equal(await readFile(outsideFilePath, "utf8"), "outside fact");
665
+ } finally {
666
+ await rm(tempRoot, { recursive: true, force: true });
667
+ }
668
+ });
669
+
670
+ test("secure-store migration rejects symlinked memory roots without rewriting the target", async () => {
671
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-root-symlink-"));
672
+ try {
673
+ const outsideDir = path.join(tempRoot, "outside");
674
+ const memoryDir = path.join(tempRoot, "memory-link");
675
+ const filePath = path.join(outsideDir, "facts", "note.md");
676
+ await mkdir(path.dirname(filePath), { recursive: true });
677
+ await writeFile(filePath, "outside fact", "utf8");
678
+ await symlink(outsideDir, memoryDir, "dir");
679
+
680
+ const key = deriveKeyScrypt("root-symlink", Buffer.alloc(KDF_SALT_LENGTH, 0x66), FAST_SCRYPT);
681
+ await assert.rejects(
682
+ () => migrateMemoryDirToEncrypted(`${memoryDir}${path.sep}`, key),
683
+ /root must not be a symlink/
684
+ );
685
+ await assert.rejects(
686
+ () => migrateMemoryDirToEncrypted(`${memoryDir}${path.sep}.`, key),
687
+ /root must not be a symlink/
688
+ );
689
+
690
+ assert.equal(await readFile(filePath, "utf8"), "outside fact");
691
+ } finally {
692
+ await rm(tempRoot, { recursive: true, force: true });
693
+ }
694
+ });
695
+
696
+ test("secure-store plaintext migration rejects symlinked memory roots without rewriting the target", async () => {
697
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-decrypt-root-symlink-"));
698
+ try {
699
+ const outsideDir = path.join(tempRoot, "outside");
700
+ const memoryDir = path.join(tempRoot, "memory-link");
701
+ const realFilePath = path.join(outsideDir, "facts", "note.md");
702
+ const linkedFilePath = path.join(memoryDir, "facts", "note.md");
703
+ await mkdir(path.dirname(realFilePath), { recursive: true });
704
+ await symlink(outsideDir, memoryDir, "dir");
705
+
706
+ const key = deriveKeyScrypt("decrypt-root-symlink", Buffer.alloc(KDF_SALT_LENGTH, 0x77), FAST_SCRYPT);
707
+ await writeFile(
708
+ realFilePath,
709
+ encryptFileBody("outside encrypted fact", key, filePathAad(linkedFilePath, memoryDir))
710
+ );
711
+
712
+ await assert.rejects(
713
+ () => decryptMemoryDirToPlaintext(`${memoryDir}${path.sep}`, key),
714
+ /root must not be a symlink/
715
+ );
716
+ await assert.rejects(
717
+ () => decryptMemoryDirToPlaintext(`${memoryDir}${path.sep}.`, key),
718
+ /root must not be a symlink/
719
+ );
720
+
721
+ const stillEncrypted = await readFile(realFilePath);
722
+ assert.equal(isEncryptedFile(stillEncrypted), true);
723
+ assert.equal(
724
+ decryptFileBody(stillEncrypted, key, filePathAad(linkedFilePath, memoryDir)).toString("utf8"),
725
+ "outside encrypted fact"
726
+ );
727
+ } finally {
728
+ await rm(tempRoot, { recursive: true, force: true });
729
+ }
730
+ });
731
+
658
732
  test("secure-store can recover namespaced files encrypted with legacy namespace AAD", async () => {
659
733
  const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-legacy-aad-"));
660
734
  try {
@@ -663,17 +737,10 @@ test("secure-store can recover namespaced files encrypted with legacy namespace
663
737
  await mkdir(path.dirname(filePath), { recursive: true });
664
738
 
665
739
  const key = deriveKeyScrypt("legacy-namespace-aad", Buffer.alloc(KDF_SALT_LENGTH, 0x55), FAST_SCRYPT);
666
- const legacyEncrypted = encryptFileBody(
667
- "legacy namespaced fact",
668
- key,
669
- filePathAad(filePath, namespaceRoot),
670
- );
740
+ const legacyEncrypted = encryptFileBody("legacy namespaced fact", key, filePathAad(filePath, namespaceRoot));
671
741
  await writeFile(filePath, legacyEncrypted);
672
742
 
673
- assert.equal(
674
- await readMaybeEncryptedFile(filePath, key, memoryDir),
675
- "legacy namespaced fact",
676
- );
743
+ assert.equal(await readMaybeEncryptedFile(filePath, key, memoryDir), "legacy namespaced fact");
677
744
 
678
745
  const decrypted = await decryptMemoryDirToPlaintext(memoryDir, key);
679
746
  assert.equal(decrypted.decrypted, 1);
@@ -703,7 +770,7 @@ test("metadata rejects keyLength != 32 (codex P2 — match cipher AES-256)", ()
703
770
  meta.kdf.params.keyLength = 16;
704
771
  assert.throws(
705
772
  () => validateMetadata(meta as unknown as Parameters<typeof validateMetadata>[0]),
706
- /keyLength must be 32/,
773
+ /keyLength must be 32/
707
774
  );
708
775
  });
709
776
 
@@ -722,16 +789,11 @@ test("parseHeader rejects odd-length verifier hex (thread 6 — even-length guar
722
789
  const badJson = JSON.stringify({
723
790
  format: HEADER_FORMAT,
724
791
  formatVersion: HEADER_FORMAT_VERSION,
725
- metadata: JSON.parse(
726
- JSON.stringify(header.metadata),
727
- ),
792
+ metadata: JSON.parse(JSON.stringify(header.metadata)),
728
793
  verifier: header.verifier.slice(0, -1), // odd length
729
794
  createdAt: header.createdAt,
730
795
  });
731
- assert.throws(
732
- () => parseHeader(badJson),
733
- /even length|even/i,
734
- );
796
+ assert.throws(() => parseHeader(badJson), /even length|even/i);
735
797
  });
736
798
 
737
799
  test("validateHeader rejects odd-length verifier hex (thread 6 — even-length guard)", () => {
@@ -743,10 +805,7 @@ test("validateHeader rejects odd-length verifier hex (thread 6 — even-length g
743
805
  params: { N: 1 << 10, r: 8, p: 1, keyLength: 32, maxmem: 64 * 1024 * 1024 },
744
806
  });
745
807
  const bad = { ...header, verifier: header.verifier.slice(0, -1) }; // odd length
746
- assert.throws(
747
- () => validateHeader(bad),
748
- /even length|even/i,
749
- );
808
+ assert.throws(() => validateHeader(bad), /even length|even/i);
750
809
  });
751
810
 
752
811
  test("parseHeader rejects non-hex characters in verifier (thread 6)", () => {
@@ -761,13 +820,10 @@ test("parseHeader rejects non-hex characters in verifier (thread 6)", () => {
761
820
  format: HEADER_FORMAT,
762
821
  formatVersion: HEADER_FORMAT_VERSION,
763
822
  metadata: JSON.parse(JSON.stringify(header.metadata)),
764
- verifier: "zz" + header.verifier.slice(2), // non-hex prefix
823
+ verifier: `zz${header.verifier.slice(2)}`, // non-hex prefix
765
824
  createdAt: header.createdAt,
766
825
  });
767
- assert.throws(
768
- () => parseHeader(badJson),
769
- /hex/i,
770
- );
826
+ assert.throws(() => parseHeader(badJson), /hex/i);
771
827
  });
772
828
 
773
829
  // ─── passphrase-reader.ts — Thread 5: prompts to stderr (#737) ──────────
@@ -800,13 +856,10 @@ test("non-TTY prompt is written to errorStream, not output (thread 5)", async ()
800
856
  // The prompt must appear on stderr, not stdout.
801
857
  const stderr = stderrChunks.join("");
802
858
  const stdout = stdoutChunks.join("");
803
- assert.ok(
804
- stderr.includes("Enter passphrase: "),
805
- `expected prompt on stderr but got: ${JSON.stringify(stderr)}`,
806
- );
859
+ assert.ok(stderr.includes("Enter passphrase: "), `expected prompt on stderr but got: ${JSON.stringify(stderr)}`);
807
860
  assert.ok(
808
861
  !stdout.includes("Enter passphrase: "),
809
- `prompt must not appear on stdout but got: ${JSON.stringify(stdout)}`,
862
+ `prompt must not appear on stdout but got: ${JSON.stringify(stdout)}`
810
863
  );
811
864
  });
812
865
 
@@ -850,11 +903,7 @@ test("backspace removes full non-BMP (emoji) code point atomically (thread 7)",
850
903
 
851
904
  const result = await readPromise;
852
905
 
853
- assert.equal(
854
- result,
855
- "a",
856
- `expected 'a' after backspace removed the emoji, but got: ${JSON.stringify(result)}`,
857
- );
906
+ assert.equal(result, "a", `expected 'a' after backspace removed the emoji, but got: ${JSON.stringify(result)}`);
858
907
  });
859
908
 
860
909
  test("backspace on BMP character removes exactly one character (thread 7 — regression)", async () => {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/secure-store/secure-fs.ts"],"sourcesContent":["/**\n * Transparent file-level encryption for the secure-store module.\n *\n * Issue #690 (PR 3/4) — storage.ts integration layer.\n *\n * This module sits between the raw filesystem and StorageManager.\n * Every memory file is either:\n * - a plain UTF-8 text file (legacy, back-compat), or\n * - a REMNIC-ENC sealed file (AES-256-GCM, see format below).\n *\n * On-disk format\n * --------------\n * Encrypted files begin with a 9-byte magic header:\n *\n * REMNIC-ENC (7 ASCII bytes)\n * VER (1 byte, currently 0x01)\n * FLAGS (1 byte, reserved, must be 0x00)\n *\n * Followed immediately by a `seal()` envelope from `cipher.ts`:\n *\n * [VERSION:1][SALT:16][IV:12][AUTHTAG:16][CIPHERTEXT:...]\n *\n * The magic header makes encrypted files sniffable without attempting\n * a full `open()` call and gives operators a clear signal that the\n * file cannot be read by opening it in an editor.\n *\n * AAD\n * ---\n * The file path relative to the memory root is bound as Associated\n * Authenticated Data (AAD) on both encrypt and decrypt. This means\n * moving or renaming an encrypted file without re-encrypting it will\n * cause auth-tag failure on the next read — the file is tied to its\n * path. Callers that move files must re-encrypt them.\n *\n * Back-compat\n * -----------\n * `readMaybeEncryptedFile` transparently handles both formats: if the\n * file does NOT start with the magic bytes, it is returned as-is (plain\n * text). This lets an operator migrate incrementally: newly-written\n * files are encrypted while existing files continue to be read in plain\n * form until `migrateMemoryDirToEncrypted` is run.\n *\n * Naming: `secure-fs.ts` (not `vault-fs.ts`) — see `kdf.ts` naming note.\n */\n\nimport { createCipheriv, randomBytes, randomUUID } from \"node:crypto\";\nimport { lstat, mkdir, open as openFile, readFile, readdir, rename, unlink, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport {\n AUTH_TAG_LENGTH,\n ENVELOPE_HEADER_SIZE,\n ENVELOPE_LAYOUT,\n ENVELOPE_SALT_LENGTH,\n ENVELOPE_VERSION,\n IV_LENGTH,\n generateSalt,\n open as openEnvelope,\n parseEnvelope,\n seal,\n} from \"./cipher.js\";\n\n// ---------------------------------------------------------------------------\n// Error classes\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when a read is attempted but the keyring entry for this\n * store is absent (i.e. `secure-store unlock` has not been run\n * since the last daemon start).\n */\nexport class SecureStoreLockedError extends Error {\n constructor(message = \"secure-store is locked — run `remnic secure-store unlock` to decrypt\") {\n super(message);\n this.name = \"SecureStoreLockedError\";\n }\n}\n\n/**\n * Thrown when `open()` fails because the auth tag does not validate.\n * This covers both wrong-key and tampered-ciphertext scenarios —\n * intentionally indistinguishable from the caller's perspective.\n */\nexport class SecureStoreDecryptError extends Error {\n constructor(message = \"secure-store decryption failed — wrong key or tampered ciphertext\") {\n super(message);\n this.name = \"SecureStoreDecryptError\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Magic header\n// ---------------------------------------------------------------------------\n\n/** Magic bytes: the ASCII string \"REMNIC-ENC\" (10 bytes). */\nexport const MAGIC_BYTES = Buffer.from(\"REMNIC-ENC\", \"ascii\");\n\n/** Current on-disk version byte. */\nexport const FILE_FORMAT_VERSION = 0x01;\n\n/** Reserved flags byte — must be 0x00. */\nexport const FILE_FORMAT_FLAGS = 0x00;\n\n/** Total size of the magic header prefix (magic + version + flags). */\nexport const MAGIC_HEADER_SIZE = MAGIC_BYTES.length + 2; // 12 bytes\n\n// ---------------------------------------------------------------------------\n// Detection\n// ---------------------------------------------------------------------------\n\n/**\n * Return true iff `buf` begins with the REMNIC-ENC magic header.\n * Does not validate the envelope; just identifies the format.\n */\nexport function isEncryptedFile(buf: Uint8Array): boolean {\n if (buf.length < MAGIC_HEADER_SIZE) return false;\n const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);\n return b.subarray(0, MAGIC_BYTES.length).equals(MAGIC_BYTES);\n}\n\n// ---------------------------------------------------------------------------\n// Encrypt / decrypt file body\n// ---------------------------------------------------------------------------\n\n/**\n * Encrypt `plain` (UTF-8 content of a memory file) and return a\n * Buffer ready to write to disk.\n *\n * @param plain Plain-text file content (UTF-8 string or Buffer).\n * @param key 32-byte AES-256 key from the keyring.\n * @param aad Optional associated data — defaults to empty if omitted.\n * Callers should pass the file path relative to memoryDir\n * so the ciphertext is bound to its location.\n */\nexport function encryptFileBody(plain: string | Buffer, key: Buffer, aad?: Buffer): Buffer {\n const plainBuf = typeof plain === \"string\" ? Buffer.from(plain, \"utf8\") : plain;\n const salt = generateSalt();\n const envelope = seal(key, salt, plainBuf, aad ? { aad } : {});\n\n const header = Buffer.alloc(MAGIC_HEADER_SIZE);\n MAGIC_BYTES.copy(header, 0);\n header.writeUInt8(FILE_FORMAT_VERSION, MAGIC_BYTES.length);\n header.writeUInt8(FILE_FORMAT_FLAGS, MAGIC_BYTES.length + 1);\n\n return Buffer.concat([header, envelope]);\n}\n\n/**\n * Decrypt a buffer produced by `encryptFileBody` and return the\n * original UTF-8 content.\n *\n * Throws `SecureStoreDecryptError` on auth failure (wrong key or\n * tampered ciphertext). Throws a plain `Error` for structural problems\n * (truncated buffer, wrong magic, unsupported version).\n */\nexport function decryptFileBody(buf: Buffer, key: Buffer, aad?: Buffer): Buffer {\n if (!isEncryptedFile(buf)) {\n throw new Error(\"decryptFileBody: buffer does not start with REMNIC-ENC magic header\");\n }\n const version = buf.readUInt8(MAGIC_BYTES.length);\n if (version !== FILE_FORMAT_VERSION) {\n throw new Error(\n `decryptFileBody: unsupported file format version ${version} (this build supports ${FILE_FORMAT_VERSION})`,\n );\n }\n const flags = buf.readUInt8(MAGIC_BYTES.length + 1);\n if (flags !== FILE_FORMAT_FLAGS) {\n throw new Error(`decryptFileBody: unknown flags byte 0x${flags.toString(16).padStart(2, \"0\")}`);\n }\n const envelope = buf.subarray(MAGIC_HEADER_SIZE);\n parseEnvelope(envelope);\n try {\n return openEnvelope(key, envelope, aad ? { aad } : {});\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new SecureStoreDecryptError(\n `secure-store decryption failed: ${msg}`,\n );\n }\n}\n\nfunction buildHeaderAad(salt: Uint8Array): Buffer {\n const out = Buffer.alloc(1 + ENVELOPE_SALT_LENGTH);\n out.writeUInt8(ENVELOPE_VERSION, 0);\n Buffer.from(salt).copy(out, 1);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Path → AAD helper\n// ---------------------------------------------------------------------------\n\n/**\n * Build the AAD buffer for a file at `filePath` relative to\n * `memoryDir`. The AAD binds the ciphertext to its path so a\n * file cannot be silently relocated without re-encryption.\n *\n * When `memoryDir` is supplied and `filePath` is absolute, the\n * relative sub-path is used. Otherwise `filePath` is used verbatim.\n */\nexport function filePathAad(filePath: string, memoryDir?: string): Buffer {\n let rel = filePath;\n if (memoryDir && path.isAbsolute(filePath)) {\n rel = path.relative(memoryDir, filePath);\n }\n return Buffer.from(rel, \"utf8\");\n}\n\n// ---------------------------------------------------------------------------\n// High-level read / write helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read a file from `filePath`.\n *\n * - If the file is plaintext (no magic header), return its content\n * as-is — back-compat with unencrypted stores.\n * - If the file is encrypted AND `key` is provided, decrypt and return\n * the plaintext content.\n * - If the file is encrypted AND `key` is null, throw\n * `SecureStoreLockedError`.\n *\n * @param filePath Absolute path to the file.\n * @param key 32-byte AES-256 key, or null when the store is locked.\n * @param memoryDir Memory root for path-bound AAD. Should be absolute.\n */\nexport async function readMaybeEncryptedFileBuffer(\n filePath: string,\n key: Buffer | null,\n memoryDir?: string,\n): Promise<Buffer> {\n const buf = await readFile(filePath);\n if (!isEncryptedFile(buf)) {\n // Plain file — legacy or unencrypted store.\n return buf;\n }\n // Encrypted — key required.\n if (key === null) {\n throw new SecureStoreLockedError(\n `secure-store is locked — cannot read encrypted file at ${filePath}. ` +\n \"Run `remnic secure-store unlock` to decrypt.\",\n );\n }\n return decryptFileBodyForPath(buf, key, filePath, memoryDir);\n}\n\nexport async function readMaybeEncryptedFile(\n filePath: string,\n key: Buffer | null,\n memoryDir?: string,\n): Promise<string> {\n return (await readMaybeEncryptedFileBuffer(filePath, key, memoryDir)).toString(\"utf8\");\n}\n\nexport interface WriteMaybeEncryptedFileOptions {\n /**\n * File mode bits. Default 0o600 (owner read/write only).\n * Applied only on create; existing files inherit their existing mode.\n */\n mode?: number;\n /**\n * If true, write atomically via a temp file + rename (CLAUDE.md gotcha #54).\n * Default true.\n */\n atomic?: boolean;\n}\n\n/**\n * Write `content` to `filePath`.\n *\n * - If `key` is provided and non-null, encrypt the content first.\n * - If `key` is null, write the content as plain UTF-8 (unencrypted store).\n *\n * Writes atomically: content is written to a unique temp file\n * first, then renamed into place (CLAUDE.md gotcha #54 — never delete\n * before write).\n */\nexport async function writeMaybeEncryptedFile(\n filePath: string,\n content: string | Buffer,\n key: Buffer | null,\n options: WriteMaybeEncryptedFileOptions = {},\n memoryDir?: string,\n): Promise<void> {\n const { mode = 0o600, atomic = true } = options;\n await mkdir(path.dirname(filePath), { recursive: true });\n\n let data: Buffer | string;\n if (key !== null) {\n const aad = filePathAad(filePath, memoryDir);\n data = encryptFileBody(content, key, aad);\n } else {\n data = content;\n }\n\n if (atomic) {\n const tempPath = uniqueAtomicTempPath(filePath, \"tmp\");\n try {\n await writeFile(tempPath, data, { mode });\n await rename(tempPath, filePath);\n } catch (err) {\n // Best-effort cleanup of the temp file.\n try {\n await unlink(tempPath);\n } catch {\n // ignore\n }\n throw err;\n }\n } else {\n await writeFile(filePath, data, { mode });\n }\n}\n\nexport async function writeMaybeEncryptedFileFromChunks(\n filePath: string,\n chunks: AsyncIterable<Buffer>,\n key: Buffer | null,\n options: WriteMaybeEncryptedFileOptions = {},\n memoryDir?: string,\n): Promise<void> {\n const { mode = 0o600, atomic = true } = options;\n await mkdir(path.dirname(filePath), { recursive: true });\n const writePath = atomic ? `${filePath}.tmp-${process.pid}-${Date.now()}` : filePath;\n let completed = false;\n try {\n const handle = await openFile(writePath, \"w\", mode);\n try {\n if (key !== null) {\n const salt = generateSalt();\n const iv = randomBytes(IV_LENGTH);\n const header = Buffer.alloc(MAGIC_HEADER_SIZE + ENVELOPE_HEADER_SIZE);\n MAGIC_BYTES.copy(header, 0);\n header.writeUInt8(FILE_FORMAT_VERSION, MAGIC_BYTES.length);\n header.writeUInt8(FILE_FORMAT_FLAGS, MAGIC_BYTES.length + 1);\n const envelopeOffset = MAGIC_HEADER_SIZE;\n header.writeUInt8(ENVELOPE_VERSION, envelopeOffset + ENVELOPE_LAYOUT.version);\n salt.copy(header, envelopeOffset + ENVELOPE_LAYOUT.salt);\n iv.copy(header, envelopeOffset + ENVELOPE_LAYOUT.iv);\n await handle.write(header);\n\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv, { authTagLength: AUTH_TAG_LENGTH });\n const aad = filePathAad(filePath, memoryDir);\n cipher.setAAD(Buffer.concat([buildHeaderAad(salt), aad]));\n for await (const chunk of chunks) {\n if (chunk.length === 0) continue;\n const encrypted = cipher.update(chunk);\n if (encrypted.length > 0) await handle.write(encrypted);\n }\n const final = cipher.final();\n if (final.length > 0) await handle.write(final);\n const authTag = cipher.getAuthTag();\n await handle.write(\n authTag,\n 0,\n authTag.length,\n MAGIC_HEADER_SIZE + ENVELOPE_LAYOUT.authTag,\n );\n } else {\n for await (const chunk of chunks) {\n if (chunk.length > 0) await handle.write(chunk);\n }\n }\n } finally {\n await handle.close();\n }\n if (atomic) {\n await rename(writePath, filePath);\n }\n completed = true;\n } finally {\n if (!completed && atomic) {\n await unlink(writePath).catch(() => {});\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Migration\n// ---------------------------------------------------------------------------\n\nexport interface MigrateResult {\n /** Number of files successfully encrypted. */\n encrypted: number;\n /** Number of files already encrypted (skipped). */\n skipped: number;\n /** Files that failed to encrypt (path → error message). */\n errors: Array<{ filePath: string; error: string }>;\n}\n\nexport interface DecryptResult {\n /** Number of files successfully decrypted back to plaintext. */\n decrypted: number;\n /** Number of files already plaintext (skipped). */\n skipped: number;\n /** Files that failed to decrypt (path → error message). */\n errors: Array<{ filePath: string; error: string }>;\n}\n\n/**\n * Walk `dir` recursively, find encryptable storage-managed files that are not\n * yet encrypted, and re-write them as encrypted files under `key`.\n *\n * Safety rules per CLAUDE.md gotchas #54 and #25:\n * 1. A page-version snapshot is taken (via `createVersion`) BEFORE\n * each overwrite so the plaintext version is preserved in history.\n * Since this module has no direct access to `page-versioning.ts`\n * internals, callers who have page-versioning configured should\n * pass `onBeforeEncrypt` to take the snapshot.\n * 2. The new encrypted content is written to a temp file first,\n * then renamed atomically — never deleted before written.\n * 3. If encryption of any file fails, the error is recorded and the\n * original file is left intact (partial migration is safe).\n *\n * @param dir Absolute path to the memory directory.\n * @param key 32-byte AES-256 key.\n * @param onBeforeEncrypt Optional callback invoked before encrypting\n * each file. Can be used to take page-version\n * snapshots. Errors here are non-fatal.\n */\nexport async function migrateMemoryDirToEncrypted(\n dir: string,\n key: Buffer,\n onBeforeEncrypt?: (filePath: string) => Promise<void>,\n): Promise<MigrateResult> {\n const result: MigrateResult = { encrypted: 0, skipped: 0, errors: [] };\n\n const files = await collectEncryptableStorageFiles(dir);\n for (const filePath of files) {\n try {\n const buf = await readFile(filePath);\n if (isEncryptedFile(buf)) {\n result.skipped++;\n continue;\n }\n // Call optional pre-encryption hook (e.g. page-version snapshot).\n if (onBeforeEncrypt) {\n try {\n await onBeforeEncrypt(filePath);\n } catch {\n // Non-fatal — continue with encryption even if snapshot fails.\n }\n }\n const content = buf.toString(\"utf8\");\n const aad = filePathAad(filePath, dir);\n const encrypted = encryptFileBody(content, key, aad);\n\n // Atomic write: temp → rename (gotcha #54).\n const tempPath = uniqueAtomicTempPath(filePath, \"enc-tmp\");\n try {\n await writeFile(tempPath, encrypted, { mode: 0o600 });\n await rename(tempPath, filePath);\n result.encrypted++;\n } catch (writeErr) {\n // Clean up temp file, leave original intact.\n try {\n const { unlink } = await import(\"node:fs/promises\");\n await unlink(tempPath);\n } catch {\n // ignore\n }\n throw writeErr;\n }\n } catch (err) {\n result.errors.push({\n filePath,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return result;\n}\n\n/**\n * Walk `dir` recursively, find storage-managed encrypted files, and\n * re-write them as plaintext under the same paths.\n *\n * This is the reversible counterpart to {@link migrateMemoryDirToEncrypted}.\n * It only touches files under the same storage-managed roots, skips\n * plaintext files, skips symlinks, excludes `.secure-store/`, and writes\n * each plaintext replacement via temp-file + rename so a per-file failure\n * leaves the ciphertext intact.\n */\nexport async function decryptMemoryDirToPlaintext(\n dir: string,\n key: Buffer,\n): Promise<DecryptResult> {\n const result: DecryptResult = { decrypted: 0, skipped: 0, errors: [] };\n\n const files = await collectStorageManagedFiles(dir, isDecryptableStoragePath);\n for (const filePath of files) {\n try {\n const buf = await readFile(filePath);\n if (!isEncryptedFile(buf)) {\n result.skipped++;\n continue;\n }\n\n const plaintext = decryptFileBodyForPath(buf, key, filePath, dir);\n const tempPath = uniqueAtomicTempPath(filePath, \"dec-tmp\");\n try {\n await writeFile(tempPath, plaintext, { mode: 0o600 });\n await rename(tempPath, filePath);\n result.decrypted++;\n } catch (writeErr) {\n try {\n await unlink(tempPath);\n } catch {\n // ignore cleanup errors; original ciphertext is still intact.\n }\n throw writeErr;\n }\n } catch (err) {\n result.errors.push({\n filePath,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction uniqueAtomicTempPath(filePath: string, label: string): string {\n return `${filePath}.${label}-${process.pid}-${Date.now()}-${randomUUID()}`;\n}\n\nfunction decryptFileBodyForPath(\n buf: Buffer,\n key: Buffer,\n filePath: string,\n memoryDir?: string,\n): Buffer {\n const aad = filePathAad(filePath, memoryDir);\n try {\n return decryptFileBody(buf, key, aad);\n } catch (err) {\n if (!(err instanceof SecureStoreDecryptError)) {\n throw err;\n }\n const legacyRoot = legacyNamespaceAadRootForFile(filePath, memoryDir);\n if (legacyRoot) {\n try {\n return decryptFileBody(buf, key, filePathAad(filePath, legacyRoot));\n } catch {\n // Fall through to the namespace-reader fallback below.\n }\n }\n\n const topLevelRoot = topLevelAadRootForNamespaceReader(filePath, memoryDir);\n if (topLevelRoot) {\n try {\n return decryptFileBody(buf, key, filePathAad(filePath, topLevelRoot));\n } catch {\n // Preserve the caller-facing error from the canonical decrypt attempt.\n }\n }\n\n throw err;\n }\n}\n\nfunction topLevelAadRootForNamespaceReader(filePath: string, memoryDir?: string): string | null {\n if (!memoryDir || !path.isAbsolute(filePath)) return null;\n const resolvedMemoryDir = path.resolve(memoryDir);\n const rel = path.relative(resolvedMemoryDir, filePath);\n if (rel === \"\" || rel.startsWith(\"..\") || path.isAbsolute(rel)) return null;\n const parts = resolvedMemoryDir.split(path.sep);\n if (parts.length < 3 || parts.at(-2) !== \"namespaces\" || !parts.at(-1)) return null;\n const topLevelRoot = parts.slice(0, -2).join(path.sep) || path.sep;\n const topRel = path.relative(topLevelRoot, filePath);\n if (topRel === \"\" || topRel.startsWith(\"..\") || path.isAbsolute(topRel)) {\n return null;\n }\n const topParts = topRel.split(path.sep);\n if (topParts[0] !== \"namespaces\" || topParts[1] !== parts.at(-1)) {\n return null;\n }\n return topLevelRoot;\n}\n\nfunction legacyNamespaceAadRootForFile(filePath: string, memoryDir?: string): string | null {\n if (!memoryDir || !path.isAbsolute(filePath)) return null;\n const rel = path.relative(memoryDir, filePath);\n if (rel === \"\" || rel.startsWith(\"..\") || path.isAbsolute(rel)) return null;\n const parts = rel.split(path.sep);\n if (parts[0] === \"namespaces\" && parts.length >= 3 && parts[1]) {\n return path.join(memoryDir, \"namespaces\", parts[1]);\n }\n return null;\n}\n\n/**\n * Recursively collect files under `dir` that are read through the\n * storage-layer secure-store helpers, excluding symlinked entries and\n * `.secure-store/` metadata.\n */\nasync function collectEncryptableStorageFiles(dir: string, rootDir = dir): Promise<string[]> {\n return collectStorageManagedFiles(dir, isEncryptableStoragePath, rootDir);\n}\n\n/**\n * Recursively collect regular files under storage-managed roots, excluding\n * symlinked entries and `.secure-store/` metadata. This broader collector is\n * used by the decrypt/disable path so future encrypted sidecars can be\n * restored without requiring extension-specific logic.\n */\nasync function collectStorageManagedFiles(\n dir: string,\n includeFile: (filePath: string, rootDir: string) => boolean,\n rootDir = dir,\n): Promise<string[]> {\n const results: string[] = [];\n let names: string[];\n try {\n names = await readdir(dir, { encoding: \"utf8\" });\n } catch {\n return results;\n }\n for (const name of names) {\n if (name.startsWith(\".secure-store\")) continue;\n const full = path.join(dir, name);\n let isDir = false;\n let isFile = false;\n try {\n const s = await lstat(full);\n if (s.isSymbolicLink()) continue;\n isDir = s.isDirectory();\n isFile = s.isFile();\n } catch {\n continue;\n }\n if (isDir) {\n const sub = await collectStorageManagedFiles(full, includeFile, rootDir);\n results.push(...sub);\n } else if (isFile && includeFile(full, rootDir)) {\n results.push(full);\n }\n }\n return results;\n}\n\nfunction isEncryptableStoragePath(filePath: string, rootDir: string): boolean {\n const rel = path.relative(rootDir, filePath);\n if (rel === \"\" || rel.startsWith(\"..\") || path.isAbsolute(rel)) return false;\n const normalized = normalizeStorageRelativePath(rel);\n if (normalized === \"profile.md\") return true;\n if (isEncryptableStateSidecar(normalized)) return true;\n if (isEncryptableSummarySidecar(normalized)) return true;\n const firstSegment = normalized.split(\"/\", 1)[0];\n return ENCRYPTABLE_MARKDOWN_STORAGE_ROOTS.has(firstSegment) && normalized.endsWith(\".md\");\n}\n\nfunction isDecryptableStoragePath(filePath: string, rootDir: string): boolean {\n if (isEncryptableStoragePath(filePath, rootDir)) return true;\n const rel = path.relative(rootDir, filePath);\n if (rel === \"\" || rel.startsWith(\"..\") || path.isAbsolute(rel)) return false;\n const normalized = normalizeStorageRelativePath(rel);\n const firstSegment = normalized.split(\"/\", 1)[0];\n return DECRYPTABLE_SIDECAR_ROOTS.has(firstSegment);\n}\n\nfunction normalizeStorageRelativePath(rel: string): string {\n const normalized = rel.split(path.sep).join(\"/\");\n const parts = normalized.split(\"/\");\n if (parts[0] === \"namespaces\" && parts.length >= 3) {\n return parts.slice(2).join(\"/\");\n }\n return normalized;\n}\n\nconst ENCRYPTABLE_MARKDOWN_STORAGE_ROOTS = new Set([\n \"facts\",\n \"corrections\",\n \"procedures\",\n \"reasoning-traces\",\n \"artifacts\",\n \"archive\",\n \"entities\",\n \"identity\",\n]);\n\nconst ENCRYPTABLE_STATE_SIDECARS = new Set([\n \"state/behavior-signals.jsonl\",\n \"state/buffer-surprise-ledger.jsonl\",\n \"state/buffer.json\",\n \"state/compression-guideline-draft-state.json\",\n \"state/compression-guideline-state.json\",\n \"state/compression-guidelines.draft.md\",\n \"state/compression-guidelines.md\",\n \"state/entity-synthesis-queue.json\",\n \"state/fact-hashes.txt\",\n \"state/memory-actions.jsonl\",\n \"state/memory-lifecycle-ledger.jsonl\",\n \"state/meta.json\",\n \"state/reextract-jobs.jsonl\",\n \"state/topics.json\",\n]);\n\nfunction isEncryptableStateSidecar(normalized: string): boolean {\n return ENCRYPTABLE_STATE_SIDECARS.has(normalized);\n}\n\nfunction isEncryptableSummarySidecar(normalized: string): boolean {\n return normalized.startsWith(\"summaries/\") && normalized.endsWith(\".json\");\n}\n\nconst DECRYPTABLE_SIDECAR_ROOTS = new Set([\n \"state\",\n \"indexes\",\n \"index\",\n \"provenance\",\n]);\n"],"mappings":"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","names":["unlink"]}