@seedvault/server 0.1.5 → 0.2.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 (3) hide show
  1. package/dist/index.html +1073 -231
  2. package/dist/server.js +241 -7
  3. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import { mkdir } from "fs/promises";
6
6
 
7
7
  // src/db.ts
8
8
  import { Database } from "bun:sqlite";
9
- import { randomUUID } from "crypto";
9
+ import { randomBytes, randomUUID } from "crypto";
10
10
  var db;
11
11
  function getDb() {
12
12
  if (!db)
@@ -50,6 +50,17 @@ function initDb(dbPath) {
50
50
  PRIMARY KEY (contributor, path),
51
51
  FOREIGN KEY (contributor) REFERENCES contributors(username)
52
52
  );
53
+
54
+ CREATE TABLE IF NOT EXISTS activity (
55
+ id TEXT PRIMARY KEY,
56
+ contributor TEXT NOT NULL REFERENCES contributors(username),
57
+ action TEXT NOT NULL,
58
+ detail TEXT,
59
+ created_at TEXT NOT NULL
60
+ );
61
+ CREATE INDEX IF NOT EXISTS idx_activity_contributor ON activity(contributor);
62
+ CREATE INDEX IF NOT EXISTS idx_activity_created_at ON activity(created_at);
63
+ CREATE INDEX IF NOT EXISTS idx_activity_action ON activity(action);
53
64
  `);
54
65
  const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='items_fts'").get();
55
66
  if (!hasFts) {
@@ -137,8 +148,11 @@ function deleteContributor(username) {
137
148
  const exists = d.prepare("SELECT 1 FROM contributors WHERE username = ?").get(username);
138
149
  if (!exists)
139
150
  return false;
151
+ d.prepare("DELETE FROM activity WHERE contributor = ?").run(username);
140
152
  d.prepare("DELETE FROM items WHERE contributor = ?").run(username);
141
153
  d.prepare("DELETE FROM api_keys WHERE contributor = ?").run(username);
154
+ d.prepare("UPDATE invites SET used_by = NULL WHERE used_by = ?").run(username);
155
+ d.prepare("DELETE FROM invites WHERE created_by = ?").run(username);
142
156
  d.prepare("DELETE FROM contributors WHERE username = ?").run(username);
143
157
  return true;
144
158
  }
@@ -208,7 +222,12 @@ function upsertItem(contributor, path, content, originCtime, originMtime) {
208
222
  VALUES (?, ?, ?, ?, ?)
209
223
  ON CONFLICT (contributor, path) DO UPDATE SET
210
224
  content = excluded.content,
211
- modified_at = excluded.modified_at`).run(contributor, path, content, createdAt, modifiedAt);
225
+ modified_at = excluded.modified_at,
226
+ created_at = CASE
227
+ WHEN excluded.created_at < items.created_at
228
+ THEN excluded.created_at
229
+ ELSE items.created_at
230
+ END`).run(contributor, path, content, createdAt, modifiedAt);
212
231
  return getItem(contributor, path);
213
232
  }
214
233
  function getItem(contributor, path) {
@@ -252,6 +271,33 @@ function searchItems(query, contributor, limit = 10) {
252
271
  ORDER BY rank
253
272
  LIMIT ?`).all(query, limit);
254
273
  }
274
+ function createActivityEvent(contributor, action, detail) {
275
+ const id = `act_${randomBytes(6).toString("hex")}`;
276
+ const now = new Date().toISOString();
277
+ const detailJson = detail ? JSON.stringify(detail) : null;
278
+ getDb().prepare("INSERT INTO activity (id, contributor, action, detail, created_at) VALUES (?, ?, ?, ?, ?)").run(id, contributor, action, detailJson, now);
279
+ return { id, contributor, action, detail: detailJson, created_at: now };
280
+ }
281
+ function listActivityEvents(opts) {
282
+ const conditions = [];
283
+ const params = [];
284
+ if (opts?.contributor) {
285
+ conditions.push("contributor = ?");
286
+ params.push(opts.contributor);
287
+ }
288
+ if (opts?.action) {
289
+ conditions.push("action = ?");
290
+ params.push(opts.action);
291
+ }
292
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
293
+ const limit = opts?.limit ?? 50;
294
+ const offset = opts?.offset ?? 0;
295
+ params.push(limit, offset);
296
+ return getDb().prepare(`SELECT id, contributor, action, detail, created_at
297
+ FROM activity ${where}
298
+ ORDER BY created_at DESC
299
+ LIMIT ? OFFSET ?`).all(...params);
300
+ }
255
301
 
