@hraness/kb 0.17.1

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 (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,75 @@
1
+ /** Incrementally collect a byte stream without retaining one object per input chunk. */
2
+ export class BoundedByteBuffer {
3
+ readonly #maxBytes: number;
4
+ #storage = new Uint8Array();
5
+ #length = 0;
6
+
7
+ constructor(maxBytes: number) {
8
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
9
+ throw new RangeError("byte buffer limit must be a non-negative safe integer");
10
+ }
11
+ this.#maxBytes = maxBytes;
12
+ }
13
+
14
+ get byteLength(): number {
15
+ return this.#length;
16
+ }
17
+
18
+ /** Copy one chunk into owned geometric storage. False leaves the buffer unchanged. */
19
+ append(chunk: Uint8Array): boolean {
20
+ if (chunk.byteLength > this.#maxBytes - this.#length) return false;
21
+ if (chunk.byteLength === 0) return true;
22
+ const required = this.#length + chunk.byteLength;
23
+ if (required > this.#storage.byteLength) this.#grow(required);
24
+ this.#storage.set(chunk, this.#length);
25
+ this.#length = required;
26
+ return true;
27
+ }
28
+
29
+ /** Return an exact-length owned byte array. */
30
+ toUint8Array(): Uint8Array {
31
+ if (this.#length === this.#storage.byteLength) return this.#storage;
32
+ return this.#storage.slice(0, this.#length);
33
+ }
34
+
35
+ #grow(required: number): void {
36
+ let capacity = this.#storage.byteLength;
37
+ if (capacity === 0) capacity = Math.min(this.#maxBytes, Math.max(1_024, required));
38
+ while (capacity < required) {
39
+ capacity = capacity <= Math.floor(this.#maxBytes / 2)
40
+ ? capacity * 2
41
+ : this.#maxBytes;
42
+ }
43
+ const grown = new Uint8Array(capacity);
44
+ grown.set(this.#storage.subarray(0, this.#length));
45
+ this.#storage = grown;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Consume a byte stream into one bounded owned array.
51
+ *
52
+ * Releasing the reader lock is the caller-visible cleanup boundary. The
53
+ * underlying stream is not cancelled so its owner can decide how to terminate
54
+ * the producer after an overflow or read failure.
55
+ */
56
+ export async function readBoundedByteStream(
57
+ stream: ReadableStream<Uint8Array>,
58
+ maxBytes: number,
59
+ overflowLabel = "stream",
60
+ ): Promise<Uint8Array> {
61
+ const bytes = new BoundedByteBuffer(maxBytes);
62
+ const reader = stream.getReader();
63
+ try {
64
+ for (;;) {
65
+ const result = await reader.read();
66
+ if (result.done) break;
67
+ if (!bytes.append(result.value)) {
68
+ throw new Error(`${overflowLabel} exceeded ${maxBytes} bytes`);
69
+ }
70
+ }
71
+ } finally {
72
+ reader.releaseLock();
73
+ }
74
+ return bytes.toUint8Array();
75
+ }
@@ -0,0 +1,372 @@
1
+ import {
2
+ chmodSync,
3
+ closeSync,
4
+ constants,
5
+ existsSync,
6
+ fstatSync,
7
+ fsyncSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ openSync,
11
+ readdirSync,
12
+ readSync,
13
+ realpathSync,
14
+ rmSync,
15
+ type Stats,
16
+ unlinkSync,
17
+ writeSync,
18
+ } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
21
+
22
+ const profileSkipNames = new Set([
23
+ "Cache",
24
+ "Code Cache",
25
+ "GPUCache",
26
+ "GrShaderCache",
27
+ "GraphiteDawnCache",
28
+ "ShaderCache",
29
+ "DawnGraphiteCache",
30
+ "DawnWebGPUCache",
31
+ "Crashpad",
32
+ ]);
33
+
34
+ const maxProfileEntries = 100_000;
35
+ const maxProfileBytes = 5 * 1024 * 1024 * 1024;
36
+ const maxLocalStateBytes = 16 * 1024 * 1024;
37
+
38
+ export class BrowserProfileSnapshotError extends Error {
39
+ constructor(message: string) {
40
+ super(message);
41
+ this.name = "BrowserProfileSnapshotError";
42
+ }
43
+ }
44
+
45
+ export function isSafeNamedProfile(value: string): boolean {
46
+ if (
47
+ value.length < 1
48
+ || value.length > 256
49
+ || value.trim() !== value
50
+ || value.startsWith("-")
51
+ || value === "."
52
+ || value === ".."
53
+ ) return false;
54
+ for (let index = 0; index < value.length; index += 1) {
55
+ const code = value.charCodeAt(index);
56
+ if (
57
+ code <= 0x1f
58
+ || (code >= 0x7f && code <= 0x9f)
59
+ || code === 0x061c
60
+ || code === 0x200e
61
+ || code === 0x200f
62
+ || (code >= 0x2028 && code <= 0x202e)
63
+ || (code >= 0x2066 && code <= 0x2069)
64
+ ) return false;
65
+ }
66
+ return !value.includes("/") && !value.includes("\\");
67
+ }
68
+
69
+ /** Resolve a path-backed profile without treating a browser profile name as a path. */
70
+ export function profilePath(value: string): string | null {
71
+ const pathLike = isAbsolute(value)
72
+ || value.startsWith(`.${sep}`)
73
+ || value.startsWith(`..${sep}`)
74
+ || value.startsWith(`~${sep}`)
75
+ || value.includes("/")
76
+ || value.includes("\\");
77
+ if (!pathLike && !isSafeNamedProfile(value)) throw new Error("browser profile name is unsafe");
78
+ if (!pathLike) return null;
79
+ try {
80
+ const expanded = value.startsWith(`~${sep}`) ? join(homedir(), value.slice(2)) : resolve(value);
81
+ const entry = lstatSync(expanded);
82
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
83
+ throw new BrowserProfileSnapshotError("browser profile path must be a real directory");
84
+ }
85
+ return realpathSync(expanded);
86
+ } catch (error) {
87
+ if (error instanceof BrowserProfileSnapshotError) throw error;
88
+ throw new BrowserProfileSnapshotError("browser profile path is unavailable or unsafe");
89
+ }
90
+ }
91
+
92
+ type CopyBudget = { entries: number; bytes: number };
93
+
94
+ function sameEntry(left: Stats, right: Stats): boolean {
95
+ return left.dev === right.dev && left.ino === right.ino;
96
+ }
97
+
98
+ function copyProfileFile(
99
+ sourcePath: string,
100
+ destinationPath: string,
101
+ observed: Stats,
102
+ budget: CopyBudget,
103
+ ): void {
104
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
105
+ const nonBlock = "O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0;
106
+ let input: number | null = null;
107
+ let output: number | null = null;
108
+ let completed = false;
109
+ try {
110
+ input = openSync(sourcePath, constants.O_RDONLY | noFollow | nonBlock);
111
+ const opened = fstatSync(input);
112
+ if (!opened.isFile() || !sameEntry(observed, opened)) {
113
+ throw new BrowserProfileSnapshotError("browser profile entry changed before it could be copied");
114
+ }
115
+ if (opened.size > maxProfileBytes - budget.bytes) {
116
+ throw new BrowserProfileSnapshotError("browser profile clone exceeded its safety bounds");
117
+ }
118
+ output = openSync(
119
+ destinationPath,
120
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow,
121
+ 0o600,
122
+ );
123
+ const buffer = Buffer.allocUnsafe(64 * 1024);
124
+ let copied = 0;
125
+ for (;;) {
126
+ const count = readSync(input, buffer, 0, buffer.byteLength, null);
127
+ if (count === 0) break;
128
+ if (count > maxProfileBytes - budget.bytes - copied) {
129
+ throw new BrowserProfileSnapshotError("browser profile clone exceeded its safety bounds");
130
+ }
131
+ let offset = 0;
132
+ while (offset < count) {
133
+ const written = writeSync(output, buffer, offset, count - offset);
134
+ if (written < 1) throw new BrowserProfileSnapshotError("browser profile file copy made no progress");
135
+ offset += written;
136
+ }
137
+ copied += count;
138
+ }
139
+ const after = fstatSync(input);
140
+ if (
141
+ !sameEntry(observed, after)
142
+ || after.size !== observed.size
143
+ || copied !== observed.size
144
+ || after.mtimeMs !== observed.mtimeMs
145
+ ) throw new BrowserProfileSnapshotError("browser profile entry changed while it was copied");
146
+ fsyncSync(output);
147
+ budget.bytes += copied;
148
+ completed = true;
149
+ } catch (error) {
150
+ if (error instanceof BrowserProfileSnapshotError) throw error;
151
+ throw new BrowserProfileSnapshotError("browser profile file could not be copied");
152
+ } finally {
153
+ if (output !== null) closeSync(output);
154
+ if (input !== null) closeSync(input);
155
+ if (!completed) {
156
+ try {
157
+ unlinkSync(destinationPath);
158
+ } catch {
159
+ // The owned destination tree is removed by cloneProfile on failure.
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ function copyProfileEntry(
166
+ sourcePath: string,
167
+ destinationPath: string,
168
+ budget: CopyBudget,
169
+ root = false,
170
+ ): void {
171
+ const name = basename(sourcePath);
172
+ if (
173
+ !root
174
+ && (name.startsWith("Singleton") || name === "DevToolsActivePort" || profileSkipNames.has(name))
175
+ ) return;
176
+ const observed = lstatSync(sourcePath);
177
+ if (observed.isSymbolicLink()) {
178
+ if (root) throw new BrowserProfileSnapshotError("browser profile path must be a real directory");
179
+ return;
180
+ }
181
+ if (!observed.isFile() && !observed.isDirectory()) {
182
+ throw new BrowserProfileSnapshotError("browser profile contains an unsupported nonregular entry");
183
+ }
184
+ if (root && !observed.isDirectory()) {
185
+ throw new BrowserProfileSnapshotError("browser profile path must be a real directory");
186
+ }
187
+ budget.entries += 1;
188
+ if (budget.entries > maxProfileEntries) {
189
+ throw new BrowserProfileSnapshotError("browser profile clone exceeded its safety bounds");
190
+ }
191
+ if (observed.isFile()) {
192
+ copyProfileFile(sourcePath, destinationPath, observed, budget);
193
+ return;
194
+ }
195
+ mkdirSync(destinationPath, { mode: 0o700 });
196
+ chmodSync(destinationPath, 0o700);
197
+ for (const child of readdirSync(sourcePath).sort()) {
198
+ copyProfileEntry(join(sourcePath, child), join(destinationPath, child), budget);
199
+ }
200
+ const after = lstatSync(sourcePath);
201
+ if (
202
+ !after.isDirectory()
203
+ || after.isSymbolicLink()
204
+ || !sameEntry(observed, after)
205
+ || after.size !== observed.size
206
+ || after.mtimeMs !== observed.mtimeMs
207
+ || after.ctimeMs !== observed.ctimeMs
208
+ ) throw new BrowserProfileSnapshotError("browser profile directory changed while it was copied");
209
+ }
210
+
211
+ /** Copy a profile into a new private directory without caches, lock files, or links. */
212
+ export function cloneProfile(source: string, destination: string): void {
213
+ if (existsSync(destination)) throw new BrowserProfileSnapshotError("browser profile destination already exists");
214
+ try {
215
+ copyProfileEntry(source, destination, { entries: 0, bytes: 0 }, true);
216
+ } catch (error) {
217
+ rmSync(destination, { recursive: true, force: true });
218
+ if (error instanceof BrowserProfileSnapshotError) throw error;
219
+ throw new BrowserProfileSnapshotError("browser profile snapshot failed");
220
+ }
221
+ }
222
+
223
+ function missingFile(error: unknown): boolean {
224
+ return typeof error === "object"
225
+ && error !== null
226
+ && "code" in error
227
+ && (error as { readonly code?: unknown }).code === "ENOENT";
228
+ }
229
+
230
+ function localStateEntry(path: string): Stats | null {
231
+ try {
232
+ return lstatSync(path);
233
+ } catch (error) {
234
+ if (missingFile(error)) return null;
235
+ throw new BrowserProfileSnapshotError("Chromium Local State is unavailable or unsafe");
236
+ }
237
+ }
238
+
239
+ function assertChromiumUserDataIdle(userDataRoot: string): void {
240
+ let names: string[];
241
+ try {
242
+ names = readdirSync(userDataRoot);
243
+ } catch {
244
+ throw new BrowserProfileSnapshotError("Chromium user-data root could not be inspected");
245
+ }
246
+ if (names.some((name) => name.startsWith("Singleton") || name === "DevToolsActivePort")) {
247
+ throw new BrowserProfileSnapshotError(
248
+ "Chromium profile is active or retains a stale process lock; fully quit the browser and retry",
249
+ );
250
+ }
251
+ }
252
+
253
+ /** Copy Chromium encryption metadata without following links or exceeding the byte bound. */
254
+ export function copyBoundedLocalState(source: string, destination: string): void {
255
+ const entry = localStateEntry(source);
256
+ if (entry === null || entry.isSymbolicLink() || !entry.isFile()) {
257
+ throw new BrowserProfileSnapshotError("Chromium Local State must be one regular file");
258
+ }
259
+ if (entry.size > maxLocalStateBytes) {
260
+ throw new BrowserProfileSnapshotError("Chromium Local State exceeds its safety bound");
261
+ }
262
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
263
+ let input: number | null = null;
264
+ let output: number | null = null;
265
+ let outputCreated = false;
266
+ let completed = false;
267
+ try {
268
+ input = openSync(source, constants.O_RDONLY | noFollow);
269
+ const opened = fstatSync(input);
270
+ if (
271
+ !opened.isFile()
272
+ || !sameEntry(entry, opened)
273
+ || opened.size !== entry.size
274
+ || opened.size > maxLocalStateBytes
275
+ ) throw new BrowserProfileSnapshotError("Chromium Local State changed before it could be copied");
276
+ output = openSync(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
277
+ outputCreated = true;
278
+ const buffer = Buffer.allocUnsafe(64 * 1024);
279
+ let copied = 0;
280
+ for (;;) {
281
+ const count = readSync(input, buffer, 0, Math.min(buffer.length, maxLocalStateBytes + 1 - copied), null);
282
+ if (count === 0) break;
283
+ copied += count;
284
+ if (copied > maxLocalStateBytes) {
285
+ throw new BrowserProfileSnapshotError("Chromium Local State exceeds its safety bound");
286
+ }
287
+ let offset = 0;
288
+ while (offset < count) {
289
+ const written = writeSync(output, buffer, offset, count - offset);
290
+ if (written < 1) throw new BrowserProfileSnapshotError("Chromium Local State copy made no progress");
291
+ offset += written;
292
+ }
293
+ }
294
+ const after = fstatSync(input);
295
+ if (
296
+ copied !== entry.size
297
+ || !sameEntry(entry, after)
298
+ || after.size !== entry.size
299
+ || after.mtimeMs !== entry.mtimeMs
300
+ ) throw new BrowserProfileSnapshotError("Chromium Local State changed while it was copied");
301
+ fsyncSync(output);
302
+ completed = true;
303
+ } catch (error) {
304
+ if (error instanceof BrowserProfileSnapshotError) throw error;
305
+ throw new BrowserProfileSnapshotError("Chromium Local State could not be copied");
306
+ } finally {
307
+ if (output !== null) closeSync(output);
308
+ if (input !== null) closeSync(input);
309
+ if (outputCreated && !completed) {
310
+ try {
311
+ unlinkSync(destination);
312
+ } catch {
313
+ // The caller removes the containing private snapshot on failure.
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ export type ClonedBrowserProfile = {
320
+ readonly userDataPath: string;
321
+ readonly profileDirectory?: "Default";
322
+ };
323
+
324
+ /** Clone one selected profile while retaining Chromium's local encryption metadata. */
325
+ export function cloneBrowserProfile(source: string, privateDirectory: string): ClonedBrowserProfile {
326
+ let selectedSource = source;
327
+ let userDataRoot = dirname(source);
328
+ let localState = join(userDataRoot, "Local State");
329
+ let stateEntry = localStateEntry(localState);
330
+ if (stateEntry === null) {
331
+ const rootLocalState = join(source, "Local State");
332
+ const rootStateEntry = localStateEntry(rootLocalState);
333
+ if (rootStateEntry === null) {
334
+ const destination = join(privateDirectory, "profile");
335
+ cloneProfile(source, destination);
336
+ return { userDataPath: destination };
337
+ }
338
+ const defaultProfile = join(source, "Default");
339
+ let defaultEntry: Stats;
340
+ try {
341
+ defaultEntry = lstatSync(defaultProfile);
342
+ } catch {
343
+ throw new BrowserProfileSnapshotError("Chromium Default profile is unavailable or unsafe");
344
+ }
345
+ if (defaultEntry.isSymbolicLink() || !defaultEntry.isDirectory()) {
346
+ throw new BrowserProfileSnapshotError("Chromium Default profile must be one real directory");
347
+ }
348
+ selectedSource = defaultProfile;
349
+ userDataRoot = source;
350
+ localState = rootLocalState;
351
+ stateEntry = rootStateEntry;
352
+ }
353
+ if (stateEntry.isSymbolicLink() || !stateEntry.isFile()) {
354
+ throw new BrowserProfileSnapshotError("Chromium Local State must be one regular file");
355
+ }
356
+ if (stateEntry.size > maxLocalStateBytes) {
357
+ throw new BrowserProfileSnapshotError("Chromium Local State exceeds its safety bound");
358
+ }
359
+ assertChromiumUserDataIdle(userDataRoot);
360
+ const userData = join(privateDirectory, "profile-user-data");
361
+ mkdirSync(userData, { mode: 0o700 });
362
+ chmodSync(userData, 0o700);
363
+ const selectedProfile = join(userData, "Default");
364
+ try {
365
+ cloneProfile(selectedSource, selectedProfile);
366
+ copyBoundedLocalState(localState, join(userData, "Local State"));
367
+ return { userDataPath: userData, profileDirectory: "Default" };
368
+ } catch (error) {
369
+ rmSync(userData, { recursive: true, force: true });
370
+ throw error;
371
+ }
372
+ }