@remnic/core 9.3.538 → 9.3.540

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-DGDQZ3JW.js → chunk-2OA4PFDW.js} +2 -2
  9. package/dist/{chunk-QIWY5OKC.js → chunk-37FWWWOC.js} +5 -5
  10. package/dist/{chunk-RB7LF7K7.js → chunk-3M7OTJ5H.js} +4 -4
  11. package/dist/{chunk-KJMYHC7K.js → chunk-73JGZ5VA.js} +57 -15
  12. package/dist/chunk-73JGZ5VA.js.map +1 -0
  13. package/dist/{chunk-K4LDMTTY.js → chunk-A5TLPLUO.js} +2 -2
  14. package/dist/{chunk-67VP6I47.js → chunk-BRCYNT4I.js} +2 -2
  15. package/dist/{chunk-XI5V7WSJ.js → chunk-ENIIJ3MZ.js} +2 -2
  16. package/dist/{chunk-QK2G4EYH.js → chunk-FWYUJY4N.js} +2 -2
  17. package/dist/{chunk-TKUQG2PE.js → chunk-FXE46BJ5.js} +2 -2
  18. package/dist/{chunk-OMHRQTOD.js → chunk-GWUUEPOR.js} +2 -2
  19. package/dist/{chunk-TSBADOZU.js → chunk-IEHYNDKN.js} +5 -5
  20. package/dist/{chunk-BE5XAWSA.js → chunk-J62VXZR2.js} +2 -2
  21. package/dist/{chunk-ZQLC75BF.js → chunk-K6OABSBA.js} +2 -2
  22. package/dist/{chunk-6HF5VUBQ.js → chunk-LKUNOD7B.js} +2 -2
  23. package/dist/{chunk-Y2SXZ5KZ.js → chunk-NHZHAZCJ.js} +2 -2
  24. package/dist/{chunk-C6NYAW5B.js → chunk-O3VLN5MY.js} +4 -4
  25. package/dist/{chunk-Z5P7XYDU.js → chunk-QCJLDMY5.js} +8 -8
  26. package/dist/{chunk-7TVK7E3R.js → chunk-QKV4LVLA.js} +2 -2
  27. package/dist/{chunk-DKOIMCGB.js → chunk-QWQX7YK5.js} +2 -2
  28. package/dist/{chunk-PYGKYEDU.js → chunk-RGLJNOQN.js} +3 -3
  29. package/dist/{chunk-DRBCO4RL.js → chunk-RU7QP2GP.js} +12 -12
  30. package/dist/{chunk-O2HWL47E.js → chunk-UMXWQL3P.js} +2 -2
  31. package/dist/{chunk-6QKWLP4V.js → chunk-XE23FSDQ.js} +2 -2
  32. package/dist/{chunk-LKCE3NPZ.js → chunk-YVB6W5EX.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 +68 -15
  57. package/src/secure-store/secure-store.test.ts +63 -0
  58. package/dist/chunk-KJMYHC7K.js.map +0 -1
  59. /package/dist/{chunk-DGDQZ3JW.js.map → chunk-2OA4PFDW.js.map} +0 -0
  60. /package/dist/{chunk-QIWY5OKC.js.map → chunk-37FWWWOC.js.map} +0 -0
  61. /package/dist/{chunk-RB7LF7K7.js.map → chunk-3M7OTJ5H.js.map} +0 -0
  62. /package/dist/{chunk-K4LDMTTY.js.map → chunk-A5TLPLUO.js.map} +0 -0
  63. /package/dist/{chunk-67VP6I47.js.map → chunk-BRCYNT4I.js.map} +0 -0
  64. /package/dist/{chunk-XI5V7WSJ.js.map → chunk-ENIIJ3MZ.js.map} +0 -0
  65. /package/dist/{chunk-QK2G4EYH.js.map → chunk-FWYUJY4N.js.map} +0 -0
  66. /package/dist/{chunk-TKUQG2PE.js.map → chunk-FXE46BJ5.js.map} +0 -0
  67. /package/dist/{chunk-OMHRQTOD.js.map → chunk-GWUUEPOR.js.map} +0 -0
  68. /package/dist/{chunk-TSBADOZU.js.map → chunk-IEHYNDKN.js.map} +0 -0
  69. /package/dist/{chunk-BE5XAWSA.js.map → chunk-J62VXZR2.js.map} +0 -0
  70. /package/dist/{chunk-ZQLC75BF.js.map → chunk-K6OABSBA.js.map} +0 -0
  71. /package/dist/{chunk-6HF5VUBQ.js.map → chunk-LKUNOD7B.js.map} +0 -0
  72. /package/dist/{chunk-Y2SXZ5KZ.js.map → chunk-NHZHAZCJ.js.map} +0 -0
  73. /package/dist/{chunk-C6NYAW5B.js.map → chunk-O3VLN5MY.js.map} +0 -0
  74. /package/dist/{chunk-Z5P7XYDU.js.map → chunk-QCJLDMY5.js.map} +0 -0
  75. /package/dist/{chunk-7TVK7E3R.js.map → chunk-QKV4LVLA.js.map} +0 -0
  76. /package/dist/{chunk-DKOIMCGB.js.map → chunk-QWQX7YK5.js.map} +0 -0
  77. /package/dist/{chunk-PYGKYEDU.js.map → chunk-RGLJNOQN.js.map} +0 -0
  78. /package/dist/{chunk-DRBCO4RL.js.map → chunk-RU7QP2GP.js.map} +0 -0
  79. /package/dist/{chunk-O2HWL47E.js.map → chunk-UMXWQL3P.js.map} +0 -0
  80. /package/dist/{chunk-6QKWLP4V.js.map → chunk-XE23FSDQ.js.map} +0 -0
  81. /package/dist/{chunk-LKCE3NPZ.js.map → chunk-YVB6W5EX.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  promoteSemanticRuleFromMemory
3
- } from "./chunk-6QKWLP4V.js";
4
- import "./chunk-K4LDMTTY.js";
3
+ } from "./chunk-XE23FSDQ.js";
4
+ import "./chunk-A5TLPLUO.js";
5
5
  import "./chunk-5UZXUTVO.js";
