@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,47 @@
1
+ import { Router } from "express";
2
+ import { ChatRoomError } from "#core/errors/chatroom-error";
3
+ import { boundedIntegerQuery, requireString, } from "#presentation/http/http-utils";
4
+ import { createProcessApiRouter } from "./process-api-router.js";
5
+ import { createWorkspaceApiRouter } from "./workspace-api-router.js";
6
+ import { createGitApiRouter } from "./git-api-router.js";
7
+ import { createCloudApiRouter } from "./cloud-api-router.js";
8
+ import { createComputerApiRouter } from "./computer-api-router.js";
9
+ import { apiAuthentication, createPrivateAuthApiRouter, createPublicAuthApiRouter, } from "./auth-api-router.js";
10
+ import { createRuntimeApiRouter } from "./runtime-api-router.js";
11
+ import { createLogsApiRouter } from "./logs-api-router.js";
12
+ export function createApiRouter(application, eventBus, auth, passkeys, ingress, cloud, logs, runtimeStatus) {
13
+ const router = Router();
14
+ router.use(createPublicAuthApiRouter(auth, passkeys, ingress, logs));
15
+ router.use(apiAuthentication(auth, ingress));
16
+ router.use(createPrivateAuthApiRouter(passkeys, ingress, logs));
17
+ router.use(createRuntimeApiRouter(application, eventBus, runtimeStatus));
18
+ router.use(createLogsApiRouter(logs));
19
+ router.get("/operations", (req, res) => {
20
+ res.json(application.operations.list({
21
+ limit: boundedIntegerQuery(req.query.limit, 100, 0, 500),
22
+ offset: boundedIntegerQuery(req.query.offset, 0, 0, 100000),
23
+ ...(typeof req.query.pluginId === "string"
24
+ ? { pluginId: req.query.pluginId }
25
+ : {}),
26
+ ...(typeof req.query.status === "string"
27
+ ? { status: req.query.status }
28
+ : {}),
29
+ }));
30
+ });
31
+ router.delete("/operations", (_req, res) => {
32
+ res.json(application.operations.clearHistory());
33
+ });
34
+ router.get("/operations/:operationId", (req, res) => {
35
+ const operationId = requireString(req.params.operationId, "operationId");
36
+ const event = application.operations.get(operationId);
37
+ if (!event)
38
+ throw new ChatRoomError("NOT_FOUND", "Operation not found");
39
+ res.json(event);
40
+ });
41
+ router.use(createWorkspaceApiRouter(application));
42
+ router.use(createGitApiRouter(application));
43
+ router.use(createProcessApiRouter(application));
44
+ router.use(createComputerApiRouter(application.computer, application.operations, ingress));
45
+ router.use(createCloudApiRouter(cloud, application.operations));
46
+ return router;
47
+ }
@@ -0,0 +1,8 @@
1
+ import { Router, type RequestHandler } from "express";
2
+ import type { AuthService } from "#auth/auth-service";
3
+ import type { IngressPolicy } from "#auth/ingress-policy";
4
+ import type { PasskeyService } from "#auth/passkey-service";
5
+ import type { LogWriter } from "#core/logging/types";
6
+ export declare function createPublicAuthApiRouter(auth: AuthService, passkeys: PasskeyService, ingress: IngressPolicy, logs: LogWriter): Router;
7
+ export declare function createPrivateAuthApiRouter(passkeys: PasskeyService, ingress: IngressPolicy, logs: LogWriter): Router;
8
+ export declare function apiAuthentication(auth: AuthService, ingress: IngressPolicy): RequestHandler;
@@ -0,0 +1,123 @@
1
+ import { Router } from "express";
2
+ import { asyncRoute, bodyRecord, parseCookie, requireString, } from "#presentation/http/http-utils";
3
+ const SESSION_COOKIE = "chatroom_session";
4
+ export function createPublicAuthApiRouter(auth, passkeys, ingress, logs) {
5
+ const router = Router();
6
+ router.get("/auth/status", (req, res) => {
7
+ const token = sessionToken(req.headers.cookie);
8
+ const enabled = ingress.requiresWebAuth(req);
9
+ const passkeyAvailable = Boolean(ingress.webAuthnOrigin(req));
10
+ res.json({
11
+ enabled,
12
+ authenticated: !enabled || Boolean(token && auth.verifyWebSession(token)),
13
+ passkeyAvailable,
14
+ passkeyRegistered: passkeyAvailable && passkeys.list().length > 0,
15
+ });
16
+ });
17
+ router.post("/auth/login", asyncRoute(async (req, res) => {
18
+ if (!ingress.requiresWebAuth(req)) {
19
+ res.json({ authenticated: true });
20
+ return;
21
+ }
22
+ const body = bodyRecord(req.body);
23
+ try {
24
+ const session = auth.createWebSession(requireString(body.ownerToken, "ownerToken"), body.remember !== false);
25
+ setSessionCookie(res, session.token, session.maxAgeSeconds, ingress.secureWebCookie(req));
26
+ logs.info("auth", "auth.success", "Web authentication succeeded", {
27
+ method: "owner_token",
28
+ });
29
+ res.json({ authenticated: true, expiresAt: session.expiresAt });
30
+ }
31
+ catch (error) {
32
+ logs.warn("auth", "auth.failed", "Web authentication failed", {
33
+ method: "owner_token",
34
+ reason: "invalid_owner_token",
35
+ });
36
+ throw error;
37
+ }
38
+ }));
39
+ router.post("/auth/passkey/options", asyncRoute(async (req, res) => {
40
+ res.json(await passkeys.authenticationOptions(ingress.webAuthnOrigin(req)));
41
+ }));
42
+ router.post("/auth/passkey/verify", asyncRoute(async (req, res) => {
43
+ const body = bodyRecord(req.body);
44
+ try {
45
+ await passkeys.verifyAuthentication({
46
+ challengeId: requireString(body.challengeId, "challengeId"),
47
+ response: body.response,
48
+ });
49
+ const session = auth.createPasskeyWebSession(body.remember !== false);
50
+ setSessionCookie(res, session.token, session.maxAgeSeconds, ingress.secureWebCookie(req));
51
+ logs.info("auth", "auth.passkey_success", "Passkey authentication succeeded");
52
+ res.json({ authenticated: true, expiresAt: session.expiresAt });
53
+ }
54
+ catch (error) {
55
+ logs.warn("auth", "auth.passkey_failed", "Passkey authentication failed", { reason: "passkey_verification_failed" });
56
+ throw error;
57
+ }
58
+ }));
59
+ router.post("/auth/logout", (req, res) => {
60
+ const token = sessionToken(req.headers.cookie);
61
+ if (token) {
62
+ auth.revokeWebSession(token);
63
+ logs.info("auth", "auth.logout", "Web session signed out");
64
+ }
65
+ const secure = ingress.secureWebCookie(req) ? "; Secure" : "";
66
+ res.setHeader("Set-Cookie", `${SESSION_COOKIE}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`);
67
+ res.json({ authenticated: false });
68
+ });
69
+ return router;
70
+ }
71
+ export function createPrivateAuthApiRouter(passkeys, ingress, logs) {
72
+ const router = Router();
73
+ router.get("/auth/passkeys", (_req, res) => res.json(passkeys.list()));
74
+ router.post("/auth/passkeys/register/options", asyncRoute(async (req, res) => {
75
+ res.json(await passkeys.registrationOptions(ingress.webAuthnOrigin(req)));
76
+ }));
77
+ router.post("/auth/passkeys/register/verify", asyncRoute(async (req, res) => {
78
+ const body = bodyRecord(req.body);
79
+ try {
80
+ const passkey = await passkeys.verifyRegistration({
81
+ challengeId: requireString(body.challengeId, "challengeId"),
82
+ response: body.response,
83
+ ...(typeof body.name === "string" ? { name: body.name } : {}),
84
+ });
85
+ logs.info("auth", "auth.passkey_registered", "Passkey registered");
86
+ res.status(201).json(passkey);
87
+ }
88
+ catch (error) {
89
+ logs.warn("auth", "auth.passkey_register_failed", "Passkey registration failed", { reason: "passkey_registration_failed" });
90
+ throw error;
91
+ }
92
+ }));
93
+ router.delete("/auth/passkeys/:credentialId", (req, res) => {
94
+ passkeys.remove(requireString(req.params.credentialId, "credentialId"));
95
+ logs.info("auth", "auth.passkey_removed", "Passkey removed");
96
+ res.status(204).end();
97
+ });
98
+ return router;
99
+ }
100
+ export function apiAuthentication(auth, ingress) {
101
+ return (req, res, next) => {
102
+ if (!ingress.requiresWebAuth(req)) {
103
+ next();
104
+ return;
105
+ }
106
+ const token = sessionToken(req.headers.cookie);
107
+ if (token && auth.verifyWebSession(token)) {
108
+ next();
109
+ return;
110
+ }
111
+ res.status(401).json({
112
+ error: { code: "FORBIDDEN", message: "Authentication required" },
113
+ });
114
+ };
115
+ }
116
+ function sessionToken(cookieHeader) {
117
+ return parseCookie(cookieHeader, SESSION_COOKIE);
118
+ }
119
+ function setSessionCookie(res, token, maxAgeSeconds, secureCookie) {
120
+ const secure = secureCookie ? "; Secure" : "";
121
+ const maxAge = maxAgeSeconds === null ? "" : `; Max-Age=${maxAgeSeconds}`;
122
+ res.setHeader("Set-Cookie", `${SESSION_COOKIE}=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/${maxAge}${secure}`);
123
+ }
@@ -0,0 +1,4 @@
1
+ import { Router } from "express";
2
+ import type { OperationLog } from "#operations/operation-log";
3
+ import type { CloudController } from "#plugins/cloud/controller";
4
+ export declare function createCloudApiRouter(controller: CloudController, operations: OperationLog): Router;
@@ -0,0 +1,29 @@
1
+ import { Router } from "express";
2
+ import { z } from "zod";
3
+ import { CLOUD_SERVICE_SCHEMA } from "#plugins/cloud/types";
4
+ import { asyncRoute } from "#presentation/http/http-utils";
5
+ export function createCloudApiRouter(controller, operations) {
6
+ const router = Router();
7
+ router.get("/cloud/status", (_req, res) => res.json(controller.status()));
8
+ router.post("/cloud/sync", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "sync" }, () => controller.syncStatus()))));
9
+ router.post("/cloud/management", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "management" }, () => controller.managementUrl()))));
10
+ router.post("/cloud/restore", asyncRoute(async (req, res) => {
11
+ const body = z
12
+ .object({ recoveryKey: z.string().min(20) })
13
+ .strict()
14
+ .parse(req.body);
15
+ res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "restore", input: body }, () => controller.restore(body.recoveryKey)));
16
+ }));
17
+ router.post("/cloud/recovery-key", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "recovery-key.replace" }, async () => ({ recoveryKey: await controller.replaceRecoveryKey() })))));
18
+ router.post("/cloud/services/:service", asyncRoute(async (req, res) => {
19
+ const service = CLOUD_SERVICE_SCHEMA.parse(req.params.service);
20
+ const body = z.object({ enabled: z.boolean() }).strict().parse(req.body);
21
+ res.json(await operations.run({
22
+ pluginId: "cloud",
23
+ source: "gui",
24
+ action: "service.set",
25
+ input: { service, enabled: body.enabled },
26
+ }, () => controller.setService(service, body.enabled)));
27
+ }));
28
+ return router;
29
+ }
@@ -0,0 +1,5 @@
1
+ import { Router } from "express";
2
+ import type { ComputerService } from "#plugins/computer/computer-service";
3
+ import type { OperationLog } from "#operations/operation-log";
4
+ import type { IngressPolicy } from "#auth/ingress-policy";
5
+ export declare function createComputerApiRouter(computer: ComputerService, operations: OperationLog, ingress: IngressPolicy): Router;
@@ -0,0 +1,69 @@
1
+ import { Router } from "express";
2
+ import { z } from "zod";
3
+ import { asyncRoute } from "#presentation/http/http-utils";
4
+ import { ChatRoomError } from "#core/errors/chatroom-error";
5
+ const settingsPatchSchema = z
6
+ .object({
7
+ enabled: z.boolean().optional(),
8
+ remoteAccess: z.boolean().optional(),
9
+ })
10
+ .strict();
11
+ export function createComputerApiRouter(computer, operations, ingress) {
12
+ const router = Router();
13
+ router.get("/computer/status", asyncRoute(async (_req, res) => res.json(await computer.status())));
14
+ router.post("/computer/permissions/accessibility/request", asyncRoute(async (req, res) => {
15
+ assertLocalPermissionRequest(req);
16
+ res.json(await computer.requestPermission("accessibility"));
17
+ }));
18
+ router.post("/computer/permissions/screen-recording/request", asyncRoute(async (req, res) => {
19
+ assertLocalPermissionRequest(req);
20
+ res.json(await computer.requestPermission("screenRecording"));
21
+ }));
22
+ function assertLocalPermissionRequest(req) {
23
+ if (ingress.isExternalWeb(req))
24
+ throw new ChatRoomError("FORBIDDEN", "Computer permissions can be requested only from the local WebUI");
25
+ }
26
+ router.patch("/computer/settings", asyncRoute(async (req, res) => {
27
+ const parsed = settingsPatchSchema.parse(req.body);
28
+ const patch = {
29
+ ...(parsed.enabled === undefined ? {} : { enabled: parsed.enabled }),
30
+ ...(parsed.remoteAccess === undefined
31
+ ? {}
32
+ : { remoteAccess: parsed.remoteAccess }),
33
+ };
34
+ res.json(await operations.run({
35
+ pluginId: "computer",
36
+ source: "gui",
37
+ action: "settings.set",
38
+ input: patch,
39
+ }, async () => computer.setSettings(patch)));
40
+ }));
41
+ router.get("/computer/preview", (req, res) => {
42
+ const scope = ingress.isExternalWeb(req) ? "remote" : "local";
43
+ res.json(presentPreview(computer.latestSnapshot(scope), computer.latestSnapshotTimestamp()));
44
+ });
45
+ router.post("/computer/snapshot", asyncRoute(async (req, res) => {
46
+ const scope = ingress.isExternalWeb(req) ? "remote" : "local";
47
+ const value = await computer.snapshot(scope, {
48
+ includeScreenshot: true,
49
+ includeElements: true,
50
+ });
51
+ res.json(presentPreview(value, computer.latestSnapshotTimestamp()));
52
+ }));
53
+ return router;
54
+ }
55
+ function presentPreview(value, capturedAt) {
56
+ if (!value)
57
+ return null;
58
+ return {
59
+ snapshotId: value.snapshotId,
60
+ revision: value.revision,
61
+ capturedAt,
62
+ display: value.display,
63
+ activeApp: value.activeApp,
64
+ activeWindow: value.activeWindow,
65
+ cursor: value.cursor,
66
+ elementCount: value.elements.length,
67
+ screenshot: value.screenshot ?? null,
68
+ };
69
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from "express";
2
+ import type { WebRuntime } from "#plugins/web/runtime";
3
+ export declare function createGitApiRouter(application: WebRuntime): Router;
@@ -0,0 +1,85 @@
1
+ import { Router } from "express";
2
+ import { asyncRoute, bodyRecord, requireString, requireStringArray, } from "#presentation/http/http-utils";
3
+ export function createGitApiRouter(application) {
4
+ const router = Router();
5
+ router.get("/git/status", asyncRoute(async (req, res) => {
6
+ const root = await resolveRoot(application, req.query.root);
7
+ res.json(await application.git.status(root));
8
+ }));
9
+ router.get("/git/diff", asyncRoute(async (req, res) => {
10
+ const root = await resolveRoot(application, req.query.root);
11
+ const filePath = typeof req.query.path === "string" ? req.query.path : undefined;
12
+ res.json(await application.git.diff(root, filePath));
13
+ }));
14
+ router.get("/git/branches", asyncRoute(async (req, res) => {
15
+ const root = await resolveRoot(application, req.query.root);
16
+ res.json(await application.git.branches(root));
17
+ }));
18
+ router.get("/git/log", asyncRoute(async (req, res) => {
19
+ const root = await resolveRoot(application, req.query.root);
20
+ const requested = Number(req.query.limit ?? 30);
21
+ const limit = Number.isFinite(requested) ? requested : 30;
22
+ res.json(await application.git.log(root, limit));
23
+ }));
24
+ router.post("/git/stage", asyncRoute(async (req, res) => {
25
+ const body = bodyRecord(req.body);
26
+ const root = await resolveRoot(application, body.root);
27
+ const paths = requireStringArray(body.paths, "paths");
28
+ res.json(await mutate(application, "git.stage", root, { paths }, () => application.git.stage(root, paths)));
29
+ }));
30
+ router.post("/git/unstage", asyncRoute(async (req, res) => {
31
+ const body = bodyRecord(req.body);
32
+ const root = await resolveRoot(application, body.root);
33
+ const paths = requireStringArray(body.paths, "paths");
34
+ res.json(await mutate(application, "git.unstage", root, { paths }, () => application.git.unstage(root, paths)));
35
+ }));
36
+ router.post("/git/restore", asyncRoute(async (req, res) => {
37
+ const body = bodyRecord(req.body);
38
+ const root = await resolveRoot(application, body.root);
39
+ const filePath = requireString(body.path, "path");
40
+ res.json(await mutate(application, "git.restore", root, { path: filePath }, () => application.git.restore(root, filePath)));
41
+ }));
42
+ router.post("/git/commit", asyncRoute(async (req, res) => {
43
+ const body = bodyRecord(req.body);
44
+ const root = await resolveRoot(application, body.root);
45
+ const message = requireString(body.message, "message");
46
+ res.json(await mutate(application, "git.commit", root, { message }, () => application.git.commit(root, message)));
47
+ }));
48
+ router.post("/git/branches", asyncRoute(async (req, res) => {
49
+ const body = bodyRecord(req.body);
50
+ const root = await resolveRoot(application, body.root);
51
+ const name = requireString(body.name, "name");
52
+ res.json(await mutate(application, "git.branch.create", root, { name }, () => application.git.createBranch(root, name)));
53
+ }));
54
+ router.post("/git/switch", asyncRoute(async (req, res) => {
55
+ const body = bodyRecord(req.body);
56
+ const root = await resolveRoot(application, body.root);
57
+ const name = requireString(body.name, "name");
58
+ res.json(await mutate(application, "git.branch.switch", root, { name }, () => application.git.switchBranch(root, name)));
59
+ }));
60
+ router.delete("/git/branches", asyncRoute(async (req, res) => {
61
+ const body = bodyRecord(req.body);
62
+ const root = await resolveRoot(application, body.root);
63
+ const name = requireString(body.name, "name");
64
+ res.json(await mutate(application, "git.branch.delete", root, { name }, () => application.git.deleteBranch(root, name)));
65
+ }));
66
+ for (const action of ["fetch", "pull", "push"]) {
67
+ router.post(`/git/${action}`, asyncRoute(async (req, res) => {
68
+ const body = bodyRecord(req.body);
69
+ const root = await resolveRoot(application, body.root);
70
+ res.json(await mutate(application, `git.${action}`, root, {}, () => application.git[action](root)));
71
+ }));
72
+ }
73
+ return router;
74
+ }
75
+ async function resolveRoot(application, value) {
76
+ return application.workspaces.resolve(requireString(value, "root"));
77
+ }
78
+ function mutate(application, action, root, input, operation) {
79
+ return application.operations.run({
80
+ pluginId: "git",
81
+ source: "gui",
82
+ action,
83
+ input: { root, ...input },
84
+ }, operation);
85
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from "express";
2
+ import type { LogService } from "#core/logging/types";
3
+ export declare function createLogsApiRouter(logs: LogService): Router;
@@ -0,0 +1,49 @@
1
+ import { Router } from "express";
2
+ import { asyncRoute, boundedIntegerQuery } from "#presentation/http/http-utils";
3
+ const LEVELS = new Set(["debug", "info", "warn", "error"]);
4
+ export function createLogsApiRouter(logs) {
5
+ const router = Router();
6
+ router.get("/logs", asyncRoute(async (req, res) => {
7
+ const limit = boundedIntegerQuery(req.query.limit, 100, 1, 500);
8
+ const levels = stringSet(req.query.level)?.filter((value) => LEVELS.has(value));
9
+ const modules = stringSet(req.query.module);
10
+ res.json(await logs.list({
11
+ limit,
12
+ ...(typeof req.query.cursor === "string" && req.query.cursor
13
+ ? { cursor: req.query.cursor }
14
+ : {}),
15
+ ...(levels?.length ? { levels: new Set(levels) } : {}),
16
+ ...(modules?.length ? { modules: new Set(modules) } : {}),
17
+ }));
18
+ }));
19
+ router.get("/logs/stream", (req, res) => {
20
+ res.status(200);
21
+ res.setHeader("Content-Type", "text/event-stream");
22
+ res.setHeader("Cache-Control", "no-cache, no-transform");
23
+ res.setHeader("Connection", "keep-alive");
24
+ res.flushHeaders();
25
+ res.write(`event: ready\ndata: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
26
+ const unsubscribe = logs.subscribe((record) => {
27
+ res.write(`event: log\ndata: ${JSON.stringify(record)}\n\n`);
28
+ });
29
+ const keepalive = setInterval(() => res.write(": keepalive\n\n"), 15000);
30
+ keepalive.unref();
31
+ req.on("close", () => {
32
+ clearInterval(keepalive);
33
+ unsubscribe();
34
+ });
35
+ });
36
+ return router;
37
+ }
38
+ function stringSet(value) {
39
+ const raw = Array.isArray(value)
40
+ ? value
41
+ : typeof value === "string"
42
+ ? [value]
43
+ : [];
44
+ const values = raw
45
+ .flatMap((item) => (typeof item === "string" ? item.split(",") : []))
46
+ .map((item) => item.trim())
47
+ .filter(Boolean);
48
+ return values.length ? [...new Set(values)] : null;
49
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from "express";
2
+ import type { WebRuntime } from "#plugins/web/runtime";
3
+ export declare function createProcessApiRouter(application: WebRuntime): Router;
@@ -0,0 +1,16 @@
1
+ import { Router } from "express";
2
+ import { asyncRoute, requireString } from "#presentation/http/http-utils";
3
+ export function createProcessApiRouter(application) {
4
+ const router = Router();
5
+ router.get("/processes", (_req, res) => res.json(application.listProcesses()));
6
+ router.get("/processes/:processId", (req, res) => {
7
+ res.json(application.getProcess(requireString(req.params.processId, "processId")));
8
+ });
9
+ router.post("/processes/:processId/terminate", asyncRoute(async (req, res) => {
10
+ res.json(await application.processKill(requireString(req.params.processId, "processId"), false));
11
+ }));
12
+ router.post("/processes/:processId/kill", asyncRoute(async (req, res) => {
13
+ res.json(await application.processKill(requireString(req.params.processId, "processId"), true));
14
+ }));
15
+ return router;
16
+ }
@@ -0,0 +1,10 @@
1
+ import { Router } from "express";
2
+ import type { RuntimeEventBus } from "#app/event-bus";
3
+ import type { WebRuntime } from "#plugins/web/runtime";
4
+ interface RuntimeStatus {
5
+ version: string;
6
+ mcpRequests: number;
7
+ uptimeMinutes: number;
8
+ }
9
+ export declare function createRuntimeApiRouter(application: WebRuntime, eventBus: RuntimeEventBus, runtimeStatus: () => RuntimeStatus): Router;
10
+ export {};
@@ -0,0 +1,34 @@
1
+ import { Router } from "express";
2
+ import { ChatRoomError } from "#core/errors/chatroom-error";
3
+ import { bodyRecord, requireString } from "#presentation/http/http-utils";
4
+ export function createRuntimeApiRouter(application, eventBus, runtimeStatus) {
5
+ const router = Router();
6
+ router.get("/runtime", (_req, res) => res.json(runtimeStatus()));
7
+ router.get("/mcp/tools", (_req, res) => {
8
+ res.json(application.mcpTools.list());
9
+ });
10
+ router.patch("/mcp/tools/:toolName", (req, res) => {
11
+ const body = bodyRecord(req.body);
12
+ if (typeof body.enabled !== "boolean")
13
+ throw new ChatRoomError("INVALID_INPUT", "enabled must be a boolean");
14
+ res.json(application.mcpTools.setEnabled(requireString(req.params.toolName, "toolName"), body.enabled));
15
+ });
16
+ router.get("/events", (req, res) => {
17
+ res.status(200);
18
+ res.setHeader("Content-Type", "text/event-stream");
19
+ res.setHeader("Cache-Control", "no-cache, no-transform");
20
+ res.setHeader("Connection", "keep-alive");
21
+ res.flushHeaders();
22
+ res.write(`event: ready\ndata: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
23
+ const unsubscribe = eventBus.subscribe((event) => {
24
+ res.write(`event: runtime\ndata: ${JSON.stringify(event)}\n\n`);
25
+ });
26
+ const keepalive = setInterval(() => res.write(": keepalive\n\n"), 15000);
27
+ keepalive.unref();
28
+ req.on("close", () => {
29
+ clearInterval(keepalive);
30
+ unsubscribe();
31
+ });
32
+ });
33
+ return router;
34
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from "express";
2
+ import type { WebRuntime } from "#plugins/web/runtime";
3
+ export declare function createWorkspaceApiRouter(application: WebRuntime): Router;
@@ -0,0 +1,91 @@
1
+ import { Router } from "express";
2
+ import { ChatRoomError } from "#core/errors/chatroom-error";
3
+ import { asyncRoute, bodyRecord, requireString, } from "#presentation/http/http-utils";
4
+ const MAX_WRITE_BYTES = 1024 * 1024;
5
+ export function createWorkspaceApiRouter(application) {
6
+ const router = Router();
7
+ router.get("/workspaces", asyncRoute(async (_req, res) => {
8
+ res.json(await application.workspaces.list());
9
+ }));
10
+ router.post("/workspaces", asyncRoute(async (req, res) => {
11
+ const body = bodyRecord(req.body);
12
+ const parent = requireString(body.parent, "parent");
13
+ const name = requireString(body.name, "name");
14
+ res.json(await application.operations.run({
15
+ pluginId: "workspace",
16
+ source: "gui",
17
+ action: "create",
18
+ input: { parent, name },
19
+ }, () => application.workspaces.createProject(parent, name)));
20
+ }));
21
+ router.get("/workspace/roots", (_req, res) => {
22
+ res.json(application.workspaces.roots());
23
+ });
24
+ router.get("/workspace", asyncRoute(async (req, res) => {
25
+ res.json(await application.workspaces.info(requireString(req.query.root, "root")));
26
+ }));
27
+ router.get("/workspace/files", asyncRoute(async (req, res) => {
28
+ const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
29
+ const filePath = typeof req.query.path === "string" ? req.query.path : ".";
30
+ res.json(await fs.list(filePath, { recursive: req.query.recursive === "1" }));
31
+ }));
32
+ router.get("/workspace/file", asyncRoute(async (req, res) => {
33
+ const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
34
+ res.json(await fs.read(requireString(req.query.path, "path"), {
35
+ maxBytes: 2 * 1024 * 1024,
36
+ }));
37
+ }));
38
+ router.put("/workspace/file", asyncRoute(async (req, res) => {
39
+ const body = bodyRecord(req.body);
40
+ const fs = await application.workspaces.fs(requireString(body.root, "root"));
41
+ const filePath = requireString(body.path, "path");
42
+ if (typeof body.content !== "string")
43
+ throw new ChatRoomError("INVALID_INPUT", "content must be a string");
44
+ const bytes = Buffer.byteLength(body.content, "utf8");
45
+ if (bytes > MAX_WRITE_BYTES)
46
+ throw new ChatRoomError("INVALID_INPUT", `File content exceeds ${MAX_WRITE_BYTES} bytes`);
47
+ res.json(await application.operations.run({
48
+ pluginId: "workspace",
49
+ source: "gui",
50
+ action: "file.write",
51
+ input: { root: fs.root, path: filePath, bytes },
52
+ }, () => fs.write(filePath, body.content)));
53
+ }));
54
+ router.get("/workspace/file/image", asyncRoute(async (req, res) => {
55
+ const filePath = requireString(req.query.path, "path");
56
+ const mime = imageMime(filePath);
57
+ if (!mime)
58
+ throw new ChatRoomError("INVALID_INPUT", "File type is not supported for image preview");
59
+ const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
60
+ const file = await fs.readBytes(filePath, { maxBytes: 10 * 1024 * 1024 });
61
+ if (file.truncated)
62
+ throw new ChatRoomError("INVALID_INPUT", "Image is too large to preview");
63
+ res.setHeader("Content-Type", mime);
64
+ res.setHeader("Content-Length", String(file.data.byteLength));
65
+ res.setHeader("Cache-Control", "no-store");
66
+ res.setHeader("X-Content-Type-Options", "nosniff");
67
+ res.send(file.data);
68
+ }));
69
+ return router;
70
+ }
71
+ function imageMime(filePath) {
72
+ switch (filePath.split(".").pop()?.toLowerCase()) {
73
+ case "png":
74
+ return "image/png";
75
+ case "jpg":
76
+ case "jpeg":
77
+ return "image/jpeg";
78
+ case "gif":
79
+ return "image/gif";
80
+ case "webp":
81
+ return "image/webp";
82
+ case "avif":
83
+ return "image/avif";
84
+ case "bmp":
85
+ return "image/bmp";
86
+ case "ico":
87
+ return "image/x-icon";
88
+ default:
89
+ return null;
90
+ }
91
+ }
@@ -0,0 +1,8 @@
1
+ import type { InternalPlugin } from "#plugins/types";
2
+ import { WebRuntime } from "./runtime.js";
3
+ interface WebPluginService {
4
+ application: WebRuntime;
5
+ }
6
+ export declare const WebServiceToken: import("#plugins/types").ServiceToken<WebPluginService>;
7
+ export declare function createWebPlugin(): InternalPlugin;
8
+ export {};
@@ -0,0 +1,17 @@
1
+ import { createServiceToken } from "#plugins/types";
2
+ import { ComputerServiceToken } from "#plugins/computer/plugin";
3
+ import { GitServiceToken } from "#plugins/git/plugin";
4
+ import { ProcessService } from "#plugins/process/plugin";
5
+ import { WorkspaceServiceToken } from "#plugins/workspace/plugin";
6
+ import { WebRuntime } from "./runtime.js";
7
+ export const WebServiceToken = createServiceToken("web");
8
+ export function createWebPlugin() {
9
+ return {
10
+ id: "web",
11
+ activate(context) {
12
+ context.services.provide(WebServiceToken, {
13
+ application: new WebRuntime(context.services.require(WorkspaceServiceToken), context.services.require(GitServiceToken), context.operations, context.services.require(ProcessService), context.services.require(ComputerServiceToken), context.mcpTools),
14
+ });
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,18 @@
1
+ import type { OperationLog } from "#operations/operation-log";
2
+ import type { ComputerService } from "#plugins/computer/computer-service";
3
+ import type { GitService } from "#plugins/git/git-service";
4
+ import type { ProcessSupervisor } from "#plugins/process/process-supervisor";
5
+ import type { WorkspaceService } from "#plugins/workspace/workspace-service";
6
+ import type { McpToolControl } from "#mcp/server/tool-control";
7
+ export declare class WebRuntime {
8
+ readonly workspaces: WorkspaceService;
9
+ readonly git: GitService;
10
+ readonly operations: OperationLog;
11
+ readonly processes: ProcessSupervisor;
12
+ readonly computer: ComputerService;
13
+ readonly mcpTools: McpToolControl;
14
+ constructor(workspaces: WorkspaceService, git: GitService, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService, mcpTools: McpToolControl);
15
+ processKill(processId: string, force?: boolean): Promise<import("./api-types.js").ProcessSnapshot>;
16
+ listProcesses(): import("./api-types.js").ProcessSummary[];
17
+ getProcess(processId: string): import("./api-types.js").ProcessSnapshot;
18
+ }