@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.
- package/.env.example +68 -0
- package/CHANGELOG.md +16 -0
- package/Caddyfile +8 -0
- package/Dockerfile +46 -0
- package/LICENSE +190 -0
- package/README.md +89 -0
- package/config.json.example +16 -0
- package/dist/__tests__/helpers.d.ts +56 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/helpers.js +138 -0
- package/dist/__tests__/helpers.js.map +1 -0
- package/dist/app.d.ts +38 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +60 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +88 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +148 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +15 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +47 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +14 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +26 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/not-found.d.ts +8 -0
- package/dist/middleware/not-found.d.ts.map +1 -0
- package/dist/middleware/not-found.js +12 -0
- package/dist/middleware/not-found.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +17 -0
- package/dist/middleware/request-logger.d.ts.map +1 -0
- package/dist/middleware/request-logger.js +65 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/middleware/ws-auth.d.ts +21 -0
- package/dist/middleware/ws-auth.d.ts.map +1 -0
- package/dist/middleware/ws-auth.js +59 -0
- package/dist/middleware/ws-auth.js.map +1 -0
- package/dist/routes/health.d.ts +11 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +34 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/messages.d.ts +19 -0
- package/dist/routes/messages.d.ts.map +1 -0
- package/dist/routes/messages.js +154 -0
- package/dist/routes/messages.js.map +1 -0
- package/dist/routes/peers.d.ts +17 -0
- package/dist/routes/peers.d.ts.map +1 -0
- package/dist/routes/peers.js +169 -0
- package/dist/routes/peers.js.map +1 -0
- package/dist/routes/poll.d.ts +15 -0
- package/dist/routes/poll.d.ts.map +1 -0
- package/dist/routes/poll.js +97 -0
- package/dist/routes/poll.js.map +1 -0
- package/dist/routes/sessions.d.ts +14 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +113 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/ui.d.ts +21 -0
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +173 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/routes/validation-hook.d.ts +18 -0
- package/dist/routes/validation-hook.d.ts.map +1 -0
- package/dist/routes/validation-hook.js +24 -0
- package/dist/routes/validation-hook.js.map +1 -0
- package/dist/services/auth-service.d.ts +48 -0
- package/dist/services/auth-service.d.ts.map +1 -0
- package/dist/services/auth-service.js +63 -0
- package/dist/services/auth-service.js.map +1 -0
- package/dist/services/connection-manager.d.ts +108 -0
- package/dist/services/connection-manager.d.ts.map +1 -0
- package/dist/services/connection-manager.js +216 -0
- package/dist/services/connection-manager.js.map +1 -0
- package/dist/services/message-queue.d.ts +56 -0
- package/dist/services/message-queue.d.ts.map +1 -0
- package/dist/services/message-queue.js +164 -0
- package/dist/services/message-queue.js.map +1 -0
- package/dist/services/peer-cleanup.d.ts +39 -0
- package/dist/services/peer-cleanup.d.ts.map +1 -0
- package/dist/services/peer-cleanup.js +96 -0
- package/dist/services/peer-cleanup.js.map +1 -0
- package/dist/services/session-cleanup.d.ts +44 -0
- package/dist/services/session-cleanup.d.ts.map +1 -0
- package/dist/services/session-cleanup.js +100 -0
- package/dist/services/session-cleanup.js.map +1 -0
- package/dist/services/session-store.d.ts +103 -0
- package/dist/services/session-store.d.ts.map +1 -0
- package/dist/services/session-store.js +292 -0
- package/dist/services/session-store.js.map +1 -0
- package/dist/services/stats-service.d.ts +48 -0
- package/dist/services/stats-service.d.ts.map +1 -0
- package/dist/services/stats-service.js +77 -0
- package/dist/services/stats-service.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/components/Footer.d.ts +7 -0
- package/dist/ui/components/Footer.d.ts.map +1 -0
- package/dist/ui/components/Footer.js +17 -0
- package/dist/ui/components/Footer.js.map +1 -0
- package/dist/ui/components/Layout.d.ts +13 -0
- package/dist/ui/components/Layout.d.ts.map +1 -0
- package/dist/ui/components/Layout.js +11 -0
- package/dist/ui/components/Layout.js.map +1 -0
- package/dist/ui/components/NavBar.d.ts +12 -0
- package/dist/ui/components/NavBar.d.ts.map +1 -0
- package/dist/ui/components/NavBar.js +60 -0
- package/dist/ui/components/NavBar.js.map +1 -0
- package/dist/ui/components/StatCard.d.ts +14 -0
- package/dist/ui/components/StatCard.d.ts.map +1 -0
- package/dist/ui/components/StatCard.js +32 -0
- package/dist/ui/components/StatCard.js.map +1 -0
- package/dist/ui/components/Terminal.d.ts +13 -0
- package/dist/ui/components/Terminal.d.ts.map +1 -0
- package/dist/ui/components/Terminal.js +37 -0
- package/dist/ui/components/Terminal.js.map +1 -0
- package/dist/ui/pages/AdminDashboard.d.ts +13 -0
- package/dist/ui/pages/AdminDashboard.d.ts.map +1 -0
- package/dist/ui/pages/AdminDashboard.js +59 -0
- package/dist/ui/pages/AdminDashboard.js.map +1 -0
- package/dist/ui/pages/HowToPage.d.ts +8 -0
- package/dist/ui/pages/HowToPage.d.ts.map +1 -0
- package/dist/ui/pages/HowToPage.js +312 -0
- package/dist/ui/pages/HowToPage.js.map +1 -0
- package/dist/ui/pages/LandingPage.d.ts +13 -0
- package/dist/ui/pages/LandingPage.d.ts.map +1 -0
- package/dist/ui/pages/LandingPage.js +160 -0
- package/dist/ui/pages/LandingPage.js.map +1 -0
- package/dist/ui/pages/MessageLog.d.ts +25 -0
- package/dist/ui/pages/MessageLog.d.ts.map +1 -0
- package/dist/ui/pages/MessageLog.js +146 -0
- package/dist/ui/pages/MessageLog.js.map +1 -0
- package/dist/ui/pages/SessionDetail.d.ts +14 -0
- package/dist/ui/pages/SessionDetail.d.ts.map +1 -0
- package/dist/ui/pages/SessionDetail.js +165 -0
- package/dist/ui/pages/SessionDetail.js.map +1 -0
- package/dist/ui/pages/SessionList.d.ts +22 -0
- package/dist/ui/pages/SessionList.d.ts.map +1 -0
- package/dist/ui/pages/SessionList.js +88 -0
- package/dist/ui/pages/SessionList.js.map +1 -0
- package/dist/ui/styles/theme.d.ts +35 -0
- package/dist/ui/styles/theme.d.ts.map +1 -0
- package/dist/ui/styles/theme.js +65 -0
- package/dist/ui/styles/theme.js.map +1 -0
- package/dist/ws/frames.d.ts +82 -0
- package/dist/ws/frames.d.ts.map +1 -0
- package/dist/ws/frames.js +68 -0
- package/dist/ws/frames.js.map +1 -0
- package/dist/ws/handler.d.ts +26 -0
- package/dist/ws/handler.d.ts.map +1 -0
- package/dist/ws/handler.js +72 -0
- package/dist/ws/handler.js.map +1 -0
- package/dist/ws/heartbeat.d.ts +18 -0
- package/dist/ws/heartbeat.d.ts.map +1 -0
- package/dist/ws/heartbeat.js +39 -0
- package/dist/ws/heartbeat.js.map +1 -0
- package/docker-compose.yml +38 -0
- package/nginx.conf.example +63 -0
- 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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"}
|