@zhanguym/chatroom 1.1.0 → 1.2.0-beta.1

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 (122) hide show
  1. package/dist/app/application.d.ts +2 -0
  2. package/dist/app/application.js +7 -1
  3. package/dist/app/lifecycle.js +6 -0
  4. package/dist/auth/auth-service.js +5 -1
  5. package/dist/core/runtime/child-environment.js +3 -2
  6. package/dist/infrastructure/database/custom-mcp-repository.d.ts +9 -0
  7. package/dist/infrastructure/database/custom-mcp-repository.js +37 -0
  8. package/dist/infrastructure/database/mcp-tool-settings-repository.d.ts +1 -0
  9. package/dist/infrastructure/database/mcp-tool-settings-repository.js +6 -0
  10. package/dist/infrastructure/database/schema.d.ts +1 -1
  11. package/dist/infrastructure/database/schema.js +9 -0
  12. package/dist/infrastructure/http/http-server.js +7 -1
  13. package/dist/mcp/custom/manager.d.ts +54 -0
  14. package/dist/mcp/custom/manager.js +494 -0
  15. package/dist/mcp/custom/oauth.d.ts +38 -0
  16. package/dist/mcp/custom/oauth.js +116 -0
  17. package/dist/mcp/custom/types.d.ts +42 -0
  18. package/dist/mcp/custom/types.js +1 -0
  19. package/dist/mcp/server/create-mcp-server.d.ts +2 -1
  20. package/dist/mcp/server/create-mcp-server.js +4 -3
  21. package/dist/mcp/server/tool-control.d.ts +5 -0
  22. package/dist/mcp/server/tool-control.js +17 -0
  23. package/dist/native/linux/chatroom-computer-helper +0 -0
  24. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  25. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  26. package/dist/plugins/cloud/tunnel-client.d.ts +2 -84
  27. package/dist/plugins/cloud/tunnel-client.js +2 -426
  28. package/dist/plugins/cloud/v2/stream.d.ts +48 -0
  29. package/dist/plugins/cloud/v2/stream.js +156 -0
  30. package/dist/plugins/cloud/v2/tunnel-client.d.ts +45 -0
  31. package/dist/plugins/cloud/v2/tunnel-client.js +426 -0
  32. package/dist/plugins/computer/computer-schemas.d.ts +8 -8
  33. package/dist/plugins/process/backend.d.ts +3 -0
  34. package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +2 -0
  35. package/dist/plugins/process/infrastructure/pty-process-backend.js +5 -2
  36. package/dist/plugins/process/infrastructure/terminal-shell.d.ts +4 -0
  37. package/dist/plugins/process/infrastructure/terminal-shell.js +10 -0
  38. package/dist/plugins/process/process-supervisor.d.ts +6 -0
  39. package/dist/plugins/process/process-supervisor.js +26 -0
  40. package/dist/plugins/process/types.d.ts +2 -0
  41. package/dist/plugins/types.d.ts +2 -0
  42. package/dist/plugins/web/api-types.d.ts +2 -0
  43. package/dist/plugins/web/http/api-router.d.ts +2 -1
  44. package/dist/plugins/web/http/api-router.js +8 -1
  45. package/dist/plugins/web/http/custom-mcp-api-router.d.ts +6 -0
  46. package/dist/plugins/web/http/custom-mcp-api-router.js +114 -0
  47. package/dist/plugins/web/http/logs-api-router.js +2 -2
  48. package/dist/plugins/web/http/process-api-router.js +48 -0
  49. package/dist/plugins/web/http/rtc-api-router.d.ts +4 -0
  50. package/dist/plugins/web/http/rtc-api-router.js +316 -0
  51. package/dist/plugins/web/http/runtime-api-router.js +2 -2
  52. package/dist/plugins/web/http/terminal-api-router.d.ts +4 -0
  53. package/dist/plugins/web/http/terminal-api-router.js +132 -0
  54. package/dist/plugins/web/plugin.js +1 -1
  55. package/dist/plugins/web/runtime.d.ts +3 -1
  56. package/dist/plugins/web/runtime.js +3 -1
  57. package/dist/web/assets/AgentView-BEA5mQVv.js +4 -0
  58. package/dist/web/assets/{AgentView-BGLIBoxF.css → AgentView-CMxIn3KB.css} +1 -1
  59. package/dist/web/assets/CloudView-BGcVSE22.js +1 -0
  60. package/dist/web/assets/CloudView-Bgp5G965.css +1 -0
  61. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-DfPmlGC0.js +3 -0
  62. package/dist/web/assets/ComputerView-Bz2Wsz2X.css +1 -0
  63. package/dist/web/assets/ComputerView-D6_NSXKb.js +1 -0
  64. package/dist/web/assets/{MarkdownContent-CkjR5fs7.js → MarkdownContent-DCkAALOF.js} +2 -2
  65. package/dist/web/assets/McpToolsView--8UloObb.js +2 -0
  66. package/dist/web/assets/McpToolsView-DdYOoY9_.css +1 -0
  67. package/dist/web/assets/OperationsView-CqgAPnSL.js +4 -0
  68. package/dist/web/assets/ProcessesView-BmNdR3C0.css +1 -0
  69. package/dist/web/assets/ProcessesView-DC0tjiYU.js +5 -0
  70. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-D_LKEtqT.js +1 -0
  71. package/dist/web/assets/SystemLogsView-COvwamo5.css +1 -0
  72. package/dist/web/assets/SystemLogsView-DkTeUr2k.js +1 -0
  73. package/dist/web/assets/TerminalView-Coze7IMj.css +1 -0
  74. package/dist/web/assets/TerminalView-DEP1U0aW.js +16 -0
  75. package/dist/web/assets/VSelect-B5-t612C.js +1 -0
  76. package/dist/web/assets/VSheet-CNayLacf.js +1 -0
  77. package/dist/web/assets/VSpacer-BYe3Z99W.js +1 -0
  78. package/dist/web/assets/VSwitch-cVRCnipJ.js +1 -0
  79. package/dist/web/assets/VTabs-HKX286IT.js +1 -0
  80. package/dist/web/assets/VTextarea-ypCBuKI-.js +1 -0
  81. package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +1 -0
  82. package/dist/web/assets/WorkspaceDownloadPreview-qwbTkU59.js +1 -0
  83. package/dist/web/assets/WorkspaceMediaPreview-D28Yf2yb.js +4 -0
  84. package/dist/web/assets/{WorkspacePdfPreview-CO-ZtDHG.js → WorkspacePdfPreview-BfZSPcR-.js} +2 -2
  85. package/dist/web/assets/WorkspacesView-CsCGtk5H.js +2 -0
  86. package/dist/web/assets/index-BCUxnASh.css +1 -0
  87. package/dist/web/assets/index-CFhV1jOj.js +19 -0
  88. package/dist/web/assets/{useMasterDetailFocus-D1mQ8Qjk.js → useMasterDetailFocus-CMJXBPPp.js} +1 -1
  89. package/dist/web/assets/useOperationLabels-DSbMa9Eb.js +1 -0
  90. package/dist/web/index.html +2 -2
  91. package/dist/web/transport-sw.js +417 -0
  92. package/package.json +10 -4
  93. package/scripts/ensure-pty-helper.mjs +28 -0
  94. package/dist/web/assets/AgentView-Dd6zCbr-.js +0 -4
  95. package/dist/web/assets/CloudView-DVVvwuWW.css +0 -1
  96. package/dist/web/assets/CloudView-DquY1HDZ.js +0 -1
  97. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-ekOImmBP.js +0 -3
  98. package/dist/web/assets/ComputerView-DWzfSYJ7.css +0 -1
  99. package/dist/web/assets/ComputerView-RD2gMxJZ.js +0 -1
  100. package/dist/web/assets/McpToolsView-Dg2-iaDP.js +0 -1
  101. package/dist/web/assets/McpToolsView-DjxPbTHj.css +0 -1
  102. package/dist/web/assets/OperationsView-C8yCX7WZ.js +0 -4
  103. package/dist/web/assets/ProcessesView-C834LC-f.js +0 -4
  104. package/dist/web/assets/ProcessesView-Ce6iVwjA.css +0 -1
  105. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-By2gilBw.js +0 -1
  106. package/dist/web/assets/SystemLogsView-B6KcZv1J.js +0 -1
  107. package/dist/web/assets/SystemLogsView-CofP4Jvi.css +0 -1
  108. package/dist/web/assets/VSelect-BtAJX-XJ.js +0 -1
  109. package/dist/web/assets/VSpacer-M8bpkhTH.js +0 -1
  110. package/dist/web/assets/VSwitch-C0VTnwLJ.js +0 -1
  111. package/dist/web/assets/VTabs-g61mykvY.js +0 -1
  112. package/dist/web/assets/VTextarea-CRYQYRqg.js +0 -1
  113. package/dist/web/assets/WorkspaceDownloadPreview-BrESF-82.js +0 -1
  114. package/dist/web/assets/WorkspaceDownloadPreview-DnxhH5Q7.css +0 -1
  115. package/dist/web/assets/WorkspaceMediaPreview-HYajqgZ3.js +0 -4
  116. package/dist/web/assets/WorkspacesView-BJCnzBLj.js +0 -2
  117. package/dist/web/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
  118. package/dist/web/assets/index-CZ-4tJlF.css +0 -1
  119. package/dist/web/assets/index-u-aADQP-.js +0 -19
  120. package/dist/web/assets/useOperationLabels-BjPIVnJW.js +0 -1
  121. package/dist/web/assets/utils-rtPwWNel.js +0 -1
  122. /package/dist/web/assets/{CodeViewer-CH1DEJge.css → VSheet-CH1DEJge.css} +0 -0
