@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.d.ts +38 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +368 -36
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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
|
-
|
|
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")
|
|
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
|
-
|
|
5076
|
-
let parentPath;
|
|
5130
|
+
function resolveDirectoryPath(value) {
|
|
5077
5131
|
try {
|
|
5078
|
-
|
|
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
|
-
|
|
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",
|