@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/index.mjs
CHANGED
|
@@ -41,6 +41,16 @@ function createRuntimePathPolicy() {
|
|
|
41
41
|
workspaceBoundary: "project-root-only"
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
async function assertProjectPathContained(fileSystem, projectRoot, targetPath) {
|
|
45
|
+
const [resolvedRoot, resolvedTarget] = await Promise.all([fileSystem.resolvePath(projectRoot), fileSystem.resolvePath(targetPath)]);
|
|
46
|
+
const root = normalizeComparablePath(resolvedRoot);
|
|
47
|
+
const target = normalizeComparablePath(resolvedTarget);
|
|
48
|
+
if (root === "." && !target.startsWith("/") && !/^[a-z]:\//i.test(target)) return;
|
|
49
|
+
if (target === root || target.startsWith(`${root}/`)) return;
|
|
50
|
+
const error = /* @__PURE__ */ new Error(`Project path escapes the opened root: ${targetPath}`);
|
|
51
|
+
error.code = "EACCES";
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
44
54
|
async function resolveFairyPath(fileSystem, input) {
|
|
45
55
|
const resolvedInput = fileSystem.resolve(input);
|
|
46
56
|
const stat = await fileSystem.stat(resolvedInput);
|
|
@@ -121,10 +131,14 @@ function failure(stage, startedAt, error, session, options) {
|
|
|
121
131
|
}
|
|
122
132
|
//#endregion
|
|
123
133
|
//#region src/services/session-project-writer.ts
|
|
124
|
-
function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
134
|
+
function createWriterFileSystem(fileSystem, projectRoot, writtenPaths, failedPaths) {
|
|
135
|
+
const contained = async (targetPath, operation) => {
|
|
136
|
+
await assertProjectPathContained(fileSystem, projectRoot, targetPath);
|
|
137
|
+
return operation();
|
|
138
|
+
};
|
|
125
139
|
async function trackWrite(targetPath, write) {
|
|
126
140
|
try {
|
|
127
|
-
const result = await write
|
|
141
|
+
const result = await contained(targetPath, write);
|
|
128
142
|
writtenPaths.push(targetPath);
|
|
129
143
|
return result;
|
|
130
144
|
} catch (error) {
|
|
@@ -133,8 +147,8 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
|
133
147
|
}
|
|
134
148
|
}
|
|
135
149
|
return {
|
|
136
|
-
readFile: (path) => fileSystem.readFile(path),
|
|
137
|
-
readFileRaw: (path) => fileSystem.readFileRaw(path),
|
|
150
|
+
readFile: (path) => contained(path, () => fileSystem.readFile(path)),
|
|
151
|
+
readFileRaw: (path) => contained(path, () => fileSystem.readFileRaw(path)),
|
|
138
152
|
writeFile: (path, content) => trackWrite(path, async () => {
|
|
139
153
|
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
140
154
|
await fileSystem.writeFile(path, content);
|
|
@@ -143,9 +157,10 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
|
143
157
|
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
144
158
|
await fileSystem.writeFileRaw(path, data);
|
|
145
159
|
}),
|
|
146
|
-
mkdir: (path) => fileSystem.mkdir(path, { recursive: true }),
|
|
147
|
-
readdir: (path) => fileSystem.readdir(path),
|
|
160
|
+
mkdir: (path) => contained(path, () => fileSystem.mkdir(path, { recursive: true })),
|
|
161
|
+
readdir: (path) => contained(path, () => fileSystem.readdir(path)),
|
|
148
162
|
async exists(path) {
|
|
163
|
+
await assertProjectPathContained(fileSystem, projectRoot, path);
|
|
149
164
|
try {
|
|
150
165
|
await fileSystem.stat(path);
|
|
151
166
|
return true;
|
|
@@ -160,11 +175,21 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
|
160
175
|
};
|
|
161
176
|
}
|
|
162
177
|
async function writeSessionProject(input) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
const projectRoot = input.fileSystem.dirname(input.fairyPath);
|
|
179
|
+
const write = async (fileSystem) => {
|
|
180
|
+
await new ProjectWriter(createWriterFileSystem(fileSystem, projectRoot, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, {
|
|
181
|
+
staleSourceFiles: input.staleSourceFiles,
|
|
182
|
+
staleResourceFolders: input.staleResourceFolders,
|
|
183
|
+
staleBranchDirectories: input.staleBranchDirectories
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
if (!input.fileSystem.runProjectWriteTransaction) return write(input.fileSystem);
|
|
187
|
+
try {
|
|
188
|
+
await input.fileSystem.runProjectWriteTransaction(projectRoot, write);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
input.writtenPaths.length = 0;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
168
193
|
}
|
|
169
194
|
//#endregion
|
|
170
195
|
//#region src/services/session-utils.ts
|
|
@@ -478,7 +503,7 @@ var AuthoringService = class {
|
|
|
478
503
|
lastSavedRevision: session.lastSavedRevision,
|
|
479
504
|
committedPaths,
|
|
480
505
|
failedPaths,
|
|
481
|
-
diskMayBePartiallyUpdated:
|
|
506
|
+
diskMayBePartiallyUpdated: !fileSystem.runProjectWriteTransaction
|
|
482
507
|
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
483
508
|
sessionId: session.sessionId,
|
|
484
509
|
revision: session.revision
|
|
@@ -653,7 +678,7 @@ var AuthoringService = class {
|
|
|
653
678
|
failedPaths,
|
|
654
679
|
skippedPaths,
|
|
655
680
|
diagnostics: diagnosticsFromError,
|
|
656
|
-
diskMayBePartiallyUpdated:
|
|
681
|
+
diskMayBePartiallyUpdated: !fileSystem.runProjectWriteTransaction
|
|
657
682
|
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
658
683
|
sessionId: session.sessionId,
|
|
659
684
|
revision: session.revision,
|
|
@@ -1142,7 +1167,7 @@ var ReadService = class {
|
|
|
1142
1167
|
//#endregion
|
|
1143
1168
|
//#region src/services/runtime-service.ts
|
|
1144
1169
|
function randomId() {
|
|
1145
|
-
return
|
|
1170
|
+
return crypto.randomUUID();
|
|
1146
1171
|
}
|
|
1147
1172
|
function createCapabilityUnavailableError(capability) {
|
|
1148
1173
|
const artifactCapability = capability === "artifact.publish" || capability === "artifact.restore";
|
|
@@ -1155,29 +1180,33 @@ function createCapabilityUnavailableError(capability) {
|
|
|
1155
1180
|
bridgeBoundary: artifactCapability ? "external-bridge" : void 0
|
|
1156
1181
|
};
|
|
1157
1182
|
}
|
|
1158
|
-
function createProjectReaderFileSystem(fileSystem) {
|
|
1183
|
+
function createProjectReaderFileSystem(fileSystem, projectRoot) {
|
|
1184
|
+
const contained = async (filePath, operation) => {
|
|
1185
|
+
await assertProjectPathContained(fileSystem, projectRoot, filePath);
|
|
1186
|
+
return operation();
|
|
1187
|
+
};
|
|
1159
1188
|
return {
|
|
1160
1189
|
readFile(filePath) {
|
|
1161
|
-
return fileSystem.readFile(filePath);
|
|
1190
|
+
return contained(filePath, () => fileSystem.readFile(filePath));
|
|
1162
1191
|
},
|
|
1163
1192
|
readFileRaw(filePath) {
|
|
1164
|
-
return fileSystem.readFileRaw(filePath);
|
|
1193
|
+
return contained(filePath, () => fileSystem.readFileRaw(filePath));
|
|
1165
1194
|
},
|
|
1166
1195
|
writeFile(filePath, content) {
|
|
1167
|
-
return fileSystem.writeFile(filePath, content);
|
|
1196
|
+
return contained(filePath, () => fileSystem.writeFile(filePath, content));
|
|
1168
1197
|
},
|
|
1169
1198
|
writeFileRaw(filePath, data) {
|
|
1170
|
-
return fileSystem.writeFileRaw(filePath, data);
|
|
1199
|
+
return contained(filePath, () => fileSystem.writeFileRaw(filePath, data));
|
|
1171
1200
|
},
|
|
1172
1201
|
async mkdir(dirPath) {
|
|
1173
|
-
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
1202
|
+
await contained(dirPath, () => fileSystem.mkdir(dirPath, { recursive: true }));
|
|
1174
1203
|
},
|
|
1175
1204
|
readdir(dirPath) {
|
|
1176
|
-
return fileSystem.readdir(dirPath);
|
|
1205
|
+
return contained(dirPath, () => fileSystem.readdir(dirPath));
|
|
1177
1206
|
},
|
|
1178
1207
|
async exists(filePath) {
|
|
1179
1208
|
try {
|
|
1180
|
-
await fileSystem.stat(filePath);
|
|
1209
|
+
await contained(filePath, () => fileSystem.stat(filePath));
|
|
1181
1210
|
return true;
|
|
1182
1211
|
} catch {
|
|
1183
1212
|
return false;
|
|
@@ -1271,9 +1300,25 @@ var RuntimeService = class {
|
|
|
1271
1300
|
const startedAt = Date.now();
|
|
1272
1301
|
if (!this.context.fileSystem) return failure("runtime", startedAt, createCapabilityUnavailableError("fileSystem"));
|
|
1273
1302
|
const fileSystem = this.context.fileSystem;
|
|
1303
|
+
if (this.context.allowedProjectRoots?.length) {
|
|
1304
|
+
let allowed = false;
|
|
1305
|
+
for (const root of this.context.allowedProjectRoots) try {
|
|
1306
|
+
await assertProjectPathContained(fileSystem, root, input.projectPath);
|
|
1307
|
+
allowed = true;
|
|
1308
|
+
break;
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
if (error.code !== "EACCES") throw error;
|
|
1311
|
+
}
|
|
1312
|
+
if (!allowed) return failure("runtime", startedAt, {
|
|
1313
|
+
code: "project_root_not_allowed",
|
|
1314
|
+
message: "Project path is outside the configured allowed roots.",
|
|
1315
|
+
projectPath: input.projectPath
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1274
1318
|
const { fairyPath, canonicalProjectPath, canonicalPathKey } = await resolveCanonicalProjectRoot(fileSystem, input.projectPath);
|
|
1319
|
+
await fileSystem.validateProjectRoot?.(canonicalProjectPath);
|
|
1275
1320
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
1276
|
-
const lockFilePath = fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
|
|
1321
|
+
const lockFilePath = fileSystem.getSessionLockPath?.(canonicalProjectPath) ?? fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
|
|
1277
1322
|
if (existingSessionId) return failure("runtime", startedAt, {
|
|
1278
1323
|
code: "lock_conflict",
|
|
1279
1324
|
kind: "in_process_session_exists",
|
|
@@ -1293,7 +1338,7 @@ var RuntimeService = class {
|
|
|
1293
1338
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1294
1339
|
canonicalPathKey
|
|
1295
1340
|
}));
|
|
1296
|
-
const read = await new ProjectReader(createProjectReaderFileSystem(fileSystem)).readDetailed(fairyPath, { hydrateResourceBytes: true });
|
|
1341
|
+
const read = await new ProjectReader(createProjectReaderFileSystem(fileSystem, canonicalProjectPath)).readDetailed(fairyPath, { hydrateResourceBytes: true });
|
|
1297
1342
|
if (!read.document) throw new Error(read.diagnostics[0]?.message ?? `Unable to read project: ${fairyPath}`);
|
|
1298
1343
|
const document = read.document;
|
|
1299
1344
|
const project = liftDocumentToUamProject(document);
|
|
@@ -1351,6 +1396,11 @@ var RuntimeService = class {
|
|
|
1351
1396
|
openProjectSession(input) {
|
|
1352
1397
|
const startedAt = Date.now();
|
|
1353
1398
|
const sessionId = input.sessionId ?? randomId();
|
|
1399
|
+
if (this.context.sessions.has(sessionId)) return failure("runtime", startedAt, {
|
|
1400
|
+
code: "session_id_conflict",
|
|
1401
|
+
message: `Session id is already in use: ${sessionId}`,
|
|
1402
|
+
sessionId
|
|
1403
|
+
});
|
|
1354
1404
|
const storage = input.storage;
|
|
1355
1405
|
const memoryProjectPath = `memory://${sessionId}`;
|
|
1356
1406
|
const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
|
|
@@ -1477,7 +1527,7 @@ const ARTIFACT_BRIDGE_CAPABILITY = {
|
|
|
1477
1527
|
bridgeEntrypoint: "@openfairygui/backend/node",
|
|
1478
1528
|
reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
|
|
1479
1529
|
};
|
|
1480
|
-
function createCapabilities() {
|
|
1530
|
+
function createCapabilities(atomicSave = false) {
|
|
1481
1531
|
return {
|
|
1482
1532
|
contractVersion: BACKEND_CONTRACT_VERSION,
|
|
1483
1533
|
capabilitySchemaVersion: 3,
|
|
@@ -1549,7 +1599,7 @@ function createCapabilities() {
|
|
|
1549
1599
|
sessionRuntime: true,
|
|
1550
1600
|
advisoryLocking: true,
|
|
1551
1601
|
coordinatedSave: true,
|
|
1552
|
-
atomicSave
|
|
1602
|
+
atomicSave,
|
|
1553
1603
|
staleRevisionProtection: true,
|
|
1554
1604
|
pathPolicy: createRuntimePathPolicy(),
|
|
1555
1605
|
events: {
|
|
@@ -1595,10 +1645,11 @@ var BackendRuntime = class {
|
|
|
1595
1645
|
jobService;
|
|
1596
1646
|
constructor(options = {}) {
|
|
1597
1647
|
this.fileSystem = options.fileSystem;
|
|
1598
|
-
this.capabilities = createCapabilities();
|
|
1648
|
+
this.capabilities = createCapabilities(Boolean(options.fileSystem?.runProjectWriteTransaction));
|
|
1599
1649
|
this.context = {
|
|
1600
1650
|
fileSystem: this.fileSystem,
|
|
1601
1651
|
host: options.host,
|
|
1652
|
+
allowedProjectRoots: options.allowedProjectRoots,
|
|
1602
1653
|
capabilities: this.capabilities,
|
|
1603
1654
|
sessions: this.sessions,
|
|
1604
1655
|
sessionsByPath: this.sessionsByPath,
|
|
@@ -1621,7 +1672,16 @@ var BackendRuntime = class {
|
|
|
1621
1672
|
return this.readService.getCapabilities();
|
|
1622
1673
|
}
|
|
1623
1674
|
async openSession(input) {
|
|
1624
|
-
|
|
1675
|
+
const startedAt = Date.now();
|
|
1676
|
+
try {
|
|
1677
|
+
return await this.runtimeService.openSession(input);
|
|
1678
|
+
} catch {
|
|
1679
|
+
return failure("runtime", startedAt, {
|
|
1680
|
+
code: "project_open_failed",
|
|
1681
|
+
message: "Unable to open project.",
|
|
1682
|
+
projectPath: input.projectPath
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1625
1685
|
}
|
|
1626
1686
|
openProjectSession(input) {
|
|
1627
1687
|
return this.runtimeService.openProjectSession(input);
|
|
@@ -1645,7 +1705,7 @@ var BackendRuntime = class {
|
|
|
1645
1705
|
return this.authoringService.materializeSession(input);
|
|
1646
1706
|
}
|
|
1647
1707
|
async closeSession(input) {
|
|
1648
|
-
return this.runtimeService.closeSession(input);
|
|
1708
|
+
return this.authoringService.runSessionExclusive(input.sessionId, () => this.runtimeService.closeSession(input));
|
|
1649
1709
|
}
|
|
1650
1710
|
getEvents(input) {
|
|
1651
1711
|
return this.eventService.getEvents(input);
|