@youdie006/prodex 0.38.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/store-writer.js +210 -0
- package/dist/store.js +146 -8
- package/package.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Write a bridge record into a directory the kernel has pinned for us.
|
|
2
|
+
//
|
|
3
|
+
// The store's normal path renders an open directory handle as /proc/self/fd/N
|
|
4
|
+
// and joins the file name onto it, so the write lands in the directory that was
|
|
5
|
+
// validated rather than in one a symlink swap redirected. macOS has no
|
|
6
|
+
// traversable equivalent - /dev/fd/N stands in for the descriptor, not for a
|
|
7
|
+
// walkable directory - so that path fails there with a bare ENOENT and takes the
|
|
8
|
+
// whole ledger with it.
|
|
9
|
+
//
|
|
10
|
+
// This process buys the same guarantee a different way. It is spawned with its
|
|
11
|
+
// cwd already set to the directory to descend from, it refuses to continue
|
|
12
|
+
// unless "." is the very inode the parent validated, and it descends by chdir
|
|
13
|
+
// one no-symlink segment at a time, re-checking the inode after each step. The
|
|
14
|
+
// kernel holds the cwd's vnode, so once a step is confirmed nothing can redirect
|
|
15
|
+
// the relative paths that follow. That is precisely what the fd path bought.
|
|
16
|
+
//
|
|
17
|
+
// Everything after the anchoring uses the same helpers the in-process path uses,
|
|
18
|
+
// on relative names.
|
|
19
|
+
import { closeSync, constants, fstatSync, openSync } from "node:fs";
|
|
20
|
+
import { pathToFileURL } from "node:url";
|
|
21
|
+
import { link, lstat, readdir, rename, rm } from "node:fs/promises";
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
import { writeVerifiedUtf8File } from "./safe-file.js";
|
|
24
|
+
function identityOfOpenDirectory(fd) {
|
|
25
|
+
const stat = fstatSync(fd, { bigint: true });
|
|
26
|
+
if (!stat.isDirectory())
|
|
27
|
+
throw new Error("Anchored writer expected a directory");
|
|
28
|
+
return { dev: stat.dev.toString(), ino: stat.ino.toString() };
|
|
29
|
+
}
|
|
30
|
+
function sameIdentity(a, b) {
|
|
31
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
32
|
+
}
|
|
33
|
+
/** Open a name relative to the cwd, refusing symlinks and non-directories. */
|
|
34
|
+
function openDirectoryHere(name) {
|
|
35
|
+
const directoryFlag = typeof constants.O_DIRECTORY === "number" ? constants.O_DIRECTORY : 0;
|
|
36
|
+
const noFollowFlag = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
37
|
+
return openSync(name, constants.O_RDONLY | directoryFlag | noFollowFlag);
|
|
38
|
+
}
|
|
39
|
+
function currentDirectoryIdentity() {
|
|
40
|
+
const fd = openDirectoryHere(".");
|
|
41
|
+
try {
|
|
42
|
+
return identityOfOpenDirectory(fd);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Confirm the cwd is the directory the parent meant, then descend the segments
|
|
50
|
+
* so that the cwd ends up pinned to the directory the record belongs in.
|
|
51
|
+
*/
|
|
52
|
+
export function anchorCurrentDirectory(anchor, segments) {
|
|
53
|
+
let here = currentDirectoryIdentity();
|
|
54
|
+
if (!sameIdentity(here, anchor)) {
|
|
55
|
+
throw new Error("Anchored writer was not started in the directory the caller validated");
|
|
56
|
+
}
|
|
57
|
+
for (const segment of segments) {
|
|
58
|
+
if (segment.length === 0 || segment === "." || segment === ".." || segment.includes("/")) {
|
|
59
|
+
throw new Error(`Anchored writer refuses to descend into ${JSON.stringify(segment)}`);
|
|
60
|
+
}
|
|
61
|
+
// O_NOFOLLOW proves the name is a real directory rather than a symlink, and
|
|
62
|
+
// the identity check after chdir proves we landed on that same directory
|
|
63
|
+
// and not on something swapped in between the two calls.
|
|
64
|
+
const fd = openDirectoryHere(segment);
|
|
65
|
+
let expected;
|
|
66
|
+
try {
|
|
67
|
+
expected = identityOfOpenDirectory(fd);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
process.chdir(segment);
|
|
73
|
+
here = currentDirectoryIdentity();
|
|
74
|
+
if (!sameIdentity(here, expected)) {
|
|
75
|
+
throw new Error(`Anchored writer landed somewhere other than ${JSON.stringify(segment)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return here;
|
|
79
|
+
}
|
|
80
|
+
async function assertRegularFileIfExists(name) {
|
|
81
|
+
try {
|
|
82
|
+
const stat = await lstat(name);
|
|
83
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
84
|
+
throw new Error("Bridge record path must be a regular file and must not be a symlink");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code !== "ENOENT")
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function temporaryName(fileName) {
|
|
93
|
+
return `.${fileName}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
94
|
+
}
|
|
95
|
+
export async function runAnchoredJob(job) {
|
|
96
|
+
const pinned = anchorCurrentDirectory(job.anchor, job.segments);
|
|
97
|
+
const stillPinned = async () => {
|
|
98
|
+
if (!sameIdentity(currentDirectoryIdentity(), pinned)) {
|
|
99
|
+
throw new Error("Anchored writer's directory changed underneath it");
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const { fileName } = job;
|
|
103
|
+
if (fileName.length === 0 || fileName.includes("/") || fileName === "." || fileName === "..") {
|
|
104
|
+
throw new Error(`Anchored writer refuses the file name ${JSON.stringify(fileName)}`);
|
|
105
|
+
}
|
|
106
|
+
if (job.op === "deleteIfPresent") {
|
|
107
|
+
let stat;
|
|
108
|
+
try {
|
|
109
|
+
stat = await lstat(fileName);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === "ENOENT")
|
|
113
|
+
return { ok: true };
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
117
|
+
throw new Error("Bridge record path must be a regular file and must not be a symlink");
|
|
118
|
+
}
|
|
119
|
+
await rm(fileName, { force: true });
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}
|
|
122
|
+
if (job.op === "cleanupTempHardLinks") {
|
|
123
|
+
let targetStat;
|
|
124
|
+
try {
|
|
125
|
+
targetStat = await lstat(fileName);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error.code === "ENOENT")
|
|
129
|
+
return { ok: true };
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
if (targetStat.isSymbolicLink() || !targetStat.isFile() || targetStat.nlink <= 1)
|
|
133
|
+
return { ok: true };
|
|
134
|
+
const prefix = `.${fileName}.`;
|
|
135
|
+
for (const entry of await readdir(".", { withFileTypes: true })) {
|
|
136
|
+
if (!entry.isFile() || !entry.name.startsWith(prefix) || !entry.name.endsWith(".tmp"))
|
|
137
|
+
continue;
|
|
138
|
+
const tempStat = await lstat(entry.name).catch(() => undefined);
|
|
139
|
+
if (!tempStat?.isFile() || tempStat.isSymbolicLink())
|
|
140
|
+
continue;
|
|
141
|
+
if (tempStat.dev === targetStat.dev && tempStat.ino === targetStat.ino) {
|
|
142
|
+
await rm(entry.name, { force: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { ok: true };
|
|
146
|
+
}
|
|
147
|
+
const content = job.content ?? "";
|
|
148
|
+
const tmpName = temporaryName(fileName);
|
|
149
|
+
if (job.op === "writeByRename") {
|
|
150
|
+
await assertRegularFileIfExists(fileName);
|
|
151
|
+
try {
|
|
152
|
+
await writeVerifiedUtf8File(tmpName, content, stillPinned, { create: true, mode: job.mode });
|
|
153
|
+
await rename(tmpName, fileName);
|
|
154
|
+
await assertRegularFileIfExists(fileName);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
await rm(tmpName, { force: true }).catch(() => undefined);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
// linkIfAbsent: the hard link is what makes "create only if absent" atomic.
|
|
163
|
+
let linked = false;
|
|
164
|
+
try {
|
|
165
|
+
await writeVerifiedUtf8File(tmpName, content, stillPinned, { create: true, exclusive: true, mode: job.mode });
|
|
166
|
+
try {
|
|
167
|
+
await link(tmpName, fileName);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error.code === "EEXIST") {
|
|
171
|
+
await rm(tmpName, { force: true }).catch(() => undefined);
|
|
172
|
+
return { ok: true, created: false };
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
linked = true;
|
|
177
|
+
await rm(tmpName, { force: true });
|
|
178
|
+
await assertRegularFileIfExists(fileName);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (!linked)
|
|
182
|
+
await rm(tmpName, { force: true }).catch(() => undefined);
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, created: true };
|
|
186
|
+
}
|
|
187
|
+
async function readAllStdin() {
|
|
188
|
+
const chunks = [];
|
|
189
|
+
for await (const chunk of process.stdin)
|
|
190
|
+
chunks.push(Buffer.from(chunk));
|
|
191
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
192
|
+
}
|
|
193
|
+
async function main() {
|
|
194
|
+
let outcome;
|
|
195
|
+
try {
|
|
196
|
+
outcome = await runAnchoredJob(JSON.parse(await readAllStdin()));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const maybe = error;
|
|
200
|
+
outcome = { ok: false, error: maybe.message ?? String(error), code: maybe.code };
|
|
201
|
+
}
|
|
202
|
+
process.stdout.write(`${JSON.stringify(outcome)}\n`);
|
|
203
|
+
if (!outcome.ok)
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
}
|
|
206
|
+
// Only run when this file is the entry point, so the exported pieces stay
|
|
207
|
+
// importable from tests without the process trying to read stdin.
|
|
208
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
209
|
+
await main();
|
|
210
|
+
}
|
package/dist/store.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { registerBridgeRoot } from "./registry.js";
|
|
3
|
-
import { constants, existsSync } from "node:fs";
|
|
4
|
+
import { closeSync, constants, existsSync, openSync, readdirSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
4
6
|
import { link, lstat, mkdir, open, readdir, realpath, rename, rm, stat } from "node:fs/promises";
|
|
5
7
|
import path from "node:path";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
10
|
import { assertRepoRelativePath } from "./repo.js";
|
|
7
11
|
import { readVerifiedUtf8File, writeVerifiedUtf8File } from "./safe-file.js";
|
|
8
12
|
import { makeBridgeId, nowIso, ReceiptSchema, ResultSchema, SCHEMA_VERSION, SessionSchema, TaskSchema } from "./schema.js";
|
|
@@ -656,6 +660,16 @@ export class BridgeStore {
|
|
|
656
660
|
const artifactPath = this.resolveArtifactPath(relativePath);
|
|
657
661
|
const parentPath = path.dirname(artifactPath);
|
|
658
662
|
await this.assertArtifactParentDirectory(parentPath);
|
|
663
|
+
if (!hasStableDirectoryFdPaths()) {
|
|
664
|
+
const segments = path.relative(this.bridgeDir, parentPath).split(path.sep).filter((part) => part.length > 0);
|
|
665
|
+
await runAnchoredWrite(this.bridgeDir, segments, {
|
|
666
|
+
op: "deleteIfPresent",
|
|
667
|
+
fileName: path.basename(artifactPath),
|
|
668
|
+
mode: BRIDGE_FILE_MODE
|
|
669
|
+
});
|
|
670
|
+
await this.assertArtifactParentDirectory(parentPath);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
659
673
|
const parentHandle = await openNoFollowDirectory(parentPath, "Artifact directory");
|
|
660
674
|
try {
|
|
661
675
|
const targetPath = path.join(directoryFdPath(parentHandle.fd), path.basename(artifactPath));
|
|
@@ -975,13 +989,31 @@ export class BridgeStore {
|
|
|
975
989
|
await this.writeTextByStableStorageRename(kind, filePath, content);
|
|
976
990
|
return;
|
|
977
991
|
}
|
|
978
|
-
|
|
992
|
+
await this.runAnchoredRecordJob(kind, filePath, { op: "writeByRename", content });
|
|
979
993
|
}
|
|
980
994
|
async writeTextByCreateExclusive(kind, filePath, content) {
|
|
981
995
|
if (hasStableDirectoryFdPaths()) {
|
|
982
996
|
return await this.writeTextByStableStorageLinkIfAbsent(kind, filePath, content);
|
|
983
997
|
}
|
|
984
|
-
|
|
998
|
+
const outcome = await this.runAnchoredRecordJob(kind, filePath, { op: "linkIfAbsent", content });
|
|
999
|
+
return outcome.created !== false;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Run one record operation in the anchored child, with the same storage
|
|
1003
|
+
* directory checks the in-process path makes on either side of it.
|
|
1004
|
+
*/
|
|
1005
|
+
async runAnchoredRecordJob(kind, filePath, job) {
|
|
1006
|
+
if (path.dirname(filePath) !== this.dir(kind)) {
|
|
1007
|
+
throw new Error(`Bridge record path must stay under .bridge/${kind}`);
|
|
1008
|
+
}
|
|
1009
|
+
await this.assertStorageDirIsRealDirectory(kind);
|
|
1010
|
+
const outcome = await runAnchoredWrite(this.bridgeDir, [kind], {
|
|
1011
|
+
...job,
|
|
1012
|
+
fileName: path.basename(filePath),
|
|
1013
|
+
mode: BRIDGE_FILE_MODE
|
|
1014
|
+
});
|
|
1015
|
+
await this.assertStorageDirIsRealDirectory(kind);
|
|
1016
|
+
return outcome.ok ? outcome : {};
|
|
985
1017
|
}
|
|
986
1018
|
async deleteRecordIfPresent(kind, id) {
|
|
987
1019
|
const filePath = this.pathFor(kind, id);
|
|
@@ -990,6 +1022,10 @@ export class BridgeStore {
|
|
|
990
1022
|
if (path.dirname(filePath) !== expectedDir) {
|
|
991
1023
|
throw new Error(`Bridge record path must stay under .bridge/${kind}`);
|
|
992
1024
|
}
|
|
1025
|
+
if (!hasStableDirectoryFdPaths()) {
|
|
1026
|
+
await this.runAnchoredRecordJob(kind, filePath, { op: "deleteIfPresent" });
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
993
1029
|
const bridgeHandle = await openNoFollowDirectory(this.bridgeDir, "Bridge directory");
|
|
994
1030
|
try {
|
|
995
1031
|
const storageHandle = await openNoFollowDirectory(path.join(directoryFdPath(bridgeHandle.fd), kind), `Bridge storage directory .bridge/${kind}`);
|
|
@@ -1115,6 +1151,10 @@ export class BridgeStore {
|
|
|
1115
1151
|
if (path.dirname(filePath) !== expectedDir) {
|
|
1116
1152
|
throw new Error(`Bridge record path must stay under .bridge/${kind}`);
|
|
1117
1153
|
}
|
|
1154
|
+
if (!hasStableDirectoryFdPaths()) {
|
|
1155
|
+
await this.runAnchoredRecordJob(kind, filePath, { op: "cleanupTempHardLinks" });
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1118
1158
|
const bridgeHandle = await openNoFollowDirectory(this.bridgeDir, "Bridge directory");
|
|
1119
1159
|
try {
|
|
1120
1160
|
const storageHandle = await openNoFollowDirectory(path.join(directoryFdPath(bridgeHandle.fd), kind), `Bridge storage directory .bridge/${kind}`);
|
|
@@ -1552,14 +1592,112 @@ function directoryFdPath(fd) {
|
|
|
1552
1592
|
}
|
|
1553
1593
|
return `${base}/${fd}`;
|
|
1554
1594
|
}
|
|
1595
|
+
// Whether a base can be USED, not merely whether it exists. macOS has /dev/fd,
|
|
1596
|
+
// so an existence check accepted it, and every record write then failed with
|
|
1597
|
+
// ENOENT on a path like /dev/fd/12/receipts - measured there, /dev/fd exists and
|
|
1598
|
+
// is not traversable, which took down the whole bridge write surface while the
|
|
1599
|
+
// same checks pass on Linux. Existence was never the property being relied on.
|
|
1600
|
+
let directoryFdBaseProbe;
|
|
1601
|
+
function probeDirectoryFdBase() {
|
|
1602
|
+
for (const base of ["/proc/self/fd", "/dev/fd"]) {
|
|
1603
|
+
if (!existsSync(base))
|
|
1604
|
+
continue;
|
|
1605
|
+
let fd;
|
|
1606
|
+
try {
|
|
1607
|
+
fd = openSync(tmpdir(), constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
|
1608
|
+
// Reading the directory THROUGH the rendered path is exactly what the
|
|
1609
|
+
// writes do, so that is what gets tested.
|
|
1610
|
+
readdirSync(`${base}/${fd}`);
|
|
1611
|
+
return base;
|
|
1612
|
+
}
|
|
1613
|
+
catch {
|
|
1614
|
+
// this base cannot be walked here; try the next
|
|
1615
|
+
}
|
|
1616
|
+
finally {
|
|
1617
|
+
if (fd !== undefined) {
|
|
1618
|
+
try {
|
|
1619
|
+
closeSync(fd);
|
|
1620
|
+
}
|
|
1621
|
+
catch {
|
|
1622
|
+
// best effort
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return undefined;
|
|
1628
|
+
}
|
|
1629
|
+
/** Exported so a test can hold the probe to what the running platform can do. */
|
|
1630
|
+
export function directoryFdPathsUsable() {
|
|
1631
|
+
return hasStableDirectoryFdPaths();
|
|
1632
|
+
}
|
|
1633
|
+
// Running the write in a child process whose cwd the kernel has pinned is how
|
|
1634
|
+
// platforms without a traversable /proc/self/fd keep the guarantee the fd path
|
|
1635
|
+
// gives everywhere else. See src/store-writer.ts for what the child checks.
|
|
1636
|
+
function anchoredWriterCommand() {
|
|
1637
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1638
|
+
const built = path.join(here, "store-writer.js");
|
|
1639
|
+
if (existsSync(built))
|
|
1640
|
+
return { command: process.execPath, args: [built] };
|
|
1641
|
+
const source = path.join(here, "store-writer.ts");
|
|
1642
|
+
if (existsSync(source)) {
|
|
1643
|
+
// Only reachable when running from TypeScript sources. The child's cwd is
|
|
1644
|
+
// the bridge directory, so tsx has to be named by absolute URL or Node
|
|
1645
|
+
// looks for it next to the records.
|
|
1646
|
+
const tsx = pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href;
|
|
1647
|
+
return { command: process.execPath, args: ["--import", tsx, source] };
|
|
1648
|
+
}
|
|
1649
|
+
throw new Error("Bridge record writes need the anchored writer, which is missing from this installation.");
|
|
1650
|
+
}
|
|
1651
|
+
async function runAnchoredWrite(bridgeDir, segments, job) {
|
|
1652
|
+
const bridgeHandle = await openNoFollowDirectory(bridgeDir, "Bridge directory");
|
|
1653
|
+
let anchor;
|
|
1654
|
+
try {
|
|
1655
|
+
const stat = await bridgeHandle.stat({ bigint: true });
|
|
1656
|
+
anchor = { dev: stat.dev.toString(), ino: stat.ino.toString() };
|
|
1657
|
+
}
|
|
1658
|
+
finally {
|
|
1659
|
+
await bridgeHandle.close();
|
|
1660
|
+
}
|
|
1661
|
+
const { command, args } = anchoredWriterCommand();
|
|
1662
|
+
// cwd is resolved by path here, which is exactly the lookup an attacker could
|
|
1663
|
+
// redirect - so the child refuses to proceed unless the directory it landed in
|
|
1664
|
+
// is the inode just measured through the no-follow handle.
|
|
1665
|
+
const child = spawn(command, args, { cwd: bridgeDir, stdio: ["pipe", "pipe", "pipe"] });
|
|
1666
|
+
const stdout = [];
|
|
1667
|
+
const stderr = [];
|
|
1668
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
1669
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
1670
|
+
const finished = new Promise((resolve, reject) => {
|
|
1671
|
+
child.once("error", reject);
|
|
1672
|
+
child.once("close", () => resolve());
|
|
1673
|
+
});
|
|
1674
|
+
child.stdin.end(JSON.stringify({ ...job, anchor, segments }));
|
|
1675
|
+
await finished;
|
|
1676
|
+
const text = Buffer.concat(stdout).toString("utf8").trim();
|
|
1677
|
+
let outcome;
|
|
1678
|
+
try {
|
|
1679
|
+
outcome = text.length > 0 ? JSON.parse(text) : undefined;
|
|
1680
|
+
}
|
|
1681
|
+
catch {
|
|
1682
|
+
outcome = undefined;
|
|
1683
|
+
}
|
|
1684
|
+
if (!outcome) {
|
|
1685
|
+
const detail = Buffer.concat(stderr).toString("utf8").trim() || text || "no output";
|
|
1686
|
+
throw new Error(`Bridge record write helper failed: ${detail}`);
|
|
1687
|
+
}
|
|
1688
|
+
if (!outcome.ok) {
|
|
1689
|
+
const error = new Error(outcome.error);
|
|
1690
|
+
if (outcome.code)
|
|
1691
|
+
error.code = outcome.code;
|
|
1692
|
+
throw error;
|
|
1693
|
+
}
|
|
1694
|
+
return outcome;
|
|
1695
|
+
}
|
|
1555
1696
|
function directoryFdPathBase() {
|
|
1556
1697
|
if (storeTestHooks.disableDirectoryFdPaths)
|
|
1557
1698
|
return undefined;
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
if (existsSync("/dev/fd"))
|
|
1561
|
-
return "/dev/fd";
|
|
1562
|
-
return undefined;
|
|
1699
|
+
directoryFdBaseProbe ??= { base: probeDirectoryFdBase() };
|
|
1700
|
+
return directoryFdBaseProbe.base;
|
|
1563
1701
|
}
|
|
1564
1702
|
function assertBridgeRecordId(kind, id) {
|
|
1565
1703
|
if (!isBridgeRecordId(kind, id)) {
|