256
302
  class ItemTooLargeError extends Error {
257
303
  size;
@@ -1786,9 +1832,9 @@ import { readFileSync } from "fs";
1786
1832
  import { resolve } from "path";
1787
1833
 
1788
1834
  // src/auth.ts
1789
- import { createHash, randomBytes } from "crypto";
1835
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
1790
1836
  function generateToken() {
1791
- return `sv_${randomBytes(16).toString("hex")}`;
1837
+ return `sv_${randomBytes2(16).toString("hex")}`;
1792
1838
  }
1793
1839
  function hashToken(raw2) {
1794
1840
  return createHash("sha256").update(raw2).digest("hex");
@@ -1847,9 +1893,161 @@ data: ${JSON.stringify(data)}
1847
1893
  }
1848
1894
  }
1849
1895
 
1896
+ // src/diff.ts
1897
+ var DIFF_MAX_BYTES = 5000;
1898
+ function computeDiff(oldText, newText) {
1899
+ if (oldText === newText)
1900
+ return null;
1901
+ if (oldText === "")
1902
+ return null;
1903
+ const oldLines = oldText.split(`
1904
+ `);
1905
+ const newLines = newText.split(`
1906
+ `);
1907
+ const editScript = myersDiff(oldLines, newLines);
1908
+ const hunks = buildHunks(editScript, oldLines, newLines, 3);
1909
+ let diff = "";
1910
+ let truncated = false;
1911
+ for (const hunk of hunks) {
1912
+ const header = `@@ -${hunk.oldStart},${hunk.oldCount}` + ` +${hunk.newStart},${hunk.newCount} @@
1913
+ `;
1914
+ diff += header;
1915
+ for (const line of hunk.lines) {
1916
+ diff += line + `
1917
+ `;
1918
+ }
1919
+ if (diff.length > DIFF_MAX_BYTES) {
1920
+ diff = diff.slice(0, DIFF_MAX_BYTES);
1921
+ truncated = true;
1922
+ break;
1923
+ }
1924
+ }
1925
+ return { diff, truncated };
1926
+ }
1927
+ function myersDiff(a, b) {
1928
+ const n = a.length;
1929
+ const m = b.length;
1930
+ const max = n + m;
1931
+ const vSize = 2 * max + 1;
1932
+ const v = new Int32Array(vSize);
1933
+ v.fill(-1);
1934
+ const offset = max;
1935
+ v[offset + 1] = 0;
1936
+ const trace = [];
1937
+ outer:
1938
+ for (let d = 0;d <= max; d++) {
1939
+ trace.push(v.slice());
1940
+ for (let k = -d;k <= d; k += 2) {
1941
+ let x2;
1942
+ if (k === -d || k !== d && v[offset + k - 1] < v[offset + k + 1]) {
1943
+ x2 = v[offset + k + 1];
1944
+ } else {
1945
+ x2 = v[offset + k - 1] + 1;
1946
+ }
1947
+ let y2 = x2 - k;
1948
+ while (x2 < n && y2 < m && a[x2] === b[y2]) {
1949
+ x2++;
1950
+ y2++;
1951
+ }
1952
+ v[offset + k] = x2;
1953
+ if (x2 >= n && y2 >= m)
1954
+ break outer;
1955
+ }
1956
+ }
1957
+ const edits = [];
1958
+ let x = n;
1959
+ let y = m;
1960
+ for (let d = trace.length - 1;d >= 0; d--) {
1961
+ const tv = trace[d];
1962
+ const k = x - y;
1963
+ let prevK;
1964
+ if (k === -d || k !== d && tv[offset + k - 1] < tv[offset + k + 1]) {
1965
+ prevK = k + 1;
1966
+ } else {
1967
+ prevK = k - 1;
1968
+ }
1969
+ const prevX = tv[offset + prevK];
1970
+ const prevY = prevX - prevK;
1971
+ while (x > prevX && y > prevY) {
1972
+ x--;
1973
+ y--;
1974
+ edits.push({ op: 0 /* Equal */, oldIdx: x, newIdx: y });
1975
+ }
1976
+ if (d > 0) {
1977
+ if (x === prevX) {
1978
+ edits.push({ op: 1 /* Insert */, oldIdx: x, newIdx: prevY });
1979
+ y--;
1980
+ } else {
1981
+ edits.push({ op: 2 /* Delete */, oldIdx: prevX, newIdx: y });
1982
+ x--;
1983
+ }
1984
+ }
1985
+ }
1986
+ edits.reverse();
1987
+ return edits;
1988
+ }
1989
+ function buildHunks(edits, oldLines, newLines, context) {
1990
+ const hunks = [];
1991
+ let i = 0;
1992
+ while (i < edits.length) {
1993
+ while (i < edits.length && edits[i].op === 0 /* Equal */)
1994
+ i++;
1995
+ if (i >= edits.length)
1996
+ break;
1997
+ let start = i;
1998
+ for (let c = 0;c < context && start > 0; c++)
1999
+ start--;
2000
+ let end = i;
2001
+ while (end < edits.length) {
2002
+ if (edits[end].op !== 0 /* Equal */) {
2003
+ end++;
2004
+ continue;
2005
+ }
2006
+ let run = 0;
2007
+ let j = end;
2008
+ while (j < edits.length && edits[j].op === 0 /* Equal */) {
2009
+ run++;
2010
+ j++;
2011
+ }
2012
+ if (j >= edits.length || run > context * 2) {
2013
+ end = Math.min(end + context, edits.length);
2014
+ break;
2015
+ }
2016
+ end = j;
2017
+ }
2018
+ const hunkEdits = edits.slice(start, end);
2019
+ const firstEdit = hunkEdits[0];
2020
+ let oldStart = firstEdit.oldIdx + 1;
2021
+ let newStart = firstEdit.newIdx + 1;
2022
+ let oldCount = 0;
2023
+ let newCount = 0;
2024
+ const lines = [];
2025
+ for (const e of hunkEdits) {
2026
+ if (e.op === 0 /* Equal */) {
2027
+ lines.push(" " + oldLines[e.oldIdx]);
2028
+ oldCount++;
2029
+ newCount++;
2030
+ } else if (e.op === 2 /* Delete */) {
2031
+ lines.push("-" + oldLines[e.oldIdx]);
2032
+ oldCount++;
2033
+ } else {
2034
+ lines.push("+" + newLines[e.newIdx]);
2035
+ newCount++;
2036
+ }
2037
+ }
2038
+ hunks.push({ oldStart, oldCount, newStart, newCount, lines });
2039
+ i = end;
2040
+ }
2041
+ return hunks;
2042
+ }
2043
+
1850
2044
  // src/routes.ts
