@seedvault/server 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/dist/server.js +165 -67
  2. package/package.json +1 -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 extractFilePath(reqPath, username) {
1911
- const raw2 = reqPath.replace(`/v1/contributors/${username}/files/`, "");
1997
+ function extractFileInfo(reqPath) {
1998
+ const raw2 = reqPath.replace("/v1/files/", "");
1999
+ let decoded;
1912
2000
  try {
1913
- return decodeURIComponent(raw2);
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.put("/v1/contributors/:username/files/*", async (c) => {
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 username = c.req.param("username");
2000
- if (contributor.username !== username) {
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 filePath = extractFilePath(c.req.path, username);
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/contributors/:username/files/*", async (c) => {
2148
+ authed.delete("/v1/files/*", async (c) => {
2033
2149
  const { contributor } = getAuthCtx(c);
2034
- const username = c.req.param("username");
2035
- if (contributor.username !== username) {
2036
- return c.json({ error: "You can only delete from your own contributor" }, 403);
2150
+ const parsed = extractFileInfo(c.req.path);
2151
+ if (!parsed) {
2152
+ return c.json({ error: "Invalid file path" }, 400);
2037
2153
  }
2038
- const filePath = extractFilePath(c.req.path, username);
2039
- if (filePath === null) {
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/contributors/:username/files", async (c) => {
2062
- const username = c.req.param("username");
2063
- if (!getContributor(username)) {
2064
- return c.json({ error: "Contributor not found" }, 404);
2065
- }
2066
- const prefix = c.req.query("prefix") || undefined;
2067
- const files = await listFiles(storageRoot, username, prefix);
2068
- return c.json({ files });
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 filePath = extractFilePath(c.req.path, username);
2076
- if (filePath === null) {
2077
- return c.json({ error: "Invalid URL encoding in path" }, 400);
2078
- }
2079
- const pathError = validatePath(filePath);
2080
- if (pathError) {
2081
- return c.json({ error: pathError }, 400);
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 src_default = {
2265
+ var server = Bun.serve({
2166
2266
  port: PORT,
2167
2267
  fetch: app.fetch
2168
- };
2169
- export {
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.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "seedvault-server": "bin/seedvault-server.mjs"