@openfairygui/backend 0.3.0 → 0.3.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/README.md +4 -2
- package/dist/index.cjs +89 -29
- package/dist/index.d.cts +30 -7
- package/dist/index.d.mts +30 -7
- package/dist/index.mjs +89 -29
- package/dist/node.cjs +277 -44
- package/dist/node.d.cts +29 -6
- package/dist/node.d.mts +29 -6
- package/dist/node.mjs +276 -44
- package/package.json +8 -5
- package/src/index.ts +3 -0
- package/src/node.ts +213 -14
- package/src/path-policy.ts +18 -0
- package/src/runtime/capabilities.ts +2 -2
- package/src/runtime/contracts.ts +35 -3
- package/src/runtime.ts +27 -8
- package/src/services/authoring-service.ts +3 -3
- package/src/services/context.ts +1 -0
- package/src/services/runtime-service.ts +53 -14
- package/src/services/session-project-writer.ts +31 -13
package/dist/node.cjs
CHANGED
|
@@ -23,8 +23,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let node_fs_promises = require("node:fs/promises");
|
|
25
25
|
node_fs_promises = __toESM(node_fs_promises);
|
|
26
|
+
let node_os = require("node:os");
|
|
27
|
+
node_os = __toESM(node_os);
|
|
26
28
|
let node_path = require("node:path");
|
|
27
29
|
node_path = __toESM(node_path);
|
|
30
|
+
let node_crypto = require("node:crypto");
|
|
28
31
|
let _openfairygui_core_uam = require("@openfairygui/core/uam");
|
|
29
32
|
let _openfairygui_functions_uam = require("@openfairygui/functions/uam");
|
|
30
33
|
let _openfairygui_core_project_io = require("@openfairygui/core/project-io");
|
|
@@ -59,6 +62,16 @@ function createRuntimePathPolicy() {
|
|
|
59
62
|
workspaceBoundary: "project-root-only"
|
|
60
63
|
};
|
|
61
64
|
}
|
|
65
|
+
async function assertProjectPathContained(fileSystem, projectRoot, targetPath) {
|
|
66
|
+
const [resolvedRoot, resolvedTarget] = await Promise.all([fileSystem.resolvePath(projectRoot), fileSystem.resolvePath(targetPath)]);
|
|
67
|
+
const root = normalizeComparablePath(resolvedRoot);
|
|
68
|
+
const target = normalizeComparablePath(resolvedTarget);
|
|
69
|
+
if (root === "." && !target.startsWith("/") && !/^[a-z]:\//i.test(target)) return;
|
|
70
|
+
if (target === root || target.startsWith(`${root}/`)) return;
|
|
71
|
+
const error = /* @__PURE__ */ new Error(`Project path escapes the opened root: ${targetPath}`);
|
|
72
|
+
error.code = "EACCES";
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
62
75
|
async function resolveFairyPath(fileSystem, input) {
|
|
63
76
|
const resolvedInput = fileSystem.resolve(input);
|
|
64
77
|
const stat = await fileSystem.stat(resolvedInput);
|
|
@@ -147,10 +160,14 @@ function failure(stage, startedAt, error, session, options) {
|
|
|
147
160
|
}
|
|
148
161
|
//#endregion
|
|
149
162
|
//#region src/services/session-project-writer.ts
|
|
150
|
-
function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
163
|
+
function createWriterFileSystem(fileSystem, projectRoot, writtenPaths, failedPaths) {
|
|
164
|
+
const contained = async (targetPath, operation) => {
|
|
165
|
+
await assertProjectPathContained(fileSystem, projectRoot, targetPath);
|
|
166
|
+
return operation();
|
|
167
|
+
};
|
|
151
168
|
async function trackWrite(targetPath, write) {
|
|
152
169
|
try {
|
|
153
|
-
const result = await write
|
|
170
|
+
const result = await contained(targetPath, write);
|
|
154
171
|
writtenPaths.push(targetPath);
|
|
155
172
|
return result;
|
|
156
173
|
} catch (error) {
|
|
@@ -159,8 +176,8 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
178
|
return {
|
|
162
|
-
readFile: (path) => fileSystem.readFile(path),
|
|
163
|
-
readFileRaw: (path) => fileSystem.readFileRaw(path),
|
|
179
|
+
readFile: (path) => contained(path, () => fileSystem.readFile(path)),
|
|
180
|
+
readFileRaw: (path) => contained(path, () => fileSystem.readFileRaw(path)),
|
|
164
181
|
writeFile: (path, content) => trackWrite(path, async () => {
|
|
165
182
|
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
166
183
|
await fileSystem.writeFile(path, content);
|
|
@@ -169,9 +186,10 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
|
169
186
|
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
170
187
|
await fileSystem.writeFileRaw(path, data);
|
|
171
188
|
}),
|
|
172
|
-
mkdir: (path) => fileSystem.mkdir(path, { recursive: true }),
|
|
173
|
-
readdir: (path) => fileSystem.readdir(path),
|
|
189
|
+
mkdir: (path) => contained(path, () => fileSystem.mkdir(path, { recursive: true })),
|
|
190
|
+
readdir: (path) => contained(path, () => fileSystem.readdir(path)),
|
|
174
191
|
async exists(path) {
|
|
192
|
+
await assertProjectPathContained(fileSystem, projectRoot, path);
|
|
175
193
|
try {
|
|
176
194
|
await fileSystem.stat(path);
|
|
177
195
|
return true;
|
|
@@ -186,11 +204,21 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
|
186
204
|
};
|
|
187
205
|
}
|
|
188
206
|
async function writeSessionProject(input) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
const projectRoot = input.fileSystem.dirname(input.fairyPath);
|
|
208
|
+
const write = async (fileSystem) => {
|
|
209
|
+
await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, projectRoot, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, {
|
|
210
|
+
staleSourceFiles: input.staleSourceFiles,
|
|
211
|
+
staleResourceFolders: input.staleResourceFolders,
|
|
212
|
+
staleBranchDirectories: input.staleBranchDirectories
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
if (!input.fileSystem.runProjectWriteTransaction) return write(input.fileSystem);
|
|
216
|
+
try {
|
|
217
|
+
await input.fileSystem.runProjectWriteTransaction(projectRoot, write);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
input.writtenPaths.length = 0;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
194
222
|
}
|
|
195
223
|
//#endregion
|
|
196
224
|
//#region src/services/session-utils.ts
|
|
@@ -504,7 +532,7 @@ var AuthoringService = class {
|
|
|
504
532
|
lastSavedRevision: session.lastSavedRevision,
|
|
505
533
|
committedPaths,
|
|
506
534
|
failedPaths,
|
|
507
|
-
diskMayBePartiallyUpdated:
|
|
535
|
+
diskMayBePartiallyUpdated: !fileSystem.runProjectWriteTransaction
|
|
508
536
|
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
509
537
|
sessionId: session.sessionId,
|
|
510
538
|
revision: session.revision
|
|
@@ -679,7 +707,7 @@ var AuthoringService = class {
|
|
|
679
707
|
failedPaths,
|
|
680
708
|
skippedPaths,
|
|
681
709
|
diagnostics: diagnosticsFromError,
|
|
682
|
-
diskMayBePartiallyUpdated:
|
|
710
|
+
diskMayBePartiallyUpdated: !fileSystem.runProjectWriteTransaction
|
|
683
711
|
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
684
712
|
sessionId: session.sessionId,
|
|
685
713
|
revision: session.revision,
|
|
@@ -1168,7 +1196,7 @@ var ReadService = class {
|
|
|
1168
1196
|
//#endregion
|
|
1169
1197
|
//#region src/services/runtime-service.ts
|
|
1170
1198
|
function randomId() {
|
|
1171
|
-
return
|
|
1199
|
+
return crypto.randomUUID();
|
|
1172
1200
|
}
|
|
1173
1201
|
function createCapabilityUnavailableError(capability) {
|
|
1174
1202
|
const artifactCapability = capability === "artifact.publish" || capability === "artifact.restore";
|
|
@@ -1181,29 +1209,33 @@ function createCapabilityUnavailableError(capability) {
|
|
|
1181
1209
|
bridgeBoundary: artifactCapability ? "external-bridge" : void 0
|
|
1182
1210
|
};
|
|
1183
1211
|
}
|
|
1184
|
-
function createProjectReaderFileSystem(fileSystem) {
|
|
1212
|
+
function createProjectReaderFileSystem(fileSystem, projectRoot) {
|
|
1213
|
+
const contained = async (filePath, operation) => {
|
|
1214
|
+
await assertProjectPathContained(fileSystem, projectRoot, filePath);
|
|
1215
|
+
return operation();
|
|
1216
|
+
};
|
|
1185
1217
|
return {
|
|
1186
1218
|
readFile(filePath) {
|
|
1187
|
-
return fileSystem.readFile(filePath);
|
|
1219
|
+
return contained(filePath, () => fileSystem.readFile(filePath));
|
|
1188
1220
|
},
|
|
1189
1221
|
readFileRaw(filePath) {
|
|
1190
|
-
return fileSystem.readFileRaw(filePath);
|
|
1222
|
+
return contained(filePath, () => fileSystem.readFileRaw(filePath));
|
|
1191
1223
|
},
|
|
1192
1224
|
writeFile(filePath, content) {
|
|
1193
|
-
return fileSystem.writeFile(filePath, content);
|
|
1225
|
+
return contained(filePath, () => fileSystem.writeFile(filePath, content));
|
|
1194
1226
|
},
|
|
1195
1227
|
writeFileRaw(filePath, data) {
|
|
1196
|
-
return fileSystem.writeFileRaw(filePath, data);
|
|
1228
|
+
return contained(filePath, () => fileSystem.writeFileRaw(filePath, data));
|
|
1197
1229
|
},
|
|
1198
1230
|
async mkdir(dirPath) {
|
|
1199
|
-
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
1231
|
+
await contained(dirPath, () => fileSystem.mkdir(dirPath, { recursive: true }));
|
|
1200
1232
|
},
|
|
1201
1233
|
readdir(dirPath) {
|
|
1202
|
-
return fileSystem.readdir(dirPath);
|
|
1234
|
+
return contained(dirPath, () => fileSystem.readdir(dirPath));
|
|
1203
1235
|
},
|
|
1204
1236
|
async exists(filePath) {
|
|
1205
1237
|
try {
|
|
1206
|
-
await fileSystem.stat(filePath);
|
|
1238
|
+
await contained(filePath, () => fileSystem.stat(filePath));
|
|
1207
1239
|
return true;
|
|
1208
1240
|
} catch {
|
|
1209
1241
|
return false;
|
|
@@ -1297,9 +1329,25 @@ var RuntimeService = class {
|
|
|
1297
1329
|
const startedAt = Date.now();
|
|
1298
1330
|
if (!this.context.fileSystem) return failure("runtime", startedAt, createCapabilityUnavailableError("fileSystem"));
|
|
1299
1331
|
const fileSystem = this.context.fileSystem;
|
|
1332
|
+
if (this.context.allowedProjectRoots?.length) {
|
|
1333
|
+
let allowed = false;
|
|
1334
|
+
for (const root of this.context.allowedProjectRoots) try {
|
|
1335
|
+
await assertProjectPathContained(fileSystem, root, input.projectPath);
|
|
1336
|
+
allowed = true;
|
|
1337
|
+
break;
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
if (error.code !== "EACCES") throw error;
|
|
1340
|
+
}
|
|
1341
|
+
if (!allowed) return failure("runtime", startedAt, {
|
|
1342
|
+
code: "project_root_not_allowed",
|
|
1343
|
+
message: "Project path is outside the configured allowed roots.",
|
|
1344
|
+
projectPath: input.projectPath
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1300
1347
|
const { fairyPath, canonicalProjectPath, canonicalPathKey } = await resolveCanonicalProjectRoot(fileSystem, input.projectPath);
|
|
1348
|
+
await fileSystem.validateProjectRoot?.(canonicalProjectPath);
|
|
1301
1349
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
1302
|
-
const lockFilePath = fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
|
|
1350
|
+
const lockFilePath = fileSystem.getSessionLockPath?.(canonicalProjectPath) ?? fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
|
|
1303
1351
|
if (existingSessionId) return failure("runtime", startedAt, {
|
|
1304
1352
|
code: "lock_conflict",
|
|
1305
1353
|
kind: "in_process_session_exists",
|
|
@@ -1319,7 +1367,7 @@ var RuntimeService = class {
|
|
|
1319
1367
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1320
1368
|
canonicalPathKey
|
|
1321
1369
|
}));
|
|
1322
|
-
const read = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).readDetailed(fairyPath, { hydrateResourceBytes: true });
|
|
1370
|
+
const read = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem, canonicalProjectPath)).readDetailed(fairyPath, { hydrateResourceBytes: true });
|
|
1323
1371
|
if (!read.document) throw new Error(read.diagnostics[0]?.message ?? `Unable to read project: ${fairyPath}`);
|
|
1324
1372
|
const document = read.document;
|
|
1325
1373
|
const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
|
|
@@ -1377,6 +1425,11 @@ var RuntimeService = class {
|
|
|
1377
1425
|
openProjectSession(input) {
|
|
1378
1426
|
const startedAt = Date.now();
|
|
1379
1427
|
const sessionId = input.sessionId ?? randomId();
|
|
1428
|
+
if (this.context.sessions.has(sessionId)) return failure("runtime", startedAt, {
|
|
1429
|
+
code: "session_id_conflict",
|
|
1430
|
+
message: `Session id is already in use: ${sessionId}`,
|
|
1431
|
+
sessionId
|
|
1432
|
+
});
|
|
1380
1433
|
const storage = input.storage;
|
|
1381
1434
|
const memoryProjectPath = `memory://${sessionId}`;
|
|
1382
1435
|
const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
|
|
@@ -1503,7 +1556,7 @@ const ARTIFACT_BRIDGE_CAPABILITY = {
|
|
|
1503
1556
|
bridgeEntrypoint: "@openfairygui/backend/node",
|
|
1504
1557
|
reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
|
|
1505
1558
|
};
|
|
1506
|
-
function createCapabilities() {
|
|
1559
|
+
function createCapabilities(atomicSave = false) {
|
|
1507
1560
|
return {
|
|
1508
1561
|
contractVersion: BACKEND_CONTRACT_VERSION,
|
|
1509
1562
|
capabilitySchemaVersion: 3,
|
|
@@ -1575,7 +1628,7 @@ function createCapabilities() {
|
|
|
1575
1628
|
sessionRuntime: true,
|
|
1576
1629
|
advisoryLocking: true,
|
|
1577
1630
|
coordinatedSave: true,
|
|
1578
|
-
atomicSave
|
|
1631
|
+
atomicSave,
|
|
1579
1632
|
staleRevisionProtection: true,
|
|
1580
1633
|
pathPolicy: createRuntimePathPolicy(),
|
|
1581
1634
|
events: {
|
|
@@ -1621,10 +1674,11 @@ var BackendRuntime = class {
|
|
|
1621
1674
|
jobService;
|
|
1622
1675
|
constructor(options = {}) {
|
|
1623
1676
|
this.fileSystem = options.fileSystem;
|
|
1624
|
-
this.capabilities = createCapabilities();
|
|
1677
|
+
this.capabilities = createCapabilities(Boolean(options.fileSystem?.runProjectWriteTransaction));
|
|
1625
1678
|
this.context = {
|
|
1626
1679
|
fileSystem: this.fileSystem,
|
|
1627
1680
|
host: options.host,
|
|
1681
|
+
allowedProjectRoots: options.allowedProjectRoots,
|
|
1628
1682
|
capabilities: this.capabilities,
|
|
1629
1683
|
sessions: this.sessions,
|
|
1630
1684
|
sessionsByPath: this.sessionsByPath,
|
|
@@ -1647,7 +1701,16 @@ var BackendRuntime = class {
|
|
|
1647
1701
|
return this.readService.getCapabilities();
|
|
1648
1702
|
}
|
|
1649
1703
|
async openSession(input) {
|
|
1650
|
-
|
|
1704
|
+
const startedAt = Date.now();
|
|
1705
|
+
try {
|
|
1706
|
+
return await this.runtimeService.openSession(input);
|
|
1707
|
+
} catch {
|
|
1708
|
+
return failure("runtime", startedAt, {
|
|
1709
|
+
code: "project_open_failed",
|
|
1710
|
+
message: "Unable to open project.",
|
|
1711
|
+
projectPath: input.projectPath
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1651
1714
|
}
|
|
1652
1715
|
openProjectSession(input) {
|
|
1653
1716
|
return this.runtimeService.openProjectSession(input);
|
|
@@ -1671,7 +1734,7 @@ var BackendRuntime = class {
|
|
|
1671
1734
|
return this.authoringService.materializeSession(input);
|
|
1672
1735
|
}
|
|
1673
1736
|
async closeSession(input) {
|
|
1674
|
-
return this.runtimeService.closeSession(input);
|
|
1737
|
+
return this.authoringService.runSessionExclusive(input.sessionId, () => this.runtimeService.closeSession(input));
|
|
1675
1738
|
}
|
|
1676
1739
|
getEvents(input) {
|
|
1677
1740
|
return this.eventService.getEvents(input);
|
|
@@ -1694,13 +1757,161 @@ var BackendRuntime = class {
|
|
|
1694
1757
|
};
|
|
1695
1758
|
//#endregion
|
|
1696
1759
|
//#region src/node.ts
|
|
1760
|
+
const PROCESS_START_TIME = Math.trunc(Date.now() - process.uptime() * 1e3);
|
|
1761
|
+
function parseLockMetadata(content) {
|
|
1762
|
+
try {
|
|
1763
|
+
const value = JSON.parse(content);
|
|
1764
|
+
if (value.schemaVersion !== 1 || !Number.isSafeInteger(value.pid) || !Number.isFinite(value.processStartTime) || typeof value.hostname !== "string" || typeof value.token !== "string") return null;
|
|
1765
|
+
return value;
|
|
1766
|
+
} catch {
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function isProcessAlive(metadata) {
|
|
1771
|
+
if (metadata.hostname !== node_os.default.hostname()) return true;
|
|
1772
|
+
if (metadata.pid === process.pid) return Math.abs(metadata.processStartTime - PROCESS_START_TIME) < 1e3;
|
|
1773
|
+
try {
|
|
1774
|
+
process.kill(metadata.pid, 0);
|
|
1775
|
+
return true;
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
return error.code !== "ESRCH";
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
async function recoverStaleLock(filePath) {
|
|
1781
|
+
let before;
|
|
1782
|
+
try {
|
|
1783
|
+
before = await node_fs_promises.default.readFile(filePath, "utf-8");
|
|
1784
|
+
} catch {
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
const metadata = parseLockMetadata(before);
|
|
1788
|
+
if (!metadata || isProcessAlive(metadata)) return false;
|
|
1789
|
+
const current = parseLockMetadata(await node_fs_promises.default.readFile(filePath, "utf-8").catch(() => ""));
|
|
1790
|
+
if (!current || current.token !== metadata.token) return false;
|
|
1791
|
+
await node_fs_promises.default.unlink(filePath);
|
|
1792
|
+
return true;
|
|
1793
|
+
}
|
|
1794
|
+
async function resolvePathThroughExistingAncestor(filePath) {
|
|
1795
|
+
const missing = [];
|
|
1796
|
+
let candidate = node_path.default.resolve(filePath);
|
|
1797
|
+
for (;;) try {
|
|
1798
|
+
const resolved = await node_fs_promises.default.realpath(candidate);
|
|
1799
|
+
return node_path.default.join(resolved, ...missing);
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
const code = error.code;
|
|
1802
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
1803
|
+
const parent = node_path.default.dirname(candidate);
|
|
1804
|
+
if (parent === candidate) return node_path.default.resolve(filePath);
|
|
1805
|
+
missing.unshift(node_path.default.basename(candidate));
|
|
1806
|
+
candidate = parent;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async function pathExists(filePath) {
|
|
1810
|
+
return node_fs_promises.default.stat(filePath).then(() => true, (error) => {
|
|
1811
|
+
if (error.code === "ENOENT") return false;
|
|
1812
|
+
throw error;
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
async function assertNoSymlinks(dirPath) {
|
|
1816
|
+
for (const entry of await node_fs_promises.default.readdir(dirPath, { withFileTypes: true })) {
|
|
1817
|
+
const entryPath = node_path.default.join(dirPath, entry.name);
|
|
1818
|
+
if (entry.isSymbolicLink()) throw new Error(`Symbolic links are not supported in project directories: ${entryPath}`);
|
|
1819
|
+
if (entry.isDirectory()) await assertNoSymlinks(entryPath);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
function createStagedNodeFileSystem(projectRoot, stagingRoot) {
|
|
1823
|
+
const { runProjectWriteTransaction: _, ...base } = createNodeBackendFileSystem();
|
|
1824
|
+
const translate = (filePath) => {
|
|
1825
|
+
const relative = node_path.default.relative(projectRoot, node_path.default.resolve(filePath));
|
|
1826
|
+
if (relative.startsWith("..") || node_path.default.isAbsolute(relative)) {
|
|
1827
|
+
const error = /* @__PURE__ */ new Error(`Project path escapes the staged root: ${filePath}`);
|
|
1828
|
+
error.code = "EACCES";
|
|
1829
|
+
throw error;
|
|
1830
|
+
}
|
|
1831
|
+
return node_path.default.join(stagingRoot, relative);
|
|
1832
|
+
};
|
|
1833
|
+
return {
|
|
1834
|
+
...base,
|
|
1835
|
+
stat: (filePath) => node_fs_promises.default.stat(translate(filePath)),
|
|
1836
|
+
async readdir(dirPath) {
|
|
1837
|
+
const entries = await node_fs_promises.default.readdir(translate(dirPath), { withFileTypes: true });
|
|
1838
|
+
const symlink = entries.find((entry) => entry.isSymbolicLink());
|
|
1839
|
+
if (symlink) throw new Error(`Symbolic links are not supported in project directories: ${node_path.default.join(dirPath, symlink.name)}`);
|
|
1840
|
+
return entries.map((entry) => entry.name);
|
|
1841
|
+
},
|
|
1842
|
+
readFile: (filePath) => node_fs_promises.default.readFile(translate(filePath), "utf-8"),
|
|
1843
|
+
async readFileRaw(filePath) {
|
|
1844
|
+
const buffer = await node_fs_promises.default.readFile(translate(filePath));
|
|
1845
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
1846
|
+
},
|
|
1847
|
+
writeFile: (filePath, content) => node_fs_promises.default.writeFile(translate(filePath), content, "utf-8"),
|
|
1848
|
+
writeFileRaw: (filePath, data) => node_fs_promises.default.writeFile(translate(filePath), data),
|
|
1849
|
+
async mkdir(dirPath, options) {
|
|
1850
|
+
await node_fs_promises.default.mkdir(translate(dirPath), { recursive: options?.recursive ?? false });
|
|
1851
|
+
},
|
|
1852
|
+
resolvePath: (filePath) => resolvePathThroughExistingAncestor(translate(filePath)),
|
|
1853
|
+
unlink: (filePath) => node_fs_promises.default.unlink(translate(filePath)),
|
|
1854
|
+
rmdir: (dirPath) => node_fs_promises.default.rmdir(translate(dirPath))
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
async function runNodeProjectWriteTransaction(projectRoot, write) {
|
|
1858
|
+
const root = node_path.default.resolve(projectRoot);
|
|
1859
|
+
const parent = node_path.default.dirname(root);
|
|
1860
|
+
const name = node_path.default.basename(root);
|
|
1861
|
+
const staging = node_path.default.join(parent, `.${name}.save-${(0, node_crypto.randomUUID)()}`);
|
|
1862
|
+
const backup = node_path.default.join(parent, `.${name}.save-backup-${(0, node_crypto.randomUUID)()}`);
|
|
1863
|
+
const existed = await pathExists(root);
|
|
1864
|
+
if (existed) {
|
|
1865
|
+
await assertNoSymlinks(root);
|
|
1866
|
+
await node_fs_promises.default.cp(root, staging, {
|
|
1867
|
+
recursive: true,
|
|
1868
|
+
errorOnExist: true,
|
|
1869
|
+
force: false
|
|
1870
|
+
});
|
|
1871
|
+
} else await node_fs_promises.default.mkdir(staging, { recursive: true });
|
|
1872
|
+
try {
|
|
1873
|
+
await write(createStagedNodeFileSystem(root, staging));
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
await node_fs_promises.default.rm(staging, {
|
|
1876
|
+
recursive: true,
|
|
1877
|
+
force: true
|
|
1878
|
+
});
|
|
1879
|
+
throw error;
|
|
1880
|
+
}
|
|
1881
|
+
if (!existed) {
|
|
1882
|
+
await node_fs_promises.default.rename(staging, root);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
await node_fs_promises.default.rename(root, backup);
|
|
1886
|
+
try {
|
|
1887
|
+
await node_fs_promises.default.rename(staging, root);
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
await node_fs_promises.default.rename(backup, root);
|
|
1890
|
+
await node_fs_promises.default.rm(staging, {
|
|
1891
|
+
recursive: true,
|
|
1892
|
+
force: true
|
|
1893
|
+
});
|
|
1894
|
+
throw error;
|
|
1895
|
+
}
|
|
1896
|
+
await node_fs_promises.default.rm(backup, {
|
|
1897
|
+
recursive: true,
|
|
1898
|
+
force: true
|
|
1899
|
+
}).catch(() => void 0);
|
|
1900
|
+
}
|
|
1697
1901
|
function createNodeBackendFileSystem() {
|
|
1698
1902
|
return {
|
|
1699
1903
|
stat(filePath) {
|
|
1700
1904
|
return node_fs_promises.default.stat(filePath);
|
|
1701
1905
|
},
|
|
1702
|
-
readdir(dirPath) {
|
|
1703
|
-
|
|
1906
|
+
async readdir(dirPath) {
|
|
1907
|
+
const entries = await node_fs_promises.default.readdir(dirPath, { withFileTypes: true });
|
|
1908
|
+
const symlink = entries.find((entry) => entry.isSymbolicLink());
|
|
1909
|
+
if (symlink) {
|
|
1910
|
+
const error = /* @__PURE__ */ new Error(`Symbolic links are not supported in project directories: ${node_path.default.join(dirPath, symlink.name)}`);
|
|
1911
|
+
error.code = "ELOOP";
|
|
1912
|
+
throw error;
|
|
1913
|
+
}
|
|
1914
|
+
return entries.map((entry) => entry.name);
|
|
1704
1915
|
},
|
|
1705
1916
|
readFile(filePath) {
|
|
1706
1917
|
return node_fs_promises.default.readFile(filePath, "utf-8");
|
|
@@ -1719,16 +1930,32 @@ function createNodeBackendFileSystem() {
|
|
|
1719
1930
|
await node_fs_promises.default.mkdir(dirPath, { recursive: options?.recursive ?? false });
|
|
1720
1931
|
},
|
|
1721
1932
|
async resolvePath(filePath) {
|
|
1722
|
-
|
|
1723
|
-
return await node_fs_promises.default.realpath(filePath);
|
|
1724
|
-
} catch {
|
|
1725
|
-
return node_path.default.resolve(filePath);
|
|
1726
|
-
}
|
|
1933
|
+
return resolvePathThroughExistingAncestor(filePath);
|
|
1727
1934
|
},
|
|
1935
|
+
validateProjectRoot: assertNoSymlinks,
|
|
1936
|
+
getSessionLockPath(canonicalProjectPath) {
|
|
1937
|
+
return node_path.default.join(node_path.default.dirname(canonicalProjectPath), `.${node_path.default.basename(canonicalProjectPath)}.openfairygui.backend.lock`);
|
|
1938
|
+
},
|
|
1939
|
+
runProjectWriteTransaction: runNodeProjectWriteTransaction,
|
|
1728
1940
|
async acquireSessionLock(filePath) {
|
|
1729
|
-
|
|
1941
|
+
let handle;
|
|
1942
|
+
try {
|
|
1943
|
+
handle = await node_fs_promises.default.open(filePath, "wx");
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (error.code !== "EEXIST" || !await recoverStaleLock(filePath)) throw error;
|
|
1946
|
+
handle = await node_fs_promises.default.open(filePath, "wx");
|
|
1947
|
+
}
|
|
1948
|
+
const owner = {
|
|
1949
|
+
schemaVersion: 1,
|
|
1950
|
+
pid: process.pid,
|
|
1951
|
+
processStartTime: PROCESS_START_TIME,
|
|
1952
|
+
hostname: node_os.default.hostname(),
|
|
1953
|
+
token: (0, node_crypto.randomUUID)(),
|
|
1954
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1955
|
+
};
|
|
1730
1956
|
let closed = false;
|
|
1731
1957
|
let released = false;
|
|
1958
|
+
let metadataWritten = false;
|
|
1732
1959
|
const closeHandle = async () => {
|
|
1733
1960
|
if (closed) return;
|
|
1734
1961
|
await handle.close();
|
|
@@ -1736,13 +1963,23 @@ function createNodeBackendFileSystem() {
|
|
|
1736
1963
|
};
|
|
1737
1964
|
return {
|
|
1738
1965
|
async writeMetadata(content) {
|
|
1739
|
-
|
|
1966
|
+
let supplied = {};
|
|
1967
|
+
try {
|
|
1968
|
+
const parsed = JSON.parse(content);
|
|
1969
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) supplied = parsed;
|
|
1970
|
+
} catch {}
|
|
1971
|
+
await handle.writeFile(JSON.stringify({
|
|
1972
|
+
...supplied,
|
|
1973
|
+
...owner
|
|
1974
|
+
}, null, 2), "utf-8");
|
|
1975
|
+
metadataWritten = true;
|
|
1740
1976
|
await closeHandle();
|
|
1741
1977
|
},
|
|
1742
1978
|
async release() {
|
|
1743
1979
|
if (released) return;
|
|
1744
1980
|
await closeHandle();
|
|
1745
|
-
await node_fs_promises.default.
|
|
1981
|
+
const current = metadataWritten ? parseLockMetadata(await node_fs_promises.default.readFile(filePath, "utf-8").catch(() => "")) : null;
|
|
1982
|
+
if (!metadataWritten || current?.token === owner.token) await node_fs_promises.default.unlink(filePath).catch((error) => {
|
|
1746
1983
|
if (error.code !== "ENOENT") throw error;
|
|
1747
1984
|
});
|
|
1748
1985
|
released = true;
|
|
@@ -1768,11 +2005,7 @@ function createNodeBackendFileSystem() {
|
|
|
1768
2005
|
}
|
|
1769
2006
|
function createNodeBackendHostAdapter() {
|
|
1770
2007
|
return { lockMetadata(input) {
|
|
1771
|
-
return {
|
|
1772
|
-
pid: process.pid,
|
|
1773
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1774
|
-
canonicalPathKey: input.canonicalPathKey
|
|
1775
|
-
};
|
|
2008
|
+
return { canonicalPathKey: input.canonicalPathKey };
|
|
1776
2009
|
} };
|
|
1777
2010
|
}
|
|
1778
2011
|
function createNodeBackendRuntime(options = {}) {
|
package/dist/node.d.cts
CHANGED
|
@@ -72,6 +72,12 @@ interface BackendFileSystem {
|
|
|
72
72
|
recursive?: boolean;
|
|
73
73
|
}): Promise<void>;
|
|
74
74
|
resolvePath(filePath: string): Promise<string>;
|
|
75
|
+
/** Optional host validation before a project is read. Node rejects links anywhere in the project tree. */
|
|
76
|
+
validateProjectRoot?(projectRoot: string): Promise<void>;
|
|
77
|
+
/** Optional host-specific lock location. Node keeps it beside the project so directory swaps do not move it. */
|
|
78
|
+
getSessionLockPath?(canonicalProjectPath: string): string;
|
|
79
|
+
/** Runs project writes against a staged copy and commits them as one directory swap. */
|
|
80
|
+
runProjectWriteTransaction?(projectRoot: string, write: (stagedFileSystem: BackendFileSystem) => Promise<void>): Promise<void>;
|
|
75
81
|
acquireSessionLock(lockPath: string): Promise<BackendSessionLock>;
|
|
76
82
|
unlink(filePath: string): Promise<void>;
|
|
77
83
|
rmdir(dirPath: string): Promise<void>;
|
|
@@ -163,7 +169,7 @@ interface BackendCapabilities {
|
|
|
163
169
|
sessionRuntime: true;
|
|
164
170
|
advisoryLocking: true;
|
|
165
171
|
coordinatedSave: true;
|
|
166
|
-
atomicSave:
|
|
172
|
+
atomicSave: boolean;
|
|
167
173
|
staleRevisionProtection: true;
|
|
168
174
|
pathPolicy: {
|
|
169
175
|
canonicalization: 'realpath+normalized-casefold';
|
|
@@ -297,6 +303,11 @@ interface AdvisoryLockConflictError {
|
|
|
297
303
|
holderSessionId?: string;
|
|
298
304
|
lockFilePath: string;
|
|
299
305
|
}
|
|
306
|
+
interface SessionIdConflictError {
|
|
307
|
+
code: 'session_id_conflict';
|
|
308
|
+
message: string;
|
|
309
|
+
sessionId: string;
|
|
310
|
+
}
|
|
300
311
|
interface SavePartialFailureError {
|
|
301
312
|
code: 'save_partial_failure';
|
|
302
313
|
message: string;
|
|
@@ -306,7 +317,7 @@ interface SavePartialFailureError {
|
|
|
306
317
|
lastSavedRevision: number;
|
|
307
318
|
committedPaths: string[];
|
|
308
319
|
failedPaths: string[];
|
|
309
|
-
diskMayBePartiallyUpdated:
|
|
320
|
+
diskMayBePartiallyUpdated: boolean;
|
|
310
321
|
}
|
|
311
322
|
interface UamFidelityUnsupportedError {
|
|
312
323
|
code: 'uam_fidelity_unsupported';
|
|
@@ -333,7 +344,7 @@ interface MaterializeWriteFailedError {
|
|
|
333
344
|
failedPaths: string[];
|
|
334
345
|
skippedPaths: string[];
|
|
335
346
|
diagnostics: BackendDiagnostic[];
|
|
336
|
-
diskMayBePartiallyUpdated:
|
|
347
|
+
diskMayBePartiallyUpdated: boolean;
|
|
337
348
|
}
|
|
338
349
|
type BackendEventKind = 'session.opened' | 'transaction.applied' | 'transaction.rejected' | 'save.started' | 'save.completed' | 'save.failed' | 'session.closeRequested' | 'session.closed' | 'cache.invalidated' | 'cache.updated' | 'job.created' | 'job.started' | 'job.progress' | 'job.cancelRequested' | 'job.cancelled' | 'job.completed' | 'job.failed';
|
|
339
350
|
interface BackendEvent {
|
|
@@ -465,7 +476,17 @@ interface RefreshCacheInput {
|
|
|
465
476
|
sessionId: string;
|
|
466
477
|
reason?: 'manual' | 'session_open' | 'after_save';
|
|
467
478
|
}
|
|
468
|
-
type BackendError = SessionNotFoundError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ApplyUamTransactionAppError;
|
|
479
|
+
type BackendError = SessionNotFoundError | SessionIdConflictError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ProjectRootNotAllowedError | ProjectOpenFailedError | ApplyUamTransactionAppError;
|
|
480
|
+
interface ProjectRootNotAllowedError {
|
|
481
|
+
code: 'project_root_not_allowed';
|
|
482
|
+
message: string;
|
|
483
|
+
projectPath: string;
|
|
484
|
+
}
|
|
485
|
+
interface ProjectOpenFailedError {
|
|
486
|
+
code: 'project_open_failed';
|
|
487
|
+
message: string;
|
|
488
|
+
projectPath: string;
|
|
489
|
+
}
|
|
469
490
|
interface ApplySessionTransactionInput {
|
|
470
491
|
sessionId: string;
|
|
471
492
|
expectedRevision: number;
|
|
@@ -512,6 +533,8 @@ interface MaterializeSessionInput {
|
|
|
512
533
|
interface BackendRuntimeOptions {
|
|
513
534
|
fileSystem?: BackendFileSystem;
|
|
514
535
|
host?: BackendHostAdapter;
|
|
536
|
+
/** Canonical filesystem roots available to file-backed sessions. Omit for unrestricted library use. */
|
|
537
|
+
allowedProjectRoots?: readonly string[];
|
|
515
538
|
}
|
|
516
539
|
//#endregion
|
|
517
540
|
//#region src/runtime.d.ts
|
|
@@ -535,8 +558,8 @@ declare class BackendRuntime {
|
|
|
535
558
|
getCapabilities(): BackendSuccess<BackendCapabilities>;
|
|
536
559
|
openSession(input: {
|
|
537
560
|
projectPath: string;
|
|
538
|
-
}): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError>>;
|
|
539
|
-
openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot>;
|
|
561
|
+
}): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError | ProjectRootNotAllowedError | ProjectOpenFailedError>>;
|
|
562
|
+
openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot, InProcessLockConflictError | SessionIdConflictError>;
|
|
540
563
|
getSession(input: {
|
|
541
564
|
sessionId: string;
|
|
542
565
|
}): BackendResult<BackendSessionSnapshot, SessionNotFoundError>;
|