@essentialai/cogent-server 2.0.0

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 (166) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +16 -0
  3. package/Caddyfile +8 -0
  4. package/Dockerfile +46 -0
  5. package/LICENSE +190 -0
  6. package/README.md +89 -0
  7. package/config.json.example +16 -0
  8. package/dist/__tests__/helpers.d.ts +56 -0
  9. package/dist/__tests__/helpers.d.ts.map +1 -0
  10. package/dist/__tests__/helpers.js +138 -0
  11. package/dist/__tests__/helpers.js.map +1 -0
  12. package/dist/app.d.ts +38 -0
  13. package/dist/app.d.ts.map +1 -0
  14. package/dist/app.js +60 -0
  15. package/dist/app.js.map +1 -0
  16. package/dist/config.d.ts +88 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +148 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +102 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/middleware/auth.d.ts +15 -0
  25. package/dist/middleware/auth.d.ts.map +1 -0
  26. package/dist/middleware/auth.js +47 -0
  27. package/dist/middleware/auth.js.map +1 -0
  28. package/dist/middleware/error-handler.d.ts +14 -0
  29. package/dist/middleware/error-handler.d.ts.map +1 -0
  30. package/dist/middleware/error-handler.js +26 -0
  31. package/dist/middleware/error-handler.js.map +1 -0
  32. package/dist/middleware/not-found.d.ts +8 -0
  33. package/dist/middleware/not-found.d.ts.map +1 -0
  34. package/dist/middleware/not-found.js +12 -0
  35. package/dist/middleware/not-found.js.map +1 -0
  36. package/dist/middleware/request-logger.d.ts +17 -0
  37. package/dist/middleware/request-logger.d.ts.map +1 -0
  38. package/dist/middleware/request-logger.js +65 -0
  39. package/dist/middleware/request-logger.js.map +1 -0
  40. package/dist/middleware/ws-auth.d.ts +21 -0
  41. package/dist/middleware/ws-auth.d.ts.map +1 -0
  42. package/dist/middleware/ws-auth.js +59 -0
  43. package/dist/middleware/ws-auth.js.map +1 -0
  44. package/dist/routes/health.d.ts +11 -0
  45. package/dist/routes/health.d.ts.map +1 -0
  46. package/dist/routes/health.js +34 -0
  47. package/dist/routes/health.js.map +1 -0
  48. package/dist/routes/messages.d.ts +19 -0
  49. package/dist/routes/messages.d.ts.map +1 -0
  50. package/dist/routes/messages.js +154 -0
  51. package/dist/routes/messages.js.map +1 -0
  52. package/dist/routes/peers.d.ts +17 -0
  53. package/dist/routes/peers.d.ts.map +1 -0
  54. package/dist/routes/peers.js +169 -0
  55. package/dist/routes/peers.js.map +1 -0
  56. package/dist/routes/poll.d.ts +15 -0
  57. package/dist/routes/poll.d.ts.map +1 -0
  58. package/dist/routes/poll.js +97 -0
  59. package/dist/routes/poll.js.map +1 -0
  60. package/dist/routes/sessions.d.ts +14 -0
  61. package/dist/routes/sessions.d.ts.map +1 -0
  62. package/dist/routes/sessions.js +113 -0
  63. package/dist/routes/sessions.js.map +1 -0
  64. package/dist/routes/ui.d.ts +21 -0
  65. package/dist/routes/ui.d.ts.map +1 -0
  66. package/dist/routes/ui.js +173 -0
  67. package/dist/routes/ui.js.map +1 -0
  68. package/dist/routes/validation-hook.d.ts +18 -0
  69. package/dist/routes/validation-hook.d.ts.map +1 -0
  70. package/dist/routes/validation-hook.js +24 -0
  71. package/dist/routes/validation-hook.js.map +1 -0
  72. package/dist/services/auth-service.d.ts +48 -0
  73. package/dist/services/auth-service.d.ts.map +1 -0
  74. package/dist/services/auth-service.js +63 -0
  75. package/dist/services/auth-service.js.map +1 -0
  76. package/dist/services/connection-manager.d.ts +108 -0
  77. package/dist/services/connection-manager.d.ts.map +1 -0
  78. package/dist/services/connection-manager.js +216 -0
  79. package/dist/services/connection-manager.js.map +1 -0
  80. package/dist/services/message-queue.d.ts +56 -0
  81. package/dist/services/message-queue.d.ts.map +1 -0
  82. package/dist/services/message-queue.js +164 -0
  83. package/dist/services/message-queue.js.map +1 -0
  84. package/dist/services/peer-cleanup.d.ts +39 -0
  85. package/dist/services/peer-cleanup.d.ts.map +1 -0
  86. package/dist/services/peer-cleanup.js +96 -0
  87. package/dist/services/peer-cleanup.js.map +1 -0
  88. package/dist/services/session-cleanup.d.ts +44 -0
  89. package/dist/services/session-cleanup.d.ts.map +1 -0
  90. package/dist/services/session-cleanup.js +100 -0
  91. package/dist/services/session-cleanup.js.map +1 -0
  92. package/dist/services/session-store.d.ts +103 -0
  93. package/dist/services/session-store.d.ts.map +1 -0
  94. package/dist/services/session-store.js +292 -0
  95. package/dist/services/session-store.js.map +1 -0
  96. package/dist/services/stats-service.d.ts +48 -0
  97. package/dist/services/stats-service.d.ts.map +1 -0
  98. package/dist/services/stats-service.js +77 -0
  99. package/dist/services/stats-service.js.map +1 -0
  100. package/dist/types.d.ts +60 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/types.js +2 -0
  103. package/dist/types.js.map +1 -0
  104. package/dist/ui/components/Footer.d.ts +7 -0
  105. package/dist/ui/components/Footer.d.ts.map +1 -0
  106. package/dist/ui/components/Footer.js +17 -0
  107. package/dist/ui/components/Footer.js.map +1 -0
  108. package/dist/ui/components/Layout.d.ts +13 -0
  109. package/dist/ui/components/Layout.d.ts.map +1 -0
  110. package/dist/ui/components/Layout.js +11 -0
  111. package/dist/ui/components/Layout.js.map +1 -0
  112. package/dist/ui/components/NavBar.d.ts +12 -0
  113. package/dist/ui/components/NavBar.d.ts.map +1 -0
  114. package/dist/ui/components/NavBar.js +60 -0
  115. package/dist/ui/components/NavBar.js.map +1 -0
  116. package/dist/ui/components/StatCard.d.ts +14 -0
  117. package/dist/ui/components/StatCard.d.ts.map +1 -0
  118. package/dist/ui/components/StatCard.js +32 -0
  119. package/dist/ui/components/StatCard.js.map +1 -0
  120. package/dist/ui/components/Terminal.d.ts +13 -0
  121. package/dist/ui/components/Terminal.d.ts.map +1 -0
  122. package/dist/ui/components/Terminal.js +37 -0
  123. package/dist/ui/components/Terminal.js.map +1 -0
  124. package/dist/ui/pages/AdminDashboard.d.ts +13 -0
  125. package/dist/ui/pages/AdminDashboard.d.ts.map +1 -0
  126. package/dist/ui/pages/AdminDashboard.js +59 -0
  127. package/dist/ui/pages/AdminDashboard.js.map +1 -0
  128. package/dist/ui/pages/HowToPage.d.ts +8 -0
  129. package/dist/ui/pages/HowToPage.d.ts.map +1 -0
  130. package/dist/ui/pages/HowToPage.js +312 -0
  131. package/dist/ui/pages/HowToPage.js.map +1 -0
  132. package/dist/ui/pages/LandingPage.d.ts +13 -0
  133. package/dist/ui/pages/LandingPage.d.ts.map +1 -0
  134. package/dist/ui/pages/LandingPage.js +160 -0
  135. package/dist/ui/pages/LandingPage.js.map +1 -0
  136. package/dist/ui/pages/MessageLog.d.ts +25 -0
  137. package/dist/ui/pages/MessageLog.d.ts.map +1 -0
  138. package/dist/ui/pages/MessageLog.js +146 -0
  139. package/dist/ui/pages/MessageLog.js.map +1 -0
  140. package/dist/ui/pages/SessionDetail.d.ts +14 -0
  141. package/dist/ui/pages/SessionDetail.d.ts.map +1 -0
  142. package/dist/ui/pages/SessionDetail.js +165 -0
  143. package/dist/ui/pages/SessionDetail.js.map +1 -0
  144. package/dist/ui/pages/SessionList.d.ts +22 -0
  145. package/dist/ui/pages/SessionList.d.ts.map +1 -0
  146. package/dist/ui/pages/SessionList.js +88 -0
  147. package/dist/ui/pages/SessionList.js.map +1 -0
  148. package/dist/ui/styles/theme.d.ts +35 -0
  149. package/dist/ui/styles/theme.d.ts.map +1 -0
  150. package/dist/ui/styles/theme.js +65 -0
  151. package/dist/ui/styles/theme.js.map +1 -0
  152. package/dist/ws/frames.d.ts +82 -0
  153. package/dist/ws/frames.d.ts.map +1 -0
  154. package/dist/ws/frames.js +68 -0
  155. package/dist/ws/frames.js.map +1 -0
  156. package/dist/ws/handler.d.ts +26 -0
  157. package/dist/ws/handler.d.ts.map +1 -0
  158. package/dist/ws/handler.js +72 -0
  159. package/dist/ws/handler.js.map +1 -0
  160. package/dist/ws/heartbeat.d.ts +18 -0
  161. package/dist/ws/heartbeat.d.ts.map +1 -0
  162. package/dist/ws/heartbeat.js +39 -0
  163. package/dist/ws/heartbeat.js.map +1 -0
  164. package/docker-compose.yml +38 -0
  165. package/nginx.conf.example +63 -0
  166. package/package.json +61 -0
