@minhspark/codex-mcp-bridge 1.22.7 → 1.22.8
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +1 -1
- package/src/claude-desktop-context.mjs +7 -7
- package/src/codex-account-context.mjs +7 -7
- package/src/codex-native-response.mjs +8 -7
- package/src/desktop-account-context.mjs +8 -8
- package/src/desktop-reply-receipts.mjs +11 -10
- package/src/file-identity.mjs +13 -0
- package/src/index.mjs +1 -1
- package/src/native-creation-receipts.mjs +26 -25
- package/src/native-relay-companion.mjs +1 -1
- package/src/project-scope.mjs +7 -7
- package/src/rollout-reader.mjs +15 -12
- package/src/rollout-segments.mjs +2 -2
- package/src/sites-connector.mjs +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.22.8] - 2026-10-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Compare file identities with exact 64-bit values everywhere. Reply and native creation receipts, the creation signing key, Codex rollout watermarks and segment checks, Codex account evidence, Claude Desktop task metadata and account state, project repository metadata, and the Sites connector's private files read `dev`, `ino`, `size` and nanosecond `mtime`/`ctime` as bigint. Number stats round inodes above 2^53, which NTFS file IDs and some macOS volumes (such as `/tmp` on the sealed system volume) exceed, so a replaced file or directory could pass as unchanged; this made `rejects replacement of a pinned receipt directory` fail intermittently on Windows CI.
|
|
12
|
+
|
|
13
|
+
### Security
|
|
14
|
+
|
|
15
|
+
- A rollout watermark now stores its file identity as exact decimal strings. A reply receipt written by an earlier version still matches only when its numeric identity is an exactly representable integer equal to the current file; a rounded identity no longer matches, and the observation reports the rollout as changed instead.
|
|
16
|
+
|
|
17
|
+
### Verification
|
|
18
|
+
|
|
19
|
+
- 7 new regression tests. Two identities that differ only below Number precision (`2^60` and `2^60 + 1`) compare as different for both `ino` and `dev`; the reply and native creation receipt stores reject a pinned directory whose inode changes only below 2^53, and both store tests fail on v1.22.7. The rollout watermark keeps exact string identities through JSON and honors only exactly representable legacy numbers.
|
|
20
|
+
- A review of the change found a test that now passed without reaching its byte comparison: its mock returned Number stats to code that requests bigint. It was fixed and checked by removing the comparison, which makes the test fail.
|
|
21
|
+
- CI passed on macOS, Ubuntu and Windows with Node 22 and 24, including `test (windows-latest, 22)` where the flake appeared. CodeQL re-reported 12 `js/path-injection` flows on the edited stat lines; each source is operator `process.env` configuration, matching the earlier dismissals, and they were dismissed as false positives.
|
|
22
|
+
|
|
23
|
+
### Limitations
|
|
24
|
+
|
|
25
|
+
- The 64-bit NTFS case runs for real only on Windows CI; local tests simulate it with mocked bigint stats. A reply observation whose earlier receipt stored a rounded rollout identity reports the rollout as changed; inspect that task instead of resending.
|
|
26
|
+
|
|
7
27
|
## [1.22.7] - 2026-10-20
|
|
8
28
|
|
|
9
29
|
### Added
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "codex-mcp-bridge",
|
|
4
|
-
"version": "1.22.
|
|
4
|
+
"version": "1.22.8",
|
|
5
5
|
"description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bui Dang Minh",
|
|
@@ -24,7 +24,7 @@ import { stableInstallPath } from "../src/install-paths.mjs";
|
|
|
24
24
|
|
|
25
25
|
exitForVersionRequest(import.meta.url);
|
|
26
26
|
|
|
27
|
-
const VERSION = "1.22.
|
|
27
|
+
const VERSION = "1.22.8";
|
|
28
28
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
29
29
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
30
30
|
const entryArgs = ["native-relay-companion.mjs"];
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { discoverClaudeProject } from "./claude-project-discovery.mjs";
|
|
|
30
30
|
|
|
31
31
|
exitForVersionRequest(import.meta.url);
|
|
32
32
|
|
|
33
|
-
const VERSION = "1.22.
|
|
33
|
+
const VERSION = "1.22.8";
|
|
34
34
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
35
35
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
36
36
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
@@ -9,7 +9,7 @@ const TASK_FILE = new RegExp(`^(local_${UUID})\\.json$`, "i");
|
|
|
9
9
|
const MAX_ENTRIES = 8192;
|
|
10
10
|
const MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
11
11
|
const MAX_IDENTITY_BYTES = 32 * 1024 * 1024;
|
|
12
|
-
const VERSION_FIELDS = ["size", "
|
|
12
|
+
const VERSION_FIELDS = ["size", "mtimeNs", "ctimeNs", "ino", "dev"];
|
|
13
13
|
|
|
14
14
|
const sameVersion = (left, right) => VERSION_FIELDS.every((field) => left[field] === right[field]);
|
|
15
15
|
|
|
@@ -68,12 +68,12 @@ function metadataFiles(root, accountId) {
|
|
|
68
68
|
function readMetadata(file, budget) {
|
|
69
69
|
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
70
70
|
try {
|
|
71
|
-
const before = fs.fstatSync(descriptor);
|
|
72
|
-
if (!before.isFile() || before.size > MAX_FILE_BYTES) throw new Error("file limit");
|
|
73
|
-
const bytes = Buffer.alloc(before.size);
|
|
71
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
72
|
+
if (!before.isFile() || before.size > BigInt(MAX_FILE_BYTES)) throw new Error("file limit");
|
|
73
|
+
const bytes = Buffer.alloc(Number(before.size));
|
|
74
74
|
if (fs.readSync(descriptor, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("short read");
|
|
75
|
-
const after = fs.fstatSync(descriptor);
|
|
76
|
-
const current = fs.lstatSync(file);
|
|
75
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
76
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
77
77
|
if (!sameVersion(before, after) || current.isSymbolicLink() || !sameVersion(current, after)) throw new Error("metadata changed");
|
|
78
78
|
budget.versions.set(file, current);
|
|
79
79
|
budget.digests.set(file, createHash("sha256").update(bytes).digest("hex"));
|
|
@@ -217,7 +217,7 @@ export function readClaudeDesktopTasks({ platform = process.platform, env = proc
|
|
|
217
217
|
const currentFiles = metadataFiles(directory, account?.accountId);
|
|
218
218
|
if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) throw new Error("metadata changed");
|
|
219
219
|
for (const file of files) {
|
|
220
|
-
const current = fs.lstatSync(file);
|
|
220
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
221
221
|
if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || rereadMetadata(file) !== budget.digests.get(file)) throw new Error("metadata changed");
|
|
222
222
|
}
|
|
223
223
|
return records;
|
|
@@ -16,22 +16,22 @@ function identity(value) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function readStable(file, limit) {
|
|
19
|
-
const entry = fs.lstatSync(file);
|
|
19
|
+
const entry = fs.lstatSync(file, { bigint: true });
|
|
20
20
|
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("Codex account evidence must be a regular file");
|
|
21
21
|
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
22
22
|
try {
|
|
23
|
-
const before = fs.fstatSync(descriptor);
|
|
24
|
-
if (!before.isFile() || before.size > limit || entry.ino !== before.ino || entry.dev !== before.dev) throw new Error("Codex account evidence is invalid or exceeds its read limit");
|
|
25
|
-
const data = Buffer.alloc(before.size);
|
|
23
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
24
|
+
if (!before.isFile() || before.size > BigInt(limit) || entry.ino !== before.ino || entry.dev !== before.dev) throw new Error("Codex account evidence is invalid or exceeds its read limit");
|
|
25
|
+
const data = Buffer.alloc(Number(before.size));
|
|
26
26
|
let offset = 0;
|
|
27
27
|
while (offset < data.length) {
|
|
28
28
|
const count = fs.readSync(descriptor, data, offset, data.length - offset, offset);
|
|
29
29
|
if (!count) throw new Error("Codex account evidence changed while reading; retry on the next call");
|
|
30
30
|
offset += count;
|
|
31
31
|
}
|
|
32
|
-
const after = fs.fstatSync(descriptor);
|
|
33
|
-
const current = fs.lstatSync(file);
|
|
34
|
-
if (!current.isFile() || current.isSymbolicLink() || before.size !== after.size || before.
|
|
32
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
33
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
34
|
+
if (!current.isFile() || current.isSymbolicLink() || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || before.ino !== current.ino || before.dev !== current.dev || after.size !== current.size || after.mtimeNs !== current.mtimeNs || after.ctimeNs !== current.ctimeNs) throw new Error("Codex account evidence changed while reading; retry on the next call");
|
|
35
35
|
return data.toString("utf8");
|
|
36
36
|
} finally {
|
|
37
37
|
fs.closeSync(descriptor);
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
6
6
|
import { scanRollout } from "./rollout-reader.mjs";
|
|
7
|
+
import { fileIdentity, sameFileIdentity } from "./file-identity.mjs";
|
|
7
8
|
import { findRolloutSegments, confirmsDesktopTask, isLegacyCliSession, assertRolloutSetStable } from "./rollout-segments.mjs";
|
|
8
9
|
|
|
9
10
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
@@ -52,9 +53,9 @@ function findRollouts(threadId, env) {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function pathIdentity(candidate, kind) {
|
|
55
|
-
const status = fs.lstatSync(candidate);
|
|
56
|
+
const status = fs.lstatSync(candidate, { bigint: true });
|
|
56
57
|
if ((kind === "file" ? !status.isFile() : !status.isDirectory()) || status.isSymbolicLink()) throw new Error(`The Codex rollout ${kind === "file" ? "file" : "ancestor"} is not regular`);
|
|
57
|
-
return { path: candidate,
|
|
58
|
+
return { path: candidate, ...fileIdentity(status) };
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function capturePathBoundary(file, sessions) {
|
|
@@ -77,9 +78,9 @@ function capturePathBoundary(file, sessions) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function samePathBoundary(expected, current) {
|
|
80
|
-
return expected.file
|
|
81
|
+
return sameFileIdentity(expected.file, current.file) &&
|
|
81
82
|
expected.ancestors.length === current.ancestors.length && expected.ancestors.every((entry, index) =>
|
|
82
|
-
entry.path === current.ancestors[index].path && entry
|
|
83
|
+
entry.path === current.ancestors[index].path && sameFileIdentity(entry, current.ancestors[index]));
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function readStable(found, maxBytes, { threadId, turnId, afterOffset = 0, prefixSize, dispatchOutputs, dispatchAfterOffset = afterOffset } = {}) {
|
|
@@ -136,7 +137,7 @@ function readStableOnce(found, maxBytes, { threadId, turnId, afterOffset, prefix
|
|
|
136
137
|
},
|
|
137
138
|
});
|
|
138
139
|
const afterBoundary = capturePathBoundary(file, sessions);
|
|
139
|
-
if (!samePathBoundary(beforeBoundary, afterBoundary) || snapshot.identity
|
|
140
|
+
if (!samePathBoundary(beforeBoundary, afterBoundary) || !sameFileIdentity(snapshot.identity, beforeBoundary.file)) {
|
|
140
141
|
throw new Error("The Codex rollout changed while reading");
|
|
141
142
|
}
|
|
142
143
|
return { ...snapshot, file, sessions, records, dispatches, otherTurnStarts, retainedBytes, session, latestContext, turnBeforeWatermark, prefixSha256: digest.digest("hex") };
|
|
@@ -427,7 +428,7 @@ export function resolveCodexNativeDispatchResponse(binding, { env = process.env,
|
|
|
427
428
|
const mark = marks.find(({ file }) => file === entry.file);
|
|
428
429
|
const snapshot = readStable(entry, maxRolloutBytes, { threadId, afterOffset: mark?.size ?? 0, prefixSize: mark?.size, dispatchOutputs: outputs });
|
|
429
430
|
if (mark) {
|
|
430
|
-
if (snapshot.identity
|
|
431
|
+
if (!sameFileIdentity(snapshot.identity, mark.identity) || snapshot.size < mark.size) throw new Error("The selected native task rollout identity changed after dispatch");
|
|
431
432
|
if (snapshot.prefixSha256 !== mark.prefixSha256) throw new Error("The pre-send Codex rollout history changed after dispatch");
|
|
432
433
|
}
|
|
433
434
|
validateIdentity(snapshot, threadId, cwd, desktopEvidence);
|
|
@@ -473,7 +474,7 @@ export function readCodexNativeTurnResponse({ threadId, turnId, previousTurnId,
|
|
|
473
474
|
retainedBytes += snapshot.retainedBytes;
|
|
474
475
|
if (retainedBytes > maxRolloutBytes) throw new Error("The native turn exceeds the retained-record limit across segments");
|
|
475
476
|
if (mark) {
|
|
476
|
-
if (snapshot.identity
|
|
477
|
+
if (!sameFileIdentity(snapshot.identity, mark.identity) || snapshot.size < mark.size) throw new Error("The selected native task rollout identity changed after dispatch");
|
|
477
478
|
if (snapshot.prefixSha256 !== mark.prefixSha256) throw new Error("The pre-send Codex rollout history changed after dispatch");
|
|
478
479
|
}
|
|
479
480
|
if (snapshot.turnBeforeWatermark) throw new Error("The observed turn was already present before dispatch");
|
|
@@ -8,7 +8,7 @@ const PACKAGE_NAME = /^Claude_[a-z0-9._-]+$/i;
|
|
|
8
8
|
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
9
9
|
const MAX_PACKAGES = 512;
|
|
10
10
|
const MAX_ROOTS = 16;
|
|
11
|
-
const VERSION_FIELDS = ["size", "
|
|
11
|
+
const VERSION_FIELDS = ["size", "mtimeNs", "ctimeNs", "ino", "dev"];
|
|
12
12
|
const sameVersion = (left, right) => VERSION_FIELDS.every((field) => left[field] === right[field]);
|
|
13
13
|
const isMissing = (error) => error.code === "ENOENT";
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ function absoluteDirectory(value) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function directoryStat(directory) {
|
|
31
|
-
const stat = fs.lstatSync(directory);
|
|
31
|
+
const stat = fs.lstatSync(directory, { bigint: true });
|
|
32
32
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("invalid directory");
|
|
33
33
|
return stat;
|
|
34
34
|
}
|
|
@@ -73,22 +73,22 @@ function readConfig(root) {
|
|
|
73
73
|
}
|
|
74
74
|
const file = path.join(root, "config.json");
|
|
75
75
|
let initial;
|
|
76
|
-
try { initial = fs.lstatSync(file); }
|
|
76
|
+
try { initial = fs.lstatSync(file, { bigint: true }); }
|
|
77
77
|
catch (error) {
|
|
78
78
|
if (isMissing(error)) return { kind: "missing-config", rootStat };
|
|
79
79
|
throw error;
|
|
80
80
|
}
|
|
81
|
-
if (!initial.isFile() || initial.isSymbolicLink() || initial.size > MAX_CONFIG_BYTES) throw new Error("invalid config");
|
|
81
|
+
if (!initial.isFile() || initial.isSymbolicLink() || initial.size > BigInt(MAX_CONFIG_BYTES)) throw new Error("invalid config");
|
|
82
82
|
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
83
83
|
let bytes;
|
|
84
84
|
let current;
|
|
85
85
|
try {
|
|
86
|
-
const before = fs.fstatSync(descriptor);
|
|
86
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
87
87
|
if (!before.isFile() || !sameVersion(initial, before)) throw new Error("config changed");
|
|
88
|
-
bytes = Buffer.alloc(before.size);
|
|
88
|
+
bytes = Buffer.alloc(Number(before.size));
|
|
89
89
|
if (fs.readSync(descriptor, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("short read");
|
|
90
|
-
const after = fs.fstatSync(descriptor);
|
|
91
|
-
current = fs.lstatSync(file);
|
|
90
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
91
|
+
current = fs.lstatSync(file, { bigint: true });
|
|
92
92
|
if (!current.isFile() || current.isSymbolicLink() || !sameVersion(before, after) || !sameVersion(after, current)) throw new Error("config changed");
|
|
93
93
|
} finally {
|
|
94
94
|
fs.closeSync(descriptor);
|
|
@@ -4,10 +4,11 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import { creationRequestHash, creationIdValid } from "./native-creation-receipts.mjs";
|
|
6
6
|
import { homeDir } from "./platform.mjs";
|
|
7
|
+
import { fileIdentity, sameFileIdentity } from "./file-identity.mjs";
|
|
7
8
|
|
|
8
9
|
const ID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
9
10
|
const LIMIT = 256 * 1024;
|
|
10
|
-
const sameFile = (left, right) => ["dev", "ino", "size", "mode", "uid", "nlink", "
|
|
11
|
+
const sameFile = (left, right) => ["dev", "ino", "size", "mode", "uid", "nlink", "mtimeNs", "ctimeNs"].every(field => left[field] === right[field]);
|
|
11
12
|
|
|
12
13
|
function owner(context) {
|
|
13
14
|
const { sessionId, taskId } = context?.caller ?? {};
|
|
@@ -29,11 +30,11 @@ export class DesktopReplyReceipts {
|
|
|
29
30
|
file(id, create = false) {
|
|
30
31
|
if (typeof id !== "string" || !ID.test(id)) throw new Error("Invalid reply deliveryId");
|
|
31
32
|
if (create) fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
32
|
-
const info = fs.lstatSync(this.directory);
|
|
33
|
+
const info = fs.lstatSync(this.directory, { bigint: true });
|
|
33
34
|
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Unsafe reply receipt directory");
|
|
34
35
|
const canonical = fs.realpathSync.native(this.directory);
|
|
35
|
-
if (this.#boundary && (this.#boundary.path !== canonical || this.#boundary
|
|
36
|
-
this.#boundary ??= { path: canonical,
|
|
36
|
+
if (this.#boundary && (this.#boundary.path !== canonical || !sameFileIdentity(this.#boundary, fileIdentity(info)))) throw new Error("Reply receipt directory changed");
|
|
37
|
+
this.#boundary ??= { path: canonical, ...fileIdentity(info) };
|
|
37
38
|
return path.join(canonical, `${id}.json`);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -82,11 +83,11 @@ export class DesktopReplyReceipts {
|
|
|
82
83
|
|
|
83
84
|
read(id, context) {
|
|
84
85
|
const file = this.file(id);
|
|
85
|
-
const before = fs.lstatSync(file);
|
|
86
|
-
if (!before.isFile() || before.isSymbolicLink() || before.nlink !==
|
|
86
|
+
const before = fs.lstatSync(file, { bigint: true });
|
|
87
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > BigInt(LIMIT)) throw new Error("Unsafe reply receipt");
|
|
87
88
|
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
88
89
|
try {
|
|
89
|
-
const opened = fs.fstatSync(fd);
|
|
90
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
90
91
|
if (!opened.isFile() || !sameFile(before, opened)) throw new Error("Reply receipt changed while opening");
|
|
91
92
|
const buffer = Buffer.alloc(LIMIT + 1);
|
|
92
93
|
let length = 0;
|
|
@@ -96,9 +97,9 @@ export class DesktopReplyReceipts {
|
|
|
96
97
|
length += count;
|
|
97
98
|
if (length > LIMIT) throw new Error("Reply receipt exceeds its read limit");
|
|
98
99
|
}
|
|
99
|
-
const after = fs.fstatSync(fd);
|
|
100
|
-
const current = fs.lstatSync(this.file(id));
|
|
101
|
-
if (current.isSymbolicLink() || !sameFile(opened, after) || !sameFile(after, current) || length !== opened.size) throw new Error("Reply receipt changed while reading");
|
|
100
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
101
|
+
const current = fs.lstatSync(this.file(id), { bigint: true });
|
|
102
|
+
if (current.isSymbolicLink() || !sameFile(opened, after) || !sameFile(after, current) || BigInt(length) !== opened.size) throw new Error("Reply receipt changed while reading");
|
|
102
103
|
const receipt = JSON.parse(buffer.subarray(0, length).toString("utf8"));
|
|
103
104
|
const expected = owner(context);
|
|
104
105
|
if (receipt.version !== 1 || receipt.deliveryId !== id || !receipt.owner ||
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const exact = (value) => typeof value === "bigint" ? String(value)
|
|
2
|
+
: typeof value === "string" ? (/^-?\d+$/.test(value) ? value : null)
|
|
3
|
+
: Number.isSafeInteger(value) ? String(value) : null;
|
|
4
|
+
|
|
5
|
+
export function fileIdentity(stat) {
|
|
6
|
+
if (typeof stat?.dev !== "bigint" || typeof stat.ino !== "bigint") throw new TypeError("File identity requires bigint stats");
|
|
7
|
+
return { dev: String(stat.dev), ino: String(stat.ino) };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function sameFileIdentity(left, right) {
|
|
11
|
+
const values = [left?.dev, left?.ino, right?.dev, right?.ino].map(exact);
|
|
12
|
+
return values.every((value) => value !== null) && values[0] === values[2] && values[1] === values[3];
|
|
13
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { PROJECT_SETUP_TOOLS, registerProjectOnboardingTools } from "./project-o
|
|
|
40
40
|
|
|
41
41
|
exitForVersionRequest(import.meta.url);
|
|
42
42
|
|
|
43
|
-
const VERSION = "1.22.
|
|
43
|
+
const VERSION = "1.22.8";
|
|
44
44
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
45
45
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
46
46
|
|
|
@@ -3,8 +3,9 @@ import path from "node:path";
|
|
|
3
3
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { homeDir } from "./platform.mjs";
|
|
6
|
+
import { fileIdentity, sameFileIdentity } from "./file-identity.mjs";
|
|
6
7
|
|
|
7
|
-
const sameFile = (left, right) => ["dev", "ino", "size", "mode", "uid", "nlink", "
|
|
8
|
+
const sameFile = (left, right) => ["dev", "ino", "size", "mode", "uid", "nlink", "mtimeNs", "ctimeNs"].every(field => left[field] === right[field]);
|
|
8
9
|
|
|
9
10
|
export const creationIdValid = value => typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(value);
|
|
10
11
|
export function creationRequestHash(args) {
|
|
@@ -31,23 +32,23 @@ export class NativeCreationReceipts {
|
|
|
31
32
|
if (process.platform === "win32") this.#protectWindowsPrivate(this.#keyDirectory, true);
|
|
32
33
|
} catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
33
34
|
}
|
|
34
|
-
const directory = fs.lstatSync(this.#keyDirectory);
|
|
35
|
+
const directory = fs.lstatSync(this.#keyDirectory, { bigint: true });
|
|
35
36
|
const canonical = fs.realpathSync.native(this.#keyDirectory);
|
|
36
37
|
if (!directory.isDirectory() || directory.isSymbolicLink() ||
|
|
37
|
-
process.platform !== "win32" && ((directory.mode &
|
|
38
|
-
this.#keyDirectoryBoundary && (this.#keyDirectoryBoundary.path !== canonical || this.#keyDirectoryBoundary
|
|
38
|
+
process.platform !== "win32" && ((directory.mode & 0o077n) || directory.uid !== BigInt(process.getuid())) ||
|
|
39
|
+
this.#keyDirectoryBoundary && (this.#keyDirectoryBoundary.path !== canonical || !sameFileIdentity(this.#keyDirectoryBoundary, fileIdentity(directory)))) {
|
|
39
40
|
throw Error("Unsafe native creation authority directory");
|
|
40
41
|
}
|
|
41
42
|
if (process.platform === "win32" && !this.#keyDirectoryBoundary) {
|
|
42
43
|
this.#assertWindowsPrivate(canonical, true);
|
|
43
44
|
}
|
|
44
|
-
this.#keyDirectoryBoundary ??= { path: canonical,
|
|
45
|
+
this.#keyDirectoryBoundary ??= { path: canonical, ...fileIdentity(directory) };
|
|
45
46
|
const file = path.join(canonical, "signing-key");
|
|
46
47
|
if (create) {
|
|
47
48
|
let created, createdBoundary;
|
|
48
49
|
try {
|
|
49
50
|
created = fs.openSync(file, "wx", 0o600);
|
|
50
|
-
createdBoundary = fs.fstatSync(created);
|
|
51
|
+
createdBoundary = fs.fstatSync(created, { bigint: true });
|
|
51
52
|
if (process.platform === "win32") this.#protectWindowsPrivate(file, false);
|
|
52
53
|
fs.writeFileSync(created, randomBytes(32));
|
|
53
54
|
fs.fsyncSync(created);
|
|
@@ -56,32 +57,32 @@ export class NativeCreationReceipts {
|
|
|
56
57
|
if (created !== undefined) {
|
|
57
58
|
fs.closeSync(created);
|
|
58
59
|
created = undefined;
|
|
59
|
-
const failed = fs.lstatSync(file);
|
|
60
|
-
if (failed
|
|
60
|
+
const failed = fs.lstatSync(file, { bigint: true });
|
|
61
|
+
if (sameFileIdentity(fileIdentity(failed), fileIdentity(createdBoundary)) && !failed.isSymbolicLink()) fs.unlinkSync(file);
|
|
61
62
|
}
|
|
62
63
|
if (error.code !== "EEXIST") throw error;
|
|
63
64
|
}
|
|
64
65
|
finally { if (created !== undefined) fs.closeSync(created); }
|
|
65
66
|
}
|
|
66
|
-
const before = fs.lstatSync(file);
|
|
67
|
-
if (!before.isFile() || before.isSymbolicLink() || before.nlink !==
|
|
68
|
-
process.platform !== "win32" && ((before.mode &
|
|
67
|
+
const before = fs.lstatSync(file, { bigint: true });
|
|
68
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size !== 32n ||
|
|
69
|
+
process.platform !== "win32" && ((before.mode & 0o077n) || before.uid !== BigInt(process.getuid()))) throw Error("Unsafe native creation signing key");
|
|
69
70
|
if (process.platform === "win32" && !this.#keyBoundary) this.#assertWindowsPrivate(file, false);
|
|
70
71
|
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
71
72
|
let key;
|
|
72
73
|
try {
|
|
73
|
-
const opened = fs.fstatSync(fd);
|
|
74
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
74
75
|
if (!opened.isFile() || !sameFile(before, opened)) throw Error("Native creation signing key changed");
|
|
75
76
|
const bytes = Buffer.alloc(33);
|
|
76
77
|
let count = 0, read;
|
|
77
78
|
while (count < bytes.length && (read = fs.readSync(fd, bytes, count, bytes.length - count, count))) count += read;
|
|
78
79
|
key = bytes.subarray(0, count);
|
|
79
|
-
const after = fs.fstatSync(fd);
|
|
80
|
-
const current = fs.lstatSync(file);
|
|
80
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
81
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
81
82
|
if (key.length !== 32 || current.isSymbolicLink() || !sameFile(opened, after) || !sameFile(after, current)) throw Error("Native creation signing key changed");
|
|
82
83
|
} finally { fs.closeSync(fd); }
|
|
83
|
-
if (this.#keyBoundary && (this.#keyBoundary
|
|
84
|
-
this.#keyBoundary ??=
|
|
84
|
+
if (this.#keyBoundary && (!sameFileIdentity(this.#keyBoundary, fileIdentity(before)) || !timingSafeEqual(this.#key, key))) throw Error("Native creation signing key changed");
|
|
85
|
+
this.#keyBoundary ??= fileIdentity(before);
|
|
85
86
|
this.#key ??= key;
|
|
86
87
|
return this.#key;
|
|
87
88
|
}
|
|
@@ -110,11 +111,11 @@ export class NativeCreationReceipts {
|
|
|
110
111
|
file(id) {
|
|
111
112
|
if (!creationIdValid(id)) throw Error("Invalid native creation receipt id");
|
|
112
113
|
fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
113
|
-
const st = fs.lstatSync(this.directory);
|
|
114
|
+
const st = fs.lstatSync(this.directory, { bigint: true });
|
|
114
115
|
if (!st.isDirectory() || st.isSymbolicLink()) throw Error("Unsafe native creation receipt directory");
|
|
115
116
|
const canonical = fs.realpathSync.native(this.directory);
|
|
116
|
-
if (this.boundary && (this.boundary.path !== canonical || this.boundary
|
|
117
|
-
this.boundary ??= { path: canonical,
|
|
117
|
+
if (this.boundary && (this.boundary.path !== canonical || !sameFileIdentity(this.boundary, fileIdentity(st)))) throw Error("Native creation receipt directory changed");
|
|
118
|
+
this.boundary ??= { path: canonical, ...fileIdentity(st) };
|
|
118
119
|
this.directory = canonical;
|
|
119
120
|
return path.join(canonical, id + ".json");
|
|
120
121
|
}
|
|
@@ -122,18 +123,18 @@ export class NativeCreationReceipts {
|
|
|
122
123
|
const file = this.file(id);
|
|
123
124
|
let fd, receiptFound = false;
|
|
124
125
|
try {
|
|
125
|
-
const before = fs.lstatSync(file);
|
|
126
|
+
const before = fs.lstatSync(file, { bigint: true });
|
|
126
127
|
receiptFound = true;
|
|
127
|
-
if (!before.isFile() || before.isSymbolicLink() || before.nlink !==
|
|
128
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > 1024n * 1024n) throw Error("Unsafe native creation receipt");
|
|
128
129
|
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
129
|
-
const opened = fs.fstatSync(fd);
|
|
130
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
130
131
|
if (!opened.isFile() || !sameFile(before, opened)) throw Error("Native creation receipt changed");
|
|
131
132
|
const bytes = Buffer.alloc(1024 * 1024 + 1);
|
|
132
133
|
let count = 0, n;
|
|
133
134
|
while ((n = fs.readSync(fd, bytes, count, bytes.length - count, count))) { count += n; if (count > 1024 * 1024) throw Error("Oversized native creation receipt"); }
|
|
134
|
-
const after = fs.fstatSync(fd);
|
|
135
|
-
const current = fs.lstatSync(this.file(id));
|
|
136
|
-
if (current.isSymbolicLink() || !sameFile(opened, after) || !sameFile(after, current) || count !== opened.size) throw Error("Native creation receipt changed");
|
|
135
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
136
|
+
const current = fs.lstatSync(this.file(id), { bigint: true });
|
|
137
|
+
if (current.isSymbolicLink() || !sameFile(opened, after) || !sameFile(after, current) || BigInt(count) !== opened.size) throw Error("Native creation receipt changed");
|
|
137
138
|
const row = JSON.parse(bytes.subarray(0, count).toString());
|
|
138
139
|
if (row.version !== 1 || row.id !== id || row.hash !== hash || creationRequestHash(row.args) !== hash ||
|
|
139
140
|
!accounts || ["claude", "codex"].some(k => !accounts[k] || row.accounts?.[k] !== accounts[k]) ||
|
|
@@ -38,7 +38,7 @@ import { NativeCreationReceipts, creationIdValid } from "./native-creation-recei
|
|
|
38
38
|
|
|
39
39
|
exitForVersionRequest(import.meta.url);
|
|
40
40
|
|
|
41
|
-
const VERSION = "1.22.
|
|
41
|
+
const VERSION = "1.22.8";
|
|
42
42
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
43
43
|
|
|
44
44
|
function errorResponse(code, message, sent, details) {
|
package/src/project-scope.mjs
CHANGED
|
@@ -15,23 +15,23 @@ function samePath(left, right) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function metadata(file) {
|
|
18
|
-
const entry = fs.lstatSync(file);
|
|
19
|
-
if (!entry.isFile() || entry.isSymbolicLink() || entry.size >
|
|
18
|
+
const entry = fs.lstatSync(file, { bigint: true });
|
|
19
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 4096n) throw new Error("Invalid project repository metadata");
|
|
20
20
|
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
21
21
|
try {
|
|
22
|
-
const before = fs.fstatSync(descriptor);
|
|
23
|
-
const fields = ["dev", "ino", "size", "
|
|
22
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
23
|
+
const fields = ["dev", "ino", "size", "mtimeNs", "ctimeNs"];
|
|
24
24
|
const matches = (left, right) => fields.every((field) => left[field] === right[field]);
|
|
25
25
|
if (!before.isFile() || !matches(entry, before)) throw new Error("Project repository metadata changed during access");
|
|
26
|
-
const data = Buffer.alloc(before.size);
|
|
26
|
+
const data = Buffer.alloc(Number(before.size));
|
|
27
27
|
let offset = 0;
|
|
28
28
|
while (offset < data.length) {
|
|
29
29
|
const count = fs.readSync(descriptor, data, offset, data.length - offset, offset);
|
|
30
30
|
if (!count) throw new Error("Project repository metadata changed during access");
|
|
31
31
|
offset += count;
|
|
32
32
|
}
|
|
33
|
-
const after = fs.fstatSync(descriptor);
|
|
34
|
-
const current = fs.lstatSync(file);
|
|
33
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
34
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
35
35
|
if (!current.isFile() || current.isSymbolicLink() || !matches(before, after) || !matches(after, current)) throw new Error("Project repository metadata changed during access");
|
|
36
36
|
return data.toString("utf8").trim();
|
|
37
37
|
} finally {
|
package/src/rollout-reader.mjs
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { fileIdentity } from "./file-identity.mjs";
|
|
2
3
|
|
|
3
4
|
const CHUNK_BYTES = 256 * 1024;
|
|
4
|
-
const VERSION_FIELDS = ["size", "
|
|
5
|
+
const VERSION_FIELDS = ["size", "mtimeNs", "ctimeNs", "ino", "dev"];
|
|
5
6
|
const sameVersion = (a, b) => VERSION_FIELDS.every((key) => a[key] === b[key]);
|
|
6
7
|
|
|
7
8
|
// Scan the complete history, but retain at most one bounded JSONL record.
|
|
8
9
|
// Skipping the middle of a rollout would miss duplicate identities or corruption.
|
|
9
10
|
export function scanRollout(file, { maxRecordBytes, onRecord, onChunk = () => {} }) {
|
|
10
11
|
if (!Number.isSafeInteger(maxRecordBytes) || maxRecordBytes < 1) throw new Error("The rollout record read limit is invalid");
|
|
11
|
-
const named = fs.lstatSync(file);
|
|
12
|
+
const named = fs.lstatSync(file, { bigint: true });
|
|
12
13
|
if (!named.isFile() || named.isSymbolicLink()) throw new Error("The rollout is not a regular file");
|
|
13
14
|
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
14
15
|
try {
|
|
15
|
-
const before = fs.fstatSync(descriptor);
|
|
16
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
16
17
|
if (!before.isFile()) throw new Error("The opened rollout is not a regular file");
|
|
17
18
|
if (!sameVersion(named, before)) throw new Error("The rollout changed while opening");
|
|
18
|
-
if (before.size
|
|
19
|
+
if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("The rollout exceeds the safe size range");
|
|
20
|
+
const size = Number(before.size);
|
|
21
|
+
if (size === 0) throw new Error("The rollout is empty (0 bytes)");
|
|
19
22
|
let parts = [], length = 0, start = 0, position = 0;
|
|
20
23
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
21
|
-
while (position <
|
|
22
|
-
const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES,
|
|
24
|
+
while (position < size) {
|
|
25
|
+
const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, size - position));
|
|
23
26
|
const count = fs.readSync(descriptor, buffer, 0, buffer.length, position);
|
|
24
27
|
if (!count) throw new Error("The rollout changed while reading");
|
|
25
28
|
const chunk = buffer.subarray(0, count);
|
|
@@ -29,7 +32,7 @@ export function scanRollout(file, { maxRecordBytes, onRecord, onChunk = () => {}
|
|
|
29
32
|
const newline = chunk.indexOf(0x0a, offset);
|
|
30
33
|
const end = newline < 0 ? count : newline;
|
|
31
34
|
length += end - offset;
|
|
32
|
-
if (length > maxRecordBytes) throw new Error(`The rollout record at byte ${start} exceeds the ${maxRecordBytes}-byte per-record read limit (file: ${
|
|
35
|
+
if (length > maxRecordBytes) throw new Error(`The rollout record at byte ${start} exceeds the ${maxRecordBytes}-byte per-record read limit (file: ${size} bytes)`);
|
|
33
36
|
parts.push(chunk.subarray(offset, end));
|
|
34
37
|
if (newline < 0) break;
|
|
35
38
|
if (length) {
|
|
@@ -45,14 +48,14 @@ export function scanRollout(file, { maxRecordBytes, onRecord, onChunk = () => {}
|
|
|
45
48
|
}
|
|
46
49
|
position += count;
|
|
47
50
|
}
|
|
48
|
-
if (start !==
|
|
49
|
-
const after = fs.fstatSync(descriptor);
|
|
50
|
-
const current = fs.lstatSync(file);
|
|
51
|
+
if (start !== size) throw new Error("The rollout has an incomplete final record");
|
|
52
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
53
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
51
54
|
if (!current.isFile() || current.isSymbolicLink() || !sameVersion(before, after) || !sameVersion(after, current)) {
|
|
52
55
|
throw new Error("The rollout changed while reading");
|
|
53
56
|
}
|
|
54
|
-
return { size
|
|
55
|
-
version: Object.fromEntries(VERSION_FIELDS.map((key) => [key, before[key]])) };
|
|
57
|
+
return { size, identity: fileIdentity(before),
|
|
58
|
+
version: Object.fromEntries(VERSION_FIELDS.map((key) => [key, String(before[key])])) };
|
|
56
59
|
} finally {
|
|
57
60
|
fs.closeSync(descriptor);
|
|
58
61
|
}
|
package/src/rollout-segments.mjs
CHANGED
|
@@ -59,7 +59,7 @@ export function assertRolloutSetStable(sessions, threadId, snapshots) {
|
|
|
59
59
|
const files = findRolloutSegments(sessions, threadId);
|
|
60
60
|
if (files.length !== snapshots.length || files.some((file, i) => file !== snapshots[i].file)) throw new Error("The rollout segment set changed while reading");
|
|
61
61
|
for (const { file, version } of snapshots) {
|
|
62
|
-
const current = fs.lstatSync(file);
|
|
63
|
-
if (!current.isFile() || current.isSymbolicLink() || Object.entries(version).some(([key, value]) => current[key] !== value)) throw new Error("A rollout segment changed while reading");
|
|
62
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
63
|
+
if (!current.isFile() || current.isSymbolicLink() || Object.entries(version).some(([key, value]) => typeof value !== "string" || String(current[key]) !== value)) throw new Error("A rollout segment changed while reading");
|
|
64
64
|
}
|
|
65
65
|
}
|
package/src/sites-connector.mjs
CHANGED
|
@@ -49,24 +49,24 @@ export function validateSitesConfig(value, { allowLoopback = false } = {}) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
async function readPrivateJson(file, maxBytes = 2 * 1024 * 1024) {
|
|
52
|
-
const info = await fs.lstat(file);
|
|
53
|
-
if (!info.isFile() || info.isSymbolicLink() || info.nlink !==
|
|
52
|
+
const info = await fs.lstat(file, { bigint: true });
|
|
53
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1n || info.size > BigInt(maxBytes)) throw new Error("Private connector file is unsafe or oversized");
|
|
54
54
|
const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
55
55
|
try {
|
|
56
|
-
const current = await handle.stat();
|
|
57
|
-
const fields = ["dev", "ino", "size", "
|
|
56
|
+
const current = await handle.stat({ bigint: true });
|
|
57
|
+
const fields = ["dev", "ino", "size", "mtimeNs", "ctimeNs"];
|
|
58
58
|
const matches = (left, right) => fields.every((field) => left[field] === right[field]);
|
|
59
|
-
if (!current.isFile() || current.nlink !==
|
|
60
|
-
const data = Buffer.alloc(current.size);
|
|
59
|
+
if (!current.isFile() || current.nlink !== 1n || current.size > BigInt(maxBytes) || !matches(info, current)) throw new Error("Private connector file changed during access");
|
|
60
|
+
const data = Buffer.alloc(Number(current.size));
|
|
61
61
|
let offset = 0;
|
|
62
62
|
while (offset < data.length) {
|
|
63
63
|
const { bytesRead } = await handle.read(data, offset, data.length - offset, offset);
|
|
64
64
|
if (!bytesRead) throw new Error("Private connector file changed during access");
|
|
65
65
|
offset += bytesRead;
|
|
66
66
|
}
|
|
67
|
-
const after = await handle.stat();
|
|
68
|
-
const entry = await fs.lstat(file);
|
|
69
|
-
if (!entry.isFile() || entry.isSymbolicLink() || entry.nlink !==
|
|
67
|
+
const after = await handle.stat({ bigint: true });
|
|
68
|
+
const entry = await fs.lstat(file, { bigint: true });
|
|
69
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.nlink !== 1n || !matches(current, after) || !matches(after, entry)) throw new Error("Private connector file changed during access");
|
|
70
70
|
return JSON.parse(data.toString("utf8"));
|
|
71
71
|
} catch (error) {
|
|
72
72
|
if (error.code === "ENOENT") throw new Error("Private connector file changed during access", { cause: error });
|