@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.
- package/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- 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
|
+
}
|