@hraness/ghostget 0.18.0 → 0.18.2

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 (76) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +7 -7
  3. package/dist/apple-photos-client.js +2 -1
  4. package/dist/beeper-client.js +2 -1
  5. package/dist/client.js +1 -0
  6. package/dist/imessage-direct-install-2gm3kge7.js +324 -0
  7. package/dist/index-01eeae9e.js +5 -0
  8. package/dist/index-2ymnp8xv.js +145 -0
  9. package/dist/index-en5hycxp.js +175 -0
  10. package/dist/{index-9wca02er.js → index-hfbygww8.js} +1 -1
  11. package/dist/index-n4szk3nw.js +50 -0
  12. package/dist/index-yq6maz71.js +1105 -0
  13. package/dist/index-z1w83f81.js +4 -0
  14. package/dist/index.js +4 -47
  15. package/dist/messaging-automation-api.js +21 -0
  16. package/dist/messaging-automation-j4274hvc.js +609 -0
  17. package/dist/messaging-native-install-8w1j36ah.js +16 -0
  18. package/dist/messaging.js +1 -0
  19. package/dist/omni-client.js +1 -0
  20. package/dist/whatsapp-automation-runtime-hgh1e9rm.js +845 -0
  21. package/dist/whatsapp-client.js +1 -0
  22. package/docs/control-panel.md +2 -2
  23. package/docs/imessage-direct-provider.md +31 -7
  24. package/docs/messaging-automation.md +103 -0
  25. package/package.json +22 -2
  26. package/skills/ghostget/references/control-panel.md +1 -1
  27. package/skills/ghostget/references/install.md +5 -5
  28. package/skills/ghostget/references/linkedin-adapter.md +209 -13
  29. package/skills/ghostget/references/platform-patterns.md +1 -1
  30. package/src/args.ts +18 -5
  31. package/src/assets/adapters/imessage/wrench-web-adapter.json +126 -0
  32. package/src/assets/adapters/linkedin/wrench-web-adapter.json +1 -1
  33. package/src/assets/adapters/whatsapp/wrench-web-adapter.json +144 -0
  34. package/src/assets/messaging-runtime/NOTICE.txt +2580 -0
  35. package/src/assets/messaging-runtime/imsg-darwin-arm64.gz +0 -0
  36. package/src/assets/messaging-runtime/phone-number-info.plist.gz +0 -0
  37. package/src/assets/messaging-runtime/phone-number-metadata.json.gz +0 -0
  38. package/src/assets/messaging-runtime/phone-number-privacy.plist.gz +0 -0
  39. package/src/assets/messaging-runtime/wacli-darwin-arm64.gz +0 -0
  40. package/src/beeper-client-types.ts +1 -1
  41. package/src/ghostget.ts +16 -4
  42. package/src/messaging-automation-api.ts +12 -0
  43. package/src/messaging-automation-descriptors.ts +29 -0
  44. package/src/messaging-automation-factory.ts +148 -0
  45. package/src/messaging-automation-server.ts +183 -0
  46. package/src/messaging-automation-types.ts +155 -0
  47. package/src/messaging-automation-validation.ts +110 -0
  48. package/src/messaging-automation.ts +356 -0
  49. package/src/plugins/imessage-direct/plugin.ts +12 -1
  50. package/src/plugins/imessage-direct/vendor/0003-feat-rpc-add-no-fetch-rich-cards.patch +276 -0
  51. package/src/plugins/imessage-direct/vendor/provenance.json +84 -10
  52. package/src/plugins/linkedin-web/plugin.ts +1 -1
  53. package/src/plugins/whatsapp-linked-device/plugin.ts +7 -2
  54. package/src/plugins/whatsapp-linked-device/vendor/0001-ghostget-private-messaging.patch +1093 -0
  55. package/src/plugins/whatsapp-linked-device/vendor/README.md +39 -0
  56. package/src/plugins/whatsapp-linked-device/vendor/provenance.json +45 -0
  57. package/src/provider-plugin.ts +14 -0
  58. package/src/providers/imessage-automation.ts +250 -0
  59. package/src/providers/imessage-direct-install.ts +7 -0
  60. package/src/providers/imessage-direct-runtime.ts +32 -2
  61. package/src/providers/imessage-direct.ts +13 -3
  62. package/src/providers/linkedin-contact-failure.ts +21 -0
  63. package/src/providers/linkedin-contact-platform.ts +8 -1
  64. package/src/providers/linkedin-contact-program.ts +34 -5
  65. package/src/providers/linkedin-web-contact.ts +1023 -51
  66. package/src/providers/linkedin-web-profile-browser.ts +659 -45
  67. package/src/providers/linkedin-web-runtime.ts +1 -0
  68. package/src/providers/linkedin-web.ts +46 -2
  69. package/src/providers/messaging-native-artifacts.ts +38 -0
  70. package/src/providers/messaging-native-install.ts +75 -0
  71. package/src/providers/whatsapp-automation-runtime.ts +240 -0
  72. package/src/providers/whatsapp-automation.ts +153 -0
  73. package/src/usage.ts +3 -1
  74. package/src/version.ts +1 -1
  75. package/src/web-session-contract-definitions.ts +1 -1
  76. package/tsconfig.json +3 -0