6
6
  import "./chunk-4H5ZJHEN.js";
7
7
  import "./chunk-M5T4Q2ZU.js";
@@ -17,7 +17,7 @@ import "./chunk-G7D6GZ5J.js";
17
17
  import "./chunk-ALEPI75L.js";
18
18
  import "./chunk-4DJQYKMN.js";
19
19
  import "./chunk-2ODBA7MQ.js";
20
- import "./chunk-KJMYHC7K.js";
20
+ import "./chunk-73JGZ5VA.js";
21
21
  import "./chunk-A6XUJE5D.js";
22
22
  import "./chunk-P7FMDTKL.js";
23
23
  import "./chunk-PZ5AY32C.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  searchVerifiedSemanticRules
3
- } from "./chunk-BE5XAWSA.js";
4
- import "./chunk-K4LDMTTY.js";
3
+ } from "./chunk-J62VXZR2.js";
4
+ import "./chunk-A5TLPLUO.js";
5
5
  import "./chunk-5UZXUTVO.js";
6
6
  import "./chunk-4H5ZJHEN.js";
7
7
  import "./chunk-M5T4Q2ZU.js";
@@ -18,7 +18,7 @@ import "./chunk-ALEPI75L.js";
18
18
  import "./chunk-DT5TVLJE.js";
19
19
  import "./chunk-4DJQYKMN.js";
20
20
  import "./chunk-2ODBA7MQ.js";
21
- import "./chunk-KJMYHC7K.js";
21
+ import "./chunk-73JGZ5VA.js";
22
22
  import "./chunk-A6XUJE5D.js";
23
23
  import "./chunk-P7FMDTKL.js";
24
24
  import "./chunk-PZ5AY32C.js";
package/dist/storage.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  normalizeEntityName,
9
9
  parseEntityFile,
10
10
  serializeEntityFile
11
- } from "./chunk-K4LDMTTY.js";
11
+ } from "./chunk-A5TLPLUO.js";
12
12
  import "./chunk-5UZXUTVO.js";
13
13
  import "./chunk-4H5ZJHEN.js";
14
14
  import "./chunk-M5T4Q2ZU.js";
@@ -24,7 +24,7 @@ import "./chunk-G7D6GZ5J.js";
24
24
  import "./chunk-ALEPI75L.js";
25
25
  import "./chunk-4DJQYKMN.js";
26
26
  import "./chunk-2ODBA7MQ.js";
