@pcircle/footprint 1.2.2 → 1.5.0
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/LICENSE +1 -1
- package/README.md +125 -161
- package/SKILL.md +50 -50
- package/dist/src/analyzers/content-analyzer.d.ts.map +1 -1
- package/dist/src/analyzers/content-analyzer.js +20 -4
- package/dist/src/analyzers/content-analyzer.js.map +1 -1
- package/dist/src/cli/constants.d.ts +20 -0
- package/dist/src/cli/constants.d.ts.map +1 -0
- package/dist/src/cli/constants.js +25 -0
- package/dist/src/cli/constants.js.map +1 -0
- package/dist/src/cli/index.d.ts +3 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +25 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/setup.d.ts +6 -0
- package/dist/src/cli/setup.d.ts.map +1 -0
- package/dist/src/cli/setup.js +356 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/types.d.ts +38 -0
- package/dist/src/cli/types.d.ts.map +1 -0
- package/dist/src/cli/types.js +5 -0
- package/dist/src/cli/types.js.map +1 -0
- package/dist/src/cli/utils/config.d.ts +19 -0
- package/dist/src/cli/utils/config.d.ts.map +1 -0
- package/dist/src/cli/utils/config.js +86 -0
- package/dist/src/cli/utils/config.js.map +1 -0
- package/dist/src/cli/utils/detect.d.ts +14 -0
- package/dist/src/cli/utils/detect.d.ts.map +1 -0
- package/dist/src/cli/utils/detect.js +57 -0
- package/dist/src/cli/utils/detect.js.map +1 -0
- package/dist/src/cli/utils/env.d.ts +15 -0
- package/dist/src/cli/utils/env.d.ts.map +1 -0
- package/dist/src/cli/utils/env.js +85 -0
- package/dist/src/cli/utils/env.js.map +1 -0
- package/dist/src/cli/utils/validation.d.ts +17 -0
- package/dist/src/cli/utils/validation.d.ts.map +1 -0
- package/dist/src/cli/utils/validation.js +77 -0
- package/dist/src/cli/utils/validation.js.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +53 -38
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/crypto/decrypt.d.ts.map +1 -1
- package/dist/src/lib/crypto/decrypt.js +12 -8
- package/dist/src/lib/crypto/decrypt.js.map +1 -1
- package/dist/src/lib/crypto/encrypt.d.ts.map +1 -1
- package/dist/src/lib/crypto/encrypt.js +6 -3
- package/dist/src/lib/crypto/encrypt.js.map +1 -1
- package/dist/src/lib/crypto/key-derivation.d.ts +1 -1
- package/dist/src/lib/crypto/key-derivation.d.ts.map +1 -1
- package/dist/src/lib/crypto/key-derivation.js +11 -11
- package/dist/src/lib/crypto/key-derivation.js.map +1 -1
- package/dist/src/lib/storage/database.d.ts +46 -3
- package/dist/src/lib/storage/database.d.ts.map +1 -1
- package/dist/src/lib/storage/database.js +175 -80
- package/dist/src/lib/storage/database.js.map +1 -1
- package/dist/src/lib/storage/export.d.ts +3 -4
- package/dist/src/lib/storage/export.d.ts.map +1 -1
- package/dist/src/lib/storage/export.js +75 -62
- package/dist/src/lib/storage/export.js.map +1 -1
- package/dist/src/lib/storage/salt-storage.d.ts +1 -1
- package/dist/src/lib/storage/salt-storage.d.ts.map +1 -1
- package/dist/src/lib/storage/salt-storage.js +26 -18
- package/dist/src/lib/storage/salt-storage.js.map +1 -1
- package/dist/src/lib/storage/schema.d.ts +1 -1
- package/dist/src/lib/storage/schema.d.ts.map +1 -1
- package/dist/src/lib/storage/schema.js +29 -47
- package/dist/src/lib/storage/schema.js.map +1 -1
- package/dist/src/lib/tool-wrapper.d.ts.map +1 -1
- package/dist/src/lib/tool-wrapper.js +2 -2
- package/dist/src/lib/tool-wrapper.js.map +1 -1
- package/dist/src/prompts/skill-prompt.d.ts +6 -0
- package/dist/src/prompts/skill-prompt.d.ts.map +1 -0
- package/dist/src/prompts/skill-prompt.js +125 -0
- package/dist/src/prompts/skill-prompt.js.map +1 -0
- package/dist/src/tools/capture-footprint.d.ts +2 -2
- package/dist/src/tools/capture-footprint.d.ts.map +1 -1
- package/dist/src/tools/capture-footprint.js +53 -12
- package/dist/src/tools/capture-footprint.js.map +1 -1
- package/dist/src/tools/delete-footprints.d.ts +19 -2
- package/dist/src/tools/delete-footprints.d.ts.map +1 -1
- package/dist/src/tools/delete-footprints.js +56 -8
- package/dist/src/tools/delete-footprints.js.map +1 -1
- package/dist/src/tools/export-footprints.d.ts +14 -6
- package/dist/src/tools/export-footprints.d.ts.map +1 -1
- package/dist/src/tools/export-footprints.js +54 -15
- package/dist/src/tools/export-footprints.js.map +1 -1
- package/dist/src/tools/get-footprint.d.ts +1 -7
- package/dist/src/tools/get-footprint.d.ts.map +1 -1
- package/dist/src/tools/get-footprint.js +26 -22
- package/dist/src/tools/get-footprint.js.map +1 -1
- package/dist/src/tools/index.d.ts +1 -3
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +1 -3
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-footprints.d.ts +3 -17
- package/dist/src/tools/list-footprints.d.ts.map +1 -1
- package/dist/src/tools/list-footprints.js +27 -16
- package/dist/src/tools/list-footprints.js.map +1 -1
- package/dist/src/tools/manage-tags.d.ts +47 -0
- package/dist/src/tools/manage-tags.d.ts.map +1 -0
- package/dist/src/tools/manage-tags.js +109 -0
- package/dist/src/tools/manage-tags.js.map +1 -0
- package/dist/src/tools/search-footprints.d.ts +4 -18
- package/dist/src/tools/search-footprints.d.ts.map +1 -1
- package/dist/src/tools/search-footprints.js +32 -16
- package/dist/src/tools/search-footprints.js.map +1 -1
- package/dist/src/tools/suggest-capture.d.ts +1 -1
- package/dist/src/tools/suggest-capture.d.ts.map +1 -1
- package/dist/src/tools/suggest-capture.js +6 -2
- package/dist/src/tools/suggest-capture.js.map +1 -1
- package/dist/src/tools/verify-footprint.d.ts +7 -54
- package/dist/src/tools/verify-footprint.d.ts.map +1 -1
- package/dist/src/tools/verify-footprint.js +22 -19
- package/dist/src/tools/verify-footprint.js.map +1 -1
- package/dist/src/types.d.ts +4 -4
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/ui/register.js +3 -3
- package/dist/src/ui/register.js.map +1 -1
- package/dist/ui/dashboard.html +78 -65
- package/dist/ui/detail.html +69 -56
- package/dist/ui/export.html +72 -59
- package/package.json +28 -16
- package/dist/src/lib/errors/base-error.d.ts +0 -15
- package/dist/src/lib/errors/base-error.d.ts.map +0 -1
- package/dist/src/lib/errors/base-error.js +0 -34
- package/dist/src/lib/errors/base-error.js.map +0 -1
- package/dist/src/lib/errors/crypto-error.d.ts +0 -29
- package/dist/src/lib/errors/crypto-error.d.ts.map +0 -1
- package/dist/src/lib/errors/crypto-error.js +0 -43
- package/dist/src/lib/errors/crypto-error.js.map +0 -1
- package/dist/src/lib/errors/index.d.ts +0 -26
- package/dist/src/lib/errors/index.d.ts.map +0 -1
- package/dist/src/lib/errors/index.js +0 -26
- package/dist/src/lib/errors/index.js.map +0 -1
- package/dist/src/lib/errors/storage-error.d.ts +0 -25
- package/dist/src/lib/errors/storage-error.d.ts.map +0 -1
- package/dist/src/lib/errors/storage-error.js +0 -38
- package/dist/src/lib/errors/storage-error.js.map +0 -1
- package/dist/src/lib/errors/validation-error.d.ts +0 -21
- package/dist/src/lib/errors/validation-error.d.ts.map +0 -1
- package/dist/src/lib/errors/validation-error.js +0 -29
- package/dist/src/lib/errors/validation-error.js.map +0 -1
- package/dist/src/test-helpers.d.ts +0 -33
- package/dist/src/test-helpers.d.ts.map +0 -1
- package/dist/src/test-helpers.js +0 -108
- package/dist/src/test-helpers.js.map +0 -1
- package/dist/src/tools/get-tag-stats.d.ts +0 -30
- package/dist/src/tools/get-tag-stats.d.ts.map +0 -1
- package/dist/src/tools/get-tag-stats.js +0 -33
- package/dist/src/tools/get-tag-stats.js.map +0 -1
- package/dist/src/tools/remove-tag.d.ts +0 -22
- package/dist/src/tools/remove-tag.d.ts.map +0 -1
- package/dist/src/tools/remove-tag.js +0 -30
- package/dist/src/tools/remove-tag.js.map +0 -1
- package/dist/src/tools/rename-tag.d.ts +0 -24
- package/dist/src/tools/rename-tag.d.ts.map +0 -1
- package/dist/src/tools/rename-tag.js +0 -34
- package/dist/src/tools/rename-tag.js.map +0 -1
- package/dist/tests/error-handling.test.d.ts +0 -2
- package/dist/tests/error-handling.test.d.ts.map +0 -1
- package/dist/tests/error-handling.test.js +0 -114
- package/dist/tests/error-handling.test.js.map +0 -1
- package/dist/tests/fixtures.d.ts +0 -87
- package/dist/tests/fixtures.d.ts.map +0 -1
- package/dist/tests/fixtures.js +0 -130
- package/dist/tests/fixtures.js.map +0 -1
- package/dist/tests/integration.test.d.ts +0 -2
- package/dist/tests/integration.test.d.ts.map +0 -1
- package/dist/tests/integration.test.js +0 -115
- package/dist/tests/integration.test.js.map +0 -1
- package/dist/tests/resources.test.d.ts +0 -2
- package/dist/tests/resources.test.d.ts.map +0 -1
- package/dist/tests/resources.test.js +0 -73
- package/dist/tests/resources.test.js.map +0 -1
- package/dist/tests/setup.d.ts +0 -8
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/setup.js +0 -8
- package/dist/tests/setup.js.map +0 -1
- package/dist/tests/tools.test.d.ts +0 -2
- package/dist/tests/tools.test.d.ts.map +0 -1
- package/dist/tests/tools.test.js +0 -224
- package/dist/tests/tools.test.js.map +0 -1
- package/dist/ui-tmp/ui/export.html +0 -409
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/* global TextDecoder */
|
|
2
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
2
3
|
/**
|
|
3
4
|
* Decrypt ciphertext using XChaCha20-Poly1305 AEAD
|
|
4
5
|
*
|
|
@@ -10,10 +11,10 @@ import { xchacha20poly1305 } from '@noble/ciphers/chacha';
|
|
|
10
11
|
*/
|
|
11
12
|
export function decrypt(ciphertext, nonce, key) {
|
|
12
13
|
if (key.length !== 32) {
|
|
13
|
-
throw new Error(
|
|
14
|
+
throw new Error("Key must be 32 bytes");
|
|
14
15
|
}
|
|
15
16
|
if (nonce.length !== 24) {
|
|
16
|
-
throw new Error(
|
|
17
|
+
throw new Error("Nonce must be 24 bytes");
|
|
17
18
|
}
|
|
18
19
|
// Create cipher instance
|
|
19
20
|
const cipher = xchacha20poly1305(key, nonce);
|
|
@@ -21,14 +22,17 @@ export function decrypt(ciphertext, nonce, key) {
|
|
|
21
22
|
// Decrypt and verify authentication tag
|
|
22
23
|
const plaintextBytes = cipher.decrypt(ciphertext);
|
|
23
24
|
// Convert bytes back to string
|
|
24
|
-
|
|
25
|
+
const result = new TextDecoder().decode(plaintextBytes);
|
|
26
|
+
// Zero decrypted bytes from memory (defense-in-depth)
|
|
27
|
+
plaintextBytes.fill(0);
|
|
28
|
+
return result;
|
|
25
29
|
}
|
|
26
30
|
catch (error) {
|
|
27
|
-
// Log detailed error for debugging (server-side only, never exposed to client)
|
|
28
|
-
const originalError = error instanceof Error ? error.message : String(error);
|
|
29
|
-
console.error('[Decrypt] Decryption failed:', originalError);
|
|
30
31
|
// User-facing error remains generic (security best practice)
|
|
31
|
-
|
|
32
|
+
// Original error preserved in cause chain for debugging
|
|
33
|
+
throw new Error("Decryption failed: invalid key or tampered data", {
|
|
34
|
+
cause: error,
|
|
35
|
+
});
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
//# sourceMappingURL=decrypt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decrypt.js","sourceRoot":"","sources":["../../../../src/lib/crypto/decrypt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"decrypt.js","sourceRoot":"","sources":["../../../../src/lib/crypto/decrypt.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CACrB,UAAsB,EACtB,KAAiB,EACjB,GAAe;IAEf,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC5C,CAAC;IAED,yBAAyB;IACzB,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAE7C,IAAI,CAAC;QACH,wCAAwC;QACxC,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAElD,+BAA+B;QAC/B,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAExD,sDAAsD;QACtD,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAEvB,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,6DAA6D;QAC7D,wDAAwD;QACxD,MAAM,IAAI,KAAK,CAAC,iDAAiD,EAAE;YACjE,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../../../src/lib/crypto/encrypt.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../../../src/lib/crypto/encrypt.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,CAAC;IACvB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,aAAa,CAqBzE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
/* global TextEncoder */
|
|
2
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
3
|
+
import { randomBytes } from "@noble/hashes/utils.js";
|
|
3
4
|
/**
|
|
4
5
|
* Encrypt plaintext using XChaCha20-Poly1305 AEAD
|
|
5
6
|
*
|
|
@@ -9,7 +10,7 @@ import { randomBytes } from '@noble/hashes/utils';
|
|
|
9
10
|
*/
|
|
10
11
|
export function encrypt(plaintext, key) {
|
|
11
12
|
if (key.length !== 32) {
|
|
12
|
-
throw new Error(
|
|
13
|
+
throw new Error("Key must be 32 bytes");
|
|
13
14
|
}
|
|
14
15
|
// Generate random 24-byte nonce (XChaCha20 extended nonce)
|
|
15
16
|
const nonce = randomBytes(24);
|
|
@@ -19,6 +20,8 @@ export function encrypt(plaintext, key) {
|
|
|
19
20
|
const cipher = xchacha20poly1305(key, nonce);
|
|
20
21
|
// Encrypt with authentication
|
|
21
22
|
const ciphertext = cipher.encrypt(plaintextBytes);
|
|
23
|
+
// Zero plaintext bytes from memory (defense-in-depth)
|
|
24
|
+
plaintextBytes.fill(0);
|
|
22
25
|
return { ciphertext, nonce };
|
|
23
26
|
}
|
|
24
27
|
//# sourceMappingURL=encrypt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypt.js","sourceRoot":"","sources":["../../../../src/lib/crypto/encrypt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"encrypt.js","sourceRoot":"","sources":["../../../../src/lib/crypto/encrypt.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAOrD;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,SAAiB,EAAE,GAAe;IACxD,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IAED,2DAA2D;IAC3D,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAE9B,6BAA6B;IAC7B,MAAM,cAAc,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAE3D,yBAAyB;IACzB,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAE7C,8BAA8B;IAC9B,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAElD,sDAAsD;IACtD,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAEvB,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-derivation.d.ts","sourceRoot":"","sources":["../../../../src/lib/crypto/key-derivation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGlE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,OAAO,CAAC,mBAAmB,CAAM,GACxC,OAAO,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"key-derivation.d.ts","sourceRoot":"","sources":["../../../../src/lib/crypto/key-derivation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGlE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,OAAO,CAAC,mBAAmB,CAAM,GACxC,OAAO,CAAC,UAAU,CAAC,CAmBrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,UAAU,EAChB,MAAM,GAAE,OAAO,CAAC,mBAAmB,CAAM,GACxC,OAAO,CAAC,UAAU,CAAC,CAoBrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,UAAU,EACvB,MAAM,GAAE,OAAO,CAAC,mBAAmB,CAAM,GACxC,OAAO,CAAC,OAAO,CAAC,CA8BlB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { argon2id } from
|
|
2
|
-
import { randomBytes } from
|
|
3
|
-
import { timingSafeEqual } from
|
|
4
|
-
import { DEFAULT_KDF_PARAMS } from
|
|
1
|
+
import { argon2id } from "@noble/hashes/argon2.js";
|
|
2
|
+
import { randomBytes } from "@noble/hashes/utils.js";
|
|
3
|
+
import { timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { DEFAULT_KDF_PARAMS } from "./types.js";
|
|
5
5
|
/**
|
|
6
6
|
* Derive encryption key from password using Argon2id
|
|
7
7
|
*
|
|
@@ -22,7 +22,7 @@ import { DEFAULT_KDF_PARAMS } from './types.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export async function deriveKey(password, params = {}) {
|
|
24
24
|
if (!password || password.length === 0) {
|
|
25
|
-
throw new Error(
|
|
25
|
+
throw new Error("Password cannot be empty");
|
|
26
26
|
}
|
|
27
27
|
const kdfParams = { ...DEFAULT_KDF_PARAMS, ...params };
|
|
28
28
|
// Generate random salt (16 bytes)
|
|
@@ -47,11 +47,11 @@ export async function deriveKey(password, params = {}) {
|
|
|
47
47
|
*/
|
|
48
48
|
export async function rederiveKey(password, salt, params = {}) {
|
|
49
49
|
if (!password || password.length === 0) {
|
|
50
|
-
throw new Error(
|
|
50
|
+
throw new Error("Password cannot be empty");
|
|
51
51
|
}
|
|
52
52
|
const kdfParams = { ...DEFAULT_KDF_PARAMS, ...params };
|
|
53
53
|
if (salt.length !== 16) {
|
|
54
|
-
throw new Error(
|
|
54
|
+
throw new Error("Salt must be 16 bytes");
|
|
55
55
|
}
|
|
56
56
|
// Re-derive key using same salt
|
|
57
57
|
const key = argon2id(password, salt, {
|
|
@@ -76,13 +76,13 @@ export async function rederiveKey(password, salt, params = {}) {
|
|
|
76
76
|
*/
|
|
77
77
|
export async function verifyKey(password, salt, expectedKey, params = {}) {
|
|
78
78
|
if (!password || password.length === 0) {
|
|
79
|
-
throw new Error(
|
|
79
|
+
throw new Error("Password cannot be empty");
|
|
80
80
|
}
|
|
81
81
|
if (salt.length !== 16) {
|
|
82
|
-
throw new Error(
|
|
82
|
+
throw new Error("Salt must be 16 bytes");
|
|
83
83
|
}
|
|
84
84
|
if (expectedKey.length !== 32) {
|
|
85
|
-
throw new Error(
|
|
85
|
+
throw new Error("Expected key must be 32 bytes");
|
|
86
86
|
}
|
|
87
87
|
const kdfParams = { ...DEFAULT_KDF_PARAMS, ...params };
|
|
88
88
|
// Re-derive key using same salt
|
|
@@ -94,7 +94,7 @@ export async function verifyKey(password, salt, expectedKey, params = {}) {
|
|
|
94
94
|
});
|
|
95
95
|
// Constant-time comparison to prevent timing attacks
|
|
96
96
|
try {
|
|
97
|
-
return timingSafeEqual(
|
|
97
|
+
return timingSafeEqual(derivedKey, expectedKey);
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
100
100
|
// timingSafeEqual throws if lengths don't match
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-derivation.js","sourceRoot":"","sources":["../../../../src/lib/crypto/key-derivation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"key-derivation.js","sourceRoot":"","sources":["../../../../src/lib/crypto/key-derivation.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Database from
|
|
2
|
-
import type { Evidence } from
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import type { Evidence } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Evidence database with CRUD operations
|
|
5
5
|
* Manages encrypted evidence storage with SQLite backend
|
|
@@ -17,7 +17,7 @@ export declare class EvidenceDatabase {
|
|
|
17
17
|
* @param evidence - Evidence data without id, createdAt, updatedAt
|
|
18
18
|
* @returns UUID of created evidence
|
|
19
19
|
*/
|
|
20
|
-
create(evidence: Omit<Evidence,
|
|
20
|
+
create(evidence: Omit<Evidence, "id" | "createdAt" | "updatedAt">): string;
|
|
21
21
|
/**
|
|
22
22
|
* Finds evidence by ID
|
|
23
23
|
* @param id - Evidence UUID
|
|
@@ -53,6 +53,38 @@ export declare class EvidenceDatabase {
|
|
|
53
53
|
* @throws Error if tags array is empty or all tags are whitespace
|
|
54
54
|
*/
|
|
55
55
|
addTags(id: string, tags: string[]): void;
|
|
56
|
+
/**
|
|
57
|
+
* Builds a WHERE clause from search/filter options
|
|
58
|
+
* @param options - Filter criteria
|
|
59
|
+
* @returns SQL WHERE clause string and parameter values
|
|
60
|
+
*/
|
|
61
|
+
private buildWhereClause;
|
|
62
|
+
/**
|
|
63
|
+
* Gets count of evidences matching filter criteria
|
|
64
|
+
* @param options - Filter criteria (query, tags, date range)
|
|
65
|
+
* @returns Number of matching evidences
|
|
66
|
+
*/
|
|
67
|
+
getFilteredCount(options: {
|
|
68
|
+
query?: string;
|
|
69
|
+
tags?: string[];
|
|
70
|
+
dateFrom?: string;
|
|
71
|
+
dateTo?: string;
|
|
72
|
+
}): number;
|
|
73
|
+
/**
|
|
74
|
+
* Search evidences and return both paginated results and total matching count
|
|
75
|
+
* in a single pass (builds WHERE clause once instead of twice)
|
|
76
|
+
*/
|
|
77
|
+
searchWithCount(options: {
|
|
78
|
+
query?: string;
|
|
79
|
+
tags?: string[];
|
|
80
|
+
dateFrom?: string;
|
|
81
|
+
dateTo?: string;
|
|
82
|
+
limit?: number;
|
|
83
|
+
offset?: number;
|
|
84
|
+
}): {
|
|
85
|
+
evidences: Evidence[];
|
|
86
|
+
total: number;
|
|
87
|
+
};
|
|
56
88
|
/**
|
|
57
89
|
* Search and filter evidences by various criteria
|
|
58
90
|
* @param options - Search and filter options
|
|
@@ -105,6 +137,17 @@ export declare class EvidenceDatabase {
|
|
|
105
137
|
* @returns Map of tag to count
|
|
106
138
|
*/
|
|
107
139
|
getTagCounts(): Map<string, number>;
|
|
140
|
+
/**
|
|
141
|
+
* Gets total count of all evidence records
|
|
142
|
+
* @returns Total number of evidences in the database
|
|
143
|
+
*/
|
|
144
|
+
getTotalCount(): number;
|
|
145
|
+
/**
|
|
146
|
+
* Gets count of evidence records matching a search query
|
|
147
|
+
* @param query - Search text to match against conversationId and tags
|
|
148
|
+
* @returns Number of matching evidences
|
|
149
|
+
*/
|
|
150
|
+
getSearchCount(query: string): number;
|
|
108
151
|
/**
|
|
109
152
|
* Get the underlying database instance
|
|
110
153
|
* Used for salt storage and other low-level operations
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../../src/lib/storage/database.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../../src/lib/storage/database.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
/* global Buffer, crypto */
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
import { createSchema } from "./schema.js";
|
|
4
|
+
function escapeLikePattern(pattern) {
|
|
5
|
+
return pattern.replace(/[%_\\]/g, "\\$&");
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Evidence database with CRUD operations
|
|
5
9
|
* Manages encrypted evidence storage with SQLite backend
|
|
@@ -94,18 +98,18 @@ export class EvidenceDatabase {
|
|
|
94
98
|
try {
|
|
95
99
|
const limit = options?.limit;
|
|
96
100
|
const offset = options?.offset ?? 0;
|
|
97
|
-
let query =
|
|
101
|
+
let query = "SELECT * FROM evidences ORDER BY timestamp DESC";
|
|
98
102
|
const params = [];
|
|
99
103
|
if (limit !== undefined) {
|
|
100
|
-
query +=
|
|
104
|
+
query += " LIMIT ?";
|
|
101
105
|
params.push(limit);
|
|
102
106
|
if (offset > 0) {
|
|
103
|
-
query +=
|
|
107
|
+
query += " OFFSET ?";
|
|
104
108
|
params.push(offset);
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
else if (offset > 0) {
|
|
108
|
-
query +=
|
|
112
|
+
query += " LIMIT -1 OFFSET ?";
|
|
109
113
|
params.push(offset);
|
|
110
114
|
}
|
|
111
115
|
const stmt = this.db.prepare(query);
|
|
@@ -149,37 +153,132 @@ export class EvidenceDatabase {
|
|
|
149
153
|
addTags(id, tags) {
|
|
150
154
|
try {
|
|
151
155
|
if (tags.length === 0) {
|
|
152
|
-
throw new Error(
|
|
156
|
+
throw new Error("Tags array cannot be empty");
|
|
153
157
|
}
|
|
154
158
|
// Filter out empty/whitespace-only tags
|
|
155
|
-
const validTags = tags.map(t => t.trim()).filter(t => t.length > 0);
|
|
159
|
+
const validTags = tags.map((t) => t.trim()).filter((t) => t.length > 0);
|
|
156
160
|
if (validTags.length === 0) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
}
|
|
159
|
-
// First, get existing tags
|
|
160
|
-
const evidence = this.findById(id);
|
|
161
|
-
if (!evidence) {
|
|
162
|
-
throw new Error(`Evidence with id ${id} not found`);
|
|
161
|
+
throw new Error("All provided tags are empty or whitespace");
|
|
163
162
|
}
|
|
164
|
-
//
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
163
|
+
// Wrap in transaction to prevent read-modify-write race condition
|
|
164
|
+
const transaction = this.db.transaction(() => {
|
|
165
|
+
const evidence = this.findById(id);
|
|
166
|
+
if (!evidence) {
|
|
167
|
+
throw new Error(`Evidence with id ${id} not found`);
|
|
168
|
+
}
|
|
169
|
+
// Parse existing tags (comma-separated) or create empty array
|
|
170
|
+
const existingTags = evidence.tags
|
|
171
|
+
? evidence.tags
|
|
172
|
+
.split(",")
|
|
173
|
+
.map((t) => t.trim())
|
|
174
|
+
.filter((t) => t)
|
|
175
|
+
: [];
|
|
176
|
+
// Merge tags (deduplicate) using validTags instead of raw tags
|
|
177
|
+
const mergedTags = [...new Set([...existingTags, ...validTags])];
|
|
178
|
+
// Update database with comma-separated format
|
|
179
|
+
const stmt = this.db.prepare(`
|
|
180
|
+
UPDATE evidences
|
|
181
|
+
SET tags = ?,
|
|
182
|
+
updatedAt = ?
|
|
183
|
+
WHERE id = ?
|
|
184
|
+
`);
|
|
185
|
+
stmt.run(mergedTags.join(","), new Date().toISOString(), id);
|
|
186
|
+
});
|
|
187
|
+
transaction();
|
|
178
188
|
}
|
|
179
189
|
catch (error) {
|
|
180
190
|
throw new Error(`Failed to add tags: ${error instanceof Error ? error.message : String(error)}`);
|
|
181
191
|
}
|
|
182
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Builds a WHERE clause from search/filter options
|
|
195
|
+
* @param options - Filter criteria
|
|
196
|
+
* @returns SQL WHERE clause string and parameter values
|
|
197
|
+
*/
|
|
198
|
+
buildWhereClause(options) {
|
|
199
|
+
const conditions = [];
|
|
200
|
+
const params = [];
|
|
201
|
+
if (options.query && options.query.trim()) {
|
|
202
|
+
conditions.push(`(conversationId LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')`);
|
|
203
|
+
const searchPattern = `%${escapeLikePattern(options.query.trim())}%`;
|
|
204
|
+
params.push(searchPattern, searchPattern);
|
|
205
|
+
}
|
|
206
|
+
if (options.tags && options.tags.length > 0) {
|
|
207
|
+
for (const tag of options.tags) {
|
|
208
|
+
conditions.push(`(tags LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\' OR tags = ?)`);
|
|
209
|
+
const trimmedTag = escapeLikePattern(tag.trim());
|
|
210
|
+
params.push(`${trimmedTag},%`, `%,${trimmedTag},%`, `%,${trimmedTag}`, tag.trim());
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (options.dateFrom) {
|
|
214
|
+
conditions.push(`timestamp >= ?`);
|
|
215
|
+
params.push(options.dateFrom);
|
|
216
|
+
}
|
|
217
|
+
if (options.dateTo) {
|
|
218
|
+
conditions.push(`timestamp <= ?`);
|
|
219
|
+
params.push(options.dateTo);
|
|
220
|
+
}
|
|
221
|
+
const sql = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
|
|
222
|
+
return { sql, params };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Gets count of evidences matching filter criteria
|
|
226
|
+
* @param options - Filter criteria (query, tags, date range)
|
|
227
|
+
* @returns Number of matching evidences
|
|
228
|
+
*/
|
|
229
|
+
getFilteredCount(options) {
|
|
230
|
+
try {
|
|
231
|
+
const { sql: whereClause, params } = this.buildWhereClause(options);
|
|
232
|
+
const row = this.db
|
|
233
|
+
.prepare(`SELECT COUNT(*) as count FROM evidences${whereClause}`)
|
|
234
|
+
.get(...params);
|
|
235
|
+
return row.count;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw new Error(`Failed to get filtered count: ${error instanceof Error ? error.message : String(error)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Search evidences and return both paginated results and total matching count
|
|
243
|
+
* in a single pass (builds WHERE clause once instead of twice)
|
|
244
|
+
*/
|
|
245
|
+
searchWithCount(options) {
|
|
246
|
+
try {
|
|
247
|
+
const { limit, offset = 0 } = options;
|
|
248
|
+
const { sql: whereClause, params: baseParams } = this.buildWhereClause(options);
|
|
249
|
+
// Wrap both queries in a transaction for consistent snapshot
|
|
250
|
+
const query = this.db.transaction(() => {
|
|
251
|
+
// Get total count with same WHERE clause
|
|
252
|
+
const countRow = this.db
|
|
253
|
+
.prepare(`SELECT COUNT(*) as count FROM evidences${whereClause}`)
|
|
254
|
+
.get(...baseParams);
|
|
255
|
+
// Build paginated query (clone params since we append to it)
|
|
256
|
+
const searchParams = [...baseParams];
|
|
257
|
+
let sql = `SELECT * FROM evidences${whereClause} ORDER BY timestamp DESC`;
|
|
258
|
+
if (limit !== undefined) {
|
|
259
|
+
sql += " LIMIT ?";
|
|
260
|
+
searchParams.push(limit);
|
|
261
|
+
if (offset > 0) {
|
|
262
|
+
sql += " OFFSET ?";
|
|
263
|
+
searchParams.push(offset);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (offset > 0) {
|
|
267
|
+
sql += " LIMIT -1 OFFSET ?";
|
|
268
|
+
searchParams.push(offset);
|
|
269
|
+
}
|
|
270
|
+
const rows = this.db.prepare(sql).all(...searchParams);
|
|
271
|
+
return {
|
|
272
|
+
evidences: rows.map((row) => this.rowToEvidence(row)),
|
|
273
|
+
total: countRow.count,
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
return query();
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
throw new Error(`Failed to search evidences: ${error instanceof Error ? error.message : String(error)}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
183
282
|
/**
|
|
184
283
|
* Search and filter evidences by various criteria
|
|
185
284
|
* @param options - Search and filter options
|
|
@@ -187,56 +286,21 @@ export class EvidenceDatabase {
|
|
|
187
286
|
*/
|
|
188
287
|
search(options) {
|
|
189
288
|
try {
|
|
190
|
-
const {
|
|
191
|
-
|
|
192
|
-
const conditions = [];
|
|
193
|
-
const params = [];
|
|
194
|
-
// Text search in conversationId and tags
|
|
195
|
-
if (query && query.trim()) {
|
|
196
|
-
conditions.push(`(conversationId LIKE ? OR tags LIKE ?)`);
|
|
197
|
-
const searchPattern = `%${query.trim()}%`;
|
|
198
|
-
params.push(searchPattern, searchPattern);
|
|
199
|
-
}
|
|
200
|
-
// Tag filtering (AND logic - all specified tags must be present)
|
|
201
|
-
// Tags are stored as comma-separated strings: "tag1,tag2,tag3"
|
|
202
|
-
if (tags && tags.length > 0) {
|
|
203
|
-
for (const tag of tags) {
|
|
204
|
-
// Match tag at start, middle, or end of comma-separated list
|
|
205
|
-
conditions.push(`(tags LIKE ? OR tags LIKE ? OR tags LIKE ? OR tags = ?)`);
|
|
206
|
-
const trimmedTag = tag.trim();
|
|
207
|
-
params.push(`${trimmedTag},%`, // tag at start: "tag1,..."
|
|
208
|
-
`%,${trimmedTag},%`, // tag in middle: "...,tag1,..."
|
|
209
|
-
`%,${trimmedTag}`, // tag at end: "...,tag1"
|
|
210
|
-
trimmedTag // exact match (single tag)
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// Date range filtering
|
|
215
|
-
if (dateFrom) {
|
|
216
|
-
conditions.push(`timestamp >= ?`);
|
|
217
|
-
params.push(dateFrom);
|
|
218
|
-
}
|
|
219
|
-
if (dateTo) {
|
|
220
|
-
conditions.push(`timestamp <= ?`);
|
|
221
|
-
params.push(dateTo);
|
|
222
|
-
}
|
|
289
|
+
const { limit, offset = 0 } = options;
|
|
290
|
+
const { sql: whereClause, params } = this.buildWhereClause(options);
|
|
223
291
|
// Build final query
|
|
224
|
-
let sql =
|
|
225
|
-
if (conditions.length > 0) {
|
|
226
|
-
sql += ' WHERE ' + conditions.join(' AND ');
|
|
227
|
-
}
|
|
228
|
-
sql += ' ORDER BY timestamp DESC';
|
|
292
|
+
let sql = `SELECT * FROM evidences${whereClause} ORDER BY timestamp DESC`;
|
|
229
293
|
// Add pagination
|
|
230
294
|
if (limit !== undefined) {
|
|
231
|
-
sql +=
|
|
295
|
+
sql += " LIMIT ?";
|
|
232
296
|
params.push(limit);
|
|
233
297
|
if (offset > 0) {
|
|
234
|
-
sql +=
|
|
298
|
+
sql += " OFFSET ?";
|
|
235
299
|
params.push(offset);
|
|
236
300
|
}
|
|
237
301
|
}
|
|
238
302
|
else if (offset > 0) {
|
|
239
|
-
sql +=
|
|
303
|
+
sql += " LIMIT -1 OFFSET ?";
|
|
240
304
|
params.push(offset);
|
|
241
305
|
}
|
|
242
306
|
const stmt = this.db.prepare(sql);
|
|
@@ -271,10 +335,17 @@ export class EvidenceDatabase {
|
|
|
271
335
|
if (ids.length === 0)
|
|
272
336
|
return 0;
|
|
273
337
|
try {
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
338
|
+
// Batch deletions to stay under SQLite's 999 parameter limit
|
|
339
|
+
const BATCH_SIZE = 999;
|
|
340
|
+
let totalDeleted = 0;
|
|
341
|
+
for (let i = 0; i < ids.length; i += BATCH_SIZE) {
|
|
342
|
+
const batch = ids.slice(i, i + BATCH_SIZE);
|
|
343
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
344
|
+
const stmt = this.db.prepare(`DELETE FROM evidences WHERE id IN (${placeholders})`);
|
|
345
|
+
const result = stmt.run(...batch);
|
|
346
|
+
totalDeleted += result.changes;
|
|
347
|
+
}
|
|
348
|
+
return totalDeleted;
|
|
278
349
|
}
|
|
279
350
|
catch (error) {
|
|
280
351
|
throw new Error(`Failed to delete evidences: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -318,9 +389,9 @@ export class EvidenceDatabase {
|
|
|
318
389
|
for (const evidence of evidences) {
|
|
319
390
|
if (!evidence.tags)
|
|
320
391
|
continue;
|
|
321
|
-
const tags = evidence.tags.split(
|
|
322
|
-
const newTags = tags.map(t => t === oldTag ? newTag : t);
|
|
323
|
-
if (this.updateTags(evidence.id, newTags.join(
|
|
392
|
+
const tags = evidence.tags.split(",").map((t) => t.trim());
|
|
393
|
+
const newTags = tags.map((t) => (t === oldTag ? newTag : t));
|
|
394
|
+
if (this.updateTags(evidence.id, newTags.join(","))) {
|
|
324
395
|
updatedCount++;
|
|
325
396
|
}
|
|
326
397
|
}
|
|
@@ -348,8 +419,11 @@ export class EvidenceDatabase {
|
|
|
348
419
|
for (const evidence of evidences) {
|
|
349
420
|
if (!evidence.tags)
|
|
350
421
|
continue;
|
|
351
|
-
const tags = evidence.tags
|
|
352
|
-
|
|
422
|
+
const tags = evidence.tags
|
|
423
|
+
.split(",")
|
|
424
|
+
.map((t) => t.trim())
|
|
425
|
+
.filter((t) => t !== tag);
|
|
426
|
+
const newTags = tags.length > 0 ? tags.join(",") : null;
|
|
353
427
|
if (this.updateTags(evidence.id, newTags)) {
|
|
354
428
|
updatedCount++;
|
|
355
429
|
}
|
|
@@ -372,7 +446,10 @@ export class EvidenceDatabase {
|
|
|
372
446
|
const rows = stmt.all();
|
|
373
447
|
const tagCounts = new Map();
|
|
374
448
|
for (const row of rows) {
|
|
375
|
-
const tags = row.tags
|
|
449
|
+
const tags = row.tags
|
|
450
|
+
.split(",")
|
|
451
|
+
.map((t) => t.trim())
|
|
452
|
+
.filter((t) => t);
|
|
376
453
|
for (const tag of tags) {
|
|
377
454
|
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
378
455
|
}
|
|
@@ -383,6 +460,24 @@ export class EvidenceDatabase {
|
|
|
383
460
|
throw new Error(`Failed to get tag counts: ${error instanceof Error ? error.message : String(error)}`);
|
|
384
461
|
}
|
|
385
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Gets total count of all evidence records
|
|
465
|
+
* @returns Total number of evidences in the database
|
|
466
|
+
*/
|
|
467
|
+
getTotalCount() {
|
|
468
|
+
const row = this.db
|
|
469
|
+
.prepare("SELECT COUNT(*) as count FROM evidences")
|
|
470
|
+
.get();
|
|
471
|
+
return row.count;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Gets count of evidence records matching a search query
|
|
475
|
+
* @param query - Search text to match against conversationId and tags
|
|
476
|
+
* @returns Number of matching evidences
|
|
477
|
+
*/
|
|
478
|
+
getSearchCount(query) {
|
|
479
|
+
return this.getFilteredCount({ query });
|
|
480
|
+
}
|
|
386
481
|
/**
|
|
387
482
|
* Get the underlying database instance
|
|
388
483
|
* Used for salt storage and other low-level operations
|
|
@@ -419,7 +514,7 @@ export class EvidenceDatabase {
|
|
|
419
514
|
gitTimestamp: row.gitTimestamp,
|
|
420
515
|
tags: row.tags,
|
|
421
516
|
createdAt: row.createdAt,
|
|
422
|
-
updatedAt: row.updatedAt
|
|
517
|
+
updatedAt: row.updatedAt,
|
|
423
518
|
};
|
|
424
519
|
}
|
|
425
520
|
}
|