@sleep2agi/commhub-server 0.9.0-preview.120 → 0.9.0-preview.121
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/package.json +1 -1
- package/src/adoption-candidates-http.test.ts +137 -0
- package/src/adoption-candidates.ts +158 -0
- package/src/agent-acl-http.test.ts +2 -1
- package/src/api-host-supervisors-runtime-readiness.test.ts +13 -0
- package/src/auth.ts +8 -7
- package/src/codex-config-flags.ts +77 -0
- package/src/config-apply-validate.test.ts +33 -1
- package/src/config-apply-validate.ts +24 -0
- package/src/create-node-validate.test.ts +26 -1
- package/src/create-node-validate.ts +45 -1
- package/src/create-node-workdir.test.ts +13 -0
- package/src/daemon-provider-snapshots.test.ts +256 -0
- package/src/daemon-provider-snapshots.ts +226 -0
- package/src/db.ts +31 -0
- package/src/member-presence-http.test.ts +8 -4
- package/src/node-lifecycle-read-http.test.ts +13 -1
- package/src/node-lifecycle-read.ts +6 -0
- package/src/node-permissions.ts +2 -0
- package/src/report-status-host-schema.test.ts +24 -0
- package/src/runtime-readiness.ts +7 -0
- package/src/server.ts +40 -4
- package/src/shared/codex-inventory.ts +34 -0
- package/src/shared/daemon-provider-snapshot.ts +49 -0
- package/src/shared/provider-application.ts +24 -0
- package/src/tool-audience-http.test.ts +13 -1
- package/src/tool-audience.ts +2 -0
- package/src/tools.ts +47 -14
- package/src/user-avatar-http.test.ts +74 -0
- package/src/user-avatar-migration.test.ts +25 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { register, login, createNetworkTokenForNode, addNetworkMember } from "./auth.js";
|
|
3
|
+
import { db } from "./db.js";
|
|
4
|
+
|
|
5
|
+
// One sequential HTTP scenario: fail a prerequisite before running the next layer.
|
|
6
|
+
// Docker supplies an isolated DB; never import this against a production DB.
|
|
7
|
+
test("human avatar: auth → save/read/login → member visibility → isolation/rejection → clear", async () => {
|
|
8
|
+
const a = register("avatar_alice", "AvatarPassw0rd!");
|
|
9
|
+
const b = register("avatar_bob", "AvatarPassw0rd!");
|
|
10
|
+
const stranger = register("avatar_stranger", "AvatarPassw0rd!");
|
|
11
|
+
expect(a.ok && b.ok && stranger.ok).toBe(true);
|
|
12
|
+
expect(addNetworkMember(a.network_id!, b.user!.user_id, "member").ok).toBe(true);
|
|
13
|
+
const node = createNetworkTokenForNode(a.user!.user_id, a.network_id!, "avatar_alice", "avatar_node");
|
|
14
|
+
expect(node.ok).toBe(true);
|
|
15
|
+
const { bootServer } = await import("./server.js");
|
|
16
|
+
const hub = bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
17
|
+
const base = `http://127.0.0.1:${hub.port}`;
|
|
18
|
+
const call = async (token: string, method: string, path: string, body?: unknown) => {
|
|
19
|
+
const res = await fetch(base + path, { method,
|
|
20
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
21
|
+
body: body === undefined ? undefined : JSON.stringify(body) });
|
|
22
|
+
return { status: res.status, body: await res.json() as any };
|
|
23
|
+
};
|
|
24
|
+
const put = (token: string, body: unknown) => call(token, "PUT", "/api/auth/me/avatar", body);
|
|
25
|
+
const me = (token: string) => call(token, "GET", "/api/auth/me");
|
|
26
|
+
try {
|
|
27
|
+
expect((await me(a.token!)).status).toBe(200);
|
|
28
|
+
expect((await me(a.token!)).body.user.avatar_url).toBeNull();
|
|
29
|
+
expect((await put("invalid", { avatar_url: null })).status).toBe(401);
|
|
30
|
+
expect((await put(node.token!, { avatar_url: null })).status).toBe(403);
|
|
31
|
+
|
|
32
|
+
const avatar = "/avatars/avatar-03.webp";
|
|
33
|
+
const saved = await put(a.token!, { avatar_url: avatar });
|
|
34
|
+
expect(saved.status).toBe(200);
|
|
35
|
+
expect(saved.body).toEqual({ ok: true, user_id: a.user!.user_id, avatar_url: avatar });
|
|
36
|
+
expect((await me(a.token!)).body.user.avatar_url).toBe(avatar);
|
|
37
|
+
const secondDevice = login("avatar_alice", "AvatarPassw0rd!");
|
|
38
|
+
expect(secondDevice.ok).toBe(true);
|
|
39
|
+
expect(secondDevice.user!.avatar_url).toBe(avatar);
|
|
40
|
+
expect((await me(secondDevice.token!)).body.user.avatar_url).toBe(avatar);
|
|
41
|
+
// An ordinary profile edit must preserve and return the saved avatar.
|
|
42
|
+
const profile = await call(a.token!, "PUT", "/api/auth/me", { display_name: "Alice" });
|
|
43
|
+
expect(profile.status).toBe(200);
|
|
44
|
+
expect(profile.body.user.avatar_url).toBe(avatar);
|
|
45
|
+
const people = await call(b.token!, "GET", `/api/networks/${a.network_id}/humans`);
|
|
46
|
+
expect(people.status).toBe(200);
|
|
47
|
+
expect(people.body.humans.find((u: any) => u.user_id === a.user!.user_id).avatar_url).toBe(avatar);
|
|
48
|
+
expect((await call(stranger.token!, "GET", `/api/networks/${a.network_id}/humans`)).status).toBe(403);
|
|
49
|
+
expect((await me(b.token!)).body.user.avatar_url).toBeNull();
|
|
50
|
+
|
|
51
|
+
for (const body of [null, [], {}, { avatar_url: avatar, user_id: b.user!.user_id },
|
|
52
|
+
...[123, {}, "javascript:alert(1)", "file:///tmp/photo.png", "data:image/png;base64,AAAA",
|
|
53
|
+
"//evil.example/a.png", "/avatars/../x.png", "https://user:secret@example.com/x.png", "x".repeat(3000)]
|
|
54
|
+
.map((avatar_url) => ({ avatar_url }))]) {
|
|
55
|
+
expect((await put(a.token!, body)).status).toBe(400);
|
|
56
|
+
expect((await me(a.token!)).body.user.avatar_url).toBe(avatar);
|
|
57
|
+
}
|
|
58
|
+
const malformed = await fetch(base + "/api/auth/me/avatar", { method: "PUT",
|
|
59
|
+
headers: { Authorization: `Bearer ${a.token}`, "Content-Type": "application/json" }, body: "{" });
|
|
60
|
+
expect(malformed.status).toBe(400);
|
|
61
|
+
expect((await put(node.token!, { avatar_url: "/avatars/avatar-09.webp" })).status).toBe(403);
|
|
62
|
+
expect((await me(a.token!)).body.user.avatar_url).toBe(avatar);
|
|
63
|
+
expect((await me(b.token!)).body.user.avatar_url).toBeNull();
|
|
64
|
+
expect(db.get<any>("SELECT avatar_url FROM nodes WHERE node_id = ?1", "avatar_node")?.avatar_url ?? null).toBeNull();
|
|
65
|
+
|
|
66
|
+
const remote = "https://example.invalid/photo.png";
|
|
67
|
+
expect((await put(a.token!, { avatar_url: remote })).status).toBe(200);
|
|
68
|
+
expect((await me(a.token!)).body.user.avatar_url).toBe(remote);
|
|
69
|
+
expect((await put(a.token!, { avatar_url: null })).status).toBe(200);
|
|
70
|
+
expect((await me(secondDevice.token!)).body.user.avatar_url).toBeNull();
|
|
71
|
+
expect((await put(a.token!, { avatar_url: avatar })).status).toBe(200);
|
|
72
|
+
expect((await put(a.token!, { avatar_url: " " })).body.avatar_url).toBeNull();
|
|
73
|
+
} finally { hub.stop(true); }
|
|
74
|
+
}, 30_000);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
|
|
4
|
+
test("existing users survive additive avatar migration and next process preserves it", async () => {
|
|
5
|
+
const path = process.env.COMMHUB_DB!;
|
|
6
|
+
expect(typeof path).toBe("string");
|
|
7
|
+
const old = new Database(path);
|
|
8
|
+
old.exec(`CREATE TABLE users (
|
|
9
|
+
user_id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL,
|
|
10
|
+
email TEXT, display_name TEXT, role TEXT DEFAULT 'user',
|
|
11
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
12
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
13
|
+
); INSERT INTO users (user_id, username, password_hash) VALUES ('old_user', 'old_user', 'test-only-hash');`);
|
|
14
|
+
old.close();
|
|
15
|
+
const { db } = await import("./db.js");
|
|
16
|
+
expect(db.get<any>("SELECT avatar_url, password_hash FROM users WHERE user_id = ?1", "old_user"))
|
|
17
|
+
.toEqual({ avatar_url: null, password_hash: "test-only-hash" });
|
|
18
|
+
db.run("UPDATE users SET avatar_url = ?1 WHERE user_id = ?2", ["/avatars/avatar-03.webp", "old_user"]);
|
|
19
|
+
const child = Bun.spawn([process.execPath, "-e", `
|
|
20
|
+
const { db } = await import('./src/db.ts');
|
|
21
|
+
const row = db.get('SELECT avatar_url, password_hash FROM users WHERE user_id = ?1', 'old_user');
|
|
22
|
+
if (row.avatar_url !== '/avatars/avatar-03.webp' || row.password_hash !== 'test-only-hash') process.exit(1);
|
|
23
|
+
`], { cwd: new URL("..", import.meta.url).pathname, env: process.env, stdout: "pipe", stderr: "pipe" });
|
|
24
|
+
expect(await child.exited).toBe(0);
|
|
25
|
+
});
|