@sleep2agi/commhub-server 0.9.0-preview.28 → 0.9.0-preview.29

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 (56) hide show
  1. package/bin/commhub.ts +4 -0
  2. package/package.json +4 -4
  3. package/src/ack-create-request-transport.test.ts +159 -0
  4. package/src/ack-create-request.test.ts +5 -5
  5. package/src/admin-networks-http.test.ts +84 -0
  6. package/src/api-host-supervisors-fallback.test.ts +1 -6
  7. package/src/auth.ts +88 -14
  8. package/src/dashboard-slash-routing-http.test.ts +103 -0
  9. package/src/db-adapter-guard.test.ts +85 -0
  10. package/src/db-adapter.ts +64 -23
  11. package/src/db.ts +279 -32
  12. package/src/ext-token-shared.test.ts +60 -0
  13. package/src/ext-validation-regression.test.ts +10 -17
  14. package/src/external-schedule-edits-http.test.ts +316 -0
  15. package/src/external-schedule-edits.ts +314 -0
  16. package/src/file-download-authz-dev-open.test.ts +1 -1
  17. package/src/file-download-authz.test.ts +21 -1
  18. package/src/file-download-date-bucket.test.ts +1 -1
  19. package/src/file-network-scope-dev-open.test.ts +1 -1
  20. package/src/file-network-scope.test.ts +1 -1
  21. package/src/health-redaction.test.ts +12 -0
  22. package/src/index.ts +6 -2
  23. package/src/network-name-http.test.ts +71 -0
  24. package/src/network-scope.ts +20 -0
  25. package/src/node-attrs.test.ts +1 -1
  26. package/src/peer-reply-atomic.test.ts +455 -0
  27. package/src/peer-reply-capability.test.ts +70 -0
  28. package/src/require-explicit-test-db.ts +9 -0
  29. package/src/rest-explicit-columns-http.test.ts +149 -0
  30. package/src/rest-identity.test.ts +120 -0
  31. package/src/rest-identity.ts +93 -0
  32. package/src/rest-projections.ts +67 -0
  33. package/src/scheduled-run-terminal.test.ts +268 -0
  34. package/src/scheduled-tasks-http.test.ts +460 -0
  35. package/src/scheduled-tasks.ts +538 -0
  36. package/src/send-reply-agent-warning.test.ts +38 -38
  37. package/src/server.ts +315 -60
  38. package/src/shared/external-schedule-contract.ts +60 -0
  39. package/src/skillhub-http.test.ts +88 -0
  40. package/src/task-auth-origin-http.test.ts +87 -0
  41. package/src/task-auth-origin.test.ts +24 -0
  42. package/src/task-auth-origin.ts +7 -0
  43. package/src/task-consumption.test.ts +329 -0
  44. package/src/task-diagnostic-http.test.ts +145 -0
  45. package/src/task-diagnostic.test.ts +82 -0
  46. package/src/task-diagnostic.ts +89 -0
  47. package/src/task-idempotency-mcp.test.ts +137 -0
  48. package/src/task-idempotency.test.ts +60 -0
  49. package/src/task-idempotency.ts +75 -0
  50. package/src/task-lifecycle-watcher.test.ts +160 -0
  51. package/src/task-lifecycle-watcher.ts +49 -0
  52. package/src/task-network-resolution-http.test.ts +137 -0
  53. package/src/tasks-pagination-http.test.ts +137 -0
  54. package/src/tools.ts +635 -107
  55. package/src/uploads-http.test.ts +1 -3
  56. package/src/uploads.ts +14 -3
@@ -27,7 +27,7 @@ const UPLOADS_DIR = mkdtempSync(join(tmpdir(), "anet-495-devopen-fs-"));
27
27
 
28
28
  // Set env BEFORE importing anything that touches db-adapter.ts or
29
29
  // server.ts (both capture env at module load).
30
- process.env.COMMHUB_DB = SERVER_DB;
30
+ process.env.COMMHUB_DB ||= SERVER_DB;
31
31
  process.env.COMMHUB_UPLOADS_DIR = UPLOADS_DIR;
32
32
  process.env.HOST = "127.0.0.1";
33
33
  process.env.COMMHUB_DEV_OPEN = "1";
