@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.
- package/dist/index.html +1073 -231
- package/dist/server.js +241 -7
- 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
|
|
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_${
|
|
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
|
|
1980
|
-
const
|
|
1981
|
-
const
|
|
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
|
}
|