@oscharko-dev/keiko-local-knowledge 0.2.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 (290) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/bounded-document-extraction.d.ts +27 -0
  3. package/dist/bounded-document-extraction.d.ts.map +1 -0
  4. package/dist/bounded-document-extraction.js +214 -0
  5. package/dist/capsule-lifecycle.d.ts +33 -0
  6. package/dist/capsule-lifecycle.d.ts.map +1 -0
  7. package/dist/capsule-lifecycle.js +292 -0
  8. package/dist/capsule-set-lifecycle.d.ts +15 -0
  9. package/dist/capsule-set-lifecycle.d.ts.map +1 -0
  10. package/dist/capsule-set-lifecycle.js +158 -0
  11. package/dist/chunking/chunker-persist.d.ts +36 -0
  12. package/dist/chunking/chunker-persist.d.ts.map +1 -0
  13. package/dist/chunking/chunker-persist.js +74 -0
  14. package/dist/chunking/chunker-runner.d.ts +9 -0
  15. package/dist/chunking/chunker-runner.d.ts.map +1 -0
  16. package/dist/chunking/chunker-runner.js +218 -0
  17. package/dist/chunking/chunker.d.ts +7 -0
  18. package/dist/chunking/chunker.d.ts.map +1 -0
  19. package/dist/chunking/chunker.js +139 -0
  20. package/dist/chunking/citation-mapper.d.ts +4 -0
  21. package/dist/chunking/citation-mapper.d.ts.map +1 -0
  22. package/dist/chunking/citation-mapper.js +180 -0
  23. package/dist/chunking/index.d.ts +6 -0
  24. package/dist/chunking/index.d.ts.map +1 -0
  25. package/dist/chunking/index.js +8 -0
  26. package/dist/chunking/token-estimator.d.ts +3 -0
  27. package/dist/chunking/token-estimator.d.ts.map +1 -0
  28. package/dist/chunking/token-estimator.js +26 -0
  29. package/dist/chunking/types.d.ts +49 -0
  30. package/dist/chunking/types.d.ts.map +1 -0
  31. package/dist/chunking/types.js +26 -0
  32. package/dist/composition.d.ts +57 -0
  33. package/dist/composition.d.ts.map +1 -0
  34. package/dist/composition.js +310 -0
  35. package/dist/conversation/citation-attacher.d.ts +8 -0
  36. package/dist/conversation/citation-attacher.d.ts.map +1 -0
  37. package/dist/conversation/citation-attacher.js +55 -0
  38. package/dist/conversation/citation-excerpts.d.ts +4 -0
  39. package/dist/conversation/citation-excerpts.d.ts.map +1 -0
  40. package/dist/conversation/citation-excerpts.js +41 -0
  41. package/dist/conversation/grounded-answer-runner.d.ts +9 -0
  42. package/dist/conversation/grounded-answer-runner.d.ts.map +1 -0
  43. package/dist/conversation/grounded-answer-runner.js +61 -0
  44. package/dist/conversation/index.d.ts +5 -0
  45. package/dist/conversation/index.d.ts.map +1 -0
  46. package/dist/conversation/index.js +7 -0
  47. package/dist/conversation/model-gateway-answer-generator.d.ts +28 -0
  48. package/dist/conversation/model-gateway-answer-generator.d.ts.map +1 -0
  49. package/dist/conversation/model-gateway-answer-generator.js +105 -0
  50. package/dist/conversation/types.d.ts +35 -0
  51. package/dist/conversation/types.d.ts.map +1 -0
  52. package/dist/conversation/types.js +24 -0
  53. package/dist/discovery/discovery-runner.d.ts +23 -0
  54. package/dist/discovery/discovery-runner.d.ts.map +1 -0
  55. package/dist/discovery/discovery-runner.js +109 -0
  56. package/dist/discovery/extract-progressive.d.ts +17 -0
  57. package/dist/discovery/extract-progressive.d.ts.map +1 -0
  58. package/dist/discovery/extract-progressive.js +522 -0
  59. package/dist/discovery/extract.d.ts +26 -0
  60. package/dist/discovery/extract.d.ts.map +1 -0
  61. package/dist/discovery/extract.js +906 -0
  62. package/dist/discovery/glob.d.ts +10 -0
  63. package/dist/discovery/glob.d.ts.map +1 -0
  64. package/dist/discovery/glob.js +72 -0
  65. package/dist/discovery/index.d.ts +6 -0
  66. package/dist/discovery/index.d.ts.map +1 -0
  67. package/dist/discovery/index.js +8 -0
  68. package/dist/discovery/media-type.d.ts +4 -0
  69. package/dist/discovery/media-type.d.ts.map +1 -0
  70. package/dist/discovery/media-type.js +62 -0
  71. package/dist/discovery/persist.d.ts +63 -0
  72. package/dist/discovery/persist.d.ts.map +1 -0
  73. package/dist/discovery/persist.js +345 -0
  74. package/dist/discovery/test-support.d.ts +16 -0
  75. package/dist/discovery/test-support.d.ts.map +1 -0
  76. package/dist/discovery/test-support.js +127 -0
  77. package/dist/discovery/types.d.ts +63 -0
  78. package/dist/discovery/types.d.ts.map +1 -0
  79. package/dist/discovery/types.js +28 -0
  80. package/dist/discovery/walk.d.ts +12 -0
  81. package/dist/discovery/walk.d.ts.map +1 -0
  82. package/dist/discovery/walk.js +302 -0
  83. package/dist/errors.d.ts +13 -0
  84. package/dist/errors.d.ts.map +1 -0
  85. package/dist/errors.js +22 -0
  86. package/dist/evaluations/dimensions.d.ts +14 -0
  87. package/dist/evaluations/dimensions.d.ts.map +1 -0
  88. package/dist/evaluations/dimensions.js +191 -0
  89. package/dist/evaluations/fixtures.d.ts +18 -0
  90. package/dist/evaluations/fixtures.d.ts.map +1 -0
  91. package/dist/evaluations/fixtures.js +858 -0
  92. package/dist/evaluations/index.d.ts +7 -0
  93. package/dist/evaluations/index.d.ts.map +1 -0
  94. package/dist/evaluations/index.js +10 -0
  95. package/dist/evaluations/report.d.ts +3 -0
  96. package/dist/evaluations/report.d.ts.map +1 -0
  97. package/dist/evaluations/report.js +31 -0
  98. package/dist/evaluations/runner-seed.d.ts +12 -0
  99. package/dist/evaluations/runner-seed.d.ts.map +1 -0
  100. package/dist/evaluations/runner-seed.js +175 -0
  101. package/dist/evaluations/runner.d.ts +8 -0
  102. package/dist/evaluations/runner.d.ts.map +1 -0
  103. package/dist/evaluations/runner.js +205 -0
  104. package/dist/evaluations/scripted-embedding-adapter.d.ts +13 -0
  105. package/dist/evaluations/scripted-embedding-adapter.d.ts.map +1 -0
  106. package/dist/evaluations/scripted-embedding-adapter.js +163 -0
  107. package/dist/evaluations/types.d.ts +116 -0
  108. package/dist/evaluations/types.d.ts.map +1 -0
  109. package/dist/evaluations/types.js +27 -0
  110. package/dist/index.d.ts +23 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +41 -0
  113. package/dist/indexing/bounded-indexing.d.ts +41 -0
  114. package/dist/indexing/bounded-indexing.d.ts.map +1 -0
  115. package/dist/indexing/bounded-indexing.js +240 -0
  116. package/dist/indexing/checkpoint-persist.d.ts +8 -0
  117. package/dist/indexing/checkpoint-persist.d.ts.map +1 -0
  118. package/dist/indexing/checkpoint-persist.js +135 -0
  119. package/dist/indexing/checkpoint-resume.d.ts +20 -0
  120. package/dist/indexing/checkpoint-resume.d.ts.map +1 -0
  121. package/dist/indexing/checkpoint-resume.js +50 -0
  122. package/dist/indexing/embedding-batcher.d.ts +3 -0
  123. package/dist/indexing/embedding-batcher.d.ts.map +1 -0
  124. package/dist/indexing/embedding-batcher.js +390 -0
  125. package/dist/indexing/index.d.ts +7 -0
  126. package/dist/indexing/index.d.ts.map +1 -0
  127. package/dist/indexing/index.js +11 -0
  128. package/dist/indexing/job-persist.d.ts +46 -0
  129. package/dist/indexing/job-persist.d.ts.map +1 -0
  130. package/dist/indexing/job-persist.js +157 -0
  131. package/dist/indexing/job-resume.d.ts +4 -0
  132. package/dist/indexing/job-resume.d.ts.map +1 -0
  133. package/dist/indexing/job-resume.js +14 -0
  134. package/dist/indexing/orchestrator.d.ts +3 -0
  135. package/dist/indexing/orchestrator.d.ts.map +1 -0
  136. package/dist/indexing/orchestrator.js +1151 -0
  137. package/dist/indexing/types.d.ts +156 -0
  138. package/dist/indexing/types.d.ts.map +1 -0
  139. package/dist/indexing/types.js +30 -0
  140. package/dist/indexing/vector-persist.d.ts +32 -0
  141. package/dist/indexing/vector-persist.d.ts.map +1 -0
  142. package/dist/indexing/vector-persist.js +105 -0
  143. package/dist/parsers/_internal.d.ts +20 -0
  144. package/dist/parsers/_internal.d.ts.map +1 -0
  145. package/dist/parsers/_internal.js +122 -0
  146. package/dist/parsers/csv-parser.d.ts +3 -0
  147. package/dist/parsers/csv-parser.d.ts.map +1 -0
  148. package/dist/parsers/csv-parser.js +202 -0
  149. package/dist/parsers/docx-parser.d.ts +3 -0
  150. package/dist/parsers/docx-parser.d.ts.map +1 -0
  151. package/dist/parsers/docx-parser.js +390 -0
  152. package/dist/parsers/html-parser.d.ts +3 -0
  153. package/dist/parsers/html-parser.d.ts.map +1 -0
  154. package/dist/parsers/html-parser.js +310 -0
  155. package/dist/parsers/index.d.ts +15 -0
  156. package/dist/parsers/index.d.ts.map +1 -0
  157. package/dist/parsers/index.js +41 -0
  158. package/dist/parsers/json-parser.d.ts +3 -0
  159. package/dist/parsers/json-parser.d.ts.map +1 -0
  160. package/dist/parsers/json-parser.js +192 -0
  161. package/dist/parsers/large-document/capability-discovery.d.ts +27 -0
  162. package/dist/parsers/large-document/capability-discovery.d.ts.map +1 -0
  163. package/dist/parsers/large-document/capability-discovery.js +76 -0
  164. package/dist/parsers/large-document/diagnostics.d.ts +3 -0
  165. package/dist/parsers/large-document/diagnostics.d.ts.map +1 -0
  166. package/dist/parsers/large-document/diagnostics.js +11 -0
  167. package/dist/parsers/large-document/index.d.ts +15 -0
  168. package/dist/parsers/large-document/index.d.ts.map +1 -0
  169. package/dist/parsers/large-document/index.js +10 -0
  170. package/dist/parsers/large-document/legacy-format.d.ts +5 -0
  171. package/dist/parsers/large-document/legacy-format.d.ts.map +1 -0
  172. package/dist/parsers/large-document/legacy-format.js +25 -0
  173. package/dist/parsers/large-document/preflight.d.ts +9 -0
  174. package/dist/parsers/large-document/preflight.d.ts.map +1 -0
  175. package/dist/parsers/large-document/preflight.js +43 -0
  176. package/dist/parsers/large-document/progressive-extraction.d.ts +55 -0
  177. package/dist/parsers/large-document/progressive-extraction.d.ts.map +1 -0
  178. package/dist/parsers/large-document/progressive-extraction.js +123 -0
  179. package/dist/parsers/large-document/progressive-pdf.d.ts +20 -0
  180. package/dist/parsers/large-document/progressive-pdf.d.ts.map +1 -0
  181. package/dist/parsers/large-document/progressive-pdf.js +145 -0
  182. package/dist/parsers/large-document/synthetic-source.d.ts +9 -0
  183. package/dist/parsers/large-document/synthetic-source.d.ts.map +1 -0
  184. package/dist/parsers/large-document/synthetic-source.js +101 -0
  185. package/dist/parsers/large-document/window-builder.d.ts +24 -0
  186. package/dist/parsers/large-document/window-builder.d.ts.map +1 -0
  187. package/dist/parsers/large-document/window-builder.js +75 -0
  188. package/dist/parsers/ocr/index.d.ts +4 -0
  189. package/dist/parsers/ocr/index.d.ts.map +1 -0
  190. package/dist/parsers/ocr/index.js +4 -0
  191. package/dist/parsers/ocr/null-ocr-adapter.d.ts +3 -0
  192. package/dist/parsers/ocr/null-ocr-adapter.d.ts.map +1 -0
  193. package/dist/parsers/ocr/null-ocr-adapter.js +14 -0
  194. package/dist/parsers/ocr/ocr-pipeline-parser.d.ts +8 -0
  195. package/dist/parsers/ocr/ocr-pipeline-parser.d.ts.map +1 -0
  196. package/dist/parsers/ocr/ocr-pipeline-parser.js +147 -0
  197. package/dist/parsers/ocr/types.d.ts +16 -0
  198. package/dist/parsers/ocr/types.d.ts.map +1 -0
  199. package/dist/parsers/ocr/types.js +4 -0
  200. package/dist/parsers/parser-test-fixtures.d.ts +28 -0
  201. package/dist/parsers/parser-test-fixtures.d.ts.map +1 -0
  202. package/dist/parsers/parser-test-fixtures.js +139 -0
  203. package/dist/parsers/pdf-parser.d.ts +43 -0
  204. package/dist/parsers/pdf-parser.d.ts.map +1 -0
  205. package/dist/parsers/pdf-parser.js +388 -0
  206. package/dist/parsers/registry.d.ts +8 -0
  207. package/dist/parsers/registry.d.ts.map +1 -0
  208. package/dist/parsers/registry.js +57 -0
  209. package/dist/parsers/text-parser.d.ts +3 -0
  210. package/dist/parsers/text-parser.d.ts.map +1 -0
  211. package/dist/parsers/text-parser.js +214 -0
  212. package/dist/parsers/types.d.ts +53 -0
  213. package/dist/parsers/types.d.ts.map +1 -0
  214. package/dist/parsers/types.js +21 -0
  215. package/dist/parsers/unsupported-parser.d.ts +4 -0
  216. package/dist/parsers/unsupported-parser.d.ts.map +1 -0
  217. package/dist/parsers/unsupported-parser.js +97 -0
  218. package/dist/parsers/xlsx-parser.d.ts +3 -0
  219. package/dist/parsers/xlsx-parser.d.ts.map +1 -0
  220. package/dist/parsers/xlsx-parser.js +425 -0
  221. package/dist/privacy/audit-emitter.d.ts +5 -0
  222. package/dist/privacy/audit-emitter.d.ts.map +1 -0
  223. package/dist/privacy/audit-emitter.js +93 -0
  224. package/dist/privacy/diagnostic-redactor.d.ts +2 -0
  225. package/dist/privacy/diagnostic-redactor.d.ts.map +1 -0
  226. package/dist/privacy/diagnostic-redactor.js +153 -0
  227. package/dist/privacy/index.d.ts +5 -0
  228. package/dist/privacy/index.d.ts.map +1 -0
  229. package/dist/privacy/index.js +6 -0
  230. package/dist/privacy/retention-applier.d.ts +5 -0
  231. package/dist/privacy/retention-applier.d.ts.map +1 -0
  232. package/dist/privacy/retention-applier.js +88 -0
  233. package/dist/privacy/types.d.ts +98 -0
  234. package/dist/privacy/types.d.ts.map +1 -0
  235. package/dist/privacy/types.js +12 -0
  236. package/dist/qualityIntelligence/capsuleCorpus.d.ts +27 -0
  237. package/dist/qualityIntelligence/capsuleCorpus.d.ts.map +1 -0
  238. package/dist/qualityIntelligence/capsuleCorpus.js +58 -0
  239. package/dist/qualityIntelligence/index.d.ts +3 -0
  240. package/dist/qualityIntelligence/index.d.ts.map +1 -0
  241. package/dist/qualityIntelligence/index.js +5 -0
  242. package/dist/qualityIntelligence/qiHandoff.d.ts +36 -0
  243. package/dist/qualityIntelligence/qiHandoff.d.ts.map +1 -0
  244. package/dist/qualityIntelligence/qiHandoff.js +82 -0
  245. package/dist/retrieval/answer-grounding.d.ts +9 -0
  246. package/dist/retrieval/answer-grounding.d.ts.map +1 -0
  247. package/dist/retrieval/answer-grounding.js +31 -0
  248. package/dist/retrieval/context-pack-assembler.d.ts +24 -0
  249. package/dist/retrieval/context-pack-assembler.d.ts.map +1 -0
  250. package/dist/retrieval/context-pack-assembler.js +50 -0
  251. package/dist/retrieval/index.d.ts +6 -0
  252. package/dist/retrieval/index.d.ts.map +1 -0
  253. package/dist/retrieval/index.js +9 -0
  254. package/dist/retrieval/retrieval-runner.d.ts +10 -0
  255. package/dist/retrieval/retrieval-runner.d.ts.map +1 -0
  256. package/dist/retrieval/retrieval-runner.js +163 -0
  257. package/dist/retrieval/scoped-vector-search.d.ts +24 -0
  258. package/dist/retrieval/scoped-vector-search.d.ts.map +1 -0
  259. package/dist/retrieval/scoped-vector-search.js +864 -0
  260. package/dist/retrieval/types.d.ts +28 -0
  261. package/dist/retrieval/types.d.ts.map +1 -0
  262. package/dist/retrieval/types.js +33 -0
  263. package/dist/section-path-hash.d.ts +3 -0
  264. package/dist/section-path-hash.d.ts.map +1 -0
  265. package/dist/section-path-hash.js +9 -0
  266. package/dist/source-lifecycle.d.ts +14 -0
  267. package/dist/source-lifecycle.d.ts.map +1 -0
  268. package/dist/source-lifecycle.js +155 -0
  269. package/dist/source-routing-validation.d.ts +11 -0
  270. package/dist/source-routing-validation.d.ts.map +1 -0
  271. package/dist/source-routing-validation.js +140 -0
  272. package/dist/store-content-cipher.d.ts +11 -0
  273. package/dist/store-content-cipher.d.ts.map +1 -0
  274. package/dist/store-content-cipher.js +67 -0
  275. package/dist/store-content-encryption.d.ts +12 -0
  276. package/dist/store-content-encryption.d.ts.map +1 -0
  277. package/dist/store-content-encryption.js +275 -0
  278. package/dist/store-paths.d.ts +6 -0
  279. package/dist/store-paths.d.ts.map +1 -0
  280. package/dist/store-paths.js +61 -0
  281. package/dist/store.d.ts +30 -0
  282. package/dist/store.d.ts.map +1 -0
  283. package/dist/store.js +219 -0
  284. package/dist/testing.d.ts +47 -0
  285. package/dist/testing.d.ts.map +1 -0
  286. package/dist/testing.js +170 -0
  287. package/dist/version.d.ts +2 -0
  288. package/dist/version.d.ts.map +1 -0
  289. package/dist/version.js +4 -0
  290. package/package.json +43 -0