@@ -34,7 +34,7 @@ const MASTER_TOKEN = process.env.COMMHUB_AUTH_TOKEN ?? `master-495-${Date.now()}
34
34
  // Set env BEFORE importing anything that pulls in db-adapter.ts (which
35
35
  // refuses to open a default DB under NODE_ENV=test) or server.ts (which
36
36
  // captures DEV_OPEN + AUTH_TOKEN at module load).
37
- process.env.COMMHUB_DB = SERVER_DB;
37
+ process.env.COMMHUB_DB ||= SERVER_DB;
38
38
  process.env.COMMHUB_UPLOADS_DIR = UPLOADS_DIR;
39
39
  process.env.HOST = "127.0.0.1";
40
40
  process.env.COMMHUB_AUTH_TOKEN = MASTER_TOKEN;
@@ -168,6 +168,10 @@ async function downloadAs(token: string | null, fileId: string): Promise<Respons
168
168
  return fetch(`${BASE}/api/files/${fileId}`, { headers });
169
169
  }
170
170
 
171
+ async function downloadWithQueryToken(token: string, fileId: string): Promise<Response> {
172
+ return fetch(`${BASE}/api/files/${fileId}?token=${encodeURIComponent(token)}`);
173
+ }
174
+
171
175
  describe.skipIf(!isProdMode)("#495 — GET /api/files/:file_id authorization (owner-only staged)", () => {
172
176
  let userBFileId = "";
173
177
 
@@ -238,6 +242,13 @@ describe.skipIf(!isProdMode)("#495 — GET /api/files/:file_id authorization (ow
238
242
  expect(res.status).toBe(401);
239
243
  });
240
244
 
245
+ test("🔴 valid query token is refused on file GET (credential must stay out of URLs)", async () => {
246
+ const res = await downloadWithQueryToken(userBToken, userBFileId);
247
+ expect(res.status).toBe(401);
248
+ const body: any = await res.json();
249
+ expect(body.error).toBe("unauthorized");
250
+ });
251
+
241
252
  test("non-owner request for an UNKNOWN file_id → 404 with same shape as owner-denied (no enumeration signal)", async () => {
242
253
  const res = await downloadAs(userAToken, "0123456789abcdef0123456789abcdef");
243
254
  expect(res.status).toBe(404);
@@ -257,6 +268,10 @@ async function headAs(token: string | null, fileId: string): Promise<Response> {
257
268
  return fetch(`${BASE}/api/files/${fileId}`, { method: "HEAD", headers });
258
269
  }
259
270
 
271
+ async function headWithQueryToken(token: string, fileId: string): Promise<Response> {
272
+ return fetch(`${BASE}/api/files/${fileId}?token=${encodeURIComponent(token)}`, { method: "HEAD" });
273
+ }
274
+
260
275
  describe.skipIf(!isProdMode)("#500 CR2 — HEAD /api/files/:file_id authorization (parity with GET)", () => {
261
276
  let userBFileId = "";
262
277
 
@@ -296,6 +311,11 @@ describe.skipIf(!isProdMode)("#500 CR2 — HEAD /api/files/:file_id authorizatio
296
311
  expect(res.status).toBe(401);
297
312
  });
298
313
 
314
+ test("🔴 valid query token is refused on file HEAD (same gate as GET)", async () => {
315
+ const res = await headWithQueryToken(userBToken, userBFileId);
316
+ expect(res.status).toBe(401);
317
+ });
318
+
299
319
  test("admin HEAD any file → 200 (operational access preserved on HEAD)", async () => {
300
320
  const res = await headAs(adminToken, userBFileId);
301
321
  expect(res.status).toBe(200);
@@ -34,7 +34,7 @@ const UPLOADS_DIR = mkdtempSync(join(tmpdir(), "anet-509-fs-"));
34
34
 
35
35
  // Set env BEFORE importing anything that pulls in db-adapter.ts or
36
36
  // server.ts (which capture DEV_OPEN + AUTH_TOKEN at module load).
37
- process.env.COMMHUB_DB = SERVER_DB;
37
+ process.env.COMMHUB_DB ||= SERVER_DB;
38
38
  process.env.COMMHUB_UPLOADS_DIR = UPLOADS_DIR;
39
39
  process.env.HOST = "127.0.0.1";
40
40
  delete process.env.COMMHUB_DEV_OPEN;
@@ -20,7 +20,7 @@ import { tmpdir } from "os";
20
20
  const SERVER_DB = mkdtempSync(join(tmpdir(), "anet-503-devopen-db-")) + "/commhub.db";
21
21
  const UPLOADS_DIR = mkdtempSync(join(tmpdir(), "anet-503-devopen-fs-"));
22
22
 
23
- process.env.COMMHUB_DB = SERVER_DB;
23
+ process.env.COMMHUB_DB ||= SERVER_DB;
24
24
  process.env.COMMHUB_UPLOADS_DIR = UPLOADS_DIR;
25
25
  process.env.HOST = "127.0.0.1";
26
26
  process.env.COMMHUB_DEV_OPEN = "1";
@@ -39,7 +39,7 @@ const MASTER_TOKEN = `master-503-${Date.now()}-${Math.floor(Math.random() * 1000
39
39
  // Env must be set before anything pulls in db-adapter.ts (which refuses
40
40
  // the default DB under NODE_ENV=test) or server.ts (which freezes
41
41
  // DEV_OPEN + AUTH_TOKEN at module load).
42
- process.env.COMMHUB_DB = SERVER_DB;
42
+ process.env.COMMHUB_DB ||= SERVER_DB;
43
43
  process.env.COMMHUB_UPLOADS_DIR = UPLOADS_DIR;
44
44
  process.env.HOST = "127.0.0.1";
45
45
  process.env.COMMHUB_AUTH_TOKEN = MASTER_TOKEN;
@@ -17,6 +17,7 @@ import { join } from "path";
17
17
  import { tmpdir } from "os";
18
18
  import { register, addNetworkMember, removeNetworkMember } from "./auth.js";
19
19
  import { db } from "./db.js";
20
+ import { MAX_REQUEST_CONTENT_LENGTH, MAX_UPLOAD_BYTES } from "./uploads.js";
20
21
 
21
22
  const SERVER_DB = mkdtempSync(join(tmpdir(), "anet-health-red-db-")) + "/commhub.db";
22
23
 
@@ -91,6 +92,17 @@ describe("#473 anonymous /health — watchdog contract intact, no topology leak"
91
92
  expect(body.sse_connections).toBeGreaterThanOrEqual(1);
92
93
  });
93
94
 
95
+ test("exposes stable upload limits anonymously without exposing topology", async () => {
96
+ const res = await fetch(`${BASE}/health`);
97
+ expect(res.status).toBe(200);
98
+ const body = await res.json() as any;
99
+ expect(body.limits).toEqual({
100
+ max_upload_bytes: MAX_UPLOAD_BYTES,
101
+ max_request_content_length: MAX_REQUEST_CONTENT_LENGTH,
102
+ });
103
+ expect(body.limits.max_request_content_length).toBeGreaterThan(body.limits.max_upload_bytes);
104
+ });
105
+
94
106
  test("CLI count source stays truthful: health.sse_connections == /api/stats/sse total (no fake 0)", async () => {
95
107
  // The CLI's `anet doctor` line reads health.sse_connections now
96
108
  // (was Object.keys(sse_sessions).length). This pins that aggregate to
package/src/index.ts CHANGED
@@ -6,6 +6,10 @@
6
6
  // tests and tooling import "./server.js" and get bootServer/startHub
7
7
  // without binding any port. Do NOT add logic here, and do NOT import
8
8
  // this module to "get at the server" — that starts one.
9
- import { startHub } from "./server.js";
10
-
9
+ // The default production DB path is deliberately unavailable to ordinary
10
+ // imports / bun -e probes. This run-entry is one of the two canonical places
11
+ // allowed to opt in, and it must do so before dynamically importing server.ts
12
+ // (whose dependency graph constructs the DB adapter at module evaluation).
13
+ process.env.COMMHUB_SERVER = "1";
14
+ const { startHub } = await import("./server.js");
11
15
  startHub();
@@ -0,0 +1,71 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { createNetworkTokenForNode, register } from "./auth.js";
6
+ import { db } from "./db.js";
7
+
8
+ const PRIVATE_DB_DIR = mkdtempSync(join(tmpdir(), "anet-network-name-http-"));
9
+ let server: any;
10
+ let base = "";
11
+ let userToken = "";
12
+ let nodeToken = "";
13
+ let networkId = "";
14
+ let username = "";
15
+
16
+ beforeAll(async () => {
17
+ process.env.COMMHUB_DB = process.env.COMMHUB_DB || join(PRIVATE_DB_DIR, "hub.db");
18
+ username = `network_name_${Date.now()}_${process.pid}`;
19
+ const registered = register(username, "NetworkName123!", undefined, "seed");
20
+ expect(registered.ok).toBe(true);
21
+ userToken = registered.token!;
22
+ networkId = registered.network_id!;
23
+ const userId = db.get<{ user_id: string }>(
24
+ "SELECT user_id FROM users WHERE username = ?1",
25
+ [username],
26
+ )!.user_id;
27
+ const minted = createNetworkTokenForNode(userId, networkId, "network-name-node");
28
+ expect(minted.ok).toBe(true);
29
+ nodeToken = minted.token!;
30
+ const mod: any = await import("./server.js");
31
+ server = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
32
+ base = `http://127.0.0.1:${server.port}`;
33
+ });
34
+
35
+ afterAll(() => {
36
+ try { server?.stop?.(true); } catch {}
37
+ try { rmSync(PRIVATE_DB_DIR, { recursive: true, force: true }); } catch {}
38
+ });
39
+
40
+ async function networks(token: string): Promise<any[]> {
41
+ const response = await fetch(`${base}/api/networks`, {
42
+ headers: { Authorization: `Bearer ${token}` },
43
+ });
44
+ expect(response.status).toBe(200);
45
+ const body = await response.json() as any;
46
+ expect(body.ok).toBe(true);
47
+ return body.networks;
48
+ }
49
+
50
+ describe("GET /api/networks name compatibility (#449)", () => {
51
+ test("utok rows expose equal non-null name and network_name", async () => {
52
+ const rows = await networks(userToken);
53
+ expect(rows).toHaveLength(1);
54
+ expect(rows[0].network_id).toBe(networkId);
55
+ // #449 is about `name` and `network_name` agreeing and being non-null.
56
+ // Pin them to the registered username rather than the literal "default":
57
+ // the auto-created network is named after its owner, so hardcoding the
58
+ // old literal would test the naming scheme instead of the compat shim.
59
+ expect(rows[0].network_name).toBe(username);
60
+ expect(rows[0].name).toBe(username);
61
+ });
62
+
63
+ test("network-bound ntok rows expose the same alias", async () => {
64
+ const rows = await networks(nodeToken);
65
+ expect(rows).toHaveLength(1);
66
+ expect(rows[0].network_id).toBe(networkId);
67
+ expect(rows[0].network_name).toBe(username);
68
+ expect(rows[0].name).toBe(rows[0].network_name);
69
+ expect(rows[0].name).not.toBeNull();
70
+ });
71
+ });
@@ -72,6 +72,26 @@ export function singleNetworkId(scope: RestNetworkScope): string | null {
72
72
  return null;
73
73
  }
74
74
 
75
+ /**
76
+ * Resolve one unambiguous network for a REST write.
77
+ *
78
+ * Admin read scope is intentionally global (`networkIds=null`), so it cannot
79
+ * by itself express that an admin happens to have exactly one membership.
80
+ * Writes must not inherit that ambiguity: use the sole real membership when
81
+ * one exists, and keep requiring an explicit network for zero or 2+.
82
+ */
83
+ export function resolveRestWriteNetworkId(
84
+ scope: RestNetworkScope,
85
+ authCtx: { userId: string; networkId: string | null } | null,
86
+ isAdmin: boolean,
87
+ ): string | null {
88
+ const scoped = singleNetworkId(scope);
89
+ if (scoped) return scoped;
90
+ if (!authCtx || !isAdmin) return null;
91
+ const memberships = getUserNetworkIds(authCtx.userId);
92
+ return memberships.length === 1 ? memberships[0] : null;
93
+ }
94
+
75
95
  export function canRestWriteNetwork(authCtx: { userId: string; networkId: string | null } | null, networkId: string | null, isAdmin: boolean): boolean {
76
96
  if (!authCtx) return true; // legacy global token or open dev mode
77
97
  if (isAdmin) return true;
@@ -25,7 +25,7 @@ import { tmpdir } from "os";
25
25
  import { join } from "path";
26
26
 
27
27
  const tmpDb = join(mkdtempSync(join(tmpdir(), "anet-attrs-")), "commhub.db");
28
- process.env.COMMHUB_DB = tmpDb;
28
+ process.env.COMMHUB_DB ||= tmpDb;
29
29
  const PORT = 23000 + Math.floor(Math.random() * 1500);
30
30
  process.env.PORT = String(PORT);
31
31
  process.env.HOST = "127.0.0.1";