@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 CHANGED
@@ -10,7 +10,7 @@ It owns:
10
10
 
11
11
  - project/session lifecycle
12
12
  - revisioned request handling
13
- - coordinated but non-atomic save semantics
13
+ - coordinated save semantics, with atomic staged directory swaps on the Node adapter
14
14
  - browser-safe project sessions
15
15
  - browser-safe async project storage adapter
16
16
  - adapter-backed file sessions and backend-local session locking
@@ -39,7 +39,9 @@ and browser editors can inject an async storage adapter for OPFS, IndexedDB, ZIP
39
39
  or File System Access API bridges. Storage adapters must implement `unlink()` so resource rename/move/remove
40
40
  can clean up stale source files. Existing browser projects use a session-lifetime Web Lock: a live peer tab
41
41
  receives `lock_conflict`, while reload or abrupt document termination releases ownership without leaving a
42
- persistent `.openfairygui.backend.lock` marker. When Web Locks are unavailable, the storage adapter must
42
+ persistent `.openfairygui.backend.lock` marker. The Node adapter instead keeps a token-protected lock beside
43
+ the project directory, recovers only valid same-host stale ownership, and rejects project-directory symbolic
44
+ links. When Web Locks are unavailable, the storage adapter must
43
45
  provide `acquireSessionLock()` with the same atomic cross-context and owner-termination semantics. The default
44
46
  Node filesystem/runtime lives under `@openfairygui/backend/node` and retains its advisory lock file behavior.
45
47
  Adapter-backed `openSession` hydrates primary resource bytes so browser-safe transactions can rename/move
package/dist/index.cjs CHANGED
@@ -42,6 +42,16 @@ function createRuntimePathPolicy() {
42
42
  workspaceBoundary: "project-root-only"
43
43
  };
44
44
  }
