@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.
Files changed (184) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +125 -161
  3. package/SKILL.md +50 -50
  4. package/dist/src/analyzers/content-analyzer.d.ts.map +1 -1
  5. package/dist/src/analyzers/content-analyzer.js +20 -4
  6. package/dist/src/analyzers/content-analyzer.js.map +1 -1
  7. package/dist/src/cli/constants.d.ts +20 -0
  8. package/dist/src/cli/constants.d.ts.map +1 -0
  9. package/dist/src/cli/constants.js +25 -0
  10. package/dist/src/cli/constants.js.map +1 -0
  11. package/dist/src/cli/index.d.ts +3 -0
  12. package/dist/src/cli/index.d.ts.map +1 -0
  13. package/dist/src/cli/index.js +25 -0
  14. package/dist/src/cli/index.js.map +1 -0
  15. package/dist/src/cli/setup.d.ts +6 -0
  16. package/dist/src/cli/setup.d.ts.map +1 -0
  17. package/dist/src/cli/setup.js +356 -0
  18. package/dist/src/cli/setup.js.map +1 -0
  19. package/dist/src/cli/types.d.ts +38 -0
  20. package/dist/src/cli/types.d.ts.map +1 -0
  21. package/dist/src/cli/types.js +5 -0
  22. package/dist/src/cli/types.js.map +1 -0
  23. package/dist/src/cli/utils/config.d.ts +19 -0
  24. package/dist/src/cli/utils/config.d.ts.map +1 -0
  25. package/dist/src/cli/utils/config.js +86 -0
  26. package/dist/src/cli/utils/config.js.map +1 -0
  27. package/dist/src/cli/utils/detect.d.ts +14 -0
  28. package/dist/src/cli/utils/detect.d.ts.map +1 -0
  29. package/dist/src/cli/utils/detect.js +57 -0
  30. package/dist/src/cli/utils/detect.js.map +1 -0
  31. package/dist/src/cli/utils/env.d.ts +15 -0
  32. package/dist/src/cli/utils/env.d.ts.map +1 -0
  33. package/dist/src/cli/utils/env.js +85 -0
  34. package/dist/src/cli/utils/env.js.map +1 -0
  35. package/dist/src/cli/utils/validation.d.ts +17 -0
  36. package/dist/src/cli/utils/validation.d.ts.map +1 -0
  37. package/dist/src/cli/utils/validation.js +77 -0
  38. package/dist/src/cli/utils/validation.js.map +1 -0
  39. package/dist/src/index.d.ts +1 -1
  40. package/dist/src/index.d.ts.map +1 -1
  41. package/dist/src/index.js +53 -38
  42. package/dist/src/index.js.map +1 -1
  43. package/dist/src/lib/crypto/decrypt.d.ts.map +1 -1
  44. package/dist/src/lib/crypto/decrypt.js +12 -8
  45. package/dist/src/lib/crypto/decrypt.js.map +1 -1
  46. package/dist/src/lib/crypto/encrypt.d.ts.map +1 -1
  47. package/dist/src/lib/crypto/encrypt.js +6 -3
  48. package/dist/src/lib/crypto/encrypt.js.map +1 -1
  49. package/dist/src/lib/crypto/key-derivation.d.ts +1 -1
  50. package/dist/src/lib/crypto/key-derivation.d.ts.map +1 -1
  51. package/dist/src/lib/crypto/key-derivation.js +11 -11
  52. package/dist/src/lib/crypto/key-derivation.js.map +1 -1
  53. package/dist/src/lib/storage/database.d.ts +46 -3
  54. package/dist/src/lib/storage/database.d.ts.map +1 -1
  55. package/dist/src/lib/storage/database.js +175 -80
  56. package/dist/src/lib/storage/database.js.map +1 -1
  57. package/dist/src/lib/storage/export.d.ts +3 -4
  58. package/dist/src/lib/storage/export.d.ts.map +1 -1
  59. package/dist/src/lib/storage/export.js +75 -62
  60. package/dist/src/lib/storage/export.js.map +1 -1
  61. package/dist/src/lib/storage/salt-storage.d.ts +1 -1
  62. package/dist/src/lib/storage/salt-storage.d.ts.map +1 -1
  63. package/dist/src/lib/storage/salt-storage.js +26 -18
  64. package/dist/src/lib/storage/salt-storage.js.map +1 -1
  65. package/dist/src/lib/storage/schema.d.ts +1 -1
  66. package/dist/src/lib/storage/schema.d.ts.map +1 -1
  67. package/dist/src/lib/storage/schema.js +29 -47
  68. package/dist/src/lib/storage/schema.js.map +1 -1
  69. package/dist/src/lib/tool-wrapper.d.ts.map +1 -1
  70. package/dist/src/lib/tool-wrapper.js +2 -2
  71. package/dist/src/lib/tool-wrapper.js.map +1 -1
  72. package/dist/src/prompts/skill-prompt.d.ts +6 -0
  73. package/dist/src/prompts/skill-prompt.d.ts.map +1 -0
  74. package/dist/src/prompts/skill-prompt.js +125 -0
  75. package/dist/src/prompts/skill-prompt.js.map +1 -0
  76. package/dist/src/tools/capture-footprint.d.ts +2 -2
  77. package/dist/src/tools/capture-footprint.d.ts.map +1 -1
  78. package/dist/src/tools/capture-footprint.js +53 -12
  79. package/dist/src/tools/capture-footprint.js.map +1 -1
  80. package/dist/src/tools/delete-footprints.d.ts +19 -2
  81. package/dist/src/tools/delete-footprints.d.ts.map +1 -1
  82. package/dist/src/tools/delete-footprints.js +56 -8
  83. package/dist/src/tools/delete-footprints.js.map +1 -1
  84. package/dist/src/tools/export-footprints.d.ts +14 -6
  85. package/dist/src/tools/export-footprints.d.ts.map +1 -1
  86. package/dist/src/tools/export-footprints.js +54 -15
  87. package/dist/src/tools/export-footprints.js.map +1 -1
  88. package/dist/src/tools/get-footprint.d.ts +1 -7
  89. package/dist/src/tools/get-footprint.d.ts.map +1 -1
  90. package/dist/src/tools/get-footprint.js +26 -22
  91. package/dist/src/tools/get-footprint.js.map +1 -1
  92. package/dist/src/tools/index.d.ts +1 -3
  93. package/dist/src/tools/index.d.ts.map +1 -1
  94. package/dist/src/tools/index.js +1 -3
  95. package/dist/src/tools/index.js.map +1 -1
  96. package/dist/src/tools/list-footprints.d.ts +3 -17
  97. package/dist/src/tools/list-footprints.d.ts.map +1 -1
  98. package/dist/src/tools/list-footprints.js +27 -16
  99. package/dist/src/tools/list-footprints.js.map +1 -1
  100. package/dist/src/tools/manage-tags.d.ts +47 -0
  101. package/dist/src/tools/manage-tags.d.ts.map +1 -0
  102. package/dist/src/tools/manage-tags.js +109 -0
  103. package/dist/src/tools/manage-tags.js.map +1 -0
  104. package/dist/src/tools/search-footprints.d.ts +4 -18
  105. package/dist/src/tools/search-footprints.d.ts.map +1 -1
  106. package/dist/src/tools/search-footprints.js +32 -16
  107. package/dist/src/tools/search-footprints.js.map +1 -1
  108. package/dist/src/tools/suggest-capture.d.ts +1 -1
  109. package/dist/src/tools/suggest-capture.d.ts.map +1 -1
  110. package/dist/src/tools/suggest-capture.js +6 -2
  111. package/dist/src/tools/suggest-capture.js.map +1 -1
  112. package/dist/src/tools/verify-footprint.d.ts +7 -54
  113. package/dist/src/tools/verify-footprint.d.ts.map +1 -1
  114. package/dist/src/tools/verify-footprint.js +22 -19
  115. package/dist/src/tools/verify-footprint.js.map +1 -1
  116. package/dist/src/types.d.ts +4 -4
  117. package/dist/src/types.d.ts.map +1 -1
  118. package/dist/src/ui/register.js +3 -3
  119. package/dist/src/ui/register.js.map +1 -1
  120. package/dist/ui/dashboard.html +78 -65
  121. package/dist/ui/detail.html +69 -56
  122. package/dist/ui/export.html +72 -59
  123. package/package.json +28 -16
  124. package/dist/src/lib/errors/base-error.d.ts +0 -15
  125. package/dist/src/lib/errors/base-error.d.ts.map +0 -1
  126. package/dist/src/lib/errors/base-error.js +0 -34
  127. package/dist/src/lib/errors/base-error.js.map +0 -1
  128. package/dist/src/lib/errors/crypto-error.d.ts +0 -29
  129. package/dist/src/lib/errors/crypto-error.d.ts.map +0 -1
  130. package/dist/src/lib/errors/crypto-error.js +0 -43
  131. package/dist/src/lib/errors/crypto-error.js.map +0 -1
  132. package/dist/src/lib/errors/index.d.ts +0 -26
  133. package/dist/src/lib/errors/index.d.ts.map +0 -1
  134. package/dist/src/lib/errors/index.js +0 -26
  135. package/dist/src/lib/errors/index.js.map +0 -1
  136. package/dist/src/lib/errors/storage-error.d.ts +0 -25
  137. package/dist/src/lib/errors/storage-error.d.ts.map +0 -1
  138. package/dist/src/lib/errors/storage-error.js +0 -38
  139. package/dist/src/lib/errors/storage-error.js.map +0 -1
  140. package/dist/src/lib/errors/validation-error.d.ts +0 -21
  141. package/dist/src/lib/errors/validation-error.d.ts.map +0 -1
  142. package/dist/src/lib/errors/validation-error.js +0 -29
  143. package/dist/src/lib/errors/validation-error.js.map +0 -1
  144. package/dist/src/test-helpers.d.ts +0 -33
  145. package/dist/src/test-helpers.d.ts.map +0 -1
  146. package/dist/src/test-helpers.js +0 -108
  147. package/dist/src/test-helpers.js.map +0 -1
  148. package/dist/src/tools/get-tag-stats.d.ts +0 -30
  149. package/dist/src/tools/get-tag-stats.d.ts.map +0 -1
  150. package/dist/src/tools/get-tag-stats.js +0 -33
  151. package/dist/src/tools/get-tag-stats.js.map +0 -1
  152. package/dist/src/tools/remove-tag.d.ts +0 -22
  153. package/dist/src/tools/remove-tag.d.ts.map +0 -1
  154. package/dist/src/tools/remove-tag.js +0 -30
  155. package/dist/src/tools/remove-tag.js.map +0 -1
  156. package/dist/src/tools/rename-tag.d.ts +0 -24
  157. package/dist/src/tools/rename-tag.d.ts.map +0 -1
  158. package/dist/src/tools/rename-tag.js +0 -34
  159. package/dist/src/tools/rename-tag.js.map +0 -1
  160. package/dist/tests/error-handling.test.d.ts +0 -2
  161. package/dist/tests/error-handling.test.d.ts.map +0 -1
  162. package/dist/tests/error-handling.test.js +0 -114
  163. package/dist/tests/error-handling.test.js.map +0 -1
  164. package/dist/tests/fixtures.d.ts +0 -87
  165. package/dist/tests/fixtures.d.ts.map +0 -1
  166. package/dist/tests/fixtures.js +0 -130
  167. package/dist/tests/fixtures.js.map +0 -1
  168. package/dist/tests/integration.test.d.ts +0 -2
  169. package/dist/tests/integration.test.d.ts.map +0 -1
  170. package/dist/tests/integration.test.js +0 -115
  171. package/dist/tests/integration.test.js.map +0 -1
  172. package/dist/tests/resources.test.d.ts +0 -2
  173. package/dist/tests/resources.test.d.ts.map +0 -1
  174. package/dist/tests/resources.test.js +0 -73
  175. package/dist/tests/resources.test.js.map +0 -1
  176. package/dist/tests/setup.d.ts +0 -8
  177. package/dist/tests/setup.d.ts.map +0 -1
  178. package/dist/tests/setup.js +0 -8
  179. package/dist/tests/setup.js.map +0 -1
  180. package/dist/tests/tools.test.d.ts +0 -2
  181. package/dist/tests/tools.test.d.ts.map +0 -1
  182. package/dist/tests/tools.test.js +0 -224
  183. package/dist/tests/tools.test.js.map +0 -1
  184. package/dist/ui-tmp/ui/export.html +0 -409