@@ -0,0 +1,275 @@
1
+ // Content-encryption lifecycle for the Local Knowledge capsule store (Issue #1322, Epic #1319;
2
+ // ADR-0047 D4). Runs once at store-open, AFTER the schema migrations and BEFORE any content read.
3
+ //
4
+ // Case matrix (marker in schema_meta records that a store is encrypted):
5
+ //
6
+ // marker present, opened WITH a key provider → verify the sealed probe (fail-closed on a wrong
7
+ // key / tampered probe), then proceed.
8
+ // marker present, opened WITHOUT a key provider → throw: the store is encrypted, a key is required.
9
+ // marker absent, probe present, WITHOUT a key → throw: incomplete encrypted migration, a key is
10
+ // required.
11
+ // marker absent, probe present, WITH a key → verify the existing probe before any mutation,
12
+ // then finish the idempotent migration.
13
+ // marker absent, probe absent, opened WITHOUT a key → plaintext store opened plaintext: no-op.
14
+ // marker absent, opened WITH a key provider → forward migration: seal every plaintext content
15
+ // row, write the probe, set the marker, then flush
16
+ // plaintext from the WAL and freelist.
17
+ //
18
+ // The migration is crash-aware and idempotent: it verifies any existing probe before retrying, seals
19
+ // only rows that do not authenticate with the current key, and sets the marker only after plaintext
20
+ // has been checkpointed and vacuumed away.
21
+ import { KnowledgeStoreError } from "./errors.js";
22
+ import { sectionPathHashFromJson } from "./section-path-hash.js";
23
+ const ENCRYPTION_MARKER_KEY = "content_encryption";
24
+ const ENCRYPTION_MARKER_VALUE = "aes-256-gcm/v1";
25
+ const ENCRYPTION_PROBE_KEY = "content_encryption_probe";
26
+ const ENCRYPTION_SCOPE_KEY = "content_encryption_scope";
27
+ const ENCRYPTION_SCOPE_VALUE = "reconstructive-columns/v2";
28
+ // Fixed, non-secret sentinel. Sealed at migration time and re-opened on every encrypted open to prove
29
+ // the resolved key matches the one the store was sealed with. Never carries customer content.
30
+ const ENCRYPTION_PROBE_PLAINTEXT = "keiko-local-knowledge-content-encryption-v1";
31
+ const BYTES_PER_FLOAT32 = 4;
32
+ function readSchemaMeta(db, key) {
33
+ const row = db.prepare("SELECT value FROM schema_meta WHERE key = :k").get({ k: key });
34
+ return row?.value;
35
+ }
36
+ function writeSchemaMeta(db, key, value) {
37
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES (:k, :v)").run({
38
+ k: key,
39
+ v: value,
40
+ });
41
+ }
42
+ // Collects a table's rowids up front so the seal pass holds at most one row's content in memory at a
43
+ // time, and never UPDATEs the table while a SELECT cursor over it is still open (SQLite does not
44
+ // guarantee a consistent enumeration in that case). rowids are integers, so the collection is tiny
45
+ // relative to the content it gates — the Issue #1286 bounded-memory invariant holds during migration.
46
+ function collectRowIds(db, table) {
47
+ const ids = [];
48
+ for (const row of db.prepare(`SELECT rowid AS rowid FROM ${table}`).iterate()) {
49
+ ids.push(row.rowid);
50
+ }
51
+ return ids;
52
+ }
53
+ const LEGACY_TEXT_TARGETS = [
54
+ { table: "document_texts", column: "normalized_text" },
55
+ { table: "document_text_windows", column: "normalized_text" },
56
+ ];
57
+ const PATH_TEXT_TARGETS = [
58
+ { table: "sections", column: "section_path_json" },
59
+ { table: "parsed_units", column: "section_path_json" },
60
+ { table: "parsed_units", column: "heading_path_json" },
61
+ ];
62
+ const TEXT_TARGETS = [...LEGACY_TEXT_TARGETS, ...PATH_TEXT_TARGETS];
63
+ function isAlreadySealed(cipher, value) {
64
+ if (!cipher.isSealed(value))
65
+ return false;
66
+ try {
67
+ cipher.openText(value);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ function openMaybeSealedText(cipher, value) {
75
+ return cipher.isEncrypted && isAlreadySealed(cipher, value) ? cipher.openText(value) : value;
76
+ }
77
+ // Seals every TEXT content row that does not authenticate under the current key, one row at a time.
78
+ // This intentionally does NOT trust the "kv1." prefix alone: a legacy plaintext value can literally
79
+ // start with "kv1.", and must be sealed as plaintext instead of skipped.
80
+ function sealTextColumn(db, target, cipher) {
81
+ const select = db.prepare(`SELECT ${target.column} AS value FROM ${target.table} WHERE rowid = :id`);
82
+ const update = db.prepare(`UPDATE ${target.table} SET ${target.column} = :t WHERE rowid = :id`);
83
+ for (const id of collectRowIds(db, target.table)) {
84
+ const row = select.get({ id });
85
+ const value = row?.value;
86
+ if (value === undefined || value === null || isAlreadySealed(cipher, value))
87
+ continue;
88
+ update.run({ t: cipher.sealText(value), id });
89
+ }
90
+ }
91
+ function ensureSectionPathHashes(db, cipher) {
92
+ const select = db.prepare("SELECT section_path_json, section_path_hash FROM sections WHERE rowid = :id");
93
+ const update = db.prepare("UPDATE sections SET section_path_hash = :h WHERE rowid = :id");
94
+ for (const id of collectRowIds(db, "sections")) {
95
+ const row = select.get({ id });
96
+ if (row === undefined)
97
+ continue;
98
+ const canonicalJson = openMaybeSealedText(cipher, row.section_path_json);
99
+ const hash = sectionPathHashFromJson(canonicalJson);
100
+ if (row.section_path_hash !== hash)
101
+ update.run({ h: hash, id });
102
+ }
103
+ }
104
+ function sealTextColumns(db, cipher) {
105
+ for (const target of TEXT_TARGETS) {
106
+ sealTextColumn(db, target, cipher);
107
+ }
108
+ }
109
+ function sealPathTextColumns(db, cipher) {
110
+ for (const target of PATH_TEXT_TARGETS) {
111
+ sealTextColumn(db, target, cipher);
112
+ }
113
+ }
114
+ function assertTextColumnSealed(db, target, cipher) {
115
+ const select = db.prepare(`SELECT ${target.column} AS value FROM ${target.table} WHERE rowid = :id`);
116
+ for (const id of collectRowIds(db, target.table)) {
117
+ const row = select.get({ id });
118
+ const value = row?.value;
119
+ if (value === undefined || value === null)
120
+ continue;
121
+ if (isAlreadySealed(cipher, value))
122
+ continue;
123
+ throw new KnowledgeStoreError(`encrypted Local Knowledge store contains unsealed ${target.table}.${target.column}`);
124
+ }
125
+ }
126
+ function assertLegacyTextColumnsSealed(db, cipher) {
127
+ for (const target of LEGACY_TEXT_TARGETS) {
128
+ assertTextColumnSealed(db, target, cipher);
129
+ }
130
+ }
131
+ function assertVectorOpens(row, cipher) {
132
+ const plaintextByteLength = row.vector_dimensions * BYTES_PER_FLOAT32;
133
+ try {
134
+ cipher.openVector(row.embedding, plaintextByteLength);
135
+ }
136
+ catch (cause) {
137
+ throw new KnowledgeStoreError("encrypted Local Knowledge vector row is neither plaintext nor a valid sealed envelope", { cause });
138
+ }
139
+ }
140
+ function sealVectorColumn(db, cipher) {
141
+ const select = db.prepare("SELECT embedding, vector_dimensions FROM vectors WHERE rowid = :id");
142
+ const update = db.prepare("UPDATE vectors SET embedding = :e WHERE rowid = :id");
143
+ for (const id of collectRowIds(db, "vectors")) {
144
+ const row = select.get({ id });
145
+ if (row === undefined)
146
+ continue;
147
+ if (row.embedding.byteLength === row.vector_dimensions * BYTES_PER_FLOAT32) {
148
+ update.run({ e: cipher.sealVector(row.embedding), id });
149
+ continue;
150
+ }
151
+ assertVectorOpens(row, cipher);
152
+ }
153
+ }
154
+ function assertVectorColumnSealed(db, cipher) {
155
+ const select = db.prepare("SELECT embedding, vector_dimensions FROM vectors WHERE rowid = :id");
156
+ for (const id of collectRowIds(db, "vectors")) {
157
+ const row = select.get({ id });
158
+ if (row !== undefined)
159
+ assertVectorOpens(row, cipher);
160
+ }
161
+ }
162
+ function sealReconstructiveContent(db, cipher) {
163
+ ensureSectionPathHashes(db, cipher);
164
+ sealTextColumns(db, cipher);
165
+ sealVectorColumn(db, cipher);
166
+ }
167
+ function flushPlaintextResidue(db) {
168
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
169
+ db.exec("VACUUM");
170
+ }
171
+ function migrateToEncrypted(db, cipher) {
172
+ // Phase 1 (transactional): seal every content row and write the sealed key-verification probe. The
173
+ // completion MARKER is deliberately NOT written here — see phase 2.
174
+ db.exec("BEGIN");
175
+ try {
176
+ sealReconstructiveContent(db, cipher);
177
+ writeSchemaMeta(db, ENCRYPTION_PROBE_KEY, cipher.sealText(ENCRYPTION_PROBE_PLAINTEXT));
178
+ db.exec("COMMIT");
179
+ }
180
+ catch (cause) {
181
+ db.exec("ROLLBACK");
182
+ throw new KnowledgeStoreError("failed to migrate Local Knowledge store content to encrypted storage", { cause });
183
+ }
184
+ // Phase 2: after the in-place UPDATEs, plaintext can linger in the WAL and on freed pages. Truncate
185
+ // the WAL and VACUUM so the rewritten file holds no plaintext extracted text, path labels, or
186
+ // vector bytes (ADR-0047 D4). These run outside the transaction; VACUUM cannot run inside one.
187
+ // Phase 3: mark the store encrypted ONLY after the file has been rewritten free of plaintext. If
188
+ // phase 2 throws (I/O error, disk full), the marker stays unset, this open fails closed, and the
189
+ // next open re-runs the idempotent migration instead of skipping it over a WAL that still holds
190
+ // plaintext. The seal sweep is a no-op on the already-sealed rows, so the retry only re-checkpoints
191
+ // and re-VACUUMs.
192
+ flushPlaintextResidue(db);
193
+ writeSchemaMeta(db, ENCRYPTION_MARKER_KEY, ENCRYPTION_MARKER_VALUE);
194
+ writeSchemaMeta(db, ENCRYPTION_SCOPE_KEY, ENCRYPTION_SCOPE_VALUE);
195
+ }
196
+ function upgradeEncryptedScope(db, cipher) {
197
+ db.exec("BEGIN");
198
+ try {
199
+ ensureSectionPathHashes(db, cipher);
200
+ assertLegacyTextColumnsSealed(db, cipher);
201
+ assertVectorColumnSealed(db, cipher);
202
+ sealPathTextColumns(db, cipher);
203
+ db.exec("COMMIT");
204
+ }
205
+ catch (cause) {
206
+ db.exec("ROLLBACK");
207
+ throw new KnowledgeStoreError("failed to upgrade Local Knowledge encrypted-content coverage", {
208
+ cause,
209
+ });
210
+ }
211
+ flushPlaintextResidue(db);
212
+ writeSchemaMeta(db, ENCRYPTION_SCOPE_KEY, ENCRYPTION_SCOPE_VALUE);
213
+ }
214
+ function verifyProbe(db, cipher) {
215
+ const probe = readSchemaMeta(db, ENCRYPTION_PROBE_KEY);
216
+ if (probe === undefined || !cipher.isSealed(probe)) {
217
+ throw new KnowledgeStoreError("encrypted Local Knowledge store is missing or has a malformed key-verification probe " +
218
+ "(corrupt store or incomplete migration)");
219
+ }
220
+ let opened;
221
+ try {
222
+ opened = cipher.openText(probe);
223
+ }
224
+ catch (cause) {
225
+ throw new KnowledgeStoreError("cannot open encrypted Local Knowledge store: wrong key or tampered content", { cause });
226
+ }
227
+ if (opened !== ENCRYPTION_PROBE_PLAINTEXT) {
228
+ throw new KnowledgeStoreError("cannot open encrypted Local Knowledge store: key-verification probe mismatch");
229
+ }
230
+ }
231
+ // Reconciles the store's on-disk encryption state with the resolved cipher. Called once from
232
+ // openKnowledgeStore after migrations and before the handle is returned.
233
+ export function applyStoreContentEncryption(db, cipher) {
234
+ const marker = readSchemaMeta(db, ENCRYPTION_MARKER_KEY);
235
+ const probe = readSchemaMeta(db, ENCRYPTION_PROBE_KEY);
236
+ const scope = readSchemaMeta(db, ENCRYPTION_SCOPE_KEY);
237
+ if (marker !== undefined) {
238
+ if (marker !== ENCRYPTION_MARKER_VALUE) {
239
+ throw new KnowledgeStoreError(`unsupported Local Knowledge content encryption marker: ${marker}`);
240
+ }
241
+ if (!cipher.isEncrypted) {
242
+ throw new KnowledgeStoreError("this Local Knowledge store is encrypted; a key provider is required to open it");
243
+ }
244
+ verifyProbe(db, cipher);
245
+ if (scope !== undefined && scope !== ENCRYPTION_SCOPE_VALUE) {
246
+ throw new KnowledgeStoreError(`unsupported Local Knowledge content encryption scope marker: ${scope}`);
247
+ }
248
+ if (scope !== ENCRYPTION_SCOPE_VALUE) {
249
+ upgradeEncryptedScope(db, cipher);
250
+ }
251
+ return;
252
+ }
253
+ if (probe !== undefined) {
254
+ if (!cipher.isEncrypted) {
255
+ throw new KnowledgeStoreError("this Local Knowledge store has an incomplete encrypted-content migration; " +
256
+ "a key provider is required to finish opening it");
257
+ }
258
+ verifyProbe(db, cipher);
259
+ migrateToEncrypted(db, cipher);
260
+ return;
261
+ }
262
+ if (!cipher.isEncrypted) {
263
+ ensureSectionPathHashes(db, cipher);
264
+ return;
265
+ }
266
+ migrateToEncrypted(db, cipher);
267
+ }
268
+ export const STORE_CONTENT_ENCRYPTION_TEST_CONSTANTS = {
269
+ markerKey: ENCRYPTION_MARKER_KEY,
270
+ markerValue: ENCRYPTION_MARKER_VALUE,
271
+ probeKey: ENCRYPTION_PROBE_KEY,
272
+ probePlaintext: ENCRYPTION_PROBE_PLAINTEXT,
273
+ scopeKey: ENCRYPTION_SCOPE_KEY,
274
+ scopeValue: ENCRYPTION_SCOPE_VALUE,
275
+ };
@@ -0,0 +1,6 @@
1
+ export interface ResolveKnowledgeStorePathOptions {
2
+ readonly runtimeStateDir: string;
3
+ readonly namespace?: string;
4
+ }
5
+ export declare function resolveKnowledgeStorePath(opts: ResolveKnowledgeStorePathOptions): string;
6
+ //# sourceMappingURL=store-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-paths.d.ts","sourceRoot":"","sources":["../src/store-paths.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AA0CD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,gCAAgC,GAAG,MAAM,CAcxF"}
@@ -0,0 +1,61 @@
1
+ // Pure path arithmetic for the local-knowledge capsule store. Resolves the on-disk DB path
2
+ // from a runtime-state directory + optional namespace, with hard fail-closed rules:
3
+ // * runtimeStateDir must be non-empty.
4
+ // * namespace (when provided) must be non-empty, must not contain NUL, must not start with
5
+ // `~`, must not be absolute, must not contain `..` segments, and must reduce to a single
6
+ // path segment (no slashes in either direction).
7
+ // * The resolved path is asserted to live inside `runtimeStateDir` — caller code that
8
+ // trusts a runtime-state root for path containment must never receive a value outside it.
9
+ //
10
+ // This module performs NO filesystem access (no `node:fs` import). The runtime opener in
11
+ // store.ts owns directory creation and atomic operations.
12
+ import { isAbsolute, join, resolve, sep } from "node:path";
13
+ import { KnowledgePathError } from "./errors.js";
14
+ const DEFAULT_NAMESPACE = "default";
15
+ const DB_FILE_NAME = "capsules.db";
16
+ const SUBSYSTEM_DIR = "local-knowledge";
17
+ function rejectInvalidNamespace(namespace) {
18
+ if (namespace.length === 0) {
19
+ throw new KnowledgePathError("namespace must not be empty when provided.");
20
+ }
21
+ if (namespace.includes("\0")) {
22
+ throw new KnowledgePathError("namespace must not contain NUL bytes.");
23
+ }
24
+ if (namespace.startsWith("~")) {
25
+ throw new KnowledgePathError("namespace must not start with `~`; resolve home directories at the caller.");
26
+ }
27
+ if (isAbsolute(namespace)) {
28
+ throw new KnowledgePathError("namespace must not be an absolute path.");
29
+ }
30
+ if (namespace.includes("/") || namespace.includes("\\")) {
31
+ throw new KnowledgePathError("namespace must be a single path segment.");
32
+ }
33
+ if (namespace === "." || namespace === "..") {
34
+ throw new KnowledgePathError("namespace must not be `.` or `..`.");
35
+ }
36
+ }
37
+ function assertContained(resolvedPath, base) {
38
+ // resolve() normalises away `.`/`..` segments. If the result is not a strict descendant of
39
+ // the resolved base, the input escaped the root via some path the prior segment-level
40
+ // rejections missed and we MUST fail closed.
41
+ const normalisedBase = resolve(base);
42
+ const normalisedPath = resolve(resolvedPath);
43
+ if (normalisedPath !== normalisedBase && !normalisedPath.startsWith(normalisedBase + sep)) {
44
+ throw new KnowledgePathError("Resolved knowledge-store path escaped its runtimeStateDir; refusing to open.");
45
+ }
46
+ }
47
+ export function resolveKnowledgeStorePath(opts) {
48
+ if (opts.runtimeStateDir.length === 0) {
49
+ throw new KnowledgePathError("runtimeStateDir must not be empty.");
50
+ }
51
+ if (opts.runtimeStateDir.includes("\0")) {
52
+ throw new KnowledgePathError("runtimeStateDir must not contain NUL bytes.");
53
+ }
54
+ const namespace = opts.namespace ?? DEFAULT_NAMESPACE;
55
+ if (opts.namespace !== undefined) {
56
+ rejectInvalidNamespace(namespace);
57
+ }
58
+ const candidate = join(opts.runtimeStateDir, SUBSYSTEM_DIR, namespace, DB_FILE_NAME);
59
+ assertContained(candidate, opts.runtimeStateDir);
60
+ return candidate;
61
+ }
@@ -0,0 +1,30 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { type StoreContentCipher } from "./store-content-cipher.js";
3
+ export interface OpenKnowledgeStoreOptions {
4
+ readonly dbPath: string;
5
+ readonly clock?: () => number;
6
+ readonly protection?: KnowledgeStoreProtectionOptions;
7
+ }
8
+ export interface KnowledgeStoreKeyProviderContext {
9
+ readonly dbPath: string;
10
+ readonly schemaVersion: number;
11
+ }
12
+ export interface KnowledgeStoreKeyProvider {
13
+ readonly providerId: string;
14
+ readonly resolveKey: (context: KnowledgeStoreKeyProviderContext) => Uint8Array;
15
+ }
16
+ export interface KnowledgeStoreProtectionOptions {
17
+ readonly mode?: "plaintext-local-file-permissions" | "encrypted-key-provider";
18
+ readonly keyProvider?: KnowledgeStoreKeyProvider;
19
+ }
20
+ export interface KnowledgeStore {
21
+ readonly close: () => void;
22
+ readonly _internal: {
23
+ readonly db: DatabaseSync;
24
+ readonly now: () => number;
25
+ readonly contentCipher: StoreContentCipher;
26
+ };
27
+ }
28
+ export declare const LK_STORE_BUSY_TIMEOUT_MS = 5000;
29
+ export declare function openKnowledgeStore(opts: OpenKnowledgeStoreOptions): KnowledgeStore;
30
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,+BAA+B,CAAC;CACvD;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,gCAAgC,KAAK,UAAU,CAAC;CAChF;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,kCAAkC,GAAG,wBAAwB,CAAC;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,yBAAyB,CAAC;CAClD;AAMD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE;QAClB,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;QAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;QAI3B,QAAQ,CAAC,aAAa,EAAE,kBAAkB,CAAC;KAC5C,CAAC;CACH;AAkBD,eAAO,MAAM,wBAAwB,OAAQ,CAAC;AA8K9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,GAAG,cAAc,CAoClF"}
package/dist/store.js ADDED
@@ -0,0 +1,219 @@
1
+ // Capsule store runtime. Opens a node:sqlite database, sets durability/correctness pragmas
2
+ // (WAL journal + NORMAL synchronous + foreign_keys), applies the #265 DDL via the migration
3
+ // runner, and quarantines partial-write / wrong-schema files to `.corrupt.<iso>`.
4
+ //
5
+ // Foundry-IQ invariants enforced here:
6
+ // * PRAGMA foreign_keys=ON before any insert so capsule_id NOT NULL cascades and the
7
+ // composite (capsule_id, source/document/chunk) FKs cannot be silently bypassed.
8
+ // * Mirroring keiko-server's #62 pattern, a structurally corrupt file is renamed aside
9
+ // (preserving sidecars) rather than reformatted in place — operators recover the file
10
+ // by hand if it was actually data.
11
+ //
12
+ // Crash-safe write tradeoffs documented inline next to each PRAGMA call.
13
+ import { chmodSync, existsSync, mkdirSync, renameSync } from "node:fs";
14
+ import { dirname } from "node:path";
15
+ import { DatabaseSync } from "node:sqlite";
16
+ import { KNOWLEDGE_CAPSULE_MIGRATIONS, KNOWLEDGE_CAPSULE_TABLES, KNOWLEDGE_CAPSULE_V1_TABLES, } from "@oscharko-dev/keiko-contracts";
17
+ import { KnowledgeStoreError } from "./errors.js";
18
+ import { createEncryptedContentCipher, PLAINTEXT_CONTENT_CIPHER, } from "./store-content-cipher.js";
19
+ import { applyStoreContentEncryption } from "./store-content-encryption.js";
20
+ function defaultClock() {
21
+ return Date.now();
22
+ }
23
+ // Concurrent writers (indexing write + grounded-ask audit INSERT) must wait for the writer
24
+ // lock instead of failing immediately with SQLITE_BUSY → HTTP 500. 5_000ms matches the
25
+ // keiko-server UI DB constant (packages/keiko-server/src/store/db.ts, issue #639) and is
26
+ // the conservative default for the local single-writer desktop pattern.
27
+ export const LK_STORE_BUSY_TIMEOUT_MS = 5_000;
28
+ function applyDurabilityPragmas(db) {
29
+ // WAL: crash-safe single-writer; readers do not block the writer. Right tradeoff for
30
+ // the indexing+retrieval mix the local-knowledge layer will see.
31
+ db.exec("PRAGMA journal_mode = WAL");
32
+ // synchronous=NORMAL: fsyncs on commit boundaries but skips the fsync between WAL
33
+ // appends. Standard durability/latency choice for embedded apps where the user controls
34
+ // the host process; matches keiko-server's #62 store.
35
+ db.exec("PRAGMA synchronous = NORMAL");
36
+ db.exec("PRAGMA foreign_keys = ON");
37
+ db.exec(`PRAGMA busy_timeout = ${String(LK_STORE_BUSY_TIMEOUT_MS)}`);
38
+ }
39
+ // Resolves the store-boundary content cipher from the protection options. Plaintext (identity cipher)
40
+ // is the default: a store opened without a key provider behaves exactly as before. Encryption is
41
+ // requested by `encrypted-key-provider` mode or by supplying a keyProvider; the provider's 32-byte key
42
+ // (resolved for this dbPath + schema version) binds an AES-256-GCM cipher (ADR-0047 D1/D2).
43
+ function resolveContentCipher(opts, schemaVersion) {
44
+ const protection = opts.protection;
45
+ if (protection === undefined)
46
+ return PLAINTEXT_CONTENT_CIPHER;
47
+ const encryptionRequested = protection.mode === "encrypted-key-provider" || protection.keyProvider !== undefined;
48
+ if (!encryptionRequested)
49
+ return PLAINTEXT_CONTENT_CIPHER;
50
+ const provider = protection.keyProvider;
51
+ if (provider === undefined) {
52
+ throw new KnowledgeStoreError("encrypted-key-provider protection requires a keyProvider to resolve the store key");
53
+ }
54
+ const key = provider.resolveKey({ dbPath: opts.dbPath, schemaVersion });
55
+ return createEncryptedContentCipher(key);
56
+ }
57
+ function currentUserVersion(db) {
58
+ const row = db.prepare("PRAGMA user_version").get();
59
+ return typeof row?.user_version === "number" ? row.user_version : 0;
60
+ }
61
+ function setUserVersion(db, version) {
62
+ // user_version is not parameterisable. The value here is an integer constant from the
63
+ // contracts package's migration manifest, never caller input.
64
+ db.exec(`PRAGMA user_version = ${String(version)}`);
65
+ }
66
+ function runMigrations(db) {
67
+ const start = currentUserVersion(db);
68
+ const pending = KNOWLEDGE_CAPSULE_MIGRATIONS.filter((m) => m.version > start);
69
+ if (pending.length === 0)
70
+ return;
71
+ db.exec("BEGIN");
72
+ try {
73
+ for (const migration of pending) {
74
+ for (const statement of migration.up) {
75
+ db.exec(statement);
76
+ }
77
+ setUserVersion(db, migration.version);
78
+ }
79
+ db.exec("COMMIT");
80
+ }
81
+ catch (error) {
82
+ db.exec("ROLLBACK");
83
+ throw new KnowledgeStoreError(`Failed to apply knowledge-capsule migration (start=${String(start)})`, { cause: error });
84
+ }
85
+ }
86
+ function listExistingTables(db) {
87
+ const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all();
88
+ return rows.map((row) => row.name);
89
+ }
90
+ function hasAnyUserContent(db) {
91
+ // Non-empty sqlite_master OR non-zero user_version → the file is "in use" by something.
92
+ // Either means: don't silently overwrite; quarantine and start fresh.
93
+ const tables = listExistingTables(db);
94
+ const userTables = tables.filter((n) => !n.startsWith("sqlite_"));
95
+ if (userTables.length > 0)
96
+ return true;
97
+ return currentUserVersion(db) !== 0;
98
+ }
99
+ function expectedV1TablesPresent(db) {
100
+ // Only checks the v1 table set (pre-migration). Used before runMigrations so that a v1
101
+ // database with a valid pre-v2 schema is not quarantined as corrupt.
102
+ const present = new Set(listExistingTables(db));
103
+ for (const expected of KNOWLEDGE_CAPSULE_V1_TABLES) {
104
+ if (!present.has(expected))
105
+ return false;
106
+ }
107
+ return true;
108
+ }
109
+ function expectedTablesPresent(db) {
110
+ const present = new Set(listExistingTables(db));
111
+ for (const expected of KNOWLEDGE_CAPSULE_TABLES) {
112
+ if (!present.has(expected))
113
+ return false;
114
+ }
115
+ return true;
116
+ }
117
+ function quarantineFile(target) {
118
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
119
+ if (existsSync(target)) {
120
+ renameSync(target, `${target}.corrupt.${ts}`);
121
+ }
122
+ for (const sidecar of [`${target}-wal`, `${target}-shm`]) {
123
+ if (existsSync(sidecar)) {
124
+ renameSync(sidecar, `${sidecar}.corrupt.${ts}`);
125
+ }
126
+ }
127
+ }
128
+ function tryOpenAndMigrate(dbPath, onError) {
129
+ // Returns the opened, migrated handle on success; undefined when the file is unusable
130
+ // (open threw OR the post-migrate schema is missing expected tables OR the file held
131
+ // foreign content that we refuse to coexist with). Callers handle quarantine + retry.
132
+ let db;
133
+ try {
134
+ db = new DatabaseSync(dbPath);
135
+ }
136
+ catch {
137
+ // Cannot open at all (file missing permissions, OS error). Caller quarantines and retries.
138
+ return undefined;
139
+ }
140
+ try {
141
+ applyDurabilityPragmas(db);
142
+ if (hasAnyUserContent(db) && !expectedV1TablesPresent(db)) {
143
+ // Pre-existing foreign schema or a partial install missing even the v1 tables.
144
+ // Quarantine and retry. The v1 check is intentionally narrow: a v1 database that
145
+ // has not yet been migrated to v2 passes here, then runMigrations upgrades it.
146
+ db.close();
147
+ return undefined;
148
+ }
149
+ runMigrations(db);
150
+ if (!expectedTablesPresent(db)) {
151
+ db.close();
152
+ return undefined;
153
+ }
154
+ return db;
155
+ }
156
+ catch (cause) {
157
+ onError?.(cause);
158
+ try {
159
+ db.close();
160
+ }
161
+ catch {
162
+ // ignore close failure; outer caller will quarantine and retry
163
+ }
164
+ return undefined;
165
+ }
166
+ }
167
+ function ensureParentDir(dbPath) {
168
+ const dir = dirname(dbPath);
169
+ if (!existsSync(dir)) {
170
+ // 0o700: best-effort on POSIX; ignored on win32.
171
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
172
+ }
173
+ restrictPathPermissions(dir, 0o700);
174
+ }
175
+ function restrictPathPermissions(target, mode) {
176
+ if (process.platform === "win32" || !existsSync(target))
177
+ return;
178
+ chmodSync(target, mode);
179
+ }
180
+ function restrictStoreFilePermissions(dbPath) {
181
+ restrictPathPermissions(dbPath, 0o600);
182
+ restrictPathPermissions(`${dbPath}-wal`, 0o600);
183
+ restrictPathPermissions(`${dbPath}-shm`, 0o600);
184
+ }
185
+ export function openKnowledgeStore(opts) {
186
+ ensureParentDir(opts.dbPath);
187
+ let db = tryOpenAndMigrate(opts.dbPath);
188
+ let lastError;
189
+ if (db === undefined) {
190
+ quarantineFile(opts.dbPath);
191
+ db = tryOpenAndMigrate(opts.dbPath, (cause) => {
192
+ lastError = cause;
193
+ });
194
+ }
195
+ if (db === undefined) {
196
+ throw new KnowledgeStoreError(`Failed to open knowledge-capsule store at ${opts.dbPath} even after quarantine.`, lastError !== undefined ? { cause: lastError } : undefined);
197
+ }
198
+ // The cipher binds the key resolved for the migrated schema version. Reconcile the store's on-disk
199
+ // encryption state before the handle is usable: this seals a legacy plaintext store forward, or
200
+ // fails closed on a wrong key / missing provider for an already-encrypted store (ADR-0047 D4). A
201
+ // failure here closes the handle so a half-open store never leaks.
202
+ const contentCipher = resolveContentCipher(opts, currentUserVersion(db));
203
+ try {
204
+ applyStoreContentEncryption(db, contentCipher);
205
+ }
206
+ catch (cause) {
207
+ db.close();
208
+ throw cause;
209
+ }
210
+ restrictStoreFilePermissions(opts.dbPath);
211
+ const now = opts.clock ?? defaultClock;
212
+ const handle = db;
213
+ return {
214
+ close: () => {
215
+ handle.close();
216
+ },
217
+ _internal: { db: handle, now, contentCipher },
218
+ };
219
+ }
@@ -0,0 +1,47 @@
1
+ import type { ChunkId, DocumentId, EmbeddingModelIdentity, KnowledgeCapsuleId, KnowledgeSourceId, ParsedUnit } from "@oscharko-dev/keiko-contracts";
2
+ import type { OpenAIEmbeddingAdapter, OpenAIEmbeddingOutcome, OpenAIEmbeddingRequest } from "@oscharko-dev/keiko-model-gateway";
3
+ import type { ChunkingOptions } from "./chunking/types.js";
4
+ import type { KnowledgeStore } from "./store.js";
5
+ type ParsedUnitWithoutDocId = Omit<Extract<ParsedUnit, {
6
+ kind: "page";
7
+ }>, "documentId"> | Omit<Extract<ParsedUnit, {
8
+ kind: "section";
9
+ }>, "documentId"> | Omit<Extract<ParsedUnit, {
10
+ kind: "json-path";
11
+ }>, "documentId"> | Omit<Extract<ParsedUnit, {
12
+ kind: "csv-row";
13
+ }>, "documentId"> | Omit<Extract<ParsedUnit, {
14
+ kind: "html-block";
15
+ }>, "documentId"> | Omit<Extract<ParsedUnit, {
16
+ kind: "unsupported-media";
17
+ }>, "documentId">;
18
+ export interface SeedVectorsOptions {
19
+ readonly capsuleId?: string;
20
+ readonly displayName?: string;
21
+ readonly sourceId?: string;
22
+ readonly documentId?: string;
23
+ readonly text?: string;
24
+ readonly unit?: ParsedUnitWithoutDocId;
25
+ readonly identity?: EmbeddingModelIdentity;
26
+ readonly contentHash?: string;
27
+ readonly safeDisplayName?: string;
28
+ readonly chunkingOptions?: ChunkingOptions;
29
+ }
30
+ export interface SeededVectors {
31
+ readonly capsuleId: KnowledgeCapsuleId;
32
+ readonly sourceId: KnowledgeSourceId;
33
+ readonly documentId: DocumentId;
34
+ readonly chunkIds: readonly ChunkId[];
35
+ readonly vectorTexts: readonly string[];
36
+ }
37
+ export declare function deterministicVector(input: string, dimensions: number): Float32Array;
38
+ export interface ScriptedAdapterOptions {
39
+ readonly responder?: (request: OpenAIEmbeddingRequest) => OpenAIEmbeddingOutcome;
40
+ readonly identity?: EmbeddingModelIdentity;
41
+ readonly endpoint?: string;
42
+ readonly apiKey?: string;
43
+ }
44
+ export declare function scriptedAdapter(options?: ScriptedAdapterOptions): OpenAIEmbeddingAdapter;
45
+ export declare function seedCapsuleWithVectors(store: KnowledgeStore, options?: SeedVectorsOptions): Promise<SeededVectors>;
46
+ export {};
47
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,UAAU,EACV,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACX,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAM3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AASjD,KAAK,sBAAsB,GACvB,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,YAAY,CAAC,GACzD,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,EAAE,YAAY,CAAC,GAC5D,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,EAAE,YAAY,CAAC,GAC9D,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,EAAE,YAAY,CAAC,GAC5D,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,EAAE,YAAY,CAAC,GAC/D,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAAC,EAAE,YAAY,CAAC,CAAC;AAE3E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,sBAAsB,CAAC;IACvC,QAAQ,CAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;CAC5C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY,CAWnF;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,sBAAsB,CAAC;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,sBAAsB,CAkB5F;AAkLD,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,cAAc,EACrB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CAYxB"}