1851
2045
  var uiPath = resolve(import.meta.dirname, "index.html");
1852
2046
  var isDev = true;
2047
+ function logActivity(contributor, action, detail) {
2048
+ const event = createActivityEvent(contributor, action, detail);
2049
+ broadcast("activity", event);
2050
+ }
1853
2051
  var uiHtmlCached = readFileSync(uiPath, "utf-8");
1854
2052
  function extractFileInfo(reqPath) {
1855
2053
  const raw2 = reqPath.replace("/v1/files/", "");
@@ -1907,6 +2105,9 @@ function createApp() {
1907
2105
  if (!isFirstUser && body.invite) {
1908
2106
  markInviteUsed(body.invite, contributor.username);
1909
2107
  }
2108
+ logActivity(contributor.username, "contributor_created", {
2109
+ username: contributor.username
2110
+ });
1910
2111
  return c.json({
1911
2112
  contributor: {
1912
2113
  username: contributor.username,
@@ -1930,6 +2131,9 @@ function createApp() {
1930
2131
  return c.json({ error: "Only the admin can generate invite codes" }, 403);
1931
2132
  }
1932
2133
  const invite = createInvite(contributor.username);
2134
+ logActivity(contributor.username, "invite_created", {
2135
+ invite: invite.id
2136
+ });
1933
2137
  return c.json({
1934
2138
  invite: invite.id,
1935
2139
  createdAt: invite.created_at
@@ -1957,6 +2161,9 @@ function createApp() {
1957
2161
  if (!found) {
1958
2162
  return c.json({ error: "Contributor not found" }, 404);
1959
2163
  }
2164
+ logActivity(contributor.username, "contributor_deleted", {
2165
+ username: target
2166
+ });
1960
2167
  return c.body(null, 204);
1961
2168
  });
1962
2169
  authed.put("/v1/files/*", async (c) => {
@@ -1976,11 +2183,22 @@ function createApp() {
1976
2183
  return c.json({ error: pathError }, 400);
1977
2184
  }
1978
2185
  const content = await c.req.text();
1979
- const now = new Date().toISOString();
1980
- const originCtime = c.req.header("X-Origin-Ctime") || now;
1981
- const originMtime = c.req.header("X-Origin-Mtime") || now;
2186
+ const originCtime = c.req.header("X-Origin-Ctime") || undefined;
2187
+ const originMtime = c.req.header("X-Origin-Mtime") || undefined;
2188
+ const existing = getItem(parsed.username, parsed.filePath);
1982
2189
  try {
1983
2190
  const item = upsertItem(parsed.username, parsed.filePath, content, originCtime, originMtime);
2191
+ const detail = {
2192
+ path: item.path,
2193
+ size: Buffer.byteLength(item.content)
2194
+ };
2195
+ const diffResult = computeDiff(existing?.content ?? "", content);
2196
+ if (diffResult) {
2197
+ detail.diff = diffResult.diff;
2198
+ if (diffResult.truncated)
2199
+ detail.diff_truncated = true;
2200
+ }
2201
+ logActivity(contributor.username, "file_upserted", detail);
1984
2202
  broadcast("file_updated", {
1985
2203
  contributor: parsed.username,
1986
2204
  path: item.path,
@@ -2017,6 +2235,9 @@ function createApp() {
2017
2235
  if (!found) {
2018
2236
  return c.json({ error: "File not found" }, 404);
2019
2237
  }
2238
+ logActivity(contributor.username, "file_deleted", {
2239
+ path: parsed.filePath
2240
+ });
2020
2241
  broadcast("file_deleted", {
2021
2242
  contributor: parsed.username,
2022
2243
  path: parsed.filePath
@@ -2111,6 +2332,19 @@ data: {}
2111
2332
  const results = searchItems(q, contributorParam, limit);
2112
2333
  return c.json({ results });
2113
2334
  });
2335
+ authed.get("/v1/activity", (c) => {
2336
+ const contributor = c.req.query("contributor") || undefined;
2337
+ const action = c.req.query("action") || undefined;
2338
+ const limit = c.req.query("limit") ? parseInt(c.req.query("limit"), 10) : undefined;
2339
+ const offset = c.req.query("offset") ? parseInt(c.req.query("offset"), 10) : undefined;
2340
+ const events = listActivityEvents({
2341
+ contributor,
2342
+ action,
2343
+ limit,
2344
+ offset
2345
+ });
2346
+ return c.json({ events });
2347
+ });
2114
2348
  app.route("/", authed);
2115
2349
  return app;
2116
2350
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedvault/server",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "seedvault-server": "bin/seedvault-server.mjs"