@remnic/core 9.3.544 → 9.3.546
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-cli.js +15 -15
- package/dist/access-http.js +9 -9
- package/dist/access-mcp.js +8 -8
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +6 -6
- package/dist/briefing.js +3 -3
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-FWYUJY4N.js → chunk-25BY3HHZ.js} +2 -2
- package/dist/{chunk-PR577DSG.js → chunk-2R6MP5C6.js} +12 -12
- package/dist/{chunk-K6OABSBA.js → chunk-5GX5MUQ2.js} +2 -2
- package/dist/{chunk-NHZHAZCJ.js → chunk-5RIRL3XL.js} +2 -2
- package/dist/{chunk-QKV4LVLA.js → chunk-5WLYNZPC.js} +2 -2
- package/dist/{chunk-25XNFWT3.js → chunk-BD5LHQWD.js} +2 -2
- package/dist/{chunk-BRCYNT4I.js → chunk-CC2ESOOG.js} +2 -2
- package/dist/{chunk-INMWM3UZ.js → chunk-DEGH5BUP.js} +4 -4
- package/dist/{chunk-QWQX7YK5.js → chunk-E5OECWZ5.js} +2 -2
- package/dist/{chunk-J62VXZR2.js → chunk-FADZBOR4.js} +2 -2
- package/dist/{chunk-GWUUEPOR.js → chunk-FVCZINOF.js} +2 -2
- package/dist/{chunk-7NEW7PTS.js → chunk-GSFFWOWU.js} +5 -5
- package/dist/{chunk-73JGZ5VA.js → chunk-ILXTATKK.js} +47 -20
- package/dist/chunk-ILXTATKK.js.map +1 -0
- package/dist/{chunk-FXE46BJ5.js → chunk-JFN6K74Q.js} +2 -2
- package/dist/{chunk-ENIIJ3MZ.js → chunk-M3BAMVAE.js} +2 -2
- package/dist/{chunk-QCJLDMY5.js → chunk-OF46AKZC.js} +8 -8
- package/dist/{chunk-LKUNOD7B.js → chunk-S53PAX2V.js} +2 -2
- package/dist/{chunk-3M7OTJ5H.js → chunk-SI3QCHWF.js} +4 -4
- package/dist/{chunk-UMXWQL3P.js → chunk-SOTR74FK.js} +2 -2
- package/dist/{chunk-A5TLPLUO.js → chunk-TVZ6LKKS.js} +2 -2
- package/dist/{chunk-RGLJNOQN.js → chunk-Y4YATXHL.js} +3 -3
- package/dist/{chunk-GAMKRZRP.js → chunk-YN3WHXBG.js} +5 -5
- package/dist/{chunk-XE23FSDQ.js → chunk-Z56KAZQL.js} +2 -2
- package/dist/{chunk-NJBIGWAI.js → chunk-ZLDUQWT2.js} +3 -2
- package/dist/chunk-ZLDUQWT2.js.map +1 -0
- package/dist/cli.js +18 -18
- package/dist/compounding/engine.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/index.js +24 -24
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/offline-sync.js +2 -2
- package/dist/operator-toolkit.js +6 -6
- package/dist/orchestrator.js +11 -11
- package/dist/secure-store/index.js +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/storage.js +2 -2
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/offline-sync.test.ts +1 -0
- package/src/offline-sync.ts +1 -0
- package/src/secure-store/secure-fs.ts +63 -38
- package/src/secure-store/secure-store.test.ts +155 -106
- package/dist/chunk-73JGZ5VA.js.map +0 -1
- package/dist/chunk-NJBIGWAI.js.map +0 -1
- /package/dist/{chunk-FWYUJY4N.js.map → chunk-25BY3HHZ.js.map} +0 -0
- /package/dist/{chunk-PR577DSG.js.map → chunk-2R6MP5C6.js.map} +0 -0
- /package/dist/{chunk-K6OABSBA.js.map → chunk-5GX5MUQ2.js.map} +0 -0
- /package/dist/{chunk-NHZHAZCJ.js.map → chunk-5RIRL3XL.js.map} +0 -0
- /package/dist/{chunk-QKV4LVLA.js.map → chunk-5WLYNZPC.js.map} +0 -0
- /package/dist/{chunk-25XNFWT3.js.map → chunk-BD5LHQWD.js.map} +0 -0
- /package/dist/{chunk-BRCYNT4I.js.map → chunk-CC2ESOOG.js.map} +0 -0
- /package/dist/{chunk-INMWM3UZ.js.map → chunk-DEGH5BUP.js.map} +0 -0
- /package/dist/{chunk-QWQX7YK5.js.map → chunk-E5OECWZ5.js.map} +0 -0
- /package/dist/{chunk-J62VXZR2.js.map → chunk-FADZBOR4.js.map} +0 -0
- /package/dist/{chunk-GWUUEPOR.js.map → chunk-FVCZINOF.js.map} +0 -0
- /package/dist/{chunk-7NEW7PTS.js.map → chunk-GSFFWOWU.js.map} +0 -0
- /package/dist/{chunk-FXE46BJ5.js.map → chunk-JFN6K74Q.js.map} +0 -0
- /package/dist/{chunk-ENIIJ3MZ.js.map → chunk-M3BAMVAE.js.map} +0 -0
- /package/dist/{chunk-QCJLDMY5.js.map → chunk-OF46AKZC.js.map} +0 -0
- /package/dist/{chunk-LKUNOD7B.js.map → chunk-S53PAX2V.js.map} +0 -0
- /package/dist/{chunk-3M7OTJ5H.js.map → chunk-SI3QCHWF.js.map} +0 -0
- /package/dist/{chunk-UMXWQL3P.js.map → chunk-SOTR74FK.js.map} +0 -0
- /package/dist/{chunk-A5TLPLUO.js.map → chunk-TVZ6LKKS.js.map} +0 -0
- /package/dist/{chunk-RGLJNOQN.js.map → chunk-Y4YATXHL.js.map} +0 -0
- /package/dist/{chunk-GAMKRZRP.js.map → chunk-YN3WHXBG.js.map} +0 -0
- /package/dist/{chunk-XE23FSDQ.js.map → chunk-Z56KAZQL.js.map} +0 -0
|
@@ -160,7 +160,7 @@ export function decryptFileBody(buf: Buffer, key: Buffer, aad?: Buffer): Buffer
|
|
|
160
160
|
const version = buf.readUInt8(MAGIC_BYTES.length);
|
|
161
161
|
if (version !== FILE_FORMAT_VERSION) {
|
|
162
162
|
throw new Error(
|
|
163
|
-
`decryptFileBody: unsupported file format version ${version} (this build supports ${FILE_FORMAT_VERSION})
|
|
163
|
+
`decryptFileBody: unsupported file format version ${version} (this build supports ${FILE_FORMAT_VERSION})`
|
|
164
164
|
);
|
|
165
165
|
}
|
|
166
166
|
const flags = buf.readUInt8(MAGIC_BYTES.length + 1);
|
|
@@ -173,9 +173,7 @@ export function decryptFileBody(buf: Buffer, key: Buffer, aad?: Buffer): Buffer
|
|
|
173
173
|
return openEnvelope(key, envelope, aad ? { aad } : {});
|
|
174
174
|
} catch (err) {
|
|
175
175
|
const msg = err instanceof Error ? err.message : String(err);
|
|
176
|
-
throw new SecureStoreDecryptError(
|
|
177
|
-
`secure-store decryption failed: ${msg}`,
|
|
178
|
-
);
|
|
176
|
+
throw new SecureStoreDecryptError(`secure-store decryption failed: ${msg}`);
|
|
179
177
|
}
|
|
180
178
|
}
|
|
181
179
|
|
|
@@ -227,7 +225,7 @@ export function filePathAad(filePath: string, memoryDir?: string): Buffer {
|
|
|
227
225
|
export async function readMaybeEncryptedFileBuffer(
|
|
228
226
|
filePath: string,
|
|
229
227
|
key: Buffer | null,
|
|
230
|
-
memoryDir?: string
|
|
228
|
+
memoryDir?: string
|
|
231
229
|
): Promise<Buffer> {
|
|
232
230
|
const buf = await readFile(filePath);
|
|
233
231
|
if (!isEncryptedFile(buf)) {
|
|
@@ -237,8 +235,7 @@ export async function readMaybeEncryptedFileBuffer(
|
|
|
237
235
|
// Encrypted — key required.
|
|
238
236
|
if (key === null) {
|
|
239
237
|
throw new SecureStoreLockedError(
|
|
240
|
-
`secure-store is locked — cannot read encrypted file at ${filePath}.
|
|
241
|
-
"Run `remnic secure-store unlock` to decrypt.",
|
|
238
|
+
`secure-store is locked — cannot read encrypted file at ${filePath}. Run \`remnic secure-store unlock\` to decrypt.`
|
|
242
239
|
);
|
|
243
240
|
}
|
|
244
241
|
return decryptFileBodyForPath(buf, key, filePath, memoryDir);
|
|
@@ -247,7 +244,7 @@ export async function readMaybeEncryptedFileBuffer(
|
|
|
247
244
|
export async function readMaybeEncryptedFile(
|
|
248
245
|
filePath: string,
|
|
249
246
|
key: Buffer | null,
|
|
250
|
-
memoryDir?: string
|
|
247
|
+
memoryDir?: string
|
|
251
248
|
): Promise<string> {
|
|
252
249
|
return (await readMaybeEncryptedFileBuffer(filePath, key, memoryDir)).toString("utf8");
|
|
253
250
|
}
|
|
@@ -280,7 +277,7 @@ export async function writeMaybeEncryptedFile(
|
|
|
280
277
|
content: string | Buffer,
|
|
281
278
|
key: Buffer | null,
|
|
282
279
|
options: WriteMaybeEncryptedFileOptions = {},
|
|
283
|
-
memoryDir?: string
|
|
280
|
+
memoryDir?: string
|
|
284
281
|
): Promise<void> {
|
|
285
282
|
const { mode = 0o600, atomic = true } = options;
|
|
286
283
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -317,7 +314,7 @@ export async function writeMaybeEncryptedFileFromChunks(
|
|
|
317
314
|
chunks: AsyncIterable<Buffer>,
|
|
318
315
|
key: Buffer | null,
|
|
319
316
|
options: WriteMaybeEncryptedFileOptions = {},
|
|
320
|
-
memoryDir?: string
|
|
317
|
+
memoryDir?: string
|
|
321
318
|
): Promise<void> {
|
|
322
319
|
const { mode = 0o600, atomic = true } = options;
|
|
323
320
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -350,12 +347,7 @@ export async function writeMaybeEncryptedFileFromChunks(
|
|
|
350
347
|
const final = cipher.final();
|
|
351
348
|
if (final.length > 0) await handle.write(final);
|
|
352
349
|
const authTag = cipher.getAuthTag();
|
|
353
|
-
await handle.write(
|
|
354
|
-
authTag,
|
|
355
|
-
0,
|
|
356
|
-
authTag.length,
|
|
357
|
-
MAGIC_HEADER_SIZE + ENVELOPE_LAYOUT.authTag,
|
|
358
|
-
);
|
|
350
|
+
await handle.write(authTag, 0, authTag.length, MAGIC_HEADER_SIZE + ENVELOPE_LAYOUT.authTag);
|
|
359
351
|
} else {
|
|
360
352
|
for await (const chunk of chunks) {
|
|
361
353
|
if (chunk.length > 0) await handle.write(chunk);
|
|
@@ -421,7 +413,7 @@ export interface DecryptResult {
|
|
|
421
413
|
export async function migrateMemoryDirToEncrypted(
|
|
422
414
|
dir: string,
|
|
423
415
|
key: Buffer,
|
|
424
|
-
onBeforeEncrypt?: (filePath: string) => Promise<void
|
|
416
|
+
onBeforeEncrypt?: (filePath: string) => Promise<void>
|
|
425
417
|
): Promise<MigrateResult> {
|
|
426
418
|
const result: MigrateResult = { encrypted: 0, skipped: 0, errors: [] };
|
|
427
419
|
|
|
@@ -482,10 +474,7 @@ export async function migrateMemoryDirToEncrypted(
|
|
|
482
474
|
* each plaintext replacement via temp-file + rename so a per-file failure
|
|
483
475
|
* leaves the ciphertext intact.
|
|
484
476
|
*/
|
|
485
|
-
export async function decryptMemoryDirToPlaintext(
|
|
486
|
-
dir: string,
|
|
487
|
-
key: Buffer,
|
|
488
|
-
): Promise<DecryptResult> {
|
|
477
|
+
export async function decryptMemoryDirToPlaintext(dir: string, key: Buffer): Promise<DecryptResult> {
|
|
489
478
|
const result: DecryptResult = { decrypted: 0, skipped: 0, errors: [] };
|
|
490
479
|
|
|
491
480
|
const files = await collectStorageManagedFiles(dir, isDecryptableStoragePath);
|
|
@@ -530,12 +519,7 @@ function uniqueAtomicTempPath(filePath: string, label: string): string {
|
|
|
530
519
|
return `${filePath}.${label}-${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
531
520
|
}
|
|
532
521
|
|
|
533
|
-
function decryptFileBodyForPath(
|
|
534
|
-
buf: Buffer,
|
|
535
|
-
key: Buffer,
|
|
536
|
-
filePath: string,
|
|
537
|
-
memoryDir?: string,
|
|
538
|
-
): Buffer {
|
|
522
|
+
function decryptFileBodyForPath(buf: Buffer, key: Buffer, filePath: string, memoryDir?: string): Buffer {
|
|
539
523
|
const aad = filePathAad(filePath, memoryDir);
|
|
540
524
|
try {
|
|
541
525
|
return decryptFileBody(buf, key, aad);
|
|
@@ -613,18 +597,26 @@ async function collectEncryptableStorageFiles(dir: string, rootDir = dir): Promi
|
|
|
613
597
|
async function collectStorageManagedFiles(
|
|
614
598
|
dir: string,
|
|
615
599
|
includeFile: (filePath: string, rootDir: string) => boolean,
|
|
616
|
-
rootDir = dir
|
|
600
|
+
rootDir = dir
|
|
617
601
|
): Promise<string[]> {
|
|
618
602
|
const results: string[] = [];
|
|
603
|
+
let scanDir = dir;
|
|
604
|
+
let scanRootDir = rootDir;
|
|
605
|
+
if (path.resolve(dir) === path.resolve(rootDir)) {
|
|
606
|
+
const normalizedRoot = await resolveStorageManagedRootForScan(dir);
|
|
607
|
+
if (!normalizedRoot) return results;
|
|
608
|
+
scanDir = normalizedRoot;
|
|
609
|
+
scanRootDir = normalizedRoot;
|
|
610
|
+
}
|
|
619
611
|
let names: string[];
|
|
620
612
|
try {
|
|
621
|
-
names = await readdir(
|
|
613
|
+
names = await readdir(scanDir, { encoding: "utf8" });
|
|
622
614
|
} catch {
|
|
623
615
|
return results;
|
|
624
616
|
}
|
|
625
617
|
for (const name of names) {
|
|
626
618
|
if (name.startsWith(".secure-store")) continue;
|
|
627
|
-
const full = path.join(
|
|
619
|
+
const full = path.join(scanDir, name);
|
|
628
620
|
let isDir = false;
|
|
629
621
|
let isFile = false;
|
|
630
622
|
try {
|
|
@@ -636,15 +628,53 @@ async function collectStorageManagedFiles(
|
|
|
636
628
|
continue;
|
|
637
629
|
}
|
|
638
630
|
if (isDir) {
|
|
639
|
-
const sub = await collectStorageManagedFiles(full, includeFile,
|
|
631
|
+
const sub = await collectStorageManagedFiles(full, includeFile, scanRootDir);
|
|
640
632
|
results.push(...sub);
|
|
641
|
-
} else if (isFile && includeFile(full,
|
|
633
|
+
} else if (isFile && includeFile(full, scanRootDir)) {
|
|
642
634
|
results.push(full);
|
|
643
635
|
}
|
|
644
636
|
}
|
|
645
637
|
return results;
|
|
646
638
|
}
|
|
647
639
|
|
|
640
|
+
async function resolveStorageManagedRootForScan(dir: string): Promise<string | null> {
|
|
641
|
+
const lstatPath = normalizePathForLstat(dir);
|
|
642
|
+
let stat: Awaited<ReturnType<typeof lstat>>;
|
|
643
|
+
try {
|
|
644
|
+
stat = await lstat(lstatPath);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (isFsErrorWithCode(error, "ENOENT")) return null;
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
if (stat.isSymbolicLink()) {
|
|
650
|
+
throw new Error(`secure-store migration root must not be a symlink: ${dir}`);
|
|
651
|
+
}
|
|
652
|
+
if (!stat.isDirectory()) {
|
|
653
|
+
throw new Error(`secure-store migration root must be a directory: ${dir}`);
|
|
654
|
+
}
|
|
655
|
+
return lstatPath;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function normalizePathForLstat(filePath: string): string {
|
|
659
|
+
return stripTrailingPathSeparators(path.normalize(filePath));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function isFsErrorWithCode(error: unknown, code: string): boolean {
|
|
663
|
+
return typeof error === "object" && error !== null && (error as { code?: unknown }).code === code;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function stripTrailingPathSeparators(filePath: string): string {
|
|
667
|
+
const root = path.parse(filePath).root;
|
|
668
|
+
let end = filePath.length;
|
|
669
|
+
while (
|
|
670
|
+
end > root.length &&
|
|
671
|
+
(filePath[end - 1] === path.sep || filePath[end - 1] === path.posix.sep || filePath[end - 1] === path.win32.sep)
|
|
672
|
+
) {
|
|
673
|
+
end -= 1;
|
|
674
|
+
}
|
|
675
|
+
return end === filePath.length ? filePath : filePath.slice(0, end);
|
|
676
|
+
}
|
|
677
|
+
|
|
648
678
|
function isEncryptableStoragePath(filePath: string, rootDir: string): boolean {
|
|
649
679
|
const rel = path.relative(rootDir, filePath);
|
|
650
680
|
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
@@ -710,9 +740,4 @@ function isEncryptableSummarySidecar(normalized: string): boolean {
|
|
|
710
740
|
return normalized.startsWith("summaries/") && normalized.endsWith(".json");
|
|
711
741
|
}
|
|
712
742
|
|
|
713
|
-
const DECRYPTABLE_SIDECAR_ROOTS = new Set([
|
|
714
|
-
"state",
|
|
715
|
-
"indexes",
|
|
716
|
-
"index",
|
|
717
|
-
"provenance",
|
|
718
|
-
]);
|
|
743
|
+
const DECRYPTABLE_SIDECAR_ROOTS = new Set(["state", "indexes", "index", "provenance"]);
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* silently regress them.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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:
|
|
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 () => {
|