45
+ async function assertProjectPathContained(fileSystem, projectRoot, targetPath) {
46
+ const [resolvedRoot, resolvedTarget] = await Promise.all([fileSystem.resolvePath(projectRoot), fileSystem.resolvePath(targetPath)]);
47
+ const root = normalizeComparablePath(resolvedRoot);
48
+ const target = normalizeComparablePath(resolvedTarget);
49
+ if (root === "." && !target.startsWith("/") && !/^[a-z]:\//i.test(target)) return;
50
+ if (target === root || target.startsWith(`${root}/`)) return;
51
+ const error = /* @__PURE__ */ new Error(`Project path escapes the opened root: ${targetPath}`);
52
+ error.code = "EACCES";
53
+ throw error;
54
+ }
45
55
  async function resolveFairyPath(fileSystem, input) {
46
56
  const resolvedInput = fileSystem.resolve(input);
47
57
  const stat = await fileSystem.stat(resolvedInput);
@@ -122,10 +132,14 @@ function failure(stage, startedAt, error, session, options) {
122
132
  }
123
133
  //#endregion
124
134
  //#region src/services/session-project-writer.ts
125
- function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
135
+ function createWriterFileSystem(fileSystem, projectRoot, writtenPaths, failedPaths) {
136
+ const contained = async (targetPath, operation) => {
137
+ await assertProjectPathContained(fileSystem, projectRoot, targetPath);
138
+ return operation();
139
+ };
126
140
  async function trackWrite(targetPath, write) {
127
141
  try {
128
- const result = await write();
142
+ const result = await contained(targetPath, write);
129
143
  writtenPaths.push(targetPath);
130
144
  return result;
131
145
  } catch (error) {
@@ -134,8 +148,8 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
134
148
  }
135
149
  }
136
150
  return {
137
- readFile: (path) => fileSystem.readFile(path),
138
- readFileRaw: (path) => fileSystem.readFileRaw(path),
151
+ readFile: (path) => contained(path, () => fileSystem.readFile(path)),
152
+ readFileRaw: (path) => contained(path, () => fileSystem.readFileRaw(path)),
139
153
  writeFile: (path, content) => trackWrite(path, async () => {
140
154
  await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
141
155
  await fileSystem.writeFile(path, content);
@@ -144,9 +158,10 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
144
158
  await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
145
159
  await fileSystem.writeFileRaw(path, data);
146
160
  }),
147
- mkdir: (path) => fileSystem.mkdir(path, { recursive: true }),
148
- readdir: (path) => fileSystem.readdir(path),
161
+ mkdir: (path) => contained(path, () => fileSystem.mkdir(path, { recursive: true })),
162
+ readdir: (path) => contained(path, () => fileSystem.readdir(path)),
149
163
  async exists(path) {
164
+ await assertProjectPathContained(fileSystem, projectRoot, path);
150
165
  try {
151
166
  await fileSystem.stat(path);
152
167
  return true;
@@ -161,11 +176,21 @@ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
161
176
  };
162
177
  }
163
178
  async function writeSessionProject(input) {
164
- await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(input.fileSystem, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, {
165
- staleSourceFiles: input.staleSourceFiles,
166
- staleResourceFolders: input.staleResourceFolders,
167
- staleBranchDirectories: input.staleBranchDirectories
168
- });
179
+ const projectRoot = input.fileSystem.dirname(input.fairyPath);
180
+ const write = async (fileSystem) => {
181
+ await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, projectRoot, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, {
182
+ staleSourceFiles: input.staleSourceFiles,
183
+ staleResourceFolders: input.staleResourceFolders,
184
+ staleBranchDirectories: input.staleBranchDirectories
185
+ });
186
+ };
187
+ if (!input.fileSystem.runProjectWriteTransaction) return write(input.fileSystem);
188
+ try {
189
+ await input.fileSystem.runProjectWriteTransaction(projectRoot, write);
190
+ } catch (error) {
191
+ input.writtenPaths.length = 0;
192
+ throw error;
193
+ }
169
194
  }
170
195
  //#endregion
171
196
  //#region src/services/session-utils.ts
@@ -479,7 +504,7 @@ var AuthoringService = class {
479
504
  lastSavedRevision: session.lastSavedRevision,
480
505
  committedPaths,
481
506
  failedPaths,
482
- diskMayBePartiallyUpdated: true
507
+ diskMayBePartiallyUpdated: !fileSystem.runProjectWriteTransaction
483
508
  }, toSessionSnapshot(session, this.context.capabilities), {
484
509
  sessionId: session.sessionId,
485
510
  revision: session.revision
@@ -654,7 +679,7 @@ var AuthoringService = class {
654
679
  failedPaths,
655
680
  skippedPaths,
656
681
  diagnostics: diagnosticsFromError,
657
- diskMayBePartiallyUpdated: true
682
+ diskMayBePartiallyUpdated: !fileSystem.runProjectWriteTransaction
658
683
  }, toSessionSnapshot(session, this.context.capabilities), {
659
684
  sessionId: session.sessionId,
660
685
  revision: session.revision,
@@ -1143,7 +1168,7 @@ var ReadService = class {
1143
1168
  //#endregion
1144
1169
  //#region src/services/runtime-service.ts
1145
1170
  function randomId() {
1146
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1171
+ return crypto.randomUUID();
1147
1172
  }
1148
1173
  function createCapabilityUnavailableError(capability) {
1149
1174
  const artifactCapability = capability === "artifact.publish" || capability === "artifact.restore";
@@ -1156,29 +1181,33 @@ function createCapabilityUnavailableError(capability) {
1156
1181
  bridgeBoundary: artifactCapability ? "external-bridge" : void 0
1157
1182
  };
1158
1183
  }
1159
- function createProjectReaderFileSystem(fileSystem) {
1184
+ function createProjectReaderFileSystem(fileSystem, projectRoot) {
1185
+ const contained = async (filePath, operation) => {
1186
+ await assertProjectPathContained(fileSystem, projectRoot, filePath);
1187
+ return operation();
1188
+ };
1160
1189
  return {
1161
1190
  readFile(filePath) {
1162
- return fileSystem.readFile(filePath);
1191
+ return contained(filePath, () => fileSystem.readFile(filePath));
1163
1192
  },
1164
1193
  readFileRaw(filePath) {
1165
- return fileSystem.readFileRaw(filePath);
1194
+ return contained(filePath, () => fileSystem.readFileRaw(filePath));
1166
1195
  },
1167
1196
  writeFile(filePath, content) {
1168
- return fileSystem.writeFile(filePath, content);
1197
+ return contained(filePath, () => fileSystem.writeFile(filePath, content));
1169
1198
  },
1170
1199
  writeFileRaw(filePath, data) {
1171
- return fileSystem.writeFileRaw(filePath, data);
1200
+ return contained(filePath, () => fileSystem.writeFileRaw(filePath, data));
1172
1201
  },
1173
1202
  async mkdir(dirPath) {
1174
- await fileSystem.mkdir(dirPath, { recursive: true });
1203
+ await contained(dirPath, () => fileSystem.mkdir(dirPath, { recursive: true }));
1175
1204
  },
1176
1205
  readdir(dirPath) {
1177
- return fileSystem.readdir(dirPath);
1206
+ return contained(dirPath, () => fileSystem.readdir(dirPath));
1178
1207
  },
1179
1208
  async exists(filePath) {
1180
1209
  try {
1181
- await fileSystem.stat(filePath);
1210
+ await contained(filePath, () => fileSystem.stat(filePath));
1182
1211
  return true;
1183
1212
  } catch {
1184
1213
  return false;
@@ -1272,9 +1301,25 @@ var RuntimeService = class {
1272
1301
  const startedAt = Date.now();
1273
1302
  if (!this.context.fileSystem) return failure("runtime", startedAt, createCapabilityUnavailableError("fileSystem"));
1274
1303
  const fileSystem = this.context.fileSystem;
1304
+ if (this.context.allowedProjectRoots?.length) {
1305
+ let allowed = false;
1306
+ for (const root of this.context.allowedProjectRoots) try {
1307
+ await assertProjectPathContained(fileSystem, root, input.projectPath);
1308
+ allowed = true;
1309
+ break;
1310
+ } catch (error) {
1311
+ if (error.code !== "EACCES") throw error;
1312
+ }
1313
+ if (!allowed) return failure("runtime", startedAt, {
1314
+ code: "project_root_not_allowed",
1315
+ message: "Project path is outside the configured allowed roots.",
1316
+ projectPath: input.projectPath
1317
+ });
1318
+ }
1275
1319
  const { fairyPath, canonicalProjectPath, canonicalPathKey } = await resolveCanonicalProjectRoot(fileSystem, input.projectPath);
1320
+ await fileSystem.validateProjectRoot?.(canonicalProjectPath);
1276
1321
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
1277
- const lockFilePath = fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
1322
+ const lockFilePath = fileSystem.getSessionLockPath?.(canonicalProjectPath) ?? fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
1278
1323
  if (existingSessionId) return failure("runtime", startedAt, {
1279
1324
  code: "lock_conflict",
1280
1325
  kind: "in_process_session_exists",
@@ -1294,7 +1339,7 @@ var RuntimeService = class {
1294
1339
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1295
1340
  canonicalPathKey
1296
1341
  }));
1297
- const read = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).readDetailed(fairyPath, { hydrateResourceBytes: true });
1342
+ const read = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem, canonicalProjectPath)).readDetailed(fairyPath, { hydrateResourceBytes: true });
1298
1343
  if (!read.document) throw new Error(read.diagnostics[0]?.message ?? `Unable to read project: ${fairyPath}`);
1299
1344
  const document = read.document;
1300
1345
  const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
@@ -1352,6 +1397,11 @@ var RuntimeService = class {
1352
1397
  openProjectSession(input) {
1353
1398
  const startedAt = Date.now();
1354
1399
  const sessionId = input.sessionId ?? randomId();
1400
+ if (this.context.sessions.has(sessionId)) return failure("runtime", startedAt, {
1401
+ code: "session_id_conflict",
1402
+ message: `Session id is already in use: ${sessionId}`,
1403
+ sessionId
1404
+ });
1355
1405
  const storage = input.storage;
1356
1406
  const memoryProjectPath = `memory://${sessionId}`;
1357
1407
  const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
@@ -1478,7 +1528,7 @@ const ARTIFACT_BRIDGE_CAPABILITY = {
1478
1528
  bridgeEntrypoint: "@openfairygui/backend/node",
1479
1529
  reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
1480
1530
  };
1481
- function createCapabilities() {
1531
+ function createCapabilities(atomicSave = false) {
1482
1532
  return {
1483
1533
  contractVersion: BACKEND_CONTRACT_VERSION,
1484
1534
  capabilitySchemaVersion: 3,
@@ -1550,7 +1600,7 @@ function createCapabilities() {
1550
1600
  sessionRuntime: true,
1551
1601
  advisoryLocking: true,
1552
1602
  coordinatedSave: true,
1553
- atomicSave: false,
1603
+ atomicSave,
1554
1604
  staleRevisionProtection: true,
1555
1605
  pathPolicy: createRuntimePathPolicy(),
1556
1606
  events: {
@@ -1596,10 +1646,11 @@ var BackendRuntime = class {
1596
1646
  jobService;
1597
1647
  constructor(options = {}) {
1598
1648
  this.fileSystem = options.fileSystem;
1599
- this.capabilities = createCapabilities();
1649
+ this.capabilities = createCapabilities(Boolean(options.fileSystem?.runProjectWriteTransaction));
1600
1650
  this.context = {
1601
1651
  fileSystem: this.fileSystem,
1602
1652
  host: options.host,
1653
+ allowedProjectRoots: options.allowedProjectRoots,
1603
1654
  capabilities: this.capabilities,
1604
1655
  sessions: this.sessions,
1605
1656
  sessionsByPath: this.sessionsByPath,
@@ -1622,7 +1673,16 @@ var BackendRuntime = class {
1622
1673
  return this.readService.getCapabilities();
1623
1674
  }
1624
1675
  async openSession(input) {
1625
- return this.runtimeService.openSession(input);
1676
+ const startedAt = Date.now();
1677
+ try {
1678
+ return await this.runtimeService.openSession(input);
1679
+ } catch {
1680
+ return failure("runtime", startedAt, {
1681
+ code: "project_open_failed",
1682
+ message: "Unable to open project.",
1683
+ projectPath: input.projectPath
1684
+ });
1685
+ }
1626
1686
  }
1627
1687
  openProjectSession(input) {
1628
1688
  return this.runtimeService.openProjectSession(input);
@@ -1646,7 +1706,7 @@ var BackendRuntime = class {
1646
1706
  return this.authoringService.materializeSession(input);
1647
1707
  }
1648
1708
  async closeSession(input) {
1649
- return this.runtimeService.closeSession(input);
1709
+ return this.authoringService.runSessionExclusive(input.sessionId, () => this.runtimeService.closeSession(input));
1650
1710
  }
1651
1711
  getEvents(input) {
1652
1712
  return this.eventService.getEvents(input);
package/dist/index.d.cts CHANGED
@@ -73,6 +73,12 @@ interface BackendFileSystem {
73
73
  recursive?: boolean;
74
74
  }): Promise<void>;
75
75
  resolvePath(filePath: string): Promise<string>;
76
+ /** Optional host validation before a project is read. Node rejects links anywhere in the project tree. */
77
+ validateProjectRoot?(projectRoot: string): Promise<void>;
78
+ /** Optional host-specific lock location. Node keeps it beside the project so directory swaps do not move it. */
79
+ getSessionLockPath?(canonicalProjectPath: string): string;
80
+ /** Runs project writes against a staged copy and commits them as one directory swap. */
81
+ runProjectWriteTransaction?(projectRoot: string, write: (stagedFileSystem: BackendFileSystem) => Promise<void>): Promise<void>;
76
82
  acquireSessionLock(lockPath: string): Promise<BackendSessionLock>;
77
83
  unlink(filePath: string): Promise<void>;
78
84
  rmdir(dirPath: string): Promise<void>;
@@ -164,7 +170,7 @@ interface BackendCapabilities {
164
170
  sessionRuntime: true;
165
171
  advisoryLocking: true;
166
172
  coordinatedSave: true;
167
- atomicSave: false;
173
+ atomicSave: boolean;
168
174
  staleRevisionProtection: true;
169
175
  pathPolicy: {
170
176
  canonicalization: 'realpath+normalized-casefold';
@@ -298,6 +304,11 @@ interface AdvisoryLockConflictError {
298
304
  holderSessionId?: string;
299
305
  lockFilePath: string;
300
306
  }
307
+ interface SessionIdConflictError {
308
+ code: 'session_id_conflict';
309
+ message: string;
310
+ sessionId: string;
311
+ }
301
312
  interface SavePartialFailureError {
302
313
  code: 'save_partial_failure';
303
314
  message: string;
@@ -307,7 +318,7 @@ interface SavePartialFailureError {
307
318
  lastSavedRevision: number;
308
319
  committedPaths: string[];
309
320
  failedPaths: string[];
310
- diskMayBePartiallyUpdated: true;
321
+ diskMayBePartiallyUpdated: boolean;
311
322
  }
312
323
  interface UamFidelityUnsupportedError {
313
324
  code: 'uam_fidelity_unsupported';
@@ -334,7 +345,7 @@ interface MaterializeWriteFailedError {
334
345
  failedPaths: string[];
335
346
  skippedPaths: string[];
336
347
  diagnostics: BackendDiagnostic[];
337
- diskMayBePartiallyUpdated: true;
348
+ diskMayBePartiallyUpdated: boolean;
338
349
  }
339
350
  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';
340
351
  interface BackendEvent {
@@ -467,7 +478,17 @@ interface RefreshCacheInput {
467
478
  sessionId: string;
468
479
  reason?: 'manual' | 'session_open' | 'after_save';
469
480
  }
470
- type BackendError = SessionNotFoundError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ApplyUamTransactionAppError;
481
+ type BackendError = SessionNotFoundError | SessionIdConflictError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ProjectRootNotAllowedError | ProjectOpenFailedError | ApplyUamTransactionAppError;
482
+ interface ProjectRootNotAllowedError {
483
+ code: 'project_root_not_allowed';
484
+ message: string;
485
+ projectPath: string;
486
+ }
487
+ interface ProjectOpenFailedError {
488
+ code: 'project_open_failed';
489
+ message: string;
490
+ projectPath: string;
491
+ }
471
492
  interface ApplySessionTransactionInput {
472
493
  sessionId: string;
473
494
  expectedRevision: number;
@@ -514,6 +535,8 @@ interface MaterializeSessionInput {
514
535
  interface BackendRuntimeOptions {
515
536
  fileSystem?: BackendFileSystem;
516
537
  host?: BackendHostAdapter;
538
+ /** Canonical filesystem roots available to file-backed sessions. Omit for unrestricted library use. */
539
+ allowedProjectRoots?: readonly string[];
517
540
  }
518
541
  //#endregion
519
542
  //#region src/runtime.d.ts
@@ -537,8 +560,8 @@ declare class BackendRuntime {
537
560
  getCapabilities(): BackendSuccess<BackendCapabilities>;
538
561
  openSession(input: {
539
562
  projectPath: string;
540
- }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError>>;
541
- openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot>;
563
+ }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError | ProjectRootNotAllowedError | ProjectOpenFailedError>>;
564
+ openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot, InProcessLockConflictError | SessionIdConflictError>;
542
565
  getSession(input: {
543
566
  sessionId: string;
544
567
  }): BackendResult<BackendSessionSnapshot, SessionNotFoundError>;
@@ -596,4 +619,4 @@ interface BackendAsyncStorageAdapter {
596
619
  type BackendStorageFileSystem = BackendFileSystem & FileSystem;
597
620
  declare function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem;
598
621
  //#endregion
599
- export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectOutline, type BackendProjectOutlinePackage, type BackendProjectOutlineResource, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionLock, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type GetProjectOutlineInput, type InProcessLockConflictError, type ListJobsInput, type MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, type UamFidelityUnsupportedError, type ValidateSessionInput, createBackendStorageFileSystem };
622
+ export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectOutline, type BackendProjectOutlinePackage, type BackendProjectOutlineResource, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionLock, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type GetProjectOutlineInput, type InProcessLockConflictError, type ListJobsInput, type MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type ProjectOpenFailedError, type ProjectRootNotAllowedError, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionIdConflictError, type SessionNotFoundError, type SessionStaleWriteError, type UamFidelityUnsupportedError, type ValidateSessionInput, createBackendStorageFileSystem };
package/dist/index.d.mts CHANGED
@@ -73,6 +73,12 @@ interface BackendFileSystem {
73
73
  recursive?: boolean;
74
74
  }): Promise<void>;
75
75
  resolvePath(filePath: string): Promise<string>;
76
+ /** Optional host validation before a project is read. Node rejects links anywhere in the project tree. */
77
+ validateProjectRoot?(projectRoot: string): Promise<void>;
78
+ /** Optional host-specific lock location. Node keeps it beside the project so directory swaps do not move it. */
79
+ getSessionLockPath?(canonicalProjectPath: string): string;
80
+ /** Runs project writes against a staged copy and commits them as one directory swap. */
81
+ runProjectWriteTransaction?(projectRoot: string, write: (stagedFileSystem: BackendFileSystem) => Promise<void>): Promise<void>;
76
82
  acquireSessionLock(lockPath: string): Promise<BackendSessionLock>;
77
83
  unlink(filePath: string): Promise<void>;
78
84
  rmdir(dirPath: string): Promise<void>;
@@ -164,7 +170,7 @@ interface BackendCapabilities {
164
170
  sessionRuntime: true;
165
171
  advisoryLocking: true;
166
172
  coordinatedSave: true;
167
- atomicSave: false;
173
+ atomicSave: boolean;
168
174
  staleRevisionProtection: true;
169
175
  pathPolicy: {
170
176
  canonicalization: 'realpath+normalized-casefold';
@@ -298,6 +304,11 @@ interface AdvisoryLockConflictError {
298
304
  holderSessionId?: string;
299
305
  lockFilePath: string;
300
306
  }
307
+ interface SessionIdConflictError {
308
+ code: 'session_id_conflict';
309
+ message: string;
310
+ sessionId: string;
311
+ }
301
312
  interface SavePartialFailureError {
302
313
  code: 'save_partial_failure';
303
314
  message: string;
@@ -307,7 +318,7 @@ interface SavePartialFailureError {
307
318
  lastSavedRevision: number;
308
319
  committedPaths: string[];
309
320
  failedPaths: string[];
310
- diskMayBePartiallyUpdated: true;
321
+ diskMayBePartiallyUpdated: boolean;
311
322
  }
312
323
  interface UamFidelityUnsupportedError {
313
324
  code: 'uam_fidelity_unsupported';
@@ -334,7 +345,7 @@ interface MaterializeWriteFailedError {
334
345
  failedPaths: string[];
335
346
  skippedPaths: string[];
336
347
  diagnostics: BackendDiagnostic[];
337
- diskMayBePartiallyUpdated: true;
348
+ diskMayBePartiallyUpdated: boolean;
338
349
  }
339
350
  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';
340
351
  interface BackendEvent {
@@ -467,7 +478,17 @@ interface RefreshCacheInput {
467
478
  sessionId: string;
468
479
  reason?: 'manual' | 'session_open' | 'after_save';
469
480
  }
470
- type BackendError = SessionNotFoundError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ApplyUamTransactionAppError;
481
+ type BackendError = SessionNotFoundError | SessionIdConflictError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ProjectRootNotAllowedError | ProjectOpenFailedError | ApplyUamTransactionAppError;
482
+ interface ProjectRootNotAllowedError {
483
+ code: 'project_root_not_allowed';
484
+ message: string;
485
+ projectPath: string;
486
+ }
487
+ interface ProjectOpenFailedError {
488
+ code: 'project_open_failed';
489
+ message: string;
490
+ projectPath: string;
491
+ }
471
492
  interface ApplySessionTransactionInput {
472
493
  sessionId: string;
473
494
  expectedRevision: number;
@@ -514,6 +535,8 @@ interface MaterializeSessionInput {
514
535
  interface BackendRuntimeOptions {
515
536
  fileSystem?: BackendFileSystem;
516
537
  host?: BackendHostAdapter;
538
+ /** Canonical filesystem roots available to file-backed sessions. Omit for unrestricted library use. */
539
+ allowedProjectRoots?: readonly string[];
517
540
  }
518
541
  //#endregion
519
542
  //#region src/runtime.d.ts
@@ -537,8 +560,8 @@ declare class BackendRuntime {
537
560
  getCapabilities(): BackendSuccess<BackendCapabilities>;
538
561
  openSession(input: {
539
562
  projectPath: string;
540
- }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError>>;
541
- openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot>;
563
+ }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError | ProjectRootNotAllowedError | ProjectOpenFailedError>>;
564
+ openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot, InProcessLockConflictError | SessionIdConflictError>;
542
565
  getSession(input: {
543
566
  sessionId: string;
544
567
  }): BackendResult<BackendSessionSnapshot, SessionNotFoundError>;
@@ -596,4 +619,4 @@ interface BackendAsyncStorageAdapter {
596
619
  type BackendStorageFileSystem = BackendFileSystem & FileSystem;
597
620
  declare function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem;
598
621
  //#endregion
599
- export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectOutline, type BackendProjectOutlinePackage, type BackendProjectOutlineResource, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionLock, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type GetProjectOutlineInput, type InProcessLockConflictError, type ListJobsInput, type MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, type UamFidelityUnsupportedError, type ValidateSessionInput, createBackendStorageFileSystem };
622
+ export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectOutline, type BackendProjectOutlinePackage, type BackendProjectOutlineResource, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionLock, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type GetProjectOutlineInput, type InProcessLockConflictError, type ListJobsInput, type MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type ProjectOpenFailedError, type ProjectRootNotAllowedError, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionIdConflictError, type SessionNotFoundError, type SessionStaleWriteError, type UamFidelityUnsupportedError, type ValidateSessionInput, createBackendStorageFileSystem };