27
- import "./chunk-KJMYHC7K.js";
27
+ import "./chunk-73JGZ5VA.js";
28
28
  import "./chunk-A6XUJE5D.js";
29
29
  import "./chunk-P7FMDTKL.js";
30
30
  import "./chunk-PZ5AY32C.js";
@@ -313,13 +313,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
313
313
  peerProfiles: boolean;
314
314
  }>;
315
315
  }, "strip", z.ZodTypeAny, {
316
- schemaVersion: string;
317
316
  includes: {
318
317
  procedural: boolean;
319
318
  taxonomy: boolean;
320
319
  identityAnchors: boolean;
321
320
  peerProfiles: boolean;
322
321
  };
322
+ schemaVersion: string;
323
323
  id: string;
324
324
  description: string;
325
325
  version: string;
@@ -334,13 +334,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
334
334
  directAnswerEnabled: boolean;
335
335
  };
336
336
  }, {
337
- schemaVersion: string;
338
337
  includes: {
339
338
  procedural: boolean;
340
339
  taxonomy: boolean;
341
340
  identityAnchors: boolean;
342
341
  peerProfiles: boolean;
343
342
  };
343
+ schemaVersion: string;
344
344
  id: string;
345
345
  description: string;
346
346
  version: string;
@@ -464,13 +464,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
464
464
  peerProfiles: boolean;
465
465
  }>;
466
466
  }, "strip", z.ZodTypeAny, {
467
- schemaVersion: string;
468
467
  includes: {
469
468
  procedural: boolean;
470
469
  taxonomy: boolean;
471
470
  identityAnchors: boolean;
472
471
  peerProfiles: boolean;
473
472
  };
473
+ schemaVersion: string;
474
474
  id: string;
475
475
  description: string;
476
476
  version: string;
@@ -485,13 +485,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
485
485
  directAnswerEnabled: boolean;
486
486
  };
