@seedvault/server 0.1.0 → 0.1.2
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/server.js +165 -67
- package/package.json +6 -1
package/dist/server.js
CHANGED
|
@@ -1743,13 +1743,6 @@ async function deleteFile(storageRoot, contributor, filePath) {
|
|
|
1743
1743
|
}
|
|
1744
1744
|
}
|
|
1745
1745
|
}
|
|
1746
|
-
async function readFileContent(storageRoot, contributor, filePath) {
|
|
1747
|
-
const absPath = resolvePath(storageRoot, contributor, filePath);
|
|
1748
|
-
if (!existsSync(absPath)) {
|
|
1749
|
-
throw new FileNotFoundError(filePath);
|
|
1750
|
-
}
|
|
1751
|
-
return await readFile(absPath, "utf-8");
|
|
1752
|
-
}
|
|
1753
1746
|
async function listFiles(storageRoot, contributor, prefix) {
|
|
1754
1747
|
const contributorRoot = join(storageRoot, contributor);
|
|
1755
1748
|
if (!existsSync(contributorRoot)) {
|
|
@@ -1903,17 +1896,119 @@ async function syncContributors(storageRoot, contributors) {
|
|
|
1903
1896
|
}
|
|
1904
1897
|
}
|
|
1905
1898
|
|
|
1899
|
+
// src/shell.ts
|
|
1900
|
+
var ALLOWED_COMMANDS = new Set([
|
|
1901
|
+
"ls",
|
|
1902
|
+
"cat",
|
|
1903
|
+
"head",
|
|
1904
|
+
"tail",
|
|
1905
|
+
"find",
|
|
1906
|
+
"grep",
|
|
1907
|
+
"wc",
|
|
1908
|
+
"tree",
|
|
1909
|
+
"stat"
|
|
1910
|
+
]);
|
|
1911
|
+
var MAX_STDOUT = 1024 * 1024;
|
|
1912
|
+
var TIMEOUT_MS = 1e4;
|
|
1913
|
+
function parseCommand(cmd) {
|
|
1914
|
+
const args = [];
|
|
1915
|
+
let current = "";
|
|
1916
|
+
let inSingle = false;
|
|
1917
|
+
let inDouble = false;
|
|
1918
|
+
for (let i = 0;i < cmd.length; i++) {
|
|
1919
|
+
const ch = cmd[i];
|
|
1920
|
+
if (ch === "'" && !inDouble) {
|
|
1921
|
+
inSingle = !inSingle;
|
|
1922
|
+
} else if (ch === '"' && !inSingle) {
|
|
1923
|
+
inDouble = !inDouble;
|
|
1924
|
+
} else if (ch === " " && !inSingle && !inDouble) {
|
|
1925
|
+
if (current.length > 0) {
|
|
1926
|
+
args.push(current);
|
|
1927
|
+
current = "";
|
|
1928
|
+
}
|
|
1929
|
+
} else {
|
|
1930
|
+
current += ch;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (current.length > 0) {
|
|
1934
|
+
args.push(current);
|
|
1935
|
+
}
|
|
1936
|
+
return args;
|
|
1937
|
+
}
|
|
1938
|
+
async function executeCommand(cmd, storageRoot) {
|
|
1939
|
+
const argv = parseCommand(cmd.trim());
|
|
1940
|
+
if (argv.length === 0) {
|
|
1941
|
+
throw new ShellValidationError("Empty command");
|
|
1942
|
+
}
|
|
1943
|
+
const command = argv[0];
|
|
1944
|
+
if (!ALLOWED_COMMANDS.has(command)) {
|
|
1945
|
+
throw new ShellValidationError(`Command not allowed: ${command}. Allowed: ${[...ALLOWED_COMMANDS].join(", ")}`);
|
|
1946
|
+
}
|
|
1947
|
+
for (const arg of argv.slice(1)) {
|
|
1948
|
+
if (arg.includes("..")) {
|
|
1949
|
+
throw new ShellValidationError("Path traversal (..) is not allowed");
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const proc = Bun.spawn(argv, {
|
|
1953
|
+
cwd: storageRoot,
|
|
1954
|
+
stdout: "pipe",
|
|
1955
|
+
stderr: "pipe",
|
|
1956
|
+
env: {}
|
|
1957
|
+
});
|
|
1958
|
+
const timeout = setTimeout(() => {
|
|
1959
|
+
proc.kill();
|
|
1960
|
+
}, TIMEOUT_MS);
|
|
1961
|
+
try {
|
|
1962
|
+
const [stdoutBuf, stderrBuf] = await Promise.all([
|
|
1963
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
1964
|
+
new Response(proc.stderr).arrayBuffer()
|
|
1965
|
+
]);
|
|
1966
|
+
await proc.exited;
|
|
1967
|
+
const truncated = stdoutBuf.byteLength > MAX_STDOUT;
|
|
1968
|
+
const stdoutBytes = truncated ? stdoutBuf.slice(0, MAX_STDOUT) : stdoutBuf;
|
|
1969
|
+
let stdout = new TextDecoder().decode(stdoutBytes);
|
|
1970
|
+
if (truncated) {
|
|
1971
|
+
stdout += `
|
|
1972
|
+
[truncated]`;
|
|
1973
|
+
}
|
|
1974
|
+
const stderr = new TextDecoder().decode(stderrBuf);
|
|
1975
|
+
return {
|
|
1976
|
+
stdout,
|
|
1977
|
+
stderr,
|
|
1978
|
+
exitCode: proc.exitCode ?? 1,
|
|
1979
|
+
truncated
|
|
1980
|
+
};
|
|
1981
|
+
} finally {
|
|
1982
|
+
clearTimeout(timeout);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
class ShellValidationError extends Error {
|
|
1987
|
+
constructor(message) {
|
|
1988
|
+
super(message);
|
|
1989
|
+
this.name = "ShellValidationError";
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1906
1993
|
// src/routes.ts
|
|
1907
1994
|
var uiPath = resolve(import.meta.dirname, "index.html");
|
|
1908
1995
|
var isDev = true;
|
|
1909
1996
|
var uiHtmlCached = readFileSync(uiPath, "utf-8");
|
|
1910
|
-
function
|
|
1911
|
-
const raw2 = reqPath.replace(
|
|
1997
|
+
function extractFileInfo(reqPath) {
|
|
1998
|
+
const raw2 = reqPath.replace("/v1/files/", "");
|
|
1999
|
+
let decoded;
|
|
1912
2000
|
try {
|
|
1913
|
-
|
|
2001
|
+
decoded = decodeURIComponent(raw2);
|
|
1914
2002
|
} catch {
|
|
1915
2003
|
return null;
|
|
1916
2004
|
}
|
|
2005
|
+
const slashIdx = decoded.indexOf("/");
|
|
2006
|
+
if (slashIdx === -1)
|
|
2007
|
+
return null;
|
|
2008
|
+
return {
|
|
2009
|
+
username: decoded.slice(0, slashIdx),
|
|
2010
|
+
filePath: decoded.slice(slashIdx + 1)
|
|
2011
|
+
};
|
|
1917
2012
|
}
|
|
1918
2013
|
function createApp(storageRoot) {
|
|
1919
2014
|
const app = new Hono2;
|
|
@@ -1994,28 +2089,49 @@ function createApp(storageRoot) {
|
|
|
1994
2089
|
}))
|
|
1995
2090
|
});
|
|
1996
2091
|
});
|
|
1997
|
-
authed.
|
|
2092
|
+
authed.post("/v1/sh", async (c) => {
|
|
2093
|
+
const body = await c.req.json();
|
|
2094
|
+
if (!body.cmd || typeof body.cmd !== "string") {
|
|
2095
|
+
return c.json({ error: "cmd is required" }, 400);
|
|
2096
|
+
}
|
|
2097
|
+
try {
|
|
2098
|
+
const result = await executeCommand(body.cmd, storageRoot);
|
|
2099
|
+
return new Response(result.stdout, {
|
|
2100
|
+
status: 200,
|
|
2101
|
+
headers: {
|
|
2102
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
2103
|
+
"X-Exit-Code": String(result.exitCode),
|
|
2104
|
+
"X-Stderr": encodeURIComponent(result.stderr)
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
} catch (e) {
|
|
2108
|
+
if (e instanceof ShellValidationError) {
|
|
2109
|
+
return c.json({ error: e.message }, 400);
|
|
2110
|
+
}
|
|
2111
|
+
throw e;
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
authed.put("/v1/files/*", async (c) => {
|
|
1998
2115
|
const { contributor } = getAuthCtx(c);
|
|
1999
|
-
const
|
|
2000
|
-
if (
|
|
2116
|
+
const parsed = extractFileInfo(c.req.path);
|
|
2117
|
+
if (!parsed) {
|
|
2118
|
+
return c.json({ error: "Invalid file path" }, 400);
|
|
2119
|
+
}
|
|
2120
|
+
if (contributor.username !== parsed.username) {
|
|
2001
2121
|
return c.json({ error: "You can only write to your own contributor" }, 403);
|
|
2002
2122
|
}
|
|
2003
|
-
if (!getContributor(username)) {
|
|
2123
|
+
if (!getContributor(parsed.username)) {
|
|
2004
2124
|
return c.json({ error: "Contributor not found" }, 404);
|
|
2005
2125
|
}
|
|
2006
|
-
const
|
|
2007
|
-
if (filePath === null) {
|
|
2008
|
-
return c.json({ error: "Invalid URL encoding in path" }, 400);
|
|
2009
|
-
}
|
|
2010
|
-
const pathError = validatePath(filePath);
|
|
2126
|
+
const pathError = validatePath(parsed.filePath);
|
|
2011
2127
|
if (pathError) {
|
|
2012
2128
|
return c.json({ error: pathError }, 400);
|
|
2013
2129
|
}
|
|
2014
2130
|
const content = await c.req.text();
|
|
2015
2131
|
try {
|
|
2016
|
-
const result = await writeFileAtomic(storageRoot, username, filePath, content);
|
|
2132
|
+
const result = await writeFileAtomic(storageRoot, parsed.username, parsed.filePath, content);
|
|
2017
2133
|
broadcast("file_updated", {
|
|
2018
|
-
contributor: username,
|
|
2134
|
+
contributor: parsed.username,
|
|
2019
2135
|
path: result.path,
|
|
2020
2136
|
size: result.size,
|
|
2021
2137
|
modifiedAt: result.modifiedAt
|
|
@@ -2029,25 +2145,24 @@ function createApp(storageRoot) {
|
|
|
2029
2145
|
throw e;
|
|
2030
2146
|
}
|
|
2031
2147
|
});
|
|
2032
|
-
authed.delete("/v1/
|
|
2148
|
+
authed.delete("/v1/files/*", async (c) => {
|
|
2033
2149
|
const { contributor } = getAuthCtx(c);
|
|
2034
|
-
const
|
|
2035
|
-
if (
|
|
2036
|
-
return c.json({ error: "
|
|
2150
|
+
const parsed = extractFileInfo(c.req.path);
|
|
2151
|
+
if (!parsed) {
|
|
2152
|
+
return c.json({ error: "Invalid file path" }, 400);
|
|
2037
2153
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
return c.json({ error: "Invalid URL encoding in path" }, 400);
|
|
2154
|
+
if (contributor.username !== parsed.username) {
|
|
2155
|
+
return c.json({ error: "You can only delete from your own contributor" }, 403);
|
|
2041
2156
|
}
|
|
2042
|
-
const pathError = validatePath(filePath);
|
|
2157
|
+
const pathError = validatePath(parsed.filePath);
|
|
2043
2158
|
if (pathError) {
|
|
2044
2159
|
return c.json({ error: pathError }, 400);
|
|
2045
2160
|
}
|
|
2046
2161
|
try {
|
|
2047
|
-
await deleteFile(storageRoot, username, filePath);
|
|
2162
|
+
await deleteFile(storageRoot, parsed.username, parsed.filePath);
|
|
2048
2163
|
broadcast("file_deleted", {
|
|
2049
|
-
contributor: username,
|
|
2050
|
-
path: filePath
|
|
2164
|
+
contributor: parsed.username,
|
|
2165
|
+
path: parsed.filePath
|
|
2051
2166
|
});
|
|
2052
2167
|
triggerUpdate();
|
|
2053
2168
|
return c.body(null, 204);
|
|
@@ -2058,39 +2173,24 @@ function createApp(storageRoot) {
|
|
|
2058
2173
|
throw e;
|
|
2059
2174
|
}
|
|
2060
2175
|
});
|
|
2061
|
-
authed.get("/v1/
|
|
2062
|
-
const
|
|
2063
|
-
if (!
|
|
2064
|
-
return c.json({ error: "
|
|
2065
|
-
}
|
|
2066
|
-
const
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
});
|
|
2070
|
-
authed.get("/v1/contributors/:username/files/*", async (c) => {
|
|
2071
|
-
const username = c.req.param("username");
|
|
2176
|
+
authed.get("/v1/files", async (c) => {
|
|
2177
|
+
const prefix = c.req.query("prefix") || "";
|
|
2178
|
+
if (!prefix) {
|
|
2179
|
+
return c.json({ error: "prefix query parameter is required" }, 400);
|
|
2180
|
+
}
|
|
2181
|
+
const slashIdx = prefix.indexOf("/");
|
|
2182
|
+
const username = slashIdx === -1 ? prefix : prefix.slice(0, slashIdx);
|
|
2183
|
+
const subPrefix = slashIdx === -1 ? undefined : prefix.slice(slashIdx + 1) || undefined;
|
|
2072
2184
|
if (!getContributor(username)) {
|
|
2073
2185
|
return c.json({ error: "Contributor not found" }, 404);
|
|
2074
2186
|
}
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
}
|
|
2083
|
-
try {
|
|
2084
|
-
const content = await readFileContent(storageRoot, username, filePath);
|
|
2085
|
-
return c.text(content, 200, {
|
|
2086
|
-
"Content-Type": "text/markdown"
|
|
2087
|
-
});
|
|
2088
|
-
} catch (e) {
|
|
2089
|
-
if (e instanceof FileNotFoundError) {
|
|
2090
|
-
return c.json({ error: "File not found" }, 404);
|
|
2091
|
-
}
|
|
2092
|
-
throw e;
|
|
2093
|
-
}
|
|
2187
|
+
const files = await listFiles(storageRoot, username, subPrefix);
|
|
2188
|
+
return c.json({
|
|
2189
|
+
files: files.map((f) => ({
|
|
2190
|
+
...f,
|
|
2191
|
+
path: `${username}/${f.path}`
|
|
2192
|
+
}))
|
|
2193
|
+
});
|
|
2094
2194
|
});
|
|
2095
2195
|
authed.get("/v1/events", (c) => {
|
|
2096
2196
|
let ctrl;
|
|
@@ -2162,10 +2262,8 @@ if (qmdAvailable) {
|
|
|
2162
2262
|
} else {
|
|
2163
2263
|
console.log(" QMD: not found (search disabled)");
|
|
2164
2264
|
}
|
|
2165
|
-
var
|
|
2265
|
+
var server = Bun.serve({
|
|
2166
2266
|
port: PORT,
|
|
2167
2267
|
fetch: app.fetch
|
|
2168
|
-
};
|
|
2169
|
-
|
|
2170
|
-
src_default as default
|
|
2171
|
-
};
|
|
2268
|
+
});
|
|
2269
|
+
console.log(`Listening on http://localhost:${server.port}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seedvault/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"seedvault-server": "bin/seedvault-server.mjs"
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
"check": "tsc --noEmit",
|
|
17
17
|
"prepublishOnly": "bun run build"
|
|
18
18
|
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/collaborator-ai/seedvault.git",
|
|
22
|
+
"directory": "server"
|
|
23
|
+
},
|
|
19
24
|
"dependencies": {
|
|
20
25
|
"hono": "^4"
|
|
21
26
|
},
|