@@ -0,0 +1,17 @@
1
+ import type { ServerEnv } from "../types.js";
2
+ /**
3
+ * Request/response logger middleware.
4
+ *
5
+ * In production mode (NODE_ENV=production):
6
+ * - Outputs a single JSON line per request for log aggregation
7
+ * - Skips debug-level header logging entirely (security: avoid header leakage)
8
+ *
9
+ * In non-production mode:
10
+ * - Uses human-readable format with optional debug header logging
11
+ * - CRITICAL (AUTH-04): Authorization header values are ALWAYS replaced
12
+ * with [REDACTED] in log output to prevent bearer token leakage.
13
+ *
14
+ * Uses console.error (stderr) matching the existing project convention.
15
+ */
16
+ export declare function requestLogger(): import("hono").MiddlewareHandler<ServerEnv, string, {}, Response>;
17
+ //# sourceMappingURL=request-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-logger.d.ts","sourceRoot":"","sources":["../../src/middleware/request-logger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAe7C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,sEA2C5B"}
@@ -0,0 +1,65 @@
1
+ import { createMiddleware } from "hono/factory";
2
+ /**
3
+ * Write a structured JSON log entry to stderr.
4
+ * Used in production mode for machine-parseable log aggregation.
5
+ */
6
+ function logJson(entry) {
7
+ console.error(JSON.stringify(entry));
8
+ }
9
+ /**
10
+ * Request/response logger middleware.
11
+ *
12
+ * In production mode (NODE_ENV=production):
13
+ * - Outputs a single JSON line per request for log aggregation
14
+ * - Skips debug-level header logging entirely (security: avoid header leakage)
15
+ *
16
+ * In non-production mode:
17
+ * - Uses human-readable format with optional debug header logging
18
+ * - CRITICAL (AUTH-04): Authorization header values are ALWAYS replaced
19
+ * with [REDACTED] in log output to prevent bearer token leakage.
20
+ *
21
+ * Uses console.error (stderr) matching the existing project convention.
22
+ */
23
+ export function requestLogger() {
24
+ return createMiddleware(async (c, next) => {
25
+ const method = c.req.method;
26
+ const path = c.req.path;
27
+ const start = Date.now();
28
+ const isProduction = process.env.NODE_ENV === "production";
29
+ // In non-production mode, log request headers at debug level
30
+ if (!isProduction && process.env.COGENT_SERVER_LOG_LEVEL === "debug") {
31
+ // Sanitize headers for debug logging
32
+ const headers = {};
33
+ c.req.raw.headers.forEach((value, key) => {
34
+ headers[key] =
35
+ key.toLowerCase() === "authorization" ? "[REDACTED]" : value;
36
+ });
37
+ console.error(`--> ${method} ${path}`, JSON.stringify(headers));
38
+ }
39
+ await next();
40
+ const duration = Date.now() - start;
41
+ const status = c.res.status;
42
+ if (isProduction) {
43
+ // JSON structured logging for production log aggregation
44
+ logJson({
45
+ timestamp: new Date().toISOString(),
46
+ level: status >= 400 ? "error" : "info",
47
+ message: "http_request",
48
+ method,
49
+ path,
50
+ status,
51
+ duration,
52
+ });
53
+ }
54
+ else {
55
+ // Human-readable format for development
56
+ if (status >= 400) {
57
+ console.error(`<-- ${method} ${path} ${status} ${duration}ms`);
58
+ }
59
+ else if (process.env.COGENT_SERVER_LOG_LEVEL === "debug") {
60
+ console.error(`<-- ${method} ${path} ${status} ${duration}ms`);
61
+ }
62
+ }
63
+ });
64
+ }
65
+ //# sourceMappingURL=request-logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-logger.js","sourceRoot":"","sources":["../../src/middleware/request-logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD;;;GAGG;AACH,SAAS,OAAO,CAAC,KAKhB;IACC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,gBAAgB,CAAY,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACnD,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;QAC5B,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;QAE3D,6DAA6D;QAC7D,IAAI,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,OAAO,EAAE,CAAC;YACrE,qCAAqC;YACrC,MAAM,OAAO,GAA2B,EAAE,CAAC;YAC3C,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;gBACvC,OAAO,CAAC,GAAG,CAAC;oBACV,GAAG,CAAC,WAAW,EAAE,KAAK,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;YACjE,CAAC,CAAC,CAAC;YACH,OAAO,CAAC,KAAK,CAAC,OAAO,MAAM,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,IAAI,EAAE,CAAC;QAEb,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QACpC,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;QAE5B,IAAI,YAAY,EAAE,CAAC;YACjB,yDAAyD;YACzD,OAAO,CAAC;gBACN,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,KAAK,EAAE,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;gBACvC,OAAO,EAAE,cAAc;gBACvB,MAAM;gBACN,IAAI;gBACJ,MAAM;gBACN,QAAQ;aACT,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,wCAAwC;YACxC,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;gBAClB,OAAO,CAAC,KAAK,CAAC,OAAO,MAAM,IAAI,IAAI,IAAI,MAAM,IAAI,QAAQ,IAAI,CAAC,CAAC;YACjE,CAAC;iBAAM,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,OAAO,EAAE,CAAC;gBAC3D,OAAO,CAAC,KAAK,CAAC,OAAO,MAAM,IAAI,IAAI,IAAI,MAAM,IAAI,QAAQ,IAAI,CAAC,CAAC;YACjE,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { SessionStore } from "../services/session-store.js";
2
+ import type { AuthService } from "../services/auth-service.js";
3
+ import type { ServerEnv } from "../types.js";
4
+ /**
5
+ * Create WebSocket-specific auth middleware.
6
+ *
7
+ * Unlike the standard bearer auth middleware (which reads the Authorization
8
+ * header), this middleware reads the token from the `token` query parameter.
9
+ * WebSocket clients pass auth via query params because browser WebSocket APIs
10
+ * cannot set custom HTTP headers. Our CLI clients use query params for
11
+ * consistency with the standard WebSocket authentication pattern.
12
+ *
13
+ * This middleware runs BEFORE `upgradeWebSocket`, so auth failures return
14
+ * HTTP error responses (never establish a WebSocket connection).
15
+ *
16
+ * Additionally validates that the `peerId` query parameter is present and
17
+ * that the peer is registered in the session -- preventing unregistered
18
+ * peers from establishing WebSocket connections.
19
+ */
20
+ export declare function createWsAuthMiddleware(sessionStore: SessionStore, authService: AuthService): import("hono").MiddlewareHandler<ServerEnv, string, {}, Response>;
21
+ //# sourceMappingURL=ws-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-auth.d.ts","sourceRoot":"","sources":["../../src/middleware/ws-auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,qEA0EzB"}
@@ -0,0 +1,59 @@
1
+ import { createMiddleware } from "hono/factory";
2
+ import { BridgeError, ErrorCode } from "@essentialai/cogent";
3
+ /**
4
+ * Create WebSocket-specific auth middleware.
5
+ *
6
+ * Unlike the standard bearer auth middleware (which reads the Authorization
7
+ * header), this middleware reads the token from the `token` query parameter.
8
+ * WebSocket clients pass auth via query params because browser WebSocket APIs
9
+ * cannot set custom HTTP headers. Our CLI clients use query params for
10
+ * consistency with the standard WebSocket authentication pattern.
11
+ *
12
+ * This middleware runs BEFORE `upgradeWebSocket`, so auth failures return
13
+ * HTTP error responses (never establish a WebSocket connection).
14
+ *
15
+ * Additionally validates that the `peerId` query parameter is present and
16
+ * that the peer is registered in the session -- preventing unregistered
17
+ * peers from establishing WebSocket connections.
18
+ */
19
+ export function createWsAuthMiddleware(sessionStore, authService) {
20
+ return createMiddleware(async (c, next) => {
21
+ // 1. Extract token from query parameter
22
+ const token = c.req.query("token");
23
+ if (!token) {
24
+ throw new BridgeError(ErrorCode.AUTH_MISSING_TOKEN, "Token query parameter required for WebSocket connection", "Include '?token=<bearer-token>' in the WebSocket URL");
25
+ }
26
+ // 2. Hash the token with SHA-256 for index lookup
27
+ const tokenHash = authService.hashToken(token);
28
+ // 3. Look up session by token hash
29
+ const result = await sessionStore.getSessionByTokenHash(tokenHash);
30
+ if (!result) {
31
+ throw new BridgeError(ErrorCode.AUTH_INVALID_TOKEN, "Invalid or expired token");
32
+ }
33
+ // 4. Defense-in-depth: timing-safe comparison of the token hash
34
+ const storedTokenHash = result.state.tokens.find((t) => t.tokenHash === tokenHash)?.tokenHash;
35
+ if (!storedTokenHash ||
36
+ !authService.timingSafeCompare(tokenHash, storedTokenHash)) {
37
+ throw new BridgeError(ErrorCode.AUTH_INVALID_TOKEN, "Invalid or expired token");
38
+ }
39
+ // 5. Verify path sessionId matches the authenticated session
40
+ const pathSessionId = c.req.param("sessionId");
41
+ if (pathSessionId !== result.sessionId) {
42
+ throw new BridgeError(ErrorCode.AUTH_INVALID_TOKEN, "Token does not match the requested session");
43
+ }
44
+ // 6. Validate peerId query parameter is present and registered
45
+ const peerId = c.req.query("peerId");
46
+ if (!peerId) {
47
+ throw new BridgeError(ErrorCode.PEER_NOT_FOUND, "peerId query parameter required for WebSocket connection", "Include '&peerId=<peer-id>' in the WebSocket URL");
48
+ }
49
+ const peer = result.state.peers[peerId];
50
+ if (!peer) {
51
+ throw new BridgeError(ErrorCode.PEER_NOT_FOUND, `Peer '${peerId}' is not registered in this session`, "Register the peer via POST /api/sessions/:sessionId/peers first");
52
+ }
53
+ // 7. Set session context for downstream handlers
54
+ c.set("sessionId", result.sessionId);
55
+ c.set("session", result.state);
56
+ await next();
57
+ });
58
+ }
59
+ //# sourceMappingURL=ws-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-auth.js","sourceRoot":"","sources":["../../src/middleware/ws-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAK7D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,sBAAsB,CACpC,YAA0B,EAC1B,WAAwB;IAExB,OAAO,gBAAgB,CAAY,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACnD,wCAAwC;QACxC,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,WAAW,CACnB,SAAS,CAAC,kBAAkB,EAC5B,yDAAyD,EACzD,sDAAsD,CACvD,CAAC;QACJ,CAAC;QAED,kDAAkD;QAClD,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE/C,mCAAmC;QACnC,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,WAAW,CACnB,SAAS,CAAC,kBAAkB,EAC5B,0BAA0B,CAC3B,CAAC;QACJ,CAAC;QAED,gEAAgE;QAChE,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAC9C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CACjC,EAAE,SAAS,CAAC;QAEb,IACE,CAAC,eAAe;YAChB,CAAC,WAAW,CAAC,iBAAiB,CAAC,SAAS,EAAE,eAAe,CAAC,EAC1D,CAAC;YACD,MAAM,IAAI,WAAW,CACnB,SAAS,CAAC,kBAAkB,EAC5B,0BAA0B,CAC3B,CAAC;QACJ,CAAC;QAED,6DAA6D;QAC7D,MAAM,aAAa,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAC/C,IAAI,aAAa,KAAK,MAAM,CAAC,SAAS,EAAE,CAAC;YACvC,MAAM,IAAI,WAAW,CACnB,SAAS,CAAC,kBAAkB,EAC5B,4CAA4C,CAC7C,CAAC;QACJ,CAAC;QAED,+DAA+D;QAC/D,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,WAAW,CACnB,SAAS,CAAC,cAAc,EACxB,0DAA0D,EAC1D,kDAAkD,CACnD,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,WAAW,CACnB,SAAS,CAAC,cAAc,EACxB,SAAS,MAAM,qCAAqC,EACpD,iEAAiE,CAClE,CAAC;QACJ,CAAC;QAED,iDAAiD;QACjD,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAE/B,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { Hono } from "hono";
2
+ import type { SessionStore } from "../services/session-store.js";
3
+ import type { ConnectionManager } from "../services/connection-manager.js";
4
+ import type { ServerEnv } from "../types.js";
5
+ /**
6
+ * Create health check routes sub-router.
7
+ *
8
+ * GET / -> Server health status (maps to /api/health via app.route)
9
+ */
10
+ export declare function createHealthRoutes(sessionStore: SessionStore, startTime: number, connectionManager?: ConnectionManager): Hono<ServerEnv>;
11
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/routes/health.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,MAAM,EACjB,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,IAAI,CAAC,SAAS,CAAC,CAuBjB"}
@@ -0,0 +1,34 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Hono } from "hono";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8"));
7
+ const SERVER_VERSION = pkg.version;
8
+ /**
9
+ * Create health check routes sub-router.
10
+ *
11
+ * GET / -> Server health status (maps to /api/health via app.route)
12
+ */
13
+ export function createHealthRoutes(sessionStore, startTime, connectionManager) {
14
+ const health = new Hono();
15
+ /**
16
+ * GET / - Server health check.
17
+ *
18
+ * Returns server status, uptime, version, active session count,
19
+ * and connected peer count from the WebSocket connection manager.
20
+ */
21
+ health.get("/", async (c) => {
22
+ const uptimeSeconds = (Date.now() - startTime) / 1000;
23
+ const sessions = await sessionStore.listSessions();
24
+ return c.json({
25
+ status: "ok",
26
+ uptime: uptimeSeconds,
27
+ version: SERVER_VERSION,
28
+ activeSessions: sessions.length,
29
+ connectedPeers: connectionManager?.getTotalConnectedPeers() ?? 0,
30
+ });
31
+ });
32
+ return health;
33
+ }
34
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/routes/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;AACrF,MAAM,cAAc,GAAW,GAAG,CAAC,OAAO,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,YAA0B,EAC1B,SAAiB,EACjB,iBAAqC;IAErC,MAAM,MAAM,GAAG,IAAI,IAAI,EAAa,CAAC;IAErC;;;;;OAKG;IACH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1B,MAAM,aAAa,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;QACtD,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,YAAY,EAAE,CAAC;QAEnD,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,IAAa;YACrB,MAAM,EAAE,aAAa;YACrB,OAAO,EAAE,cAAc;YACvB,cAAc,EAAE,QAAQ,CAAC,MAAM;YAC/B,cAAc,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,IAAI,CAAC;SACjE,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { Hono } from "hono";
2
+ import type { SessionStore } from "../services/session-store.js";
3
+ import type { AuthService } from "../services/auth-service.js";
4
+ import type { ConnectionManager } from "../services/connection-manager.js";
5
+ import type { MessageQueueService } from "../services/message-queue.js";
6
+ import type { ServerEnv } from "../types.js";
7
+ /**
8
+ * Create message routes sub-router.
9
+ *
10
+ * POST /:sessionId/messages -> Send a message (direct or broadcast)
11
+ * GET /:sessionId/messages -> Retrieve message history with filtering/pagination
12
+ *
13
+ * All routes require bearer token auth. The auth middleware sets
14
+ * sessionId and session on the Hono context.
15
+ *
16
+ * @param maxMessages - Maximum messages to retain per session (oldest trimmed on write)
17
+ */
18
+ export declare function createMessageRoutes(sessionStore: SessionStore, authService: AuthService, maxMessages: number, connectionManager?: ConnectionManager, messageQueue?: MessageQueueService): Hono<ServerEnv>;
19
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/routes/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAU5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAK7C;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,MAAM,EACnB,iBAAiB,CAAC,EAAE,iBAAiB,EACrC,YAAY,CAAC,EAAE,mBAAmB,GACjC,IAAI,CAAC,SAAS,CAAC,CA8LjB"}
@@ -0,0 +1,154 @@
1
+ import { Hono } from "hono";
2
+ import { zValidator } from "@hono/zod-validator";
3
+ import { BridgeError, ErrorCode, SendMessageRequestSchema, GetHistoryQuerySchema, generateMessageId, } from "@essentialai/cogent";
4
+ import { createAuthMiddleware } from "../middleware/auth.js";
5
+ import { validationHook } from "./validation-hook.js";
6
+ import { createMessageFrame } from "../ws/frames.js";
7
+ /**
8
+ * Create message routes sub-router.
9
+ *
10
+ * POST /:sessionId/messages -> Send a message (direct or broadcast)
11
+ * GET /:sessionId/messages -> Retrieve message history with filtering/pagination
12
+ *
13
+ * All routes require bearer token auth. The auth middleware sets
14
+ * sessionId and session on the Hono context.
15
+ *
16
+ * @param maxMessages - Maximum messages to retain per session (oldest trimmed on write)
17
+ */
18
+ export function createMessageRoutes(sessionStore, authService, maxMessages, connectionManager, messageQueue) {
19
+ const messages = new Hono();
20
+ // Auth middleware applied per-route (not wildcard) to avoid intercepting
21
+ // unauthenticated session routes mounted at the same /api/sessions prefix.
22
+ const auth = createAuthMiddleware(sessionStore, authService);
23
+ /**
24
+ * POST /:sessionId/messages - Send a message (direct or broadcast).
25
+ *
26
+ * Direct: toPeerId is a specific peer ID (must exist in session).
27
+ * Broadcast: toPeerId is "*" (delivered to all peers, stored as-is).
28
+ *
29
+ * Updates sender's lastSeenAt as a heartbeat on activity.
30
+ * Enforces per-session message cap by trimming oldest messages.
31
+ */
32
+ messages.post("/:sessionId/messages", auth, zValidator("json", SendMessageRequestSchema, validationHook), async (c) => {
33
+ const authSessionId = c.get("sessionId");
34
+ const pathSessionId = c.req.param("sessionId");
35
+ // Verify path param matches authenticated session
36
+ if (pathSessionId !== authSessionId) {
37
+ throw new BridgeError(ErrorCode.AUTH_INVALID_TOKEN, "Token does not belong to this session");
38
+ }
39
+ const { fromPeerId, toPeerId, message } = c.req.valid("json");
40
+ // Variables to capture for the response (set inside updateSession callback)
41
+ let messageId;
42
+ let timestamp;
43
+ let record;
44
+ await sessionStore.updateSession(authSessionId, (state) => {
45
+ // Verify sender is a registered peer in this session
46
+ if (!state.peers[fromPeerId]) {
47
+ throw new BridgeError(ErrorCode.PEER_NOT_FOUND, "Sender peer not registered in session", `Register peer '${fromPeerId}' before sending messages`);
48
+ }
49
+ // For direct messages, verify recipient exists (skip for broadcast)
50
+ if (toPeerId !== "*" && !state.peers[toPeerId]) {
51
+ throw new BridgeError(ErrorCode.PEER_NOT_FOUND, "Recipient peer not found in session", `Check that peer '${toPeerId}' is registered in the session`);
52
+ }
53
+ // Update sender's lastSeenAt (heartbeat on activity)
54
+ const now = new Date().toISOString();
55
+ state.peers[fromPeerId].lastSeenAt = now;
56
+ // Generate message ID and create the record
57
+ messageId = generateMessageId();
58
+ timestamp = now;
59
+ record = {
60
+ id: messageId,
61
+ fromPeerId,
62
+ toPeerId,
63
+ message,
64
+ response: null,
65
+ timestamp,
66
+ durationMs: null,
67
+ success: true,
68
+ error: null,
69
+ };
70
+ state.messages.push(record);
71
+ // Enforce message cap (session store also does this, belt-and-suspenders)
72
+ if (state.messages.length > maxMessages) {
73
+ state.messages = state.messages.slice(-maxMessages);
74
+ }
75
+ return state;
76
+ });
77
+ // Push message to connected WebSocket peers (fire-and-forget)
78
+ if (connectionManager) {
79
+ try {
80
+ const frame = createMessageFrame(record);
81
+ const data = JSON.stringify(frame);
82
+ if (toPeerId === "*") {
83
+ // Broadcast: send to all connected peers except the sender
84
+ connectionManager.broadcastToSession(authSessionId, data, fromPeerId);
85
+ }
86
+ else {
87
+ // Direct message: push to recipient if connected, else queue for offline
88
+ if (connectionManager.isConnected(authSessionId, toPeerId)) {
89
+ connectionManager.sendToPeer(authSessionId, toPeerId, data);
90
+ }
91
+ else if (messageQueue) {
92
+ await messageQueue.enqueue(authSessionId, toPeerId, record);
93
+ }
94
+ }
95
+ }
96
+ catch (err) {
97
+ // WS push is best-effort -- log but don't fail the REST response
98
+ console.error("WebSocket push failed:", err);
99
+ }
100
+ }
101
+ return c.json({
102
+ id: messageId,
103
+ success: true,
104
+ timestamp: timestamp,
105
+ }, 201);
106
+ });
107
+ /**
108
+ * GET /:sessionId/messages - Retrieve message history.
109
+ *
110
+ * Supports optional filtering by peerId and pagination via limit/offset.
111
+ * When peerId is provided, returns messages where the peer is sender,
112
+ * recipient, or the message is a broadcast (toPeerId: "*").
113
+ */
114
+ messages.get("/:sessionId/messages", auth, zValidator("query", GetHistoryQuerySchema, validationHook), async (c) => {
115
+ const authSessionId = c.get("sessionId");
116
+ const pathSessionId = c.req.param("sessionId");
117
+ // Verify path param matches authenticated session
118
+ if (pathSessionId !== authSessionId) {
119
+ throw new BridgeError(ErrorCode.AUTH_INVALID_TOKEN, "Token does not belong to this session");
120
+ }
121
+ const query = c.req.valid("query");
122
+ const peerId = query.peerId;
123
+ // Zod schema provides defaults but the type annotation includes undefined
124
+ const limit = query.limit ?? 300;
125
+ const offset = query.offset ?? 0;
126
+ // Fresh read from store (not cached from auth middleware)
127
+ const session = await sessionStore.getSession(authSessionId);
128
+ if (!session) {
129
+ throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${authSessionId} not found`);
130
+ }
131
+ // Filter messages by peer if specified
132
+ let filtered = session.messages;
133
+ if (peerId) {
134
+ filtered = filtered.filter((msg) => msg.fromPeerId === peerId ||
135
+ msg.toPeerId === peerId ||
136
+ msg.toPeerId === "*");
137
+ }
138
+ // Compute total before pagination
139
+ const total = filtered.length;
140
+ // Apply pagination (newest-first: offset counts back from the end)
141
+ // offset=0, limit=20 → last 20 messages (most recent)
142
+ // offset=20, limit=20 → messages 20-40 from the end (next page back)
143
+ // Returned slice is still chronological (oldest-first within the page).
144
+ const endPos = Math.max(0, filtered.length - offset);
145
+ const startPos = Math.max(0, endPos - limit);
146
+ const paginated = filtered.slice(startPos, endPos);
147
+ return c.json({
148
+ messages: paginated,
149
+ total,
150
+ });
151
+ });
152
+ return messages;
153
+ }
154
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/routes/messages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { Hono } from "hono";
2
+ import type { SessionStore } from "../services/session-store.js";
3
+ import type { AuthService } from "../services/auth-service.js";
4
+ import type { ConnectionManager } from "../services/connection-manager.js";
5
+ import type { ServerEnv } from "../types.js";
6
+ /**
7
+ * Create peer management routes sub-router.
8
+ *
9
+ * POST /:sessionId/peers -> Register a peer in a session
10
+ * GET /:sessionId/peers -> List all peers in a session
11
+ * DELETE /:sessionId/peers/:peerId -> Deregister a peer
12
+ *
13
+ * All routes require valid Bearer token authentication.
14
+ * The sub-router is mounted at /api/sessions via app.route().
15
+ */
16
+ export declare function createPeerRoutes(sessionStore: SessionStore, authService: AuthService, connectionManager?: ConnectionManager): Hono<ServerEnv>;
17
+ //# sourceMappingURL=peers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peers.d.ts","sourceRoot":"","sources":["../../src/routes/peers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAS5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAsB7C;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,EACxB,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,IAAI,CAAC,SAAS,CAAC,CA6LjB"}
@@ -0,0 +1,169 @@
1
+ import { Hono } from "hono";
2
+ import { zValidator } from "@hono/zod-validator";
3
+ import { BridgeError, ErrorCode, RegisterPeerRequestSchema, DeregisterPeerRequestSchema, } from "@essentialai/cogent";
4
+ import { createAuthMiddleware } from "../middleware/auth.js";
5
+ import { validationHook } from "./validation-hook.js";
6
+ import { createPeerConnectedFrame, createPeerDisconnectedFrame, } from "../ws/frames.js";
7
+ /**
8
+ * Verify that the path parameter sessionId matches the auth context sessionId.
9
+ * Prevents token misuse across sessions.
10
+ */
11
+ function verifySessionMatch(pathSessionId, authSessionId) {
12
+ if (pathSessionId !== authSessionId) {
13
+ throw new BridgeError(ErrorCode.AUTH_INVALID_TOKEN, "Token does not belong to this session", "Use a token issued for this session");
14
+ }
15
+ }
16
+ /**
17
+ * Create peer management routes sub-router.
18
+ *
19
+ * POST /:sessionId/peers -> Register a peer in a session
20
+ * GET /:sessionId/peers -> List all peers in a session
21
+ * DELETE /:sessionId/peers/:peerId -> Deregister a peer
22
+ *
23
+ * All routes require valid Bearer token authentication.
24
+ * The sub-router is mounted at /api/sessions via app.route().
25
+ */
26
+ export function createPeerRoutes(sessionStore, authService, connectionManager) {
27
+ const peers = new Hono();
28
+ // Auth middleware applied per-route (not wildcard) to avoid intercepting
29
+ // unauthenticated session routes mounted at the same /api/sessions prefix.
30
+ const auth = createAuthMiddleware(sessionStore, authService);
31
+ /**
32
+ * POST /:sessionId/peers - Register a new peer in a session.
33
+ *
34
+ * Validates the request body against RegisterPeerRequestSchema,
35
+ * creates a PeerInfo record, adds it to the session state, and
36
+ * records a peer_connected event.
37
+ *
38
+ * Returns 201 with the PeerInfo object.
39
+ */
40
+ peers.post("/:sessionId/peers", auth, zValidator("json", RegisterPeerRequestSchema, validationHook), async (c) => {
41
+ const pathSessionId = c.req.param("sessionId");
42
+ const authSessionId = c.get("sessionId");
43
+ verifySessionMatch(pathSessionId, authSessionId);
44
+ const { peerId, cwd, label } = c.req.valid("json");
45
+ const now = new Date().toISOString();
46
+ const peerInfo = {
47
+ peerId,
48
+ sessionId: authSessionId,
49
+ cwd,
50
+ label,
51
+ registeredAt: now,
52
+ lastSeenAt: now,
53
+ };
54
+ await sessionStore.updateSession(authSessionId, (state) => {
55
+ // Add peer to session
56
+ state.peers[peerId] = peerInfo;
57
+ // Record peer_connected event
58
+ const event = {
59
+ type: "peer_connected",
60
+ peerId,
61
+ timestamp: now,
62
+ };
63
+ state.peerEvents.push(event);
64
+ return state;
65
+ });
66
+ // Broadcast peer_connected event to connected WebSocket peers (fire-and-forget)
67
+ if (connectionManager) {
68
+ try {
69
+ const frame = createPeerConnectedFrame({
70
+ peerId,
71
+ name: label || peerId,
72
+ project: cwd,
73
+ });
74
+ const data = JSON.stringify(frame);
75
+ // Exclude the registering peer (not yet connected via WS at registration time)
76
+ connectionManager.broadcastToSession(authSessionId, data, peerId);
77
+ }
78
+ catch (err) {
79
+ console.error("WebSocket peer_connected broadcast failed:", err);
80
+ }
81
+ }
82
+ return c.json(peerInfo, 201);
83
+ });
84
+ /**
85
+ * GET /:sessionId/peers - List all peers in a session.
86
+ *
87
+ * Returns the full list of currently registered peers.
88
+ * Stale peers are cleaned up by the background PeerCleanup service,
89
+ * so this endpoint simply returns whatever is in the session state.
90
+ */
91
+ peers.get("/:sessionId/peers", auth, async (c) => {
92
+ const pathSessionId = c.req.param("sessionId");
93
+ const authSessionId = c.get("sessionId");
94
+ verifySessionMatch(pathSessionId, authSessionId);
95
+ // Re-read from store for maximum freshness
96
+ const session = await sessionStore.getSession(authSessionId);
97
+ if (!session) {
98
+ throw new BridgeError(ErrorCode.SESSION_NOT_FOUND, `Session ${authSessionId} not found`, "Check the session ID or create a new session");
99
+ }
100
+ const peerList = Object.values(session.peers);
101
+ return c.json({ peers: peerList });
102
+ });
103
+ /**
104
+ * DELETE /:sessionId/peers/:peerId - Deregister a peer from a session.
105
+ *
106
+ * Removes the peer from the session state and records a
107
+ * peer_disconnected event. Returns 204 (no body).
108
+ */
109
+ peers.delete("/:sessionId/peers/:peerId", auth, zValidator("json", DeregisterPeerRequestSchema, validationHook), async (c) => {
110
+ const pathSessionId = c.req.param("sessionId");
111
+ const authSessionId = c.get("sessionId");
112
+ verifySessionMatch(pathSessionId, authSessionId);
113
+ const peerId = c.req.param("peerId");
114
+ const now = new Date().toISOString();
115
+ await sessionStore.updateSession(authSessionId, (state) => {
116
+ // Verify peer exists
117
+ if (!state.peers[peerId]) {
118
+ throw new BridgeError(ErrorCode.PEER_NOT_FOUND, `Peer ${peerId} not found in session ${authSessionId}`, "Check the peer ID or register the peer first");
119
+ }
120
+ // Remove the peer
121
+ delete state.peers[peerId];
122
+ // Record peer_disconnected event
123
+ const event = {
124
+ type: "peer_disconnected",
125
+ peerId,
126
+ timestamp: now,
127
+ };
128
+ state.peerEvents.push(event);
129
+ return state;
130
+ });
131
+ // Broadcast peer_disconnected event to connected WebSocket peers (fire-and-forget)
132
+ if (connectionManager) {
133
+ try {
134
+ const frame = createPeerDisconnectedFrame(peerId);
135
+ const data = JSON.stringify(frame);
136
+ // Exclude the deregistering peer from the broadcast
137
+ connectionManager.broadcastToSession(authSessionId, data, peerId);
138
+ }
139
+ catch (err) {
140
+ console.error("WebSocket peer_disconnected broadcast failed:", err);
141
+ }
142
+ }
143
+ return c.body(null, 204);
144
+ });
145
+ /**
146
+ * POST /:sessionId/heartbeat - Lightweight keepalive for a registered peer.
147
+ *
148
+ * Updates the peer's lastSeenAt timestamp. Returns 204 with no body.
149
+ * Designed for periodic client heartbeats to prevent stale peer cleanup.
150
+ */
151
+ peers.post("/:sessionId/heartbeat", auth, async (c) => {
152
+ const pathSessionId = c.req.param("sessionId");
153
+ const authSessionId = c.get("sessionId");
154
+ verifySessionMatch(pathSessionId, authSessionId);
155
+ const { peerId } = await c.req.json();
156
+ if (!peerId || typeof peerId !== "string") {
157
+ throw new BridgeError(ErrorCode.INVALID_INPUT, "peerId is required", "Include { peerId: 'your-peer-id' } in the request body");
158
+ }
159
+ await sessionStore.updateSession(authSessionId, (state) => {
160
+ if (state.peers[peerId]) {
161
+ state.peers[peerId].lastSeenAt = new Date().toISOString();
162
+ }
163
+ return state;
164
+ });
165
+ return c.body(null, 204);
166
+ });
167
+ return peers;
168
+ }
169
+ //# sourceMappingURL=peers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peers.js","sourceRoot":"","sources":["../../src/routes/peers.ts"],"names":[],"mappings":"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"}