487
487
  }, {
488
- schemaVersion: string;
489
488
  includes: {
490
489
  procedural: boolean;
491
490
  taxonomy: boolean;
492
491
  identityAnchors: boolean;
493
492
  peerProfiles: boolean;
494
493
  };
494
+ schemaVersion: string;
495
495
  id: string;
496
496
  description: string;
497
497
  version: string;
@@ -518,13 +518,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
518
518
  pluginVersion: string;
519
519
  includesTranscripts: boolean;
520
520
  capsule: {
521
- schemaVersion: string;
522
521
  includes: {
523
522
  procedural: boolean;
524
523
  taxonomy: boolean;
525
524
  identityAnchors: boolean;
526
525
  peerProfiles: boolean;
527
526
  };
527
+ schemaVersion: string;
528
528
  id: string;
529
529
  description: string;
530
530
  version: string;
@@ -551,13 +551,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
551
551
  pluginVersion: string;
552
552
  includesTranscripts: boolean;
553
553
  capsule: {
554
- schemaVersion: string;
555
554
  includes: {
556
555
  procedural: boolean;
557
556
  taxonomy: boolean;
558
557
  identityAnchors: boolean;
559
558
  peerProfiles: boolean;
560
559
  };
560
+ schemaVersion: string;
561
561
  id: string;
562
562
  description: string;
563
563
  version: string;
@@ -683,13 +683,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
683
683
  peerProfiles: boolean;
684
684
  }>;
685
685
  }, "strip", z.ZodTypeAny, {
686
- schemaVersion: string;
687
686
  includes: {
688
687
  procedural: boolean;
689
688
  taxonomy: boolean;
690
689
  identityAnchors: boolean;
691
690
  peerProfiles: boolean;
692
691
  };
692
+ schemaVersion: string;
693
693
  id: string;
694
694
  description: string;
695
695
  version: string;
@@ -704,13 +704,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
704
704
  directAnswerEnabled: boolean;
705
705
  };
706
706
  }, {
707
- schemaVersion: string;
708
707
  includes: {
709
708
  procedural: boolean;
710
709
  taxonomy: boolean;
711
710
  identityAnchors: boolean;
712
711
  peerProfiles: boolean;
713
712
  };
713
+ schemaVersion: string;
714
714
  id: string;
715
715
  description: string;
716
716
  version: string;
@@ -737,13 +737,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
737
737
  pluginVersion: string;
738
738
  includesTranscripts: boolean;
739
739
  capsule: {
740
- schemaVersion: string;
741
740
  includes: {
742
741
  procedural: boolean;
743
742
  taxonomy: boolean;
744
743
  identityAnchors: boolean;
745
744
  peerProfiles: boolean;
746
745
  };
746
+ schemaVersion: string;
747
747
  id: string;
748
748
  description: string;
749
749
  version: string;
@@ -770,13 +770,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
770
770
  pluginVersion: string;
771
771
  includesTranscripts: boolean;
772
772
  capsule: {
773
- schemaVersion: string;
774
773
  includes: {
775
774
  procedural: boolean;
776
775
  taxonomy: boolean;
777
776
  identityAnchors: boolean;
778
777
  peerProfiles: boolean;
779
778
  };
779
+ schemaVersion: string;
780
780
  id: string;
781
781
  description: string;
782
782
  version: string;
@@ -815,13 +815,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
815
815
  pluginVersion: string;
816
816
  includesTranscripts: boolean;
817
817
  capsule: {
818
- schemaVersion: string;
819
818
  includes: {
820
819
  procedural: boolean;
821
820
  taxonomy: boolean;
822
821
  identityAnchors: boolean;
823
822
  peerProfiles: boolean;
824
823
  };
824
+ schemaVersion: string;
825
825
  id: string;
826
826
  description: string;
827
827
  version: string;
@@ -854,13 +854,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
854
854
  pluginVersion: string;
855
855
  includesTranscripts: boolean;
856
856
  capsule: {
857
- schemaVersion: string;
858
857
  includes: {
859
858
  procedural: boolean;
860
859
  taxonomy: boolean;
861
860
  identityAnchors: boolean;
862
861
  peerProfiles: boolean;
863
862
  };
863
+ schemaVersion: string;
864
864
  id: string;
865
865
  description: string;
866
866
  version: string;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  searchVerifiedEpisodes
3
- } from "./chunk-7TVK7E3R.js";
3
+ } from "./chunk-QKV4LVLA.js";
4
4
  import "./chunk-HQ6NIBL6.js";
5
- import "./chunk-K4LDMTTY.js";
5
+ import "./chunk-A5TLPLUO.js";
6
6
  import "./chunk-5UZXUTVO.js";
7
7
  import "./chunk-4H5ZJHEN.js";
8
8
  import "./chunk-M5T4Q2ZU.js";
@@ -19,7 +19,7 @@ import "./chunk-ALEPI75L.js";
19
19
  import "./chunk-DT5TVLJE.js";
20
20
  import "./chunk-4DJQYKMN.js";
21
21
  import "./chunk-2ODBA7MQ.js";
22
- import "./chunk-KJMYHC7K.js";
22
+ import "./chunk-73JGZ5VA.js";
23
23
  import "./chunk-A6XUJE5D.js";
24
24
  import "./chunk-P7FMDTKL.js";
25
25
  import "./chunk-PZ5AY32C.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.538",
3
+ "version": "9.3.540",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -241,8 +241,7 @@ export async function readMaybeEncryptedFileBuffer(
241
241
  "Run `remnic secure-store unlock` to decrypt.",
242
242
  );
243
243
  }
244
- const aad = filePathAad(filePath, memoryDir);
245
- return decryptFileBody(buf, key, aad);
244
+ return decryptFileBodyForPath(buf, key, filePath, memoryDir);
246
245
  }
247
246
 
248
247
  export async function readMaybeEncryptedFile(
@@ -443,7 +442,7 @@ export async function migrateMemoryDirToEncrypted(
443
442
  }
444
443
  }
445
444
  const content = buf.toString("utf8");
446
- const aad = filePathAad(filePath, storageAadRootForFile(filePath, dir));
445
+ const aad = filePathAad(filePath, dir);
447
446
  const encrypted = encryptFileBody(content, key, aad);
448
447
 
449
448
  // Atomic write: temp → rename (gotcha #54).
@@ -498,8 +497,7 @@ export async function decryptMemoryDirToPlaintext(
498
497
  continue;
499
498
  }
500
499
 
501
- const aad = filePathAad(filePath, storageAadRootForFile(filePath, dir));
502
- const plaintext = decryptFileBody(buf, key, aad);
500
+ const plaintext = decryptFileBodyForPath(buf, key, filePath, dir);
503
501
  const tempPath = uniqueAtomicTempPath(filePath, "dec-tmp");
504
502
  try {
505
503
  await writeFile(tempPath, plaintext, { mode: 0o600 });
@@ -532,6 +530,71 @@ function uniqueAtomicTempPath(filePath: string, label: string): string {
532
530
  return `${filePath}.${label}-${process.pid}-${Date.now()}-${randomUUID()}`;
533
531
  }
534
532
 
533
+ function decryptFileBodyForPath(
534
+ buf: Buffer,
535
+ key: Buffer,
536
+ filePath: string,
537
+ memoryDir?: string,
538
+ ): Buffer {
539
+ const aad = filePathAad(filePath, memoryDir);
540
+ try {
541
+ return decryptFileBody(buf, key, aad);
542
+ } catch (err) {
543
+ if (!(err instanceof SecureStoreDecryptError)) {
544
+ throw err;
545
+ }
546
+ const legacyRoot = legacyNamespaceAadRootForFile(filePath, memoryDir);
547
+ if (legacyRoot) {
548
+ try {
549
+ return decryptFileBody(buf, key, filePathAad(filePath, legacyRoot));
550
+ } catch {
551
+ // Fall through to the namespace-reader fallback below.
552
+ }
553
+ }
554
+
555
+ const topLevelRoot = topLevelAadRootForNamespaceReader(filePath, memoryDir);
556
+ if (topLevelRoot) {
557
+ try {
558
+ return decryptFileBody(buf, key, filePathAad(filePath, topLevelRoot));
559
+ } catch {
560
+ // Preserve the caller-facing error from the canonical decrypt attempt.
561
+ }
562
+ }
563
+
564
+ throw err;
565
+ }
566
+ }
567
+
568
+ function topLevelAadRootForNamespaceReader(filePath: string, memoryDir?: string): string | null {
569
+ if (!memoryDir || !path.isAbsolute(filePath)) return null;
570
+ const resolvedMemoryDir = path.resolve(memoryDir);
571
+ const rel = path.relative(resolvedMemoryDir, filePath);
572
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return null;
573
+ const parts = resolvedMemoryDir.split(path.sep);
574
+ if (parts.length < 3 || parts.at(-2) !== "namespaces" || !parts.at(-1)) return null;
575
+ const topLevelRoot = parts.slice(0, -2).join(path.sep) || path.sep;
576
+ const topRel = path.relative(topLevelRoot, filePath);
577
+ if (topRel === "" || topRel.startsWith("..") || path.isAbsolute(topRel)) {
578
+ return null;
579
+ }
580
+ const topParts = topRel.split(path.sep);
581
+ if (topParts[0] !== "namespaces" || topParts[1] !== parts.at(-1)) {
582
+ return null;
583
+ }
584
+ return topLevelRoot;
585
+ }
586
+
587
+ function legacyNamespaceAadRootForFile(filePath: string, memoryDir?: string): string | null {
588
+ if (!memoryDir || !path.isAbsolute(filePath)) return null;
589
+ const rel = path.relative(memoryDir, filePath);
590
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return null;
591
+ const parts = rel.split(path.sep);
592
+ if (parts[0] === "namespaces" && parts.length >= 3 && parts[1]) {
593
+ return path.join(memoryDir, "namespaces", parts[1]);
594
+ }
595
+ return null;
596
+ }
597
+
535
598
  /**
536
599
  * Recursively collect files under `dir` that are read through the
537
600
  * storage-layer secure-store helpers, excluding symlinked entries and
@@ -611,16 +674,6 @@ function normalizeStorageRelativePath(rel: string): string {
611
674
  return normalized;
612
675
  }
613
676
 
614
- function storageAadRootForFile(filePath: string, rootDir: string): string {
615
- const rel = path.relative(rootDir, filePath);
616
- if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return rootDir;
617
- const parts = rel.split(path.sep);
618
- if (parts[0] === "namespaces" && parts.length >= 3 && parts[1]) {
619
- return path.join(rootDir, "namespaces", parts[1]);
620
- }
621
- return rootDir;
622
- }
623
-
624
677
  const ENCRYPTABLE_MARKDOWN_STORAGE_ROOTS = new Set([
625
678
  "facts",
626
679
  "corrections",
@@ -15,6 +15,9 @@
15
15
  */
16
16
 
17
17
  import { PassThrough } from "node:stream";
18
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
19
+ import { tmpdir } from "node:os";
20
+ import path from "node:path";
18
21
  import assert from "node:assert/strict";
19
22
  import test from "node:test";
20
23
 
@@ -42,6 +45,11 @@ import {
42
45
  MAGIC_BYTES,
43
46
  SecureStoreDecryptError,
44
47
  decryptFileBody,
48
+ decryptMemoryDirToPlaintext,
49
+ encryptFileBody,
50
+ filePathAad,
51
+ migrateMemoryDirToEncrypted,
52
+ readMaybeEncryptedFile,
45
53
  } from "./secure-fs.js";
46
54
  import {
47
55
  DEFAULT_ARGON2ID_PARAMS,
@@ -621,6 +629,61 @@ test("end-to-end: build metadata → derive key → seal → open round-trip", (
621
629
  assert.equal(opened.toString("utf8"), "end-to-end");
622
630
  });
623
631
 
632
+ test("secure-store migration keeps namespaced file AAD readable through memory root", async () => {
633
+ const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-aad-"));
634
+ try {
635
+ const filePath = path.join(memoryDir, "namespaces", "project-a", "facts", "note.md");
636
+ await mkdir(path.dirname(filePath), { recursive: true });
637
+ await writeFile(filePath, "namespaced fact", "utf8");
638
+
639
+ const key = deriveKeyScrypt("namespace-aad", Buffer.alloc(KDF_SALT_LENGTH, 0x44), FAST_SCRYPT);
640
+ const migrated = await migrateMemoryDirToEncrypted(memoryDir, key);
641
+
642
+ assert.equal(migrated.encrypted, 1);
643
+ assert.deepEqual(migrated.errors, []);
644
+ assert.equal(
645
+ await readMaybeEncryptedFile(filePath, key, memoryDir),
646
+ "namespaced fact",
647
+ );
648
+
649
+ const decrypted = await decryptMemoryDirToPlaintext(memoryDir, key);
650
+ assert.equal(decrypted.decrypted, 1);
651
+ assert.deepEqual(decrypted.errors, []);
652
+ assert.equal(await readFile(filePath, "utf8"), "namespaced fact");
653
+ } finally {
654
+ await rm(memoryDir, { recursive: true, force: true });
655
+ }
656
+ });
657
+
658
+ test("secure-store can recover namespaced files encrypted with legacy namespace AAD", async () => {
659
+ const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-secure-store-legacy-aad-"));
660
+ try {
661
+ const namespaceRoot = path.join(memoryDir, "namespaces", "project-a");
662
+ const filePath = path.join(namespaceRoot, "facts", "note.md");
663
+ await mkdir(path.dirname(filePath), { recursive: true });
664
+
665
+ 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
+ );
671
+ await writeFile(filePath, legacyEncrypted);
672
+
673
+ assert.equal(
674
+ await readMaybeEncryptedFile(filePath, key, memoryDir),
675
+ "legacy namespaced fact",
676
+ );
677
+
678
+ const decrypted = await decryptMemoryDirToPlaintext(memoryDir, key);
679
+ assert.equal(decrypted.decrypted, 1);
680
+ assert.deepEqual(decrypted.errors, []);
681
+ assert.equal(await readFile(filePath, "utf8"), "legacy namespaced fact");
682
+ } finally {
683
+ await rm(memoryDir, { recursive: true, force: true });
684
+ }
685
+ });
686
+
624
687
  test("tampered envelope salt fails decryption (codex P1 — header AAD)", () => {
625
688
  const salt = generateSalt();
626
689
  const key = deriveKeyScrypt("passphrase", salt, FAST_SCRYPT);
@@ -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 const aad = filePathAad(filePath, memoryDir);\n return decryptFileBody(buf, key, aad);\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, storageAadRootForFile(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 aad = filePathAad(filePath, storageAadRootForFile(filePath, dir));\n const plaintext = decryptFileBody(buf, key, aad);\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\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\nfunction storageAadRootForFile(filePath: string, rootDir: string): string {\n const rel = path.relative(rootDir, filePath);\n if (rel === \"\" || rel.startsWith(\"..\") || path.isAbsolute(rel)) return rootDir;\n const parts = rel.split(path.sep);\n if (parts[0] === \"namespaces\" && parts.length >= 3 && parts[1]) {\n return path.join(rootDir, \"namespaces\", parts[1]);\n }\n return rootDir;\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"]}