@@ -12,8 +12,8 @@ export class PtyProcessBackend {
12
12
  const env = Object.fromEntries(Object.entries(options.env).filter((entry) => typeof entry[1] === "string"));
13
13
  const processHandle = pty.spawn(command, args, {
14
14
  name: env.TERM ?? "xterm-256color",
15
- cols: 120,
16
- rows: 30,
15
+ cols: options.cols ?? 120,
16
+ rows: options.rows ?? 30,
17
17
  cwd: options.cwd,
18
18
  env,
19
19
  });
@@ -31,6 +31,9 @@ class PtyBackendProcess {
31
31
  write(data) {
32
32
  this.processHandle.write(data);
33
33
  }
34
+ resize(cols, rows) {
35
+ this.processHandle.resize(cols, rows);
36
+ }
34
37
  kill(signal = "SIGTERM") {
35
38
  if (process.platform === "win32") {
36
39
  this.processHandle.kill();
@@ -0,0 +1,4 @@
1
+ /** Select the OS default interactive shell for a local terminal session. */
2
+ export declare function defaultTerminalShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string;
3
+ /** macOS Terminal starts an interactive login shell; match its PATH initialization. */
4
+ export declare function defaultTerminalArgs(platform?: NodeJS.Platform): string[];
@@ -0,0 +1,10 @@
1
+ /** Select the OS default interactive shell for a local terminal session. */
2
+ export function defaultTerminalShell(platform = process.platform, env = process.env) {
3
+ return platform === "win32"
4
+ ? env.ComSpec || env.COMSPEC || "cmd.exe"
5
+ : env.SHELL || "/bin/sh";
6
+ }
7
+ /** macOS Terminal starts an interactive login shell; match its PATH initialization. */
8
+ export function defaultTerminalArgs(platform = process.platform) {
9
+ return platform === "darwin" ? ["-l"] : [];
10
+ }
@@ -27,6 +27,12 @@ export declare class ProcessSupervisor {
27
27
  list(): ProcessSnapshot[];
28
28
  summaries(): ProcessSummary[];
29
29
  write(processId: ProcessId, data: string): ProcessSnapshot;
30
+ /** Subscribe to raw PTY output; terminal reconnects start from the snapshot. */
31
+ subscribeOutput(processId: ProcessId, listener: (chunk: Buffer | null, stream?: "stdout" | "stderr") => void): {
32
+ snapshot: ProcessSnapshot;
33
+ unsubscribe: () => void;
34
+ };
35
+ resize(processId: ProcessId, cols: number, rows: number): void;
30
36
  kill(processId: ProcessId, force?: boolean): ProcessSnapshot;
31
37
  shutdown(): Promise<void>;
32
38
  private onOutput;
@@ -36,6 +36,7 @@ export class ProcessSupervisor {
36
36
  args,
37
37
  cwd: request.cwd,
38
38
  pty: request.pty ?? false,
39
+ ...(request.pty ? { cols: request.cols, rows: request.rows } : {}),
39
40
  timeoutMs: request.timeoutMs ?? this.defaultTimeoutMs,
40
41
  env: request.env ?? {},
41
42
  },
@@ -47,6 +48,8 @@ export class ProcessSupervisor {
47
48
  backend = await (request.pty ? this.backends.pty : this.backends.pipe).start(request.command, args, {
48
49
  cwd: request.cwd,
49
50
  env: childEnvironment(request.env),
51
+ ...(request.cols !== undefined ? { cols: request.cols } : {}),
52
+ ...(request.rows !== undefined ? { rows: request.rows } : {}),
50
53
  });
51
54
  }
52
55
  catch (error) {
@@ -81,6 +84,7 @@ export class ProcessSupervisor {
81
84
  timeout: null,
82
85
  forceTimeout: null,
83
86
  outputEventTimer: null,
87
+ outputListeners: new Set(),
84
88
  timedOut: false,
85
89
  operationId,
86
90
  settled,
@@ -129,6 +133,23 @@ export class ProcessSupervisor {
129
133
  item.backend.write(data);
130
134
  return this.snapshotOf(processId, item);
131
135
  }
136
+ /** Subscribe to raw PTY output; terminal reconnects start from the snapshot. */
137
+ subscribeOutput(processId, listener) {
138
+ const item = this.require(processId);
139
+ item.outputListeners.add(listener);
140
+ return {
141
+ snapshot: this.snapshotOf(processId, item),
142
+ unsubscribe: () => item.outputListeners.delete(listener),
143
+ };
144
+ }
145
+ resize(processId, cols, rows) {
146
+ const item = this.requireRunning(processId);
147
+ if (!item.request.pty || !item.backend.resize)
148
+ throw new ChatRoomError("UNSUPPORTED", "Not a resizable PTY");
149
+ if (![cols, rows].every((value) => Number.isInteger(value) && value >= 2 && value <= 500))
150
+ throw new ChatRoomError("INVALID_INPUT", "Invalid terminal dimensions");
151
+ item.backend.resize(cols, rows);
152
+ }
132
153
  kill(processId, force = false) {
133
154
  const item = this.requireRunning(processId);
134
155
  item.backend.kill(force ? "SIGKILL" : "SIGTERM");
@@ -161,6 +182,8 @@ export class ProcessSupervisor {
161
182
  if (!item || item.state !== "running")
162
183
  return;
163
184
  (stream === "stdout" ? item.stdout : item.stderr).append(chunk);
185
+ for (const listener of item.outputListeners)
186
+ listener(chunk, stream);
164
187
  if (!item.outputEventTimer) {
165
188
  item.outputEventTimer = setTimeout(() => {
166
189
  item.outputEventTimer = null;
@@ -183,6 +206,9 @@ export class ProcessSupervisor {
183
206
  item.exitCode = exitCode;
184
207
  item.signal = signal;
185
208
  item.state = signal ? "killed" : exitCode === 0 ? "exited" : "failed";
209
+ for (const listener of item.outputListeners)
210
+ listener(null);
211
+ item.outputListeners.clear();
186
212
  const snapshot = this.snapshotOf(processId, item);
187
213
  this.operations.finish(item.operationId, exitCode === 0 && !signal ? "success" : signal ? "cancelled" : "error", {
188
214
  stdout: snapshot.stdout,
@@ -6,6 +6,8 @@ export interface ProcessStartRequest {
6
6
  args?: string[];
7
7
  env?: Record<string, string>;
8
8
  pty?: boolean;
9
+ cols?: number;
10
+ rows?: number;
9
11
  timeoutMs?: number;
10
12
  }
11
13
  export interface ProcessSnapshot {
@@ -3,6 +3,7 @@ import type { AppDatabase } from "#infrastructure/database/app-database";
3
3
  import type { PluginMcpRegistrar } from "#mcp/server/plugin-mcp-registrar";
4
4
  import type { OperationLog } from "#operations/operation-log";
5
5
  import type { McpToolControl } from "#mcp/server/tool-control";
6
+ import type { CustomMcpManager } from "#mcp/custom/manager";
6
7
  import type { RuntimeEventBus } from "#app/event-bus";
7
8
  import type { ExternalAccessRegistry } from "#app/external-access-registry";
8
9
  import type { LogWriter } from "#core/logging/types";
@@ -12,6 +13,7 @@ export interface PluginContext {
12
13
  database: AppDatabase;
13
14
  operations: OperationLog;
14
15
  mcpTools: McpToolControl;
16
+ customMcp: CustomMcpManager;
15
17
  events: RuntimeEventBus;
16
18
  externalAccess: ExternalAccessRegistry;
17
19
  services: ServiceRegistry;
@@ -2,6 +2,7 @@ import type { Operation as DomainOperation } from "#core/operations/types";
2
2
  import type { GitBranch, GitChange, GitCommit, GitDiff, GitStatus } from "#app/git-access";
3
3
  import type { ProcessSnapshot, ProcessSummary } from "#plugins/process/types";
4
4
  import type { AgentAttachment, AgentHistory, AgentHistoryUpdate, AgentInteractionResponse, AgentItem, AgentModel, AgentProviderId, AgentProviderDetails, AgentReviewTarget, AgentProviderStatus, AgentSession, AgentTurn, AgentTurnSource } from "#plugins/agent/types";
5
+ import type { CustomMcpServerSummary } from "#mcp/custom/types";
5
6
  import type { McpToolSummary as DomainMcpToolSummary } from "#mcp/server/tool-control";
6
7
  import type { CloudServiceId, CloudStatus as DomainCloudStatus } from "#plugins/cloud/types";
7
8
  import type { ComputerDisplay, ComputerPermission, ComputerStatus } from "#plugins/computer/types";
@@ -46,6 +47,7 @@ export interface UpdateStatus {
46
47
  releaseUrl: string | null;
47
48
  }
48
49
  export type McpToolSummary = DomainMcpToolSummary;
50
+ export type { CustomMcpServerSummary };
49
51
  export type CloudService = CloudServiceId;
50
52
  export type CloudStatus = DomainCloudStatus;
51
53
  export interface WorkspaceFileContent {
@@ -6,7 +6,8 @@ import type { PasskeyService } from "#auth/passkey-service";
6
6
  import type { IngressPolicy } from "#auth/ingress-policy";
7
7
  import type { CloudController } from "#plugins/cloud/controller";
8
8
  import type { LogService } from "#core/logging/types";
9
- export declare function createApiRouter(application: WebRuntime, eventBus: RuntimeEventBus, auth: AuthService, passkeys: PasskeyService, ingress: IngressPolicy, cloud: CloudController, logs: LogService, runtimeStatus: () => {
9
+ import type { ChatRoomConfig } from "#config/types";
10
+ export declare function createApiRouter(application: WebRuntime, eventBus: RuntimeEventBus, auth: AuthService, passkeys: PasskeyService, ingress: IngressPolicy, cloud: CloudController, logs: LogService, config: ChatRoomConfig, runtimeStatus: () => {
10
11
  version: string;
11
12
  mcpRequests: number;
12
13
  uptimeMinutes: number;
@@ -2,20 +2,26 @@ import { Router } from "express";
2
2
  import { ChatRoomError } from "#core/errors/chatroom-error";
3
3
  import { boundedIntegerQuery, requireString, } from "#presentation/http/http-utils";
4
4
  import { createProcessApiRouter } from "./process-api-router.js";
5
+ import { createTerminalApiRouter } from "./terminal-api-router.js";
5
6
  import { createWorkspaceApiRouter } from "./workspace-api-router.js";
6
7
  import { createGitApiRouter } from "./git-api-router.js";
7
8
  import { createCloudApiRouter } from "./cloud-api-router.js";
8
9
  import { createComputerApiRouter } from "./computer-api-router.js";
9
10
  import { apiAuthentication, createPrivateAuthApiRouter, createPublicAuthApiRouter, } from "./auth-api-router.js";
11
+ import { createCustomMcpApiRouter, createCustomMcpOAuthCallbackRouter, } from "./custom-mcp-api-router.js";
10
12
  import { createRuntimeApiRouter } from "./runtime-api-router.js";
11
13
  import { createLogsApiRouter } from "./logs-api-router.js";
12
14
  import { createAgentApiRouter } from "./agent-api-router.js";
13
- export function createApiRouter(application, eventBus, auth, passkeys, ingress, cloud, logs, runtimeStatus) {
15
+ import { createRtcApiRouter } from "./rtc-api-router.js";
16
+ export function createApiRouter(application, eventBus, auth, passkeys, ingress, cloud, logs, config, runtimeStatus) {
14
17
  const router = Router();
15
18
  router.use(createPublicAuthApiRouter(auth, passkeys, ingress, logs));
19
+ router.use(createCustomMcpOAuthCallbackRouter(application.customMcp));
16
20
  router.use(apiAuthentication(auth, ingress));
17
21
  router.use(createPrivateAuthApiRouter(passkeys, ingress, logs));
22
+ router.use(createRtcApiRouter(config));
18
23
  router.use(createRuntimeApiRouter(application, eventBus, runtimeStatus));
24
+ router.use(createCustomMcpApiRouter(application.customMcp, ingress));
19
25
  router.use(createLogsApiRouter(logs));
20
26
  router.get("/operations", (req, res) => {
21
27
  res.json(application.operations.list({
@@ -49,6 +55,7 @@ export function createApiRouter(application, eventBus, auth, passkeys, ingress,
49
55
  router.use(createWorkspaceApiRouter(application));
50
56
  router.use(createGitApiRouter(application));
51
57
  router.use(createProcessApiRouter(application));
58
+ router.use(createTerminalApiRouter(application));
52
59
  router.use(createAgentApiRouter(application.agents));
53
60
  router.use(createComputerApiRouter(application.computer, application.operations, ingress));
54
61
  router.use(createCloudApiRouter(cloud, application.operations));
@@ -0,0 +1,6 @@
1
+ import { Router } from "express";
2
+ import type { IngressPolicy } from "#auth/ingress-policy";
3
+ import type { CustomMcpManager } from "#mcp/custom/manager";
4
+ export declare function createCustomMcpApiRouter(manager: CustomMcpManager, ingress: IngressPolicy): Router;
5
+ /** Public OAuth callback: the random state is single-use and scoped to an initiated login. */
6
+ export declare function createCustomMcpOAuthCallbackRouter(manager: CustomMcpManager): Router;
@@ -0,0 +1,114 @@
1
+ import { Router } from "express";
2
+ import { ChatRoomError } from "#core/errors/chatroom-error";
3
+ import { bodyRecord, requireString } from "#presentation/http/http-utils";
4
+ function settings(body, partial = false) {
5
+ const config = {};
6
+ const str = (key) => {
7
+ if (typeof body[key] !== "string")
8
+ throw new ChatRoomError("INVALID_INPUT", key + " must be a string");
9
+ return body[key];
10
+ };
11
+ if (!partial || "name" in body)
12
+ config.name = str("name").trim();
13
+ if (!partial || "type" in body) {
14
+ const type = str("type");
15
+ if (type !== "stdio" && type !== "http")
16
+ throw new ChatRoomError("INVALID_INPUT", "Unsupported MCP transport");
17
+ config.type = type;
18
+ }
19
+ for (const key of ["command", "cwd", "url"])
20
+ if (key in body)
21
+ config[key] = str(key);
22
+ if ("args" in body) {
23
+ if (!Array.isArray(body.args) ||
24
+ body.args.some((value) => typeof value !== "string"))
25
+ throw new ChatRoomError("INVALID_INPUT", "args must be an array of strings");
26
+ config.args = body.args;
27
+ }
28
+ for (const key of ["env", "headers"]) {
29
+ if (!(key in body))
30
+ continue;
31
+ const data = body[key];
32
+ if (!data ||
33
+ typeof data !== "object" ||
34
+ Array.isArray(data) ||
35
+ Object.entries(data).some(([name, value]) => !name.trim() || typeof value !== "string"))
36
+ throw new ChatRoomError("INVALID_INPUT", key + " must be a string map");
37
+ config[key] = data;
38
+ }
39
+ if ("authMode" in body) {
40
+ const value = str("authMode");
41
+ if (!["auto", "none", "headers"].includes(value))
42
+ throw new ChatRoomError("INVALID_INPUT", "Unsupported MCP authentication");
43
+ config.authMode = value;
44
+ }
45
+ if (!partial || "enabled" in body) {
46
+ if (typeof body.enabled !== "boolean")
47
+ throw new ChatRoomError("INVALID_INPUT", "enabled must be a boolean");
48
+ config.enabled = body.enabled;
49
+ }
50
+ return config;
51
+ }
52
+ export function createCustomMcpApiRouter(manager, ingress) {
53
+ const router = Router();
54
+ router.get("/mcp/servers", (_req, res) => res.json(manager.list()));
55
+ router.post("/mcp/servers", async (req, res) => {
56
+ const config = settings(bodyRecord(req.body));
57
+ res.status(201).json(await manager.create(config));
58
+ });
59
+ router.post("/mcp/servers/test", async (req, res) => {
60
+ const config = settings(bodyRecord(req.body));
61
+ res.json(await manager.test(config));
62
+ });
63
+ router.post("/mcp/servers/:id/test", async (req, res) => {
64
+ res.json(await manager.testStored(requireString(req.params.id, "id")));
65
+ });
66
+ router.patch("/mcp/servers/:id", async (req, res) => {
67
+ const id = requireString(req.params.id, "id");
68
+ res.json(await manager.update(id, settings(bodyRecord(req.body), true)));
69
+ });
70
+ router.delete("/mcp/servers/:id", async (req, res) => {
71
+ await manager.remove(requireString(req.params.id, "id"));
72
+ res.json({ ok: true });
73
+ });
74
+ router.post("/mcp/servers/:id/authorize", async (req, res) => {
75
+ const id = requireString(req.params.id, "id");
76
+ const origin = ingress.expectedWebOrigin(req) ??
77
+ `http://${req.get("host") ?? "localhost:8765"}`;
78
+ res.json(await manager.authorize(id, origin + "/api/mcp/oauth/callback"));
79
+ });
80
+ router.post("/mcp/servers/:id/revoke", async (req, res) => {
81
+ await manager.revokeAuthorization(requireString(req.params.id, "id"));
82
+ res.json({ ok: true });
83
+ });
84
+ router.post("/mcp/servers/:id/refresh", async (req, res) => {
85
+ res.json(await manager.refresh(requireString(req.params.id, "id")));
86
+ });
87
+ return router;
88
+ }
89
+ /** Public OAuth callback: the random state is single-use and scoped to an initiated login. */
90
+ export function createCustomMcpOAuthCallbackRouter(manager) {
91
+ const router = Router();
92
+ router.get("/mcp/oauth/callback", async (req, res) => {
93
+ const query = new URLSearchParams();
94
+ for (const key of ["state", "code", "iss", "error"])
95
+ if (typeof req.query[key] === "string")
96
+ query.set(key, req.query[key]);
97
+ let success = false;
98
+ try {
99
+ await manager.completeAuthorization(query);
100
+ success = true;
101
+ }
102
+ catch {
103
+ // Never expose OAuth codes, provider errors, or credentials in this page.
104
+ }
105
+ res.setHeader("Cache-Control", "no-store");
106
+ res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'; frame-ancestors 'none'");
107
+ res.status(success ? 200 : 400).type("html")
108
+ .send(`<!doctype html><html lang="en"><meta charset="utf-8"><title>ChatRoom MCP</title>
109
+ <body><p>${success ? "Authorization completed. You may close this window." : "Authorization failed. Return to ChatRoom and retry."}</p>
110
+ <script>window.opener?.postMessage({type:"chatroom:mcp-oauth-completed",success:${success}},window.location.origin);window.close();</script>
111
+ </body></html>`);
112
+ });
113
+ return router;
114
+ }
@@ -16,7 +16,7 @@ export function createLogsApiRouter(logs) {
16
16
  ...(modules?.length ? { modules: new Set(modules) } : {}),
17
17
  }));
18
18
  }));
19
- router.get("/logs/stream", (req, res) => {
19
+ router.get("/logs/stream", (_req, res) => {
20
20
  res.status(200);
21
21
  res.setHeader("Content-Type", "text/event-stream");
22
22
  res.setHeader("Cache-Control", "no-cache, no-transform");
@@ -28,7 +28,7 @@ export function createLogsApiRouter(logs) {
28
28
  });
29
29
  const keepalive = setInterval(() => res.write(": keepalive\n\n"), 15000);
30
30
  keepalive.unref();
31
- req.on("close", () => {
31
+ res.on("close", () => {
32
32
  clearInterval(keepalive);
33
33
  unsubscribe();
34
34
  });
@@ -6,6 +6,54 @@ export function createProcessApiRouter(application) {
6
6
  router.get("/processes/:processId/summary", (req, res) => {
7
7
  res.json(application.getProcessSummary(requireString(req.params.processId, "processId")));
8
8
  });
9
+ router.get("/processes/:processId/stream", (req, res) => {
10
+ const processId = requireString(req.params.processId, "processId");
11
+ // Validate and attach before flushing headers: invalid IDs return JSON 404.
12
+ let closed = false;
13
+ const write = (type, data) => {
14
+ if (closed || res.destroyed)
15
+ return;
16
+ if (res.writableLength > 512 * 1024) {
17
+ res.destroy(new Error("Process output stream is too slow"));
18
+ return;
19
+ }
20
+ res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
21
+ };
22
+ const { snapshot, unsubscribe } = application.processes.subscribeOutput(processId, (chunk, stream) => {
23
+ if (!chunk) {
24
+ const { stdout: _stdout, stderr: _stderr, ...status } = application.getProcess(processId);
25
+ write("exit", status);
26
+ res.end();
27
+ }
28
+ else {
29
+ for (let offset = 0; offset < chunk.length; offset += 16 * 1024)
30
+ write("output", {
31
+ stream: stream ?? "stdout",
32
+ bytes: chunk
33
+ .subarray(offset, offset + 16 * 1024)
34
+ .toString("base64"),
35
+ });
36
+ }
37
+ });
38
+ res.status(200);
39
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
40
+ res.setHeader("Cache-Control", "private, no-store, no-transform");
41
+ res.setHeader("X-Accel-Buffering", "no");
42
+ res.flushHeaders();
43
+ write("snapshot", snapshot);
44
+ if (snapshot.state !== "running") {
45
+ res.end();
46
+ unsubscribe();
47
+ return;
48
+ }
49
+ const heartbeat = setInterval(() => write("ping", null), 15_000);
50
+ heartbeat.unref();
51
+ res.on("close", () => {
52
+ closed = true;
53
+ clearInterval(heartbeat);
54
+ unsubscribe();
55
+ });
56
+ });
9
57
  router.get("/processes/:processId", (req, res) => {
10
58
  res.json(application.getProcess(requireString(req.params.processId, "processId")));
11
59
  });
@@ -0,0 +1,4 @@
1
+ import { Router } from "express";
2
+ import type { ChatRoomConfig } from "#config/types";
3
+ /** V2 DataChannel HTTP transport. Cloud and self-hosting use the same HTTPS signaling API. */
4
+ export declare function createRtcApiRouter(config: ChatRoomConfig): Router;