@zhanguym/chatroom 1.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 (223) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +52 -0
  3. package/assets/chatroom.svg +15 -0
  4. package/chatroom.config.example.json +24 -0
  5. package/dist/app/application.d.ts +20 -0
  6. package/dist/app/application.js +75 -0
  7. package/dist/app/event-bus.d.ts +25 -0
  8. package/dist/app/event-bus.js +11 -0
  9. package/dist/app/external-access-registry.d.ts +17 -0
  10. package/dist/app/external-access-registry.js +41 -0
  11. package/dist/app/lifecycle.d.ts +14 -0
  12. package/dist/app/lifecycle.js +130 -0
  13. package/dist/auth/auth-service.d.ts +88 -0
  14. package/dist/auth/auth-service.js +221 -0
  15. package/dist/auth/ingress-policy.d.ts +23 -0
  16. package/dist/auth/ingress-policy.js +61 -0
  17. package/dist/auth/passkey-service.d.ts +39 -0
  18. package/dist/auth/passkey-service.js +145 -0
  19. package/dist/cli/index.d.ts +2 -0
  20. package/dist/cli/index.js +10 -0
  21. package/dist/cli/run-cli.d.ts +1 -0
  22. package/dist/cli/run-cli.js +41 -0
  23. package/dist/config/load-config.d.ts +7 -0
  24. package/dist/config/load-config.js +197 -0
  25. package/dist/config/platform-paths.d.ts +7 -0
  26. package/dist/config/platform-paths.js +34 -0
  27. package/dist/config/types.d.ts +24 -0
  28. package/dist/config/types.js +1 -0
  29. package/dist/core/auth/passkey-repository.d.ts +18 -0
  30. package/dist/core/auth/passkey-repository.js +1 -0
  31. package/dist/core/auth/repository.d.ts +36 -0
  32. package/dist/core/auth/repository.js +1 -0
  33. package/dist/core/auth/session-repository.d.ts +6 -0
  34. package/dist/core/auth/session-repository.js +1 -0
  35. package/dist/core/errors/chatroom-error.d.ts +9 -0
  36. package/dist/core/errors/chatroom-error.js +31 -0
  37. package/dist/core/logging/types.d.ts +31 -0
  38. package/dist/core/logging/types.js +1 -0
  39. package/dist/core/operations/bounded-value.d.ts +6 -0
  40. package/dist/core/operations/bounded-value.js +57 -0
  41. package/dist/core/operations/redactor.d.ts +4 -0
  42. package/dist/core/operations/redactor.js +69 -0
  43. package/dist/core/operations/repository.d.ts +18 -0
  44. package/dist/core/operations/repository.js +1 -0
  45. package/dist/core/operations/types.d.ts +28 -0
  46. package/dist/core/operations/types.js +8 -0
  47. package/dist/core/runtime/child-environment.d.ts +1 -0
  48. package/dist/core/runtime/child-environment.js +39 -0
  49. package/dist/core/runtime/command-runner.d.ts +16 -0
  50. package/dist/core/runtime/command-runner.js +48 -0
  51. package/dist/core/runtime/identity.d.ts +2 -0
  52. package/dist/core/runtime/identity.js +9 -0
  53. package/dist/infrastructure/database/app-database.d.ts +6 -0
  54. package/dist/infrastructure/database/app-database.js +22 -0
  55. package/dist/infrastructure/database/mcp-tool-settings-repository.d.ts +8 -0
  56. package/dist/infrastructure/database/mcp-tool-settings-repository.js +22 -0
  57. package/dist/infrastructure/database/oauth-repository.d.ts +20 -0
  58. package/dist/infrastructure/database/oauth-repository.js +140 -0
  59. package/dist/infrastructure/database/operation-repository.d.ts +16 -0
  60. package/dist/infrastructure/database/operation-repository.js +120 -0
  61. package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
  62. package/dist/infrastructure/database/passkey-repository.js +57 -0
  63. package/dist/infrastructure/database/schema.d.ts +1 -0
  64. package/dist/infrastructure/database/schema.js +90 -0
  65. package/dist/infrastructure/database/web-session-repository.d.ts +10 -0
  66. package/dist/infrastructure/database/web-session-repository.js +26 -0
  67. package/dist/infrastructure/http/http-server.d.ts +28 -0
  68. package/dist/infrastructure/http/http-server.js +146 -0
  69. package/dist/infrastructure/http/ingress-middleware.d.ts +6 -0
  70. package/dist/infrastructure/http/ingress-middleware.js +58 -0
  71. package/dist/infrastructure/logging/file-log-store.d.ts +9 -0
  72. package/dist/infrastructure/logging/file-log-store.js +173 -0
  73. package/dist/infrastructure/logging/system-log.d.ts +22 -0
  74. package/dist/infrastructure/logging/system-log.js +117 -0
  75. package/dist/mcp/server/create-mcp-server.d.ts +3 -0
  76. package/dist/mcp/server/create-mcp-server.js +16 -0
  77. package/dist/mcp/server/plugin-mcp-registrar.d.ts +31 -0
  78. package/dist/mcp/server/plugin-mcp-registrar.js +70 -0
  79. package/dist/mcp/server/request-context.d.ts +3 -0
  80. package/dist/mcp/server/request-context.js +8 -0
  81. package/dist/mcp/server/tool-control.d.ts +31 -0
  82. package/dist/mcp/server/tool-control.js +76 -0
  83. package/dist/mcp/server/tool-support.d.ts +44 -0
  84. package/dist/mcp/server/tool-support.js +72 -0
  85. package/dist/native/linux/chatroom-computer-helper +0 -0
  86. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +14 -0
  87. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  88. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/_CodeSignature/CodeResources +115 -0
  89. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  90. package/dist/operations/operation-log.d.ts +24 -0
  91. package/dist/operations/operation-log.js +98 -0
  92. package/dist/plugins/cloud/api-client.d.ts +50 -0
  93. package/dist/plugins/cloud/api-client.js +112 -0
  94. package/dist/plugins/cloud/controller.d.ts +58 -0
  95. package/dist/plugins/cloud/controller.js +425 -0
  96. package/dist/plugins/cloud/device-proof.d.ts +9 -0
  97. package/dist/plugins/cloud/device-proof.js +17 -0
  98. package/dist/plugins/cloud/plugin.d.ts +4 -0
  99. package/dist/plugins/cloud/plugin.js +17 -0
  100. package/dist/plugins/cloud/state-store.d.ts +7 -0
  101. package/dist/plugins/cloud/state-store.js +79 -0
  102. package/dist/plugins/cloud/tunnel-client.d.ts +84 -0
  103. package/dist/plugins/cloud/tunnel-client.js +427 -0
  104. package/dist/plugins/cloud/types.d.ts +72 -0
  105. package/dist/plugins/cloud/types.js +60 -0
  106. package/dist/plugins/computer/audit.d.ts +34 -0
  107. package/dist/plugins/computer/audit.js +42 -0
  108. package/dist/plugins/computer/computer-native-backend.d.ts +11 -0
  109. package/dist/plugins/computer/computer-native-backend.js +58 -0
  110. package/dist/plugins/computer/computer-native-host.d.ts +30 -0
  111. package/dist/plugins/computer/computer-native-host.js +411 -0
  112. package/dist/plugins/computer/computer-protocol.d.ts +22 -0
  113. package/dist/plugins/computer/computer-protocol.js +74 -0
  114. package/dist/plugins/computer/computer-schemas.d.ts +285 -0
  115. package/dist/plugins/computer/computer-schemas.js +152 -0
  116. package/dist/plugins/computer/computer-service.d.ts +29 -0
  117. package/dist/plugins/computer/computer-service.js +117 -0
  118. package/dist/plugins/computer/computer-settings-repository.d.ts +8 -0
  119. package/dist/plugins/computer/computer-settings-repository.js +41 -0
  120. package/dist/plugins/computer/mcp.d.ts +3 -0
  121. package/dist/plugins/computer/mcp.js +126 -0
  122. package/dist/plugins/computer/plugin.d.ts +4 -0
  123. package/dist/plugins/computer/plugin.js +29 -0
  124. package/dist/plugins/computer/types.d.ts +174 -0
  125. package/dist/plugins/computer/types.js +1 -0
  126. package/dist/plugins/git/git-service.d.ts +30 -0
  127. package/dist/plugins/git/git-service.js +428 -0
  128. package/dist/plugins/git/plugin.d.ts +4 -0
  129. package/dist/plugins/git/plugin.js +12 -0
  130. package/dist/plugins/git/types.d.ts +32 -0
  131. package/dist/plugins/git/types.js +1 -0
  132. package/dist/plugins/plugin-manager.d.ts +11 -0
  133. package/dist/plugins/plugin-manager.js +63 -0
  134. package/dist/plugins/process/backend.d.ts +18 -0
  135. package/dist/plugins/process/backend.js +1 -0
  136. package/dist/plugins/process/head-tail-buffer.d.ts +17 -0
  137. package/dist/plugins/process/head-tail-buffer.js +49 -0
  138. package/dist/plugins/process/infrastructure/pipe-process-backend.d.ts +8 -0
  139. package/dist/plugins/process/infrastructure/pipe-process-backend.js +71 -0
  140. package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
  141. package/dist/plugins/process/infrastructure/pty-process-backend.js +51 -0
  142. package/dist/plugins/process/mcp.d.ts +3 -0
  143. package/dist/plugins/process/mcp.js +58 -0
  144. package/dist/plugins/process/plugin.d.ts +4 -0
  145. package/dist/plugins/process/plugin.js +25 -0
  146. package/dist/plugins/process/process-supervisor.d.ts +39 -0
  147. package/dist/plugins/process/process-supervisor.js +244 -0
  148. package/dist/plugins/process/types.d.ts +30 -0
  149. package/dist/plugins/process/types.js +1 -0
  150. package/dist/plugins/types.d.ts +35 -0
  151. package/dist/plugins/types.js +17 -0
  152. package/dist/plugins/web/api-types.d.ts +56 -0
  153. package/dist/plugins/web/api-types.js +1 -0
  154. package/dist/plugins/web/http/api-router.d.ts +13 -0
  155. package/dist/plugins/web/http/api-router.js +47 -0
  156. package/dist/plugins/web/http/auth-api-router.d.ts +8 -0
  157. package/dist/plugins/web/http/auth-api-router.js +123 -0
  158. package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
  159. package/dist/plugins/web/http/cloud-api-router.js +29 -0
  160. package/dist/plugins/web/http/computer-api-router.d.ts +5 -0
  161. package/dist/plugins/web/http/computer-api-router.js +69 -0
  162. package/dist/plugins/web/http/git-api-router.d.ts +3 -0
  163. package/dist/plugins/web/http/git-api-router.js +85 -0
  164. package/dist/plugins/web/http/logs-api-router.d.ts +3 -0
  165. package/dist/plugins/web/http/logs-api-router.js +49 -0
  166. package/dist/plugins/web/http/process-api-router.d.ts +3 -0
  167. package/dist/plugins/web/http/process-api-router.js +16 -0
  168. package/dist/plugins/web/http/runtime-api-router.d.ts +10 -0
  169. package/dist/plugins/web/http/runtime-api-router.js +34 -0
  170. package/dist/plugins/web/http/workspace-api-router.d.ts +3 -0
  171. package/dist/plugins/web/http/workspace-api-router.js +91 -0
  172. package/dist/plugins/web/plugin.d.ts +8 -0
  173. package/dist/plugins/web/plugin.js +17 -0
  174. package/dist/plugins/web/runtime.d.ts +18 -0
  175. package/dist/plugins/web/runtime.js +31 -0
  176. package/dist/plugins/workspace/mcp.d.ts +3 -0
  177. package/dist/plugins/workspace/mcp.js +24 -0
  178. package/dist/plugins/workspace/metadata.d.ts +6 -0
  179. package/dist/plugins/workspace/metadata.js +111 -0
  180. package/dist/plugins/workspace/plugin.d.ts +4 -0
  181. package/dist/plugins/workspace/plugin.js +22 -0
  182. package/dist/plugins/workspace/types.d.ts +23 -0
  183. package/dist/plugins/workspace/types.js +1 -0
  184. package/dist/plugins/workspace/workspace-fs.d.ts +28 -0
  185. package/dist/plugins/workspace/workspace-fs.js +200 -0
  186. package/dist/plugins/workspace/workspace-service.d.ts +15 -0
  187. package/dist/plugins/workspace/workspace-service.js +140 -0
  188. package/dist/presentation/http/http-utils.d.ts +9 -0
  189. package/dist/presentation/http/http-utils.js +89 -0
  190. package/dist/presentation/http/oauth-router.d.ts +5 -0
  191. package/dist/presentation/http/oauth-router.js +172 -0
  192. package/dist/web/assets/CloudView-CHjVbp-5.css +1 -0
  193. package/dist/web/assets/CloudView-CvX0dkgu.js +1 -0
  194. package/dist/web/assets/CodeViewer-CH1DEJge.css +1 -0
  195. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-B5TxGkUW.js +3 -0
  196. package/dist/web/assets/ComputerView-91Z9Cefw.css +1 -0
  197. package/dist/web/assets/ComputerView-OOyEqWml.js +1 -0
  198. package/dist/web/assets/McpToolsView-BTnolyS6.css +1 -0
  199. package/dist/web/assets/McpToolsView-DjXWTKrZ.js +1 -0
  200. package/dist/web/assets/OperationsView-DqTY51Cr.js +4 -0
  201. package/dist/web/assets/ProcessesView-C13BykIa.js +4 -0
  202. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DyOhJmR6.js +1 -0
  203. package/dist/web/assets/SystemLogsView-Ci0u4BTb.js +1 -0
  204. package/dist/web/assets/VSelect-CAxEESw-.css +1 -0
  205. package/dist/web/assets/VSelect-DrRpKyYI.js +1 -0
  206. package/dist/web/assets/VSpacer-C3wAY3F0.js +1 -0
  207. package/dist/web/assets/VSpacer-DfbUir7X.css +1 -0
  208. package/dist/web/assets/VSwitch-Cos8PNSP.css +1 -0
  209. package/dist/web/assets/VSwitch-CxusR4iO.js +1 -0
  210. package/dist/web/assets/VTabs-DSWgbwhc.css +1 -0
  211. package/dist/web/assets/VTabs-qBcuuXh3.js +1 -0
  212. package/dist/web/assets/WorkspacesView-BzQ0aH74.css +1 -0
  213. package/dist/web/assets/WorkspacesView-CijIvfsp.js +2 -0
  214. package/dist/web/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
  215. package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
  216. package/dist/web/assets/index-BDkarAkX.js +2 -0
  217. package/dist/web/assets/index-BegKQ3Yd.js +19 -0
  218. package/dist/web/assets/index-CkjqXRSj.css +1 -0
  219. package/dist/web/assets/syntax-highlight-B5ArnyrC.js +3 -0
  220. package/dist/web/assets/useOperationLabels-BDjzGDkT.js +1 -0
  221. package/dist/web/assets/utils-rtPwWNel.js +1 -0
  222. package/dist/web/index.html +19 -0
  223. package/package.json +125 -0