@@ -0,0 +1,1105 @@
1
+ // @bun
2
+ // src/storage.ts
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ constants,
7
+ existsSync,
8
+ fchmodSync,
9
+ fstatSync,
10
+ fsyncSync,
11
+ lstatSync,
12
+ mkdtempSync,
13
+ openSync,
14
+ opendirSync,
15
+ readSync,
16
+ realpathSync,
17
+ rmSync,
18
+ writeFileSync
19
+ } from "fs";
20
+ import { spawnSync } from "child_process";
21
+ import { homedir, tmpdir } from "os";
22
+ import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "path";
23
+ import { fileURLToPath } from "url";
24
+
25
+ // src/process-identity.ts
26
+ import { execFileSync } from "child_process";
27
+ import { createHash } from "crypto";
28
+ import { readFileSync } from "fs";
29
+ import { dlopen, ptr } from "bun:ffi";
30
+ var cachedBootId = null;
31
+ var cachedCurrentProcessStartIdentity = null;
32
+ var cachedDarwinProcPidInfo;
33
+ var DARWIN_PROCESS_STATE_PATTERN = /^[DIRSTUZ][+<>AELNSsVWX]*$/u;
34
+ var DARWIN_PROC_PID_T_BSDINFO_WITH_UNIQID = 18;
35
+ var DARWIN_PROC_BSDINFO_SIZE = 136;
36
+ var DARWIN_PROC_UNIQIDENTIFIERINFO_SIZE = 56;
37
+ var DARWIN_PROC_BSDINFO_WITH_UNIQID_SIZE = DARWIN_PROC_BSDINFO_SIZE + DARWIN_PROC_UNIQIDENTIFIERINFO_SIZE;
38
+ var DARWIN_PROC_BSDINFO_FLAGS_OFFSET = 0;
39
+ var DARWIN_PROC_BSDINFO_STATUS_OFFSET = 4;
40
+ var DARWIN_PROC_BSDINFO_PID_OFFSET = 12;
41
+ var DARWIN_PROC_BSDINFO_START_SECONDS_OFFSET = 120;
42
+ var DARWIN_PROC_BSDINFO_START_MICROSECONDS_OFFSET = 128;
43
+ var DARWIN_PROC_UNIQUE_ID_OFFSET = 16;
44
+ var DARWIN_PROC_ID_VERSION_OFFSET = 32;
45
+ var DARWIN_PROC_FLAG_INEXIT = 4;
46
+ var DARWIN_PROCESS_STATUS_IDLE = 1;
47
+ var DARWIN_PROCESS_STATUS_RUNNING = 2;
48
+ var DARWIN_PROCESS_STATUS_SLEEPING = 3;
49
+ var DARWIN_PROCESS_STATUS_STOPPED = 4;
50
+ var DARWIN_PROCESS_STATUS_ZOMBIE = 5;
51
+ var DARWIN_PROCESS_INSPECTION_ATTEMPTS = 3;
52
+ var UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/iu;
53
+ function identityDigest(kind, value) {
54
+ return createHash("sha256").update(kind, "utf8").update("\x00", "utf8").update(value, "utf8").digest("hex");
55
+ }
56
+ function commandText(command, arguments_) {
57
+ const environment = process.platform === "win32" ? {
58
+ NODE_ENV: "production",
59
+ SystemRoot: "C:\\Windows",
60
+ WINDIR: "C:\\Windows",
61
+ PATH: "C:\\Windows\\System32"
62
+ } : { LANG: "C", LC_ALL: "C", NODE_ENV: "production", TZ: "UTC" };
63
+ return execFileSync(command, arguments_, {
64
+ encoding: "utf8",
65
+ env: environment,
66
+ timeout: 2000,
67
+ windowsHide: true
68
+ }).trim();
69
+ }
70
+ function windowsPowerShellPath() {
71
+ return "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
72
+ }
73
+ function normalizeDarwinBootSessionUuid(raw) {
74
+ const value = raw.trim();
75
+ if (!UUID_PATTERN.test(value)) {
76
+ throw new Error("system boot identity is malformed");
77
+ }
78
+ return value.toLowerCase();
79
+ }
80
+ function darwinBootId(raw) {
81
+ return identityDigest("io-boot", normalizeDarwinBootSessionUuid(raw));
82
+ }
83
+ function currentBootId() {
84
+ if (cachedBootId !== null)
85
+ return cachedBootId;
86
+ let raw;
87
+ if (process.platform === "linux") {
88
+ raw = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
89
+ if (!/^[a-f0-9-]{36}$/iu.test(raw)) {
90
+ throw new Error("system boot identity is malformed");
91
+ }
92
+ } else if (process.platform === "darwin") {
93
+ cachedBootId = darwinBootId(commandText("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"]));
94
+ return cachedBootId;
95
+ } else if (process.platform === "win32") {
96
+ raw = commandText(windowsPowerShellPath(), [
97
+ "-NoProfile",
98
+ "-NonInteractive",
99
+ "-Command",
100
+ "(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().Ticks"
101
+ ]);
102
+ if (!/^\d{10,30}$/u.test(raw)) {
103
+ throw new Error("system boot identity is malformed");
104
+ }
105
+ } else {
106
+ throw new Error(`system boot identity is unsupported on ${process.platform}`);
107
+ }
108
+ cachedBootId = identityDigest("io-boot", raw);
109
+ return cachedBootId;
110
+ }
111
+ function processIsDefinitelyMissing(pid) {
112
+ try {
113
+ process.kill(pid, 0);
114
+ return false;
115
+ } catch (error) {
116
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
117
+ }
118
+ }
119
+ function inspectLinuxProcessRecord(raw) {
120
+ const close = raw.lastIndexOf(")");
121
+ if (close < 1)
122
+ return { status: "unknown" };
123
+ const fields = raw.slice(close + 1).trim().split(/\s+/u);
124
+ const state = fields[0];
125
+ if (state === "Z" || state === "X" || state === "x") {
126
+ return { status: "dead" };
127
+ }
128
+ if (state !== "R" && state !== "S" && state !== "D" && state !== "T" && state !== "t" && state !== "W" && state !== "K" && state !== "P" && state !== "I") {
129
+ return { status: "unknown" };
130
+ }
131
+ const startTicks = fields[19];
132
+ if (startTicks === undefined || !/^\d+$/u.test(startTicks)) {
133
+ return { status: "unknown" };
134
+ }
135
+ return { status: "alive", rawStartId: startTicks };
136
+ }
137
+ function inspectDarwinProcessInfo(raw, expectedPid) {
138
+ if (raw.byteLength < DARWIN_PROC_BSDINFO_SIZE) {
139
+ return { status: "unknown" };
140
+ }
141
+ const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
142
+ const flags = view.getUint32(DARWIN_PROC_BSDINFO_FLAGS_OFFSET, true);
143
+ const status = view.getUint32(DARWIN_PROC_BSDINFO_STATUS_OFFSET, true);
144
+ const pid = view.getUint32(DARWIN_PROC_BSDINFO_PID_OFFSET, true);
145
+ if (pid !== expectedPid)
146
+ return { status: "unknown" };
147
+ if (status === DARWIN_PROCESS_STATUS_ZOMBIE || (flags & DARWIN_PROC_FLAG_INEXIT) !== 0) {
148
+ return { status: "dead" };
149
+ }
150
+ if (status !== DARWIN_PROCESS_STATUS_IDLE && status !== DARWIN_PROCESS_STATUS_RUNNING && status !== DARWIN_PROCESS_STATUS_SLEEPING && status !== DARWIN_PROCESS_STATUS_STOPPED) {
151
+ return { status: "unknown" };
152
+ }
153
+ const seconds = view.getBigUint64(DARWIN_PROC_BSDINFO_START_SECONDS_OFFSET, true);
154
+ const microseconds = view.getBigUint64(DARWIN_PROC_BSDINFO_START_MICROSECONDS_OFFSET, true);
155
+ if (seconds === 0n || microseconds >= 1000000n) {
156
+ return { status: "unknown" };
157
+ }
158
+ return {
159
+ status: "alive",
160
+ rawStartId: `${seconds}:${microseconds}`
161
+ };
162
+ }
163
+ function darwinUniqueProcessIdentity(raw) {
164
+ if (raw.byteLength < DARWIN_PROC_UNIQIDENTIFIERINFO_SIZE)
165
+ return null;
166
+ const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
167
+ const uniqueId = view.getBigUint64(DARWIN_PROC_UNIQUE_ID_OFFSET, true);
168
+ const idVersion = view.getUint32(DARWIN_PROC_ID_VERSION_OFFSET, true);
169
+ if (uniqueId === 0n || idVersion === 0)
170
+ return null;
171
+ return `${uniqueId}:${idVersion}`;
172
+ }
173
+ function inspectDarwinProcessIdentity(raw, expectedPid) {
174
+ if (raw.byteLength < DARWIN_PROC_BSDINFO_WITH_UNIQID_SIZE) {
175
+ return { status: "unknown" };
176
+ }
177
+ const bsdInfo = raw.subarray(0, DARWIN_PROC_BSDINFO_SIZE);
178
+ const uniqueInfo = raw.subarray(DARWIN_PROC_BSDINFO_SIZE, DARWIN_PROC_BSDINFO_WITH_UNIQID_SIZE);
179
+ const record = inspectDarwinProcessInfo(bsdInfo, expectedPid);
180
+ if (record.status !== "alive")
181
+ return record;
182
+ const uniqueIdentity = darwinUniqueProcessIdentity(uniqueInfo);
183
+ if (uniqueIdentity === null)
184
+ return { status: "unknown" };
185
+ return {
186
+ status: "alive",
187
+ rawStartId: `${uniqueIdentity}:${record.rawStartId}`
188
+ };
189
+ }
190
+ function darwinProcPidInfo() {
191
+ if (cachedDarwinProcPidInfo !== undefined)
192
+ return cachedDarwinProcPidInfo;
193
+ try {
194
+ const library = dlopen("/usr/lib/libproc.dylib", {
195
+ proc_pidinfo: {
196
+ args: ["int", "int", "u64", "ptr", "int"],
197
+ returns: "int"
198
+ }
199
+ });
200
+ cachedDarwinProcPidInfo = (pid, flavor, buffer) => library.symbols.proc_pidinfo(pid, flavor, 0, ptr(buffer), buffer.byteLength);
201
+ } catch {
202
+ return null;
203
+ }
204
+ return cachedDarwinProcPidInfo;
205
+ }
206
+ function darwinProcessIsDefinitelyDead(pid) {
207
+ if (processIsDefinitelyMissing(pid))
208
+ return true;
209
+ try {
210
+ const state = commandText("/bin/ps", [
211
+ "-o",
212
+ "state=",
213
+ "-p",
214
+ String(pid)
215
+ ]);
216
+ if (DARWIN_PROCESS_STATE_PATTERN.test(state) && state.startsWith("Z")) {
217
+ return true;
218
+ }
219
+ } catch {}
220
+ return processIsDefinitelyMissing(pid);
221
+ }
222
+ function inspectDarwinProcessWithReader(pid, readRecord, isDefinitelyDead) {
223
+ for (let attempt = 0;attempt < DARWIN_PROCESS_INSPECTION_ATTEMPTS; attempt += 1) {
224
+ let record = { status: "unknown" };
225
+ try {
226
+ const raw = readRecord(pid);
227
+ if (raw !== null)
228
+ record = inspectDarwinProcessIdentity(raw, pid);
229
+ } catch {}
230
+ if (record.status === "alive" || record.status === "dead") {
231
+ return record;
232
+ }
233
+ if (isDefinitelyDead(pid))
234
+ return { status: "dead" };
235
+ }
236
+ return { status: "unknown" };
237
+ }
238
+ function inspectDarwinProcess(pid) {
239
+ return inspectDarwinProcessWithReader(pid, (targetPid) => {
240
+ const inspect = darwinProcPidInfo();
241
+ if (inspect === null)
242
+ return null;
243
+ const processInfo = Buffer.alloc(DARWIN_PROC_BSDINFO_WITH_UNIQID_SIZE);
244
+ const bytesRead = inspect(targetPid, DARWIN_PROC_PID_T_BSDINFO_WITH_UNIQID, processInfo);
245
+ if (!Number.isSafeInteger(bytesRead) || bytesRead < 0 || bytesRead > DARWIN_PROC_BSDINFO_WITH_UNIQID_SIZE) {
246
+ return null;
247
+ }
248
+ return processInfo.subarray(0, bytesRead);
249
+ }, darwinProcessIsDefinitelyDead);
250
+ }
251
+ function inspectProcessStartId(pid, bootId) {
252
+ if (pid === process.pid && cachedCurrentProcessStartIdentity?.pid === pid && cachedCurrentProcessStartIdentity.bootId === bootId) {
253
+ return {
254
+ status: "alive",
255
+ processStartId: cachedCurrentProcessStartIdentity.processStartId
256
+ };
257
+ }
258
+ try {
259
+ let raw;
260
+ if (process.platform === "linux") {
261
+ const record = inspectLinuxProcessRecord(readFileSync(`/proc/${pid}/stat`, "utf8").trim());
262
+ if (record.status !== "alive")
263
+ return record;
264
+ raw = record.rawStartId;
265
+ } else if (process.platform === "win32") {
266
+ raw = commandText(windowsPowerShellPath(), [
267
+ "-NoProfile",
268
+ "-NonInteractive",
269
+ "-Command",
270
+ "$p=[int]$args[0]; (Get-Process -Id $p -ErrorAction Stop).StartTime.ToUniversalTime().Ticks",
271
+ String(pid)
272
+ ]);
273
+ if (!/^\d{10,30}$/u.test(raw))
274
+ return { status: "unknown" };
275
+ } else if (process.platform === "darwin") {
276
+ const record = inspectDarwinProcess(pid);
277
+ if (record.status !== "alive")
278
+ return record;
279
+ raw = record.rawStartId;
280
+ } else {
281
+ return { status: "unknown" };
282
+ }
283
+ const processStartId = identityDigest("io-process-start", `${bootId}\x00${raw}`);
284
+ if (pid === process.pid) {
285
+ cachedCurrentProcessStartIdentity = { pid, bootId, processStartId };
286
+ }
287
+ return { status: "alive", processStartId };
288
+ } catch (error) {
289
+ if (processIsDefinitelyMissing(pid) || process.platform === "linux" && typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
290
+ return { status: "missing" };
291
+ }
292
+ return { status: "unknown" };
293
+ }
294
+ }
295
+ function captureProcessOwnerIdentity(pid) {
296
+ if (!Number.isSafeInteger(pid) || pid < 1) {
297
+ throw new Error("process identity PID is malformed");
298
+ }
299
+ const bootId = currentBootId();
300
+ const inspection = inspectProcessStartId(pid, bootId);
301
+ if (inspection.status !== "alive") {
302
+ throw new Error("exact live process identity is unavailable");
303
+ }
304
+ return Object.freeze({
305
+ pid,
306
+ bootId,
307
+ processStartId: inspection.processStartId
308
+ });
309
+ }
310
+ function processOwnerStatus(owner) {
311
+ const bootId = currentBootId();
312
+ if (bootId !== owner.bootId)
313
+ return "different-or-dead";
314
+ const inspection = inspectProcessStartId(owner.pid, bootId);
315
+ if (inspection.status !== "alive") {
316
+ return inspection.status === "unknown" ? "unknown" : "different-or-dead";
317
+ }
318
+ return inspection.processStartId === owner.processStartId ? "exact-live-owner" : "different-or-dead";
319
+ }
320
+
321
+ // src/storage.ts
322
+ var MAX_WRENCH_JSON_BYTES = 1024 * 1024;
323
+ var MAX_PRIVATE_STATE_BATCH_FILES = 1000;
324
+ var MAX_PRIVATE_STATE_BATCH_FILE_BYTES = 2 * 1024 * 1024;
325
+ var MAX_PRIVATE_STATE_BATCH_TOTAL_BYTES = 64 * 1024 * 1024;
326
+ var DEFAULT_PRIVATE_STATE_EXPECTED_CONTENT_BYTES = 2 * 1024 * 1024;
327
+ var MAX_PRIVATE_STATE_EXPECTED_CONTENT_BYTES = 4 * 1024 * 1024;
328
+ var MAX_PRIVATE_STATE_BATCH_NAME_BYTES = 256 * 1024;
329
+ var MAX_PRIVATE_STATE_BATCH_STDOUT_BYTES = 96 * 1024 * 1024;
330
+ var TEST_STATE_HELPER_TIMEOUT_MS = 120000;
331
+ var knownStateRoots = new Map;
332
+ var stateDirectoryNames = [
333
+ "adapter-generations",
334
+ "adapters",
335
+ "auth",
336
+ "browser-snapshots",
337
+ "captures",
338
+ "control",
339
+ "derivations",
340
+ "idempotency",
341
+ "linked-device-stores",
342
+ "messaging",
343
+ "omni-read-projections",
344
+ "operation-permissions",
345
+ "plan-assets",
346
+ "plans",
347
+ "provider-plugin-state",
348
+ "provider-plugins",
349
+ "read-projection-control",
350
+ "read-projections",
351
+ "recovery",
352
+ "run-journals",
353
+ "runs",
354
+ "session-secrets",
355
+ "tools"
356
+ ];
357
+ var stateMarkerName = ".io-state.json";
358
+ var stateMarkerText = `{"kind":"io-state","schemaVersion":1}
359
+ `;
360
+ var stateHelperPath = join(dirname(fileURLToPath(import.meta.url)), "state-helper.ts");
361
+ var stateHelperConfigPath = join(dirname(fileURLToPath(import.meta.url)), "state-helper.bunfig.toml");
362
+ var pathHelperPath = join(dirname(fileURLToPath(import.meta.url)), "path-helper.ts");
363
+ var ghostgetSourcePackageRoot = realpathSync(resolve(dirname(fileURLToPath(import.meta.url)), ".."));
364
+ function isWithinPath(root, candidate) {
365
+ const pathFromRoot = relative(root, candidate);
366
+ return pathFromRoot === "" || pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`) && !isAbsolute(pathFromRoot);
367
+ }
368
+ function hasCode(error, code) {
369
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
370
+ }
371
+ function readDescriptorBounded(descriptor, maximumBytes) {
372
+ const chunks = [];
373
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maximumBytes + 1));
374
+ let total = 0;
375
+ for (;; ) {
376
+ const remaining = maximumBytes + 1 - total;
377
+ const count = readSync(descriptor, buffer, 0, Math.min(buffer.byteLength, remaining), null);
378
+ if (count === 0)
379
+ return Buffer.concat(chunks, total);
380
+ total += count;
381
+ if (total > maximumBytes)
382
+ throw new Error("file grew beyond its byte bound");
383
+ chunks.push(Buffer.from(buffer.subarray(0, count)));
384
+ }
385
+ }
386
+ function pathInside(root, target) {
387
+ const child = relative(root, target);
388
+ return child === "" || !isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`);
389
+ }
390
+ function sameIdentity(left, right) {
391
+ return left === null || right === null ? left === right : left.device === right.device && left.inode === right.inode;
392
+ }
393
+ function exactObjectKeys(value, required, optional = []) {
394
+ const allowed = new Set([...required, ...optional]);
395
+ return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
396
+ }
397
+ function isRecord(value) {
398
+ return typeof value === "object" && value !== null && !Array.isArray(value);
399
+ }
400
+ function isSafeBatchFileName(value) {
401
+ if (typeof value !== "string" || value === "" || value === "." || value === ".." || value.includes("/") || value.includes("\\") || value.includes("\uFFFD") || Buffer.byteLength(value, "utf8") > 255)
402
+ return false;
403
+ for (const character of value) {
404
+ const codePoint = character.codePointAt(0);
405
+ if (codePoint !== undefined && (codePoint <= 31 || codePoint === 127)) {
406
+ return false;
407
+ }
408
+ }
409
+ return true;
410
+ }
411
+ function decodeCanonicalBase64(value, maximumBytes) {
412
+ if (typeof value !== "string" || value.length > Math.ceil(maximumBytes / 3) * 4 + 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value))
413
+ return null;
414
+ const decoded = Buffer.from(value, "base64");
415
+ return decoded.byteLength <= maximumBytes && decoded.toString("base64") === value ? decoded : null;
416
+ }
417
+ function parseStateHelperBatchFiles(value) {
418
+ if (!Array.isArray(value) || value.length > MAX_PRIVATE_STATE_BATCH_FILES) {
419
+ throw new Error("state helper returned a malformed response");
420
+ }
421
+ const names = new Set;
422
+ const files = [];
423
+ let contentBytes = 0;
424
+ const invalidReasons = new Set([
425
+ "unsafe-file",
426
+ "unreadable",
427
+ "file-byte-bound",
428
+ "aggregate-byte-bound",
429
+ "changed-during-read"
430
+ ]);
431
+ for (const candidate of value) {
432
+ if (!isRecord(candidate) || !isSafeBatchFileName(candidate.name)) {
433
+ throw new Error("state helper returned a malformed response");
434
+ }
435
+ const name = candidate.name;
436
+ if (names.has(name)) {
437
+ throw new Error("state helper returned a malformed response");
438
+ }
439
+ names.add(name);
440
+ if (candidate.status === "present" && exactObjectKeys(candidate, ["name", "status", "contentBase64"])) {
441
+ const content = decodeCanonicalBase64(candidate.contentBase64, MAX_PRIVATE_STATE_BATCH_FILE_BYTES);
442
+ if (content === null) {
443
+ throw new Error("state helper returned a malformed response");
444
+ }
445
+ contentBytes += content.byteLength;
446
+ if (contentBytes > MAX_PRIVATE_STATE_BATCH_TOTAL_BYTES) {
447
+ throw new Error("state helper returned a malformed response");
448
+ }
449
+ files.push({
450
+ name,
451
+ status: "present",
452
+ contentBase64: candidate.contentBase64
453
+ });
454
+ continue;
455
+ }
456
+ if (candidate.status === "absent" && exactObjectKeys(candidate, ["name", "status"])) {
457
+ files.push({ name, status: "absent" });
458
+ continue;
459
+ }
460
+ if (candidate.status === "invalid" && exactObjectKeys(candidate, ["name", "status", "reason"]) && invalidReasons.has(candidate.reason)) {
461
+ files.push({
462
+ name,
463
+ status: "invalid",
464
+ reason: candidate.reason
465
+ });
466
+ continue;
467
+ }
468
+ throw new Error("state helper returned a malformed response");
469
+ }
470
+ return files;
471
+ }
472
+ function parseStateHelperBatchChildFiles(value) {
473
+ if (!Array.isArray(value) || value.length > MAX_PRIVATE_STATE_BATCH_FILES) {
474
+ throw new Error("state helper returned a malformed response");
475
+ }
476
+ const keys = new Set;
477
+ const files = [];
478
+ let contentBytes = 0;
479
+ const invalidReasons = new Set([
480
+ "unsafe-file",
481
+ "unreadable",
482
+ "file-byte-bound",
483
+ "aggregate-byte-bound",
484
+ "changed-during-read"
485
+ ]);
486
+ for (const candidate of value) {
487
+ if (!isRecord(candidate) || !isSafeBatchFileName(candidate.directoryName) || !isSafeBatchFileName(candidate.fileName)) {
488
+ throw new Error("state helper returned a malformed response");
489
+ }
490
+ const directoryName = candidate.directoryName;
491
+ const fileName = candidate.fileName;
492
+ const key = `${directoryName}\x00${fileName}`;
493
+ if (keys.has(key)) {
494
+ throw new Error("state helper returned a malformed response");
495
+ }
496
+ keys.add(key);
497
+ if (candidate.status === "present" && exactObjectKeys(candidate, [
498
+ "directoryName",
499
+ "fileName",
500
+ "status",
501
+ "contentBase64"
502
+ ])) {
503
+ const content = decodeCanonicalBase64(candidate.contentBase64, MAX_PRIVATE_STATE_BATCH_FILE_BYTES);
504
+ if (content === null) {
505
+ throw new Error("state helper returned a malformed response");
506
+ }
507
+ contentBytes += content.byteLength;
508
+ if (contentBytes > MAX_PRIVATE_STATE_BATCH_TOTAL_BYTES) {
509
+ throw new Error("state helper returned a malformed response");
510
+ }
511
+ files.push({
512
+ directoryName,
513
+ fileName,
514
+ status: "present",
515
+ contentBase64: candidate.contentBase64
516
+ });
517
+ continue;
518
+ }
519
+ if (candidate.status === "absent" && exactObjectKeys(candidate, [
520
+ "directoryName",
521
+ "fileName",
522
+ "status"
523
+ ])) {
524
+ files.push({ directoryName, fileName, status: "absent" });
525
+ continue;
526
+ }
527
+ if (candidate.status === "invalid" && exactObjectKeys(candidate, [
528
+ "directoryName",
529
+ "fileName",
530
+ "status",
531
+ "reason"
532
+ ]) && invalidReasons.has(candidate.reason)) {
533
+ files.push({
534
+ directoryName,
535
+ fileName,
536
+ status: "invalid",
537
+ reason: candidate.reason
538
+ });
539
+ continue;
540
+ }
541
+ throw new Error("state helper returned a malformed response");
542
+ }
543
+ return files;
544
+ }
545
+ function parseResponseIdentity(value) {
546
+ if (!isRecord(value) || !exactObjectKeys(value, ["device", "inode"]) || typeof value.device !== "string" || !/^\d{1,40}$/u.test(value.device) || typeof value.inode !== "string" || !/^\d{1,40}$/u.test(value.inode))
547
+ return null;
548
+ return { device: value.device, inode: value.inode };
549
+ }
550
+ function parseStateHelperResponse(value) {
551
+ if (!isRecord(value) || !exactObjectKeys(value, ["ok", "identity"], ["created", "removed", "present", "contentBase64", "entries", "files", "childFiles", "targetIdentity"])) {
552
+ throw new Error("state helper returned a malformed response");
553
+ }
554
+ const identityValue = value.identity;
555
+ const parsedIdentity = parseResponseIdentity(identityValue);
556
+ if (value.ok !== true || parsedIdentity === null)
557
+ throw new Error("state helper returned a malformed response");
558
+ const created = value.created;
559
+ const removed = value.removed;
560
+ const present = value.present;
561
+ const contentBase64 = value.contentBase64;
562
+ const entries = value.entries;
563
+ const files = value.files;
564
+ const childFiles = value.childFiles;
565
+ const targetIdentityValue = value.targetIdentity;
566
+ const targetIdentity = targetIdentityValue === undefined ? undefined : parseResponseIdentity(targetIdentityValue);
567
+ if (created !== undefined && typeof created !== "boolean")
568
+ throw new Error("state helper returned a malformed response");
569
+ if (removed !== undefined && typeof removed !== "boolean")
570
+ throw new Error("state helper returned a malformed response");
571
+ if (present !== undefined && typeof present !== "boolean")
572
+ throw new Error("state helper returned a malformed response");
573
+ if (contentBase64 !== undefined && typeof contentBase64 !== "string")
574
+ throw new Error("state helper returned a malformed response");
575
+ if (targetIdentityValue !== undefined && targetIdentity === null) {
576
+ throw new Error("state helper returned a malformed response");
577
+ }
578
+ const responseShape = Object.keys(value).filter((key) => key !== "ok" && key !== "identity").sort().join(",");
579
+ if (!new Set([
580
+ "",
581
+ "contentBase64",
582
+ "contentBase64,present",
583
+ "created",
584
+ "created,targetIdentity",
585
+ "entries",
586
+ "entries,targetIdentity",
587
+ "childFiles,targetIdentity",
588
+ "files,targetIdentity",
589
+ "present",
590
+ "removed",
591
+ "targetIdentity"
592
+ ]).has(responseShape))
593
+ throw new Error("state helper returned a malformed response");
594
+ if (present === true !== (responseShape === "contentBase64,present")) {
595
+ if (present !== undefined)
596
+ throw new Error("state helper returned a malformed response");
597
+ }
598
+ if (entries !== undefined && (!Array.isArray(entries) || entries.length > 1e4 || entries.some((entry) => !isRecord(entry) || !exactObjectKeys(entry, ["name", "kind"], ["identity"]) || typeof entry.name !== "string" || entry.name === "" || entry.name === "." || entry.name === ".." || entry.name.includes("/") || entry.name.includes("\\") || entry.name.includes("\x00") || Buffer.byteLength(entry.name, "utf8") > 255 || entry.kind !== "file" && entry.kind !== "directory" && entry.kind !== "symbolic-link" && entry.kind !== "other" || entry.kind === "directory" !== (parseResponseIdentity(entry.identity) !== null))))
599
+ throw new Error("state helper returned a malformed response");
600
+ const parsedFiles = files === undefined ? undefined : parseStateHelperBatchFiles(files);
601
+ const parsedChildFiles = childFiles === undefined ? undefined : parseStateHelperBatchChildFiles(childFiles);
602
+ return {
603
+ ok: true,
604
+ identity: parsedIdentity,
605
+ ...created === undefined ? {} : { created },
606
+ ...removed === undefined ? {} : { removed },
607
+ ...present === undefined ? {} : { present },
608
+ ...contentBase64 === undefined ? {} : { contentBase64 },
609
+ ...entries === undefined ? {} : { entries },
610
+ ...parsedFiles === undefined ? {} : { files: parsedFiles },
611
+ ...parsedChildFiles === undefined ? {} : { childFiles: parsedChildFiles },
612
+ ...targetIdentity === undefined || targetIdentity === null ? {} : { targetIdentity }
613
+ };
614
+ }
615
+ function runStateHelper(directory, expected, operation, expectCreatedIdentity = false, faultForTest) {
616
+ const requestId = crypto.randomUUID();
617
+ const child = spawnSync(process.execPath, [
618
+ "--no-env-file",
619
+ "--no-install",
620
+ "--no-macros",
621
+ "--no-addons",
622
+ `--config=${stateHelperConfigPath}`,
623
+ stateHelperPath
624
+ ], {
625
+ cwd: directory,
626
+ encoding: "utf8",
627
+ env: faultForTest === undefined ? { NODE_ENV: "production" } : {
628
+ NODE_ENV: "test",
629
+ ...faultForTest === "insert-after-quarantine" || faultForTest === "replace-target-after-validation" ? { GHOSTGET_TEST_EMPTY_DIRECTORY_REMOVAL_RACE: faultForTest } : faultForTest === "pause-after-cas-claim" || faultForTest === "pause-after-mutation-claim-read" || faultForTest === "fail-after-cas-commit" ? { GHOSTGET_TEST_CAS_FAULT: faultForTest } : { GHOSTGET_TEST_BATCH_READ_FAULT: faultForTest }
630
+ },
631
+ input: JSON.stringify({ schemaVersion: 1, requestId, expected, operation }),
632
+ maxBuffer: operation.kind === "batch-read-files" || operation.kind === "batch-read-child-files" ? MAX_PRIVATE_STATE_BATCH_STDOUT_BYTES : 180 * 1024 * 1024,
633
+ shell: false,
634
+ timeout: faultForTest === "pause-after-cas-claim" || faultForTest === "pause-after-mutation-claim-read" ? TEST_STATE_HELPER_TIMEOUT_MS : 30000,
635
+ windowsHide: true
636
+ });
637
+ const current = inspectRealDirectoryIdentity(directory);
638
+ if (!sameIdentity(current, expected))
639
+ throw new Error(`GHOSTGET_STATE_HOME changed identity after validation: ${directory}`);
640
+ if (child.error !== undefined)
641
+ throw new Error("bound state helper failed to start", { cause: child.error });
642
+ if (child.status !== 0) {
643
+ const detail = child.stderr.trim().slice(0, 512);
644
+ throw new Error(detail === "" ? "bound state helper rejected the operation" : detail);
645
+ }
646
+ let parsed;
647
+ try {
648
+ parsed = JSON.parse(child.stdout);
649
+ } catch (error) {
650
+ throw new Error("state helper returned invalid JSON", { cause: error });
651
+ }
652
+ const response = parseStateHelperResponse(parsed);
653
+ if (!expectCreatedIdentity && !sameIdentity(response.identity, expected)) {
654
+ throw new Error("state helper response came from the wrong directory identity");
655
+ }
656
+ return response;
657
+ }
658
+ function stateSegments(root, path) {
659
+ const child = relative(root, canonicalNonStatePath(path));
660
+ if (child === "")
661
+ return [];
662
+ if (isAbsolute(child) || child === ".." || child.startsWith(`..${sep}`))
663
+ throw new Error(`state path escapes its root: ${path}`);
664
+ return child.split(sep);
665
+ }
666
+ function captureStateDirectoryExpectations(root, segments) {
667
+ const expectations = [];
668
+ let current = root;
669
+ let missing = false;
670
+ for (const segment of segments) {
671
+ current = join(current, segment);
672
+ if (missing) {
673
+ expectations.push(null);
674
+ continue;
675
+ }
676
+ let stats;
677
+ try {
678
+ stats = lstatSync(current, { bigint: true });
679
+ } catch (error) {
680
+ if (!hasCode(error, "ENOENT"))
681
+ throw error;
682
+ missing = true;
683
+ expectations.push(null);
684
+ continue;
685
+ }
686
+ if (!stats.isDirectory() || stats.isSymbolicLink() || !ownedByCurrentUser(stats) || (stats.mode & 0o777n) !== 0o700n) {
687
+ throw new Error(`ghostget state directory must be an owned real directory with mode 0700: ${current}`);
688
+ }
689
+ expectations.push({ device: stats.dev.toString(), inode: stats.ino.toString() });
690
+ }
691
+ return expectations;
692
+ }
693
+ function inspectStateRootIdentity(root) {
694
+ let stats;
695
+ try {
696
+ stats = lstatSync(root, { bigint: true });
697
+ } catch (error) {
698
+ if (hasCode(error, "ENOENT"))
699
+ return null;
700
+ throw error;
701
+ }
702
+ if (!stats.isDirectory() || stats.isSymbolicLink() || !ownedByCurrentUser(stats)) {
703
+ throw new Error(`GHOSTGET_STATE_HOME must be an owned real directory: ${root}`);
704
+ }
705
+ return { device: stats.dev.toString(), inode: stats.ino.toString() };
706
+ }
707
+ function inspectRealDirectoryIdentity(path) {
708
+ let stats;
709
+ try {
710
+ stats = lstatSync(path, { bigint: true });
711
+ } catch (error) {
712
+ if (hasCode(error, "ENOENT"))
713
+ return null;
714
+ throw error;
715
+ }
716
+ if (!stats.isDirectory() || stats.isSymbolicLink())
717
+ throw new Error(`path must be a real directory: ${path}`);
718
+ return { device: stats.dev.toString(), inode: stats.ino.toString() };
719
+ }
720
+ function findCreationAnchor(root) {
721
+ const segments = [];
722
+ let current = root;
723
+ for (;; ) {
724
+ let stats;
725
+ try {
726
+ stats = lstatSync(current, { bigint: true });
727
+ } catch (error) {
728
+ if (!hasCode(error, "ENOENT"))
729
+ throw error;
730
+ const parent = dirname(current);
731
+ if (parent === current)
732
+ throw new Error(`GHOSTGET_STATE_HOME has no real existing creation anchor: ${root}`);
733
+ segments.unshift(basename(current));
734
+ current = parent;
735
+ continue;
736
+ }
737
+ if (!stats.isDirectory() || stats.isSymbolicLink() || !ownedByCurrentUser(stats) || (stats.mode & 0o022n) !== 0n) {
738
+ throw new Error(`GHOSTGET_STATE_HOME creation path contains a non-owned real directory: ${current}`);
739
+ }
740
+ return {
741
+ identity: { device: stats.dev.toString(), inode: stats.ino.toString() },
742
+ path: current,
743
+ segments
744
+ };
745
+ }
746
+ }
747
+ function assertStateRootIdentity(root) {
748
+ const expected = knownStateRoots.get(root);
749
+ if (expected === undefined)
750
+ throw new Error(`ghostget state root has not been validated: ${root}`);
751
+ const actual = inspectStateRootIdentity(root);
752
+ if (!sameIdentity(actual, expected.identity)) {
753
+ throw new Error(`GHOSTGET_STATE_HOME changed identity after validation: ${root}`);
754
+ }
755
+ if (expected.identity === null) {
756
+ const anchor = findCreationAnchor(root);
757
+ if (expected.creationAnchor === null || anchor.path !== expected.creationAnchor.path || !sameIdentity(anchor.identity, expected.creationAnchor.identity) || anchor.segments.join("\x00") !== expected.creationAnchor.segments.join("\x00"))
758
+ throw new Error(`GHOSTGET_STATE_HOME creation path changed after validation: ${root}`);
759
+ }
760
+ if (expected.claimed) {
761
+ if (actual === null)
762
+ throw new Error(`GHOSTGET_STATE_HOME disappeared after validation: ${root}`);
763
+ const stats = lstatSync(root);
764
+ if ((stats.mode & 511) !== 448)
765
+ throw new Error(`GHOSTGET_STATE_HOME must remain private (mode 0700): ${root}`);
766
+ readStateMarker(join(root, stateMarkerName));
767
+ }
768
+ return expected;
769
+ }
770
+ function assertNoSymbolicLinks(root, target, includeTarget, requirePrivateDirectories = false) {
771
+ const canonicalRoot = resolve(root);
772
+ const absoluteTarget = resolve(target);
773
+ if (!pathInside(canonicalRoot, absoluteTarget))
774
+ throw new Error(`state path escapes its root: ${absoluteTarget}`);
775
+ const checkedTarget = includeTarget ? absoluteTarget : dirname(absoluteTarget);
776
+ const child = relative(canonicalRoot, checkedTarget);
777
+ const components = child === "" ? [] : child.split(sep);
778
+ let current = canonicalRoot;
779
+ const paths = [current, ...components.map((component) => {
780
+ current = join(current, component);
781
+ return current;
782
+ })];
783
+ for (const [index, path] of paths.entries()) {
784
+ let stats;
785
+ try {
786
+ stats = lstatSync(path);
787
+ } catch (error) {
788
+ if (hasCode(error, "ENOENT"))
789
+ return;
790
+ throw error;
791
+ }
792
+ if (stats.isSymbolicLink())
793
+ throw new Error(`ghostget state path contains a symbolic link: ${path}`);
794
+ if (stats.isDirectory() && requirePrivateDirectories && (!ownedByCurrentUser(stats) || (stats.mode & 511) !== 448)) {
795
+ throw new Error(`ghostget state directory must be owned and private (mode 0700): ${path}`);
796
+ }
797
+ if (index < paths.length - 1 && !stats.isDirectory()) {
798
+ throw new Error(`ghostget state ancestor is not a directory: ${path}`);
799
+ }
800
+ }
801
+ }
802
+ function assertSafeStatePath(path, environment = process.env, includeTarget = true) {
803
+ const root = ghostgetStateHome(environment);
804
+ assertStateRootIdentity(root);
805
+ assertNoSymbolicLinks(root, path, includeTarget, true);
806
+ assertStateRootIdentity(root);
807
+ }
808
+ function canonicalPotentialPath(value) {
809
+ const suffix = [];
810
+ let ancestor = resolve(value);
811
+ while (!existsSync(ancestor)) {
812
+ const parent = dirname(ancestor);
813
+ if (parent === ancestor)
814
+ throw new Error(`path has no existing ancestor: ${value}`);
815
+ suffix.unshift(ancestor.slice(parent.length + (parent.endsWith("/") ? 0 : 1)));
816
+ ancestor = parent;
817
+ }
818
+ const stats = lstatSync(ancestor);
819
+ if (stats.isSymbolicLink())
820
+ return resolve(realpathSync(ancestor), ...suffix);
821
+ return resolve(realpathSync(ancestor), ...suffix);
822
+ }
823
+ function ownedByCurrentUser(stats) {
824
+ const currentUid = typeof process.getuid === "function" ? process.getuid() : undefined;
825
+ return currentUid === undefined || stats.uid === (typeof stats.uid === "bigint" ? BigInt(currentUid) : currentUid);
826
+ }
827
+ function readStateMarker(path) {
828
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
829
+ const nonBlocking = "O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0;
830
+ const descriptor = openSync(path, constants.O_RDONLY | noFollow | nonBlocking);
831
+ try {
832
+ const stats = fstatSync(descriptor);
833
+ if (!stats.isFile() || stats.size > 256 || !ownedByCurrentUser(stats) || (stats.mode & 63) !== 0) {
834
+ throw new Error("ghostget state marker must be a private, owned regular file");
835
+ }
836
+ const content = new TextDecoder("utf-8", {
837
+ fatal: true,
838
+ ignoreBOM: true
839
+ }).decode(readDescriptorBounded(descriptor, 256));
840
+ if (content !== stateMarkerText)
841
+ throw new Error("ghostget state marker is malformed");
842
+ const value = JSON.parse(content);
843
+ if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).sort().join(",") !== "kind,schemaVersion" || !("schemaVersion" in value) || value.schemaVersion !== 1 || !("kind" in value) || value.kind !== "io-state")
844
+ throw new Error("ghostget state marker is malformed");
845
+ } finally {
846
+ closeSync(descriptor);
847
+ }
848
+ }
849
+ function hasGhostgetPathIdentity(path) {
850
+ return resolve(path).split(sep).filter((segment) => segment !== "").slice(-3).some((segment) => /(?:^|[^a-z0-9])(?:ghostget|wrench|oh|io)(?:[^a-z0-9]|$)/iu.test(segment));
851
+ }
852
+ function validateUnmarkedStateRoot(root) {
853
+ if (!hasGhostgetPathIdentity(root)) {
854
+ throw new Error(`GHOSTGET_STATE_HOME is not marked as wrench-owned and its path does not identify dedicated ghostget state: ${root}`);
855
+ }
856
+ const allowed = new Set([
857
+ ...stateDirectoryNames,
858
+ ".cursor-encryption-key",
859
+ ".plan-encryption-key",
860
+ ".projection-encryption-key",
861
+ ".recovery-encryption-key",
862
+ ".session-encryption-key"
863
+ ]);
864
+ const entries = [];
865
+ const directory = opendirSync(root);
866
+ try {
867
+ for (;; ) {
868
+ const entry = directory.readSync();
869
+ if (entry === null)
870
+ break;
871
+ if (entries.length >= 1e4)
872
+ throw new Error("GHOSTGET_STATE_HOME contains more than 10000 entries");
873
+ if (entry.name.includes("\uFFFD") || Buffer.byteLength(entry.name, "utf8") > 255) {
874
+ throw new Error("GHOSTGET_STATE_HOME contains an unsafe entry name");
875
+ }
876
+ entries.push(entry);
877
+ }
878
+ } finally {
879
+ directory.closeSync();
880
+ }
881
+ for (const entry of entries) {
882
+ if (entry.isSymbolicLink())
883
+ throw new Error(`GHOSTGET_STATE_HOME contains a symbolic link: ${join(root, entry.name)}`);
884
+ if (/^\.io-state\.stage-\d+-[0-9a-f-]{36}\.json$/u.test(entry.name)) {
885
+ const stats2 = lstatSync(join(root, entry.name));
886
+ if (!stats2.isFile() || !ownedByCurrentUser(stats2) || (stats2.mode & 63) !== 0 || stats2.size > 256) {
887
+ throw new Error(`GHOSTGET_STATE_HOME contains an invalid interrupted state-marker stage: ${entry.name}`);
888
+ }
889
+ continue;
890
+ }
891
+ if (!allowed.has(entry.name)) {
892
+ throw new Error(`GHOSTGET_STATE_HOME is not an empty or recognizable dedicated ghostget state directory: ${root}`);
893
+ }
894
+ if (stateDirectoryNames.includes(entry.name) && !entry.isDirectory()) {
895
+ throw new Error(`GHOSTGET_STATE_HOME contains an invalid state entry: ${entry.name}`);
896
+ }
897
+ if ((entry.name === ".plan-encryption-key" || entry.name === ".cursor-encryption-key" || entry.name === ".projection-encryption-key" || entry.name === ".recovery-encryption-key" || entry.name === ".session-encryption-key") && !entry.isFile()) {
898
+ throw new Error("GHOSTGET_STATE_HOME contains an invalid encryption key entry");
899
+ }
900
+ const stats = lstatSync(join(root, entry.name));
901
+ const hasPrivateMode = entry.isDirectory() ? (stats.mode & 511) === 448 : (stats.mode & 63) === 0;
902
+ if (!ownedByCurrentUser(stats) || !hasPrivateMode) {
903
+ throw new Error(`GHOSTGET_STATE_HOME contains a state entry that is not owned and private: ${entry.name}`);
904
+ }
905
+ }
906
+ return entries.length > 0;
907
+ }
908
+ function validateStateRoot(root) {
909
+ if (!existsSync(root))
910
+ return { claimed: false, creationAnchor: findCreationAnchor(root), identity: null };
911
+ const stats = lstatSync(root, { bigint: true });
912
+ if (!stats.isDirectory() || stats.isSymbolicLink() || !ownedByCurrentUser(stats)) {
913
+ throw new Error(`GHOSTGET_STATE_HOME must be an owned real directory: ${root}`);
914
+ }
915
+ const marker = join(root, stateMarkerName);
916
+ const claimed = existsSync(marker);
917
+ const hasUnmarkedState = claimed ? false : validateUnmarkedStateRoot(root);
918
+ if (claimed)
919
+ readStateMarker(marker);
920
+ if (!claimed && !hasUnmarkedState && (stats.mode & 0o022n) !== 0n) {
921
+ throw new Error(`an unclaimed GHOSTGET_STATE_HOME must not be group/world-writable: ${root}`);
922
+ }
923
+ if ((claimed || hasUnmarkedState) && (stats.mode & 0o777n) !== 0o700n) {
924
+ throw new Error(`GHOSTGET_STATE_HOME must be private (mode 0700) before trusted state is read: ${root}`);
925
+ }
926
+ return { claimed, creationAnchor: null, identity: { device: stats.dev.toString(), inode: stats.ino.toString() } };
927
+ }
928
+ function selectStateHome(environment) {
929
+ const configuredRoots = [
930
+ ["GHOSTGET_STATE_HOME", environment.GHOSTGET_STATE_HOME],
931
+ ["WRENCH_STATE_HOME", environment.WRENCH_STATE_HOME],
932
+ ["OH_STATE_HOME", environment.OH_STATE_HOME],
933
+ ["IO_HOME", environment.IO_HOME]
934
+ ];
935
+ const requestedRoots = configuredRoots.flatMap(([name, value]) => {
936
+ const trimmed = value?.trim() ?? "";
937
+ return trimmed === "" ? [] : [{ name, root: canonicalPotentialPath(trimmed) }];
938
+ });
939
+ const distinctRequestedRoots = new Set(requestedRoots.map(({ root: root2 }) => root2));
940
+ if (distinctRequestedRoots.size > 1) {
941
+ throw new Error(`${requestedRoots.map(({ name }) => name).join(", ")} select different state roots`);
942
+ }
943
+ const dataRoot = environment.XDG_DATA_HOME !== undefined && environment.XDG_DATA_HOME.trim() !== "" ? resolve(environment.XDG_DATA_HOME) : join(homedir(), ".local", "share");
944
+ const currentDefault = canonicalPotentialPath(join(dataRoot, "ghostget"));
945
+ const legacyDefaults = [
946
+ canonicalPotentialPath(join(dataRoot, "wrench")),
947
+ canonicalPotentialPath(join(dataRoot, "oh")),
948
+ canonicalPotentialPath(join(dataRoot, "io"))
949
+ ];
950
+ let root = requestedRoots[0]?.root ?? null;
951
+ if (root === null) {
952
+ const existingDefaults = [currentDefault, ...legacyDefaults].filter((candidate) => existsSync(candidate));
953
+ if (existingDefaults.length > 1) {
954
+ throw new Error(`multiple Ghostget and legacy state roots exist; set GHOSTGET_STATE_HOME explicitly after reconciling ${existingDefaults.join(", ")}`);
955
+ }
956
+ root = existingDefaults[0] ?? currentDefault;
957
+ }
958
+ const home = canonicalPotentialPath(homedir());
959
+ const forbiddenRoots = new Set([
960
+ parse(root).root,
961
+ home,
962
+ dirname(home),
963
+ canonicalPotentialPath(tmpdir()),
964
+ canonicalPotentialPath(process.cwd()),
965
+ ...["Desktop", "Documents", "Downloads", "Library", ".cache", ".config", ".local"].map((name) => canonicalPotentialPath(join(home, name))),
966
+ ...environment.XDG_DATA_HOME === undefined || environment.XDG_DATA_HOME.trim() === "" ? [] : [canonicalPotentialPath(environment.XDG_DATA_HOME)]
967
+ ]);
968
+ if (forbiddenRoots.has(root) || isWithinPath(ghostgetSourcePackageRoot, root)) {
969
+ throw new Error(`GHOSTGET_STATE_HOME must be a dedicated child directory, not a filesystem, home, temporary, repository, or shared data root: ${root}`);
970
+ }
971
+ return root;
972
+ }
973
+ function ghostgetStateHome(environment = process.env) {
974
+ const root = selectStateHome(environment);
975
+ const inspected = validateStateRoot(root);
976
+ const remembered = knownStateRoots.get(root);
977
+ if (remembered !== undefined) {
978
+ if (!sameIdentity(remembered.identity, inspected.identity)) {
979
+ throw new Error(`GHOSTGET_STATE_HOME changed identity after validation: ${root}`);
980
+ }
981
+ if (remembered.identity === null && (remembered.creationAnchor === null || inspected.creationAnchor === null || remembered.creationAnchor.path !== inspected.creationAnchor.path || !sameIdentity(remembered.creationAnchor.identity, inspected.creationAnchor.identity) || remembered.creationAnchor.segments.join("\x00") !== inspected.creationAnchor.segments.join("\x00")))
982
+ throw new Error(`GHOSTGET_STATE_HOME creation path changed after validation: ${root}`);
983
+ if (remembered.claimed && !inspected.claimed) {
984
+ throw new Error(`GHOSTGET_STATE_HOME lost its ownership marker after validation: ${root}`);
985
+ }
986
+ knownStateRoots.set(root, { ...inspected, claimed: remembered.claimed || inspected.claimed });
987
+ } else {
988
+ knownStateRoots.set(root, inspected);
989
+ }
990
+ for (const name of stateDirectoryNames)
991
+ assertNoSymbolicLinks(root, join(root, name), true);
992
+ return root;
993
+ }
994
+ function ensureClaimedStateRoot(root) {
995
+ let remembered = assertStateRootIdentity(root);
996
+ if (remembered.identity === null) {
997
+ const anchor = remembered.creationAnchor;
998
+ if (anchor === null)
999
+ throw new Error(`GHOSTGET_STATE_HOME has no validated creation anchor: ${root}`);
1000
+ const response = runStateHelper(anchor.path, anchor.identity, { kind: "create-root", segments: anchor.segments }, true);
1001
+ const current = inspectStateRootIdentity(root);
1002
+ if (!sameIdentity(current, response.identity))
1003
+ throw new Error(`GHOSTGET_STATE_HOME changed identity while being created: ${root}`);
1004
+ remembered = { claimed: true, creationAnchor: null, identity: response.identity };
1005
+ knownStateRoots.set(root, remembered);
1006
+ } else if (!remembered.claimed) {
1007
+ validateUnmarkedStateRoot(root);
1008
+ const descriptor = openSync(root, constants.O_RDONLY | ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0) | ("O_DIRECTORY" in constants ? constants.O_DIRECTORY : 0));
1009
+ try {
1010
+ const stats = fstatSync(descriptor, { bigint: true });
1011
+ const actual = { device: stats.dev.toString(), inode: stats.ino.toString() };
1012
+ if (!sameIdentity(actual, remembered.identity) || !ownedByCurrentUser(stats)) {
1013
+ throw new Error(`GHOSTGET_STATE_HOME changed identity while being claimed: ${root}`);
1014
+ }
1015
+ fchmodSync(descriptor, 448);
1016
+ fsyncSync(descriptor);
1017
+ } finally {
1018
+ closeSync(descriptor);
1019
+ }
1020
+ if (!sameIdentity(inspectStateRootIdentity(root), remembered.identity)) {
1021
+ throw new Error(`GHOSTGET_STATE_HOME changed identity while being claimed: ${root}`);
1022
+ }
1023
+ runStateHelper(root, remembered.identity, { kind: "claim" });
1024
+ if (!sameIdentity(inspectStateRootIdentity(root), remembered.identity)) {
1025
+ throw new Error(`GHOSTGET_STATE_HOME changed identity while being claimed: ${root}`);
1026
+ }
1027
+ remembered = { claimed: true, creationAnchor: null, identity: remembered.identity };
1028
+ knownStateRoots.set(root, remembered);
1029
+ }
1030
+ if (remembered.identity === null)
1031
+ throw new Error(`GHOSTGET_STATE_HOME is unavailable after being claimed: ${root}`);
1032
+ assertStateRootIdentity(root);
1033
+ return remembered.identity;
1034
+ }
1035
+ function canonicalNonStatePath(value) {
1036
+ const absolute = resolve(value);
1037
+ const filesystemRoot = parse(absolute).root;
1038
+ const child = relative(filesystemRoot, absolute);
1039
+ const segments = child === "" ? [] : child.split(sep);
1040
+ let current = filesystemRoot;
1041
+ for (const [index, segment] of segments.entries()) {
1042
+ const candidate = join(current, segment);
1043
+ let stats;
1044
+ try {
1045
+ stats = lstatSync(candidate);
1046
+ } catch (error) {
1047
+ if (!hasCode(error, "ENOENT"))
1048
+ throw error;
1049
+ return join(current, ...segments.slice(index));
1050
+ }
1051
+ if (stats.isSymbolicLink()) {
1052
+ if (process.platform === "win32" || Number(stats.uid) !== 0) {
1053
+ throw new Error(`private path is not a real directory (symbolic link): ${candidate}`);
1054
+ }
1055
+ current = realpathSync(candidate);
1056
+ continue;
1057
+ }
1058
+ current = candidate;
1059
+ }
1060
+ return current;
1061
+ }
1062
+ function ensurePrivateStateDirectory(path, environment = process.env) {
1063
+ assertSafeStatePath(path, environment);
1064
+ const root = ghostgetStateHome(environment);
1065
+ const identity = ensureClaimedStateRoot(root);
1066
+ const segments = stateSegments(root, path);
1067
+ if (segments.length === 0)
1068
+ return identity;
1069
+ const response = runStateHelper(root, identity, {
1070
+ kind: "ensure-directories",
1071
+ segments,
1072
+ directoryExpectations: captureStateDirectoryExpectations(root, segments)
1073
+ });
1074
+ if (response.targetIdentity === undefined) {
1075
+ throw new Error("state helper omitted the ensured directory identity");
1076
+ }
1077
+ return response.targetIdentity;
1078
+ }
1079
+ function snapshotPrivateStateDirectory(path, environment = process.env, expectedTarget, options = {}) {
1080
+ const root = ghostgetStateHome(environment);
1081
+ const identity = ensureClaimedStateRoot(root);
1082
+ const segments = stateSegments(root, path);
1083
+ if (segments.length === 0)
1084
+ throw new Error("the ghostget state root cannot be listed as a state collection");
1085
+ const directoryExpectations = [...captureStateDirectoryExpectations(root, segments)];
1086
+ if (expectedTarget !== undefined && directoryExpectations.at(-1) !== null) {
1087
+ directoryExpectations[directoryExpectations.length - 1] = expectedTarget;
1088
+ }
1089
+ const response = runStateHelper(root, identity, {
1090
+ kind: "list-directory",
1091
+ segments,
1092
+ directoryExpectations,
1093
+ recoverOrphanedMutationClaims: options.recoverOrphanedMutationClaims === true
1094
+ });
1095
+ if (response.entries === undefined)
1096
+ throw new Error("state helper omitted its directory entries");
1097
+ return Object.freeze({
1098
+ identity: response.targetIdentity ?? null,
1099
+ entries: response.entries
1100
+ });
1101
+ }
1102
+ var adapterGenerationMaximumIndexBytes = 1024 * 1024;
1103
+ var activeAdapterGenerationTransactions = new Map;
1104
+
1105
+ export { captureProcessOwnerIdentity, processOwnerStatus, assertSafeStatePath, ghostgetStateHome, ensurePrivateStateDirectory, snapshotPrivateStateDirectory };