@@ -1,4 +1,5 @@
1
- import { xchacha20poly1305 } from '@noble/ciphers/chacha';
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('Key must be 32 bytes');
14
+ throw new Error("Key must be 32 bytes");
14
15
  }
15
16
  if (nonce.length !== 24) {
16
- throw new Error('Nonce must be 24 bytes');
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
- return new TextDecoder().decode(plaintextBytes);
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
- throw new Error('Decryption failed: invalid key or tampered data');
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,uBAAuB,CAAC;AAE1D;;;;;;;;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,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAClD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,+EAA+E;QAC/E,MAAM,aAAa,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC7E,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,aAAa,CAAC,CAAC;QAE7D,6DAA6D;QAC7D,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;AACH,CAAC"}
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":"AAGA,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,CAkBzE"}
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
- import { xchacha20poly1305 } from '@noble/ciphers/chacha';
2
- import { randomBytes } from '@noble/hashes/utils';
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('Key must be 32 bytes');
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,uBAAuB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAOlD;;;;;;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,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/B,CAAC"}
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,4 +1,4 @@
1
- import type { DerivedKey, KeyDerivationParams } from './types.js';
1
+ import type { DerivedKey, KeyDerivationParams } from "./types.js";
2
2
  /**
3
3
  * Derive encryption key from password using Argon2id
4
4
  *
@@ -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,CAuBrB;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,CAwBrB;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,CAkClB"}
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 '@noble/hashes/argon2';
2
- import { randomBytes } from '@noble/hashes/utils';
3
- import { timingSafeEqual } from 'node:crypto';
4
- import { DEFAULT_KDF_PARAMS } from './types.js';
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('Password cannot be empty');
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('Password cannot be empty');
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('Salt must be 16 bytes');
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('Password cannot be empty');
79
+ throw new Error("Password cannot be empty");
80
80
  }
81
81
  if (salt.length !== 16) {
82
- throw new Error('Salt must be 16 bytes');
82
+ throw new Error("Salt must be 16 bytes");
83
83
  }
84
84
  if (expectedKey.length !== 32) {
85
- throw new Error('Expected key must be 32 bytes');
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(Buffer.from(derivedKey), Buffer.from(expectedKey));
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":"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"}
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 'better-sqlite3';
2
- import type { Evidence } from './types.js';
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, 'id' | 'createdAt' | 'updatedAt'>): string;
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":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAEtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAqB3C;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,EAAE,CAAoB;IAE9B;;;;OAIG;gBACS,MAAM,EAAE,MAAM;IAW1B;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GAAG,MAAM;IAmC1E;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAkBrC;;;;OAIG;IACH,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,QAAQ,EAAE;IAiBxD;;;;OAIG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,QAAQ,EAAE;IA4B/D;;;;;OAKG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAoB5E;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAwCzC;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,QAAQ,EAAE;IAqEd;;;;OAIG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAU3B;;;;OAIG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM;IAajC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IAepD;;;;;;OAMG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IA4BjD;;;;;OAKG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IA4B9B;;;OAGG;IACH,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAmBnC;;;;;OAKG;IACH,KAAK,IAAI,QAAQ,CAAC,QAAQ;IAI1B;;;OAGG;IACH,KAAK,IAAI,IAAI;IAIb;;;;;OAKG;IACH,OAAO,CAAC,aAAa;CAiBtB"}
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
- import Database from 'better-sqlite3';
2
- import { createSchema } from './schema.js';
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 = 'SELECT * FROM evidences ORDER BY timestamp DESC';
101
+ let query = "SELECT * FROM evidences ORDER BY timestamp DESC";
98
102
  const params = [];
99
103
  if (limit !== undefined) {
100
- query += ' LIMIT ?';
104
+ query += " LIMIT ?";
101
105
  params.push(limit);
102
106
  if (offset > 0) {
103
- query += ' OFFSET ?';
107
+ query += " OFFSET ?";
104
108
  params.push(offset);
105
109
  }
106
110
  }
107
111
  else if (offset > 0) {
108
- query += ' LIMIT -1 OFFSET ?';
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('Tags array cannot be empty');
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('All provided tags are empty or whitespace');
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
- // Parse existing tags (comma-separated) or create empty array
165
- const existingTags = evidence.tags
166
- ? evidence.tags.split(',').map(t => t.trim()).filter(t => t)
167
- : [];
168
- // Merge tags (deduplicate) using validTags instead of raw tags
169
- const mergedTags = [...new Set([...existingTags, ...validTags])];
170
- // Update database with comma-separated format
171
- const stmt = this.db.prepare(`
172
- UPDATE evidences
173
- SET tags = ?,
174
- updatedAt = ?
175
- WHERE id = ?
176
- `);
177
- stmt.run(mergedTags.join(','), new Date().toISOString(), id);
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 { query, tags, dateFrom, dateTo, limit, offset = 0 } = options;
191
- // Build WHERE conditions
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 = 'SELECT * FROM evidences';
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 += ' LIMIT ?';
295
+ sql += " LIMIT ?";
232
296
  params.push(limit);
233
297
  if (offset > 0) {
234
- sql += ' OFFSET ?';
298
+ sql += " OFFSET ?";
235
299
  params.push(offset);
236
300
  }
237
301
  }
238
302
  else if (offset > 0) {
239
- sql += ' LIMIT -1 OFFSET ?';
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
- const placeholders = ids.map(() => '?').join(',');
275
- const stmt = this.db.prepare(`DELETE FROM evidences WHERE id IN (${placeholders})`);
276
- const result = stmt.run(...ids);
277
- return result.changes;
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(',').map(t => t.trim());
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.split(',').map(t => t.trim()).filter(t => t !== tag);
352
- const newTags = tags.length > 0 ? tags.join(',') : null;
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.split(',').map(t => t.trim()).filter(t => t);
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
  }