@@ -0,0 +1,28 @@
1
+ import type { AddressInfo } from "node:net";
2
+ import type { McpHttpHandler } from "@modelcontextprotocol/server";
3
+ import type { ChatRoomConfig } from "#config/types";
4
+ import type { WebRuntime } from "#plugins/web/runtime";
5
+ import type { RuntimeEventBus } from "#app/event-bus";
6
+ import type { ExternalAccessRegistry } from "#app/external-access-registry";
7
+ import type { AuthService } from "#auth/auth-service";
8
+ import type { PasskeyService } from "#auth/passkey-service";
9
+ import type { CloudController } from "#plugins/cloud/controller";
10
+ import type { LogService } from "#core/logging/types";
11
+ export declare class HttpServer {
12
+ private readonly config;
13
+ private readonly application;
14
+ private readonly eventBus;
15
+ private readonly auth;
16
+ private readonly passkeys;
17
+ private readonly mcp;
18
+ private readonly cloud;
19
+ private readonly logs;
20
+ private server;
21
+ private readonly ingress;
22
+ private readonly startedAt;
23
+ private mcpRequestCount;
24
+ constructor(config: ChatRoomConfig, application: WebRuntime, eventBus: RuntimeEventBus, auth: AuthService, passkeys: PasskeyService, mcp: McpHttpHandler, externalAccess: ExternalAccessRegistry, cloud: CloudController, logs: LogService);
25
+ start(): Promise<void>;
26
+ address(): AddressInfo | null;
27
+ close(): Promise<void>;
28
+ }
@@ -0,0 +1,146 @@
1
+ import { createServer } from "node:http";
2
+ import path from "node:path";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import express from "express";
6
+ import compression from "compression";
7
+ import { toNodeHandler } from "@modelcontextprotocol/node";
8
+ import { createApiRouter } from "#plugins/web/http/api-router";
9
+ import { createOAuthRouter } from "#presentation/http/oauth-router";
10
+ import { errorMiddleware } from "#presentation/http/http-utils";
11
+ import { IngressPolicy } from "#auth/ingress-policy";
12
+ import { CHATROOM_VERSION } from "#core/runtime/identity";
13
+ import { runWithMcpAccessScope } from "#mcp/server/request-context";
14
+ import { hostValidation, mcpAuthentication, webMutationOrigin, } from "./ingress-middleware.js";
15
+ const WEB_UI_RESERVED_PREFIXES = [
16
+ "/api",
17
+ "/mcp",
18
+ "/oauth",
19
+ "/.well-known",
20
+ "/assets",
21
+ ];
22
+ export class HttpServer {
23
+ config;
24
+ application;
25
+ eventBus;
26
+ auth;
27
+ passkeys;
28
+ mcp;
29
+ cloud;
30
+ logs;
31
+ server = null;
32
+ ingress;
33
+ startedAt = Date.now();
34
+ mcpRequestCount = 0;
35
+ constructor(config, application, eventBus, auth, passkeys, mcp, externalAccess, cloud, logs) {
36
+ this.config = config;
37
+ this.application = application;
38
+ this.eventBus = eventBus;
39
+ this.auth = auth;
40
+ this.passkeys = passkeys;
41
+ this.mcp = mcp;
42
+ this.cloud = cloud;
43
+ this.logs = logs;
44
+ this.ingress = new IngressPolicy(config, externalAccess);
45
+ }
46
+ async start() {
47
+ const app = express();
48
+ app.disable("x-powered-by");
49
+ app.set("trust proxy", false);
50
+ app.use(hostValidation(this.ingress));
51
+ app.use(compression({ threshold: 1024 }));
52
+ app.use(express.json({ limit: "2mb" }));
53
+ app.use(express.urlencoded({ extended: false, limit: "64kb" }));
54
+ app.use(createOAuthRouter(this.auth, this.ingress, this.logs));
55
+ app.use("/api", webMutationOrigin(this.ingress));
56
+ app.use("/api", createApiRouter(this.application, this.eventBus, this.auth, this.passkeys, this.ingress, this.cloud, this.logs, () => ({
57
+ version: CHATROOM_VERSION,
58
+ mcpRequests: this.mcpRequestCount,
59
+ uptimeMinutes: Math.floor((Date.now() - this.startedAt) / 60000),
60
+ })));
61
+ const nodeMcp = toNodeHandler(this.mcp, {
62
+ onerror: (error) => this.logs.error("mcp", "mcp.error", "MCP request failed", { error }),
63
+ });
64
+ app.all("/mcp", (_req, _res, next) => {
65
+ this.mcpRequestCount += 1;
66
+ next();
67
+ }, mcpAuthentication(this.auth, this.ingress), (req, res) => {
68
+ const scope = this.ingress.isExternalMcp(req) ? "remote" : "local";
69
+ runWithMcpAccessScope(scope, () => {
70
+ void nodeMcp(req, res, req.body);
71
+ });
72
+ });
73
+ const webRoot = fileURLToPath(new URL("../../web/", import.meta.url));
74
+ const webIndexPath = path.join(webRoot, "index.html");
75
+ if (existsSync(webIndexPath)) {
76
+ const webIndex = readFileSync(webIndexPath, "utf8");
77
+ app.use("/assets", express.static(path.join(webRoot, "assets"), {
78
+ index: false,
79
+ maxAge: "1y",
80
+ immutable: true,
81
+ }));
82
+ app.use((req, res, next) => {
83
+ const isReserved = WEB_UI_RESERVED_PREFIXES.some((prefix) => req.path === prefix || req.path.startsWith(`${prefix}/`));
84
+ if (req.method !== "GET" || isReserved || !req.accepts("html")) {
85
+ next();
86
+ return;
87
+ }
88
+ res.setHeader("Cache-Control", "no-cache");
89
+ res.status(200).type("html").send(webIndex);
90
+ });
91
+ }
92
+ app.use(errorMiddleware);
93
+ this.server = createServer(app);
94
+ try {
95
+ await new Promise((resolve, reject) => {
96
+ this.server.once("error", reject);
97
+ this.server.listen(this.config.server.port, this.config.server.host, () => {
98
+ this.server.off("error", reject);
99
+ resolve();
100
+ });
101
+ });
102
+ this.logs.info("http", "http.started", "HTTP server started", {
103
+ host: this.config.server.host,
104
+ port: this.config.server.port,
105
+ });
106
+ }
107
+ catch (error) {
108
+ this.logs.error("http", "http.start_failed", "HTTP server failed to start", {
109
+ error,
110
+ });
111
+ throw error;
112
+ }
113
+ }
114
+ address() {
115
+ const value = this.server?.address();
116
+ return value && typeof value === "object" ? value : null;
117
+ }
118
+ async close() {
119
+ const server = this.server;
120
+ if (!server)
121
+ return;
122
+ this.server = null;
123
+ const errors = [];
124
+ const closed = server.listening
125
+ ? new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
126
+ : Promise.resolve();
127
+ try {
128
+ await this.mcp.close();
129
+ }
130
+ catch (error) {
131
+ errors.push(error);
132
+ }
133
+ server.closeAllConnections();
134
+ try {
135
+ await closed;
136
+ }
137
+ catch (error) {
138
+ errors.push(error);
139
+ }
140
+ if (errors.length === 1)
141
+ throw errors[0];
142
+ if (errors.length > 1)
143
+ throw new AggregateError(errors, "HTTP server shutdown encountered errors");
144
+ this.logs.info("http", "http.stopped", "HTTP server stopped");
145
+ }
146
+ }
@@ -0,0 +1,6 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { AuthService } from "#auth/auth-service";
3
+ import type { IngressPolicy } from "#auth/ingress-policy";
4
+ export declare function webMutationOrigin(ingress: IngressPolicy): RequestHandler;
5
+ export declare function hostValidation(ingress: IngressPolicy): RequestHandler;
6
+ export declare function mcpAuthentication(auth: AuthService, ingress: IngressPolicy): RequestHandler;
@@ -0,0 +1,58 @@
1
+ export function webMutationOrigin(ingress) {
2
+ return (req, res, next) => {
3
+ if (req.method === "GET" ||
4
+ req.method === "HEAD" ||
5
+ req.method === "OPTIONS") {
6
+ next();
7
+ return;
8
+ }
9
+ const expectedOrigin = ingress.expectedWebOrigin(req);
10
+ if (!expectedOrigin) {
11
+ next();
12
+ return;
13
+ }
14
+ if (req.headers.origin !== expectedOrigin) {
15
+ res.status(403).json({
16
+ error: {
17
+ code: "FORBIDDEN",
18
+ message: "Request origin is not allowed",
19
+ },
20
+ });
21
+ return;
22
+ }
23
+ next();
24
+ };
25
+ }
26
+ export function hostValidation(ingress) {
27
+ return (req, res, next) => {
28
+ const hostname = req.hostname;
29
+ if (!hostname || !ingress.allowsHost(hostname)) {
30
+ res.status(403).json({
31
+ error: { code: "FORBIDDEN", message: "Host header is not allowed" },
32
+ });
33
+ return;
34
+ }
35
+ next();
36
+ };
37
+ }
38
+ export function mcpAuthentication(auth, ingress) {
39
+ return (req, res, next) => {
40
+ if (!ingress.requiresMcpAuth(req)) {
41
+ next();
42
+ return;
43
+ }
44
+ const header = req.headers.authorization;
45
+ const token = typeof header === "string" && header.startsWith("Bearer ")
46
+ ? header.slice(7)
47
+ : null;
48
+ const info = token ? auth.verifyMcpToken(token) : null;
49
+ if (!info) {
50
+ const base = ingress.mcpBaseUrl(req);
51
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource/mcp"`);
52
+ res.status(401).json({ error: "invalid_token" });
53
+ return;
54
+ }
55
+ req.auth = info;
56
+ next();
57
+ };
58
+ }
@@ -0,0 +1,9 @@
1
+ import type { LogPage, LogQuery, LogRecord } from "#core/logging/types";
2
+ export declare class FileLogStore {
3
+ readonly filePath: string;
4
+ private currentBytes;
5
+ constructor(dataDir: string);
6
+ append(record: LogRecord): Promise<void>;
7
+ list(query: LogQuery): Promise<LogPage>;
8
+ private rotate;
9
+ }
@@ -0,0 +1,173 @@
1
+ import { appendFile, mkdir, open, rename, rm, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ChatRoomError } from "#core/errors/chatroom-error";
4
+ const MAX_FILE_BYTES = 10 * 1024 * 1024;
5
+ const MAX_FILES = 5;
6
+ const READ_CHUNK_BYTES = 64 * 1024;
7
+ export class FileLogStore {
8
+ filePath;
9
+ currentBytes = null;
10
+ constructor(dataDir) {
11
+ this.filePath = path.join(dataDir, "logs", "chatroom.log");
12
+ }
13
+ async append(record) {
14
+ const line = `${JSON.stringify(record)}\n`;
15
+ await mkdir(path.dirname(this.filePath), { recursive: true });
16
+ if (this.currentBytes === null) {
17
+ this.currentBytes = await stat(this.filePath)
18
+ .then((value) => value.size)
19
+ .catch((error) => {
20
+ if (error.code === "ENOENT")
21
+ return 0;
22
+ throw error;
23
+ });
24
+ }
25
+ const bytes = Buffer.byteLength(line);
26
+ if (this.currentBytes > 0 && this.currentBytes + bytes > MAX_FILE_BYTES) {
27
+ await this.rotate();
28
+ this.currentBytes = 0;
29
+ }
30
+ await appendFile(this.filePath, line, { encoding: "utf8", mode: 0o600 });
31
+ this.currentBytes += bytes;
32
+ }
33
+ async list(query) {
34
+ const cursor = query.cursor ? decodeCursor(query.cursor) : null;
35
+ let cursorPassed = cursor === null;
36
+ const matches = [];
37
+ const wanted = query.limit + 1;
38
+ for (let index = 0; index < MAX_FILES && matches.length < wanted; index += 1) {
39
+ const file = index === 0 ? this.filePath : `${this.filePath}.${index}`;
40
+ for await (const line of readLinesReverse(file)) {
41
+ const record = parseRecord(line);
42
+ if (!record)
43
+ continue;
44
+ if (!cursorPassed && cursor) {
45
+ if (record.id === cursor.id &&
46
+ record.timestamp === cursor.timestamp) {
47
+ cursorPassed = true;
48
+ continue;
49
+ }
50
+ if (record.timestamp < cursor.timestamp)
51
+ cursorPassed = true;
52
+ else
53
+ continue;
54
+ }
55
+ if (query.levels && !query.levels.has(record.level))
56
+ continue;
57
+ if (query.modules && !query.modules.has(record.module))
58
+ continue;
59
+ matches.push(record);
60
+ if (matches.length >= wanted)
61
+ break;
62
+ }
63
+ }
64
+ const hasMore = matches.length > query.limit;
65
+ const items = hasMore ? matches.slice(0, query.limit) : matches;
66
+ const last = items.at(-1);
67
+ return {
68
+ items,
69
+ nextCursor: hasMore && last ? encodeCursor(last) : null,
70
+ };
71
+ }
72
+ async rotate() {
73
+ await rm(`${this.filePath}.${MAX_FILES - 1}`, { force: true });
74
+ for (let index = MAX_FILES - 2; index >= 1; index -= 1) {
75
+ await renameIfExists(`${this.filePath}.${index}`, `${this.filePath}.${index + 1}`);
76
+ }
77
+ await renameIfExists(this.filePath, `${this.filePath}.1`);
78
+ }
79
+ }
80
+ async function* readLinesReverse(file) {
81
+ const handle = await open(file, "r").catch((error) => {
82
+ if (error.code === "ENOENT")
83
+ return null;
84
+ throw error;
85
+ });
86
+ if (!handle)
87
+ return;
88
+ try {
89
+ const { size } = await handle.stat();
90
+ let position = size;
91
+ let remainder = Buffer.alloc(0);
92
+ while (position > 0) {
93
+ const length = Math.min(READ_CHUNK_BYTES, position);
94
+ position -= length;
95
+ const buffer = Buffer.allocUnsafe(length);
96
+ let offset = 0;
97
+ while (offset < length) {
98
+ const { bytesRead } = await handle.read(buffer, offset, length - offset, position + offset);
99
+ if (bytesRead === 0)
100
+ break;
101
+ offset += bytesRead;
102
+ }
103
+ const chunk = Buffer.concat([buffer.subarray(0, offset), remainder]);
104
+ let end = chunk.length;
105
+ for (let index = chunk.length - 1; index >= 0; index -= 1) {
106
+ if (chunk[index] !== 0x0a)
107
+ continue;
108
+ const line = chunk
109
+ .subarray(index + 1, end)
110
+ .toString("utf8")
111
+ .trim();
112
+ if (line)
113
+ yield line;
114
+ end = index;
115
+ }
116
+ remainder = chunk.subarray(0, end);
117
+ }
118
+ const firstLine = remainder.toString("utf8").trim();
119
+ if (firstLine)
120
+ yield firstLine;
121
+ }
122
+ finally {
123
+ await handle.close();
124
+ }
125
+ }
126
+ async function renameIfExists(from, to) {
127
+ try {
128
+ await rename(from, to);
129
+ }
130
+ catch (error) {
131
+ if (error.code !== "ENOENT")
132
+ throw error;
133
+ }
134
+ }
135
+ function parseRecord(line) {
136
+ try {
137
+ const value = JSON.parse(line);
138
+ if (typeof value.id !== "string" ||
139
+ typeof value.timestamp !== "string" ||
140
+ !isLevel(value.level) ||
141
+ typeof value.module !== "string" ||
142
+ typeof value.event !== "string" ||
143
+ typeof value.message !== "string")
144
+ return null;
145
+ return value;
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ function isLevel(value) {
152
+ return (value === "debug" ||
153
+ value === "info" ||
154
+ value === "warn" ||
155
+ value === "error");
156
+ }
157
+ function encodeCursor(record) {
158
+ return Buffer.from(JSON.stringify({ timestamp: record.timestamp, id: record.id }), "utf8").toString("base64url");
159
+ }
160
+ function decodeCursor(cursor) {
161
+ try {
162
+ const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
163
+ if (typeof value.timestamp !== "string" ||
164
+ Number.isNaN(Date.parse(value.timestamp)) ||
165
+ typeof value.id !== "string" ||
166
+ !value.id)
167
+ throw new Error();
168
+ return { timestamp: value.timestamp, id: value.id };
169
+ }
170
+ catch {
171
+ throw new ChatRoomError("INVALID_INPUT", "Invalid log cursor");
172
+ }
173
+ }
@@ -0,0 +1,22 @@
1
+ import type { LogPage, LogQuery, LogRecord, LogService } from "#core/logging/types";
2
+ import { FileLogStore } from "./file-log-store.js";
3
+ export declare class SystemLog implements LogService {
4
+ private readonly store;
5
+ private readonly redactor;
6
+ private readonly listeners;
7
+ private queue;
8
+ private persistenceDisabled;
9
+ private lastTimestampMs;
10
+ constructor(store: FileLogStore);
11
+ debug(module: string, event: string, message: string, data?: Record<string, unknown>): void;
12
+ info(module: string, event: string, message: string, data?: Record<string, unknown>): void;
13
+ warn(module: string, event: string, message: string, data?: Record<string, unknown>): void;
14
+ error(module: string, event: string, message: string, data?: Record<string, unknown>): void;
15
+ list(query: LogQuery): Promise<LogPage>;
16
+ subscribe(listener: (record: LogRecord) => void): () => void;
17
+ flush(): Promise<void>;
18
+ private write;
19
+ private nextTimestamp;
20
+ private writeConsole;
21
+ private disablePersistence;
22
+ }
@@ -0,0 +1,117 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { SecretRedactor } from "#core/operations/redactor";
3
+ export class SystemLog {
4
+ store;
5
+ redactor = new SecretRedactor();
6
+ listeners = new Set();
7
+ queue = Promise.resolve();
8
+ persistenceDisabled = false;
9
+ lastTimestampMs = 0;
10
+ constructor(store) {
11
+ this.store = store;
12
+ }
13
+ debug(module, event, message, data) {
14
+ this.write("debug", module, event, message, data);
15
+ }
16
+ info(module, event, message, data) {
17
+ this.write("info", module, event, message, data);
18
+ }
19
+ warn(module, event, message, data) {
20
+ this.write("warn", module, event, message, data);
21
+ }
22
+ error(module, event, message, data) {
23
+ this.write("error", module, event, message, data);
24
+ }
25
+ async list(query) {
26
+ await this.flush();
27
+ return this.store.list(query);
28
+ }
29
+ subscribe(listener) {
30
+ this.listeners.add(listener);
31
+ return () => this.listeners.delete(listener);
32
+ }
33
+ async flush() {
34
+ await this.queue;
35
+ }
36
+ write(level, module, event, message, data) {
37
+ const redacted = this.redactor.redact({
38
+ message,
39
+ ...(data ? { data: normalizeValue(data) } : {}),
40
+ });
41
+ const record = {
42
+ id: randomUUID(),
43
+ timestamp: this.nextTimestamp(),
44
+ level,
45
+ module,
46
+ event,
47
+ message: redacted.message,
48
+ ...(redacted.data && Object.keys(redacted.data).length
49
+ ? { data: redacted.data }
50
+ : {}),
51
+ };
52
+ this.writeConsole(record);
53
+ for (const listener of this.listeners) {
54
+ try {
55
+ listener(record);
56
+ }
57
+ catch {
58
+ this.listeners.delete(listener);
59
+ console.warn("[WARN] [logging] Log subscriber removed after failure");
60
+ }
61
+ }
62
+ if (this.persistenceDisabled)
63
+ return;
64
+ this.queue = this.queue
65
+ .then(() => this.store.append(record))
66
+ .catch((error) => this.disablePersistence(error));
67
+ }
68
+ nextTimestamp() {
69
+ const now = Date.now();
70
+ this.lastTimestampMs = Math.max(now, this.lastTimestampMs + 1);
71
+ return new Date(this.lastTimestampMs).toISOString();
72
+ }
73
+ writeConsole(record) {
74
+ const suffix = record.data ? ` ${JSON.stringify(record.data)}` : "";
75
+ const line = `[${record.level.toUpperCase()}] [${record.module}] ${record.message}${suffix}`;
76
+ if (record.level === "error")
77
+ console.error(line);
78
+ else if (record.level === "warn")
79
+ console.warn(line);
80
+ else
81
+ console.log(line);
82
+ }
83
+ disablePersistence(error) {
84
+ if (this.persistenceDisabled)
85
+ return;
86
+ this.persistenceDisabled = true;
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ console.warn(`[WARN] [logging] File logging unavailable: ${message}`);
89
+ }
90
+ }
91
+ function normalizeValue(value, seen = new WeakSet()) {
92
+ if (typeof value === "bigint")
93
+ return value.toString();
94
+ if (value === null || typeof value !== "object")
95
+ return value;
96
+ if (seen.has(value))
97
+ return "[circular]";
98
+ seen.add(value);
99
+ try {
100
+ if (value instanceof Error) {
101
+ return {
102
+ name: value.name,
103
+ message: value.message,
104
+ ...(value.cause ? { cause: normalizeValue(value.cause, seen) } : {}),
105
+ };
106
+ }
107
+ if (Array.isArray(value))
108
+ return value.map((item) => normalizeValue(item, seen));
109
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
110
+ key,
111
+ normalizeValue(item, seen),
112
+ ]));
113
+ }
114
+ finally {
115
+ seen.delete(value);
116
+ }
117
+ }
@@ -0,0 +1,3 @@
1
+ import { type McpHttpHandler } from "@modelcontextprotocol/server";
2
+ import type { PluginManager } from "#plugins/plugin-manager";
3
+ export declare function createChatRoomMcpHandler(plugins: PluginManager): McpHttpHandler;
@@ -0,0 +1,16 @@
1
+ import { createMcpHandler, McpServer, } from "@modelcontextprotocol/server";
2
+ import { CHATROOM_NAME, CHATROOM_VERSION } from "#core/runtime/identity";
3
+ export function createChatRoomMcpHandler(plugins) {
4
+ return createMcpHandler(() => createServer(plugins), {
5
+ legacy: "stateless",
6
+ responseMode: "auto",
7
+ });
8
+ }
9
+ function createServer(plugins) {
10
+ const server = new McpServer({
11
+ name: CHATROOM_NAME,
12
+ version: CHATROOM_VERSION,
13
+ });
14
+ plugins.registerMcp(server);
15
+ return server;
16
+ }
@@ -0,0 +1,31 @@
1
+ import type { McpServer, StandardSchemaWithJSON, ToolAnnotations, CallToolResult } from "@modelcontextprotocol/server";
2
+ import type { OperationLog } from "#operations/operation-log";
3
+ import type { McpToolControl } from "./tool-control.js";
4
+ type PluginToolInput<Schema extends StandardSchemaWithJSON> = StandardSchemaWithJSON.InferOutput<Schema>;
5
+ type PluginToolAction<Input> = string | ((input: Input) => string);
6
+ export interface PluginToolExecution {
7
+ readonly operationId: string;
8
+ deferCompletion(): void;
9
+ }
10
+ export interface PluginToolConfig<InputSchema extends StandardSchemaWithJSON, OutputSchema extends StandardSchemaWithJSON> {
11
+ title: string;
12
+ description: string;
13
+ inputSchema: InputSchema;
14
+ outputSchema: OutputSchema;
15
+ annotations: ToolAnnotations;
16
+ action: PluginToolAction<PluginToolInput<InputSchema>>;
17
+ audit?: {
18
+ input?: (input: PluginToolInput<InputSchema>) => unknown;
19
+ output?: (output: unknown) => unknown;
20
+ };
21
+ present?: (output: unknown) => CallToolResult;
22
+ }
23
+ export declare class PluginMcpRegistrar {
24
+ private readonly server;
25
+ private readonly operations;
26
+ private readonly pluginId;
27
+ private readonly toolControl;
28
+ constructor(server: McpServer | null, operations: OperationLog, pluginId: string, toolControl: McpToolControl);
29
+ registerTool<InputSchema extends StandardSchemaWithJSON, OutputSchema extends StandardSchemaWithJSON>(name: string, config: PluginToolConfig<InputSchema, OutputSchema>, handler: (input: PluginToolInput<InputSchema>, execution: PluginToolExecution) => Promise<unknown> | unknown): void;
30
+ }
31
+ export {};