@nextclaw/server 0.15.22 → 0.15.24

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/index.js CHANGED
@@ -4,7 +4,7 @@ import { serve } from "@hono/node-server";
4
4
  import { AccessManager, PanelAppError, injectUiContentParamsBootstrap, isInboxDeliveryError, isPanelAppError, isPreferenceError, isProjectError, isServiceAppError, isSessionContextCompactionError, isSessionMessageCursorError, isSessionSettingsError } from "@nextclaw/kernel";
5
5
  import { WebSocket, WebSocketServer } from "ws";
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
- import { mkdir, open, readFile, readdir, realpath, stat } from "node:fs/promises";
7
+ import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
9
9
  import { createHash, randomBytes, randomUUID } from "node:crypto";
10
10
  import * as NextclawCore from "@nextclaw/core";
@@ -316,13 +316,15 @@ var EventStreamAuthService = class {
316
316
  authenticateExtension = (request) => {
317
317
  const token = readBearerToken(readHeaderValue(request.headers.authorization));
318
318
  const extensionId = readHeaderValue(request.headers["x-nextclaw-extension-id"]);
319
+ const generation = readHeaderValue(request.headers["x-nextclaw-extension-generation"]);
319
320
  const result = this.deps.extensionAuth?.authenticateEventStreamCredential({
320
321
  extensionId,
322
+ generation,
321
323
  token
322
324
  });
323
325
  if (!result) return null;
324
326
  return {
325
- principalId: `extension:${result.extensionId}`,
327
+ principalId: `extension:${result.extensionId}:${result.generation}`,
326
328
  grants: [
327
329
  "event-stream:extension-requests",
328
330
  "event-stream:ncp-events",
@@ -330,7 +332,12 @@ var EventStreamAuthService = class {
330
332
  ],
331
333
  scopes: {
332
334
  extensionIds: [result.extensionId],
335
+ extensionGenerations: [`${result.extensionId}:${result.generation}`],
333
336
  channelIds: this.getExtensionChannelIds(result.extensionId)
337
+ },
338
+ extension: {
339
+ id: result.extensionId,
340
+ generation: result.generation
334
341
  }
335
342
  };
336
343
  };
@@ -364,7 +371,11 @@ function hasScopeValue(principal, key, value) {
364
371
  return Boolean(value && scopeValues(principal, key).includes(value));
365
372
  }
366
373
  function readExtensionRequestTarget(event) {
367
- return readString(readRecord(event.payload).extensionId);
374
+ const payload = readRecord(event.payload);
375
+ return {
376
+ extensionId: readString(payload.extensionId),
377
+ generation: readString(payload.generation)
378
+ };
368
379
  }
369
380
  function parseAgentSessionChannel(sessionId) {
370
381
  if (!sessionId) return null;
@@ -379,7 +390,10 @@ function readNcpEventChannel(event) {
379
390
  return readString(metadata.channelId) ?? readString(metadata.channel) ?? parseAgentSessionChannel(readString(message.sessionId) ?? readString(payload.sessionId));
380
391
  }
381
392
  function canStreamAppEventToPrincipal(principal, event) {
382
- if (event.type === "extension.request") return hasGrant(principal, "event-stream:extension-requests") && hasScopeValue(principal, "extensionIds", readExtensionRequestTarget(event));
393
+ if (event.type === "extension.request") {
394
+ const target = readExtensionRequestTarget(event);
395
+ return hasGrant(principal, "event-stream:extension-requests") && hasScopeValue(principal, "extensionIds", target.extensionId) && hasScopeValue(principal, "extensionGenerations", target.extensionId && target.generation ? `${target.extensionId}:${target.generation}` : null);
396
+ }
383
397
  if (event.type === "ncp.event") {
384
398
  if (hasGrant(principal, "event-stream:ui-events")) return true;
385
399
  return hasGrant(principal, "event-stream:ncp-events") && hasScopeValue(principal, "channelIds", readNcpEventChannel(event));
@@ -397,6 +411,13 @@ function canStreamAppEventToPrincipal(principal, event) {
397
411
  var EventStreamClientRegistry = class {
398
412
  clients = /* @__PURE__ */ new Set();
399
413
  add = (socket, principal) => {
414
+ const extensionId = principal.extension?.id;
415
+ if (extensionId) {
416
+ for (const existing of this.clients) if (existing.principal.extension?.id === extensionId) {
417
+ this.clients.delete(existing);
418
+ existing.socket.close();
419
+ }
420
+ }
400
421
  const client = {
401
422
  socket,
402
423
  principal
@@ -453,6 +474,7 @@ const ingressKeys = {
453
474
  channelMessageSubmit: createTypedKey("extension.channel.message.submit"),
454
475
  channelCommandList: createTypedKey("extension.channel.command.list"),
455
476
  channelCommandExecute: createTypedKey("extension.channel.command.execute"),
477
+ runtimeReady: createTypedKey("extension.runtime.ready"),
456
478
  response: createTypedKey("extension.response")
457
479
  },
458
480
  agentRun: {
@@ -677,6 +699,7 @@ var AppRoutesController = class {
677
699
  }));
678
700
  appMeta = (c) => c.json(ok(buildAppMetaView(this.options)));
679
701
  bootstrapStatus = (c) => c.json(ok(this.options.bootstrapStatus?.getStatus() ?? buildFallbackBootstrapStatus()));
702
+ extensionRuntimeStatus = (c) => c.json(ok(this.options.extensions?.getRuntimeStatus?.() ?? []));
680
703
  };
681
704
  //#endregion
682
705
  //#region src/features/config/providers/server-builtin-provider.provider.ts
@@ -4816,6 +4839,8 @@ var ServiceAppsRoutesController = class {
4816
4839
  };
4817
4840
  //#endregion
4818
4841
  //#region src/app/utils/ncp-session-event-stream.utils.ts
4842
+ const NCP_SESSION_STREAM_HEARTBEAT_INTERVAL_MS = 25e3;
4843
+ const NCP_SESSION_STREAM_HEARTBEAT_FRAME = ": keepalive\n\n";
4819
4844
  function readEventSessionId(event) {
4820
4845
  const payload = "payload" in event ? event.payload : null;
4821
4846
  if (!payload || typeof payload !== "object") return null;
@@ -4829,8 +4854,13 @@ function createNcpSessionEventStreamResponse(eventBus, payload, signal) {
4829
4854
  let controller = null;
4830
4855
  let closed = false;
4831
4856
  let unsubscribe = () => void 0;
4857
+ let heartbeat = null;
4832
4858
  const cleanup = () => {
4833
4859
  unsubscribe();
4860
+ if (heartbeat) {
4861
+ clearInterval(heartbeat);
4862
+ heartbeat = null;
4863
+ }
4834
4864
  signal.removeEventListener("abort", close);
4835
4865
  };
4836
4866
  const close = () => {
@@ -4847,6 +4877,9 @@ function createNcpSessionEventStreamResponse(eventBus, payload, signal) {
4847
4877
  start: (streamController) => {
4848
4878
  controller = streamController;
4849
4879
  unsubscribe = eventBus.on(eventKeys.ncpEvent, push);
4880
+ heartbeat = setInterval(() => {
4881
+ if (!closed && !signal.aborted) controller?.enqueue(encoder.encode(NCP_SESSION_STREAM_HEARTBEAT_FRAME));
4882
+ }, NCP_SESSION_STREAM_HEARTBEAT_INTERVAL_MS);
4850
4883
  signal.addEventListener("abort", close, { once: true });
4851
4884
  if (signal.aborted) close();
4852
4885
  },
@@ -5056,6 +5089,28 @@ function isServerPathBrowseError(error) {
5056
5089
  return error instanceof ServerPathBrowseError;
5057
5090
  }
5058
5091
  //#endregion
5092
+ //#region src/features/server-path/utils/server-path-search.utils.ts
5093
+ function isServerPathInside(basePath, candidatePath) {
5094
+ const relativePath = relative(basePath, candidatePath);
5095
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
5096
+ }
5097
+ function normalizeServerPathRelativePath(value) {
5098
+ return value.split(sep).join("/");
5099
+ }
5100
+ function resolveServerPathSearchScore(entry, query) {
5101
+ if (!query) return 1;
5102
+ const normalizedQuery = query.toLocaleLowerCase();
5103
+ const normalizedName = entry.name.toLocaleLowerCase();
5104
+ const normalizedPath = entry.relativePath.toLocaleLowerCase();
5105
+ if (!normalizedQuery.split(/\s+/).filter(Boolean).every((term) => normalizedPath.includes(term))) return 0;
5106
+ if (normalizedName === normalizedQuery) return 1e3;
5107
+ if (normalizedName.startsWith(normalizedQuery)) return 850;
5108
+ if (normalizedName.includes(normalizedQuery)) return 700;
5109
+ if (normalizedPath.startsWith(normalizedQuery)) return 600;
5110
+ if (normalizedPath.split("/").some((segment) => segment.startsWith(normalizedQuery))) return 550;
5111
+ return 400 - Math.min(normalizedPath.length, 300);
5112
+ }
5113
+ //#endregion
5059
5114
  //#region src/features/server-path/utils/server-path-directory.utils.ts
5060
5115
  var ServerPathDirectoryCreateError = class extends Error {
5061
5116
  constructor(code, message) {
@@ -5069,28 +5124,46 @@ function normalizeDirectoryName(value) {
5069
5124
  if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) throw new ServerPathDirectoryCreateError("SERVER_PATH_DIRECTORY_NAME_INVALID", "directory name is invalid");
5070
5125
  return name;
5071
5126
  }
5072
- function readFileErrorCode(error) {
5127
+ function readFileErrorCode$1(error) {
5073
5128
  return typeof error === "object" && error !== null && "code" in error ? String(error.code ?? "") : void 0;
5074
5129
  }
5075
- async function createServerPathDirectory(input) {
5076
- let parentPath;
5130
+ function resolveDirectoryPath(value) {
5077
5131
  try {
5078
- parentPath = resolveServerPath({ path: typeof input.parentPath === "string" ? input.parentPath : null });
5132
+ return resolveServerPath({ path: typeof value === "string" ? value : null });
5079
5133
  } catch (error) {
5080
5134
  if (error instanceof ServerPathResolutionError) throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_INVALID", error.message);
5081
5135
  throw error;
5082
5136
  }
5137
+ }
5138
+ async function assertParentInsideBase(baseValue, parentPath) {
5139
+ if (typeof baseValue !== "string" || !baseValue.trim()) return;
5140
+ const basePath = resolveDirectoryPath(baseValue);
5141
+ if (!isServerPathInside(basePath, parentPath)) throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_INVALID", "parent path must stay inside the project root");
5142
+ try {
5143
+ const [realBasePath, realParentPath] = await Promise.all([realpath(basePath), realpath(parentPath)]);
5144
+ if (!isServerPathInside(realBasePath, realParentPath)) throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_INVALID", "parent path must stay inside the project root");
5145
+ } catch (error) {
5146
+ if (error instanceof ServerPathDirectoryCreateError) throw error;
5147
+ throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_INVALID", "project root and parent path must exist");
5148
+ }
5149
+ }
5150
+ function throwDirectoryCreationError(error) {
5151
+ const code = readFileErrorCode$1(error);
5152
+ if (code === "EEXIST") throw new ServerPathDirectoryCreateError("SERVER_PATH_DIRECTORY_EXISTS", "directory already exists");
5153
+ if (code === "ENOENT") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_FOUND", "parent directory does not exist");
5154
+ if (code === "ENOTDIR") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_DIRECTORY", "parent path must point to a directory");
5155
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_WRITABLE", "parent directory is not writable");
5156
+ throw error;
5157
+ }
5158
+ async function createServerPathDirectory(input) {
5159
+ const parentPath = resolveDirectoryPath(input.parentPath);
5160
+ await assertParentInsideBase(input.basePath, parentPath);
5083
5161
  const directoryPath = join(parentPath, normalizeDirectoryName(input.name));
5084
5162
  try {
5085
5163
  await mkdir(directoryPath);
5086
5164
  return { path: directoryPath };
5087
5165
  } catch (error) {
5088
- const code = readFileErrorCode(error);
5089
- if (code === "EEXIST") throw new ServerPathDirectoryCreateError("SERVER_PATH_DIRECTORY_EXISTS", "directory already exists");
5090
- if (code === "ENOENT") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_FOUND", "parent directory does not exist");
5091
- if (code === "ENOTDIR") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_DIRECTORY", "parent path must point to a directory");
5092
- if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_WRITABLE", "parent directory is not writable");
5093
- throw error;
5166
+ throwDirectoryCreationError(error);
5094
5167
  }
5095
5168
  }
5096
5169
  function isServerPathDirectoryCreateError(error) {
@@ -5382,28 +5455,6 @@ function isServerPathContentError(error) {
5382
5455
  return error instanceof ServerPathContentError;
5383
5456
  }
5384
5457
  //#endregion
5385
- //#region src/features/server-path/utils/server-path-search.utils.ts
5386
- function isServerPathInside(basePath, candidatePath) {
5387
- const relativePath = relative(basePath, candidatePath);
5388
- return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
5389
- }
5390
- function normalizeServerPathRelativePath(value) {
5391
- return value.split(sep).join("/");
5392
- }
5393
- function resolveServerPathSearchScore(entry, query) {
5394
- if (!query) return 1;
5395
- const normalizedQuery = query.toLocaleLowerCase();
5396
- const normalizedName = entry.name.toLocaleLowerCase();
5397
- const normalizedPath = entry.relativePath.toLocaleLowerCase();
5398
- if (!normalizedQuery.split(/\s+/).filter(Boolean).every((term) => normalizedPath.includes(term))) return 0;
5399
- if (normalizedName === normalizedQuery) return 1e3;
5400
- if (normalizedName.startsWith(normalizedQuery)) return 850;
5401
- if (normalizedName.includes(normalizedQuery)) return 700;
5402
- if (normalizedPath.startsWith(normalizedQuery)) return 600;
5403
- if (normalizedPath.split("/").some((segment) => segment.startsWith(normalizedQuery))) return 550;
5404
- return 400 - Math.min(normalizedPath.length, 300);
5405
- }
5406
- //#endregion
5407
5458
  //#region src/features/server-path/services/server-path-search.service.ts
5408
5459
  const DEFAULT_RESULT_LIMIT = 50;
5409
5460
  const MAX_RESULT_LIMIT = 100;
@@ -5591,6 +5642,199 @@ function isServerPathSearchError(error) {
5591
5642
  return error instanceof ServerPathSearchError;
5592
5643
  }
5593
5644
  //#endregion
5645
+ //#region src/features/server-path/utils/server-path-mutation.utils.ts
5646
+ var ServerPathMutationError = class extends Error {
5647
+ constructor(code, message, details) {
5648
+ super(message);
5649
+ this.code = code;
5650
+ this.details = details;
5651
+ this.name = "ServerPathMutationError";
5652
+ }
5653
+ };
5654
+ function readFileErrorCode(error) {
5655
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code ?? "") : void 0;
5656
+ }
5657
+ function resolveScopedPaths(input) {
5658
+ let basePath;
5659
+ let targetPath;
5660
+ try {
5661
+ basePath = resolveServerPath({ path: typeof input.basePath === "string" ? input.basePath : null });
5662
+ targetPath = resolveServerPath({
5663
+ path: typeof input.targetPath === "string" ? input.targetPath : null,
5664
+ basePath
5665
+ });
5666
+ } catch (error) {
5667
+ if (error instanceof ServerPathResolutionError) throw new ServerPathMutationError("SERVER_PATH_BASE_INVALID", error.message);
5668
+ throw error;
5669
+ }
5670
+ if (!isServerPathInside(basePath, targetPath)) throw new ServerPathMutationError("SERVER_PATH_OUTSIDE_BASE", "target path must stay inside the project root");
5671
+ if (!input.allowRoot && targetPath === basePath) throw new ServerPathMutationError("SERVER_PATH_ROOT_PROTECTED", "the project root cannot be deleted");
5672
+ return {
5673
+ basePath,
5674
+ targetPath
5675
+ };
5676
+ }
5677
+ async function resolveRealBasePath(basePath) {
5678
+ try {
5679
+ const resolved = await realpath(basePath);
5680
+ if (!(await stat(resolved)).isDirectory()) throw new ServerPathMutationError("SERVER_PATH_BASE_INVALID", "project root must point to a directory");
5681
+ return resolved;
5682
+ } catch (error) {
5683
+ if (error instanceof ServerPathMutationError) throw error;
5684
+ throw new ServerPathMutationError("SERVER_PATH_BASE_INVALID", "project root does not exist or is not accessible");
5685
+ }
5686
+ }
5687
+ async function assertExistingPathInsideRealBase(params) {
5688
+ const [realBasePath, realTargetPath] = await Promise.all([resolveRealBasePath(params.basePath), realpath(params.targetPath).catch(() => null)]);
5689
+ if (!realTargetPath) throw new ServerPathMutationError("SERVER_PATH_ENTRY_NOT_FOUND", "target path does not exist");
5690
+ if (!isServerPathInside(realBasePath, realTargetPath)) throw new ServerPathMutationError("SERVER_PATH_OUTSIDE_BASE", "target path must stay inside the project root");
5691
+ }
5692
+ function normalizeEntryName(value) {
5693
+ const name = typeof value === "string" ? value.trim() : "";
5694
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\") || name.includes("\0") || basename(name) !== name) throw new ServerPathMutationError("SERVER_PATH_FILE_NAME_INVALID", "entry name is invalid");
5695
+ return name;
5696
+ }
5697
+ async function createServerPathFile(input) {
5698
+ const { basePath, targetPath: parentPath } = resolveScopedPaths({
5699
+ basePath: input.basePath,
5700
+ targetPath: input.parentPath,
5701
+ allowRoot: true
5702
+ });
5703
+ await assertExistingPathInsideRealBase({
5704
+ basePath,
5705
+ targetPath: parentPath
5706
+ });
5707
+ if (!(await stat(parentPath).catch(() => null))?.isDirectory()) throw new ServerPathMutationError("SERVER_PATH_TARGET_NOT_DIRECTORY", "file parent must point to a directory");
5708
+ const name = normalizeEntryName(input.name);
5709
+ const path = join(parentPath, name);
5710
+ try {
5711
+ await writeFile(path, "", { flag: "wx" });
5712
+ } catch (error) {
5713
+ const code = readFileErrorCode(error);
5714
+ if (code === "EEXIST") throw new ServerPathMutationError("SERVER_PATH_FILE_EXISTS", "target file already exists");
5715
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new ServerPathMutationError("SERVER_PATH_TARGET_NOT_WRITABLE", "file parent is not writable");
5716
+ throw error;
5717
+ }
5718
+ return {
5719
+ name,
5720
+ path,
5721
+ kind: "file"
5722
+ };
5723
+ }
5724
+ async function renameServerPathEntry(input) {
5725
+ const { basePath, targetPath } = resolveScopedPaths({
5726
+ basePath: input.basePath,
5727
+ targetPath: input.path
5728
+ });
5729
+ await assertExistingPathInsideRealBase({
5730
+ basePath,
5731
+ targetPath
5732
+ });
5733
+ const entryStats = await lstat(targetPath).catch(() => null);
5734
+ if (!entryStats) throw new ServerPathMutationError("SERVER_PATH_ENTRY_NOT_FOUND", "target path does not exist");
5735
+ const name = normalizeEntryName(input.name);
5736
+ const path = join(resolve(targetPath, ".."), name);
5737
+ if (!isServerPathInside(basePath, path)) throw new ServerPathMutationError("SERVER_PATH_OUTSIDE_BASE", "target path must stay inside the project root");
5738
+ if (await lstat(path).then(() => true).catch(() => false)) throw new ServerPathMutationError("SERVER_PATH_FILE_EXISTS", "target entry already exists");
5739
+ try {
5740
+ await rename(targetPath, path);
5741
+ } catch (error) {
5742
+ const code = readFileErrorCode(error);
5743
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new ServerPathMutationError("SERVER_PATH_TARGET_NOT_WRITABLE", "target path is not writable");
5744
+ throw error;
5745
+ }
5746
+ return {
5747
+ oldPath: targetPath,
5748
+ path,
5749
+ name,
5750
+ kind: entryStats.isDirectory() ? "directory" : "file"
5751
+ };
5752
+ }
5753
+ async function deleteServerPathEntry(input) {
5754
+ const { basePath, targetPath } = resolveScopedPaths({
5755
+ basePath: input.basePath,
5756
+ targetPath: input.path
5757
+ });
5758
+ await assertExistingPathInsideRealBase({
5759
+ basePath,
5760
+ targetPath
5761
+ });
5762
+ let entryStats;
5763
+ try {
5764
+ entryStats = await lstat(targetPath);
5765
+ } catch {
5766
+ throw new ServerPathMutationError("SERVER_PATH_ENTRY_NOT_FOUND", "target path does not exist");
5767
+ }
5768
+ const kind = entryStats.isDirectory() ? "directory" : "file";
5769
+ try {
5770
+ await rm(targetPath, {
5771
+ recursive: kind === "directory",
5772
+ force: false
5773
+ });
5774
+ } catch (error) {
5775
+ const code = readFileErrorCode(error);
5776
+ if (code === "ENOENT") throw new ServerPathMutationError("SERVER_PATH_ENTRY_NOT_FOUND", "target path does not exist");
5777
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new ServerPathMutationError("SERVER_PATH_TARGET_NOT_WRITABLE", "target path is not writable");
5778
+ throw error;
5779
+ }
5780
+ return {
5781
+ path: targetPath,
5782
+ kind
5783
+ };
5784
+ }
5785
+ async function uploadServerPathFiles(input) {
5786
+ const { basePath, targetPath } = resolveScopedPaths({
5787
+ basePath: input.basePath,
5788
+ targetPath: input.targetPath,
5789
+ allowRoot: true
5790
+ });
5791
+ await assertExistingPathInsideRealBase({
5792
+ basePath,
5793
+ targetPath
5794
+ });
5795
+ if (!(await stat(targetPath).catch(() => null))?.isDirectory()) throw new ServerPathMutationError("SERVER_PATH_TARGET_NOT_DIRECTORY", "upload target must point to a directory");
5796
+ if (input.files.length === 0) throw new ServerPathMutationError("SERVER_PATH_ENTRY_INVALID", "at least one upload file is required");
5797
+ const normalizedFiles = input.files.map((file) => ({
5798
+ file,
5799
+ name: normalizeEntryName(file.name)
5800
+ }));
5801
+ const uniqueNames = /* @__PURE__ */ new Set();
5802
+ for (const { name } of normalizedFiles) {
5803
+ if (uniqueNames.has(name)) throw new ServerPathMutationError("SERVER_PATH_FILE_NAME_INVALID", "upload file names must be unique");
5804
+ uniqueNames.add(name);
5805
+ }
5806
+ if (!input.overwrite) {
5807
+ const conflicts = (await Promise.all(normalizedFiles.map(async ({ name }) => {
5808
+ return await lstat(join(targetPath, name)).then(() => true).catch(() => false) ? name : null;
5809
+ }))).filter((name) => Boolean(name));
5810
+ if (conflicts.length > 0) throw new ServerPathMutationError("SERVER_PATH_FILE_EXISTS", "one or more upload files already exist", { conflicts });
5811
+ }
5812
+ try {
5813
+ return {
5814
+ files: await Promise.all(normalizedFiles.map(async ({ file, name }) => {
5815
+ const path = resolve(targetPath, name);
5816
+ const content = Buffer.from(await file.arrayBuffer());
5817
+ await writeFile(path, content, { flag: input.overwrite ? "w" : "wx" });
5818
+ return {
5819
+ name,
5820
+ path,
5821
+ sizeBytes: content.byteLength
5822
+ };
5823
+ })),
5824
+ overwritten: input.overwrite
5825
+ };
5826
+ } catch (error) {
5827
+ const code = readFileErrorCode(error);
5828
+ if (code === "EEXIST") throw new ServerPathMutationError("SERVER_PATH_FILE_EXISTS", "one or more upload files already exist");
5829
+ if (code === "ENOENT" || code === "ENOTDIR") throw new ServerPathMutationError("SERVER_PATH_TARGET_NOT_DIRECTORY", "upload target must point to an existing directory");
5830
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new ServerPathMutationError("SERVER_PATH_TARGET_NOT_WRITABLE", "upload target is not writable");
5831
+ throw error;
5832
+ }
5833
+ }
5834
+ function isServerPathMutationError(error) {
5835
+ return error instanceof ServerPathMutationError;
5836
+ }
5837
+ //#endregion
5594
5838
  //#region src/features/server-path/controllers/server-path.controller.ts
5595
5839
  function readIncludeFilesFlag(value) {
5596
5840
  return value === "1" || value === "true";
@@ -5638,6 +5882,7 @@ var ServerPathRoutesController = class {
5638
5882
  if (!body.ok || !isRecord$2(body.data)) return c.json(err("INVALID_SERVER_PATH_DIRECTORY", "directory input is required"), 400);
5639
5883
  try {
5640
5884
  return c.json(ok(await createServerPathDirectory({
5885
+ basePath: body.data.basePath,
5641
5886
  parentPath: body.data.parentPath,
5642
5887
  name: body.data.name
5643
5888
  })), 201);
@@ -5646,6 +5891,68 @@ var ServerPathRoutesController = class {
5646
5891
  throw error;
5647
5892
  }
5648
5893
  };
5894
+ uploadFiles = async (c) => {
5895
+ let formData;
5896
+ try {
5897
+ formData = await c.req.raw.formData();
5898
+ } catch {
5899
+ return c.json(err("INVALID_SERVER_PATH_UPLOAD", "multipart upload is required"), 400);
5900
+ }
5901
+ const files = formData.getAll("files").flatMap((value) => typeof value === "string" ? [] : [{
5902
+ name: value.name,
5903
+ arrayBuffer: () => value.arrayBuffer()
5904
+ }]);
5905
+ try {
5906
+ return c.json(ok(await uploadServerPathFiles({
5907
+ basePath: formData.get("basePath"),
5908
+ targetPath: formData.get("targetPath"),
5909
+ overwrite: formData.get("overwrite") === "true",
5910
+ files
5911
+ })), 201);
5912
+ } catch (error) {
5913
+ if (isServerPathMutationError(error)) return c.json(err(error.code, error.message, error.details), error.code === "SERVER_PATH_FILE_EXISTS" ? 409 : 400);
5914
+ throw error;
5915
+ }
5916
+ };
5917
+ createFile = async (c) => {
5918
+ const body = await readJson(c.req.raw);
5919
+ if (!body.ok || !isRecord$2(body.data)) return c.json(err("INVALID_SERVER_PATH_FILE", "file input is required"), 400);
5920
+ try {
5921
+ return c.json(ok(await createServerPathFile({
5922
+ basePath: body.data.basePath,
5923
+ parentPath: body.data.parentPath,
5924
+ name: body.data.name
5925
+ })), 201);
5926
+ } catch (error) {
5927
+ if (isServerPathMutationError(error)) return c.json(err(error.code, error.message, error.details), error.code === "SERVER_PATH_FILE_EXISTS" ? 409 : 400);
5928
+ throw error;
5929
+ }
5930
+ };
5931
+ renameEntry = async (c) => {
5932
+ const body = await readJson(c.req.raw);
5933
+ if (!body.ok || !isRecord$2(body.data)) return c.json(err("INVALID_SERVER_PATH_RENAME", "rename input is required"), 400);
5934
+ try {
5935
+ return c.json(ok(await renameServerPathEntry({
5936
+ basePath: body.data.basePath,
5937
+ path: body.data.path,
5938
+ name: body.data.name
5939
+ })));
5940
+ } catch (error) {
5941
+ if (isServerPathMutationError(error)) return c.json(err(error.code, error.message, error.details), error.code === "SERVER_PATH_FILE_EXISTS" ? 409 : 400);
5942
+ throw error;
5943
+ }
5944
+ };
5945
+ deleteEntry = async (c) => {
5946
+ try {
5947
+ return c.json(ok(await deleteServerPathEntry({
5948
+ basePath: c.req.query("basePath"),
5949
+ path: c.req.query("path")
5950
+ })));
5951
+ } catch (error) {
5952
+ if (isServerPathMutationError(error)) return c.json(err(error.code, error.message, error.details), 400);
5953
+ throw error;
5954
+ }
5955
+ };
5649
5956
  read = async (c) => {
5650
5957
  try {
5651
5958
  const payload = await readServerPath({
@@ -6072,6 +6379,26 @@ var UiRouteRegistry = class {
6072
6379
  "/api/server-paths/directory",
6073
6380
  serverPath.createDirectory
6074
6381
  ],
6382
+ [
6383
+ "post",
6384
+ "/api/server-paths/file",
6385
+ serverPath.createFile
6386
+ ],
6387
+ [
6388
+ "post",
6389
+ "/api/server-paths/files",
6390
+ serverPath.uploadFiles
6391
+ ],
6392
+ [
6393
+ "patch",
6394
+ "/api/server-paths/entry",
6395
+ serverPath.renameEntry
6396
+ ],
6397
+ [
6398
+ "delete",
6399
+ "/api/server-paths/entry",
6400
+ serverPath.deleteEntry
6401
+ ],
6075
6402
  [
6076
6403
  "get",
6077
6404
  "/api/server-paths/read",
@@ -6107,6 +6434,11 @@ var UiRouteRegistry = class {
6107
6434
  "/api/runtime/bootstrap-status",
6108
6435
  app.bootstrapStatus
6109
6436
  ],
6437
+ [
6438
+ "get",
6439
+ "/api/runtime/extensions",
6440
+ app.extensionRuntimeStatus
6441
+ ],
6110
6442
  [
6111
6443
  "get",
6112
6444
  "/api/auth/status",